167 lines
5.1 KiB
TypeScript
167 lines
5.1 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useContext,
|
|
useEffect,
|
|
useState,
|
|
ReactNode,
|
|
} from "react";
|
|
import { useRouter, usePathname } from "next/navigation";
|
|
import { api } from "./api";
|
|
|
|
interface User {
|
|
id: string;
|
|
username: string;
|
|
role: string;
|
|
onboardingComplete?: boolean;
|
|
}
|
|
|
|
interface AuthContextType {
|
|
isAuthenticated: boolean;
|
|
isLoading: boolean;
|
|
user: User | null;
|
|
login: (
|
|
username: string,
|
|
password: string,
|
|
token?: string
|
|
) => Promise<void>;
|
|
logout: () => void;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
|
|
const publicPaths = ["/login", "/register", "/onboarding", "/sync"];
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
|
|
useEffect(() => {
|
|
// Check if user has valid session on mount ONLY
|
|
const checkAuth = async () => {
|
|
// Check for token in URL (from redirect after login)
|
|
if (typeof window !== "undefined") {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const tokenFromUrl = urlParams.get("token");
|
|
if (tokenFromUrl) {
|
|
// Store the token from URL
|
|
api.setToken(tokenFromUrl);
|
|
// Clean up URL (remove token param)
|
|
const cleanUrl = window.location.pathname;
|
|
window.history.replaceState({}, "", cleanUrl);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const userData = await api.getCurrentUser();
|
|
setUser(userData);
|
|
setIsAuthenticated(true);
|
|
|
|
// Check onboarding status - redirect if needed
|
|
if (
|
|
userData.onboardingComplete === false &&
|
|
pathname !== "/onboarding"
|
|
) {
|
|
router.push("/onboarding");
|
|
} else if (
|
|
userData.onboardingComplete &&
|
|
pathname === "/onboarding"
|
|
) {
|
|
router.push("/");
|
|
}
|
|
} catch (error) {
|
|
setIsAuthenticated(false);
|
|
setUser(null);
|
|
|
|
// If we're already on onboarding page, allow access
|
|
if (pathname === "/onboarding") {
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
// If not on a public path, check if we need onboarding
|
|
if (!publicPaths.includes(pathname)) {
|
|
// Check if any users exist in the system
|
|
try {
|
|
const status = await api.get<{ hasAccount: boolean }>(
|
|
"/onboarding/status"
|
|
);
|
|
|
|
if (!status.hasAccount) {
|
|
// No users exist - redirect to onboarding
|
|
router.push("/onboarding");
|
|
return;
|
|
}
|
|
} catch {
|
|
// If status check fails, assume users exist
|
|
}
|
|
// Users exist but not logged in - redirect to login
|
|
router.push("/login");
|
|
}
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
checkAuth();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []); // Only run once on mount
|
|
|
|
const login = async (
|
|
username: string,
|
|
password: string,
|
|
token?: string
|
|
) => {
|
|
try {
|
|
const userData = await api.login(username, password, token);
|
|
|
|
// Check if 2FA is required
|
|
if (userData.requires2FA) {
|
|
// Don't set user or redirect, just throw an error to trigger 2FA UI
|
|
throw new Error("2FA token required");
|
|
}
|
|
|
|
setUser(userData);
|
|
setIsAuthenticated(true);
|
|
|
|
// Redirect based on onboarding status
|
|
if (userData.onboardingComplete === false) {
|
|
router.push("/onboarding");
|
|
} else {
|
|
router.push("/");
|
|
}
|
|
} catch (error: any) {
|
|
console.error("[AUTH] Login failed:", error.message);
|
|
// Re-throw the error so the login page can handle it
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const logout = async () => {
|
|
await api.logout();
|
|
setIsAuthenticated(false);
|
|
setUser(null);
|
|
router.push("/login");
|
|
};
|
|
|
|
return (
|
|
<AuthContext.Provider
|
|
value={{ isAuthenticated, isLoading, user, login, logout }}
|
|
>
|
|
{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;
|
|
}
|