Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions app/assets/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@
"default_tag_name": "Standard",
"server_tag_desired": "Erwünscht",
"server_tag_required": "Notwendig",
"are_you_sure_delete_room": "Diesen Raum wirklich löschen?"
"are_you_sure_delete_room": "Diesen Raum wirklich löschen?",
"generate_recordings_access_code": "Zugangscode für Aufzeichnungen generieren"
}
},
"recording": {
Expand All @@ -194,7 +195,12 @@
"public_recordings_list_empty_description": "Hier werden Aufzeichnungen angezeigt,sobald sie verfügbar sind.",
"delete_recording": "Aufzeichnung löschen",
"are_you_sure_delete_recording": "Diese Aufzeichnung wirklich löschen?",
"search_not_found": "Keine Aufzeichnungen gefunden"
"search_not_found": "Keine Aufzeichnungen gefunden",
"access_code_required": "Zugangscode erforderlich",
"enter_access_code_description": "Bitte gebe den Zugangscode ein, um die öffentlichen Aufzeichnungen für diesen Raum anzusehen.",
"access_code_placeholder": "Zugangscode eingeben",
"submit_access_code": "Zugangscode senden",
"invalid_access_code": "Falscher Zugangscode"
},
"admin": {
"admin_panel": "Administration",
Expand Down
10 changes: 8 additions & 2 deletions app/assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@
"default_tag_name": "Default",
"server_tag_desired": "Desired",
"server_tag_required": "Required",
"are_you_sure_delete_room": "Are you sure you want to delete this room?"
"are_you_sure_delete_room": "Are you sure you want to delete this room?",
"generate_recordings_access_code": "Generate access code for recordings"
}
},
"recording": {
Expand Down Expand Up @@ -203,7 +204,12 @@
"public_recordings_list_empty_description": "Recordings will appear here when available.",
"delete_recording": "Delete Recording",
"are_you_sure_delete_recording": "Are you sure you want to delete this recording?",
"search_not_found": "No Recordings Found"
"search_not_found": "No Recordings Found",
"access_code_required": "Access Code Required",
"enter_access_code_description": "Please enter the access code to view the public recordings for this room.",
"access_code_placeholder": "Enter access code",
"submit_access_code": "Submit Access Code",
"invalid_access_code": "Invalid access code"
},
"admin": {
"admin_panel": "Administrator Panel",
Expand Down
17 changes: 14 additions & 3 deletions app/controllers/api/v1/room_settings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,27 @@ def update

return render_error status: :bad_request unless config_value

is_access_code = %w[glViewerAccessCode glModeratorAccessCode].include? name
is_access_code = %w[glViewerAccessCode glModeratorAccessCode glRecordingsAccessCode].include? name

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

value = infer_access_code(value:) if is_access_code # Handling access code update.

# Handling access code update
value = infer_access_code(value:) if is_access_code

option = @room.get_setting(name:)

# If the option doesn't exist, we create it for access codes
if option.nil? && is_access_code
option = RoomMeetingOption.create!(
room_id: @room.id,
meeting_option_id: MeetingOption.find_by(name: name).id,
value: value
)
return render_data status: :ok
end
Comment on lines +61 to +68
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its better to do this in a data migration


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

Expand Down
13 changes: 11 additions & 2 deletions app/controllers/api/v1/rooms_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,20 @@ def recordings
# GET /api/v1/rooms/:friendly_id/public_recordings.json
# Returns all of a specific room's PUBLIC recordings
def public_recordings
sort_config = config_sorting(allowed_columns: %w[name length])
# Get the recordings access code from the database if it exists
recordings_access_code = RoomMeetingOption.joins(:meeting_option)
.where(room_id: @room.id, meeting_options: { name: 'glRecordingsAccessCode' })
.first&.value
Comment on lines +144 to +146
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to get the value using the RoomSettingsGetter (similar to this)

    viewer_code = RoomSettingsGetter.new(
      room_id: @room.id,
      provider: @room.user.provider,
      current_user: @current_user,
      show_codes: true,
      settings: 'glViewerAccessCode'
    ).call


# If a recordings access code exists and the provided code doesn't match, require access code
if recordings_access_code.present? && params[:access_code] != recordings_access_code
return render_data data: [], meta: { requires_access_code: true }, status: :ok
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather we return something like this instead
return render_error status: :forbidden

end

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

render_data data: recordings, meta: pagy_metadata(pagy), serializer: PublicRecordingSerializer, status: :ok
render_data data: recordings, meta: pagy_metadata(pagy).merge(requires_access_code: false), serializer: PublicRecordingSerializer, status: :ok
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure what this is doing, but it shouldn't be doing it

end

# GET /api/v1/rooms/:friendly_id/recordings_processing.json
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/components/rooms/room/join/JoinCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export default function JoinCard() {
<h1 className="mt-2">
{publicRoom?.data.name}
</h1>
{ (recordValue !== 'false') && recordings?.data?.length > 0 && (
{ (recordValue !== 'false') && (recordings?.data?.length > 0 || recordings?.meta?.requires_access_code === true) && (
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh - I would look at how the viewer access code works and implement something similar

<ButtonLink
variant="brand-outline"
className="mt-3 mb-0 cursor-pointer"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
//
// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
//
// This program is free software; you can redistribute it and/or modify it under the
// terms of the GNU Lesser General Public License as published by the Free Software
// Foundation; either version 3.0 of the License, or (at your option) any later
// version.
//
// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along
// with Greenlight; if not, see <http://www.gnu.org/licenses/>.

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Form, Button, Stack } from 'react-bootstrap';
import ButtonLink from '../../../shared_components/utilities/ButtonLink';
import UserBoardIcon from '../../UserBoardIcon';

export default function AccessCodeForm({ onAccessCodeSubmit, error, friendlyId }) {
const { t } = useTranslation();
const [accessCode, setAccessCode] = useState('');
const [validated] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [emptyCodeError, setEmptyCodeError] = useState(false);

const handleSubmit = (e) => {
e.preventDefault();

// Reset error states
setEmptyCodeError(false);

// Check for empty access code first
if (!accessCode?.trim()) {
setEmptyCodeError(true);
return;
}

setIsSubmitting(true);

// Call the submit handler and reset submission state when complete
Promise.resolve(onAccessCodeSubmit(accessCode))
.finally(() => {
setIsSubmitting(false);
});
};

return (
<div className="text-center p-4">
<h2>{t('recording.access_code_required')}</h2>
<p className="mb-4">{t('recording.enter_access_code_description')}</p>

<Form noValidate validated={validated} onSubmit={handleSubmit}>
<Form.Group className="mb-3 d-flex justify-content-center">
<div className="position-relative w-50">
<Form.Control
type="text"
placeholder={t('recording.access_code_placeholder')}
value={accessCode}
onChange={(e) => {
setAccessCode(e.target.value);
if (e.target.value.trim()) {
setEmptyCodeError(false);
}
}}
required
autoFocus
isInvalid={error || emptyCodeError}
className={`${(error || emptyCodeError) ? 'border-danger' : ''} text-center`}
/>
<Form.Control.Feedback type="invalid" className="text-center">
{emptyCodeError && t('room.settings.access_code_required')}
{error && !emptyCodeError && t('recording.invalid_access_code')}
</Form.Control.Feedback>
</div>
</Form.Group>

<Stack direction="horizontal" gap={2} className="justify-content-center">
<ButtonLink
variant="brand-outline"
className="my-0 py-2"
to={`/rooms/${friendlyId}/join`}
>
<span><UserBoardIcon className="hi-s text-brand cursor-pointer" /> {t('join_session')} </span>
</ButtonLink>
<Button
variant="brand"
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? t('common.submitting') ?? 'Submitting...' : t('recording.submit_access_code')}
</Button>
</Stack>
</Form>
</div>
);
}

AccessCodeForm.propTypes = {
onAccessCodeSubmit: PropTypes.func.isRequired,
error: PropTypes.bool,
friendlyId: PropTypes.string.isRequired,
};

AccessCodeForm.defaultProps = {
error: false,
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,56 @@
// You should have received a copy of the GNU Lesser General Public License along
// with Greenlight; if not, see <http://www.gnu.org/licenses/>.

/* eslint-disable consistent-return */
import React from 'react';
import React, { useState, useEffect } from 'react';
import { Row } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom';
import Logo from '../../../shared_components/Logo';
import PublicRecordingsCard from './PublicRecordingsCard';

export default function RoomJoin() {
export default function PublicRecordings() {
const [searchParams, setSearchParams] = useSearchParams();
const [accessCode, setAccessCode] = useState('');
const [accessCodeError, setAccessCodeError] = useState(false);

useEffect(() => {
// Check if there's an access code in the URL
const code = searchParams.get('access_code');
if (code) {
setAccessCode(code);
}
}, [searchParams]);

const handleAccessCodeSubmit = (code) => {
// Update state and reset any previous errors
setAccessCode(code);
setAccessCodeError(false);

// Persist access code in URL for bookmarking and page refreshes
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.set('access_code', code);
setSearchParams(newSearchParams);

// Explicitly return a Promise for await in PublicRecordingsCard
return Promise.resolve();
};

const handleAccessCodeError = () => {
setAccessCodeError(true);
// Error message remains until the user enters a correct code
};

return (
<div className="vertical-center">
<Row className="text-center pb-4">
<Logo />
</Row>
<Row>
<PublicRecordingsCard />
<PublicRecordingsCard
accessCode={accessCode}
onAccessCodeError={handleAccessCodeError}
onAccessCodeSubmit={handleAccessCodeSubmit}
accessCodeError={accessCodeError}
/>
</Row>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,87 @@
// with Greenlight; if not, see <http://www.gnu.org/licenses/>.

/* eslint-disable consistent-return */
import React from 'react';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import Card from 'react-bootstrap/Card';
import { useParams } from 'react-router-dom';
import PublicRecordingsList from './PublicRecordingsList';
import AccessCodeForm from './AccessCodeForm';

export default function PublicRecordingsCard() {
export default function PublicRecordingsCard({
accessCode,
onAccessCodeSubmit,
onAccessCodeError,
accessCodeError
}) {
const { friendlyId } = useParams();
const [requiresAccessCode, setRequiresAccessCode] = useState(false);
const [isValidating, setIsValidating] = useState(!!accessCode);

const handleAccessCodeSubmit = async (code) => {
setIsValidating(true);

try {
// Wait until the access code is submitted
await onAccessCodeSubmit(code);

// Check the access code validity immediately after the API response
// This is more reliable than using a timeout
const checkAccessCodeValidity = () => {
if (requiresAccessCode) {
// If we still need the access code, the entered code was incorrect
onAccessCodeError();
}
};

// Use requestAnimationFrame to ensure we check after the state has been updated
requestAnimationFrame(checkAccessCodeValidity);
} catch (error) {
// Keep only essential error logging
console.error('Error submitting access code:', error);
onAccessCodeError();
}
};

// Reset requiresAccessCode when accessCode changes
useEffect(() => {
if (accessCode) {
setRequiresAccessCode(false);
setIsValidating(false); // Reset validation state when access code is provided
}
}, [accessCode]);

return (
<Card className="mx-auto p-0 border-0 card-shadow">
<Card.Body className="pt-4 px-5">
<PublicRecordingsList friendlyId={friendlyId} />
<Card.Header className="bg-white border-0">
</Card.Header>
<Card.Body>
{requiresAccessCode || (isValidating && !accessCode) ? (
<AccessCodeForm
onAccessCodeSubmit={handleAccessCodeSubmit}
error={accessCodeError}
friendlyId={friendlyId}
/>
) : (
<PublicRecordingsList
friendlyId={friendlyId}
accessCode={accessCode}
onRequiresAccessCode={() => setRequiresAccessCode(true)}
/>
)}
</Card.Body>
</Card>
);
}

PublicRecordingsCard.propTypes = {
accessCode: PropTypes.string,
onAccessCodeSubmit: PropTypes.func.isRequired,
onAccessCodeError: PropTypes.func.isRequired,
accessCodeError: PropTypes.bool,
};

PublicRecordingsCard.defaultProps = {
accessCode: '',
accessCodeError: false,
};
Loading
Loading