diff --git a/desktop/src/api/hahaOAuth.ts b/desktop/src/api/hahaOAuth.ts new file mode 100644 index 00000000..65b3de40 --- /dev/null +++ b/desktop/src/api/hahaOAuth.ts @@ -0,0 +1,38 @@ +// desktop/src/api/hahaOAuth.ts + +import { api, getBaseUrl } from './client' + +export type HahaOAuthStatus = + | { loggedIn: false } + | { + loggedIn: true + expiresAt: number | null + scopes: string[] + subscriptionType: 'pro' | 'max' | 'team' | 'enterprise' | null + } + +function currentServerPort(): number { + const port = new URL(getBaseUrl()).port + const parsed = Number.parseInt(port, 10) + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new Error(`Cannot determine server port from baseUrl: ${getBaseUrl()}`) + } + return parsed +} + +export const hahaOAuthApi = { + start() { + return api.post<{ authorizeUrl: string; state: string }>( + '/api/haha-oauth/start', + { serverPort: currentServerPort() }, + ) + }, + + status() { + return api.get('/api/haha-oauth') + }, + + logout() { + return api.delete<{ ok: true }>('/api/haha-oauth') + }, +} diff --git a/desktop/src/stores/hahaOAuthStore.ts b/desktop/src/stores/hahaOAuthStore.ts new file mode 100644 index 00000000..d6f3e49a --- /dev/null +++ b/desktop/src/stores/hahaOAuthStore.ts @@ -0,0 +1,89 @@ +// desktop/src/stores/hahaOAuthStore.ts + +import { create } from 'zustand' +import { hahaOAuthApi, type HahaOAuthStatus } from '../api/hahaOAuth' + +const POLL_INTERVAL_MS = 2_000 + +type HahaOAuthState = { + status: HahaOAuthStatus | null + isPolling: boolean + isLoading: boolean + error: string | null + + fetchStatus: () => Promise + login: () => Promise<{ authorizeUrl: string }> + logout: () => Promise + startPolling: () => void + stopPolling: () => void +} + +export const useHahaOAuthStore = create((set, get) => { + let pollTimer: ReturnType | null = null + + return { + status: null, + isPolling: false, + isLoading: false, + error: null, + + fetchStatus: async () => { + try { + const status = await hahaOAuthApi.status() + set({ status, error: null }) + } catch (err) { + set({ error: err instanceof Error ? err.message : String(err) }) + } + }, + + login: async () => { + set({ isLoading: true, error: null }) + try { + const res = await hahaOAuthApi.start() + set({ isLoading: false }) + get().startPolling() + return { authorizeUrl: res.authorizeUrl } + } catch (err) { + set({ + isLoading: false, + error: err instanceof Error ? err.message : String(err), + }) + throw err + } + }, + + logout: async () => { + set({ isLoading: true }) + try { + await hahaOAuthApi.logout() + set({ status: { loggedIn: false }, isLoading: false }) + } catch (err) { + set({ + isLoading: false, + error: err instanceof Error ? err.message : String(err), + }) + throw err + } + }, + + startPolling: () => { + if (pollTimer) return + set({ isPolling: true }) + pollTimer = setInterval(async () => { + await get().fetchStatus() + const cur = get().status + if (cur && cur.loggedIn) { + get().stopPolling() + } + }, POLL_INTERVAL_MS) + }, + + stopPolling: () => { + if (pollTimer) { + clearInterval(pollTimer) + pollTimer = null + } + set({ isPolling: false }) + }, + } +})