this repo has no description
1<script lang="ts">
2 import { navigate } from '../lib/router.svelte'
3 import { api, ApiError } from '../lib/api'
4 import { _ } from '../lib/i18n'
5
6 let newPassword = $state('')
7 let confirmPassword = $state('')
8 let submitting = $state(false)
9 let error = $state<string | null>(null)
10 let success = $state(false)
11
12 function getUrlParams(): { did: string | null; token: string | null } {
13 const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
14 return {
15 did: params.get('did'),
16 token: params.get('token'),
17 }
18 }
19
20 let { did, token } = getUrlParams()
21
22 function validateForm(): string | null {
23 if (!newPassword) return $_('recoverPasskey.validation.passwordRequired')
24 if (newPassword.length < 8) return $_('recoverPasskey.validation.passwordLength')
25 if (newPassword !== confirmPassword) return $_('recoverPasskey.validation.passwordsMismatch')
26 return null
27 }
28
29 async function handleSubmit(e: Event) {
30 e.preventDefault()
31
32 if (!did || !token) {
33 error = $_('recoverPasskey.errors.invalidLink')
34 return
35 }
36
37 const validationError = validateForm()
38 if (validationError) {
39 error = validationError
40 return
41 }
42
43 submitting = true
44 error = null
45
46 try {
47 await api.recoverPasskeyAccount(did, token, newPassword)
48 success = true
49 } catch (err) {
50 if (err instanceof ApiError) {
51 if (err.error === 'RecoveryLinkExpired') {
52 error = $_('recoverPasskey.errors.expired')
53 } else if (err.error === 'InvalidRecoveryLink') {
54 error = $_('recoverPasskey.errors.invalidLink')
55 } else {
56 error = err.message || $_('common.error')
57 }
58 } else if (err instanceof Error) {
59 error = err.message || $_('common.error')
60 } else {
61 error = $_('common.error')
62 }
63 } finally {
64 submitting = false
65 }
66 }
67
68 function goToLogin() {
69 navigate('/login')
70 }
71
72 function requestNewLink() {
73 navigate('/login')
74 }
75</script>
76
77<div class="recover-page">
78 {#if !did || !token}
79 <h1>{$_('recoverPasskey.invalidLinkTitle')}</h1>
80 <p class="error-message">{$_('recoverPasskey.invalidLinkMessage')}</p>
81 <button onclick={requestNewLink}>{$_('recoverPasskey.goToLogin')}</button>
82 {:else if success}
83 <div class="success-content">
84 <div class="success-icon">✔</div>
85 <h1>{$_('recoverPasskey.successTitle')}</h1>
86 <p class="success-message">{$_('recoverPasskey.successMessage')}</p>
87 <p class="next-steps">{$_('recoverPasskey.successNextSteps')}</p>
88 <button onclick={goToLogin}>{$_('recoverPasskey.signIn')}</button>
89 </div>
90 {:else}
91 <h1>{$_('recoverPasskey.title')}</h1>
92 <p class="subtitle">{$_('recoverPasskey.subtitle')}</p>
93
94 {#if error}
95 <div class="message error">{error}</div>
96 {/if}
97
98 <form onsubmit={handleSubmit}>
99 <div class="field">
100 <label for="new-password">{$_('recoverPasskey.newPassword')}</label>
101 <input
102 id="new-password"
103 type="password"
104 bind:value={newPassword}
105 placeholder={$_('recoverPasskey.newPasswordPlaceholder')}
106 disabled={submitting}
107 required
108 minlength="8"
109 />
110 </div>
111
112 <div class="field">
113 <label for="confirm-password">{$_('recoverPasskey.confirmPassword')}</label>
114 <input
115 id="confirm-password"
116 type="password"
117 bind:value={confirmPassword}
118 placeholder={$_('recoverPasskey.confirmPasswordPlaceholder')}
119 disabled={submitting}
120 required
121 />
122 </div>
123
124 <div class="info-box">
125 <strong>{$_('recoverPasskey.whatHappensNext')}</strong>
126 <p>{$_('recoverPasskey.whatHappensNextDetail')}</p>
127 </div>
128
129 <button type="submit" disabled={submitting}>
130 {submitting ? $_('recoverPasskey.settingPassword') : $_('recoverPasskey.setPassword')}
131 </button>
132 </form>
133 {/if}
134</div>
135
136<style>
137 .recover-page {
138 max-width: var(--width-sm);
139 margin: var(--space-9) auto;
140 padding: var(--space-7);
141 }
142
143 h1 {
144 margin: 0 0 var(--space-3) 0;
145 }
146
147 .subtitle {
148 color: var(--text-secondary);
149 margin: 0 0 var(--space-7) 0;
150 }
151
152 form {
153 display: flex;
154 flex-direction: column;
155 gap: var(--space-4);
156 }
157
158 .info-box {
159 background: var(--bg-secondary);
160 border: 1px solid var(--border-color);
161 border-radius: var(--radius-lg);
162 padding: var(--space-5);
163 font-size: var(--text-sm);
164 }
165
166 .info-box strong {
167 display: block;
168 margin-bottom: var(--space-3);
169 }
170
171 .info-box p {
172 margin: 0;
173 color: var(--text-secondary);
174 }
175
176 .error-message {
177 color: var(--text-secondary);
178 margin-bottom: var(--space-6);
179 }
180
181 .success-content {
182 text-align: center;
183 }
184
185 .success-icon {
186 font-size: var(--text-4xl);
187 color: var(--success-text);
188 margin-bottom: var(--space-4);
189 }
190
191 .success-message {
192 color: var(--text-secondary);
193 margin-bottom: var(--space-3);
194 }
195
196 .next-steps {
197 color: var(--text-muted);
198 font-size: var(--text-sm);
199 margin-bottom: var(--space-6);
200 }
201</style>