1 Commits

Author SHA1 Message Date
static
3c1e98c872 패키지 버전 업데이트 2026-03-09 06:14:37 +09:00
13 changed files with 764 additions and 770 deletions

View File

@@ -13,7 +13,6 @@ node_modules
/library /library
/thumbnails /thumbnails
/uploads /uploads
/log
# OS # OS
.DS_Store .DS_Store

1
.gitignore vendored
View File

@@ -11,7 +11,6 @@ node_modules
/library /library
/thumbnails /thumbnails
/uploads /uploads
/log
# OS # OS
.DS_Store .DS_Store

View File

@@ -10,7 +10,6 @@ services:
- ./data/library:/app/data/library - ./data/library:/app/data/library
- ./data/thumbnails:/app/data/thumbnails - ./data/thumbnails:/app/data/thumbnails
- ./data/uploads:/app/data/uploads - ./data/uploads:/app/data/uploads
- ./data/log:/app/data/log
environment: environment:
# ArkVault # ArkVault
- DATABASE_HOST=database - DATABASE_HOST=database
@@ -23,7 +22,6 @@ services:
- LIBRARY_PATH=/app/data/library - LIBRARY_PATH=/app/data/library
- THUMBNAILS_PATH=/app/data/thumbnails - THUMBNAILS_PATH=/app/data/thumbnails
- UPLOADS_PATH=/app/data/uploads - UPLOADS_PATH=/app/data/uploads
- LOG_DIR=/app/data/log
# SvelteKit # SvelteKit
- ADDRESS_HEADER=${TRUST_PROXY:+X-Forwarded-For} - ADDRESS_HEADER=${TRUST_PROXY:+X-Forwarded-For}
- XFF_DEPTH=${TRUST_PROXY:-} - XFF_DEPTH=${TRUST_PROXY:-}

View File

@@ -1,7 +1,7 @@
{ {
"name": "arkvault", "name": "arkvault",
"private": true, "private": true,
"version": "0.9.0", "version": "0.9.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@@ -16,56 +16,56 @@
"db:migrate": "kysely migrate" "db:migrate": "kysely migrate"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^2.0.1", "@eslint/compat": "^2.0.3",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.4",
"@iconify-json/material-symbols": "^1.2.51", "@iconify-json/material-symbols": "^1.2.60",
"@noble/hashes": "^2.0.1", "@noble/hashes": "^2.0.1",
"@sveltejs/adapter-node": "^5.5.1", "@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.49.5", "@sveltejs/kit": "^2.53.4",
"@sveltejs/vite-plugin-svelte": "^6.2.4", "@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tanstack/svelte-virtual": "^3.13.18", "@tanstack/svelte-virtual": "^3.13.21",
"@trpc/client": "^11.8.1", "@trpc/client": "^11.12.0",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/ms": "^0.7.34", "@types/ms": "^0.7.34",
"@types/node-schedule": "^2.1.8", "@types/node-schedule": "^2.1.8",
"@types/pg": "^8.16.0", "@types/pg": "^8.18.0",
"autoprefixer": "^10.4.23", "autoprefixer": "^10.4.27",
"axios": "^1.13.2", "axios": "^1.13.6",
"dexie": "^4.2.1", "dexie": "^4.3.0",
"es-hangul": "^2.3.8", "es-hangul": "^2.3.8",
"eslint": "^9.39.2", "eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.14.0", "eslint-plugin-svelte": "^3.15.0",
"eslint-plugin-tailwindcss": "^3.18.2", "eslint-plugin-tailwindcss": "^3.18.2",
"exifreader": "^4.36.0", "exifreader": "^4.36.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"globals": "^17.0.0", "globals": "^17.4.0",
"heic2any": "^0.0.4", "heic2any": "^0.0.4",
"kysely-ctl": "^0.20.0", "kysely-ctl": "^0.20.0",
"lru-cache": "^11.2.4", "lru-cache": "^11.2.6",
"mime": "^4.1.0", "mime": "^4.1.0",
"p-limit": "^7.2.0", "p-limit": "^7.3.0",
"prettier": "^3.8.0", "prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1", "prettier-plugin-svelte": "^3.5.1",
"prettier-plugin-tailwindcss": "^0.7.2", "prettier-plugin-tailwindcss": "^0.7.2",
"svelte": "^5.46.4", "svelte": "^5.53.7",
"svelte-check": "^4.3.5", "svelte-check": "^4.4.5",
"tailwindcss": "^3.4.19", "tailwindcss": "^3.4.19",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.53.0", "typescript-eslint": "^8.56.1",
"unplugin-icons": "^23.0.1", "unplugin-icons": "^23.0.1",
"vite": "^7.3.1" "vite": "^7.3.1"
}, },
"dependencies": { "dependencies": {
"@trpc/server": "^11.8.1", "@trpc/server": "^11.12.0",
"argon2": "^0.44.0", "argon2": "^0.44.0",
"kysely": "^0.28.9", "kysely": "^0.28.11",
"ms": "^2.1.3", "ms": "^2.1.3",
"node-schedule": "^2.1.1", "node-schedule": "^2.1.1",
"pg": "^8.17.1", "pg": "^8.20.0",
"superjson": "^2.2.6", "superjson": "^2.2.6",
"uuid": "^13.0.0", "uuid": "^13.0.0",
"zod": "^4.3.5" "zod": "^4.3.6"
}, },
"engines": { "engines": {
"node": "^22.0.0", "node": "^22.0.0",

1386
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,3 @@ export const ENCRYPTION_OVERHEAD = AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE;
export const CHUNK_SIZE = 4 * 1024 * 1024; // 4 MiB export const CHUNK_SIZE = 4 * 1024 * 1024; // 4 MiB
export const ENCRYPTED_CHUNK_SIZE = CHUNK_SIZE + ENCRYPTION_OVERHEAD; 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

View File

@@ -1,37 +0,0 @@
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<string, unknown>) => {
const timestamp = new Date().toISOString();
return JSON.stringify({ timestamp, type, ...data });
};
export const demoLogger = {
log: (type: string, data: Record<string, unknown>) => {
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);
}
},
};

