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