Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ UPLOAD_DIR=uploads
# GOOGLE_ARCHIVE_DRIVE_FOLDER_ID=the-second-account-folder-id

MAX_VIDEO_UPLOAD_MB=90
MAX_RECORDING_UPLOAD_MB=80
MAX_RECORDING_UPLOAD_MB=100

RATE_LIMIT_ENABLED=true

Expand Down
13 changes: 9 additions & 4 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ All paths are under `/api/v1`.
| --- | --- | --- |
| `GET /health` | Public | Reports database readiness. |
| `/auth/*` | Public | Signup, login, email verification, password reset, logout, and session. |
| `GET/PATCH /users/me` | Authenticated user | Read profile and update supported connections. |
| `GET/PATCH/DELETE /users/me` | Authenticated user | Read or update profile; contributors can delete their account after confirming their password. Submitted recordings remain stored. |
| `GET /categories`; `POST/PATCH/DELETE /categories/:id` | Public read; admin write | Deleting a category unassigns its videos. |
| `GET /videos`, `GET /videos/:id[/file/poster]` | Public or admin | Public visibility is published and categorised only; file streaming supports byte ranges. |
| `POST /videos`; `PATCH/DELETE /videos/:id` | Admin | Manage reference videos and optional posters. |
| `GET /demo-video[/file]`; `POST/DELETE /demo-video` | Public read; admin write | Singleton public walkthrough media. |
| `GET/POST /submissions` | Authenticated user | Contributors list their own; admins list all. New submissions are append-only. |
| `POST /contact` | Signed-in account | Email must match account; account and global daily quotas apply. |
| `POST /contact` | Signed-in account or recording-deletion request | Other topics require the account email; deletion requests can be submitted without signing in at any time. |
| `/admin/users*`, `/admin/restrictions*`, `/admin/stats`, `/admin/messages` | Admin | Account moderation, access restrictions, statistics, and inbox. |

See [API.md](docs/API.md) for the complete route table, limits, visibility, and
Expand All @@ -95,6 +95,11 @@ mounted outside production.
followed by consent acceptance before a new account is activated and signed
in. Existing contributors must accept the current consent version before
submitting; password reset invalidates existing sessions.
- Account deletion removes sign-in access and the account record but deliberately
retains submitted recordings and contribution data. A recording/data deletion
request can be sent through the contact form before or after account deletion,
without a deadline. Recordings already used in a released model cannot be
removed from that model.
- `CLIENT_ORIGIN` controls credentialed CORS and the check for browser
state-changing requests that carry an `Origin` header.
- The global, auth, contact, and upload rate limits can be disabled with
Expand All @@ -103,7 +108,7 @@ mounted outside production.
`ARCHIVE_STORAGE_DRIVER` independently selects append-only contributor archive
storage. Both default to local; Google Drive credentials can be shared or
configured separately for archives.
- Upload defaults are 90 MB for base/demo videos and 80 MB for contributor
- Upload defaults are 90 MB for base/demo videos and 100 MB for contributor
recordings. File contents are verified by signature, not browser MIME type.
- Reference and demo-video uploads accept browser-reported `width` and `height`
in pixels, each from 0 to 16,384. Zero represents unknown dimensions; these
Expand All @@ -127,7 +132,7 @@ npm test
The command runs Node's built-in test runner serially across every
`tests/*.test.js` file. The API suites use MongoDB Memory Server unless
`MONGODB_URI_TEST` is supplied. That configured database is dropped by the tests,
so use only a disposable database. Provider delivery is stubbed. The 60 cases and
so use only a disposable database. Provider delivery is stubbed. The 61 cases and
their test types are documented in [docs/TEST-CASES.md](docs/TEST-CASES.md).

