175 lines
4.3 KiB
TypeScript
175 lines
4.3 KiB
TypeScript
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;
|
|
}
|