/api/category, /api/directory, /api/file 아래의 대부분의 Endpoint들을 tRPC로 마이그레이션

This commit is contained in:
static
2025-12-25 22:45:55 +09:00
parent a08ddf2c09
commit 6d95059450
45 changed files with 691 additions and 1097 deletions

View File

@@ -1,8 +1,7 @@
import { callPostApi } from "$lib/hooks";
import { encryptData } from "$lib/modules/crypto";
import { storeFileThumbnailCache } from "$lib/modules/file";
import type { CategoryFileAddRequest } from "$lib/server/schemas";
import { requestFileThumbnailUpload } from "$lib/services/file";
import { useTRPC } from "$trpc/client";
export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category";
export { requestFileDownload } from "$lib/services/file";
@@ -23,8 +22,13 @@ export const requestThumbnailUpload = async (
};
export const requestFileAdditionToCategory = async (fileId: number, categoryId: number) => {
const res = await callPostApi<CategoryFileAddRequest>(`/api/category/${categoryId}/file/add`, {
file: fileId,
});
return res.ok;
const trpc = useTRPC();
try {
await trpc.category.addFile.mutate({ id: categoryId, file: fileId });
return true;
} catch {
// TODO: Error Handling
return false;
}
};

View File

@@ -1,14 +1,14 @@
import { error } from "@sveltejs/kit";
import { callPostApi } from "$lib/hooks";
import type { MissingThumbnailFileScanResponse } from "$lib/server/schemas";
import { useTRPC } from "$trpc/client";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch }) => {
const res = await callPostApi("/api/file/scanMissingThumbnails", undefined, fetch);
if (!res.ok) {
const trpc = useTRPC(fetch);
try {
const files = await trpc.file.listWithoutThumbnail.query();
return { files };
} catch {
error(500, "Internal server error");
}
const { files }: MissingThumbnailFileScanResponse = await res.json();
return { files };
};

View File

@@ -1,8 +1,7 @@
import { getContext, setContext } from "svelte";
import { callPostApi } from "$lib/hooks";
import { encryptString } from "$lib/modules/crypto";
import type { SelectedCategory } from "$lib/components/molecules";
import type { CategoryRenameRequest } from "$lib/server/schemas";
import { useTRPC } from "$trpc/client";
export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category";
@@ -18,17 +17,31 @@ export const useContext = () => {
};
export const requestCategoryRename = async (category: SelectedCategory, newName: string) => {
const trpc = useTRPC();
const newNameEncrypted = await encryptString(newName, category.dataKey);
const res = await callPostApi<CategoryRenameRequest>(`/api/category/${category.id}/rename`, {
dekVersion: category.dataKeyVersion.toISOString(),
name: newNameEncrypted.ciphertext,
nameIv: newNameEncrypted.iv,
});
return res.ok;
try {
await trpc.category.rename.mutate({
id: category.id,
dekVersion: category.dataKeyVersion,
name: newNameEncrypted.ciphertext,
nameIv: newNameEncrypted.iv,
});
return true;
} catch {
// TODO: Error Handling
return false;
}
};
export const requestCategoryDeletion = async (category: SelectedCategory) => {
const res = await callPostApi(`/api/category/${category.id}/delete`);
return res.ok;
const trpc = useTRPC();
try {
await trpc.category.delete.mutate({ id: category.id });
return true;
} catch {
// TODO: Error Handling
return false;
}
};

View File

@@ -1,5 +1,4 @@
import { getContext, setContext } from "svelte";
import { callPostApi } from "$lib/hooks";
import { storeHmacSecrets } from "$lib/indexedDB";
import { generateDataKey, wrapDataKey, unwrapHmacSecret, encryptString } from "$lib/modules/crypto";
import {
@@ -9,12 +8,6 @@ import {
deleteFileThumbnailCache,
uploadFile,
} from "$lib/modules/file";
import type {
DirectoryRenameRequest,
DirectoryCreateRequest,
FileRenameRequest,
DirectoryDeleteResponse,
} from "$lib/server/schemas";
import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores";
import { useTRPC } from "$trpc/client";
@@ -68,18 +61,24 @@ export const requestDirectoryCreation = async (
parentId: "root" | number,
masterKey: MasterKey,
) => {
const trpc = useTRPC();
const { dataKey, dataKeyVersion } = await generateDataKey();
const nameEncrypted = await encryptString(name, dataKey);
const res = await callPostApi<DirectoryCreateRequest>("/api/directory/create", {
parent: parentId,
mekVersion: masterKey.version,
dek: await wrapDataKey(dataKey, masterKey.key),
dekVersion: dataKeyVersion.toISOString(),
name: nameEncrypted.ciphertext,
nameIv: nameEncrypted.iv,
});
return res.ok;
try {
await trpc.directory.create.mutate({
parent: parentId,
mekVersion: masterKey.version,
dek: await wrapDataKey(dataKey, masterKey.key),
dekVersion: dataKeyVersion,
name: nameEncrypted.ciphertext,
nameIv: nameEncrypted.iv,
});
return true;
} catch {
// TODO: Error Handling
return false;
}
};
export const requestFileUpload = async (
@@ -101,37 +100,51 @@ export const requestFileUpload = async (
};
export const requestEntryRename = async (entry: SelectedEntry, newName: string) => {
const trpc = useTRPC();
const newNameEncrypted = await encryptString(newName, entry.dataKey);
let res;
if (entry.type === "directory") {
res = await callPostApi<DirectoryRenameRequest>(`/api/directory/${entry.id}/rename`, {
dekVersion: entry.dataKeyVersion.toISOString(),
name: newNameEncrypted.ciphertext,
nameIv: newNameEncrypted.iv,
});
} else {
res = await callPostApi<FileRenameRequest>(`/api/file/${entry.id}/rename`, {
dekVersion: entry.dataKeyVersion.toISOString(),
name: newNameEncrypted.ciphertext,
nameIv: newNameEncrypted.iv,
});
try {
if (entry.type === "directory") {
await trpc.directory.rename.mutate({
id: entry.id,
dekVersion: entry.dataKeyVersion,
name: newNameEncrypted.ciphertext,
nameIv: newNameEncrypted.iv,
});
} else {
await trpc.file.rename.mutate({
id: entry.id,
dekVersion: entry.dataKeyVersion,
name: newNameEncrypted.ciphertext,
nameIv: newNameEncrypted.iv,
});
}
return true;
} catch {
// TODO: Error Handling
return false;
}
return res.ok;
};
export const requestEntryDeletion = async (entry: SelectedEntry) => {
const res = await callPostApi(`/api/${entry.type}/${entry.id}/delete`);
if (!res.ok) return false;
const trpc = useTRPC();
if (entry.type === "directory") {
const { deletedFiles }: DirectoryDeleteResponse = await res.json();
await Promise.all(
deletedFiles.flatMap((fileId) => [deleteFileCache(fileId), deleteFileThumbnailCache(fileId)]),
);
return true;
} else {
await Promise.all([deleteFileCache(entry.id), deleteFileThumbnailCache(entry.id)]);
try {
if (entry.type === "directory") {
const { deletedFiles } = await trpc.directory.delete.mutate({ id: entry.id });
await Promise.all(
deletedFiles.flatMap((fileId) => [
deleteFileCache(fileId),
deleteFileThumbnailCache(fileId),
]),
);
} else {
await trpc.file.delete.mutate({ id: entry.id });
await Promise.all([deleteFileCache(entry.id), deleteFileThumbnailCache(entry.id)]);
}
return true;
} catch {
// TODO: Error Handling
return false;
}
};

View File

@@ -6,7 +6,7 @@ export const load: PageLoad = async ({ fetch }) => {
const trpc = useTRPC(fetch);
try {
const { nickname } = await trpc.user.info.query();
const { nickname } = await trpc.user.get.query();
return { nickname };
} catch {
error(500, "Internal server error");

View File

@@ -1,33 +0,0 @@
import { error, json } from "@sveltejs/kit";
import { z } from "zod";
import { authorize } from "$lib/server/modules/auth";
import { categoryInfoResponse, type CategoryInfoResponse } from "$lib/server/schemas";
import { getCategoryInformation } from "$lib/server/services/category";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals, params }) => {
const { userId } = await authorize(locals, "activeClient");
const zodRes = z
.object({
id: z.union([z.enum(["root"]), z.coerce.number().int().positive()]),
})
.safeParse(params);
if (!zodRes.success) error(400, "Invalid path parameters");
const { id } = zodRes.data;
const { metadata, categories } = await getCategoryInformation(userId, id);
return json(
categoryInfoResponse.parse({
metadata: metadata && {
parent: metadata.parentId,
mekVersion: metadata.mekVersion,
dek: metadata.encDek,
dekVersion: metadata.dekVersion.toISOString(),
name: metadata.encName.ciphertext,
nameIv: metadata.encName.iv,
},
subCategories: categories,
} satisfies CategoryInfoResponse),
);
};

View File

@@ -1,20 +0,0 @@
import { error, text } from "@sveltejs/kit";
import { z } from "zod";
import { authorize } from "$lib/server/modules/auth";
import { deleteCategory } from "$lib/server/services/category";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, params }) => {
const { userId } = await authorize(locals, "activeClient");
const zodRes = z
.object({
id: z.coerce.number().int().positive(),
})
.safeParse(params);
if (!zodRes.success) error(400, "Invalid path parameters");
const { id } = zodRes.data;
await deleteCategory(userId, id);
return text("Category deleted", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -1,25 +0,0 @@
import { error, text } from "@sveltejs/kit";
import { z } from "zod";
import { authorize } from "$lib/server/modules/auth";
import { categoryFileAddRequest } from "$lib/server/schemas";
import { addCategoryFile } from "$lib/server/services/category";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, params, request }) => {
const { userId } = await authorize(locals, "activeClient");
const paramsZodRes = z
.object({
id: z.coerce.number().int().positive(),
})
.safeParse(params);
if (!paramsZodRes.success) error(400, "Invalid path parameters");
const { id } = paramsZodRes.data;
const bodyZodRes = categoryFileAddRequest.safeParse(await request.json());
if (!bodyZodRes.success) error(400, "Invalid request body");
const { file } = bodyZodRes.data;
await addCategoryFile(userId, id, file);
return text("File added", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -1,36 +0,0 @@
import { error, json } from "@sveltejs/kit";
import { z } from "zod";
import { authorize } from "$lib/server/modules/auth";
import { categoryFileListResponse, type CategoryFileListResponse } from "$lib/server/schemas";
import { getCategoryFiles } from "$lib/server/services/category";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals, url, params }) => {
const { userId } = await authorize(locals, "activeClient");
const paramsZodRes = z
.object({
id: z.coerce.number().int().positive(),
})
.safeParse(params);
if (!paramsZodRes.success) error(400, "Invalid path parameters");
const { id } = paramsZodRes.data;
const queryZodRes = z
.object({
recurse: z
.enum(["true", "false"])
.transform((value) => value === "true")
.nullable(),
})
.safeParse({ recurse: url.searchParams.get("recurse") });
if (!queryZodRes.success) error(400, "Invalid query parameters");
const { recurse } = queryZodRes.data;
const { files } = await getCategoryFiles(userId, id, recurse ?? false);
return json(
categoryFileListResponse.parse({
files: files.map(({ id, isRecursive }) => ({ file: id, isRecursive })),
} satisfies CategoryFileListResponse),
);
};

View File

@@ -1,25 +0,0 @@
import { error, text } from "@sveltejs/kit";
import { z } from "zod";
import { authorize } from "$lib/server/modules/auth";
import { categoryFileRemoveRequest } from "$lib/server/schemas";
import { removeCategoryFile } from "$lib/server/services/category";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, params, request }) => {
const { userId } = await authorize(locals, "activeClient");
const paramsZodRes = z
.object({
id: z.coerce.number().int().positive(),
})
.safeParse(params);
if (!paramsZodRes.success) error(400, "Invalid path parameters");
const { id } = paramsZodRes.data;
const bodyZodRes = categoryFileRemoveRequest.safeParse(await request.json());
if (!bodyZodRes.success) error(400, "Invalid request body");
const { file } = bodyZodRes.data;
await removeCategoryFile(userId, id, file);
return text("File removed", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -1,25 +0,0 @@
import { error, text } from "@sveltejs/kit";
import { z } from "zod";
import { authorize } from "$lib/server/modules/auth";
import { categoryRenameRequest } from "$lib/server/schemas";
import { renameCategory } from "$lib/server/services/category";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, params, request }) => {
const { userId } = await authorize(locals, "activeClient");
const paramsZodRes = z
.object({
id: z.coerce.number().int().positive(),
})
.safeParse(params);
if (!paramsZodRes.success) error(400, "Invalid path parameters");
const { id } = paramsZodRes.data;
const bodyZodRes = categoryRenameRequest.safeParse(await request.json());
if (!bodyZodRes.success) error(400, "Invalid request body");
const { dekVersion, name, nameIv } = bodyZodRes.data;
await renameCategory(userId, id, new Date(dekVersion), { ciphertext: name, iv: nameIv });
return text("Category renamed", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -1,23 +0,0 @@
import { error, text } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import { categoryCreateRequest } from "$lib/server/schemas";
import { createCategory } from "$lib/server/services/category";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, request }) => {
const { userId } = await authorize(locals, "activeClient");
const zodRes = categoryCreateRequest.safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body");
const { parent, mekVersion, dek, dekVersion, name, nameIv } = zodRes.data;
await createCategory({
userId,
parentId: parent,
mekVersion,
encDek: dek,
dekVersion: new Date(dekVersion),
encName: { ciphertext: name, iv: nameIv },
});
return text("Category created", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -1,34 +0,0 @@
import { error, json } from "@sveltejs/kit";
import { z } from "zod";
import { authorize } from "$lib/server/modules/auth";
import { directoryInfoResponse, type DirectoryInfoResponse } from "$lib/server/schemas";
import { getDirectoryInformation } from "$lib/server/services/directory";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals, params }) => {
const { userId } = await authorize(locals, "activeClient");
const zodRes = z
.object({
id: z.union([z.enum(["root"]), z.coerce.number().int().positive()]),
})
.safeParse(params);
if (!zodRes.success) error(400, "Invalid path parameters");
const { id } = zodRes.data;
const { metadata, directories, files } = await getDirectoryInformation(userId, id);
return json(
directoryInfoResponse.parse({
metadata: metadata && {
parent: metadata.parentId,
mekVersion: metadata.mekVersion,
dek: metadata.encDek,
dekVersion: metadata.dekVersion.toISOString(),
name: metadata.encName.ciphertext,
nameIv: metadata.encName.iv,
},
subDirectories: directories,
files,
} satisfies DirectoryInfoResponse),
);
};

View File

@@ -1,23 +0,0 @@
import { error, json } from "@sveltejs/kit";
import { z } from "zod";
import { authorize } from "$lib/server/modules/auth";
import { directoryDeleteResponse, type DirectoryDeleteResponse } from "$lib/server/schemas";
import { deleteDirectory } from "$lib/server/services/directory";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, params }) => {
const { userId } = await authorize(locals, "activeClient");
const zodRes = z
.object({
id: z.coerce.number().int().positive(),
})
.safeParse(params);
if (!zodRes.success) error(400, "Invalid path parameters");
const { id } = zodRes.data;
const { files } = await deleteDirectory(userId, id);
return json(
directoryDeleteResponse.parse({ deletedFiles: files } satisfies DirectoryDeleteResponse),
);
};

