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 async function logout(): Promise<void> { 269 if (state.session) { 270 try { 271 await api.deleteSession(state.session.accessJwt) 272 } catch { 273 // Ignore errors on logout 274 } 275 } 276 state.session = null 277 saveSession(null) 278} 279 280export async function switchAccount(did: string): Promise<void> { 281 const account = state.savedAccounts.find(a => a.did === did) 282 if (!account) { 283 throw new Error('Account not found') 284 } 285 state.loading = true 286 state.error = null 287 try { 288 const session = await api.getSession(account.accessJwt) 289 state.session = { ...session, accessJwt: account.accessJwt, refreshJwt: account.refreshJwt } 290 saveSession(state.session) 291 addOrUpdateSavedAccount(state.session) 292 } catch (e) { 293 if (e instanceof ApiError && e.status === 401) { 294 try { 295 const tokens = await refreshOAuthToken(account.refreshJwt) 296 const sessionInfo = await api.getSession(tokens.access_token) 297 const session: Session = { 298 ...sessionInfo, 299 accessJwt: tokens.access_token, 300 refreshJwt: tokens.refresh_token || account.refreshJwt, 301 } 302 state.session = session 303 saveSession(session) 304 addOrUpdateSavedAccount(session) 305 } catch { 306 removeSavedAccount(did) 307 state.error = 'Session expired. Please log in again.' 308 throw new Error('Session expired') 309 } 310 } else { 311 state.error = 'Failed to switch account' 312 throw e 313 } 314 } finally { 315 state.loading = false 316 } 317} 318 319export function forgetAccount(did: string): void { 320 removeSavedAccount(did) 321} 322 323export function getAuthState() { 324 return state 325} 326 327export async function refreshSession(): Promise<void> { 328 if (!state.session) return 329 try { 330 const sessionInfo = await api.getSession(state.session.accessJwt) 331 state.session = { 332 ...sessionInfo, 333 accessJwt: state.session.accessJwt, 334 refreshJwt: state.session.refreshJwt, 335 } 336 saveSession(state.session) 337 addOrUpdateSavedAccount(state.session) 338 } catch (e) { 339 console.error('Failed to refresh session:', e) 340 } 341} 342 343export function getToken(): string | null { 344 return state.session?.accessJwt ?? null 345} 346 347export async function getValidToken(): Promise<string | null> { 348 if (!state.session) return null 349 try { 350 await api.getSession(state.session.accessJwt) 351 return state.session.accessJwt 352 } catch (e) { 353 if (e instanceof ApiError && e.status === 401) { 354 try { 355 const tokens = await refreshOAuthToken(state.session.refreshJwt) 356 const sessionInfo = await api.getSession(tokens.access_token) 357 const session: Session = { 358 ...sessionInfo, 359 accessJwt: tokens.access_token, 360 refreshJwt: tokens.refresh_token || state.session.refreshJwt, 361 } 362 state.session = session 363 saveSession(session) 364 addOrUpdateSavedAccount(session) 365 return session.accessJwt 366 } catch { 367 return null 368 } 369 } 370 return null 371 } 372} 373 374export function isAuthenticated(): boolean { 375 return state.session !== null 376} 377 378export function _testSetState(newState: { session: Session | null; loading: boolean; error: string | null; savedAccounts?: SavedAccount[] }) { 379 state.session = newState.session 380 state.loading = newState.loading 381 state.error = newState.error 382 state.savedAccounts = newState.savedAccounts ?? [] 383} 384 385export function _testReset() { 386 state.session = null 387 state.loading = true 388 state.error = null 389 state.savedAccounts = [] 390 localStorage.removeItem(STORAGE_KEY) 391 localStorage.removeItem(ACCOUNTS_KEY) 392}