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