Files
llink/js/src/stores/auth-store.ts
T
Arjun Patel ef899ee5cd security: access control for particles (#169)
* setup firebase custom token

* docs

* docs

* feat: allow admin removing members from a network

* fix: properly handle fallback avatar and names

This is especially helpful in the case of members who were removed from
a network
2026-04-16 15:14:34 -07:00

126 lines
3.4 KiB
TypeScript

import { create } from "zustand";
import { signInWithCustomToken, signOut as firebaseSignOut } from "firebase/auth";
import { apiClient, ApiError } from "@/api/client";
import type { Human } from "@/api/types";
import { firebaseAuth } from "@/firebase";
import { useSessionStore } from "./session-store";
async function signInToFirebase() {
try {
const { token } = await apiClient.getFirebaseToken();
await signInWithCustomToken(firebaseAuth, token);
} catch (e) {
console.error("Failed to sign in to Firebase", e);
}
}
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>;
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 {
useSessionStore.getState().clearToken();
set({ status: "unauthenticated", user: null });
}
},
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 {
// Best-effort — sign out locally regardless
} finally {
await firebaseSignOut(firebaseAuth).catch((e) =>
console.error("Firebase sign-out failed", e),
);
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,
});
}
}
});