Skip to content

Commit fd91e62

Browse files
author
Sebastian Clemens
committed
Add access code for public recordings
1 parent d3ad3e3 commit fd91e62

File tree

12 files changed

+392
-26
lines changed

12 files changed

+392
-26
lines changed

app/assets/locales/de.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@
168168
"default_tag_name": "Standard",
169169
"server_tag_desired": "Erwünscht",
170170
"server_tag_required": "Notwendig",
171-
"are_you_sure_delete_room": "Diesen Raum wirklich löschen?"
171+
"are_you_sure_delete_room": "Diesen Raum wirklich löschen?",
172+
"generate_recordings_access_code": "Zugangscode für Aufzeichnungen generieren"
172173
}
173174
},
174175
"recording": {
@@ -194,7 +195,12 @@
194195
"public_recordings_list_empty_description": "Hier werden Aufzeichnungen angezeigt,sobald sie verfügbar sind.",
195196
"delete_recording": "Aufzeichnung löschen",
196197
"are_you_sure_delete_recording": "Diese Aufzeichnung wirklich löschen?",
197-
"search_not_found": "Keine Aufzeichnungen gefunden"
198+
"search_not_found": "Keine Aufzeichnungen gefunden",
199+
"access_code_required": "Zugangscode erforderlich",
200+
"enter_access_code_description": "Bitte gebe den Zugangscode ein, um die öffentlichen Aufzeichnungen für diesen Raum anzusehen.",
201+
"access_code_placeholder": "Zugangscode eingeben",
202+
"submit_access_code": "Zugangscode senden",
203+
"invalid_access_code": "Falscher Zugangscode"
198204
},
199205
"admin": {
200206
"admin_panel": "Administration",

app/assets/locales/en.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,12 @@
164164
"wrong_access_code": "Wrong Access Code",
165165
"generate_viewers_access_code": "Generate access code for viewers",
166166
"generate_mods_access_code": "Generate access code for moderators",
167-
"server_tag": "Select a server type for this room",
167+
"server_tag": "Server Tag",
168168
"default_tag_name": "Default",
169169
"server_tag_desired": "Desired",
170170
"server_tag_required": "Required",
171-
"are_you_sure_delete_room": "Are you sure you want to delete this room?"
171+
"are_you_sure_delete_room": "Are you sure you want to delete this room?",
172+
"generate_recordings_access_code": "Generate Recordings Access Code"
172173
}
173174
},
174175
"recording": {
@@ -194,7 +195,12 @@
194195
"public_recordings_list_empty_description": "Recordings will appear here when available.",
195196
"delete_recording": "Delete Recording",
196197
"are_you_sure_delete_recording": "Are you sure you want to delete this recording?",
197-
"search_not_found": "No Recordings Found"
198+
"search_not_found": "No Recordings Found",
199+
"access_code_required": "Access Code Required",
200+
"enter_access_code_description": "Please enter the access code to view the public recordings for this room.",
201+
"access_code_placeholder": "Enter access code",
202+
"submit_access_code": "Submit Access Code",
203+
"invalid_access_code": "Invalid access code. Please try again."
198204
},
199205
"admin": {
200206
"admin_panel": "Administrator Panel",

app/controllers/api/v1/room_settings_controller.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,27 @@ def update
4545

4646
return render_error status: :bad_request unless config_value
4747

48-
is_access_code = %w[glViewerAccessCode glModeratorAccessCode].include? name
48+
is_access_code = %w[glViewerAccessCode glModeratorAccessCode glRecordingsAccessCode].include? name
4949

5050
# Only allow the settings to update if the room config is default or optional / if it is an access_code regeneration
5151
unless %w[optional default_enabled].include?(config_value) || (config_value == 'true' && is_access_code && value != 'false')
5252
return render_error status: :forbidden
5353
end
5454

55-
value = infer_access_code(value:) if is_access_code # Handling access code update.
56-
55+
# Handling access code update
56+
value = infer_access_code(value:) if is_access_code
57+
5758
option = @room.get_setting(name:)
59+
60+
# If the option doesn't exist, we create it for access codes
61+
if option.nil? && is_access_code
62+
option = RoomMeetingOption.create!(
63+
room_id: @room.id,
64+
meeting_option_id: MeetingOption.find_by(name: name).id,
65+
value: value
66+
)
67+
return render_data status: :ok
68+
end
5869

5970
return render_error status: :bad_request unless option&.update(value:)
6071

app/controllers/api/v1/rooms_controller.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,20 @@ def recordings
140140
# GET /api/v1/rooms/:friendly_id/public_recordings.json
141141
# Returns all of a specific room's PUBLIC recordings
142142
def public_recordings
143-
sort_config = config_sorting(allowed_columns: %w[name length])
143+
# Get the recordings access code from the database if it exists
144+
recordings_access_code = RoomMeetingOption.joins(:meeting_option)
145+
.where(room_id: @room.id, meeting_options: { name: 'glRecordingsAccessCode' })
146+
.first&.value
147+
148+
# If a recordings access code exists and the provided code doesn't match, require access code
149+
if recordings_access_code.present? && params[:access_code] != recordings_access_code
150+
return render_data data: [], meta: { requires_access_code: true }, status: :ok
151+
end
144152

153+
sort_config = config_sorting(allowed_columns: %w[name length])
145154
pagy, recordings = pagy(@room.public_recordings.order(sort_config, recorded_at: :desc).public_search(params[:search]))
146155

147-
render_data data: recordings, meta: pagy_metadata(pagy), serializer: PublicRecordingSerializer, status: :ok
156+
render_data data: recordings, meta: pagy_metadata(pagy).merge(requires_access_code: false), serializer: PublicRecordingSerializer, status: :ok
148157
end
149158

150159
# GET /api/v1/rooms/:friendly_id/recordings_processing.json

app/javascript/components/rooms/room/join/JoinCard.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export default function JoinCard() {
226226
<h1 className="mt-2">
227227
{publicRoom?.data.name}
228228
</h1>
229-
{ (recordValue !== 'false') && recordings?.data?.length > 0 && (
229+
{ (recordValue !== 'false') && (recordings?.data?.length > 0 || recordings?.meta?.requires_access_code === true) && (
230230
<ButtonLink
231231
variant="brand-outline"
232232
className="mt-3 mb-0 cursor-pointer"
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
2+
//
3+
// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
4+
//
5+
// This program is free software; you can redistribute it and/or modify it under the
6+
// terms of the GNU Lesser General Public License as published by the Free Software
7+
// Foundation; either version 3.0 of the License, or (at your option) any later
8+
// version.
9+
//
10+
// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
11+
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
12+
// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License along
15+
// with Greenlight; if not, see <http://www.gnu.org/licenses/>.
16+
17+
import React, { useState } from 'react';
18+
import PropTypes from 'prop-types';
19+
import { useTranslation } from 'react-i18next';
20+
import { Form, Button, Stack } from 'react-bootstrap';
21+
import ButtonLink from '../../../shared_components/utilities/ButtonLink';
22+
import UserBoardIcon from '../../UserBoardIcon';
23+
24+
export default function AccessCodeForm({ onAccessCodeSubmit, error, friendlyId }) {
25+
const { t } = useTranslation();
26+
const [accessCode, setAccessCode] = useState('');
27+
const [validated, setValidated] = useState(false);
28+
const [isSubmitting, setIsSubmitting] = useState(false);
29+
const [emptyCodeError, setEmptyCodeError] = useState(false);
30+
31+
const handleSubmit = (e) => {
32+
e.preventDefault();
33+
34+
// Reset error states
35+
setEmptyCodeError(false);
36+
37+
// Check for empty access code first
38+
if (!accessCode || !accessCode.trim()) {
39+
setEmptyCodeError(true);
40+
return;
41+
}
42+
43+
setIsSubmitting(true);
44+
45+
// Call the submit handler and reset submission state when complete
46+
Promise.resolve(onAccessCodeSubmit(accessCode))
47+
.finally(() => {
48+
setIsSubmitting(false);
49+
});
50+
};
51+
52+
return (
53+
<div className="text-center p-4">
54+
<h2>{t('recording.access_code_required')}</h2>
55+
<p className="mb-4">{t('recording.enter_access_code_description')}</p>
56+
57+
<Form noValidate validated={validated} onSubmit={handleSubmit}>
58+
<Form.Group className="mb-3 d-flex justify-content-center">
59+
<div className="position-relative w-50">
60+
<Form.Control
61+
type="text"
62+
placeholder={t('recording.access_code_placeholder')}
63+
value={accessCode}
64+
onChange={(e) => {
65+
setAccessCode(e.target.value);
66+
if (e.target.value.trim()) {
67+
setEmptyCodeError(false);
68+
}
69+
}}
70+
required
71+
autoFocus
72+
isInvalid={error || emptyCodeError}
73+
className={`${(error || emptyCodeError) ? 'border-danger' : ''} text-center`}
74+
/>
75+
<Form.Control.Feedback type="invalid" className="text-center">
76+
{emptyCodeError && t('room.settings.access_code_required')}
77+
{error && !emptyCodeError && t('recording.invalid_access_code')}
78+
</Form.Control.Feedback>
79+
</div>
80+
</Form.Group>
81+
82+
<Stack direction="horizontal" gap={2} className="justify-content-center">
83+
<ButtonLink
84+
variant="brand-outline"
85+
className="my-0 py-2"
86+
to={`/rooms/${friendlyId}/join`}
87+
>
88+
<span><UserBoardIcon className="hi-s text-brand cursor-pointer" /> {t('join_session')} </span>
89+
</ButtonLink>
90+
<Button
91+
variant="brand"
92+
type="submit"
93+
disabled={isSubmitting}
94+
>
95+
{isSubmitting ? t('common.submitting') || 'Submitting...' : t('recording.submit_access_code')}
96+
</Button>
97+
</Stack>
98+
</Form>
99+
</div>
100+
);
101+
}
102+
103+
AccessCodeForm.propTypes = {
104+
onAccessCodeSubmit: PropTypes.func.isRequired,
105+
error: PropTypes.bool,
106+
friendlyId: PropTypes.string.isRequired,
107+
};
108+
109+
AccessCodeForm.defaultProps = {
110+
error: false,
111+
};

app/javascript/components/rooms/room/public_recordings/PublicRecordings.jsx

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,56 @@
1414
// You should have received a copy of the GNU Lesser General Public License along
1515
// with Greenlight; if not, see <http://www.gnu.org/licenses/>.
1616

17-
/* eslint-disable consistent-return */
18-
import React from 'react';
17+
import React, { useState, useEffect } from 'react';
1918
import { Row } from 'react-bootstrap';
19+
import { useSearchParams } from 'react-router-dom';
2020
import Logo from '../../../shared_components/Logo';
2121
import PublicRecordingsCard from './PublicRecordingsCard';
2222

23-
export default function RoomJoin() {
23+
export default function PublicRecordings() {
24+
const [searchParams, setSearchParams] = useSearchParams();
25+
const [accessCode, setAccessCode] = useState('');
26+
const [accessCodeError, setAccessCodeError] = useState(false);
27+
28+
useEffect(() => {
29+
// Check if there's an access code in the URL
30+
const code = searchParams.get('access_code');
31+
if (code) {
32+
setAccessCode(code);
33+
}
34+
}, [searchParams]);
35+
36+
const handleAccessCodeSubmit = (code) => {
37+
// Update state and reset any previous errors
38+
setAccessCode(code);
39+
setAccessCodeError(false);
40+
41+
// Persist access code in URL for bookmarking and page refreshes
42+
const newSearchParams = new URLSearchParams(searchParams);
43+
newSearchParams.set('access_code', code);
44+
setSearchParams(newSearchParams);
45+
46+
// Explicitly return a Promise for await in PublicRecordingsCard
47+
return Promise.resolve();
48+
};
49+
50+
const handleAccessCodeError = () => {
51+
setAccessCodeError(true);
52+
// Error message remains until the user enters a correct code
53+
};
54+
2455
return (
2556
<div className="vertical-center">
2657
<Row className="text-center pb-4">
2758
<Logo />
2859
</Row>
2960
<Row>
30-
<PublicRecordingsCard />
61+
<PublicRecordingsCard
62+
accessCode={accessCode}
63+
onAccessCodeError={handleAccessCodeError}
64+
onAccessCodeSubmit={handleAccessCodeSubmit}
65+
accessCodeError={accessCodeError}
66+
/>
3167
</Row>
3268
</div>
3369
);

app/javascript/components/rooms/room/public_recordings/PublicRecordingsCard.jsx

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,92 @@
1515
// with Greenlight; if not, see <http://www.gnu.org/licenses/>.
1616

1717
/* eslint-disable consistent-return */
18-
import React from 'react';
18+
import React, { useState, useEffect } from 'react';
19+
import PropTypes from 'prop-types';
1920
import Card from 'react-bootstrap/Card';
2021
import { useParams } from 'react-router-dom';
2122
import PublicRecordingsList from './PublicRecordingsList';
23+
import AccessCodeForm from './AccessCodeForm';
2224

23-
export default function PublicRecordingsCard() {
25+
export default function PublicRecordingsCard({
26+
accessCode,
27+
onAccessCodeSubmit,
28+
onAccessCodeError,
29+
accessCodeError
30+
}) {
2431
const { friendlyId } = useParams();
32+
const [requiresAccessCode, setRequiresAccessCode] = useState(false);
33+
const [isValidating, setIsValidating] = useState(!!accessCode);
34+
35+
const handleAccessCodeSubmit = async (code) => {
36+
setIsValidating(true);
37+
38+
try {
39+
// Wait until the access code is submitted
40+
await onAccessCodeSubmit(code);
41+
42+
// Check the access code validity immediately after the API response
43+
// This is more reliable than using a timeout
44+
const checkAccessCodeValidity = () => {
45+
if (requiresAccessCode) {
46+
// If we still need the access code, the entered code was incorrect
47+
onAccessCodeError();
48+
}
49+
};
50+
51+
// Use requestAnimationFrame to ensure we check after the state has been updated
52+
requestAnimationFrame(checkAccessCodeValidity);
53+
} catch (error) {
54+
// Keep only essential error logging
55+
console.error('Error submitting access code:', error);
56+
onAccessCodeError();
57+
}
58+
};
59+
60+
const handleAccessCodeError = () => {
61+
setRequiresAccessCode(true);
62+
onAccessCodeError();
63+
};
64+
65+
// Reset requiresAccessCode when accessCode changes
66+
useEffect(() => {
67+
if (accessCode) {
68+
setRequiresAccessCode(false);
69+
setIsValidating(false); // Reset validation state when access code is provided
70+
}
71+
}, [accessCode]);
2572

2673
return (
2774
<Card className="mx-auto p-0 border-0 card-shadow">
28-
<Card.Body className="pt-4 px-5">
29-
<PublicRecordingsList friendlyId={friendlyId} />
75+
<Card.Header className="bg-white border-0">
76+
</Card.Header>
77+
<Card.Body>
78+
{requiresAccessCode || (isValidating && !accessCode) ? (
79+
<AccessCodeForm
80+
onAccessCodeSubmit={handleAccessCodeSubmit}
81+
error={accessCodeError}
82+
friendlyId={friendlyId}
83+
/>
84+
) : (
85+
<PublicRecordingsList
86+
friendlyId={friendlyId}
87+
accessCode={accessCode}
88+
onRequiresAccessCode={() => setRequiresAccessCode(true)}
89+
/>
90+
)}
3091
</Card.Body>
3192
</Card>
3293
);
3394
}
95+
96+
PublicRecordingsCard.propTypes = {
97+
accessCode: PropTypes.string,
98+
onAccessCodeSubmit: PropTypes.func.isRequired,
99+
onAccessCodeError: PropTypes.func.isRequired,
100+
accessCodeError: PropTypes.bool,
101+
};
102+
103+
PublicRecordingsCard.defaultProps = {
104+
accessCode: '',
105+
accessCodeError: false,
106+
};

0 commit comments

Comments
 (0)