View File

@@ -14,8 +14,8 @@
let { data } = $props(); let { data } = $props();
let email = $state("arkvault-demo@minchan.me"); let email = $state("");
let password = $state("arkvault-demo"); let password = $state("");
let isForceLoginModalOpen = $state(false); let isForceLoginModalOpen = $state(false);

View File

@@ -52,6 +52,13 @@
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<p class="font-semibold">보안</p> <p class="font-semibold">보안</p>
<MenuEntryButton
onclick={() => goto("/auth/changePassword")}
icon={IconPassword}
iconColor="text-blue-500"
>
비밀번호 바꾸기
</MenuEntryButton>
<MenuEntryButton onclick={logout} icon={IconLogout} iconColor="text-red-500"> <MenuEntryButton onclick={logout} icon={IconLogout} iconColor="text-red-500">
로그아웃 로그아웃
</MenuEntryButton> </MenuEntryButton>

View File

@@ -5,7 +5,6 @@ import { ClientRepo, SessionRepo, UserRepo, IntegrityError } from "$lib/server/d
import env from "$lib/server/loadenv"; import env from "$lib/server/loadenv";
import { cookieOptions } from "$lib/server/modules/auth"; import { cookieOptions } from "$lib/server/modules/auth";
import { generateChallenge, verifySignature, issueSessionId } from "$lib/server/modules/crypto"; import { generateChallenge, verifySignature, issueSessionId } from "$lib/server/modules/crypto";
import { demoLogger } from "$lib/server/modules/logger";
import { router, publicProcedure, roleProcedure } from "../init.server"; import { router, publicProcedure, roleProcedure } from "../init.server";
const authRouter = router({ const authRouter = router({
@@ -25,10 +24,6 @@ const authRouter = router({
const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret); const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret);
await SessionRepo.createSession(user.id, sessionId, ctx.locals.ip, ctx.locals.userAgent); await SessionRepo.createSession(user.id, sessionId, ctx.locals.ip, ctx.locals.userAgent);
ctx.cookies.set("sessionId", sessionIdSigned, cookieOptions); 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 }) => { logout: roleProcedure["any"].mutation(async ({ ctx }) => {
@@ -43,8 +38,22 @@ const authRouter = router({
newPassword: z.string().nonempty(), newPassword: z.string().nonempty(),
}), }),
) )
.mutation(() => { .mutation(async ({ ctx, input }) => {
throw new TRPCError({ code: "NOT_IMPLEMENTED" }); 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);
}), }),
upgrade: roleProcedure["notClient"] upgrade: roleProcedure["notClient"]

View File

