this repo has no description
at main 5.2 kB view raw
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">&#x2714;</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>