this repo has no description
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>