@@ -3,7 +3,6 @@ import { z } from "zod";
import { DirectoryIdSchema } from "$lib/schemas"; import { DirectoryIdSchema } from "$lib/schemas";
import { DirectoryRepo, FileRepo, IntegrityError } from "$lib/server/db"; import { DirectoryRepo, FileRepo, IntegrityError } from "$lib/server/db";
import { safeUnlink } from "$lib/server/modules/filesystem"; import { safeUnlink } from "$lib/server/modules/filesystem";
import { demoLogger } from "$lib/server/modules/logger";
import { router, roleProcedure } from "../init.server"; import { router, roleProcedure } from "../init.server";
const directoryRouter = router({ const directoryRouter = router({
@@ -135,7 +134,6 @@ const directoryRouter = router({
const files = await DirectoryRepo.unregisterDirectory(ctx.session.userId, input.id); const files = await DirectoryRepo.unregisterDirectory(ctx.session.userId, input.id);
return { return {
deletedFiles: files.map((file) => { deletedFiles: files.map((file) => {
demoLogger.log("file:delete", { ip: ctx.locals.ip, fileId: file.id, recursive: true });
safeUnlink(file.path); // Intended safeUnlink(file.path); // Intended
safeUnlink(file.thumbnailPath); // Intended safeUnlink(file.thumbnailPath); // Intended
return file.id; return file.id;

View File

@@ -2,7 +2,6 @@ import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { FileRepo, MediaRepo, IntegrityError } from "$lib/server/db"; import { FileRepo, MediaRepo, IntegrityError } from "$lib/server/db";
import { safeUnlink } from "$lib/server/modules/filesystem"; import { safeUnlink } from "$lib/server/modules/filesystem";
import { demoLogger } from "$lib/server/modules/logger";
import { router, roleProcedure } from "../init.server"; import { router, roleProcedure } from "../init.server";
const fileRouter = router({ const fileRouter = router({
@@ -175,7 +174,6 @@ const fileRouter = router({
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { try {
const { path, thumbnailPath } = await FileRepo.unregisterFile(ctx.session.userId, input.id); 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(path); // Intended
safeUnlink(thumbnailPath); // Intended safeUnlink(thumbnailPath); // Intended
} catch (e) { } catch (e) {

View File

@@ -6,13 +6,11 @@ import mime from "mime";
import { dirname } from "path"; import { dirname } from "path";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { z } from "zod"; import { z } from "zod";
import { MAX_CHUNKS } from "$lib/constants";
import { DirectoryIdSchema } from "$lib/schemas"; import { DirectoryIdSchema } from "$lib/schemas";
import { FileRepo, MediaRepo, UploadRepo, IntegrityError } from "$lib/server/db"; import { FileRepo, MediaRepo, UploadRepo, IntegrityError } from "$lib/server/db";
import db from "$lib/server/db/kysely"; import db from "$lib/server/db/kysely";
import env from "$lib/server/loadenv"; import env from "$lib/server/loadenv";
import { safeRecursiveRm, safeUnlink } from "$lib/server/modules/filesystem"; import { safeRecursiveRm, safeUnlink } from "$lib/server/modules/filesystem";
import { demoLogger } from "$lib/server/modules/logger";
import { router, roleProcedure } from "../init.server"; import { router, roleProcedure } from "../init.server";
const UPLOADS_EXPIRES = 24 * 3600 * 1000; // 24 hours const UPLOADS_EXPIRES = 24 * 3600 * 1000; // 24 hours
@@ -30,7 +28,7 @@ const uploadRouter = router({
startFileUpload: roleProcedure["activeClient"] startFileUpload: roleProcedure["activeClient"]
.input( .input(
z.object({ z.object({
chunks: z.int().positive().max(MAX_CHUNKS), chunks: z.int().positive(),
parent: DirectoryIdSchema, parent: DirectoryIdSchema,
mekVersion: z.int().positive(), mekVersion: z.int().positive(),
dek: z.base64().nonempty(), dek: z.base64().nonempty(),
@@ -78,7 +76,6 @@ const uploadRouter = router({
: null, : null,
encLastModifiedAt: { ciphertext: input.lastModifiedAt, iv: input.lastModifiedAtIv }, encLastModifiedAt: { ciphertext: input.lastModifiedAt, iv: input.lastModifiedAtIv },
}); });
demoLogger.log("upload:start", { ip: ctx.locals.ip, uploadId: id });
return { uploadId: id }; return { uploadId: id };
} catch (e) { } catch (e) {
await safeRecursiveRm(path); await safeRecursiveRm(path);
@@ -156,7 +153,6 @@ const uploadRouter = router({
}); });
await safeRecursiveRm(session.path); await safeRecursiveRm(session.path);
demoLogger.log("upload:complete", { ip: ctx.locals.ip, uploadId, fileId });
return { file: fileId }; return { file: fileId };
} catch (e) { } catch (e) {
await safeUnlink(filePath); await safeUnlink(filePath);
@@ -187,7 +183,6 @@ const uploadRouter = router({
fileId: input.file, fileId: input.file,
dekVersion: input.dekVersion, dekVersion: input.dekVersion,
}); });
demoLogger.log("thumbnail:start", { ip: ctx.locals.ip, uploadId: id });
return { uploadId: id }; return { uploadId: id };
} catch (e) { } catch (e) {
await safeRecursiveRm(path); await safeRecursiveRm(path);
@@ -243,11 +238,6 @@ const uploadRouter = router({
await UploadRepo.deleteUploadSession(trx, uploadId); await UploadRepo.deleteUploadSession(trx, uploadId);
return oldPath; return oldPath;
}); });
demoLogger.log("thumbnail:complete", {
ip: ctx.locals.ip,
uploadId,
fileId: session.fileId,
});
await Promise.all([safeUnlink(oldThumbnailPath), safeRecursiveRm(session.path)]); await Promise.all([safeUnlink(oldThumbnailPath), safeRecursiveRm(session.path)]);
} catch (e) { } catch (e) {
await safeUnlink(thumbnailPath); await safeUnlink(thumbnailPath);