mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-12 21:08:46 +00:00
암호 키 내보내기 페이지 구현
This commit is contained in:
@@ -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",
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
31
src/lib/components/BottomSheet.svelte
Normal file
31
src/lib/components/BottomSheet.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import { fade, fly } from "svelte/transition";
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
let { children, isOpen = $bindable() }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
onclick={() => {
|
||||
isOpen = false;
|
||||
}}
|
||||
class="fixed inset-0 flex items-end justify-center"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black bg-opacity-50" transition:fade={{ duration: 100 }}></div>
|
||||
<div
|
||||
onclick={(e) => 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?.()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -8,24 +8,19 @@
|
||||
}
|
||||
|
||||
let { children, isOpen = $bindable() }: Props = $props();
|
||||
|
||||
const closeModal = () => {
|
||||
isOpen = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
onclick={closeModal}
|
||||
onclick={() => {
|
||||
isOpen = false;
|
||||
}}
|
||||
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 px-2"
|
||||
transition:fade={{ duration: 100 }}
|
||||
>
|
||||
<div
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
class="max-w-full rounded-2xl bg-white p-4"
|
||||
transition:fade={{ duration: 100 }}
|
||||
>
|
||||
<div onclick={(e) => e.stopPropagation()} class="max-w-full rounded-2xl bg-white p-4">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as BottomSheet } from "./BottomSheet.svelte";
|
||||
export { default as Modal } from "./Modal.svelte";
|
||||
|
||||
@@ -3,6 +3,7 @@ import { goto } from "$app/navigation";
|
||||
type Path = "/key/export";
|
||||
|
||||
interface KeyExportState {
|
||||
redirectPath: string;
|
||||
pubKeyBase64: string;
|
||||
privKeyBase64: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { get } from "svelte/store";
|
||||
import { goto } from "$app/navigation";
|
||||
import { Button, TextButton } from "$lib/components/buttons";
|
||||
import { TitleDiv, BottomDiv } from "$lib/components/divs";
|
||||
@@ -17,7 +16,7 @@
|
||||
|
||||
if (await requestLogin(email, password)) {
|
||||
await goto(
|
||||
get(keyPairStore)
|
||||
$keyPairStore
|
||||
? data.redirectPath
|
||||
: "/key/generate?redirect=" + encodeURIComponent(data.redirectPath),
|
||||
);
|
||||
|
||||
@@ -1,31 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { saveAs } from "file-saver";
|
||||
import { goto } from "$app/navigation";
|
||||
import { Button, TextButton } from "$lib/components/buttons";
|
||||
import { BottomDiv } from "$lib/components/divs";
|
||||
import { keyPairStore } from "$lib/stores";
|
||||
import BeforeContinueBottomSheet from "./BeforeContinueBottomSheet.svelte";
|
||||
import BeforeContinueModal from "./BeforeContinueModal.svelte";
|
||||
import { requestPubKeyRegistration } from "./service";
|
||||
import {
|
||||
createBlobFromKeyPairBase64,
|
||||
requestPubKeyRegistration,
|
||||
storeKeyPairPersistently,
|
||||
} from "./service";
|
||||
|
||||
import IconKey from "~icons/material-symbols/key";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let isBeforeContinueModalOpen = $state(false);
|
||||
let isBeforeContinueBottomSheetOpen = $state(false);
|
||||
|
||||
const exportKeyPair = () => {
|
||||
// TODO
|
||||
console.log(data.pubKeyBase64);
|
||||
console.log(data.privKeyBase64);
|
||||
const keyPairBlob = createBlobFromKeyPairBase64(data.pubKeyBase64, data.privKeyBase64);
|
||||
saveAs(keyPairBlob, "arkvalut-keypair.pem");
|
||||
|
||||
if (!isBeforeContinueBottomSheetOpen) {
|
||||
setTimeout(() => {
|
||||
isBeforeContinueBottomSheetOpen = true;
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
const continueWithoutExport = async () => {
|
||||
isBeforeContinueModalOpen = false;
|
||||
|
||||
const ok = await requestPubKeyRegistration(data.pubKeyBase64);
|
||||
if (!ok) {
|
||||
// TODO
|
||||
return;
|
||||
const registerPubKey = async () => {
|
||||
if (!$keyPairStore) {
|
||||
throw new Error("Failed to find key pair");
|
||||
}
|
||||
|
||||
// TODO
|
||||
isBeforeContinueModalOpen = false;
|
||||
isBeforeContinueBottomSheetOpen = false;
|
||||
|
||||
if (await requestPubKeyRegistration(data.pubKeyBase64)) {
|
||||
await storeKeyPairPersistently($keyPairStore);
|
||||
await goto(data.redirectPath);
|
||||
} else {
|
||||
// TODO
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -64,7 +82,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BeforeContinueModal
|
||||
bind:isOpen={isBeforeContinueModalOpen}
|
||||
onContinueClick={continueWithoutExport}
|
||||
<BeforeContinueModal bind:isOpen={isBeforeContinueModalOpen} onContinueClick={registerPubKey} />
|
||||
<BeforeContinueBottomSheet
|
||||
bind:isOpen={isBeforeContinueBottomSheetOpen}
|
||||
onRetryClick={exportKeyPair}
|
||||
onContinueClick={registerPubKey}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { BottomSheet } from "$lib/components";
|
||||
import { Button } from "$lib/components/buttons";
|
||||
import { BottomDiv } from "$lib/components/divs";
|
||||
|
||||
interface Props {
|
||||
onRetryClick: () => void;
|
||||
onContinueClick: () => void;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
let { onRetryClick, onContinueClick, isOpen = $bindable() }: Props = $props();
|
||||
</script>
|
||||
|
||||
<BottomSheet bind:isOpen>
|
||||
<div class="flex flex-col justify-between space-y-4">
|
||||
<div class="space-y-2">
|
||||
<p class="break-keep text-xl font-bold">암호 키 파일을 저장하셨나요?</p>
|
||||
<p class="break-keep">
|
||||
보안상의 이유로 지금 시점 이후로는 암호 키를 파일로 내보낼 수 없어요. 파일이 저장되었는지
|
||||
다시 확인해 주세요.
|
||||
</p>
|
||||
</div>
|
||||
<BottomDiv>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="w-full">
|
||||
<Button color="gray" onclick={onRetryClick}>다시 저장할래요</Button>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<Button onclick={onContinueClick}>잘 저장되었어요</Button>
|
||||
</div>
|
||||
</div>
|
||||
</BottomDiv>
|
||||
</div>
|
||||
</BottomSheet>
|
||||
@@ -23,8 +23,10 @@
|
||||
color="gray"
|
||||
onclick={() => {
|
||||
isOpen = false;
|
||||
}}>아니요</Button
|
||||
}}
|
||||
>
|
||||
아니요
|
||||
</Button>
|
||||
<Button onclick={onContinueClick}>계속합니다</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { Button, TextButton } from "$lib/components/buttons";
|
||||
import { TitleDiv, BottomDiv } from "$lib/components/divs";
|
||||
import { gotoStateful } from "$lib/hooks";
|
||||
import { keyPairStore } from "$lib/stores";
|
||||
import Order from "./Order.svelte";
|
||||
import { generateKeyPair } from "./service";
|
||||
|
||||
import IconKey from "~icons/material-symbols/key";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const orders = [
|
||||
{
|
||||
title: "암호 키는 공개 키와 개인 키로 구성돼요.",
|
||||
@@ -27,9 +31,21 @@
|
||||
];
|
||||
|
||||
const generate = async () => {
|
||||
// TODO
|
||||
await gotoStateful("/key/export", await generateKeyPair());
|
||||
// TODO: Loading indicator
|
||||
|
||||
const keyPair = await generateKeyPair();
|
||||
await gotoStateful("/key/export", {
|
||||
redirectPath: data.redirectPath,
|
||||
pubKeyBase64: keyPair.pubKeyBase64,
|
||||
privKeyBase64: keyPair.privKeyBase64,
|
||||
});
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
if ($keyPairStore) {
|
||||
goto(data.redirectPath);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svetle:head>
|
||||
|
||||
6
src/routes/(fullscreen)/key/generate/+page.ts
Normal file
6
src/routes/(fullscreen)/key/generate/+page.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ url }) => {
|
||||
const redirectPath = url.searchParams.get("redirect") || "/";
|
||||
return { redirectPath };
|
||||
};
|
||||
Reference in New Issue
Block a user