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}