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