diff --git a/package.json b/package.json
index b57af77..ca5bc28 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/better-sqlite3": "^7.6.11",
+ "@types/file-saver": "^2.0.7",
"@types/jsonwebtoken": "^9.0.7",
"@types/ms": "^0.7.34",
"@types/node-schedule": "^2.1.7",
@@ -33,6 +34,7 @@
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"eslint-plugin-tailwindcss": "^3.17.5",
+ "file-saver": "^2.0.5",
"globals": "^15.0.0",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.6",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index df5d1cf..b791d45 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -49,6 +49,9 @@ devDependencies:
'@types/better-sqlite3':
specifier: ^7.6.11
version: 7.6.12
+ '@types/file-saver':
+ specifier: ^2.0.7
+ version: 2.0.7
'@types/jsonwebtoken':
specifier: ^9.0.7
version: 9.0.7
@@ -79,6 +82,9 @@ devDependencies:
eslint-plugin-tailwindcss:
specifier: ^3.17.5
version: 3.17.5(tailwindcss@3.4.17)
+ file-saver:
+ specifier: ^2.0.5
+ version: 2.0.5
globals:
specifier: ^15.0.0
version: 15.14.0
@@ -1288,6 +1294,10 @@ packages:
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
dev: true
+ /@types/file-saver@2.0.7:
+ resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
+ dev: true
+
/@types/json-schema@7.0.15:
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
dev: true
@@ -2269,6 +2279,10 @@ packages:
flat-cache: 4.0.1
dev: true
+ /file-saver@2.0.5:
+ resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
+ dev: true
+
/file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
dev: false
diff --git a/src/lib/components/BottomSheet.svelte b/src/lib/components/BottomSheet.svelte
new file mode 100644
index 0000000..81e24e1
--- /dev/null
+++ b/src/lib/components/BottomSheet.svelte
@@ -0,0 +1,31 @@
+
+
+{#if isOpen}
+
+
+
{
+ isOpen = false;
+ }}
+ class="fixed inset-0 flex items-end justify-center"
+ >
+
+
e.stopPropagation()}
+ class="z-10 flex max-h-[70vh] min-h-[30vh] w-full items-stretch rounded-t-2xl bg-white p-4"
+ transition:fly={{ y: 100, duration: 200 }}
+ >
+ {@render children?.()}
+
+
+{/if}
diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte
index 36c1171..37d7466 100644
--- a/src/lib/components/Modal.svelte
+++ b/src/lib/components/Modal.svelte
@@ -8,24 +8,19 @@
}
let { children, isOpen = $bindable() }: Props = $props();
-
- const closeModal = () => {
- isOpen = false;
- };
{#if isOpen}
{
+ isOpen = false;
+ }}
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 px-2"
+ transition:fade={{ duration: 100 }}
>
-
e.stopPropagation()}
- class="max-w-full rounded-2xl bg-white p-4"
- transition:fade={{ duration: 100 }}
- >
+
e.stopPropagation()} class="max-w-full rounded-2xl bg-white p-4">
{@render children?.()}
diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts
index 5ddd0fe..55979fb 100644
--- a/src/lib/components/index.ts
+++ b/src/lib/components/index.ts
@@ -1 +1,2 @@
+export { default as BottomSheet } from "./BottomSheet.svelte";
export { default as Modal } from "./Modal.svelte";
diff --git a/src/lib/hooks/gotoStateful.ts b/src/lib/hooks/gotoStateful.ts
index 1a05294..064be29 100644
--- a/src/lib/hooks/gotoStateful.ts
+++ b/src/lib/hooks/gotoStateful.ts
@@ -3,6 +3,7 @@ import { goto } from "$app/navigation";
type Path = "/key/export";
interface KeyExportState {
+ redirectPath: string;
pubKeyBase64: string;
privKeyBase64: string;
}
diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte
index ffac02d..46c03fc 100644
--- a/src/routes/(fullscreen)/auth/login/+page.svelte
+++ b/src/routes/(fullscreen)/auth/login/+page.svelte
@@ -1,5 +1,4 @@
@@ -64,7 +82,9 @@
-
+
diff --git a/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte b/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte
new file mode 100644
index 0000000..58aa9a5
--- /dev/null
+++ b/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte
@@ -0,0 +1,35 @@
+
+
+
+
+
+
암호 키 파일을 저장하셨나요?
+
+ 보안상의 이유로 지금 시점 이후로는 암호 키를 파일로 내보낼 수 없어요. 파일이 저장되었는지
+ 다시 확인해 주세요.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte
index 984f553..3255026 100644
--- a/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte
+++ b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte
@@ -23,8 +23,10 @@
color="gray"
onclick={() => {
isOpen = false;
- }}>아니요
+ 아니요
+
diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts
index 3cef2bc..c45ff50 100644
--- a/src/routes/(fullscreen)/key/export/service.ts
+++ b/src/routes/(fullscreen)/key/export/service.ts
@@ -1,4 +1,17 @@
import { callAPI } from "$lib/hooks";
+import { storeKeyPairIntoIndexedDB } from "$lib/indexedDB";
+
+export const createBlobFromKeyPairBase64 = (pubKeyBase64: string, privKeyBase64: string) => {
+ const pubKeyFormatted = pubKeyBase64.match(/.{1,64}/g)?.join("\n");
+ const privKeyFormatted = privKeyBase64.match(/.{1,64}/g)?.join("\n");
+ if (!pubKeyFormatted || !privKeyFormatted) {
+ throw new Error("Failed to format key pair");
+ }
+
+ const pubKeyPem = `-----BEGIN PUBLIC KEY-----\n${pubKeyFormatted}\n-----END PUBLIC KEY-----`;
+ const privKeyPem = `-----BEGIN PRIVATE KEY-----\n${privKeyFormatted}\n-----END PRIVATE KEY-----`;
+ return new Blob([`${pubKeyPem}\n${privKeyPem}\n`], { type: "text/plain" });
+};
export const requestPubKeyRegistration = async (pubKeyBase64: string) => {
const res = await callAPI("/api/key/register", {
@@ -10,3 +23,7 @@ export const requestPubKeyRegistration = async (pubKeyBase64: string) => {
});
return res.ok;
};
+
+export const storeKeyPairPersistently = async (keyPair: CryptoKeyPair) => {
+ await storeKeyPairIntoIndexedDB(keyPair.publicKey, keyPair.privateKey);
+};
diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte
index af84aa8..3a751b6 100644
--- a/src/routes/(fullscreen)/key/generate/+page.svelte
+++ b/src/routes/(fullscreen)/key/generate/+page.svelte
@@ -1,12 +1,16 @@
diff --git a/src/routes/(fullscreen)/key/generate/+page.ts b/src/routes/(fullscreen)/key/generate/+page.ts
new file mode 100644
index 0000000..626d2e0
--- /dev/null
+++ b/src/routes/(fullscreen)/key/generate/+page.ts
@@ -0,0 +1,6 @@
+import type { PageLoad } from "./$types";
+
+export const load: PageLoad = async ({ url }) => {
+ const redirectPath = url.searchParams.get("redirect") || "/";
+ return { redirectPath };
+};