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>