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() { 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 137 } catch (e) { 138 state.error = e instanceof Error ? e.message : 'OAuth login failed' 139 state.loading = false 140 return 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} 179 180export async function login(identifier: string, password: string): Promise<void> { 181 state.loading = true 182 state.error = null 183 try { 184 const session = await api.createSession(identifier, password) 185 state.session = session 186 saveSession(session) 187 addOrUpdateSavedAccount(session) 188 } catch (e) { 189 if (e instanceof ApiError) { 190 state.error = e.message 191 } else { 192 state.error = 'Login failed' 193 } 194 throw e 195 } finally { 196 state.loading = false 197 } 198} 199 200export async function loginWithOAuth(): Promise<void> { 201 state.loading = true 202 state.error = null 203 try { 204 await startOAuthLogin() 205 } catch (e) { 206 state.loading = false 207 state.error = e instanceof Error ? e.message : 'Failed to start OAuth login' 208 throw e 209 } 210} 211 212export async function register(params: CreateAccountParams): Promise<CreateAccountResult> { 213 try { 214 const result = await api.createAccount(params) 215 return result 216 } catch (e) { 217 if (e instanceof ApiError) { 218 state.error = e.message 219 } else { 220 state.error = 'Registration failed' 221 } 222 throw e 223 } 224} 225 226export async function confirmSignup(did: string, verificationCode: string): Promise<void> { 227 state.loading = true 228 state.error = null 229 try { 230 const result = await api.confirmSignup(did, verificationCode) 231 const session: Session = { 232 did: result.did, 233 handle: result.handle, 234 accessJwt: result.accessJwt, 235 refreshJwt: result.refreshJwt, 236 email: result.email, 237 emailConfirmed: result.emailConfirmed, 238 preferredChannel: result.preferredChannel, 239 preferredChannelVerified: result.preferredChannelVerified, 240 } 241 state.session = session 242 saveSession(session) 243 addOrUpdateSavedAccount(session) 244 } catch (e) { 245 if (e instanceof ApiError) { 246 state.error = e.message 247 } else { 248 state.error = 'Verification failed' 249 } 250 throw e 251 } finally { 252 state.loading = false 253 } 254} 255 256export async function resendVerification(did: string): Promise<void> { 257 try { 258 await api.resendVerification(did) 259 } catch (e) { 260 if (e instanceof ApiError) { 261 throw e 262 } 263 throw new Error('Failed to resend verification code') 264 } 265} 266 267export async function logout(): Promise<void> { 268 if (state.session) { 269 try { 270 await api.deleteSession(state.session.accessJwt) 271 } catch { 272 // Ignore errors on logout 273 } 274 } 275 state.session = null 276 saveSession(null) 277} 278 279export async function switchAccount(did: string): Promise<void> { 280 const account = state.savedAccounts.find(a => a.did === did) 281 if (!account) { 282 throw new Error('Account not found') 283 } 284 state.loading = true 285 state.error = null 286 try { 287 const session = await api.getSession(account.accessJwt) 288 state.session = { ...session, accessJwt: account.accessJwt, refreshJwt: account.refreshJwt } 289 saveSession(state.session) 290 addOrUpdateSavedAccount(state.session) 291 } catch (e) { 292 if (e instanceof ApiError && e.status === 401) { 293 try { 294 const tokens = await refreshOAuthToken(account.refreshJwt) 295 const sessionInfo = await api.getSession(tokens.access_token) 296 const session: Session = { 297 ...sessionInfo, 298 accessJwt: tokens.access_token, 299 refreshJwt: tokens.refresh_token || account.refreshJwt, 300 } 301 state.session = session 302 saveSession(session) 303 addOrUpdateSavedAccount(session) 304 } catch { 305 removeSavedAccount(did) 306 state.error = 'Session expired. Please log in again.' 307 throw new Error('Session expired') 308 } 309 } else { 310 state.error = 'Failed to switch account' 311 throw e 312 } 313 } finally { 314 state.loading = false 315 } 316} 317 318export function forgetAccount(did: string): void { 319 removeSavedAccount(did) 320} 321 322export function getAuthState() { 323 return state 324} 325 326export async function refreshSession(): Promise<void> { 327 if (!state.session) return 328 try { 329 const sessionInfo = await api.getSession(state.session.accessJwt) 330 state.session = { 331 ...sessionInfo, 332 accessJwt: state.session.accessJwt, 333 refreshJwt: state.session.refreshJwt, 334 } 335 saveSession(state.session) 336 addOrUpdateSavedAccount(state.session) 337 } catch (e) { 338 console.error('Failed to refresh session:', e) 339 } 340} 341 342export function getToken(): string | null { 343 return state.session?.accessJwt ?? null 344} 345 346export async function getValidToken(): Promise<string | null> { 347 if (!state.session) return null 348 try { 349 await api.getSession(state.session.accessJwt) 350 return state.session.accessJwt 351 } catch (e) { 352 if (e instanceof ApiError && e.status === 401) { 353 try { 354 const tokens = await refreshOAuthToken(state.session.refreshJwt) 355 const sessionInfo = await api.getSession(tokens.access_token) 356 const session: Session = { 357 ...sessionInfo, 358 accessJwt: tokens.access_token, 359 refreshJwt: tokens.refresh_token || state.session.refreshJwt, 360 } 361 state.session = session 362 saveSession(session) 363 addOrUpdateSavedAccount(session) 364 return session.accessJwt 365 } catch { 366 return null 367 } 368 } 369 return null 370 } 371} 372 373export function isAuthenticated(): boolean { 374 return state.session !== null 375} 376 377export function _testSetState(newState: { session: Session | null; loading: boolean; error: string | null; savedAccounts?: SavedAccount[] }) { 378 state.session = newState.session 379 state.loading = newState.loading 380 state.error = newState.error 381 state.savedAccounts = newState.savedAccounts ?? [] 382} 383 384export function _testReset() { 385 state.session = null 386 state.loading = true 387 state.error = null 388 state.savedAccounts = [] 389 localStorage.removeItem(STORAGE_KEY) 390 localStorage.removeItem(ACCOUNTS_KEY) 391}