All files / routes/pro presigned-uploads.routes.ts

100% Statements 29/29
100% Branches 12/12
100% Functions 2/2
100% Lines 29/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126                          1x                                                   1x     1x       8x 8x 8x   8x               8x 1x   7x 1x   6x 1x       5x 1x             4x     8x 2x         2x               2x   6x           1x       3x 3x 3x 3x   3x   2x           1x            
// Pro Presigned Upload Routes - Request presigned URLs and confirm uploads
import { Hono } from "hono";
import type { Dal } from "../../dal";
import type { Services } from "../../services";
import { success, handleError } from "../../lib/response";
import { requireProAccess } from "../../middleware";
import { ValidationError } from "../../lib/errors";
import {
	ALLOWED_IMAGE_TYPES,
	ALLOWED_UPLOAD_TYPES,
} from "../../lib/file-validation";
import type { UploadContext } from "../../services/upload.service";
 
const VALID_CONTEXTS: UploadContext[] = [
	"project-photo",
	"blog-cover",
	"blog-image",
	"profile",
	"certification",
	"leadership",
	"testimonial",
	"logo",
	"cover",
	"room-media",
];
 
type Env = {
	Bindings: CloudflareBindings;
	Variables: {
		user: { id: string; name: string; email: string } | null;
		session: unknown;
		dal: Dal;
		services: Services;
		db: ReturnType<typeof import("../../db").getDb>;
		proId: string;
		proRole: string;
	};
};
 
const presignedUploads = new Hono<Env>();
 
// Request a presigned upload URL
presignedUploads.post(
	"/:proId/uploads/request",
	requireProAccess,
	async (c) => {
		try {
			const proId = c.get("proId");
			const services = c.get("services");
 
			const body = await c.req.json<{
				fileName?: string;
				contentType?: string;
				context?: string;
				contextId?: string;
			}>();
 
			// Validate required fields
			if (!body.fileName?.trim()) {
				throw new ValidationError("fileName is required");
			}
			if (!body.contentType?.trim()) {
				throw new ValidationError("contentType is required");
			}
			if (!body.context?.trim()) {
				throw new ValidationError("context is required");
			}
 
			// Validate context against known values
			if (!VALID_CONTEXTS.includes(body.context as UploadContext)) {
				throw new ValidationError(
					`Invalid upload context. Allowed: ${VALID_CONTEXTS.join(", ")}`,
				);
			}
 
			// Validate contentType: room-media allows video, others are image-only
			const allowedTypes =
				body.context === "room-media"
					? ALLOWED_UPLOAD_TYPES
					: ALLOWED_IMAGE_TYPES;
			if (!allowedTypes.includes(body.contentType)) {
				throw new ValidationError(
					`Invalid content type. Allowed: ${allowedTypes.join(", ")}`,
				);
			}
 
			const result = await services.upload.requestUpload({
				proId,
				fileName: body.fileName,
				contentType: body.contentType,
				context: body.context as UploadContext,
				contextId: body.contextId,
			});
 
			return success(c, result, 201);
		} catch (err) {
			return handleError(c, err);
		}
	},
);
 
// Confirm an upload has completed
presignedUploads.post(
	"/:proId/uploads/:uploadId/confirm",
	requireProAccess,
	async (c) => {
		try {
			const proId = c.get("proId");
			const uploadId = c.req.param("uploadId");
			const services = c.get("services");
 
			const upload = await services.upload.confirmUpload(uploadId, proId);
 
			return success(c, {
				status: upload.status,
				storageKey: upload.storageKey,
				contextId: upload.contextId,
			});
		} catch (err) {
			return handleError(c, err);
		}
	},
);
 
export default presignedUploads;