this repo has no description
1<script lang="ts"> 2 import { loginWithOAuth, confirmSignup, resendVerification, getAuthState, switchAccount, forgetAccount } from '../lib/auth.svelte' 3 import { navigate } from '../lib/router.svelte' 4 import { _ } from '../lib/i18n' 5 6 let submitting = $state(false) 7 let pendingVerification = $state<{ did: string } | null>(null) 8 let verificationCode = $state('') 9 let resendingCode = $state(false) 10 let resendMessage = $state<string | null>(null) 11 let autoRedirectAttempted = $state(false) 12 const auth = getAuthState() 13 14 $effect(() => { 15 if (!auth.loading && !auth.error && auth.savedAccounts.length === 0 && !pendingVerification && !autoRedirectAttempted) { 16 autoRedirectAttempted = true 17 loginWithOAuth() 18 } 19 }) 20 21 async function handleSwitchAccount(did: string) { 22 submitting = true 23 try { 24 await switchAccount(did) 25 navigate('/dashboard') 26 } catch { 27 submitting = false 28 } 29 } 30 31 function handleForgetAccount(did: string, e: Event) { 32 e.stopPropagation() 33 forgetAccount(did) 34 } 35 36 async function handleOAuthLogin() { 37 submitting = true 38 try { 39 await loginWithOAuth() 40 } catch { 41 submitting = false 42 } 43 } 44 45 async function handleVerification(e: Event) { 46 e.preventDefault() 47 if (!pendingVerification || !verificationCode.trim()) return 48 submitting = true 49 try { 50 await confirmSignup(pendingVerification.did, verificationCode.trim()) 51 navigate('/dashboard') 52 } catch { 53 submitting = false 54 } 55 } 56 57 async function handleResendCode() { 58 if (!pendingVerification || resendingCode) return 59 resendingCode = true 60 resendMessage = null 61 try { 62 await resendVerification(pendingVerification.did) 63 resendMessage = $_('verification.resent') 64 } catch { 65 resendMessage = null 66 } finally { 67 resendingCode = false 68 } 69 } 70 71 function backToLogin() { 72 pendingVerification = null 73 verificationCode = '' 74 resendMessage = null 75 } 76</script> 77 78<div class="login-page"> 79 {#if auth.error} 80 <div class="message error">{auth.error}</div> 81 {/if} 82 83 {#if pendingVerification} 84 <header class="page-header"> 85 <h1>{$_('verification.title')}</h1> 86 <p class="subtitle">{$_('verification.subtitle')}</p> 87 </header> 88 89 {#if resendMessage} 90 <div class="message success">{resendMessage}</div> 91 {/if} 92 93 <form onsubmit={(e) => { e.preventDefault(); handleVerification(e); }}> 94 <div class="field"> 95 <label for="verification-code">{$_('verification.codeLabel')}</label> 96 <input 97 id="verification-code" 98 type="text" 99 bind:value={verificationCode} 100 placeholder={$_('verification.codePlaceholder')} 101 disabled={submitting} 102 required 103 maxlength="6" 104 pattern="[0-9]{6}" 105 autocomplete="one-time-code" 106 /> 107 </div> 108 <div class="actions"> 109 <button type="submit" disabled={submitting || !verificationCode.trim()}> 110 {submitting ? $_('verification.verifying') : $_('verification.verifyButton')} 111 </button> 112 <button type="button" class="secondary" onclick={handleResendCode} disabled={resendingCode}> 113 {resendingCode ? $_('verification.resending') : $_('verification.resendButton')} 114 </button> 115 <button type="button" class="tertiary" onclick={backToLogin}> 116 {$_('verification.backToLogin')} 117 </button> 118 </div> 119 </form> 120 121 {:else} 122 <header class="page-header"> 123 <h1>{$_('login.title')}</h1> 124 <p class="subtitle">{auth.savedAccounts.length > 0 ? $_('login.chooseAccount') : $_('login.subtitle')}</p> 125 </header> 126 127 <div class="split-layout sidebar-right"> 128 <div class="main-section"> 129 {#if auth.savedAccounts.length > 0} 130 <div class="saved-accounts"> 131 {#each auth.savedAccounts as account} 132 <div 133 class="account-item" 134 class:disabled={submitting} 135 role="button" 136 tabindex="0" 137 onclick={() => !submitting && handleSwitchAccount(account.did)} 138 onkeydown={(e) => e.key === 'Enter' && !submitting && handleSwitchAccount(account.did)} 139 > 140 <div class="account-info"> 141 <span class="account-handle">@{account.handle}</span> 142 <span class="account-did">{account.did}</span> 143 </div> 144 <button 145 type="button" 146 class="forget-btn" 147 onclick={(e) => handleForgetAccount(account.did, e)} 148 title={$_('login.removeAccount')} 149 > 150 &times; 151 </button> 152 </div> 153 {/each} 154 </div> 155 156 <p class="or-divider">{$_('login.signInToAnother')}</p> 157 {/if} 158 159 <button type="button" class="oauth-btn" onclick={handleOAuthLogin} disabled={submitting || auth.loading}> 160 {submitting ? $_('login.redirecting') : $_('login.button')} 161 </button> 162 163 <p class="forgot-links"> 164 <a href="#/reset-password">{$_('login.forgotPassword')}</a> 165 <span class="separator">&middot;</span> 166 <a href="#/request-passkey-recovery">{$_('login.lostPasskey')}</a> 167 </p> 168 169 <p class="link-text"> 170 {$_('login.noAccount')} <a href="#/register">{$_('login.createAccount')}</a> 171 </p> 172 </div> 173 174 <aside class="info-panel"> 175 {#if auth.savedAccounts.length > 0} 176 <h3>{$_('login.infoSavedAccountsTitle')}</h3> 177 <p>{$_('login.infoSavedAccountsDesc')}</p> 178 179 <h3>{$_('login.infoNewAccountTitle')}</h3> 180 <p>{$_('login.infoNewAccountDesc')}</p> 181 {:else} 182 <h3>{$_('login.infoSecureSignInTitle')}</h3> 183 <p>{$_('login.infoSecureSignInDesc')}</p> 184 185 <h3>{$_('login.infoStaySignedInTitle')}</h3> 186 <p>{$_('login.infoStaySignedInDesc')}</p> 187 {/if} 188 189 <h3>{$_('login.infoRecoveryTitle')}</h3> 190 <p>{$_('login.infoRecoveryDesc')}</p> 191 </aside> 192 </div> 193 {/if} 194</div> 195 196<style> 197 .login-page { 198 max-width: var(--width-lg); 199 margin: var(--space-9) auto; 200 padding: var(--space-7); 201 } 202 203 .page-header { 204 margin-bottom: var(--space-6); 205 } 206 207 h1 { 208 margin: 0 0 var(--space-3) 0; 209 } 210 211 .subtitle { 212 color: var(--text-secondary); 213 margin: 0; 214 } 215 216 .main-section { 217 min-width: 0; 218 } 219 220 form { 221 display: flex; 222 flex-direction: column; 223 gap: var(--space-4); 224 max-width: var(--width-sm); 225 } 226 227 .actions { 228 display: flex; 229 flex-direction: column; 230 gap: var(--space-3); 231 margin-top: var(--space-3); 232 } 233 234 @media (min-width: 600px) { 235 .actions { 236 flex-direction: row; 237 } 238 239 .actions button { 240 flex: 1; 241 } 242 } 243 244 .oauth-btn { 245 width: 100%; 246 padding: var(--space-5); 247 font-size: var(--text-lg); 248 } 249 250 .forgot-links { 251 margin-top: var(--space-4); 252 font-size: var(--text-sm); 253 color: var(--text-secondary); 254 } 255 256 .forgot-links a { 257 color: var(--accent); 258 } 259 260 .separator { 261 margin: 0 var(--space-2); 262 } 263 264 .link-text { 265 margin-top: var(--space-6); 266 font-size: var(--text-sm); 267 color: var(--text-secondary); 268 } 269 270 .link-text a { 271 color: var(--accent); 272 } 273 274 .saved-accounts { 275 display: flex; 276 flex-direction: column; 277 gap: var(--space-3); 278 margin-bottom: var(--space-5); 279 } 280 281 .account-item { 282 display: flex; 283 align-items: center; 284 justify-content: space-between; 285 padding: var(--space-5); 286 background: var(--bg-card); 287 border: 1px solid var(--border-color); 288 border-radius: var(--radius-xl); 289 cursor: pointer; 290 transition: border-color var(--transition-normal), box-shadow var(--transition-normal); 291 } 292 293 .account-item:hover:not(.disabled) { 294 border-color: var(--accent); 295 box-shadow: var(--shadow-md); 296 } 297 298 .account-item.disabled { 299 opacity: 0.6; 300 cursor: not-allowed; 301 } 302 303 .account-info { 304 display: flex; 305 flex-direction: column; 306 gap: var(--space-1); 307 } 308 309 .account-handle { 310 font-weight: var(--font-medium); 311 color: var(--text-primary); 312 } 313 314 .account-did { 315 font-size: var(--text-xs); 316 color: var(--text-muted); 317 font-family: ui-monospace, monospace; 318 overflow: hidden; 319 text-overflow: ellipsis; 320 max-width: 250px; 321 } 322 323 .forget-btn { 324 padding: var(--space-2) var(--space-3); 325 background: transparent; 326 border: none; 327 color: var(--text-muted); 328 cursor: pointer; 329 font-size: var(--text-xl); 330 line-height: 1; 331 border-radius: var(--radius-md); 332 } 333 334 .forget-btn:hover { 335 background: var(--error-bg); 336 color: var(--error-text); 337 } 338 339 .or-divider { 340 text-align: center; 341 color: var(--text-muted); 342 font-size: var(--text-sm); 343 margin: var(--space-5) 0; 344 } 345</style>