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}