feat(photos): community film strip uploads + admin moderation
Backend
- photos table + upload storage under DATA_DIR/uploads (magic-byte sniffing,
no multipart dep, SVG rejected, wx exclusive writes)
- POST/GET /api/photos, GET /api/photos/:id/file with nosniff + sandboxed CSP
- admin routes (ADMIN_EMAILS allowlist): list, delete one, clear all
- identity-keyed rate limits (login 20/15m, signup 5/h, upload 60/h)
- cookie gains Secure when the request is https (via trustProxy)
- /api/auth/me now 200 {user:null} instead of 401 when signed out
Frontend
- landing strip section: signed-in users upload straight from the reel,
guests get a /app?auth=1 link
- /admin page: grid of uploads with delete + clear all
- nginx: nosniff / X-Frame-Options / Referrer-Policy, forward
X-Forwarded-Proto so the API can mark cookies Secure behind TLS
Tests: docker/backend test/security.mjs (45 checks)
This commit is contained in:
@@ -107,6 +107,26 @@ export const en: Dict = {
|
||||
'save.saved': 'Saved "{name}"',
|
||||
'save.needLogin': 'Log in to save a recipe.',
|
||||
|
||||
'photo.contribute': 'CONTRIBUTE A PHOTO',
|
||||
'photo.uploading': 'UPLOADING…',
|
||||
'photo.thanks': 'Your frame is in the strip.',
|
||||
'photo.failed': 'Upload failed: {msg}',
|
||||
|
||||
'adm.title': 'Strip moderation',
|
||||
'adm.subtitle': 'Photos contributed by users. Delete one to pull it off the landing page.',
|
||||
'adm.signIn': 'Sign in with an admin account',
|
||||
'adm.notAdmin': 'This account has no admin rights.',
|
||||
'adm.empty': 'No contributions yet.',
|
||||
'adm.total': '{n} photos',
|
||||
'adm.owner': 'Uploaded by',
|
||||
'adm.size': 'Size',
|
||||
'adm.uploaded': 'Uploaded',
|
||||
'adm.delete': 'DELETE',
|
||||
'adm.clearAll': 'DELETE ALL',
|
||||
'adm.clearConfirm': 'Delete all {n} contributions? This cannot be undone.',
|
||||
'adm.removed': 'Deleted {n} photos.',
|
||||
'adm.back': 'Back to home',
|
||||
|
||||
'err.noWebgl':
|
||||
'The browser refused a WebGL context — this build needs WebGL to process photos (the CPU path is thousands of times slower).',
|
||||
'err.decode': 'Could not read this image. Try another JPG or PNG.',
|
||||
|
||||
@@ -110,6 +110,26 @@ export const vi = {
|
||||
'save.saved': 'Đã lưu "{name}"',
|
||||
'save.needLogin': 'Đăng nhập để lưu công thức.',
|
||||
|
||||
'photo.contribute': 'ĐÓNG GÓP ẢNH',
|
||||
'photo.uploading': 'ĐANG TẢI…',
|
||||
'photo.thanks': 'Ảnh của bạn đã vào dải phim.',
|
||||
'photo.failed': 'Không tải được ảnh: {msg}',
|
||||
|
||||
'adm.title': 'Quản trị dải phim',
|
||||
'adm.subtitle': 'Ảnh do người dùng đóng góp. Xoá một ảnh để gỡ nó khỏi trang chủ.',
|
||||
'adm.signIn': 'Đăng nhập bằng tài khoản quản trị',
|
||||
'adm.notAdmin': 'Tài khoản này không có quyền quản trị.',
|
||||
'adm.empty': 'Chưa có ảnh đóng góp nào.',
|
||||
'adm.total': '{n} ảnh',
|
||||
'adm.owner': 'Người đăng',
|
||||
'adm.size': 'Dung lượng',
|
||||
'adm.uploaded': 'Thời điểm',
|
||||
'adm.delete': 'XOÁ',
|
||||
'adm.clearAll': 'XOÁ TẤT CẢ',
|
||||
'adm.clearConfirm': 'Xoá toàn bộ {n} ảnh đóng góp? Không hoàn tác được.',
|
||||
'adm.removed': 'Đã xoá {n} ảnh.',
|
||||
'adm.back': 'Về trang chủ',
|
||||
|
||||
'err.noWebgl':
|
||||
'Trình duyệt không cấp được WebGL — bản dựng này cần WebGL để xử lý ảnh (chạy CPU chậm gấp hàng nghìn lần).',
|
||||
'err.decode': 'Không đọc được ảnh này. Thử JPG hoặc PNG khác.',
|
||||
|
||||
Reference in New Issue
Block a user