mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-14 22:08:45 +00:00
자잘한 리팩토링 2 및 TopBar가 상단에 고정되지 않던 버그 수정
This commit is contained in:
@@ -5,3 +5,6 @@ yarn.lock
|
|||||||
|
|
||||||
# Output
|
# Output
|
||||||
/drizzle
|
/drizzle
|
||||||
|
|
||||||
|
# Documents
|
||||||
|
*.md
|
||||||
|
|||||||
@@ -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와 함께 사용할 경우 설정이 필요할 수 있어요.|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user