feat: POST /auth/workspaces — create an additional collective from an existing account

Until now collectives only existed at registration; an account with a
personal workspace had no path to start one. The creator becomes Owner
and gets a session scoped to the new workspace.
This commit is contained in:
glass beadian 2026-10-14 17:16:17 +00:00
parent 177ca6eb2b
commit cbdb8c0f81
18 changed files with 2584 additions and 8 deletions

View file

@ -0,0 +1,100 @@
/**
* Wire shapes for the anonymous submission boxes (apps/api/src/submissions).
*
* Kept local to the web app so it does not collide with the shared
* api-types.ts other builders may be editing in parallel — the same standing
* as components/files/types.ts. Hand-synced with submissions.controller.ts
* and submissions.service.ts (commit 599a68c).
*
* JSON note: the API returns `createdAt: Date`, which serialises to an ISO
* string on the wire, so every date in here is typed as string.
*/
export type SubmissionKind = "video" | "photo";
export type SubmissionState = "pending" | "approved" | "rejected";
/** Moderation queue filter the API accepts (GET ?state=). */
export type SubmissionQueueFilter = "pending" | "approved" | "rejected" | "all";
/** GET /public/submit/:token — public box metadata for the submit page. */
export interface SubmissionBoxInfo {
id: string;
groupId: string;
licenseDefault: string;
enabled: boolean;
}
/** POST /public/submit/:token → 202 */
export interface SubmissionReceiveResult {
submissionId: string;
state: "pending";
}
/** GET/POST /groups/:groupId/submission-box — token + open switch. */
export interface SubmissionBoxConfig {
token: string;
enabled: boolean;
}
/** GET /groups/:groupId/submissions — contact is licensing-inquiry data that
* only this moderation surface ever renders; it is never copied onto assets. */
export interface SubmissionRecord {
id: string;
boxId: string;
groupId: string;
kind: SubmissionKind;
mime: string;
sizeBytes: number;
sha256: string;
license: string;
creditName: string | null;
contact: string | null;
state: SubmissionState;
rejectionNote: string | null;
createdAt: string;
}
/** POST /submissions/:id/approve|reject — the decided record plus where it landed. */
export type SubmissionDecisionResult = SubmissionRecord & {
videoAssetId?: string;
groupAssetId?: string;
};
/** The id/name slice of GET /groups/mine the submission surfaces need. */
export interface SubmissionGroupSummary {
id: string;
name: string;
}
/* Intake caps, mirroring apps/api/src/submissions/submissions.service.ts. */
export const SUBMISSION_MAX_VIDEO_BYTES = 200 * 1024 * 1024;
export const SUBMISSION_MAX_PHOTO_BYTES = 50 * 1024 * 1024;
/** The magic-byte window the API reads before deciding a type; shorter files are "unrecognised". */
export const SUBMISSION_SNIFF_BYTES = 32;
/** optionalText() caps in the API's receive path. */
export const SUBMISSION_CREDIT_MAX = 100;
export const SUBMISSION_CONTACT_MAX = 200;
/** submissions.rejection_note CHECK constraint (≤ 500). */
export const SUBMISSION_NOTE_MAX = 500;
/** The qualifier every submitter must tick — exact text from the box design. */
export const SUBMISSION_LICENSE_TEXT =
"I took or own this footage and release it under CC BY-SA 4.0 — it may be shared and remixed with attribution";
/** Canonical public origin of the web app; overridable for local/dev runs. */
export const SUBMIT_APP_ORIGIN = (process.env.NEXT_PUBLIC_APP_ORIGIN ?? "https://app.occupymedia.cc").replace(/\/$/, "");
/** The link a collective hands out: https://app.occupymedia.cc/submit/<token>. */
export function submissionLink(token: string): string {
return `${SUBMIT_APP_ORIGIN}/submit/${token}`;
}
/** Size cap per kind — the same rule the API applies after sniffing the bytes. */
export function submissionMaxBytes(kind: SubmissionKind): number {
return kind === "video" ? SUBMISSION_MAX_VIDEO_BYTES : SUBMISSION_MAX_PHOTO_BYTES;
}