this repo has no description
1import { api, type Session, type CreateAccountParams, type CreateAccountResult, ApiError } from './api'
2import { startOAuthLogin, handleOAuthCallback, checkForOAuthCallback, clearOAuthCallbackParams, refreshOAuthToken } from './oauth'
3
4const STORAGE_KEY = 'bspds_session'
5const ACCOUNTS_KEY = 'bspds_accounts'
6
7export interface SavedAccount {
8 did: string
9 handle: string
10 accessJwt: string
11 refreshJwt: string
12}
13
14interface AuthState {
15 session: Session | null
16 loading: boolean
17 error: string | null
18 savedAccounts: SavedAccount[]
19}
20
21let state = $state<AuthState>({
22 session: null,
23 loading: true,
24 error: null,
25 savedAccounts: [],
26})
27
28function saveSession(session: Session | null) {
29 if (session) {
30 localStorage.setItem(STORAGE_KEY, JSON.stringify(session))
31 } else {
32 localStorage.removeItem(STORAGE_KEY)
33 }
34}
35
36function loadSession(): Session | null {
37 const stored = localStorage.getItem(STORAGE_KEY)
38 if (stored) {
39 try {
40 return JSON.parse(stored)
41 } catch {
42 return null
43 }
44 }
45 return null
46}
47
48function loadSavedAccounts(): SavedAccount[] {
49 const stored = localStorage.getItem(ACCOUNTS_KEY)
50 if (stored) {
51 try {
52 return JSON.parse(stored)
53 } catch {
54 return []
55 }
56 }
57 return []
58}
59
60function saveSavedAccounts(accounts: SavedAccount[]) {
61 localStorage.setItem(ACCOUNTS_KEY, JSON.stringify(accounts))
62}
63
64function addOrUpdateSavedAccount(session: Session) {
65 const accounts = loadSavedAccounts()
66 const existing = accounts.findIndex(a => a.did === session.did)
67 const savedAccount: SavedAccount = {
68 did: session.did,
69 handle: session.handle,
70 accessJwt: session.accessJwt,
71 refreshJwt: session.refreshJwt,
72 }
73 if (existing >= 0) {
74 accounts[existing] = savedAccount
75 } else {
76 accounts.push(savedAccount)
77 }
78 saveSavedAccounts(accounts)
79 state.savedAccounts = accounts
80}
81
82function removeSavedAccount(did: string) {
83 const accounts = loadSavedAccounts().filter(a => a.did !== did)
84 saveSavedAccounts(accounts)
85 state.savedAccounts = accounts
86}
87
88export async function initAuth() {
89 state.loading = true
90 state.error = null
91 state.savedAccounts = loadSavedAccounts()
92
93 const oauthCallback = checkForOAuthCallback()
94 if (oauthCallback) {
95 clearOAuthCallbackParams()
96 try {
97 const tokens = await handleOAuthCallback(oauthCallback.code, oauthCallback.state)
98 const sessionInfo = await api.getSession(tokens.access_token)
99 const session: Session = {
100 ...sessionInfo,
101 accessJwt: tokens.access_token,
102 refreshJwt: tokens.refresh_token || '',
103 }
104 state.session = session
105 saveSession(session)
106 addOrUpdateSavedAccount(session)
107 state.loading = false
108 return
109 } catch (e) {
110 state.error = e instanceof Error ? e.message : 'OAuth login failed'
111 state.loading = false
112 return
113 }
114 }
115
116 const stored = loadSession()
117 if (stored) {
118 try {
119 const session = await api.getSession(stored.accessJwt)
120 state.session = { ...session, accessJwt: stored.accessJwt, refreshJwt: stored.refreshJwt }
121 addOrUpdateSavedAccount(state.session)
122 } catch (e) {
123 if (e instanceof ApiError && e.status === 401) {
124 try {
125 const tokens = await refreshOAuthToken(stored.refreshJwt)
126 const sessionInfo = await api.getSession(tokens.access_token)
127 const session: Session = {
128 ...sessionInfo,
129 accessJwt: tokens.access_token,
130 refreshJwt: tokens.refresh_token || stored.refreshJwt,
131 }
132 state.session = session
133 saveSession(session)
134 addOrUpdateSavedAccount(session)
135 } catch {
136 saveSession(null)
137 state.session = null
138 }
139 } else {
140 saveSession(null)
141 state.session = null
142 }
143 }
144 }
145 state.loading = false
146}
147
148export async function login(identifier: string, password: string): Promise<void> {
149 state.loading = true
150 state.error = null
151 try {
152 const session = await api.createSession(identifier, password)
153 state.session = session
154 saveSession(session)
155 addOrUpdateSavedAccount(session)
156 } catch (e) {
157 if (e instanceof ApiError) {
158 state.error = e.message
159 } else {
160 state.error = 'Login failed'
161 }
162 throw e
163 } finally {
164 state.loading = false
165 }
166}
167
168export async function loginWithOAuth(): Promise<void> {
169 state.loading = true
170 state.error = null
171 try {
172 await startOAuthLogin()
173 } catch (e) {
174 state.loading = false
175 state.error = e instanceof Error ? e.message : 'Failed to start OAuth login'
176 throw e
177 }
178}
179
180export async function register(params: CreateAccountParams): Promise<CreateAccountResult> {
181 try {
182 const result = await api.createAccount(params)
183 return result
184 } catch (e) {
185 if (e instanceof ApiError) {
186 state.error = e.message
187 } else {
188 state.error = 'Registration failed'
189 }
190 throw e
191 }
192}
193
194export async function confirmSignup(did: string, verificationCode: string): Promise<void> {
195 state.loading = true
196 state.error = null
197 try {
198 const result = await api.confirmSignup(did, verificationCode)
199 const session: Session = {
200 did: result.did,
201 handle: result.handle,
202 accessJwt: result.accessJwt,
203 refreshJwt: result.refreshJwt,
204 email: result.email,
205 emailConfirmed: result.emailConfirmed,
206 preferredChannel: result.preferredChannel,
207 preferredChannelVerified: result.preferredChannelVerified,
208 }
209 state.session = session
210 saveSession(session)
211 addOrUpdateSavedAccount(session)
212 } catch (e) {
213 if (e instanceof ApiError) {
214 state.error = e.message
215 } else {
216 state.error = 'Verification failed'
217 }
218 throw e
219 } finally {
220 state.loading = false
221 }
222}
223
224export async function resendVerification(did: string): Promise<void> {
225 try {
226 await api.resendVerification(did)
227 } catch (e) {
228 if (e instanceof ApiError) {
229 throw e
230 }
231 throw new Error('Failed to resend verification code')
232 }
233}
234
235export async function logout(): Promise<void> {
236 if (state.session) {
237 try {
238 await api.deleteSession(state.session.accessJwt)
239 } catch {
240 // Ignore errors on logout
241 }
242 }
243 state.session = null
244 saveSession(null)
245}
246
247export async function switchAccount(did: string): Promise<void> {
248 const account = state.savedAccounts.find(a => a.did === did)
249 if (!account) {
250 throw new Error('Account not found')
251 }
252 state.loading = true
253 state.error = null
254 try {
255 const session = await api.getSession(account.accessJwt)
256 state.session = { ...session, accessJwt: account.accessJwt, refreshJwt: account.refreshJwt }
257 saveSession(state.session)
258 addOrUpdateSavedAccount(state.session)
259 } catch (e) {
260 if (e instanceof ApiError && e.status === 401) {
261 try {
262 const tokens = await refreshOAuthToken(account.refreshJwt)
263 const sessionInfo = await api.getSession(tokens.access_token)
264 const session: Session = {
265 ...sessionInfo,
266 accessJwt: tokens.access_token,
267 refreshJwt: tokens.refresh_token || account.refreshJwt,
268 }
269 state.session = session
270 saveSession(session)
271 addOrUpdateSavedAccount(session)
272 } catch {
273 removeSavedAccount(did)
274 state.error = 'Session expired. Please log in again.'
275 throw new Error('Session expired')
276 }
277 } else {
278 state.error = 'Failed to switch account'
279 throw e
280 }
281 } finally {
282 state.loading = false
283 }
284}
285
286export function forgetAccount(did: string): void {
287 removeSavedAccount(did)
288}
289
290export function getAuthState() {
291 return state
292}
293
294export function getToken(): string | null {
295 return state.session?.accessJwt ?? null
296}
297
298export function isAuthenticated(): boolean {
299 return state.session !== null
300}
301
302export function _testSetState(newState: { session: Session | null; loading: boolean; error: string | null; savedAccounts?: SavedAccount[] }) {
303 state.session = newState.session
304 state.loading = newState.loading
305 state.error = newState.error
306 state.savedAccounts = newState.savedAccounts ?? []
307}
308
309export function _testReset() {
310 state.session = null
311 state.loading = true
312 state.error = null
313 state.savedAccounts = []
314 localStorage.removeItem(STORAGE_KEY)
315 localStorage.removeItem(ACCOUNTS_KEY)
316}