Skip to content

Commit 18fa825

Browse files
committed
Guest photo album access behind a per-project toggle
- New guestShowPhotos project setting (default off) with manual migration: guests can view photo albums when enabled - Guests can never download photos: album access tokens now carry a guest flag enforced by the photo content route, closing a download=true URL bypass; zip downloads remain blocked for guests - Video group rename/delete buttons only show on the expanded card, matching photo albums - Wiki: guest photo visibility, accordion album management, video drag & drop, inline previews, group deletion, page-level view toggle
1 parent e06a0f9 commit 18fa825

19 files changed

Lines changed: 66 additions & 23 deletions

File tree

docs/wiki/Admin-Settings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ Subscribe to project deadlines from any calendar app (Google Calendar, Apple Cal
176176
- Custom URL slug.
177177
- Guest mode: view-only access without credentials.
178178
- Guest latest only: restrict guests to the latest version of each video (default: true).
179+
- Show photo albums to guests: guests can view photo albums; downloads stay disabled for guests (default: false).
179180

180181
**Workflow**
181182
- Revision limit (enable/disable with max revisions count).

docs/wiki/Client-Guide.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ After authentication you see a thumbnail grid of all videos in the project:
3636
- Approved videos show a checkmark overlay.
3737
- Videos needing review appear first, followed by approved videos.
3838
- Click any thumbnail to open the video player.
39-
- A grid/list toggle switches between thumbnail cards and a compact list.
39+
- A grid/list toggle in the top bar switches both videos and photo albums between thumbnail cards and a compact list; your choice is remembered.
4040

4141
Above the grid you can see the project title, your name, and an optional project description.
4242

@@ -175,12 +175,12 @@ Files you submit are visible to the project admin but are not attached to any sp
175175

176176
## Photo albums
177177

178-
If the project contains photos, albums appear below the videos. Click an album to open the full-page album view:
178+
If the project contains photos, albums appear below the videos (for guests only when the project admin has enabled it). Click an album to open the full-page album view:
179179

180180
- The top bar has a back button, a prev/next album selector, and the theme/language toggles.
181181
- Click any photo to open it in a lightbox; use the arrow keys or on-screen arrows to browse.
182182
- Select multiple photos (checkbox on each thumbnail) to download just those.
183-
- If the admin has enabled photo downloads, you can download your selection, the whole album, or all project photos as a ZIP.
183+
- If the admin has enabled photo downloads, you can download your selection, the whole album, or all project photos as a ZIP. Photo downloads are never available in guest mode.
184184

185185
## Downloading
186186

docs/wiki/Features.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
- Photo zip downloads on the share page (selected photos, one album, or the whole project), gated by a per-project toggle.
1818
- Recipient portal at `/portal`: recipients sign in with a magic link and see all active projects they are assigned to in one place.
1919
- Loop playback toggle in the video player.
20-
- Grid/list view toggle for videos on the share page.
20+
- Grid/list view toggle on the share page, applying to both videos and photo albums.
2121
- Client asset uploads: allow clients to upload files to projects (configurable per project).
2222
- Reverse share: allow clients to upload files directly to a project without attaching them to a comment. Toggled per project. Uploaded files appear in a dedicated admin block with download and delete.
2323
- Bulk select on admin project page: multi-select video assets and client uploads for bulk download or bulk delete.
@@ -55,13 +55,15 @@
5555
- IP and domain blocking.
5656
- Browser push notification management.
5757
- Version control: multiple video versions with revision tracking and optional limits.
58-
- Guest controls (view-only, latest-version restriction).
58+
- Guest controls (view-only, latest-version restriction, optional photo album visibility).
5959
- Asset management: images, audio, subtitles, project files, and documents with content validation.
6060
- Per-version thumbnails from uploaded image assets.
6161
- Client uploads block: view, download, and delete files submitted by clients via reverse share, with multi-select bulk actions.
6262
- Configurable global defaults for new projects (resolution, watermark, downloads, comments, approval).
6363
- Per-project overrides for global settings.
6464
- Video reprocessing: re-transcode videos when settings change (resolution, watermark).
65+
- Inline video previews on the project page (plays the transcoded preview, never the original).
66+
- One-click video deletion including all versions; drag & drop video files or folders onto the Videos section to upload.
6567
- Configurable max upload size and max comment attachments.
6668
- Option to use preview quality for approved playback (instead of original).
6769
- Privacy disclosure with custom text and GDPR consent tracking.

docs/wiki/Usage-Guide.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
## Creating your first project
44
1. Login to the admin panel.
55
2. Create a project with title, description, client name/email, and password (recommended).
6-
3. Upload videos.
6+
3. Upload videos — use the Upload Video(s) card, or drag video files or entire folders straight onto the Videos section.
77
4. Share the client link.
88
5. Collect timestamped feedback.
99
6. Approve when the client accepts the final version.
1010

11+
On the project page, click a video thumbnail to play its transcoded preview in place (the original file is never streamed). Expanding a video shows the rename and delete buttons next to its name — delete removes the video with all of its versions, while individual versions can be deleted inside the expanded list.
12+
1113
## Client workflow
1214
1. Open the share link.
1315
2. Enter password/OTP if required.
@@ -113,10 +115,11 @@ Reverse share lets clients upload files directly to a project from the share pag
113115
Projects can hold photo albums alongside videos — useful for stills, BTS shots, or photo deliverables.
114116

115117
**Creating and filling albums (admin):**
116-
1. Open the project page; the **Photos** section sits below the videos.
117-
2. Create an album and drag photos onto it, or drop entire folders onto the Photos section — each folder becomes its own album.
118-
3. The worker generates webp thumbnails automatically; uploads work on both local (TUS) and S3 storage.
119-
4. Hover an album to pick its cover photo; a sort toggle switches between name and date order.
118+
1. Open the project page; the **Photo Albums** section sits below the videos.
119+
2. Create an album and drag photos onto it, or drop entire folders onto the section — each folder becomes its own album.
120+
3. Click an album to fold it open in place: photos are listed with pagination, and the rename/delete buttons appear next to the album name.
121+
4. The worker generates webp thumbnails automatically; uploads work on both local (TUS) and S3 storage.
122+
5. Use the star on a photo row to set it as the album cover; a sort toggle in the section header switches between name and date order.
120123

121124
**Client side:** albums appear on the share page below the videos, with a full-page album view, lightbox, and multi-select. To let clients download photos (selection, album, or all project photos as a ZIP), enable **Allow photo downloads** in the project settings.
122125

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "Project" ADD COLUMN "guestShowPhotos" BOOLEAN NOT NULL DEFAULT false;

prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ model Project {
5656
authMode String @default("PASSWORD") // Authentication mode: PASSWORD, OTP, BOTH, or NONE
5757
guestMode Boolean @default(false) // Allow guest access with limited view (videos only, no comments/approval)
5858
guestLatestOnly Boolean @default(true) // Guests can only view latest version of each video
59+
guestShowPhotos Boolean @default(false) // Show photo albums to guests (view-only, downloads stay blocked)
5960
6061
// Revision tracking (optional)
6162
enableRevisions Boolean @default(false)

src/app/admin/projects/[id]/settings/page.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ interface Project {
4141
authMode: string
4242
guestMode: boolean
4343
guestLatestOnly: boolean
44+
guestShowPhotos: boolean
4445
previewResolution: string
4546
watermarkEnabled: boolean
4647
watermarkText: string | null
@@ -90,6 +91,7 @@ export default function ProjectSettingsPage() {
9091
const [authMode, setAuthMode] = useState('PASSWORD')
9192
const [guestMode, setGuestMode] = useState(false)
9293
const [guestLatestOnly, setGuestLatestOnly] = useState(true)
94+
const [guestShowPhotos, setGuestShowPhotos] = useState(false)
9395
const [useCustomSlug, setUseCustomSlug] = useState(false)
9496
const [customSlugValue, setCustomSlugValue] = useState('')
9597
const [previewResolution, setPreviewResolution] = useState('720p')
@@ -197,6 +199,7 @@ export default function ProjectSettingsPage() {
197199
setAuthMode(data.authMode || 'PASSWORD')
198200
setGuestMode(data.guestMode || false)
199201
setGuestLatestOnly(data.guestLatestOnly ?? true)
202+
setGuestShowPhotos(data.guestShowPhotos ?? false)
200203
setSharePassword(data.sharePassword || '')
201204

202205
setOriginalSettings({
@@ -311,6 +314,7 @@ export default function ProjectSettingsPage() {
311314
authMode,
312315
guestMode,
313316
guestLatestOnly,
317+
guestShowPhotos,
314318
clientNotificationSchedule,
315319
clientNotificationTime: (clientNotificationSchedule === 'DAILY' || clientNotificationSchedule === 'WEEKLY') ? clientNotificationTime : null,
316320
clientNotificationDay: clientNotificationSchedule === 'WEEKLY' ? clientNotificationDay : null,
@@ -1147,6 +1151,22 @@ export default function ProjectSettingsPage() {
11471151
</div>
11481152
)}
11491153

1154+
{guestMode && (
1155+
<div className="flex items-center justify-between gap-4 pt-2 mt-2 border-t border-border">
1156+
<div className="space-y-0.5 flex-1">
1157+
<Label htmlFor="guestShowPhotos">{t('showPhotosToGuests')}</Label>
1158+
<p className="text-xs text-muted-foreground">
1159+
{t('showPhotosToGuestsDescription')}
1160+
</p>
1161+
</div>
1162+
<Switch
1163+
id="guestShowPhotos"
1164+
checked={guestShowPhotos}
1165+
onCheckedChange={setGuestShowPhotos}
1166+
/>
1167+
</div>
1168+
)}
1169+
11501170
{authMode === 'NONE' && !guestMode && (
11511171
<div className="flex items-start gap-2 p-2 bg-warning-visible/50 border border-warning-visible rounded-md">
11521172
<span className="text-warning text-xs font-bold">!</span>

src/app/api/content/photo/[token]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export async function GET(
7575
return NextResponse.json({ error: photoMessages.photoNotFound || 'Photo not found' }, { status: 404 })
7676
}
7777

78-
if (isDownload && !verifiedToken.isAdmin && !photo.album.project.allowPhotoDownload) {
78+
if (isDownload && !verifiedToken.isAdmin && (verifiedToken.isGuest || !photo.album.project.allowPhotoDownload)) {
7979
return NextResponse.json(
8080
{ error: photoMessages.downloadsNotAllowed || 'Photo downloads are not allowed for this project' },
8181
{ status: 403 }

src/app/api/projects/[id]/photo-albums/[albumId]/photos/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ export async function GET(
4242
try {
4343
const album = await prisma.photoAlbum.findUnique({
4444
where: { id: albumId },
45-
include: { project: { select: { id: true, sharePassword: true, authMode: true } } },
45+
include: { project: { select: { id: true, sharePassword: true, authMode: true, guestShowPhotos: true } } },
4646
})
4747

4848
if (!album || album.projectId !== projectId) {
4949
return NextResponse.json({ error: photoMessages.albumNotFound || 'Album not found' }, { status: 404 })
5050
}
5151

5252
const accessCheck = await verifyProjectAccess(request, projectId, album.project.sharePassword, album.project.authMode, {
53-
allowGuest: false,
53+
allowGuest: album.project.guestShowPhotos,
5454
requiredPermission: 'view',
5555
})
5656
if (!accessCheck.authorized) {
@@ -63,7 +63,7 @@ export async function GET(
6363
})
6464

6565
const sessionId = accessCheck.shareTokenSessionId || `guest:${Date.now()}`
66-
const contentToken = await generateAlbumAccessToken(albumId, projectId, request, sessionId)
66+
const contentToken = await generateAlbumAccessToken(albumId, projectId, request, sessionId, accessCheck.isGuest === true)
6767

6868
return NextResponse.json({
6969
contentToken,

src/app/api/projects/[id]/photo-albums/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ export async function GET(
3535
try {
3636
const project = await prisma.project.findUnique({
3737
where: { id: projectId },
38-
select: { id: true, sharePassword: true, authMode: true },
38+
select: { id: true, sharePassword: true, authMode: true, guestShowPhotos: true },
3939
})
4040

4141
if (!project) {
4242
return NextResponse.json({ error: photoMessages.projectNotFound || 'Project not found' }, { status: 404 })
4343
}
4444

4545
const accessCheck = await verifyProjectAccess(request, project.id, project.sharePassword, project.authMode, {
46-
allowGuest: false,
46+
allowGuest: project.guestShowPhotos,
4747
requiredPermission: 'view',
4848
})
4949
if (!accessCheck.authorized) {
@@ -79,7 +79,7 @@ export async function GET(
7979
photoCount: album._count.photos,
8080
coverPhotoId: coverId,
8181
contentToken: coverId
82-
? await generateAlbumAccessToken(album.id, projectId, request, sessionId)
82+
? await generateAlbumAccessToken(album.id, projectId, request, sessionId, accessCheck.isGuest === true)
8383
: null,
8484
createdAt: album.createdAt,
8585
}

0 commit comments

Comments
 (0)