diff --git a/.dockerignore b/.dockerignore index 6d312ec..a0fe6f6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,6 +13,7 @@ node_modules /library /thumbnails /uploads +/log # OS .DS_Store diff --git a/.gitignore b/.gitignore index a200c74..5fa6d31 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules /library /thumbnails /uploads +/log # OS .DS_Store diff --git a/docker-compose.yaml b/docker-compose.yaml index 3544f14..09717a5 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,6 +10,7 @@ services: - ./data/library:/app/data/library - ./data/thumbnails:/app/data/thumbnails - ./data/uploads:/app/data/uploads + - ./data/log:/app/data/log environment: # ArkVault - DATABASE_HOST=database @@ -22,6 +23,7 @@ services: - LIBRARY_PATH=/app/data/library - THUMBNAILS_PATH=/app/data/thumbnails - UPLOADS_PATH=/app/data/uploads + - LOG_DIR=/app/data/log # SvelteKit - ADDRESS_HEADER=${TRUST_PROXY:+X-Forwarded-For} - XFF_DEPTH=${TRUST_PROXY:-} diff --git a/src/lib/constants/upload.ts b/src/lib/constants/upload.ts index 57934d6..d197cd1 100644 --- a/src/lib/constants/upload.ts +++ b/src/lib/constants/upload.ts @@ -4,3 +4,6 @@ export const ENCRYPTION_OVERHEAD = AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE; export const CHUNK_SIZE = 4 * 1024 * 1024; // 4 MiB export const ENCRYPTED_CHUNK_SIZE = CHUNK_SIZE + ENCRYPTION_OVERHEAD; + +export const MAX_FILE_SIZE = 512 * 1024 * 1024; // 512 MiB +export const MAX_CHUNKS = Math.ceil(MAX_FILE_SIZE / CHUNK_SIZE); // 128 chunks diff --git a/src/lib/server/modules/logger.ts b/src/lib/server/modules/logger.ts new file mode 100644 index 0000000..0b458d9 --- /dev/null +++ b/src/lib/server/modules/logger.ts @@ -0,0 +1,37 @@ +import { appendFileSync, existsSync, mkdirSync } from "fs"; +import { env } from "$env/dynamic/private"; + +const LOG_DIR = env.LOG_DIR || "log"; + +const getLogFilePath = () => { + const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD + return `${LOG_DIR}/arkvault-${date}.log`; +}; + +const ensureLogDir = () => { + if (!existsSync(LOG_DIR)) { + mkdirSync(LOG_DIR, { recursive: true }); + } +}; + +const formatLogLine = (type: string, data: Record) => { + const timestamp = new Date().toISOString(); + return JSON.stringify({ timestamp, type, ...data }); +}; + +export const demoLogger = { + log: (type: string, data: Record) => { + const line = formatLogLine(type, data); + + // Output to stdout + console.log(line); + + // Output to file + try { + ensureLogDir(); + appendFileSync(getLogFilePath(), line + "\n", { encoding: "utf-8" }); + } catch (e) { + console.error("Failed to write to log file:", e); + } + }, +}; diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte index 8ec1742..b682b83 100644 --- a/src/routes/(fullscreen)/auth/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -14,8 +14,8 @@ let { data } = $props(); - let email = $state(""); - let password = $state(""); + let email = $state("arkvault-demo@minchan.me"); + let password = $state("arkvault-demo"); let isForceLoginModalOpen = $state(false); diff --git a/src/routes/(main)/menu/+page.svelte b/src/routes/(main)/menu/+page.svelte index 2bfd3fc..8245c25 100644 --- a/src/routes/(main)/menu/+page.svelte +++ b/src/routes/(main)/menu/+page.svelte @@ -52,13 +52,6 @@

보안

