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_2fa) {
77 navigate(`/oauth/2fa?request_uri=${encodeURIComponent(requestUri)}&channel=${encodeURIComponent(data.channel || '')}`)
78 return
79 }
80
81 if (data.redirect_uri) {
82 window.location.href = data.redirect_uri
83 return
84 }
85
86 error = 'Unexpected response from server'
87 submitting = false
88 } catch {
89 error = 'Failed to connect to server'
90 submitting = false
91 }
92 }
93
94 function handleDifferentAccount() {
95 const requestUri = getRequestUri()
96 if (requestUri) {
97 navigate(`/oauth/login?request_uri=${encodeURIComponent(requestUri)}`)
98 } else {
99 navigate('/oauth/login')
100 }
101 }
102
103 $effect(() => {
104 fetchAccounts()
105 })
106</script>
107
108<div class="oauth-accounts-container">
109 {#if loading}
110 <div class="loading">
111 <p>Loading accounts...</p>
112 </div>
113 {:else if error}
114 <div class="error-container">
115 <h1>Error</h1>
116 <div class="error">{error}</div>
117 <button type="button" onclick={handleDifferentAccount}>
118 Sign in with different account
119 </button>
120 </div>
121 {:else}
122 <h1>Choose an Account</h1>
123 <p class="subtitle">Select an account to continue</p>
124
125 <div class="accounts-list">
126 {#each accounts as account}
127 <button
128 type="button"
129 class="account-item"
130 class:disabled={submitting}
131 onclick={() => !submitting && handleSelectAccount(account.did)}
132 >
133 <div class="account-info">
134 <span class="account-handle">@{account.handle}</span>
135 <span class="account-email">{account.email}</span>
136 </div>
137 </button>
138 {/each}
139 </div>
140
141 <button type="button" class="secondary different-account" onclick={handleDifferentAccount}>
142 Sign in to different account
143 </button>
144 {/if}
145</div>
146
147<style>
148 .oauth-accounts-container {
149 max-width: 400px;
150 margin: 4rem auto;
151 padding: 2rem;
152 }
153
154 h1 {
155 margin: 0 0 0.5rem 0;
156 }
157
158 .subtitle {
159 color: var(--text-secondary);
160 margin: 0 0 2rem 0;
161 }
162
163 .loading {
164 display: flex;
165 align-items: center;
166 justify-content: center;
167 min-height: 200px;
168 color: var(--text-secondary);
169 }
170
171 .error-container {
172 text-align: center;
173 }
174
175 .error {
176 padding: 0.75rem;
177 background: var(--error-bg);
178 border: 1px solid var(--error-border);
179 border-radius: 4px;
180 color: var(--error-text);
181 margin-bottom: 1rem;
182 }
183
184 .accounts-list {
185 display: flex;
186 flex-direction: column;
187 gap: 0.5rem;
188 margin-bottom: 1rem;
189 }
190
191 .account-item {
192 display: flex;
193 align-items: center;
194 padding: 1rem;
195 background: var(--bg-card);
196 border: 1px solid var(--border-color);
197 border-radius: 8px;
198 cursor: pointer;
199 text-align: left;
200 width: 100%;
201 transition: border-color 0.15s, box-shadow 0.15s;
202 }
203
204 .account-item:hover:not(.disabled) {
205 border-color: var(--accent);
206 box-shadow: 0 2px 8px rgba(77, 166, 255, 0.15);
207 }
208
209 .account-item.disabled {
210 opacity: 0.6;
211 cursor: not-allowed;
212 }
213
214 .account-info {
215 display: flex;
216 flex-direction: column;
217 gap: 0.25rem;
218 }
219
220 .account-handle {
221 font-weight: 500;
222 color: var(--text-primary);
223 }
224
225 .account-email {
226 font-size: 0.875rem;
227 color: var(--text-secondary);
228 }
229
230 button {
231 padding: 0.75rem;
232 background: var(--accent);
233 color: white;
234 border: none;
235 border-radius: 4px;
236 font-size: 1rem;
237 cursor: pointer;
238 }
239
240 button:hover:not(:disabled) {
241 background: var(--accent-hover);
242 }
243
244 button:disabled {
245 opacity: 0.6;
246 cursor: not-allowed;
247 }
248
249 button.secondary {
250 background: transparent;
251 color: var(--accent);
252 border: 1px solid var(--accent);
253 width: 100%;
254 }
255
256 button.secondary:hover:not(:disabled) {
257 background: var(--accent);
258 color: white;
259 }
260
261 .different-account {
262 margin-top: 1rem;
263 }
264</style>