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