this repo has no description
1import { api, 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 95export async function initAuth() { 96 state.loading = true 97 state.error = null 98 state.savedAccounts = loadSavedAccounts() 99 100 const oauthCallback = checkForOAuthCallback() 101 if (oauthCallback) { 102 clearOAuthCallbackParams() 103 try { 104 const tokens = await handleOAuthCallback(oauthCallback.code, oauthCallback.state) 105 const sessionInfo = await api.getSession(tokens.access_token) 106 const session: Session = { 107 ...sessionInfo, 108 accessJwt: tokens.access_token, 109 refreshJwt: tokens.refresh_token || '', 110 } 111 state.session = session 112 saveSession(session) 113 addOrUpdateSavedAccount(session) 114 applyLocaleFromSession(sessionInfo) 115 state.loading = false 116 return 117 } catch (e) { 118 state.error = e instanceof Error ? e.message : 'OAuth login failed' 119 state.loading = false 120 return 121 } 122 } 123 124 const stored = loadSession() 125 if (stored) { 126 try { 127 const sessionInfo = await api.getSession(stored.accessJwt) 128 state.session = { ...sessionInfo, accessJwt: stored.accessJwt, refreshJwt: stored.refreshJwt } 129 addOrUpdateSavedAccount(state.session) 130 applyLocaleFromSession(sessionInfo) 131 } catch (e) { 132 if (e instanceof ApiError && e.status === 401) { 133 try { 134 const tokens = await refreshOAuthToken(stored.refreshJwt) 135 const sessionInfo = await api.getSession(tokens.access_token) 136 const session: Session = { 137 ...sessionInfo, 138 accessJwt: tokens.access_token, 139 refreshJwt: tokens.refresh_token || stored.refreshJwt, 140 } 141 state.session = session 142 saveSession(session) 143 addOrUpdateSavedAccount(session) 144 applyLocaleFromSession(sessionInfo) 145 } catch { 146 saveSession(null) 147 state.session = null 148 } 149 } else { 150 saveSession(null) 151 state.session = null 152 } 153 } 154 } 155 state.loading = false 156} 157 158export async function login(identifier: string, password: string): Promise<void> { 159 state.loading = true 160 state.error = null 161 try { 162 const session = await api.createSession(identifier, password) 163 state.session = session 164 saveSession(session) 165 addOrUpdateSavedAccount(session) 166 } catch (e) { 167 if (e instanceof ApiError) { 168 state.error = e.message 169 } else { 170 state.error = 'Login failed' 171 } 172 throw e 173 } finally { 174 state.loading = false 175 } 176} 177 178export async function loginWithOAuth(): Promise<void> { 179 state.loading = true 180 state.error = null 181 try { 182 await startOAuthLogin() 183 } catch (e) { 184 state.loading = false 185 state.error = e instanceof Error ? e.message : 'Failed to start OAuth login' 186 throw e 187 } 188} 189 190export async function register(params: CreateAccountParams): Promise<CreateAccountResult> { 191 try { 192 const result = await api.createAccount(params) 193 return result 194 } catch (e) { 195 if (e instanceof ApiError) { 196 state.error = e.message 197 } else { 198 state.error = 'Registration failed' 199 } 200 throw e 201 } 202} 203 204export async function confirmSignup(did: string, verificationCode: string): Promise<void> { 205 state.loading = true 206 state.error = null 207 try { 208 const result = await api.confirmSignup(did, verificationCode) 209 const session: Session = { 210 did: result.did, 211 handle: result.handle, 212 accessJwt: result.accessJwt, 213 refreshJwt: result.refreshJwt, 214 email: result.email, 215 emailConfirmed: result.emailConfirmed, 216 preferredChannel: result.preferredChannel, 217 preferredChannelVerified: result.preferredChannelVerified, 218 } 219 state.session = session 220 saveSession(session) 221 addOrUpdateSavedAccount(session) 222 } catch (e) { 223 if (e instanceof ApiError) { 224 state.error = e.message 225 } else { 226 state.error = 'Verification failed' 227 } 228 throw e 229 } finally { 230 state.loading = false 231 } 232} 233 234export async function resendVerification(did: string): Promise<void> { 235 try { 236 await api.resendVerification(did) 237 } catch (e) { 238 if (e instanceof ApiError) { 239 throw e 240 } 241 throw new Error('Failed to resend verification code') 242 } 243} 244 245export async function logout(): Promise<void> { 246 if (state.session) { 247 try { 248 await api.deleteSession(state.session.accessJwt) 249 } catch { 250 // Ignore errors on logout 251 } 252 } 253 state.session = null 254 saveSession(null) 255} 256 257export async function switchAccount(did: string): Promise<void> { 258 const account = state.savedAccounts.find(a => a.did === did) 259 if (!account) { 260 throw new Error('Account not found') 261 } 262 state.loading = true 263 state.error = null 264 try { 265 const session = await api.getSession(account.accessJwt) 266 state.session = { ...session, accessJwt: account.accessJwt, refreshJwt: account.refreshJwt } 267 saveSession(state.session) 268 addOrUpdateSavedAccount(state.session) 269 } catch (e) { 270 if (e instanceof ApiError && e.status === 401) { 271 try { 272 const tokens = await refreshOAuthToken(account.refreshJwt) 273 const sessionInfo = await api.getSession(tokens.access_token) 274 const session: Session = { 275 ...sessionInfo, 276 accessJwt: tokens.access_token, 277 refreshJwt: tokens.refresh_token || account.refreshJwt, 278 } 279 state.session = session 280 saveSession(session) 281 addOrUpdateSavedAccount(session) 282 } catch { 283 removeSavedAccount(did) 284 state.error = 'Session expired. Please log in again.' 285 throw new Error('Session expired') 286 } 287 } else { 288 state.error = 'Failed to switch account' 289 throw e 290 } 291 } finally { 292 state.loading = false 293 } 294} 295 296export function forgetAccount(did: string): void { 297 removeSavedAccount(did) 298} 299 300export function getAuthState() { 301 return state 302} 303 304export async function refreshSession(): Promise<void> { 305 if (!state.session) return 306 try { 307 const sessionInfo = await api.getSession(state.session.accessJwt) 308 state.session = { 309 ...sessionInfo, 310 accessJwt: state.session.accessJwt, 311 refreshJwt: state.session.refreshJwt, 312 } 313 saveSession(state.session) 314 addOrUpdateSavedAccount(state.session) 315 } catch (e) { 316 console.error('Failed to refresh session:', e) 317 } 318} 319 320export function getToken(): string | null { 321 return state.session?.accessJwt ?? null 322} 323 324export function isAuthenticated(): boolean { 325 return state.session !== null 326} 327 328export function _testSetState(newState: { session: Session | null; loading: boolean; error: string | null; savedAccounts?: SavedAccount[] }) { 329 state.session = newState.session 330 state.loading = newState.loading 331 state.error = newState.error 332 state.savedAccounts = newState.savedAccounts ?? [] 333} 334 335export function _testReset() { 336 state.session = null 337 state.loading = true 338 state.error = null 339 state.savedAccounts = [] 340 localStorage.removeItem(STORAGE_KEY) 341 localStorage.removeItem(ACCOUNTS_KEY) 342}