143 lines
4.9 KiB
TypeScript
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/me/roles/switch`, {
|
|
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 };
|