View File

@@ -1,25 +0,0 @@
import { error, text } from "@sveltejs/kit";
import { z } from "zod";
import { authorize } from "$lib/server/modules/auth";
import { directoryRenameRequest } from "$lib/server/schemas";
import { renameDirectory } from "$lib/server/services/directory";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, params, request }) => {
const { userId } = await authorize(locals, "activeClient");
const paramsZodRes = z
.object({
id: z.coerce.number().int().positive(),
})
.safeParse(params);
if (!paramsZodRes.success) error(400, "Invalid path parameters");
const { id } = paramsZodRes.data;
const bodyZodRes = directoryRenameRequest.safeParse(await request.json());
if (!bodyZodRes.success) error(400, "Invalid request body");
const { dekVersion, name, nameIv } = bodyZodRes.data;
await renameDirectory(userId, id, new Date(dekVersion), { ciphertext: name, iv: nameIv });
return text("Directory renamed", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -1,23 +0,0 @@
import { error, text } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import { directoryCreateRequest } from "$lib/server/schemas";
import { createDirectory } from "$lib/server/services/directory";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, request }) => {
const { userId } = await authorize(locals, "activeClient");
const zodRes = directoryCreateRequest.safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body");
const { parent, mekVersion, dek, dekVersion, name, nameIv } = zodRes.data;
await createDirectory({
userId,
parentId: parent,
mekVersion,
encDek: dek,
dekVersion: new Date(dekVersion),
encName: { ciphertext: name, iv: nameIv },
});
return text("Directory created", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -1,48 +0,0 @@
import { error, json } from "@sveltejs/kit";
import { z } from "zod";
import { authorize } from "$lib/server/modules/auth";
import { fileInfoResponse, type FileInfoResponse } from "$lib/server/schemas";
import { getFileInformation } from "$lib/server/services/file";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals, params }) => {
const { userId } = await authorize(locals, "activeClient");
const zodRes = z
.object({
id: z.coerce.number().int().positive(),
})
.safeParse(params);
if (!zodRes.success) error(400, "Invalid path parameters");
const { id } = zodRes.data;
const {
parentId,
mekVersion,
encDek,
dekVersion,
contentType,
encContentIv,
encName,
encCreatedAt,
encLastModifiedAt,
categories,
} = await getFileInformation(userId, id);
return json(
fileInfoResponse.parse({
parent: parentId,
mekVersion,
dek: encDek,
dekVersion: dekVersion.toISOString(),
contentType: contentType,
contentIv: encContentIv,
name: encName.ciphertext,
nameIv: encName.iv,
createdAt: encCreatedAt?.ciphertext,
createdAtIv: encCreatedAt?.iv,
lastModifiedAt: encLastModifiedAt.ciphertext,
lastModifiedAtIv: encLastModifiedAt.iv,
categories,
} satisfies FileInfoResponse),
);
};