- goto("/auth/changePassword")} - icon={IconPassword} - iconColor="text-blue-500" - > - 비밀번호 바꾸기 - 로그아웃 diff --git a/src/trpc/routers/auth.ts b/src/trpc/routers/auth.ts index e0d3d50..63fc79e 100644 --- a/src/trpc/routers/auth.ts +++ b/src/trpc/routers/auth.ts @@ -5,6 +5,7 @@ import { ClientRepo, SessionRepo, UserRepo, IntegrityError } from "$lib/server/d import env from "$lib/server/loadenv"; import { cookieOptions } from "$lib/server/modules/auth"; import { generateChallenge, verifySignature, issueSessionId } from "$lib/server/modules/crypto"; +import { demoLogger } from "$lib/server/modules/logger"; import { router, publicProcedure, roleProcedure } from "../init.server"; const authRouter = router({ @@ -24,6 +25,10 @@ const authRouter = router({ const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret); await SessionRepo.createSession(user.id, sessionId, ctx.locals.ip, ctx.locals.userAgent); ctx.cookies.set("sessionId", sessionIdSigned, cookieOptions); + + if (input.email === "arkvault-demo@minchan.me") { + demoLogger.log("demo:login", { ip: ctx.locals.ip, sessionId }); + } }), logout: roleProcedure["any"].mutation(async ({ ctx }) => { @@ -38,22 +43,8 @@ const authRouter = router({ newPassword: z.string().nonempty(), }), ) - .mutation(async ({ ctx, input }) => { - if (input.oldPassword === input.newPassword) { - throw new TRPCError({ code: "BAD_REQUEST", message: "Same passwords" }); - } else if (input.newPassword.length < 8) { - throw new TRPCError({ code: "BAD_REQUEST", message: "Too short password" }); - } - - const user = await UserRepo.getUser(ctx.session.userId); - if (!user) { - throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" }); - } else if (!(await argon2.verify(user.password, input.oldPassword))) { - throw new TRPCError({ code: "FORBIDDEN", message: "Invalid password" }); - } - - await UserRepo.setUserPassword(ctx.session.userId, await argon2.hash(input.newPassword)); - await SessionRepo.deleteAllOtherSessions(ctx.session.userId, ctx.session.sessionId); + .mutation(() => { + throw new TRPCError({ code: "NOT_IMPLEMENTED" }); }), upgrade: roleProcedure["notClient"] diff --git a/src/trpc/routers/directory.ts b/src/trpc/routers/directory.ts index 449d5b1..647247d 100644 --- a/src/trpc/routers/directory.ts +++ b/src/trpc/routers/directory.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { DirectoryIdSchema } from "$lib/schemas"; import { DirectoryRepo, FileRepo, IntegrityError } from "$lib/server/db"; import { safeUnlink } from "$lib/server/modules/filesystem"; +import { demoLogger } from "$lib/server/modules/logger"; import { router, roleProcedure } from "../init.server"; const directoryRouter = router({ @@ -134,6 +135,7 @@ const directoryRouter = router({ const files = await DirectoryRepo.unregisterDirectory(ctx.session.userId, input.id); return { deletedFiles: files.map((file) => { + demoLogger.log("file:delete", { ip: ctx.locals.ip, fileId: file.id, recursive: true }); safeUnlink(file.path); // Intended safeUnlink(file.thumbnailPath); // Intended return file.id; diff --git a/src/trpc/routers/file.ts b/src/trpc/routers/file.ts index 30aaacd..2ea8f7c 100644 --- a/src/trpc/routers/file.ts +++ b/src/trpc/routers/file.ts @@ -2,6 +2,7 @@ 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 { demoLogger } from "$lib/server/modules/logger"; import { router, roleProcedure } from "../init.server"; const fileRouter = router({ @@ -174,6 +175,7 @@ const fileRouter = router({ .mutation(async ({ ctx, input }) => { try { const { path, thumbnailPath } = await FileRepo.unregisterFile(ctx.session.userId, input.id); + demoLogger.log("file:delete", { ip: ctx.locals.ip, fileId: input.id }); safeUnlink(path); // Intended safeUnlink(thumbnailPath); // Intended } catch (e) { diff --git a/src/trpc/routers/upload.ts b/src/trpc/routers/upload.ts index 11b0a84..3a2de19 100644 --- a/src/trpc/routers/upload.ts +++ b/src/trpc/routers/upload.ts @@ -6,11 +6,13 @@ import mime from "mime"; import { dirname } from "path"; import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; +import { MAX_CHUNKS } from "$lib/constants"; import { DirectoryIdSchema } from "$lib/schemas"; import { FileRepo, MediaRepo, UploadRepo, IntegrityError } from "$lib/server/db"; import db from "$lib/server/db/kysely"; import env from "$lib/server/loadenv"; import { safeRecursiveRm, safeUnlink } from "$lib/server/modules/filesystem"; +import { demoLogger } from "$lib/server/modules/logger"; import { router, roleProcedure } from "../init.server"; const UPLOADS_EXPIRES = 24 * 3600 * 1000; // 24 hours @@ -28,7 +30,7 @@ const uploadRouter = router({ startFileUpload: roleProcedure["activeClient"] .input( z.object({ - chunks: z.int().positive(), + chunks: z.int().positive().max(MAX_CHUNKS), parent: DirectoryIdSchema, mekVersion: z.int().positive(), dek: z.base64().nonempty(), @@ -76,6 +78,7 @@ const uploadRouter = router({ : null, encLastModifiedAt: { ciphertext: input.lastModifiedAt, iv: input.lastModifiedAtIv }, }); + demoLogger.log("upload:start", { ip: ctx.locals.ip, uploadId: id }); return { uploadId: id }; } catch (e) { await safeRecursiveRm(path); @@ -153,6 +156,7 @@ const uploadRouter = router({ }); await safeRecursiveRm(session.path); + demoLogger.log("upload:complete", { ip: ctx.locals.ip, uploadId, fileId }); return { file: fileId }; } catch (e) { await safeUnlink(filePath); @@ -183,6 +187,7 @@ const uploadRouter = router({ fileId: input.file, dekVersion: input.dekVersion, }); + demoLogger.log("thumbnail:start", { ip: ctx.locals.ip, uploadId: id }); return { uploadId: id }; } catch (e) { await safeRecursiveRm(path); @@ -238,6 +243,11 @@ const uploadRouter = router({ await UploadRepo.deleteUploadSession(trx, uploadId); return oldPath; }); + demoLogger.log("thumbnail:complete", { + ip: ctx.locals.ip, + uploadId, + fileId: session.fileId, + }); await Promise.all([safeUnlink(oldThumbnailPath), safeRecursiveRm(session.path)]); } catch (e) { await safeUnlink(thumbnailPath);