this repo has no description
1<script lang="ts">
2 import { getAuthState, logout } from '../lib/auth.svelte'
3 import { navigate, routes, getFullUrl } from '../lib/router.svelte'
4 import { generateCodeVerifier, generateCodeChallenge, saveOAuthState, generateState } from '../lib/oauth'
5 import { _ } from '../lib/i18n'
6 import type { Session } from '../lib/types/api'
7
8 const auth = $derived(getAuthState())
9
10 function getSession(): Session | null {
11 return auth.kind === 'authenticated' ? auth.session : null
12 }
13
14 function isLoading(): boolean {
15 return auth.kind === 'loading'
16 }
17
18 const session = $derived(getSession())
19 const authLoading = $derived(isLoading())
20 let error = $state<string | null>(null)
21 let loading = $state(true)
22 let actAsInProgress = $state(false)
23
24 function getDid(): string | null {
25 const params = new URLSearchParams(window.location.search)
26 return params.get('did')
27 }
28
29 $effect(() => {
30 if (!authLoading && !session && !actAsInProgress) {
31 navigate(routes.login)
32 }
33 })
34
35 $effect(() => {
36 if (session && !actAsInProgress) {
37 actAsInProgress = true
38 initiateActAs()
39 }
40 })
41
42 async function initiateActAs() {
43 const did = getDid()
44 if (!did) {
45 error = $_('actAs.noAccountSpecified')
46 loading = false
47 return
48 }
49
50 try {
51 const response = await fetch(
52 `/xrpc/_delegation.listControlledAccounts`,
53 {
54 headers: { 'Authorization': `Bearer ${session!.accessJwt}` }
55 }
56 )
57
58 if (!response.ok) {
59 error = $_('actAs.failedToVerify')
60 loading = false
61 return
62 }
63
64 const data = await response.json()
65 const account = data.accounts?.find((a: { did: string }) => a.did === did)
66
67 if (!account) {
68 error = $_('actAs.noAccess')
69 loading = false
70 return
71 }
72
73 await logout()
74
75 const hostname = window.location.origin
76 const state = generateState()
77 const codeVerifier = generateCodeVerifier()
78 const codeChallenge = await generateCodeChallenge(codeVerifier)
79 saveOAuthState({ state, codeVerifier })
80
81 const parResponse = await fetch('/oauth/par', {
82 method: 'POST',
83 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
84 body: new URLSearchParams({
85 client_id: `${hostname}/oauth/client-metadata.json`,
86 redirect_uri: `${hostname}/`,
87 response_type: 'code',
88 scope: 'atproto',
89 state: state,
90 code_challenge: codeChallenge,
91 code_challenge_method: 'S256',
92 login_hint: account.handle
93 })
94 })
95
96 if (!parResponse.ok) {
97 error = $_('actAs.failedToInitiate')
98 loading = false
99 return
100 }
101
102 const parData = await parResponse.json()
103 if (parData.request_uri) {
104 window.location.href = `/app/oauth/login?request_uri=${encodeURIComponent(parData.request_uri)}`
105 } else {
106 error = $_('actAs.invalidResponse')
107 loading = false
108 }
109 } catch (e) {
110 error = $_('actAs.failedError', { values: { error: e instanceof Error ? e.message : String(e) } })
111 loading = false
112 }
113 }
114
115 function goBack() {
116 navigate('/controllers')
117 }
118</script>
119
120<div class="page">
121 {#if loading}
122 <div class="loading">
123 <p>{$_('actAs.preparing')}</p>
124 </div>
125 {:else}
126 <header>
127 <h1>{$_('actAs.title')}</h1>
128 </header>
129
130 {#if error}
131 <div class="message error">{error}</div>
132 {/if}
133
134 <div class="actions">
135 <button class="back-btn" onclick={goBack}>
136 {$_('actAs.backToControllers')}
137 </button>
138 </div>
139 {/if}
140</div>
141
142<style>
143 .page {
144 max-width: var(--width-md);
145 margin: var(--space-9) auto;
146 padding: var(--space-7);
147 }
148
149 .loading {
150 display: flex;
151 align-items: center;
152 justify-content: center;
153 min-height: 200px;
154 color: var(--text-secondary);
155 }
156
157 header {
158 margin-bottom: var(--space-6);
159 }
160
161 h1 {
162 margin: 0;
163 }
164
165 .message.error {
166 padding: var(--space-3);
167 background: var(--error-bg);
168 border: 1px solid var(--error-border);
169 border-radius: var(--radius-md);
170 color: var(--error-text);
171 margin-bottom: var(--space-4);
172 }
173
174 .actions {
175 margin-top: var(--space-4);
176 }
177
178 .back-btn {
179 padding: var(--space-3) var(--space-5);
180 border: 1px solid var(--border-color);
181 border-radius: var(--radius-md);
182 background: transparent;
183 color: var(--text-primary);
184 cursor: pointer;
185 }
186
187 .back-btn:hover {
188 background: var(--bg-card);
189 border-color: var(--accent);
190 }
191</style>