diff --git a/src/lib/server/db/category.ts b/src/lib/server/db/category.ts index fb85322..d710b34 100644 --- a/src/lib/server/db/category.ts +++ b/src/lib/server/db/category.ts @@ -2,7 +2,7 @@ import { IntegrityError } from "./error"; import db from "./kysely"; import type { Ciphertext } from "./schema"; -type CategoryId = "root" | number; +export type CategoryId = "root" | number; interface Category { id: number; diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index 789c3b3..8b7819b 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -3,7 +3,7 @@ import { IntegrityError } from "./error"; import db from "./kysely"; import type { Ciphertext } from "./schema"; -type DirectoryId = "root" | number; +export type DirectoryId = "root" | number; interface Directory { id: number; diff --git a/src/lib/server/schemas/category.ts b/src/lib/server/schemas/category.ts new file mode 100644 index 0000000..13032b3 --- /dev/null +++ b/src/lib/server/schemas/category.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; + +export const categoryIdSchema = z.union([z.enum(["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.infer; + +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.infer; diff --git a/src/lib/server/schemas/directory.ts b/src/lib/server/schemas/directory.ts index 15a5886..473d696 100644 --- a/src/lib/server/schemas/directory.ts +++ b/src/lib/server/schemas/directory.ts @@ -1,9 +1,11 @@ import { z } from "zod"; +export const directoryIdSchema = z.union([z.enum(["root"]), z.number().int().positive()]); + export const directoryInfoResponse = z.object({ metadata: z .object({ - parent: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: directoryIdSchema, mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), @@ -29,7 +31,7 @@ export const directoryRenameRequest = z.object({ export type DirectoryRenameRequest = z.infer; export const directoryCreateRequest = z.object({ - parent: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: directoryIdSchema, mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index 781baf2..8f552f7 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -1,8 +1,9 @@ import mime from "mime"; import { z } from "zod"; +import { directoryIdSchema } from "./directory"; export const fileInfoResponse = z.object({ - parent: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: directoryIdSchema, mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), @@ -39,7 +40,7 @@ export const duplicateFileScanResponse = z.object({ export type DuplicateFileScanResponse = z.infer; export const fileUploadRequest = z.object({ - parent: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: directoryIdSchema, mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), diff --git a/src/lib/server/schemas/index.ts b/src/lib/server/schemas/index.ts index 6f8270b..1fed0d0 100644 --- a/src/lib/server/schemas/index.ts +++ b/src/lib/server/schemas/index.ts @@ -1,4 +1,5 @@ export * from "./auth"; +export * from "./category"; export * from "./client"; export * from "./directory"; export * from "./file"; diff --git a/src/lib/server/services/category.ts b/src/lib/server/services/category.ts new file mode 100644 index 0000000..5dd6c65 --- /dev/null +++ b/src/lib/server/services/category.ts @@ -0,0 +1,45 @@ +import { error } from "@sveltejs/kit"; +import { + registerCategory, + getAllCategoriesByParent, + getCategory, + type CategoryId, + type NewCategory, +} from "$lib/server/db/category"; +import { IntegrityError } from "$lib/server/db/error"; + +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 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 index be795b0..2525069 100644 --- a/src/lib/server/services/directory.ts +++ b/src/lib/server/services/directory.ts @@ -8,11 +8,12 @@ import { 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: "root" | number) => { +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"); diff --git a/src/routes/api/category/[id]/+server.ts b/src/routes/api/category/[id]/+server.ts new file mode 100644 index 0000000..4a486fa --- /dev/null +++ b/src/routes/api/category/[id]/+server.ts @@ -0,0 +1,33 @@ +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/create/+server.ts b/src/routes/api/category/create/+server.ts new file mode 100644 index 0000000..216d850 --- /dev/null +++ b/src/routes/api/category/create/+server.ts @@ -0,0 +1,23 @@ +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" } }); +};