Compare commits
5 Commits
82b7d060ba
...
2.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 8cbe28f915 | |||
| 5472e190d9 | |||
| 45be976de1 | |||
| 6dcb1e959b | |||
| bb2d179871 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "redlight",
|
"name": "redlight",
|
||||||
"version": "2.1.0",
|
"version": "2.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "redlight",
|
"name": "redlight",
|
||||||
"version": "2.1.0",
|
"version": "2.1.2",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.0",
|
"axios": "^1.7.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "redlight",
|
"name": "redlight",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.1.0",
|
"version": "2.1.2",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import { createNotification } from '../config/notifications.js';
|
|||||||
|
|
||||||
// M13: rate limit the unauthenticated federation receive endpoint
|
// M13: rate limit the unauthenticated federation receive endpoint
|
||||||
const federationReceiveLimiter = rateLimit({
|
const federationReceiveLimiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
max: 100,
|
max: 100,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
message: { error: 'Too many federation requests. Please try again later.' },
|
message: { error: 'Too many federation requests. Please try again later.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -40,7 +40,7 @@ export function wellKnownHandler(req, res) {
|
|||||||
federation_api: '/api/federation',
|
federation_api: '/api/federation',
|
||||||
public_key: getPublicKey(),
|
public_key: getPublicKey(),
|
||||||
software: 'Redlight',
|
software: 'Redlight',
|
||||||
version: '2.1.0',
|
version: '2.1.2',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,24 +236,24 @@ router.post('/receive', federationReceiveLimiter, async (req, res) => {
|
|||||||
|
|
||||||
// Send notification email (truly fire-and-forget - never blocks the response)
|
// Send notification email (truly fire-and-forget - never blocks the response)
|
||||||
if (targetUser.email) {
|
if (targetUser.email) {
|
||||||
const appUrl = getBaseUrl(req);
|
const appUrl = getBaseUrl(req);
|
||||||
const inboxUrl = `${appUrl}/federation/inbox`;
|
const inboxUrl = `${appUrl}/federation/inbox`;
|
||||||
const appName = process.env.APP_NAME || 'Redlight';
|
const appName = process.env.APP_NAME || 'Redlight';
|
||||||
sendFederationInviteEmail(
|
sendFederationInviteEmail(
|
||||||
targetUser.email, targetUser.name, from_user,
|
targetUser.email, targetUser.name, from_user,
|
||||||
room_name, message || null, inboxUrl, appName, targetUser.language || 'en'
|
room_name, message || null, inboxUrl, appName, targetUser.language || 'en'
|
||||||
).catch(mailErr => {
|
).catch(mailErr => {
|
||||||
log.federation.warn('Federation invite mail failed (non-fatal):', mailErr.message);
|
log.federation.warn('Federation invite mail failed (non-fatal):', mailErr.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-app notification
|
// In-app notification
|
||||||
await createNotification(
|
await createNotification(
|
||||||
targetUser.id,
|
targetUser.id,
|
||||||
'federation_invite_received',
|
'federation_invite_received',
|
||||||
from_user,
|
from_user,
|
||||||
room_name,
|
room_name,
|
||||||
'/federation/inbox',
|
'/federation/inbox',
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
|
|||||||
@@ -122,9 +122,9 @@ export default function RecordingList({ recordings, onRefresh }) {
|
|||||||
href={format.url}
|
href={format.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-lg bg-th-accent/10 text-th-accent text-xs font-medium hover:bg-th-accent/20 transition-colors"
|
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-th-accent/10 text-th-accent text-sm font-medium hover:bg-th-accent/20 transition-colors"
|
||||||
>
|
>
|
||||||
<Play size={12} />
|
<Play size={14} />
|
||||||
{format.type === 'presentation' ? t('recordings.presentation') : format.type}
|
{format.type === 'presentation' ? t('recordings.presentation') : format.type}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function GuestJoin() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [joining, setJoining] = useState(false);
|
const [joining, setJoining] = useState(false);
|
||||||
const [name, setName] = useState(user?.name || '');
|
const [name, setName] = useState(user?.display_name || user?.name || '');
|
||||||
const [accessCode, setAccessCode] = useState(searchParams.get('ac') || '');
|
const [accessCode, setAccessCode] = useState(searchParams.get('ac') || '');
|
||||||
const [moderatorCode, setModeratorCode] = useState('');
|
const [moderatorCode, setModeratorCode] = useState('');
|
||||||
const [status, setStatus] = useState({ running: false });
|
const [status, setStatus] = useState({ running: false });
|
||||||
@@ -89,7 +89,7 @@ export default function GuestJoin() {
|
|||||||
// Auto-join when meeting starts while waiting
|
// Auto-join when meeting starts while waiting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!prevRunningRef.current && status.running && waiting) {
|
if (!prevRunningRef.current && status.running && waiting) {
|
||||||
new Audio('/sounds/meeting-started.mp3').play().catch(() => {});
|
new Audio('/sounds/meeting-started.mp3').play().catch(() => { });
|
||||||
toast.success(t('room.guestMeetingStartedJoining'));
|
toast.success(t('room.guestMeetingStartedJoining'));
|
||||||
joinMeeting();
|
joinMeeting();
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ export default function GuestJoin() {
|
|||||||
toast.error(t('room.guestRecordingConsent'));
|
toast.error(t('room.guestRecordingConsent'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!status.running) {
|
if (!status.running && !roomInfo?.anyone_can_start) {
|
||||||
setWaiting(true);
|
setWaiting(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -210,97 +210,97 @@ export default function GuestJoin() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleJoin} className="space-y-4">
|
<form onSubmit={handleJoin} className="space-y-4">
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('room.guestYourName')} *</label>
|
|
||||||
<div className="relative">
|
|
||||||
<User size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={name}
|
|
||||||
onChange={e => !isLoggedIn && setName(e.target.value)}
|
|
||||||
readOnly={isLoggedIn}
|
|
||||||
className={`input-field pl-11 ${isLoggedIn ? 'opacity-70 cursor-not-allowed' : ''}`}
|
|
||||||
placeholder={t('room.guestNamePlaceholder')}
|
|
||||||
required
|
|
||||||
autoFocus={!isLoggedIn}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{roomInfo.has_access_code && (
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('room.guestAccessCode')}</label>
|
<label className="block text-sm font-medium text-th-text mb-1.5">{t('room.guestYourName')} *</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Lock size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
<User size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={accessCode}
|
value={name}
|
||||||
onChange={e => setAccessCode(e.target.value)}
|
onChange={e => !isLoggedIn && setName(e.target.value)}
|
||||||
className="input-field pl-11"
|
readOnly={isLoggedIn}
|
||||||
placeholder={t('room.guestAccessCodePlaceholder')}
|
className={`input-field pl-11 ${isLoggedIn ? 'opacity-70 cursor-not-allowed' : ''}`}
|
||||||
|
placeholder={t('room.guestNamePlaceholder')}
|
||||||
|
required
|
||||||
|
autoFocus={!isLoggedIn}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
{roomInfo.has_access_code && (
|
||||||
<label className="block text-sm font-medium text-th-text mb-1.5">
|
<div>
|
||||||
{t('room.guestModeratorCode')}
|
<label className="block text-sm font-medium text-th-text mb-1.5">{t('room.guestAccessCode')}</label>
|
||||||
<span className="text-th-text-s font-normal ml-1">{t('room.guestModeratorOptional')}</span>
|
<div className="relative">
|
||||||
</label>
|
<Lock size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
||||||
<div className="relative">
|
<input
|
||||||
<Shield size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
type="text"
|
||||||
<input
|
value={accessCode}
|
||||||
type="text"
|
onChange={e => setAccessCode(e.target.value)}
|
||||||
value={moderatorCode}
|
className="input-field pl-11"
|
||||||
onChange={e => setModeratorCode(e.target.value)}
|
placeholder={t('room.guestAccessCodePlaceholder')}
|
||||||
className="input-field pl-11"
|
/>
|
||||||
placeholder={t('room.guestModeratorPlaceholder')}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Recording consent notice */}
|
|
||||||
{roomInfo.allow_recording && (
|
|
||||||
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 space-y-3">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<AlertCircle size={16} className="text-amber-500 flex-shrink-0 mt-0.5" />
|
|
||||||
<p className="text-sm text-amber-400">{t('room.guestRecordingNotice')}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center gap-2.5 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={recordingConsent}
|
|
||||||
onChange={e => setRecordingConsent(e.target.checked)}
|
|
||||||
className="w-4 h-4 rounded accent-amber-500 cursor-pointer"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-th-text">{t('room.guestRecordingConsent')}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={joining || (roomInfo.allow_recording && !recordingConsent)}
|
|
||||||
className="btn-primary w-full py-3"
|
|
||||||
>
|
|
||||||
{joining ? (
|
|
||||||
<Loader2 size={18} className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{t('room.guestJoinButton')}
|
|
||||||
<ArrowRight size={18} />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
|
||||||
|
|
||||||
{!status.running && (
|
<div>
|
||||||
<p className="text-xs text-th-text-s text-center">
|
<label className="block text-sm font-medium text-th-text mb-1.5">
|
||||||
{t('room.guestWaitingMessage')}
|
{t('room.guestModeratorCode')}
|
||||||
</p>
|
<span className="text-th-text-s font-normal ml-1">{t('room.guestModeratorOptional')}</span>
|
||||||
)}
|
</label>
|
||||||
</form>
|
<div className="relative">
|
||||||
|
<Shield size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={moderatorCode}
|
||||||
|
onChange={e => setModeratorCode(e.target.value)}
|
||||||
|
className="input-field pl-11"
|
||||||
|
placeholder={t('room.guestModeratorPlaceholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recording consent notice */}
|
||||||
|
{roomInfo.allow_recording && (
|
||||||
|
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 space-y-3">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<AlertCircle size={16} className="text-amber-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-amber-400">{t('room.guestRecordingNotice')}</p>
|
||||||
|
</div>
|
||||||
|
<label className="flex items-center gap-2.5 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={recordingConsent}
|
||||||
|
onChange={e => setRecordingConsent(e.target.checked)}
|
||||||
|
className="w-4 h-4 rounded accent-amber-500 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-th-text">{t('room.guestRecordingConsent')}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={joining || (roomInfo.allow_recording && !recordingConsent)}
|
||||||
|
className="btn-primary w-full py-3"
|
||||||
|
>
|
||||||
|
{joining ? (
|
||||||
|
<Loader2 size={18} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{t('room.guestJoinButton')}
|
||||||
|
<ArrowRight size={18} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{!status.running && !roomInfo?.anyone_can_start && (
|
||||||
|
<p className="text-xs text-th-text-s text-center">
|
||||||
|
{t('room.guestWaitingMessage')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoggedIn && (
|
{!isLoggedIn && (
|
||||||
|
|||||||
Reference in New Issue
Block a user