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