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 43const 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 const did = state.session.did; 322 const refreshToken = state.session.refreshJwt; 323 try { 324 await fetch("/oauth/revoke", { 325 method: "POST", 326 headers: { "Content-Type": "application/x-www-form-urlencoded" }, 327 body: new URLSearchParams({ token: refreshToken }), 328 }); 329 } catch { 330 // Ignore errors on logout 331 } 332 removeSavedAccount(did); 333 } 334 state.session = null; 335 saveSession(null); 336} 337 338export async function switchAccount(did: string): Promise<void> { 339 const account = state.savedAccounts.find((a) => a.did === did); 340 if (!account) { 341 throw new Error("Account not found"); 342 } 343 state.loading = true; 344 state.error = null; 345 try { 346 const session = await api.getSession(account.accessJwt); 347 state.session = { 348 ...session, 349 accessJwt: account.accessJwt, 350 refreshJwt: account.refreshJwt, 351 }; 352 saveSession(state.session); 353 addOrUpdateSavedAccount(state.session); 354 } catch (e) { 355 if (e instanceof ApiError && e.status === 401) { 356 try { 357 const tokens = await refreshOAuthToken(account.refreshJwt); 358 const sessionInfo = await api.getSession(tokens.access_token); 359 const session: Session = { 360 ...sessionInfo, 361 accessJwt: tokens.access_token, 362 refreshJwt: tokens.refresh_token || account.refreshJwt, 363 }; 364 state.session = session; 365 saveSession(session); 366 addOrUpdateSavedAccount(session); 367 } catch { 368 removeSavedAccount(did); 369 state.error = "Session expired. Please log in again."; 370 throw new Error("Session expired"); 371 } 372 } else { 373 state.error = "Failed to switch account"; 374 throw e; 375 } 376 } finally { 377 state.loading = false; 378 } 379} 380 381export function forgetAccount(did: string): void { 382 removeSavedAccount(did); 383} 384 385export function getAuthState() { 386 return state; 387} 388 389export async function refreshSession(): Promise<void> { 390 if (!state.session) return; 391 try { 392 const sessionInfo = await api.getSession(state.session.accessJwt); 393 state.session = { 394 ...sessionInfo, 395 accessJwt: state.session.accessJwt, 396 refreshJwt: state.session.refreshJwt, 397 }; 398 saveSession(state.session); 399 addOrUpdateSavedAccount(state.session); 400 } catch (e) { 401 console.error("Failed to refresh session:", e); 402 } 403} 404 405export function getToken(): string | null { 406 return state.session?.accessJwt ?? null; 407} 408 409export async function getValidToken(): Promise<string | null> { 410 if (!state.session) return null; 411 try { 412 await api.getSession(state.session.accessJwt); 413 return state.session.accessJwt; 414 } catch (e) { 415 if (e instanceof ApiError && e.status === 401) { 416 try { 417 const tokens = await refreshOAuthToken(state.session.refreshJwt); 418 const sessionInfo = await api.getSession(tokens.access_token); 419 const session: Session = { 420 ...sessionInfo, 421 accessJwt: tokens.access_token, 422 refreshJwt: tokens.refresh_token || state.session.refreshJwt, 423 }; 424 state.session = session; 425 saveSession(session); 426 addOrUpdateSavedAccount(session); 427 return session.accessJwt; 428 } catch { 429 return null; 430 } 431 } 432 return null; 433 } 434} 435 436export function isAuthenticated(): boolean { 437 return state.session !== null; 438} 439 440export function _testSetState( 441 newState: { 442 session: Session | null; 443 loading: boolean; 444 error: string | null; 445 savedAccounts?: SavedAccount[]; 446 }, 447) { 448 state.session = newState.session; 449 state.loading = newState.loading; 450 state.error = newState.error; 451 state.savedAccounts = newState.savedAccounts ?? []; 452} 453 454export function _testResetState() { 455 state.session = null; 456 state.loading = true; 457 state.error = null; 458 state.savedAccounts = []; 459} 460 461export function _testReset() { 462 _testResetState(); 463 localStorage.removeItem(STORAGE_KEY); 464 localStorage.removeItem(ACCOUNTS_KEY); 465}