mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-14 05:48:46 +00:00
암호 키 내보내기 페이지 구현
This commit is contained in:
@@ -23,6 +23,7 @@
|
|||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
"@types/better-sqlite3": "^7.6.11",
|
"@types/better-sqlite3": "^7.6.11",
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/jsonwebtoken": "^9.0.7",
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
"@types/ms": "^0.7.34",
|
"@types/ms": "^0.7.34",
|
||||||
"@types/node-schedule": "^2.1.7",
|
"@types/node-schedule": "^2.1.7",
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.36.0",
|
"eslint-plugin-svelte": "^2.36.0",
|
||||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"globals": "^15.0.0",
|
"globals": "^15.0.0",
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.3.2",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
|
|||||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -49,6 +49,9 @@ devDependencies:
|
|||||||
'@types/better-sqlite3':
|
'@types/better-sqlite3':
|
||||||
specifier: ^7.6.11
|
specifier: ^7.6.11
|
||||||
version: 7.6.12
|
version: 7.6.12
|
||||||
|
'@types/file-saver':
|
||||||
|
specifier: ^2.0.7
|
||||||
|
version: 2.0.7
|
||||||
'@types/jsonwebtoken':
|
'@types/jsonwebtoken':
|
||||||
specifier: ^9.0.7
|
specifier: ^9.0.7
|
||||||
version: 9.0.7
|
version: 9.0.7
|
||||||
@@ -79,6 +82,9 @@ devDependencies:
|
|||||||
eslint-plugin-tailwindcss:
|
eslint-plugin-tailwindcss:
|
||||||
specifier: ^3.17.5
|
specifier: ^3.17.5
|
||||||
version: 3.17.5(tailwindcss@3.4.17)
|
version: 3.17.5(tailwindcss@3.4.17)
|
||||||
|
file-saver:
|
||||||
|
specifier: ^2.0.5
|
||||||
|
version: 2.0.5
|
||||||
globals:
|
globals:
|
||||||
specifier: ^15.0.0
|
specifier: ^15.0.0
|
||||||
version: 15.14.0
|
version: 15.14.0
|
||||||
@@ -1288,6 +1294,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/file-saver@2.0.7:
|
||||||
|
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/json-schema@7.0.15:
|
/@types/json-schema@7.0.15:
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -2269,6 +2279,10 @@ packages:
|
|||||||
flat-cache: 4.0.1
|
flat-cache: 4.0.1
|
||||||
dev: true
|
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:
|
/file-uri-to-path@1.0.0:
|
||||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||||
dev: false
|
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();
|
let { children, isOpen = $bindable() }: Props = $props();
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
isOpen = false;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
onclick={closeModal}
|
onclick={() => {
|
||||||
|
isOpen = false;
|
||||||
|
}}
|
||||||
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 px-2"
|
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 px-2"
|
||||||
|
transition:fade={{ duration: 100 }}
|
||||||
>
|
>
|
||||||
<div
|
<div onclick={(e) => e.stopPropagation()} class="max-w-full rounded-2xl bg-white p-4">
|
||||||
onclick={(e) => e.stopPropagation()}
|
|
||||||
class="max-w-full rounded-2xl bg-white p-4"
|
|
||||||
transition:fade={{ duration: 100 }}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
export { default as BottomSheet } from "./BottomSheet.svelte";
|
||||||
export { default as Modal } from "./Modal.svelte";
|
export { default as Modal } from "./Modal.svelte";
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { goto } from "$app/navigation";
|
|||||||
type Path = "/key/export";
|
type Path = "/key/export";
|
||||||
|
|
||||||
interface KeyExportState {
|
interface KeyExportState {
|
||||||
|
redirectPath: string;
|
||||||
pubKeyBase64: string;
|
pubKeyBase64: string;
|
||||||
privKeyBase64: string;
|
privKeyBase64: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { get } from "svelte/store";
|
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { Button, TextButton } from "$lib/components/buttons";
|
import { Button, TextButton } from "$lib/components/buttons";
|
||||||
import { TitleDiv, BottomDiv } from "$lib/components/divs";
|
import { TitleDiv, BottomDiv } from "$lib/components/divs";
|
||||||
@@ -17,7 +16,7 @@
|
|||||||
|
|
||||||
if (await requestLogin(email, password)) {
|
if (await requestLogin(email, password)) {
|
||||||
await goto(
|
await goto(
|
||||||
get(keyPairStore)
|
$keyPairStore
|
||||||
? data.redirectPath
|
? data.redirectPath
|
||||||
: "/key/generate?redirect=" + encodeURIComponent(data.redirectPath),
|
: "/key/generate?redirect=" + encodeURIComponent(data.redirectPath),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,31 +1,49 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { saveAs } from "file-saver";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
import { Button, TextButton } from "$lib/components/buttons";
|
import { Button, TextButton } from "$lib/components/buttons";
|
||||||
import { BottomDiv } from "$lib/components/divs";
|
import { BottomDiv } from "$lib/components/divs";
|
||||||
|
import { keyPairStore } from "$lib/stores";
|
||||||
|
import BeforeContinueBottomSheet from "./BeforeContinueBottomSheet.svelte";
|
||||||
import BeforeContinueModal from "./BeforeContinueModal.svelte";
|
import BeforeContinueModal from "./BeforeContinueModal.svelte";
|
||||||
import { requestPubKeyRegistration } from "./service";
|
import {
|
||||||
|
createBlobFromKeyPairBase64,
|
||||||
|
requestPubKeyRegistration,
|
||||||
|
storeKeyPairPersistently,
|
||||||
|
} from "./service";
|
||||||
|
|
||||||
import IconKey from "~icons/material-symbols/key";
|
import IconKey from "~icons/material-symbols/key";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let isBeforeContinueModalOpen = $state(false);
|
let isBeforeContinueModalOpen = $state(false);
|
||||||
|
let isBeforeContinueBottomSheetOpen = $state(false);
|
||||||
|
|
||||||
const exportKeyPair = () => {
|
const exportKeyPair = () => {
|
||||||
// TODO
|
const keyPairBlob = createBlobFromKeyPairBase64(data.pubKeyBase64, data.privKeyBase64);
|
||||||
console.log(data.pubKeyBase64);
|
saveAs(keyPairBlob, "arkvalut-keypair.pem");
|
||||||
console.log(data.privKeyBase64);
|
|
||||||
|
if (!isBeforeContinueBottomSheetOpen) {
|
||||||
|
setTimeout(() => {
|
||||||
|
isBeforeContinueBottomSheetOpen = true;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const continueWithoutExport = async () => {
|
const registerPubKey = async () => {
|
||||||
isBeforeContinueModalOpen = false;
|
if (!$keyPairStore) {
|
||||||
|
throw new Error("Failed to find key pair");
|
||||||
const ok = await requestPubKeyRegistration(data.pubKeyBase64);
|
|
||||||
if (!ok) {
|
|
||||||
// TODO
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
isBeforeContinueModalOpen = false;
|
||||||
|
isBeforeContinueBottomSheetOpen = false;
|
||||||
|
|
||||||
|
if (await requestPubKeyRegistration(data.pubKeyBase64)) {
|
||||||
|
await storeKeyPairPersistently($keyPairStore);
|
||||||
|
await goto(data.redirectPath);
|
||||||
|
} else {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -64,7 +82,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BeforeContinueModal
|
<BeforeContinueModal bind:isOpen={isBeforeContinueModalOpen} onContinueClick={registerPubKey} />
|
||||||
bind:isOpen={isBeforeContinueModalOpen}
|
<BeforeContinueBottomSheet
|
||||||
onContinueClick={continueWithoutExport}
|
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"
|
color="gray"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
}}>아니요</Button
|
}}
|
||||||
>
|
>
|
||||||
|
아니요
|
||||||
|
</Button>
|
||||||
<Button onclick={onContinueClick}>계속합니다</Button>
|
<Button onclick={onContinueClick}>계속합니다</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
import { callAPI } from "$lib/hooks";
|
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) => {
|
export const requestPubKeyRegistration = async (pubKeyBase64: string) => {
|
||||||
const res = await callAPI("/api/key/register", {
|
const res = await callAPI("/api/key/register", {
|
||||||
@@ -10,3 +23,7 @@ export const requestPubKeyRegistration = async (pubKeyBase64: string) => {
|
|||||||
});
|
});
|
||||||
return res.ok;
|
return res.ok;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const storeKeyPairPersistently = async (keyPair: CryptoKeyPair) => {
|
||||||
|
await storeKeyPairIntoIndexedDB(keyPair.publicKey, keyPair.privateKey);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
import { Button, TextButton } from "$lib/components/buttons";
|
import { Button, TextButton } from "$lib/components/buttons";
|
||||||
import { TitleDiv, BottomDiv } from "$lib/components/divs";
|
import { TitleDiv, BottomDiv } from "$lib/components/divs";
|
||||||
import { gotoStateful } from "$lib/hooks";
|
import { gotoStateful } from "$lib/hooks";
|
||||||
|
import { keyPairStore } from "$lib/stores";
|
||||||
import Order from "./Order.svelte";
|
import Order from "./Order.svelte";
|
||||||
import { generateKeyPair } from "./service";
|
import { generateKeyPair } from "./service";
|
||||||
|
|
||||||
import IconKey from "~icons/material-symbols/key";
|
import IconKey from "~icons/material-symbols/key";
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
const orders = [
|
const orders = [
|
||||||
{
|
{
|
||||||
title: "암호 키는 공개 키와 개인 키로 구성돼요.",
|
title: "암호 키는 공개 키와 개인 키로 구성돼요.",
|
||||||
@@ -27,9 +31,21 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const generate = async () => {
|
const generate = async () => {
|
||||||
// TODO
|
// TODO: Loading indicator
|
||||||
await gotoStateful("/key/export", await generateKeyPair());
|
|
||||||
|
const keyPair = await generateKeyPair();
|
||||||
|
await gotoStateful("/key/export", {
|
||||||
|
redirectPath: data.redirectPath,
|
||||||
|
pubKeyBase64: keyPair.pubKeyBase64,
|
||||||
|
privKeyBase64: keyPair.privKeyBase64,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($keyPairStore) {
|
||||||
|
goto(data.redirectPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svetle:head>
|
<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