자잘한 리팩토링 2 및 TopBar가 상단에 고정되지 않던 버그 수정

This commit is contained in:
static
2025-01-09 04:52:52 +09:00
parent 8873337ede
commit c25d42d515
8 changed files with 70 additions and 72 deletions

View File

@@ -5,3 +5,6 @@ yarn.lock
# Output # Output
/drizzle /drizzle
# Documents
*.md

View File

@@ -9,7 +9,7 @@
- 🔒 사용자의 미디어는 클라이언트에서 암호화한 상태로 저장돼요. - 🔒 사용자의 미디어는 클라이언트에서 암호화한 상태로 저장돼요.
- 🔑 메타 데이터도 클라이언트에서 암호화돼요. - 🔑 메타 데이터도 클라이언트에서 암호화돼요.
- ⚠️ 검색의 용이성을 위해, 스키마는 암호화되지 않아요. - ⚠️ 검색의 용이성을 위해, 스키마는 암호화되지 않아요.
- ⚠️ 파일의 MIME 타입과 같은 일부 메타 데이터는 암호화되지 않아요. - ⚠️ 파일의 MIME 타입과 같은 일부 메타 데이터는 암호화되지 않아요.
- 📱 여러 디바이스에서 동시에 접근할 수 있어요. - 📱 여러 디바이스에서 동시에 접근할 수 있어요.
## How to Install ## How to Install
@@ -36,7 +36,7 @@ docker compose up --build -d
|`JWT_REFRESH_TOKEN_EXPIRES`||`14d`|Refresh Token의 유효 시간이에요.| |`JWT_REFRESH_TOKEN_EXPIRES`||`14d`|Refresh Token의 유효 시간이에요.|
|`USER_CLIENT_CHALLENGE_EXPIRES`||`5m`|암호 키를 서버에 처음 등록할 때 사용되는 챌린지의 유효 시간이에요.| |`USER_CLIENT_CHALLENGE_EXPIRES`||`5m`|암호 키를 서버에 처음 등록할 때 사용되는 챌린지의 유효 시간이에요.|
|`TOKEN_UPGRADE_CHALLENGE_EXPIRES`||`5m`|암호 키와 함께 로그인할 때 사용되는 챌린지의 유효 시간이에요.| |`TOKEN_UPGRADE_CHALLENGE_EXPIRES`||`5m`|암호 키와 함께 로그인할 때 사용되는 챌린지의 유효 시간이에요.|
|`TRUST_PROXY`|||신뢰할 수 있는 리버스 프록시의 수예요. 설정할 경우, 1 이상의 정수로 설정해 주세요. 리버스 프록시에서 `X-Forwarded-For` HTTP 헤더를 올바르게 설정하도록 구성해 주세요.| |`TRUST_PROXY`|||신뢰할 수 있는 리버스 프록시의 수예요. 설정할 경우 1 이상의 정수로 설정해 주세요. 프록시에서 `X-Forwarded-For` HTTP 헤더를 올바르게 설정하도록 구성해 주세요.|
|`NODE_ENV`||`production`|ArkVault의 사용 용도예요. `production`인 경우, 컨테이너가 실행될 때마다 DB 마이그레이션이 자동으로 실행돼요.| |`NODE_ENV`||`production`|ArkVault의 사용 용도예요. `production`인 경우, 컨테이너가 실행될 때마다 DB 마이그레이션이 자동으로 실행돼요.|
|`PORT`||`80`|ArkVault 서버의 포트예요.| |`PORT`||`80`|ArkVault 서버의 포트예요.|
|`CONTAINER_UID`||`0`|Docker 컨테이너에 매핑할 UID예요. NFS와 함께 사용할 경우 설정이 필요할 수 있어요.| |`CONTAINER_UID`||`0`|Docker 컨테이너에 매핑할 UID예요. NFS와 함께 사용할 경우 설정이 필요할 수 있어요.|

View File

@@ -16,7 +16,7 @@
}); });
</script> </script>
<div class="sticky top-0 flex items-center justify-between bg-white pt-4"> <div class="sticky top-0 z-10 flex flex-shrink-0 items-center justify-between bg-white py-4">
<button onclick={back} class="w-[2.3rem] flex-shrink-0 rounded-full p-1 active:bg-gray-100"> <button onclick={back} class="w-[2.3rem] flex-shrink-0 rounded-full p-1 active:bg-gray-100">
<IconArrowBack class="text-2xl" /> <IconArrowBack class="text-2xl" />
</button> </button>

View File

