mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-03-22 23:36:55 +09:00
Compare commits
1 Commits
demo
...
3c1e98c872
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c1e98c872 |
@@ -13,7 +13,6 @@ node_modules
|
|||||||
/library
|
/library
|
||||||
/thumbnails
|
/thumbnails
|
||||||
/uploads
|
/uploads
|
||||||
/log
|
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,7 +11,6 @@ node_modules
|
|||||||
/library
|
/library
|
||||||
/thumbnails
|
/thumbnails
|
||||||
/uploads
|
/uploads
|
||||||
/log
|
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -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:-}
|
||||||
|
|||||||
54
package.json
54
package.json
@@ -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
1386
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user