Initial commit - BraceIQMed platform with frontend, API, and brace generator

This commit is contained in:
2026-01-29 14:34:05 -08:00
commit 745f9f827f
187 changed files with 534688 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from "react";
export type User = {
id: number;
username: string;
fullName: string | null;
role: "admin" | "user" | "viewer";
};
type AuthContextType = {
user: User | null;
token: string | null;
isAuthenticated: boolean;
isLoading: boolean;
isAdmin: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
error: string | null;
clearError: () => void;
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
const AUTH_STORAGE_KEY = "braceflow_auth";
const API_BASE = import.meta.env.VITE_API_BASE || "http://localhost:3001/api";
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Check for existing session on mount
useEffect(() => {
const stored = localStorage.getItem(AUTH_STORAGE_KEY);
if (stored) {
try {
const parsed = JSON.parse(stored);
if (parsed.user && parsed.token && parsed.expiresAt > Date.now()) {
setUser(parsed.user);
setToken(parsed.token);
} else {
localStorage.removeItem(AUTH_STORAGE_KEY);
}
} catch {
localStorage.removeItem(AUTH_STORAGE_KEY);
}
}
setIsLoading(false);
}, []);
const login = useCallback(async (username: string, password: string) => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(`${API_BASE}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Invalid username or password");
}
const data = await response.json();
const userData: User = {
id: data.user.id,
username: data.user.username,
fullName: data.user.full_name,
role: data.user.role,
};
// Store auth data
const authData = {
user: userData,
token: data.token,
expiresAt: new Date(data.expiresAt).getTime(),
};
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(authData));
setUser(userData);
setToken(data.token);
} catch (err: any) {
setError(err.message || "Login failed. Please try again.");
throw err;
} finally {
setIsLoading(false);
}
}, []);
const logout = useCallback(async () => {
// Call logout endpoint if we have a token
if (token) {
try {
await fetch(`${API_BASE}/auth/logout`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
},
});
} catch {
// Ignore logout API errors
}
}
localStorage.removeItem(AUTH_STORAGE_KEY);
setUser(null);
setToken(null);
}, [token]);
const clearError = useCallback(() => {
setError(null);
}, []);
const value: AuthContextType = {
user,
token,
isAuthenticated: !!user,
isLoading,
isAdmin: user?.role === "admin",
login,
logout,
error,
clearError,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
/**
* Helper function to get auth headers for API calls
*/
export function getAuthHeaders(): Record<string, string> {
const stored = localStorage.getItem(AUTH_STORAGE_KEY);
if (stored) {
try {
const parsed = JSON.parse(stored);
if (parsed.token) {
return { "Authorization": `Bearer ${parsed.token}` };
}
} catch {
// Ignore
}
}
return {};
}
/**
* Helper function to get the auth token
*/
export function getAuthToken(): string | null {
const stored = localStorage.getItem(AUTH_STORAGE_KEY);
if (stored) {
try {
const parsed = JSON.parse(stored);
return parsed.token || null;
} catch {
// Ignore
}
}
return null;
}