@@ -1,4 +1,4 @@
import { writable } from "svelte/store"; import { writable, type Writable } from "svelte/store";
import { callGetApi } from "$lib/hooks"; import { callGetApi } from "$lib/hooks";
import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
import type { DirectoryInfoResponse, FileInfoResponse } from "$lib/server/schemas"; import type { DirectoryInfoResponse, FileInfoResponse } from "$lib/server/schemas";
@@ -9,7 +9,11 @@ import {
type FileInfo, type FileInfo,
} from "$lib/stores/file"; } from "$lib/stores/file";
const fetchDirectoryInfo = async (directoryId: "root" | number, masterKey: CryptoKey) => { const fetchDirectoryInfo = async (
directoryId: "root" | number,
masterKey: CryptoKey,
infoStore: Writable<DirectoryInfo | null>,
) => {
const res = await callGetApi(`/api/directory/${directoryId}`); const res = await callGetApi(`/api/directory/${directoryId}`);
if (!res.ok) throw new Error("Failed to fetch directory information"); if (!res.ok) throw new Error("Failed to fetch directory information");
const { metadata, subDirectories, files }: DirectoryInfoResponse = await res.json(); const { metadata, subDirectories, files }: DirectoryInfoResponse = await res.json();
@@ -33,12 +37,7 @@ const fetchDirectoryInfo = async (directoryId: "root" | number, masterKey: Crypt
}; };
} }
const info = directoryInfoStore.get(directoryId); infoStore.update(() => newInfo);
if (info) {
info.update(() => newInfo);
} else {
directoryInfoStore.set(directoryId, writable(newInfo));
}
}; };
export const getDirectoryInfo = (directoryId: "root" | number, masterKey: CryptoKey) => { export const getDirectoryInfo = (directoryId: "root" | number, masterKey: CryptoKey) => {
@@ -50,11 +49,15 @@ export const getDirectoryInfo = (directoryId: "root" | number, masterKey: Crypto
directoryInfoStore.set(directoryId, info); directoryInfoStore.set(directoryId, info);
} }
fetchDirectoryInfo(directoryId, masterKey); fetchDirectoryInfo(directoryId, masterKey, info);
return info; return info;
}; };
const fetchFileInfo = async (fileId: number, masterKey: CryptoKey) => { const fetchFileInfo = async (
fileId: number,
masterKey: CryptoKey,
infoStore: Writable<FileInfo | null>,
) => {
const res = await callGetApi(`/api/file/${fileId}`); const res = await callGetApi(`/api/file/${fileId}`);
if (!res.ok) throw new Error("Failed to fetch file information"); if (!res.ok) throw new Error("Failed to fetch file information");
const metadata: FileInfoResponse = await res.json(); const metadata: FileInfoResponse = await res.json();
@@ -69,12 +72,7 @@ const fetchFileInfo = async (fileId: number, masterKey: CryptoKey) => {
name: await decryptString(metadata.name, metadata.nameIv, dataKey), name: await decryptString(metadata.name, metadata.nameIv, dataKey),
}; };
const info = fileInfoStore.get(fileId); infoStore.update(() => newInfo);
if (info) {
info.update(() => newInfo);
} else {
fileInfoStore.set(fileId, writable(newInfo));
}
}; };
export const getFileInfo = (fileId: number, masterKey: CryptoKey) => { export const getFileInfo = (fileId: number, masterKey: CryptoKey) => {
@@ -86,6 +84,6 @@ export const getFileInfo = (fileId: number, masterKey: CryptoKey) => {
fileInfoStore.set(fileId, info); fileInfoStore.set(fileId, info);
} }
fetchFileInfo(fileId, masterKey); fetchFileInfo(fileId, masterKey, info);
return info; return info;
}; };

View File