## Current limitation
Expand Down
16 changes: 11 additions & 5 deletions backend/docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ All paths below are relative to `/api/v1`.
| GET | `/auth/session` | Public | Return the current user or `null`. |
| GET | `/users/me` | Authenticated user | Read the signed-in user's profile. |
| PATCH | `/users/me` | Authenticated user | Update supported profile connections. |
| DELETE | `/users/me` | Contributor | Delete the account after confirming the current password; submitted recordings and contribution data remain stored. |
| POST | `/users/me/consent` | Authenticated user | Accept the current notice for an existing account. |
| GET | `/categories` | Public | List categories. |
| POST | `/categories` | Admin | Create a category. |
Expand All @@ -90,10 +91,10 @@ All paths below are relative to `/api/v1`.
| DELETE | `/demo-video` | Admin | Delete the demo video. |
| GET | `/submissions` | Authenticated user | List own submissions; admins can list all. |
| POST | `/submissions` | Authenticated user | Create an append-only recording submission. |
| POST | `/contact` | Signed-in account | Send a contact message using the email on the account. |
| POST | `/contact` | Signed-in account or recording-deletion request | Other topics require the account email; deletion requests can be submitted without signing in before or after account deletion, at any time. |
| GET | `/admin/users` | Admin | List accounts. |
| PATCH | `/admin/users/:id` | Admin | Change an account's active/suspended status. |
| DELETE | `/admin/users/:id` | Admin | Delete an account and its submissions, cooldowns, and archived recordings. |
| DELETE | `/admin/users/:id` | Admin | Delete an account and its cooldown claims; submitted recordings and contribution data remain stored. |
| GET | `/admin/restrictions` | Admin | List active device and IP restrictions. |
| POST | `/admin/restrictions` | Admin | Create a permanent device or IP restriction. |
| DELETE | `/admin/restrictions/:id` | Admin | Remove a restriction. |
Expand All @@ -102,7 +103,11 @@ All paths below are relative to `/api/v1`.

There is intentionally no submission read, update, or delete endpoint, including
for admins. The archive is append-only and archived media is not streamed through
the API.
the API. Account deletion leaves submissions intact; a separate contact request
is required to ask the team to review stored-recording and contribution-data
deletion. Requests may be sent before or after account deletion, without a
deadline. Recordings already used in a released model cannot be removed from
that model.

## Request bodies and uploads

Expand Down Expand Up @@ -135,10 +140,11 @@ Submission creation uses `multipart/form-data` with a required `recording` file
and `videoId`, `trimStart`, `trimEnd`, `mirrored`, and `duration` fields. Only
published videos assigned to a category can receive a submission. Durations and
trim metadata are currently client-reported; the backend does not transcode or
physically trim the uploaded file.
physically trim the uploaded file. Submission duration must not exceed 600
seconds (10 minutes).

Uploads are bounded by `MAX_VIDEO_UPLOAD_MB` and
`MAX_RECORDING_UPLOAD_MB` (defaults: 90 MB and 80 MB). Video signatures are
`MAX_RECORDING_UPLOAD_MB` (defaults: 90 MB and 100 MB). Video signatures are
checked as MP4, WebM, or MOV. Poster signatures are checked as JPEG, PNG, or WebP;
posters must not exceed 2 MB. The browser-provided MIME type is not treated as
proof of file content.
Expand Down
7 changes: 5 additions & 2 deletions backend/docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,11 @@ Submission recordings are append-only. The archive folder is based on contributo
and per-video sequence, category slug, and video slug. MongoDB keeps the archive
reference; the API never streams or mutates the archived file. Contributor
responses omit archive paths, while admins can see archive metadata. Account
deletion removes that account's archive objects and related submissions; deleting
a base video does not remove submissions.
deletion removes the account and its cooldown claims but retains its submissions
and archived recording objects. The contact form accepts a separate
recording/data deletion request before or after account deletion, at any time;
requests made after account deletion may require the team to verify and manually
match the former contributor. Deleting a base video does not remove submissions.

Uploads first land in a temporary directory. Multer limits file size and count;
services then inspect file signatures before moving valid assets into the selected
Expand Down
62 changes: 33 additions & 29 deletions backend/docs/TEST-CASES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Backend test cases

The backend suite uses Node's built-in test runner. It currently contains **60 test
The backend suite uses Node's built-in test runner. It currently contains **61 test
cases** across thirteen test files. Each named API test suite has its own `.test.js`
file and starts with a fresh test database and account fixtures:

Expand All @@ -15,7 +15,7 @@ file and starts with a fresh test database and account fixtures:
| `tests/api-videos.test.js` | 6 | HTTP integration tests |
| `tests/api-submissions.test.js` | 8 | HTTP integration tests |
| `tests/api-admin-stats-contact.test.js` | 4 | HTTP integration tests |
| `tests/api-admin-user-controls.test.js` | 4 | HTTP integration tests |
| `tests/api-admin-user-controls.test.js` | 5 | HTTP integration tests |
| `tests/safeName.test.js` | 3 | Unit tests |
| `tests/localStorage.test.js` | 6 | Filesystem/storage integration tests |
| `tests/videoService.test.js` | 9 | Unit tests |
Expand Down Expand Up @@ -153,7 +153,8 @@ their respective test file. Each API case below is an HTTP integration test.
34. **contact delivery requires matching account email, enforces quotas, and
blocks abusive devices** — Checks contact validation, delivery failure
handling, matching account email, per-user and global daily quotas, anonymous
access rejection, device blocking, message listing, and admin-only access.
recording-deletion requests, rejection of other anonymous topics, device
blocking, message listing, and admin-only access.
35. **oversized uploads are rejected with 413** — Checks payload-too-large
responses and temporary-file cleanup.

