diff --git a/package.json b/package.json index 4a8493b..b1425b0 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "ms": "^2.1.3", "node-schedule": "^2.1.1", "pg": "^8.16.3", + "superjson": "^2.2.6", "uuid": "^13.0.0", "zod": "^3.25.76" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50affd7..b428b0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: pg: specifier: ^8.16.3 version: 8.16.3 + superjson: + specifier: ^2.2.6 + version: 2.2.6 uuid: specifier: ^13.0.0 version: 13.0.0 @@ -889,6 +892,10 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} @@ -1259,6 +1266,10 @@ packages: is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1854,6 +1865,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2767,6 +2782,10 @@ snapshots: cookie@0.6.0: {} + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + cron-parser@4.9.0: dependencies: luxon: 3.7.2 @@ -3153,6 +3172,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + is-what@5.5.0: {} + isexe@2.0.0: {} jackspeak@3.4.3: @@ -3641,6 +3662,10 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 diff --git a/src/lib/modules/file/upload.ts b/src/lib/modules/file/upload.ts index b5b00a1..7c48503 100644 --- a/src/lib/modules/file/upload.ts +++ b/src/lib/modules/file/upload.ts @@ -13,8 +13,6 @@ import { } from "$lib/modules/crypto"; import { generateThumbnail } from "$lib/modules/thumbnail"; import type { - DuplicateFileScanRequest, - DuplicateFileScanResponse, FileThumbnailUploadRequest, FileUploadRequest, FileUploadResponse, @@ -25,18 +23,18 @@ import { type HmacSecret, type FileUploadStatus, } from "$lib/stores"; +import { useTRPC } from "$trpc/client"; const requestDuplicateFileScan = limitFunction( async (file: File, hmacSecret: HmacSecret, onDuplicate: () => Promise) => { + const trpc = useTRPC(); const fileBuffer = await file.arrayBuffer(); const fileSigned = encodeToBase64(await signMessageHmac(fileBuffer, hmacSecret.secret)); - const res = await axios.post("/api/file/scanDuplicates", { + const files = await trpc.file.listByHash.query({ hskVersion: hmacSecret.version, contentHmac: fileSigned, - } satisfies DuplicateFileScanRequest); - const { files }: DuplicateFileScanResponse = res.data; - + }); if (files.length === 0 || (await onDuplicate())) { return { fileBuffer, fileSigned }; } else { diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index c160534..8b9b203 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -1,5 +1,5 @@ +import { TRPCClientError } from "@trpc/client"; import { get, writable, type Writable } from "svelte/store"; -import { callGetApi } from "$lib/hooks"; import { getDirectoryInfos as getDirectoryInfosFromIndexedDB, getDirectoryInfo as getDirectoryInfoFromIndexedDB, @@ -18,12 +18,7 @@ import { type CategoryId, } from "$lib/indexedDB"; import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; -import type { - CategoryInfoResponse, - CategoryFileListResponse, - DirectoryInfoResponse, - FileInfoResponse, -} from "$lib/server/schemas"; +import { useTRPC } from "$trpc/client"; export type DirectoryInfo = | { @@ -106,20 +101,20 @@ const fetchDirectoryInfoFromServer = async ( info: Writable, masterKey: CryptoKey, ) => { - const res = await callGetApi(`/api/directory/${id}`); - if (res.status === 404) { - info.set(null); - await deleteDirectoryInfo(id as number); - return; - } else if (!res.ok) { + const trpc = useTRPC(); + let data; + try { + data = await trpc.directory.get.query({ id }); + } catch (e) { + if (e instanceof TRPCClientError && e.data?.code === "NOT_FOUND") { + info.set(null); + await deleteDirectoryInfo(id as number); + return; + } throw new Error("Failed to fetch directory information"); } - const { - metadata, - subDirectories: subDirectoryIds, - files: fileIds, - }: DirectoryInfoResponse = await res.json(); + const { metadata, subDirectories: subDirectoryIds, files: fileIds } = data; if (id === "root") { info.set({ id, subDirectoryIds, fileIds }); @@ -179,16 +174,18 @@ const fetchFileInfoFromServer = async ( info: Writable, masterKey: CryptoKey, ) => { - const res = await callGetApi(`/api/file/${id}`); - if (res.status === 404) { - info.set(null); - await deleteFileInfo(id); - return; - } else if (!res.ok) { + const trpc = useTRPC(); + let metadata; + try { + metadata = await trpc.file.get.query({ id }); + } catch (e) { + if (e instanceof TRPCClientError && e.data?.code === "NOT_FOUND") { + info.set(null); + await deleteFileInfo(id); + return; + } throw new Error("Failed to fetch file information"); } - - const metadata: FileInfoResponse = await res.json(); const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); const name = await decryptString(metadata.name, metadata.nameIv, dataKey); @@ -273,16 +270,20 @@ const fetchCategoryInfoFromServer = async ( info: Writable, masterKey: CryptoKey, ) => { - let res = await callGetApi(`/api/category/${id}`); - if (res.status === 404) { - info.set(null); - await deleteCategoryInfo(id as number); - return; - } else if (!res.ok) { + const trpc = useTRPC(); + let data; + try { + data = await trpc.category.get.query({ id }); + } catch (e) { + if (e instanceof TRPCClientError && e.data?.code === "NOT_FOUND") { + info.set(null); + await deleteCategoryInfo(id as number); + return; + } throw new Error("Failed to fetch category information"); } - const { metadata, subCategories }: CategoryInfoResponse = await res.json(); + const { metadata, subCategories } = data; if (id === "root") { info.set({ id, subCategoryIds: subCategories }); @@ -290,12 +291,13 @@ const fetchCategoryInfoFromServer = async ( const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey); const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey); - res = await callGetApi(`/api/category/${id}/file/list?recurse=true`); - if (!res.ok) { + let files; + try { + files = await trpc.category.files.query({ id, recurse: true }); + } catch { throw new Error("Failed to fetch category files"); } - const { files }: CategoryFileListResponse = await res.json(); const filesMapped = files.map(({ file, isRecursive }) => ({ id: file, isRecursive })); let isFileRecursive: boolean | undefined = undefined; diff --git a/src/lib/server/db/user.ts b/src/lib/server/db/user.ts index 3964a94..f718d4f 100644 --- a/src/lib/server/db/user.ts +++ b/src/lib/server/db/user.ts @@ -27,10 +27,6 @@ export const getUserByEmail = async (email: string) => { return user ? (user satisfies User) : null; }; -export const setUserNickname = async (userId: number, nickname: string) => { - await db.updateTable("user").set({ nickname }).where("id", "=", userId).execute(); -}; - export const setUserPassword = async (userId: number, password: string) => { await db.updateTable("user").set({ password }).where("id", "=", userId).execute(); }; diff --git a/src/lib/server/modules/filesystem.ts b/src/lib/server/modules/filesystem.ts new file mode 100644 index 0000000..65cb9ec --- /dev/null +++ b/src/lib/server/modules/filesystem.ts @@ -0,0 +1,7 @@ +import { unlink } from "fs/promises"; + +export const safeUnlink = async (path: string | null | undefined) => { + if (path) { + await unlink(path).catch(console.error); + } +}; diff --git a/src/lib/server/schemas/category.ts b/src/lib/server/schemas/category.ts index 55ae413..408af7b 100644 --- a/src/lib/server/schemas/category.ts +++ b/src/lib/server/schemas/category.ts @@ -1,55 +1,3 @@ import { z } from "zod"; export const categoryIdSchema = z.union([z.literal("root"), z.number().int().positive()]); - -export const categoryInfoResponse = z.object({ - metadata: z - .object({ - parent: categoryIdSchema, - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), - }) - .optional(), - subCategories: z.number().int().positive().array(), -}); -export type CategoryInfoResponse = z.output; - -export const categoryFileAddRequest = z.object({ - file: z.number().int().positive(), -}); -export type CategoryFileAddRequest = z.input; - -export const categoryFileListResponse = z.object({ - files: z.array( - z.object({ - file: z.number().int().positive(), - isRecursive: z.boolean(), - }), - ), -}); -export type CategoryFileListResponse = z.output; - -export const categoryFileRemoveRequest = z.object({ - file: z.number().int().positive(), -}); -export type CategoryFileRemoveRequest = z.input; - -export const categoryRenameRequest = z.object({ - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), -}); -export type CategoryRenameRequest = z.input; - -export const categoryCreateRequest = z.object({ - parent: categoryIdSchema, - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), -}); -export type CategoryCreateRequest = z.input; diff --git a/src/lib/server/schemas/directory.ts b/src/lib/server/schemas/directory.ts index ffd13bc..107c3ee 100644 --- a/src/lib/server/schemas/directory.ts +++ b/src/lib/server/schemas/directory.ts @@ -1,41 +1,3 @@ import { z } from "zod"; export const directoryIdSchema = z.union([z.literal("root"), z.number().int().positive()]); - -export const directoryInfoResponse = z.object({ - metadata: z - .object({ - parent: directoryIdSchema, - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), - }) - .optional(), - subDirectories: z.number().int().positive().array(), - files: z.number().int().positive().array(), -}); -export type DirectoryInfoResponse = z.output; - -export const directoryDeleteResponse = z.object({ - deletedFiles: z.number().int().positive().array(), -}); -export type DirectoryDeleteResponse = z.output; - -export const directoryRenameRequest = z.object({ - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), -}); -export type DirectoryRenameRequest = z.input; - -export const directoryCreateRequest = z.object({ - parent: directoryIdSchema, - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), -}); -export type DirectoryCreateRequest = z.input; diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index 8b9cfe9..5177bbd 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -2,67 +2,12 @@ import mime from "mime"; import { z } from "zod"; import { directoryIdSchema } from "./directory"; -export const fileInfoResponse = z.object({ - parent: directoryIdSchema, - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), - dekVersion: z.string().datetime(), - contentType: z - .string() - .trim() - .nonempty() - .refine((value) => mime.getExtension(value) !== null), // MIME type - contentIv: z.string().base64().nonempty(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), - createdAt: z.string().base64().nonempty().optional(), - createdAtIv: z.string().base64().nonempty().optional(), - lastModifiedAt: z.string().base64().nonempty(), - lastModifiedAtIv: z.string().base64().nonempty(), - categories: z.number().int().positive().array(), -}); -export type FileInfoResponse = z.output; - -export const fileRenameRequest = z.object({ - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), -}); -export type FileRenameRequest = z.input; - -export const fileThumbnailInfoResponse = z.object({ - updatedAt: z.string().datetime(), - contentIv: z.string().base64().nonempty(), -}); -export type FileThumbnailInfoResponse = z.output; - export const fileThumbnailUploadRequest = z.object({ dekVersion: z.string().datetime(), contentIv: z.string().base64().nonempty(), }); export type FileThumbnailUploadRequest = z.input; -export const fileListResponse = z.object({ - files: z.number().int().positive().array(), -}); -export type FileListResponse = z.output; - -export const duplicateFileScanRequest = z.object({ - hskVersion: z.number().int().positive(), - contentHmac: z.string().base64().nonempty(), -}); -export type DuplicateFileScanRequest = z.input; - -export const duplicateFileScanResponse = z.object({ - files: z.number().int().positive().array(), -}); -export type DuplicateFileScanResponse = z.output; - -export const missingThumbnailFileScanResponse = z.object({ - files: z.number().int().positive().array(), -}); -export type MissingThumbnailFileScanResponse = z.output; - export const fileUploadRequest = z.object({ parent: directoryIdSchema, mekVersion: z.number().int().positive(), diff --git a/src/lib/server/services/category.ts b/src/lib/server/services/category.ts deleted file mode 100644 index cb3db7a..0000000 --- a/src/lib/server/services/category.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { error } from "@sveltejs/kit"; -import { - registerCategory, - getAllCategoriesByParent, - getCategory, - setCategoryEncName, - unregisterCategory, - type CategoryId, - type NewCategory, -} from "$lib/server/db/category"; -import { IntegrityError } from "$lib/server/db/error"; -import { - getAllFilesByCategory, - getFile, - addFileToCategory, - removeFileFromCategory, -} from "$lib/server/db/file"; -import type { Ciphertext } from "$lib/server/db/schema"; - -export const getCategoryInformation = async (userId: number, categoryId: CategoryId) => { - const category = categoryId !== "root" ? await getCategory(userId, categoryId) : undefined; - if (category === null) { - error(404, "Invalid category id"); - } - - const categories = await getAllCategoriesByParent(userId, categoryId); - return { - metadata: category && { - parentId: category.parentId ?? ("root" as const), - mekVersion: category.mekVersion, - encDek: category.encDek, - dekVersion: category.dekVersion, - encName: category.encName, - }, - categories: categories.map(({ id }) => id), - }; -}; - -export const deleteCategory = async (userId: number, categoryId: number) => { - try { - await unregisterCategory(userId, categoryId); - } catch (e) { - if (e instanceof IntegrityError && e.message === "Category not found") { - error(404, "Invalid category id"); - } - throw e; - } -}; - -export const addCategoryFile = async (userId: number, categoryId: number, fileId: number) => { - const category = await getCategory(userId, categoryId); - const file = await getFile(userId, fileId); - if (!category) { - error(404, "Invalid category id"); - } else if (!file) { - error(404, "Invalid file id"); - } - - try { - await addFileToCategory(fileId, categoryId); - } catch (e) { - if (e instanceof IntegrityError && e.message === "File already added to category") { - error(400, "File already added"); - } - throw e; - } -}; - -export const getCategoryFiles = async (userId: number, categoryId: number, recurse: boolean) => { - const category = await getCategory(userId, categoryId); - if (!category) { - error(404, "Invalid category id"); - } - - const files = await getAllFilesByCategory(userId, categoryId, recurse); - return { files }; -}; - -export const removeCategoryFile = async (userId: number, categoryId: number, fileId: number) => { - const category = await getCategory(userId, categoryId); - const file = await getFile(userId, fileId); - if (!category) { - error(404, "Invalid category id"); - } else if (!file) { - error(404, "Invalid file id"); - } - - try { - await removeFileFromCategory(fileId, categoryId); - } catch (e) { - if (e instanceof IntegrityError && e.message === "File not found in category") { - error(400, "File not added"); - } - throw e; - } -}; - -export const renameCategory = async ( - userId: number, - categoryId: number, - dekVersion: Date, - newEncName: Ciphertext, -) => { - try { - await setCategoryEncName(userId, categoryId, dekVersion, newEncName); - } catch (e) { - if (e instanceof IntegrityError) { - if (e.message === "Category not found") { - error(404, "Invalid category id"); - } else if (e.message === "Invalid DEK version") { - error(400, "Invalid DEK version"); - } - } - throw e; - } -}; - -export const createCategory = async (params: NewCategory) => { - const oneMinuteAgo = new Date(Date.now() - 60 * 1000); - const oneMinuteLater = new Date(Date.now() + 60 * 1000); - if (params.dekVersion <= oneMinuteAgo || params.dekVersion >= oneMinuteLater) { - error(400, "Invalid DEK version"); - } - - try { - await registerCategory(params); - } catch (e) { - if (e instanceof IntegrityError && e.message === "Inactive MEK version") { - error(400, "Inactive MEK version"); - } - throw e; - } -}; diff --git a/src/lib/server/services/directory.ts b/src/lib/server/services/directory.ts deleted file mode 100644 index fdab587..0000000 --- a/src/lib/server/services/directory.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { error } from "@sveltejs/kit"; -import { unlink } from "fs/promises"; -import { IntegrityError } from "$lib/server/db/error"; -import { - registerDirectory, - getAllDirectoriesByParent, - getDirectory, - setDirectoryEncName, - unregisterDirectory, - getAllFilesByParent, - type DirectoryId, - type NewDirectory, -} from "$lib/server/db/file"; -import type { Ciphertext } from "$lib/server/db/schema"; - -export const getDirectoryInformation = async (userId: number, directoryId: DirectoryId) => { - const directory = directoryId !== "root" ? await getDirectory(userId, directoryId) : undefined; - if (directory === null) { - error(404, "Invalid directory id"); - } - - const directories = await getAllDirectoriesByParent(userId, directoryId); - const files = await getAllFilesByParent(userId, directoryId); - return { - metadata: directory && { - parentId: directory.parentId ?? ("root" as const), - mekVersion: directory.mekVersion, - encDek: directory.encDek, - dekVersion: directory.dekVersion, - encName: directory.encName, - }, - directories: directories.map(({ id }) => id), - files: files.map(({ id }) => id), - }; -}; - -const safeUnlink = async (path: string | null) => { - if (path) { - await unlink(path).catch(console.error); - } -}; - -export const deleteDirectory = async (userId: number, directoryId: number) => { - try { - const files = await unregisterDirectory(userId, directoryId); - return { - files: files.map(({ id, path, thumbnailPath }) => { - safeUnlink(path); // Intended - safeUnlink(thumbnailPath); // Intended - return id; - }), - }; - } catch (e) { - if (e instanceof IntegrityError && e.message === "Directory not found") { - error(404, "Invalid directory id"); - } - throw e; - } -}; - -export const renameDirectory = async ( - userId: number, - directoryId: number, - dekVersion: Date, - newEncName: Ciphertext, -) => { - try { - await setDirectoryEncName(userId, directoryId, dekVersion, newEncName); - } catch (e) { - if (e instanceof IntegrityError) { - if (e.message === "Directory not found") { - error(404, "Invalid directory id"); - } else if (e.message === "Invalid DEK version") { - error(400, "Invalid DEK version"); - } - } - throw e; - } -}; - -export const createDirectory = async (params: NewDirectory) => { - const oneMinuteAgo = new Date(Date.now() - 60 * 1000); - const oneMinuteLater = new Date(Date.now() + 60 * 1000); - if (params.dekVersion <= oneMinuteAgo || params.dekVersion >= oneMinuteLater) { - error(400, "Invalid DEK version"); - } - - try { - await registerDirectory(params); - } catch (e) { - if (e instanceof IntegrityError && e.message === "Inactive MEK version") { - error(400, "Invalid MEK version"); - } - throw e; - } -}; diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts index ab98dbf..9032ffb 100644 --- a/src/lib/server/services/file.ts +++ b/src/lib/server/services/file.ts @@ -1,72 +1,17 @@ import { error } from "@sveltejs/kit"; import { createHash } from "crypto"; import { createReadStream, createWriteStream } from "fs"; -import { mkdir, stat, unlink } from "fs/promises"; +import { mkdir, stat } from "fs/promises"; import { dirname } from "path"; import { Readable } from "stream"; import { pipeline } from "stream/promises"; import { v4 as uuidv4 } from "uuid"; -import { IntegrityError } from "$lib/server/db/error"; -import { - registerFile, - getAllFileIds, - getAllFileIdsByContentHmac, - getFile, - setFileEncName, - unregisterFile, - getAllFileCategories, - type NewFile, -} from "$lib/server/db/file"; -import { - updateFileThumbnail, - getFileThumbnail, - getMissingFileThumbnails, -} from "$lib/server/db/media"; -import type { Ciphertext } from "$lib/server/db/schema"; +import { FileRepo, MediaRepo, IntegrityError } from "$lib/server/db"; import env from "$lib/server/loadenv"; - -export const getFileInformation = async (userId: number, fileId: number) => { - const file = await getFile(userId, fileId); - if (!file) { - error(404, "Invalid file id"); - } - - const categories = await getAllFileCategories(fileId); - return { - parentId: file.parentId ?? ("root" as const), - mekVersion: file.mekVersion, - encDek: file.encDek, - dekVersion: file.dekVersion, - contentType: file.contentType, - encContentIv: file.encContentIv, - encName: file.encName, - encCreatedAt: file.encCreatedAt, - encLastModifiedAt: file.encLastModifiedAt, - categories: categories.map(({ id }) => id), - }; -}; - -const safeUnlink = async (path: string | null) => { - if (path) { - await unlink(path).catch(console.error); - } -}; - -export const deleteFile = async (userId: number, fileId: number) => { - try { - const { path, thumbnailPath } = await unregisterFile(userId, fileId); - safeUnlink(path); // Intended - safeUnlink(thumbnailPath); // Intended - } catch (e) { - if (e instanceof IntegrityError && e.message === "File not found") { - error(404, "Invalid file id"); - } - throw e; - } -}; +import { safeUnlink } from "$lib/server/modules/filesystem"; export const getFileStream = async (userId: number, fileId: number) => { - const file = await getFile(userId, fileId); + const file = await FileRepo.getFile(userId, fileId); if (!file) { error(404, "Invalid file id"); } @@ -78,37 +23,8 @@ export const getFileStream = async (userId: number, fileId: number) => { }; }; -export const renameFile = async ( - userId: number, - fileId: number, - dekVersion: Date, - newEncName: Ciphertext, -) => { - try { - await setFileEncName(userId, fileId, dekVersion, newEncName); - } catch (e) { - if (e instanceof IntegrityError) { - if (e.message === "File not found") { - error(404, "Invalid file id"); - } else if (e.message === "Invalid DEK version") { - error(400, "Invalid DEK version"); - } - } - throw e; - } -}; - -export const getFileThumbnailInformation = async (userId: number, fileId: number) => { - const thumbnail = await getFileThumbnail(userId, fileId); - if (!thumbnail) { - error(404, "File or its thumbnail not found"); - } - - return { updatedAt: thumbnail.updatedAt, encContentIv: thumbnail.encContentIv }; -}; - export const getFileThumbnailStream = async (userId: number, fileId: number) => { - const thumbnail = await getFileThumbnail(userId, fileId); + const thumbnail = await MediaRepo.getFileThumbnail(userId, fileId); if (!thumbnail) { error(404, "File or its thumbnail not found"); } @@ -133,7 +49,13 @@ export const uploadFileThumbnail = async ( try { await pipeline(encContentStream, createWriteStream(path, { flags: "wx", mode: 0o600 })); - const oldPath = await updateFileThumbnail(userId, fileId, dekVersion, path, encContentIv); + const oldPath = await MediaRepo.updateFileThumbnail( + userId, + fileId, + dekVersion, + path, + encContentIv, + ); safeUnlink(oldPath); // Intended } catch (e) { await safeUnlink(path); @@ -149,27 +71,8 @@ export const uploadFileThumbnail = async ( } }; -export const getFileList = async (userId: number) => { - const fileIds = await getAllFileIds(userId); - return { files: fileIds }; -}; - -export const scanDuplicateFiles = async ( - userId: number, - hskVersion: number, - contentHmac: string, -) => { - const fileIds = await getAllFileIdsByContentHmac(userId, hskVersion, contentHmac); - return { files: fileIds }; -}; - -export const scanMissingFileThumbnails = async (userId: number) => { - const fileIds = await getMissingFileThumbnails(userId); - return { files: fileIds }; -}; - export const uploadFile = async ( - params: Omit, + params: Omit, encContentStream: Readable, encContentHash: Promise, ) => { @@ -201,7 +104,7 @@ export const uploadFile = async ( throw new Error("Invalid checksum"); } - const { id: fileId } = await registerFile({ + const { id: fileId } = await FileRepo.registerFile({ ...params, path, encContentHash: hash, diff --git a/src/lib/services/category.ts b/src/lib/services/category.ts index 587df3f..c86c93b 100644 --- a/src/lib/services/category.ts +++ b/src/lib/services/category.ts @@ -1,31 +1,40 @@ -import { callPostApi } from "$lib/hooks"; import { generateDataKey, wrapDataKey, encryptString } from "$lib/modules/crypto"; -import type { CategoryCreateRequest, CategoryFileRemoveRequest } from "$lib/server/schemas"; import type { MasterKey } from "$lib/stores"; +import { useTRPC } from "$trpc/client"; export const requestCategoryCreation = async ( name: string, parentId: "root" | number, masterKey: MasterKey, ) => { + const trpc = useTRPC(); const { dataKey, dataKeyVersion } = await generateDataKey(); const nameEncrypted = await encryptString(name, dataKey); - const res = await callPostApi("/api/category/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.category.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 requestFileRemovalFromCategory = async (fileId: number, categoryId: number) => { - const res = await callPostApi( - `/api/category/${categoryId}/file/remove`, - { file: fileId }, - ); - return res.ok; + const trpc = useTRPC(); + + try { + await trpc.category.removeFile.mutate({ id: categoryId, file: fileId }); + return true; + } catch { + // TODO: Error Handling + return false; + } }; diff --git a/src/lib/services/file.ts b/src/lib/services/file.ts index bab3dac..f428e97 100644 --- a/src/lib/services/file.ts +++ b/src/lib/services/file.ts @@ -11,11 +11,8 @@ import { downloadFile, } from "$lib/modules/file"; import { getThumbnailUrl } from "$lib/modules/thumbnail"; -import type { - FileThumbnailInfoResponse, - FileThumbnailUploadRequest, - FileListResponse, -} from "$lib/server/schemas"; +import type { FileThumbnailUploadRequest } from "$lib/server/schemas"; +import { useTRPC } from "$trpc/client"; export const requestFileDownload = async ( fileId: number, @@ -52,12 +49,17 @@ export const requestFileThumbnailDownload = async (fileId: number, dataKey?: Cry const cache = await getFileThumbnailCache(fileId); if (cache || !dataKey) return cache; - let res = await callGetApi(`/api/file/${fileId}/thumbnail`); - if (!res.ok) return null; + const trpc = useTRPC(); + let thumbnailInfo; + try { + thumbnailInfo = await trpc.file.thumbnail.query({ id: fileId }); + } catch { + // TODO: Error Handling + return null; + } + const { contentIv: thumbnailEncryptedIv } = thumbnailInfo; - const { contentIv: thumbnailEncryptedIv }: FileThumbnailInfoResponse = await res.json(); - - res = await callGetApi(`/api/file/${fileId}/thumbnail/download`); + const res = await callGetApi(`/api/file/${fileId}/thumbnail/download`); if (!res.ok) return null; const thumbnailEncrypted = await res.arrayBuffer(); @@ -68,10 +70,15 @@ export const requestFileThumbnailDownload = async (fileId: number, dataKey?: Cry }; export const requestDeletedFilesCleanup = async () => { - const res = await callGetApi("/api/file/list"); - if (!res.ok) return; + const trpc = useTRPC(); + let liveFiles; + try { + liveFiles = await trpc.file.list.query(); + } catch { + // TODO: Error Handling + return; + } - const { files: liveFiles }: FileListResponse = await res.json(); const liveFilesSet = new Set(liveFiles); const maybeCachedFiles = await getAllFileInfos(); diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts index 00614d6..73ca7f1 100644 --- a/src/routes/(fullscreen)/file/[id]/service.ts +++ b/src/routes/(fullscreen)/file/[id]/service.ts @@ -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(`/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; + } }; diff --git a/src/routes/(fullscreen)/settings/thumbnail/+page.ts b/src/routes/(fullscreen)/settings/thumbnail/+page.ts index a16cb8e..3bfa322 100644 --- a/src/routes/(fullscreen)/settings/thumbnail/+page.ts +++ b/src/routes/(fullscreen)/settings/thumbnail/+page.ts @@ -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 }; }; diff --git a/src/routes/(main)/category/[[id]]/service.svelte.ts b/src/routes/(main)/category/[[id]]/service.svelte.ts index b573041..824de8a 100644 --- a/src/routes/(main)/category/[[id]]/service.svelte.ts +++ b/src/routes/(main)/category/[[id]]/service.svelte.ts @@ -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(`/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; + } }; diff --git a/src/routes/(main)/directory/[[id]]/service.svelte.ts b/src/routes/(main)/directory/[[id]]/service.svelte.ts index 40394f7..72a8fdb 100644 --- a/src/routes/(main)/directory/[[id]]/service.svelte.ts +++ b/src/routes/(main)/directory/[[id]]/service.svelte.ts @@ -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("/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(`/api/directory/${entry.id}/rename`, { - dekVersion: entry.dataKeyVersion.toISOString(), - name: newNameEncrypted.ciphertext, - nameIv: newNameEncrypted.iv, - }); - } else { - res = await callPostApi(`/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; } }; diff --git a/src/routes/(main)/menu/+page.ts b/src/routes/(main)/menu/+page.ts index b1582e7..ecd8f0b 100644 --- a/src/routes/(main)/menu/+page.ts +++ b/src/routes/(main)/menu/+page.ts @@ -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"); diff --git a/src/routes/api/category/[id]/+server.ts b/src/routes/api/category/[id]/+server.ts deleted file mode 100644 index 4a486fa..0000000 --- a/src/routes/api/category/[id]/+server.ts +++ /dev/null @@ -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), - ); -}; diff --git a/src/routes/api/category/[id]/delete/+server.ts b/src/routes/api/category/[id]/delete/+server.ts deleted file mode 100644 index cbbe356..0000000 --- a/src/routes/api/category/[id]/delete/+server.ts +++ /dev/null @@ -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" } }); -}; diff --git a/src/routes/api/category/[id]/file/add/+server.ts b/src/routes/api/category/[id]/file/add/+server.ts deleted file mode 100644 index 2eaf2f2..0000000 --- a/src/routes/api/category/[id]/file/add/+server.ts +++ /dev/null @@ -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" } }); -}; diff --git a/src/routes/api/category/[id]/file/list/+server.ts b/src/routes/api/category/[id]/file/list/+server.ts deleted file mode 100644 index e354d8b..0000000 --- a/src/routes/api/category/[id]/file/list/+server.ts +++ /dev/null @@ -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), - ); -}; diff --git a/src/routes/api/category/[id]/file/remove/+server.ts b/src/routes/api/category/[id]/file/remove/+server.ts deleted file mode 100644 index 6fdcccf..0000000 --- a/src/routes/api/category/[id]/file/remove/+server.ts +++ /dev/null @@ -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" } }); -}; diff --git a/src/routes/api/category/[id]/rename/+server.ts b/src/routes/api/category/[id]/rename/+server.ts deleted file mode 100644 index 5351544..0000000 --- a/src/routes/api/category/[id]/rename/+server.ts +++ /dev/null @@ -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" } }); -}; diff --git a/src/routes/api/category/create/+server.ts b/src/routes/api/category/create/+server.ts deleted file mode 100644 index 216d850..0000000 --- a/src/routes/api/category/create/+server.ts +++ /dev/null @@ -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" } }); -}; diff --git a/src/routes/api/directory/[id]/+server.ts b/src/routes/api/directory/[id]/+server.ts deleted file mode 100644 index 8189160..0000000 --- a/src/routes/api/directory/[id]/+server.ts +++ /dev/null @@ -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), - ); -}; diff --git a/src/routes/api/directory/[id]/delete/+server.ts b/src/routes/api/directory/[id]/delete/+server.ts deleted file mode 100644 index 4d29fd8..0000000 --- a/src/routes/api/directory/[id]/delete/+server.ts +++ /dev/null @@ -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), - ); -}; diff --git a/src/routes/api/directory/[id]/rename/+server.ts b/src/routes/api/directory/[id]/rename/+server.ts deleted file mode 100644 index cc50b2f..0000000 --- a/src/routes/api/directory/[id]/rename/+server.ts +++ /dev/null @@ -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" } }); -}; diff --git a/src/routes/api/directory/create/+server.ts b/src/routes/api/directory/create/+server.ts deleted file mode 100644 index 7c65436..0000000 --- a/src/routes/api/directory/create/+server.ts +++ /dev/null @@ -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" } }); -}; diff --git a/src/routes/api/file/[id]/+server.ts b/src/routes/api/file/[id]/+server.ts deleted file mode 100644 index 23e9385..0000000 --- a/src/routes/api/file/[id]/+server.ts +++ /dev/null @@ -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), - ); -}; diff --git a/src/routes/api/file/[id]/delete/+server.ts b/src/routes/api/file/[id]/delete/+server.ts deleted file mode 100644 index 7baac25..0000000 --- a/src/routes/api/file/[id]/delete/+server.ts +++ /dev/null @@ -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" } }); -}; diff --git a/src/routes/api/file/[id]/rename/+server.ts b/src/routes/api/file/[id]/rename/+server.ts deleted file mode 100644 index 343f146..0000000 --- a/src/routes/api/file/[id]/rename/+server.ts +++ /dev/null @@ -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" } }); -}; diff --git a/src/routes/api/file/[id]/thumbnail/+server.ts b/src/routes/api/file/[id]/thumbnail/+server.ts deleted file mode 100644 index 12c9347..0000000 --- a/src/routes/api/file/[id]/thumbnail/+server.ts +++ /dev/null @@ -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), - ); -}; diff --git a/src/routes/api/file/list/+server.ts b/src/routes/api/file/list/+server.ts deleted file mode 100644 index c1b6888..0000000 --- a/src/routes/api/file/list/+server.ts +++ /dev/null @@ -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)); -}; diff --git a/src/routes/api/file/scanDuplicates/+server.ts b/src/routes/api/file/scanDuplicates/+server.ts deleted file mode 100644 index fb41b43..0000000 --- a/src/routes/api/file/scanDuplicates/+server.ts +++ /dev/null @@ -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)); -}; diff --git a/src/routes/api/file/scanMissingThumbnails/+server.ts b/src/routes/api/file/scanMissingThumbnails/+server.ts deleted file mode 100644 index bf2a2a6..0000000 --- a/src/routes/api/file/scanMissingThumbnails/+server.ts +++ /dev/null @@ -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), - ); -}; diff --git a/src/trpc/client.ts b/src/trpc/client.ts index dbf4e80..cb1e8c5 100644 --- a/src/trpc/client.ts +++ b/src/trpc/client.ts @@ -1,4 +1,5 @@ import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import superjson from "superjson"; import { browser } from "$app/environment"; import type { AppRouter } from "./router.server"; @@ -7,6 +8,7 @@ const createClient = (fetch: typeof globalThis.fetch) => links: [ httpBatchLink({ url: "/api/trpc", + transformer: superjson, fetch, }), ], diff --git a/src/trpc/init.server.ts b/src/trpc/init.server.ts index 15a35fa..8b88157 100644 --- a/src/trpc/init.server.ts +++ b/src/trpc/init.server.ts @@ -1,10 +1,12 @@ import type { RequestEvent } from "@sveltejs/kit"; import { initTRPC, TRPCError } from "@trpc/server"; +import superjson from "superjson"; import { authorizeMiddleware, authorizeClientMiddleware } from "./middlewares/authorize"; -export const createContext = (event: RequestEvent) => event; +export type Context = Awaited>; -export const t = initTRPC.context>>().create(); +export const createContext = (event: RequestEvent) => event; +export const t = initTRPC.context().create({ transformer: superjson }); export const router = t.router; export const publicProcedure = t.procedure; diff --git a/src/trpc/router.server.ts b/src/trpc/router.server.ts index 3c44bea..3d05e93 100644 --- a/src/trpc/router.server.ts +++ b/src/trpc/router.server.ts @@ -1,10 +1,21 @@ import type { RequestEvent } from "@sveltejs/kit"; import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import { createContext, router } from "./init.server"; -import { clientRouter, hskRouter, mekRouter, userRouter } from "./routers"; +import { + categoryRouter, + clientRouter, + directoryRouter, + fileRouter, + hskRouter, + mekRouter, + userRouter, +} from "./routers"; export const appRouter = router({ + category: categoryRouter, client: clientRouter, + directory: directoryRouter, + file: fileRouter, hsk: hskRouter, mek: mekRouter, user: userRouter, diff --git a/src/trpc/routers/category.ts b/src/trpc/routers/category.ts new file mode 100644 index 0000000..f002421 --- /dev/null +++ b/src/trpc/routers/category.ts @@ -0,0 +1,194 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { CategoryRepo, FileRepo, IntegrityError } from "$lib/server/db"; +import { categoryIdSchema } from "$lib/server/schemas"; +import { router, roleProcedure } from "../init.server"; + +const categoryRouter = router({ + get: roleProcedure["activeClient"] + .input( + z.object({ + id: categoryIdSchema, + }), + ) + .query(async ({ ctx, input }) => { + const category = + input.id !== "root" + ? await CategoryRepo.getCategory(ctx.session.userId, input.id) + : undefined; + if (category === null) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); + } + + const categories = await CategoryRepo.getAllCategoriesByParent(ctx.session.userId, input.id); + return { + metadata: category && { + parent: category.parentId, + mekVersion: category.mekVersion, + dek: category.encDek, + dekVersion: category.dekVersion, + name: category.encName.ciphertext, + nameIv: category.encName.iv, + }, + subCategories: categories.map(({ id }) => id), + }; + }), + + create: roleProcedure["activeClient"] + .input( + z.object({ + parent: categoryIdSchema, + mekVersion: z.number().int().positive(), + dek: z.string().base64().nonempty(), + dekVersion: z.date(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + const oneMinuteAgo = new Date(Date.now() - 60 * 1000); + const oneMinuteLater = new Date(Date.now() + 60 * 1000); + if (input.dekVersion <= oneMinuteAgo || input.dekVersion >= oneMinuteLater) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid DEK version" }); + } + + try { + await CategoryRepo.registerCategory({ + parentId: input.parent, + userId: ctx.session.userId, + mekVersion: input.mekVersion, + encDek: input.dek, + dekVersion: input.dekVersion, + encName: { ciphertext: input.name, iv: input.nameIv }, + }); + } catch (e) { + if (e instanceof IntegrityError && e.message === "Inactive MEK version") { + throw new TRPCError({ code: "BAD_REQUEST", message: e.message }); + } + throw e; + } + }), + + rename: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + dekVersion: z.date(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + await CategoryRepo.setCategoryEncName(ctx.session.userId, input.id, input.dekVersion, { + ciphertext: input.name, + iv: input.nameIv, + }); + } catch (e) { + if (e instanceof IntegrityError) { + if (e.message === "Category not found") { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); + } else if (e.message === "Invalid DEK version") { + throw new TRPCError({ code: "BAD_REQUEST", message: e.message }); + } + } + throw e; + } + }), + + delete: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + await CategoryRepo.unregisterCategory(ctx.session.userId, input.id); + } catch (e) { + if (e instanceof IntegrityError && e.message === "Category not found") { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); + } + throw e; + } + }), + + files: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + recurse: z.boolean().default(false), + }), + ) + .query(async ({ ctx, input }) => { + const category = await CategoryRepo.getCategory(ctx.session.userId, input.id); + if (!category) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); + } + + const files = await FileRepo.getAllFilesByCategory( + ctx.session.userId, + input.id, + input.recurse, + ); + return files.map(({ id, isRecursive }) => ({ file: id, isRecursive })); + }), + + addFile: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + file: z.number().int().positive(), + }), + ) + .mutation(async ({ ctx, input }) => { + const [category, file] = await Promise.all([ + CategoryRepo.getCategory(ctx.session.userId, input.id), + FileRepo.getFile(ctx.session.userId, input.file), + ]); + if (!category) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); + } else if (!file) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" }); + } + + try { + await FileRepo.addFileToCategory(input.file, input.id); + } catch (e) { + if (e instanceof IntegrityError && e.message === "File already added to category") { + throw new TRPCError({ code: "BAD_REQUEST", message: "File already added" }); + } + throw e; + } + }), + + removeFile: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + file: z.number().int().positive(), + }), + ) + .mutation(async ({ ctx, input }) => { + const [category, file] = await Promise.all([ + CategoryRepo.getCategory(ctx.session.userId, input.id), + FileRepo.getFile(ctx.session.userId, input.file), + ]); + if (!category) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); + } else if (!file) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" }); + } + + try { + await FileRepo.removeFileFromCategory(input.file, input.id); + } catch (e) { + if (e instanceof IntegrityError && e.message === "File not found in category") { + throw new TRPCError({ code: "BAD_REQUEST", message: "File not added" }); + } + throw e; + } + }), +}); + +export default categoryRouter; diff --git a/src/trpc/routers/directory.ts b/src/trpc/routers/directory.ts new file mode 100644 index 0000000..70e3663 --- /dev/null +++ b/src/trpc/routers/directory.ts @@ -0,0 +1,125 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { FileRepo, IntegrityError } from "$lib/server/db"; +import { safeUnlink } from "$lib/server/modules/filesystem"; +import { directoryIdSchema } from "$lib/server/schemas"; +import { router, roleProcedure } from "../init.server"; + +const directoryRouter = router({ + get: roleProcedure["activeClient"] + .input( + z.object({ + id: directoryIdSchema, + }), + ) + .query(async ({ ctx, input }) => { + const directory = + input.id !== "root" ? await FileRepo.getDirectory(ctx.session.userId, input.id) : undefined; + if (directory === null) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" }); + } + + const [directories, files] = await Promise.all([ + FileRepo.getAllDirectoriesByParent(ctx.session.userId, input.id), + FileRepo.getAllFilesByParent(ctx.session.userId, input.id), + ]); + return { + metadata: directory && { + parent: directory.parentId, + mekVersion: directory.mekVersion, + dek: directory.encDek, + dekVersion: directory.dekVersion, + name: directory.encName.ciphertext, + nameIv: directory.encName.iv, + }, + subDirectories: directories.map(({ id }) => id), + files: files.map(({ id }) => id), + }; + }), + + create: roleProcedure["activeClient"] + .input( + z.object({ + parent: directoryIdSchema, + mekVersion: z.number().int().positive(), + dek: z.string().base64().nonempty(), + dekVersion: z.date(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + const oneMinuteAgo = new Date(Date.now() - 60 * 1000); + const oneMinuteLater = new Date(Date.now() + 60 * 1000); + if (input.dekVersion <= oneMinuteAgo || input.dekVersion >= oneMinuteLater) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid DEK version" }); + } + + try { + await FileRepo.registerDirectory({ + parentId: input.parent, + userId: ctx.session.userId, + mekVersion: input.mekVersion, + encDek: input.dek, + dekVersion: input.dekVersion, + encName: { ciphertext: input.name, iv: input.nameIv }, + }); + } catch (e) { + if (e instanceof IntegrityError && e.message === "Inactive MEK version") { + throw new TRPCError({ code: "BAD_REQUEST", message: e.message }); + } + throw e; + } + }), + + rename: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + dekVersion: z.date(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + await FileRepo.setDirectoryEncName(ctx.session.userId, input.id, input.dekVersion, { + ciphertext: input.name, + iv: input.nameIv, + }); + } catch (e) { + if (e instanceof IntegrityError) { + if (e.message === "Directory not found") { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" }); + } else if (e.message === "Invalid DEK version") { + throw new TRPCError({ code: "BAD_REQUEST", message: e.message }); + } + } + throw e; + } + }), + + delete: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + const files = await FileRepo.unregisterDirectory(ctx.session.userId, input.id); + files.forEach(({ path, thumbnailPath }) => { + safeUnlink(path); // Intended + safeUnlink(thumbnailPath); // Intended + }); + return { deletedFiles: files.map(({ id }) => id) }; + } catch (e) { + if (e instanceof IntegrityError && e.message === "Directory not found") { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" }); + } + throw e; + } + }), +}); + +export default directoryRouter; diff --git a/src/trpc/routers/file.ts b/src/trpc/routers/file.ts new file mode 100644 index 0000000..b37032b --- /dev/null +++ b/src/trpc/routers/file.ts @@ -0,0 +1,122 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { FileRepo, MediaRepo, IntegrityError } from "$lib/server/db"; +import { safeUnlink } from "$lib/server/modules/filesystem"; +import { router, roleProcedure } from "../init.server"; + +const fileRouter = router({ + get: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + }), + ) + .query(async ({ ctx, input }) => { + const file = await FileRepo.getFile(ctx.session.userId, input.id); + if (!file) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" }); + } + + const categories = await FileRepo.getAllFileCategories(input.id); + return { + parent: file.parentId, + mekVersion: file.mekVersion, + dek: file.encDek, + dekVersion: file.dekVersion, + contentType: file.contentType, + contentIv: file.encContentIv, + name: file.encName.ciphertext, + nameIv: file.encName.iv, + createdAt: file.encCreatedAt?.ciphertext, + createdAtIv: file.encCreatedAt?.iv, + lastModifiedAt: file.encLastModifiedAt.ciphertext, + lastModifiedAtIv: file.encLastModifiedAt.iv, + categories: categories.map(({ id }) => id), + }; + }), + + list: roleProcedure["activeClient"].query(async ({ ctx }) => { + return await FileRepo.getAllFileIds(ctx.session.userId); + }), + + listByHash: roleProcedure["activeClient"] + .input( + z.object({ + hskVersion: z.number().int().positive(), + contentHmac: z.string().base64().nonempty(), + }), + ) + .query(async ({ ctx, input }) => { + return await FileRepo.getAllFileIdsByContentHmac( + ctx.session.userId, + input.hskVersion, + input.contentHmac, + ); + }), + + listWithoutThumbnail: roleProcedure["activeClient"].query(async ({ ctx }) => { + return await MediaRepo.getMissingFileThumbnails(ctx.session.userId); + }), + + rename: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + dekVersion: z.date(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + await FileRepo.setFileEncName(ctx.session.userId, input.id, input.dekVersion, { + ciphertext: input.name, + iv: input.nameIv, + }); + } catch (e) { + if (e instanceof IntegrityError) { + if (e.message === "File not found") { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" }); + } else if (e.message === "Invalid DEK version") { + throw new TRPCError({ code: "BAD_REQUEST", message: e.message }); + } + } + throw e; + } + }), + + delete: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + const { path, thumbnailPath } = await FileRepo.unregisterFile(ctx.session.userId, input.id); + safeUnlink(path); // Intended + safeUnlink(thumbnailPath); // Intended + } catch (e) { + if (e instanceof IntegrityError && e.message === "File not found") { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" }); + } + throw e; + } + }), + + thumbnail: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + }), + ) + .query(async ({ ctx, input }) => { + const thumbnail = await MediaRepo.getFileThumbnail(ctx.session.userId, input.id); + if (!thumbnail) { + throw new TRPCError({ code: "NOT_FOUND", message: "File or its thumbnail not found" }); + } + return { updatedAt: thumbnail.updatedAt, contentIv: thumbnail.encContentIv }; + }), +}); + +export default fileRouter; diff --git a/src/trpc/routers/index.ts b/src/trpc/routers/index.ts index 26ac7b2..c943728 100644 --- a/src/trpc/routers/index.ts +++ b/src/trpc/routers/index.ts @@ -1,4 +1,7 @@ +export { default as categoryRouter } from "./category"; export { default as clientRouter } from "./client"; +export { default as directoryRouter } from "./directory"; +export { default as fileRouter } from "./file"; export { default as hskRouter } from "./hsk"; export { default as mekRouter } from "./mek"; export { default as userRouter } from "./user"; diff --git a/src/trpc/routers/user.ts b/src/trpc/routers/user.ts index 37b2460..ec514f1 100644 --- a/src/trpc/routers/user.ts +++ b/src/trpc/routers/user.ts @@ -1,10 +1,9 @@ import { TRPCError } from "@trpc/server"; -import { z } from "zod"; import { UserRepo } from "$lib/server/db"; import { router, roleProcedure } from "../init.server"; const userRouter = router({ - info: roleProcedure.any.query(async ({ ctx }) => { + get: roleProcedure["any"].query(async ({ ctx }) => { const user = await UserRepo.getUser(ctx.session.userId); if (!user) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" }); @@ -12,16 +11,6 @@ const userRouter = router({ return { email: user.email, nickname: user.nickname }; }), - - changeNickname: roleProcedure.any - .input( - z.object({ - newNickname: z.string().trim().min(2).max(8), - }), - ) - .mutation(async ({ ctx, input }) => { - await UserRepo.setUserNickname(ctx.session.userId, input.newNickname); - }), }); export default userRouter;