feat: Introduce guest access for video viewing, implement user notification settings via email and Telegram, and add rate limiting infrastructure.

This commit is contained in:
Yusuf İpek
2026-02-07 13:56:26 +03:00
parent 296c5257a7
commit 88dcf9514c
17 changed files with 1566 additions and 39 deletions
@@ -101,7 +101,11 @@ export default function ProjectMembersPage() {
return;
}
setSuccess(`Invited ${data.user.name || data.user.email} as ${inviteRole.toLowerCase()}`);
if (data.user) {
setSuccess(`Invited ${data.user.name || data.user.email || inviteEmail} as ${inviteRole.toLowerCase()}`);
} else {
setSuccess(data.message || `Invitation sent to ${inviteEmail}`);
}
setInviteEmail('');
fetchMembers();
} catch {
@@ -16,6 +16,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { VideoCard } from '@/components/video-card';
import { GuestGate } from '@/components/guest-gate';
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
@@ -135,7 +136,55 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
});
const canEdit = isOwner || project.members[0]?.role === 'ADMIN' || workspaceRole === 'OWNER' || workspaceRole === 'ADMIN';
const isAuthenticated = !!session?.user?.id;
// Guest name gate for unauthenticated users on public projects
if (!isAuthenticated && isPublic) {
return (
<GuestGate>
<ProjectContent
project={project}
projectId={projectId}
videos={videos}
canEdit={false}
isOwner={false}
isPublic={isPublic}
workspaceRole={null}
/>
</GuestGate>
);
}
return (
<ProjectContent
project={project}
projectId={projectId}
videos={videos}
canEdit={canEdit}
isOwner={isOwner}
isPublic={isPublic}
workspaceRole={workspaceRole}
/>
);
}
function ProjectContent({
project,
projectId,
videos,
canEdit,
isOwner,
isPublic,
workspaceRole,
}: {
project: { name: string; description: string | null; visibility: string; workspace: { id: string; name: string } | null; members: { role: string }[] };
projectId: string;
videos: { id: string; title: string; thumbnailUrl: string; currentVersion: number; commentCount: number; duration: string; lastUpdated: string }[];
canEdit: boolean;
isOwner: boolean;
isPublic: boolean;
workspaceRole: string | null;
}) {
return (
<div className="px-6 lg:px-8 py-8 w-full">
{/* Back link */}
@@ -101,6 +101,7 @@ interface VideoData {
members: { role: string }[];
};
versions: (Version & { comments: Comment[] })[];
isAuthenticated: boolean;
}
function formatTime(seconds: number): string {
@@ -167,6 +168,14 @@ export default function VideoPage() {
const [isSubmittingEdit, setIsSubmittingEdit] = useState(false);
const [deletingCommentId, setDeletingCommentId] = useState<string | null>(null);
// Guest name (for unauthenticated users on public projects)
const [guestName, setGuestName] = useState('');
useEffect(() => {
const saved = localStorage.getItem('openframe_guest_name');
if (saved) setGuestName(saved);
}, []);
const isGuest = video ? !video.isAuthenticated : false;
// New version dialog
const [showVersionDialog, setShowVersionDialog] = useState(false);
const [newVersionUrl, setNewVersionUrl] = useState('');
@@ -371,6 +380,7 @@ export default function VideoPage() {
content: voiceData ? commentText.trim() || null : commentText,
timestamp: selectedTimestamp ?? currentTime,
...(voiceData && { voiceUrl: voiceData.url, voiceDuration: voiceData.duration }),
...(isGuest && guestName && { guestName }),
}),
});
@@ -395,7 +405,7 @@ export default function VideoPage() {
} finally {
setIsSubmittingComment(false);
}
}, [commentText, currentTime, selectedTimestamp, activeVersion, activeVersionId]);
}, [commentText, currentTime, selectedTimestamp, activeVersion, activeVersionId, isGuest, guestName]);
// Voice recording handlers
const startRecording = useCallback(async () => {
@@ -631,6 +641,7 @@ export default function VideoPage() {
timestamp: comments.find((c) => c.id === parentId)?.timestamp ?? currentTime,
parentId,
...(voiceData && { voiceUrl: voiceData.url, voiceDuration: voiceData.duration }),
...(isGuest && guestName && { guestName }),
}),
});
if (res.ok) {