mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 16:16:55 +00:00
Compare commits
12 Commits
9635d2a51b
...
demo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66c3f2df71 | ||
|
|
385404ece2 | ||
|
|
ac6aaa18ca | ||
|
|
7b621d6e98 | ||
|
|
3906ec4371 | ||
|
|
90ac5ba4c3 | ||
|
|
dfffa004ac | ||
|
|
0cd55a413d | ||
|
|
361d966a59 | ||
|
|
aef43b8bfa | ||
|
|
7f128cccf6 | ||
|
|
a198e5f6dc |
@@ -13,6 +13,7 @@ node_modules
|
||||
/library
|
||||
/thumbnails
|
||||
/uploads
|
||||
/log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ node_modules
|
||||
/library
|
||||
/thumbnails
|
||||
/uploads
|
||||
/log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
@@ -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:-}
|
||||
|
||||
@@ -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
|
||||
|
||||
37
src/lib/server/modules/logger.ts
Normal file
37
src/lib/server/modules/logger.ts
Normal file
@@ -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<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 email = $state("");
|
||||
let password = $state("");
|
||||
let email = $state("arkvault-demo@minchan.me");
|
||||
let password = $state("arkvault-demo");
|
||||
|
||||
let isForceLoginModalOpen = $state(false);
|
||||
|
||||
|
||||
@@ -52,13 +52,6 @@
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<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>
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user