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}