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>