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}