Files
llink/js/desktop/src/stores/auth-store.ts
T
Arjun PatelGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
4facc6b371 feat: avatars for humans (#273)
* implement avatar backend functionality

* add avatar endpoints

* typo

* implement client side avatar upload and handling

* fixes

* Update go/internal/handler/handler.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update js/desktop/src/lib/avatar-image.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* bug in order

* remove unused component

* fix invalid migration

* fix syntax errors

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-06-11 15:30:21 -07:00

139 lines
3.8 KiB
TypeScript

import { create } from 'zustand';
import {
signInWithCustomToken,
signOut as firebaseSignOut,
} from 'firebase/auth';
import { apiClient } from '@/api/client';
import type { Human } from '@/api/types';
import { firebaseAuth } from '@/firebase';
import { logError, ApiError } from '@/lib/errors';
import { useSessionStore } from './session-store';
async function signInToFirebase() {
try {
const { token } = await apiClient.getFirebaseToken();
await signInWithCustomToken(firebaseAuth, token);
} catch (err) {
// Firestore subscriptions will fail until the next successful sign-in; the
// rest of the app keeps working against Orion.
logError(err, { scope: 'auth.firebase' });
}
}
type AuthStatus = 'idle' | 'restoring' | 'unauthenticated' | 'authenticated';
interface AuthState {
status: AuthStatus;
user: Human | null;
isRequestingCode: boolean;
isSigningIn: boolean;
isSigningOut: boolean;
error: string | null;
restoreSession: () => Promise<void>;
refreshUser: () => Promise<void>;
requestCode: (email: string) => Promise<void>;
signIn: (email: string, code: string) => Promise<void>;
signOut: () => Promise<void>;
clearError: () => void;
}
export const useAuthStore = create<AuthState>((set) => ({
status: 'idle',
user: null,
isRequestingCode: false,
isSigningIn: false,
isSigningOut: false,
error: null,
restoreSession: async () => {
const token = useSessionStore.getState().token;
if (!token) {
set({ status: 'unauthenticated' });
return;
}
set({ status: 'restoring' });
try {
const user = await apiClient.me();
await signInToFirebase();
set({ status: 'authenticated', user });
} catch (err) {
// Expected on expired/invalid tokens — fall back to the login screen.
logError(err, { scope: 'auth.restore' });
useSessionStore.getState().clearToken();
set({ status: 'unauthenticated', user: null });
}
},
refreshUser: async () => {
const user = await apiClient.me();
set({ user });
},
requestCode: async (email: string) => {
set({ isRequestingCode: true, error: null });
try {
await apiClient.requestCode({ email });
} catch (e) {
const message = e instanceof ApiError ? e.message : 'Failed to send code';
set({ error: message });
throw e;
} finally {
set({ isRequestingCode: false });
}
},
signIn: async (email: string, code: string) => {
set({ isSigningIn: true, error: null });
try {
const { human, token } = await apiClient.signIn({ email, code });
useSessionStore.getState().setToken(token);
await signInToFirebase();
set({ status: 'authenticated', user: human });
} catch (e) {
const message = e instanceof ApiError ? e.message : 'Failed to sign in';
set({ error: message });
throw e;
} finally {
set({ isSigningIn: false });
}
},
signOut: async () => {
set({ isSigningOut: true });
try {
await apiClient.signOut();
} catch (err) {
// Best-effort — sign out locally regardless.
logError(err, { scope: 'auth.signOut' });
} finally {
await firebaseSignOut(firebaseAuth).catch((err) =>
logError(err, { scope: 'auth.firebaseSignOut' }),
);
useSessionStore.getState().clearToken();
set({
status: 'unauthenticated',
user: null,
isSigningOut: false,
error: null,
});
}
},
clearError: () => set({ error: null }),
}));
// React to token being cleared externally (e.g. 401 from API client)
useSessionStore.subscribe((state, prevState) => {
if (prevState.token && !state.token) {
const authState = useAuthStore.getState();
if (authState.status === 'authenticated') {
useAuthStore.setState({
status: 'unauthenticated',
user: null,
error: null,
});
}
}
});