Initial commit - BraceIQMed platform with frontend, API, and brace generator
This commit is contained in:
174
frontend/src/context/AuthContext.tsx
Normal file
174
frontend/src/context/AuthContext.tsx
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user