Replace the hand-rolled JWT auth with Better Auth 1.6.25 httpOnly cookie sessions, validated against the database on every request so revocation, bans and role changes take effect immediately. Backend: - betterAuth.ts wires the Drizzle adapter, magic links, Google sign-in and the admin plugin; auth-schema.ts maps Better Auth's models onto the existing `users` table so user IDs and their foreign keys survive intact. - routes/auth.ts is gone; Better Auth serves the standard endpoints and authExt.ts carries the flows it doesn't cover. - auth.ts shrinks to session resolution and helpers; sessions/revocation in dashboard.ts now read and delete `auth_sessions` rows directly. - Schema adds the Better Auth core + admin columns (email_verified, image, banned, ban_reason, ban_expires), with migrations and tests. - rateLimit.ts resolves client IPs spoof-resistantly: proxy headers are only honoured from loopback/RFC1918 peers plus TRUSTED_PROXIES. - passwordPolicy.ts centralises password validation. - Bump drizzle-orm, drizzle-kit and better-sqlite3 to versions compatible with Better Auth. Frontend: - auth-client.ts plus a reworked AuthContext and api/client.ts move to cookie-based sessions; no more bearer tokens in requests or middleware. photo-api: - Validate Better Auth session cookies against the shared auth_sessions table instead of verifying JWTs; JWT_SECRET is no longer needed for user auth, and PHOTO_VIEW_SECRET now signs gallery view tokens. BETTER_AUTH_SECRET and BETTER_AUTH_URL are required in production; the deprecated JWT_SECRET stays only as the photo-api view-token fallback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
photo-api
Standalone Go service for event photo galleries: admins upload photos from past events, group them into galleries, and share them with the community. Lives in this monorepo but is its own module, binary, and deployable unit. Design/decisions: PLAN.md.
What it does
- Galleries with four visibility modes:
public(listed on /photos),private(admins only),link(share-token URL),ticket(logged-in users with a confirmed ticket for the linked event — any confirmed ticket when the event is free). - Originals are stored byte-identical for download; a worker generates JPEG variants (thumb 512px q78, preview 2048px q85, EXIF stripped/orientation applied) queued in the DB with retries.
- Storage: local disk (
STORAGE_PATH) or S3/Garage/MinIO (setS3_ENDPOINTS3_BUCKET), same selection convention as the backend. S3 downloads use short-lived presigned URLs; nothing in the bucket is public.
- Auth: validates the backend's HS256 JWTs with the shared
JWT_SECRET(issuerspanglish, audiencespanglish-app) including the DB-backed tokenVersion/account-status revocation check. Admin surface isadmin/organizeronly. - Database: the same Postgres or SQLite database as the backend (
DB_TYPE,DATABASE_URL). This service owns only thephotos_*tables via its own embedded migrations (photo-api migrate); drizzle-kit never sees them. Reads of backend tables are confined tointernal/store/access.goand sanity-checked at startup.
Develop
cp .env.example .env # set JWT_SECRET + DATABASE_URL to match backend/.env
go run ./cmd/photo-api migrate
go run ./cmd/photo-api # serves on :3003 (dev)
go test ./... # SQLite; add PHOTO_TEST_PG=<url> to also run on Postgres
From the repo root: npm run dev:photos, npm run build:photos,
npm run test:photos. The Next dev server rewrites /api/photos/* to
PHOTO_API_URL (default http://localhost:3003), so the frontend needs no
extra config in dev.
HEIC uploads require a converter CLI on the host: apt install libvips-tools
(or libheif-examples). Without one, HEIC uploads are rejected with a clear
message and a startup warning is logged.
API
Everything under /api/photos. Errors are {"error": string}.
Admin (Bearer token, role admin/organizer):
| Method | Path | Purpose |
|---|---|---|
| POST | /api/photos/galleries |
create gallery |
| GET | /api/photos/galleries |
list all (?eventId=) |
| GET | /api/photos/galleries/:id |
gallery + photos (all statuses) |
| PATCH | /api/photos/galleries/:id |
update title/visibility/event/cover |
| DELETE | /api/photos/galleries/:id |
delete gallery + objects |
| POST | /api/photos/galleries/:id/photos |
multipart upload (files) |
| PATCH | /api/photos/galleries/:id/order |
reorder ({photoIds}) |
| POST | /api/photos/galleries/:id/share-token |
rotate share token |
| DELETE | /api/photos/photos/:photoId |
delete photo |
| POST | /api/photos/photos/:photoId/retry |
requeue failed photo |
Viewer (anonymous or member token; ?token= carries the share token):
| Method | Path | Purpose |
|---|---|---|
| GET | /api/photos/public/galleries |
public index |
| GET | /api/photos/public/galleries/:slug |
gallery view (access-checked) |
| GET | /api/photos/public/events/:eventSlug/gallery |
newest gallery of an event (access-checked) |
| GET | /api/photos/files/:photoId/:variant |
bytes; thumb|preview|original |
| GET | /api/photos/health |
liveness |
Deploy (systemd, primary)
cd photo-api && go build -o bin/photo-api ./cmd/photo-api
sudo apt install libvips-tools
sudo cp ../deploy/spanglish-photos.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now spanglish-photos
sudo nginx -t && sudo systemctl reload nginx # after installing the updated confs
nginx routes location ^~ /api/photos/ → 127.0.0.1:3020 (see
deploy/front-end_nginx.conf, deploy/back-end_nginx.conf,
deploy/spanglish_upstreams.conf). The frontend's production .env should
set PHOTO_API_URL=http://127.0.0.1:3020 for server-side rendering of the
public gallery pages. A Dockerfile is included as a secondary path for the
compose-scale setup.