nxtgauge-frontend-solid/src/lib/auth.ts

143 lines
4.9 KiB
TypeScript

import { createSignal, createContext, useContext, JSX, onMount } from 'solid-js';
// ── Types ──────────────────────────────────────────────────────────────────────
export interface RuntimeConfig {
role: string;
onboarding_required: boolean;
onboarding_status?: string;
enabled_modules: string[];
feature_flags: Record<string, boolean>;
permissions: Record<string, boolean>;
user: {
id: string;
full_name: string;
email: string;
roles: string[];
active_role: string;
};
}
export interface AuthState {
access_token: string | null; // Memory only — never persisted
runtime_config: RuntimeConfig | null;
loading: boolean;
error: string | null;
}
// ── Store (in-memory only) ─────────────────────────────────────────────────────
const [authState, setAuthState] = createSignal<AuthState>({
access_token: null,
runtime_config: null,
loading: false,
error: null,
});
const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:8000';
// ── Auth API Calls ─────────────────────────────────────────────────────────────
export async function login(email: string, password: string): Promise<void> {
setAuthState(s => ({ ...s, loading: true, error: null }));
const res = await fetch(`${API_BASE}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // include httpOnly refresh cookie
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
const body = await res.json();
setAuthState(s => ({ ...s, loading: false, error: body.error ?? 'Login failed' }));
throw new Error(body.error ?? 'Login failed');
}
const data = await res.json();
setAuthState(s => ({ ...s, access_token: data.access_token, loading: false }));
// Immediately fetch runtimeConfig
await fetchRuntimeConfig(data.access_token);
}
export async function logout(): Promise<void> {
const token = authState().access_token;
if (token) {
await fetch(`${API_BASE}/api/auth/logout`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
}).catch(() => {});
}
setAuthState({ access_token: null, runtime_config: null, loading: false, error: null });
}
export async function refreshToken(): Promise<boolean> {
const res = await fetch(`${API_BASE}/api/auth/refresh`, {
method: 'POST',
credentials: 'include', // reads refresh token from httpOnly cookie
});
if (!res.ok) return false;
const data = await res.json();
setAuthState(s => ({ ...s, access_token: data.access_token }));
return true;
}
export async function fetchRuntimeConfig(token?: string): Promise<void> {
const accessToken = token ?? authState().access_token;
if (!accessToken) return;
const res = await fetch(`${API_BASE}/api/runtime-config`, {
headers: { Authorization: `Bearer ${accessToken}` },
credentials: 'include',
});
if (res.ok) {
const config: RuntimeConfig = await res.json();
setAuthState(s => ({ ...s, runtime_config: config }));
}
}
export async function switchRole(roleKey: string): Promise<void> {
const token = authState().access_token;
if (!token) return;
const res = await fetch(`${API_BASE}/api/auth/switch-role`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ role_key: roleKey }),
credentials: 'include',
});
if (res.ok) {
const data = await res.json();
setAuthState(s => ({ ...s, access_token: data.access_token }));
await fetchRuntimeConfig(data.access_token);
}
}
// ── Helpers ────────────────────────────────────────────────────────────────────
export function isAuthenticated(): boolean {
return authState().access_token !== null;
}
export function hasModule(moduleKey: string): boolean {
return authState().runtime_config?.enabled_modules.includes(moduleKey) ?? false;
}
export function hasPermission(key: string): boolean {
return authState().runtime_config?.permissions[key] ?? false;
}
export function getAuthHeader(): Record<string, string> {
const token = authState().access_token;
return token ? { Authorization: `Bearer ${token}` } : {};
}
// ── Exported helpers ───────────────────────────────────────────────────────────
export { authState };