this repo has no description
1<script lang="ts">
2 import { navigate } from '../lib/router.svelte'
3 import { api, ApiError } from '../lib/api'
4
5 let newPassword = $state('')
6 let confirmPassword = $state('')
7 let submitting = $state(false)
8 let error = $state<string | null>(null)
9 let success = $state(false)
10
11 function getUrlParams(): { did: string | null; token: string | null } {
12 const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
13 return {
14 did: params.get('did'),
15 token: params.get('token'),
16 }
17 }
18
19 let { did, token } = getUrlParams()
20
21 function validateForm(): string | null {
22 if (!newPassword) return 'New password is required'
23 if (newPassword.length < 8) return 'Password must be at least 8 characters'
24 if (newPassword !== confirmPassword) return 'Passwords do not match'
25 return null
26 }
27
28 async function handleSubmit(e: Event) {
29 e.preventDefault()
30
31 if (!did || !token) {
32 error = 'Invalid recovery link. Please request a new one.'
33 return
34 }
35
36 const validationError = validateForm()
37 if (validationError) {
38 error = validationError
39 return
40 }
41
42 submitting = true
43 error = null
44
45 try {
46 await api.recoverPasskeyAccount(did, token, newPassword)
47 success = true
48 } catch (err) {
49 if (err instanceof ApiError) {
50 if (err.error === 'RecoveryLinkExpired') {
51 error = 'This recovery link has expired. Please request a new one.'
52 } else if (err.error === 'InvalidRecoveryLink') {
53 error = 'Invalid recovery link. Please request a new one.'
54 } else {
55 error = err.message || 'Recovery failed'
56 }
57 } else if (err instanceof Error) {
58 error = err.message || 'Recovery failed'
59 } else {
60 error = 'Recovery failed'
61 }
62 } finally {
63 submitting = false
64 }
65 }
66
67 function goToLogin() {
68 navigate('/login')
69 }
70
71 function requestNewLink() {
72 navigate('/login')
73 }
74</script>
75
76<div class="recover-container">
77 {#if !did || !token}
78 <h1>Invalid Recovery Link</h1>
79 <p class="error-message">
80 This recovery link is invalid or has been corrupted. Please request a new recovery email.
81 </p>
82 <button onclick={requestNewLink}>Go to Login</button>
83 {:else if success}
84 <div class="success-content">
85 <div class="success-icon">✔</div>
86 <h1>Password Set!</h1>
87 <p class="success-message">
88 Your temporary password has been set. You can now sign in with this password.
89 </p>
90 <p class="next-steps">
91 After signing in, we recommend adding a new passkey in your security settings
92 to restore passkey-only authentication.
93 </p>
94 <button onclick={goToLogin}>Sign In</button>
95 </div>
96 {:else}
97 <h1>Recover Your Account</h1>
98 <p class="subtitle">
99 Set a temporary password to regain access to your passkey-only account.
100 </p>
101
102 {#if error}
103 <div class="error">{error}</div>
104 {/if}
105
106 <form onsubmit={handleSubmit}>
107 <div class="field">
108 <label for="new-password">New Password</label>
109 <input
110 id="new-password"
111 type="password"
112 bind:value={newPassword}
113 placeholder="At least 8 characters"
114 disabled={submitting}
115 required
116 minlength="8"
117 />
118 </div>
119
120 <div class="field">
121 <label for="confirm-password">Confirm Password</label>
122 <input
123 id="confirm-password"
124 type="password"
125 bind:value={confirmPassword}
126 placeholder="Confirm your password"
127 disabled={submitting}
128 required
129 />
130 </div>
131
132 <div class="info-box">
133 <strong>What happens next?</strong>
134 <p>
135 After setting this password, you can sign in and add a new passkey in your security settings.
136 Once you have a new passkey, you can optionally remove the temporary password.
137 </p>
138 </div>
139
140 <button type="submit" disabled={submitting}>
141 {submitting ? 'Setting password...' : 'Set Password'}
142 </button>
143 </form>
144 {/if}
145</div>
146
147<style>
148 .recover-container {
149 max-width: 400px;
150 margin: 4rem auto;
151 padding: 2rem;
152 }
153
154 h1 {
155 margin: 0 0 0.5rem 0;
156 }
157
158 .subtitle {
159 color: var(--text-secondary);
160 margin: 0 0 2rem 0;
161 }
162
163 form {
164 display: flex;
165 flex-direction: column;
166 gap: 1rem;
167 }
168
169 .field {
170 display: flex;
171 flex-direction: column;
172 gap: 0.25rem;
173 }
174
175 label {
176 font-size: 0.875rem;
177 font-weight: 500;
178 }
179
180 input {
181 padding: 0.75rem;
182 border: 1px solid var(--border-color-light);
183 border-radius: 4px;
184 font-size: 1rem;
185 background: var(--bg-input);
186 color: var(--text-primary);
187 }
188
189 input:focus {
190 outline: none;
191 border-color: var(--accent);
192 }
193
194 .info-box {
195 background: var(--bg-secondary);
196 border: 1px solid var(--border-color);
197 border-radius: 6px;
198 padding: 1rem;
199 font-size: 0.875rem;
200 }
201
202 .info-box strong {
203 display: block;
204 margin-bottom: 0.5rem;
205 }
206
207 .info-box p {
208 margin: 0;
209 color: var(--text-secondary);
210 }
211
212 button {
213 padding: 0.75rem;
214 background: var(--accent);
215 color: white;
216 border: none;
217 border-radius: 4px;
218 font-size: 1rem;
219 cursor: pointer;
220 margin-top: 0.5rem;
221 }
222
223 button:hover:not(:disabled) {
224 background: var(--accent-hover);
225 }
226
227 button:disabled {
228 opacity: 0.6;
229 cursor: not-allowed;
230 }
231
232 .error {
233 padding: 0.75rem;
234 background: var(--error-bg);
235 border: 1px solid var(--error-border);
236 border-radius: 4px;
237 color: var(--error-text);
238 margin-bottom: 1rem;
239 }
240
241 .error-message {
242 color: var(--text-secondary);
243 margin-bottom: 1.5rem;
244 }
245
246 .success-content {
247 text-align: center;
248 }
249
250 .success-icon {
251 font-size: 4rem;
252 color: var(--success-text);
253 margin-bottom: 1rem;
254 }
255
256 .success-message {
257 color: var(--text-secondary);
258 margin-bottom: 0.5rem;
259 }
260
261 .next-steps {
262 color: var(--text-muted);
263 font-size: 0.875rem;
264 margin-bottom: 1.5rem;
265 }
266</style>