* 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>
139 lines
3.8 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
}
|
|
});
|