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