mirror of
https://github.com/tiennm99/bsk.git
synced 2026-08-13 18:22:28 +00:00
- private bsk-checkup-media bucket + checkup_images table; RLS: enrolled read, clinical write; storage.objects policies scoped to the bucket - webcam (getUserMedia) + file capture, client canvas JPEG compression to <=200KB; upload via browser client, metadata recorded server-side with a path-ownership guard; audit-logged - gallery with 1h signed URLs + soft-delete; code128 barcode of the checkup id (no PII); linked from the checkup page; vi/en; bwip-js added
49 lines
1.8 KiB
TypeScript
49 lines
1.8 KiB
TypeScript
/**
|
|
* Shared constants + validation for checkup imaging (Phase 5). Shared
|
|
* client/server. Storage layout: private bucket `bsk-checkup-media`, object
|
|
* key `${checkupId}/${uuid}.jpg`. Compression happens client-side (canvas →
|
|
* JPEG, quality stepped down until ≤200KB); signed URLs are issued
|
|
* server-side with a 1h TTL and never persisted.
|
|
*/
|
|
|
|
import { z } from "zod";
|
|
|
|
export const CHECKUP_MEDIA_BUCKET = "bsk-checkup-media";
|
|
export const MAX_IMAGE_BYTES = 200 * 1024; // 200 KB — Hobby-tier storage budget (PLAN §5, §7)
|
|
export const SIGNED_URL_TTL_SECONDS = 60 * 60; // 1 hour
|
|
|
|
const UUID_JPG = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jpg$/i;
|
|
|
|
/** Builds the object key for an uploaded image — `{checkupId}/{uuid}.jpg`. */
|
|
export function buildStoragePath(checkupId: number, uuid: string): string {
|
|
return `${checkupId}/${uuid}.jpg`;
|
|
}
|
|
|
|
/**
|
|
* Validates that a storage path belongs to the given checkup and matches the
|
|
* `{uuid}.jpg` naming convention. Prevents a client from recording metadata
|
|
* that points at an object under a different checkup (or an arbitrary key).
|
|
*/
|
|
export function isValidStoragePath(checkupId: number, path: string): boolean {
|
|
const parts = path.split("/");
|
|
if (parts.length !== 2 || parts[0] !== String(checkupId)) return false;
|
|
const filename = parts[1] ?? "";
|
|
return UUID_JPG.test(filename);
|
|
}
|
|
|
|
export const RecordImageSchema = z.object({
|
|
checkupId: z.coerce.number().int().positive(),
|
|
storagePath: z.string().trim().min(1).max(300),
|
|
});
|
|
|
|
export const DeleteImageSchema = z.object({
|
|
checkupId: z.coerce.number().int().positive(),
|
|
imageId: z.coerce.number().int().positive(),
|
|
storagePath: z.string().trim().min(1).max(300),
|
|
});
|
|
|
|
export type ImageActionState =
|
|
| { status: "idle" }
|
|
| { status: "error"; message: string }
|
|
| { status: "success" };
|