View File

@@ -1,20 +0,0 @@
import { error, text } from "@sveltejs/kit";
import { z } from "zod";
import { authorize } from "$lib/server/modules/auth";
import { deleteFile } from "$lib/server/services/file";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, params }) => {
const { userId } = await authorize(locals, "activeClient");
const zodRes = z
.object({
id: z.coerce.number().int().positive(),
})
.safeParse(params);
if (!zodRes.success) error(400, "Invalid path parameters");
const { id } = zodRes.data;
await deleteFile(userId, id);
return text("File deleted", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -1,25 +0,0 @@
import { error, text } from "@sveltejs/kit";
import { z } from "zod";
import { authorize } from "$lib/server/modules/auth";
import { fileRenameRequest } from "$lib/server/schemas";
import { renameFile } from "$lib/server/services/file";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, params, request }) => {
const { userId } = await authorize(locals, "activeClient");
const paramsZodRes = z
.object({
id: z.coerce.number().int().positive(),
})
.safeParse(params);
if (!paramsZodRes.success) error(400, "Invalid path parameters");
const { id } = paramsZodRes.data;
const bodyZodRes = fileRenameRequest.safeParse(await request.json());
if (!bodyZodRes.success) error(400, "Invalid request body");
const { dekVersion, name, nameIv } = bodyZodRes.data;
await renameFile(userId, id, new Date(dekVersion), { ciphertext: name, iv: nameIv });
return text("File renamed", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -1,26 +0,0 @@
import { error, json } from "@sveltejs/kit";
import { z } from "zod";
import { authorize } from "$lib/server/modules/auth";
import { fileThumbnailInfoResponse, type FileThumbnailInfoResponse } from "$lib/server/schemas";
import { getFileThumbnailInformation } from "$lib/server/services/file";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals, params }) => {
const { userId } = await authorize(locals, "activeClient");
const zodRes = z
.object({
id: z.coerce.number().int().positive(),
})
.safeParse(params);
if (!zodRes.success) error(400, "Invalid path parameters");
const { id } = zodRes.data;
const { updatedAt, encContentIv } = await getFileThumbnailInformation(userId, id);
return json(
fileThumbnailInfoResponse.parse({
updatedAt: updatedAt.toISOString(),
contentIv: encContentIv,
} satisfies FileThumbnailInfoResponse),
);
};