Expand All @@ -162,13 +163,16 @@ their respective test file. Each API case below is an HTTP integration test.
36. **suspending a user blocks its existing session and login; reactivation
restores access** — Checks self-suspension protection, suspension effects on
sessions and login, and reactivation.
37. **deleting a user removes their submissions, cooldowns, and stored
recording** — Checks account deletion cleanup across database records,
cooldowns, archive storage, sessions, and admin listings.
38. **device restrictions reject matching IDs, expose only a hint, and can be
37. **contributors can delete their account without removing submitted
recordings** — Checks current-password confirmation, session invalidation,
account removal, retained submission/archive data, and cooldown cleanup.
38. **admin deletion removes the account and cooldowns but retains submissions
and recordings** — Checks admin account removal, retained archive files,
cooldown cleanup, and updated account/submission listings.
39. **device restrictions reject matching IDs, expose only a hint, and can be
removed** — Checks device restriction creation, hashed-identifier privacy,
blocking, duplicate detection, and removal.
39. **IP restrictions block API traffic while admin routes remain available for
40. **IP restrictions block API traffic while admin routes remain available for
recovery** — Checks IP validation and restriction enforcement while
preserving admin access to remove restrictions.

Expand All @@ -179,63 +183,63 @@ routes. The cases are separated by the behavior under test.

### `tests/safeName.test.js` — Safe names (unit tests)

40. **safeName uses its fallback for nullish and blank values** — Checks fallback
41. **safeName uses its fallback for nullish and blank values** — Checks fallback
behavior for nullish, whitespace-only, and fully sanitized names.
41. **safeName replaces reserved path characters and collapses whitespace** —
42. **safeName replaces reserved path characters and collapses whitespace** —
Checks removal of path-reserved characters, whitespace normalization, and
string conversion of non-string values.
42. **safeName caps its output at 100 characters** — Checks the maximum safe-name
43. **safeName caps its output at 100 characters** — Checks the maximum safe-name
length.

### `tests/localStorage.test.js` — Local driver and storage service (filesystem/storage integration tests)

43. **local driver stores named files in folders and appends suffixes on
44. **local driver stores named files in folders and appends suffixes on
collisions** — Checks directory placement, readable keys, collision naming,
and file contents.
44. **local driver handles named files without an extension and dotfiles** —
45. **local driver handles named files without an extension and dotfiles** —
Checks collision naming for extensionless names and dotfiles.
45. **local driver generates unique UUID keys when no file name is supplied** —
46. **local driver generates unique UUID keys when no file name is supplied** —
Checks generated key format and uniqueness.
46. **local driver rejects paths escaping the storage root and keeps the source
47. **local driver rejects paths escaping the storage root and keeps the source
file** — Checks rejection of traversal through file names or folder names and
confirms rejected saves do not consume the source file.
47. **local driver stat reports missing files and remove is idempotent** — Checks
48. **local driver stat reports missing files and remove is idempotent** — Checks
missing-file errors from stat and harmless removal of an absent key.
48. **storageService forwards named folder and file name to the local driver** —
49. **storageService forwards named folder and file name to the local driver** —
Checks storage-service metadata, destination key, file size, and cleanup.

### `tests/videoService.test.js` — Video service (unit tests)

