mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 17:34:52 +10:00
99c602e3c7
* Migrate from Biome to Oxlint/Oxfmt * pin version of autofix * set version of autofix * pin version of autofix * [autofix.ci] apply automated fixes * better comments, test formatter * [autofix.ci] apply automated fixes * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
109 lines
3.2 KiB
TypeScript
109 lines
3.2 KiB
TypeScript
import { ORPCError } from "@orpc/server";
|
|
import z from "zod";
|
|
|
|
import { protectedProcedure } from "../context";
|
|
import { getStorageService, isImageFile, processImageForUpload, uploadFile } from "../services/storage";
|
|
|
|
const storageService = getStorageService();
|
|
|
|
const fileSchema = z.file().max(10 * 1024 * 1024, "File size must be less than 10MB");
|
|
|
|
const filenameSchema = z.object({
|
|
filename: z.string().min(1).describe("The path or filename of the file to delete."),
|
|
});
|
|
|
|
function normalizeKey(input: string): string {
|
|
return input.trim().replace(/^\/+/, "").split("/").filter(Boolean).join("/");
|
|
}
|
|
|
|
function isUnsafeStorageKey(key: string): boolean {
|
|
return key.split("/").some((segment) => segment === "." || segment === "..");
|
|
}
|
|
|
|
export const storageRouter = {
|
|
uploadFile: protectedProcedure
|
|
.route({
|
|
tags: ["Internal"],
|
|
operationId: "uploadFile",
|
|
summary: "Upload a file",
|
|
description:
|
|
"Uploads a file to storage. Images are automatically resized and converted to WebP format. Maximum file size is 10MB. Requires authentication.",
|
|
successDescription: "The file was uploaded successfully.",
|
|
})
|
|
.input(fileSchema)
|
|
.output(
|
|
z.object({
|
|
url: z.string().describe("The public URL to access the uploaded file."),
|
|
path: z.string().describe("The storage path of the uploaded file."),
|
|
contentType: z.string().describe("The MIME type of the uploaded file."),
|
|
}),
|
|
)
|
|
.handler(async ({ context, input: file }) => {
|
|
const originalMimeType = file.type;
|
|
const isImage = isImageFile(originalMimeType);
|
|
|
|
let data: Uint8Array;
|
|
let contentType: string;
|
|
|
|
if (isImage) {
|
|
const processed = await processImageForUpload(file);
|
|
data = processed.data;
|
|
contentType = processed.contentType;
|
|
} else {
|
|
const fileBuffer = await file.arrayBuffer();
|
|
data = new Uint8Array(fileBuffer);
|
|
contentType = originalMimeType;
|
|
}
|
|
|
|
const result = await uploadFile({
|
|
userId: context.user.id,
|
|
data,
|
|
contentType,
|
|
type: "picture",
|
|
});
|
|
|
|
return {
|
|
url: result.url,
|
|
path: result.key,
|
|
contentType,
|
|
};
|
|
}),
|
|
|
|
deleteFile: protectedProcedure
|
|
.route({
|
|
tags: ["Internal"],
|
|
operationId: "deleteFile",
|
|
summary: "Delete a file",
|
|
description:
|
|
"Deletes a file from storage by its filename or path. If the filename does not start with 'uploads/', the user's picture directory is assumed. Requires authentication.",
|
|
successDescription: "The file was deleted successfully.",
|
|
})
|
|
.input(filenameSchema)
|
|
.output(z.void())
|
|
.errors({
|
|
NOT_FOUND: {
|
|
message: "The specified file was not found in storage.",
|
|
status: 404,
|
|
},
|
|
FORBIDDEN: {
|
|
message: "You do not have permission to delete this file.",
|
|
status: 403,
|
|
},
|
|
})
|
|
.handler(async ({ context, input }): Promise<void> => {
|
|
const requestedKey = normalizeKey(input.filename);
|
|
const key = requestedKey.startsWith("uploads/")
|
|
? requestedKey
|
|
: normalizeKey(`uploads/${context.user.id}/pictures/${requestedKey}`);
|
|
const userPrefix = `uploads/${context.user.id}/`;
|
|
|
|
if (isUnsafeStorageKey(key) || !key.startsWith(userPrefix)) {
|
|
throw new ORPCError("FORBIDDEN");
|
|
}
|
|
|
|
const deleted = await storageService.delete(key);
|
|
|
|
if (!deleted) throw new ORPCError("NOT_FOUND");
|
|
}),
|
|
};
|