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">&#x2714;</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>