View File

@@ -1,11 +0,0 @@
import { json } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import { fileListResponse, type FileListResponse } from "$lib/server/schemas";
import { getFileList } from "$lib/server/services/file";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals }) => {
const { userId } = await authorize(locals, "activeClient");
const { files } = await getFileList(userId);
return json(fileListResponse.parse({ files } satisfies FileListResponse));
};

View File

@@ -1,20 +0,0 @@
import { error, json } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import {
duplicateFileScanRequest,
duplicateFileScanResponse,
type DuplicateFileScanResponse,
} from "$lib/server/schemas";
import { scanDuplicateFiles } from "$lib/server/services/file";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, request }) => {
const { userId } = await authorize(locals, "activeClient");
const zodRes = duplicateFileScanRequest.safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body");
const { hskVersion, contentHmac } = zodRes.data;
const { files } = await scanDuplicateFiles(userId, hskVersion, contentHmac);
return json(duplicateFileScanResponse.parse({ files } satisfies DuplicateFileScanResponse));
};

View File

@@ -1,16 +0,0 @@
import { json } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import {
missingThumbnailFileScanResponse,
type MissingThumbnailFileScanResponse,
} from "$lib/server/schemas/file";
import { scanMissingFileThumbnails } from "$lib/server/services/file";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals }) => {
const { userId } = await authorize(locals, "activeClient");
const { files } = await scanMissingFileThumbnails(userId);
return json(
missingThumbnailFileScanResponse.parse({ files } satisfies MissingThumbnailFileScanResponse),
);
};