this repo has no description
at main 6.2 kB view raw
1<script lang="ts"> 2 import { navigate, routes } 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.search) 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(routes.oauthTotp, { params: { request_uri: requestUri } }) 79 return 80 } 81 82 if (data.needs_2fa) { 83 navigate(routes.oauth2fa, { params: { request_uri: requestUri, channel: 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(routes.oauthLogin, { params: { request_uri: requestUri } }) 104 } else { 105 navigate(routes.oauthLogin) 106 } 107 } 108 109 $effect(() => { 110 fetchAccounts() 111 }) 112</script> 113 114<div class="oauth-accounts-container"> 115 {#if loading} 116 <div class="loading"></div> 117 {:else if error} 118 <div class="error-container"> 119 <h1>Error</h1> 120 <div class="error">{error}</div> 121 <button type="button" onclick={handleDifferentAccount}> 122 {$_('oauth.accounts.useAnother')} 123 </button> 124 </div> 125 {:else} 126 <h1>{$_('oauth.accounts.title')}</h1> 127 <p class="subtitle">{$_('oauth.accounts.subtitle')}</p> 128 129 <div class="accounts-list"> 130 {#each accounts as account} 131 <button 132 type="button" 133 class="account-item" 134 class:disabled={submitting} 135 onclick={() => !submitting && handleSelectAccount(account.did)} 136 > 137 <div class="account-info"> 138 <span class="account-handle">@{account.handle}</span> 139 <span class="account-email">{account.email}</span> 140 </div> 141 </button> 142 {/each} 143 </div> 144 145 <button type="button" class="secondary different-account" onclick={handleDifferentAccount}> 146 {$_('oauth.accounts.useAnother')} 147 </button> 148 {/if} 149</div> 150 151<style> 152 .oauth-accounts-container { 153 max-width: var(--width-sm); 154 margin: var(--space-9) auto; 155 padding: var(--space-7); 156 } 157 158 h1 { 159 margin: 0 0 var(--space-2) 0; 160 } 161 162 .subtitle { 163 color: var(--text-secondary); 164 margin: 0 0 var(--space-7) 0; 165 } 166 167 .loading { 168 display: flex; 169 align-items: center; 170 justify-content: center; 171 min-height: 200px; 172 color: var(--text-secondary); 173 } 174 175 .error-container { 176 text-align: center; 177 } 178 179 .error { 180 padding: var(--space-3); 181 background: var(--error-bg); 182 border: 1px solid var(--error-border); 183 border-radius: var(--radius-md); 184 color: var(--error-text); 185 margin-bottom: var(--space-4); 186 } 187 188 .accounts-list { 189 display: flex; 190 flex-direction: column; 191 gap: var(--space-2); 192 margin-bottom: var(--space-4); 193 } 194 195 .account-item { 196 display: flex; 197 align-items: center; 198 padding: var(--space-4); 199 background: var(--bg-card); 200 border: 1px solid var(--border-color); 201 border-radius: var(--radius-xl); 202 cursor: pointer; 203 text-align: left; 204 width: 100%; 205 transition: border-color var(--transition-fast), box-shadow var(--transition-fast); 206 } 207 208 .account-item:hover:not(.disabled) { 209 border-color: var(--accent); 210 box-shadow: var(--shadow-sm); 211 } 212 213 .account-item.disabled { 214 opacity: 0.6; 215 cursor: not-allowed; 216 } 217 218 .account-info { 219 display: flex; 220 flex-direction: column; 221 gap: var(--space-1); 222 } 223 224 .account-handle { 225 font-weight: var(--font-medium); 226 color: var(--text-primary); 227 } 228 229 .account-email { 230 font-size: var(--text-sm); 231 color: var(--text-secondary); 232 } 233 234 button { 235 padding: var(--space-3); 236 background: var(--accent); 237 color: var(--text-inverse); 238 border: none; 239 border-radius: var(--radius-md); 240 font-size: var(--text-base); 241 cursor: pointer; 242 } 243 244 button:hover:not(:disabled) { 245 background: var(--accent-hover); 246 } 247 248 button:disabled { 249 opacity: 0.6; 250 cursor: not-allowed; 251 } 252 253 button.secondary { 254 background: transparent; 255 color: var(--accent); 256 border: 1px solid var(--accent); 257 width: 100%; 258 } 259 260 button.secondary:hover:not(:disabled) { 261 background: var(--accent); 262 color: var(--text-inverse); 263 } 264 265 .different-account { 266 margin-top: var(--space-4); 267 } 268</style>