|
1 | 1 | <script lang="ts"> |
| 2 | + import { goto } from '$app/navigation'; |
| 3 | + import { onMount } from 'svelte'; |
| 4 | + import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; |
| 5 | + import { destroyVault, hasExistingVault } from '$lib/resume-vault'; |
| 6 | +
|
2 | 7 | let { |
3 | 8 | title = 'Unlock your resume vault', |
4 | 9 | copy = 'Enter your passphrase to decrypt resumes stored on this device.', |
|
14 | 19 | } = $props(); |
15 | 20 |
|
16 | 21 | let passphrase = $state(''); |
| 22 | + let resetBusy = $state(false); |
| 23 | + let resetSuccess = $state(''); |
| 24 | + let hasVault = $state<boolean | null>(null); |
| 25 | + let resetDialog = $state<{ showModal: () => Promise<void> } | null>(null); |
| 26 | +
|
| 27 | + const createTitle = 'Create your resume vault'; |
| 28 | + const createCopy = 'Choose a passphrase to create a new encrypted vault on this device.'; |
| 29 | +
|
| 30 | + let vaultKnown = $derived(hasVault !== null); |
| 31 | + let resolvedTitle = $derived(hasVault === false ? createTitle : title); |
| 32 | + let resolvedCopy = $derived(hasVault === false ? createCopy : copy); |
17 | 33 |
|
18 | 34 | async function submit() { |
19 | 35 | await onUnlock(passphrase); |
20 | 36 | passphrase = ''; |
21 | 37 | } |
| 38 | +
|
| 39 | + function openResetDialog() { |
| 40 | + void resetDialog?.showModal(); |
| 41 | + } |
| 42 | +
|
| 43 | + async function confirmReset() { |
| 44 | + resetBusy = true; |
| 45 | +
|
| 46 | + try { |
| 47 | + await destroyVault(); |
| 48 | + passphrase = ''; |
| 49 | + hasVault = false; |
| 50 | + resetSuccess = 'Vault reset complete. You can now create a new vault with a new passphrase.'; |
| 51 | + window.setTimeout(() => { |
| 52 | + void goto('/', { replaceState: true }).catch(() => { |
| 53 | + window.location.reload(); |
| 54 | + }); |
| 55 | + }, 1200); |
| 56 | + } finally { |
| 57 | + resetBusy = false; |
| 58 | + } |
| 59 | + } |
| 60 | +
|
| 61 | + onMount(() => { |
| 62 | + void hasExistingVault().then((existingVault) => { |
| 63 | + hasVault = existingVault; |
| 64 | + }); |
| 65 | + }); |
22 | 66 | </script> |
23 | 67 |
|
24 | 68 | <section class="content-card vault-panel"> |
25 | 69 | <p class="section-kicker">Encrypted Storage</p> |
26 | | - <h3>{title}</h3> |
27 | | - <p>{copy}</p> |
| 70 | + <h3>{resolvedTitle}</h3> |
| 71 | + <p>{resolvedCopy}</p> |
28 | 72 |
|
29 | 73 | <form class="stack-form" onsubmit={(event) => { |
30 | 74 | event.preventDefault(); |
|
47 | 91 | <p class="field-error" data-testid="vault-error">{error}</p> |
48 | 92 | {/if} |
49 | 93 |
|
| 94 | + {#if resetSuccess} |
| 95 | + <p class="vault-reset-success" data-testid="vault-reset-success" role="status">{resetSuccess}</p> |
| 96 | + {/if} |
| 97 | + |
50 | 98 | <button |
51 | 99 | class="shell-button shell-button--primary" |
52 | 100 | data-testid="unlock-vault" |
53 | | - disabled={busy} |
| 101 | + disabled={busy || resetBusy || !vaultKnown} |
54 | 102 | type="submit" |
55 | 103 | > |
56 | | - {#if busy}Unlocking…{:else}Unlock vault{/if} |
| 104 | + {#if !vaultKnown} |
| 105 | + Checking vault… |
| 106 | + {:else if busy} |
| 107 | + Unlocking… |
| 108 | + {:else if !hasVault} |
| 109 | + Create vault |
| 110 | + {:else} |
| 111 | + Unlock vault |
| 112 | + {/if} |
57 | 113 | </button> |
| 114 | + |
| 115 | + {#if hasVault === true} |
| 116 | + <button |
| 117 | + class="vault-reset-link" |
| 118 | + data-testid="forgot-passphrase" |
| 119 | + disabled={resetBusy} |
| 120 | + type="button" |
| 121 | + onclick={openResetDialog} |
| 122 | + > |
| 123 | + Forgot your passphrase? |
| 124 | + </button> |
| 125 | + {/if} |
58 | 126 | </form> |
59 | 127 | </section> |
| 128 | + |
| 129 | +<ConfirmDialog |
| 130 | + bind:this={resetDialog} |
| 131 | + body={`If you forgot your passphrase, your encrypted data cannot be recovered. ShieldCV uses local encryption with no server backup. Resetting the vault will permanently delete all stored resumes, scan history, and audit logs. You will create a new vault with a new passphrase.\n\nThis action cannot be undone.`} |
| 132 | + busy={resetBusy} |
| 133 | + confirmTestId="confirm-reset-vault" |
| 134 | + destructiveLabel="Delete all data and reset" |
| 135 | + kicker="Reset vault" |
| 136 | + title="Reset Vault" |
| 137 | + onConfirm={confirmReset} |
| 138 | +/> |
| 139 | + |
| 140 | +<style> |
| 141 | + .vault-reset-link { |
| 142 | + justify-self: start; |
| 143 | + border: 0; |
| 144 | + padding: 0; |
| 145 | + background: transparent; |
| 146 | + color: var(--accent); |
| 147 | + font: inherit; |
| 148 | + font-weight: 600; |
| 149 | + text-decoration: underline; |
| 150 | + text-underline-offset: 0.2em; |
| 151 | + } |
| 152 | +
|
| 153 | + .vault-reset-link:disabled { |
| 154 | + color: var(--text-muted); |
| 155 | + text-decoration: none; |
| 156 | + } |
| 157 | +
|
| 158 | + .vault-reset-success { |
| 159 | + margin: 0; |
| 160 | + color: var(--success); |
| 161 | + } |
| 162 | +</style> |
0 commit comments