this repo has no description
1<script lang="ts"> 2 import { navigate } from '../lib/router.svelte' 3 import { _ } from '../lib/i18n' 4 5 interface AccountInfo { 6 did: string 7 handle: string 8 email: string 9 } 10 11 let loading = $state(true) 12 let error = $state<string | null>(null) 13 let submitting = $state(false) 14 let accounts = $state<AccountInfo[]>([]) 15 16 function getRequestUri(): string | null { 17 const params = new URLSearchParams(window.location.hash.split('?')[1] || '') 18 return params.get('request_uri') 19 } 20 21 async function fetchAccounts() { 22 const requestUri = getRequestUri() 23 if (!requestUri) { 24 error = 'Missing request_uri parameter' 25 loading = false 26 return 27 } 28 29 try { 30 const response = await fetch(`/oauth/authorize/accounts?request_uri=${encodeURIComponent(requestUri)}`) 31 if (!response.ok) { 32 const data = await response.json() 33 error = data.error_description || data.error || 'Failed to load accounts' 34 loading = false 35 return 36 } 37 const data = await response.json() 38 accounts = data.accounts || [] 39 } catch { 40 error = 'Failed to connect to server' 41 } finally { 42 loading = false 43 } 44 } 45 46 async function handleSelectAccount(did: string) { 47 const requestUri = getRequestUri() 48 if (!requestUri) { 49 error = 'Missing request_uri parameter' 50 return 51 } 52 53 submitting = true 54 error = null 55 56 try { 57 const response = await fetch('/oauth/authorize/select', { 58 method: 'POST', 59 headers: { 60 'Content-Type': 'application/json', 61 'Accept': 'application/json' 62 }, 63 body: JSON.stringify({ 64 request_uri: requestUri, 65 did 66 }) 67 }) 68 69 const data = await response.json() 70 71 if (!response.ok) { 72 error = data.error_description || data.error || 'Selection failed' 73 submitting = false 74 return 75 } 76 77 if (data.needs_totp) { 78 navigate(`/oauth/totp?request_uri=${encodeURIComponent(requestUri)}`) 79 return 80 } 81 82 if (data.needs_2fa) { 83 navigate(`/oauth/2fa?request_uri=${encodeURIComponent(requestUri)}&channel=${encodeURIComponent(data.channel || '')}`) 84 return 85 } 86 87 if (data.redirect_uri) { 88 window.location.href = data.redirect_uri 89 return 90 } 91 92 error = 'Unexpected response from server' 93 submitting = false 94 } catch { 95 error = 'Failed to connect to server' 96 submitting = false 97 } 98 } 99 100 function handleDifferentAccount() { 101 const requestUri = getRequestUri() 102 if (requestUri) { 103 navigate(`/oauth/login?request_uri=${encodeURIComponent(requestUri)}`) 104 } else { 105 navigate('/oauth/login') 106 } 107 } 108 109 $effect(() => { 110 fetchAccounts() 111 }) 112</script> 113 114<div class="oauth-accounts-container"> 115 {#if loading} 116 <div class="loading"> 117 <p>{$_('common.loading')}</p> 118 </div> 119 {:else if error} 120 <div class="error-container"> 121 <h1>Error</h1> 122 <div class="error">{error}</div> 123 <button type="button" onclick={handleDifferentAccount}> 124 {$_('oauth.accounts.useAnother')} 125 </button> 126 </div> 127 {:else} 128 <h1>{$_('oauth.accounts.title')}</h1> 129 <p class="subtitle">{$_('oauth.accounts.subtitle')}</p> 130 131 <div class="accounts-list"> 132 {#each accounts as account} 133 <button 134 type="button" 135 class="account-item" 136 class:disabled={submitting} 137 onclick={() => !submitting && handleSelectAccount(account.did)} 138 > 139 <div class="account-info"> 140 <span class="account-handle">@{account.handle}</span> 141 <span class="account-email">{account.email}</span> 142 </div> 143 </button> 144 {/each} 145 </div> 146 147 <button type="button" class="secondary different-account" onclick={handleDifferentAccount}> 148 {$_('oauth.accounts.useAnother')} 149 </button> 150 {/if} 151</div> 152 153<style> 154 .oauth-accounts-container { 155 max-width: var(--width-sm); 156 margin: var(--space-9) auto; 157 padding: var(--space-7); 158 } 159 160 h1 { 161 margin: 0 0 var(--space-2) 0; 162 } 163 164 .subtitle { 165 color: var(--text-secondary); 166 margin: 0 0 var(--space-7) 0; 167 } 168 169 .loading { 170 display: flex; 171 align-items: center; 172 justify-content: center; 173 min-height: 200px; 174 color: var(--text-secondary); 175 } 176 177 .error-container { 178 text-align: center; 179 } 180 181 .error { 182 padding: var(--space-3); 183 background: var(--error-bg); 184 border: 1px solid var(--error-border); 185 border-radius: var(--radius-md); 186 color: var(--error-text); 187 margin-bottom: var(--space-4); 188 } 189 190 .accounts-list { 191 display: flex; 192 flex-direction: column; 193 gap: var(--space-2); 194 margin-bottom: var(--space-4); 195 } 196 197 .account-item { 198 display: flex; 199 align-items: center; 200 padding: var(--space-4); 201 background: var(--bg-card); 202 border: 1px solid var(--border-color); 203 border-radius: var(--radius-xl); 204 cursor: pointer; 205 text-align: left; 206 width: 100%; 207 transition: border-color var(--transition-fast), box-shadow var(--transition-fast); 208 } 209 210 .account-item:hover:not(.disabled) { 211 border-color: var(--accent); 212 box-shadow: var(--shadow-sm); 213 } 214 215 .account-item.disabled { 216 opacity: 0.6; 217 cursor: not-allowed; 218 } 219 220 .account-info { 221 display: flex; 222 flex-direction: column; 223 gap: var(--space-1); 224 } 225 226 .account-handle { 227 font-weight: var(--font-medium); 228 color: var(--text-primary); 229 } 230 231 .account-email { 232 font-size: var(--text-sm); 233 color: var(--text-secondary); 234 } 235 236 button { 237 padding: var(--space-3); 238 background: var(--accent); 239 color: var(--text-inverse); 240 border: none; 241 border-radius: var(--radius-md); 242 font-size: var(--text-base); 243 cursor: pointer; 244 } 245 246 button:hover:not(:disabled) { 247 background: var(--accent-hover); 248 } 249 250 button:disabled { 251 opacity: 0.6; 252 cursor: not-allowed; 253 } 254 255 button.secondary { 256 background: transparent; 257 color: var(--accent); 258 border: 1px solid var(--accent); 259 width: 100%; 260 } 261 262 button.secondary:hover:not(:disabled) { 263 background: var(--accent); 264 color: var(--text-inverse); 265 } 266 267 .different-account { 268 margin-top: var(--space-4); 269 } 270</style>