@@ -67,11 +67,9 @@
<title>파일</title> <title>파일</title>
</svelte:head> </svelte:head>
<div class="flex h-full flex-col"> <div class="flex flex-col">
<div class="flex-shrink-0"> <TopBar title={$info?.name} />
<TopBar title={$info?.name} /> <div class="mb-4 flex w-full flex-grow flex-col items-center">
</div>
<div class="flex w-full flex-grow flex-col items-center py-4">
{#snippet viewerLoading(message: string)} {#snippet viewerLoading(message: string)}
<div class="flex flex-grow items-center justify-center"> <div class="flex flex-grow items-center justify-center">
<p class="text-gray-500">{message}</p> <p class="text-gray-500">{message}</p>

View File

@@ -61,22 +61,23 @@
<input bind:this={fileInput} onchange={uploadFile} type="file" class="hidden" /> <input bind:this={fileInput} onchange={uploadFile} type="file" class="hidden" />
<div class="flex min-h-full flex-col px-4"> <div class="flex min-h-full flex-col px-4">
<div class="flex-shrink-0"> {#if data.id !== "root"}
{#if data.id !== "root"} <TopBar title={$info?.name} />
<TopBar title={$info?.name} /> {/if}
{/if}
</div>
{#if $info} {#if $info}
{#key $info} {@const topMargin = data.id === "root" ? "mt-4" : ""}
<DirectoryEntries <div class="mb-4 flex flex-grow flex-col {topMargin}">
info={$info} {#key $info}
onEntryClick={({ type, id }) => goto(`/${type}/${id}`)} <DirectoryEntries
onEntryMenuClick={(entry) => { info={$info}
selectedEntry = entry; onEntryClick={({ type, id }) => goto(`/${type}/${id}`)}
isDirectoryEntryMenuBottomSheetOpen = true; onEntryMenuClick={(entry) => {
}} selectedEntry = entry;
/> isDirectoryEntryMenuBottomSheetOpen = true;
{/key} }}
/>
{/key}
</div>
{/if} {/if}
</div> </div>

View File

@@ -42,7 +42,7 @@
</script> </script>
{#if info.subDirectoryIds.length + info.fileIds.length > 0} {#if info.subDirectoryIds.length + info.fileIds.length > 0}
<div class="my-4 pb-[4.5rem]"> <div class="pb-[4.5rem]">
{#each subDirectoryInfos as subDirectory} {#each subDirectoryInfos as subDirectory}
<SubDirectory info={subDirectory} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} /> <SubDirectory info={subDirectory} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} />
{/each} {/each}
@@ -51,7 +51,7 @@
{/each} {/each}
</div> </div>
{:else} {:else}
<div class="my-4 flex flex-grow items-center justify-center"> <div class="flex flex-grow items-center justify-center">
<p class="text-gray-500">폴더가 비어 있어요.</p> <p class="text-gray-500">폴더가 비어 있어요.</p>
</div> </div>
{/if} {/if}

View File

@@ -1,11 +1,5 @@
import { callPostApi } from "$lib/hooks"; import { callPostApi } from "$lib/hooks";
import { import { generateDataKey, wrapDataKey, encryptData, encryptString } from "$lib/modules/crypto";
encodeToBase64,
generateDataKey,
wrapDataKey,
encryptData,
encryptString,
} from "$lib/modules/crypto";
import type { import type {
DirectoryRenameRequest, DirectoryRenameRequest,
DirectoryCreateRequest, DirectoryCreateRequest,
@@ -28,42 +22,46 @@ export const requestDirectoryCreation = async (
masterKey: MasterKey, masterKey: MasterKey,
) => { ) => {
const { dataKey, dataKeyVersion } = await generateDataKey(); const { dataKey, dataKeyVersion } = await generateDataKey();
const nameEncrypted = await encryptData(new TextEncoder().encode(name), dataKey); const nameEncrypted = await encryptString(name, dataKey);
await callPostApi<DirectoryCreateRequest>("/api/directory/create", { await callPostApi<DirectoryCreateRequest>("/api/directory/create", {
parentId, parentId,
mekVersion: masterKey.version, mekVersion: masterKey.version,
dek: await wrapDataKey(dataKey, masterKey.key), dek: await wrapDataKey(dataKey, masterKey.key),
dekVersion: dataKeyVersion.toISOString(), dekVersion: dataKeyVersion.toISOString(),
name: encodeToBase64(nameEncrypted.ciphertext), name: nameEncrypted.ciphertext,
nameIv: nameEncrypted.iv, nameIv: nameEncrypted.iv,
}); });
}; };
export const requestFileUpload = (file: File, parentId: "root" | number, masterKey: MasterKey) => { export const requestFileUpload = async (
return new Promise<void>(async (resolve, reject) => { file: File,
const { dataKey, dataKeyVersion } = await generateDataKey(); parentId: "root" | number,
const fileEncrypted = await encryptData(await file.arrayBuffer(), dataKey); masterKey: MasterKey,
const nameEncrypted = await encryptString(file.name, dataKey); ) => {
const { dataKey, dataKeyVersion } = await generateDataKey();
const fileEncrypted = await encryptData(await file.arrayBuffer(), dataKey);
const nameEncrypted = await encryptString(file.name, dataKey);
const form = new FormData(); const form = new FormData();
form.set( form.set(
"metadata", "metadata",
JSON.stringify({ JSON.stringify({
parentId, parentId,
mekVersion: masterKey.version, mekVersion: masterKey.version,
dek: await wrapDataKey(dataKey, masterKey.key), dek: await wrapDataKey(dataKey, masterKey.key),
dekVersion: dataKeyVersion.toISOString(), dekVersion: dataKeyVersion.toISOString(),
contentType: file.type, contentType: file.type,
contentIv: fileEncrypted.iv, contentIv: fileEncrypted.iv,
name: nameEncrypted.ciphertext, name: nameEncrypted.ciphertext,
nameIv: nameEncrypted.iv, nameIv: nameEncrypted.iv,
} satisfies FileUploadRequest), } satisfies FileUploadRequest),
); );
form.set("content", new Blob([fileEncrypted.ciphertext])); form.set("content", new Blob([fileEncrypted.ciphertext]));
return new Promise<void>((resolve, reject) => {
// TODO: Progress, Scheduling, ... // TODO: Progress, Scheduling, ...
const xhr = new XMLHttpRequest();
const xhr = new XMLHttpRequest();
xhr.addEventListener("load", () => { xhr.addEventListener("load", () => {
if (xhr.status === 200) { if (xhr.status === 200) {
resolve(); resolve();