49. **video create names video and poster from title and category safely** —
50. **video create names video and poster from title and category safely** —
Checks sanitized names, category folder selection, poster naming, storage
calls, and category ordering.
50. **video create uses Unassigned and Untitled fallbacks when optional labels
51. **video create uses Unassigned and Untitled fallbacks when optional labels
are absent** — Checks fallback folder and file names, default ordering, and
absent poster metadata.
51. **video create selects detected video and image extensions** — Checks
52. **video create selects detected video and image extensions** — Checks
content-detected extensions for WebM, MOV, PNG, and WebP rather than relying
on the original file name.
52. **video create rejects a missing upload, unsupported content, and invalid
53. **video create rejects a missing upload, unsupported content, and invalid
categories before storage** — Checks validation errors and confirms storage is
not called for invalid input.
53. **video create rejects oversized or unsupported posters before storage** —
54. **video create rejects oversized or unsupported posters before storage** —
Checks poster size and real-content validation before any storage call.
54. **video create accepts a poster exactly at the size limit** — Checks that a
55. **video create accepts a poster exactly at the size limit** — Checks that a
poster at the configured maximum size is accepted.
55. **video create surfaces filesystem errors instead of reporting invalid video
56. **video create surfaces filesystem errors instead of reporting invalid video
content** — Checks missing-file I/O errors are preserved and storage is not
called.
56. **video create removes already stored files when poster storage fails** —
57. **video create removes already stored files when poster storage fails** —
Checks cleanup of the stored video when the subsequent poster save fails.
57. **video create removes video and poster if the database write fails** —
58. **video create removes video and poster if the database write fails** —
Checks cleanup of both stored files when database record creation fails.
58. **creates the account only after consent and records the acceptance time** —
59. **creates the account only after consent and records the acceptance time** —
Checks that verification alone cannot log in, current consent activates the
account, the acceptance time and version are stored, and consent tokens
cannot be replayed.
59. **declining consent discards the unfinished signup** — Checks the pending
60. **declining consent discards the unfinished signup** — Checks the pending
account is removed and its email can be used for a new signup.
60. **legacy accounts without consent cannot submit until they accept** — Checks
61. **legacy accounts without consent cannot submit until they accept** — Checks
that older accounts are blocked from submitting until they accept the current
notice.

Expand Down
2 changes: 1 addition & 1 deletion backend/docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ The admin is ensured on every startup. Changing `ADMIN_PASSWORD` later does not
| `ARCHIVE_STORAGE_DRIVER` | `local` or `gdrive` | Append-only storage for contributor recordings. Use `gdrive` for production archives. |
| `UPLOAD_DIR` | Local filesystem path; default `uploads` | Local storage root; temporary uploads are kept in its `tmp` subdirectory. |
| `MAX_VIDEO_UPLOAD_MB` | Set manually; default `90` | Maximum reference-video or demo-video upload size, in MiB. |
| `MAX_RECORDING_UPLOAD_MB` | Set manually; default `80` | Maximum contributor-recording upload size, in MiB. |
| `MAX_RECORDING_UPLOAD_MB` | Set manually; default `100` | Maximum contributor-recording upload size, in MiB. |
| `GOOGLE_SERVICE_ACCOUNT_JSON` | Service-account JSON from Google Cloud | Shared Google Drive credentials, available as fallback for either driver. |
| `GOOGLE_OAUTH_CLIENT_ID` | Google OAuth client configuration | Shared OAuth client ID; set with the corresponding secret and refresh token. |
| `GOOGLE_OAUTH_CLIENT_SECRET` | Google OAuth client configuration | Shared OAuth client secret. |
Expand Down
4 changes: 2 additions & 2 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "backend",
"version": "1.0.3",
"version": "1.0.4",
"description": "SignPakCommons API: Express, Node.js and MongoDB",
"main": "server.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ export const VIDEO_STATUS_LIST = Object.values(VIDEO_STATUS)
export const POSTER_MAX_BYTES = 2 * 1024 * 1024
export const MAX_RECORDING_SECONDS = 600
// Bump when the consent notice text changes materially; contributors are then asked to accept it again.
export const CONSENT_VERSION = '2026-10'
export const CONSENT_VERSION = '2026-10-15'
// Client-reported durations are rounded, so allow a little slack when checking trim ranges.
export const TRIM_TOLERANCE_SECONDS = 0.5
2 changes: 1 addition & 1 deletion backend/src/config/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const schema = z.object({
GOOGLE_ARCHIVE_SERVICE_ACCOUNT_JSON: z.string().optional(),
GOOGLE_ARCHIVE_DRIVE_FOLDER_ID: z.string().optional(),
MAX_VIDEO_UPLOAD_MB: z.coerce.number().positive().default(90),
MAX_RECORDING_UPLOAD_MB: z.coerce.number().positive().default(80),
MAX_RECORDING_UPLOAD_MB: z.coerce.number().positive().default(100),

RATE_LIMIT_ENABLED: bool.default(true),
SUBMISSION_COOLDOWN_MS: z.coerce.number().int().positive().default(30_000),
Expand Down
Loading
Loading