178 lines
4.7 KiB
TypeScript
178 lines
4.7 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useContext,
|
|
useEffect,
|
|
useState,
|
|
useCallback,
|
|
type ReactNode,
|
|
} from "react";
|
|
|
|
interface AuthContextType {
|
|
isAuthenticated: boolean;
|
|
username: string | null;
|
|
isLoading: boolean;
|
|
expirationTime: number | null;
|
|
login: (username: string, password: string) => Promise<boolean>;
|
|
logout: () => void;
|
|
checkAuth: () => Promise<void>;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
|
|
interface AuthProviderProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function AuthProvider({ children }: AuthProviderProps) {
|
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
const [username, setUsername] = useState<string | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [expirationTime, setExpirationTime] = useState<number | null>(null);
|
|
|
|
const checkAuthInternal = async (retryCount = 0) => {
|
|
try {
|
|
// First check if setup is completed
|
|
const setupResponse = await fetch("/api/settings/auth-credentials");
|
|
if (setupResponse.ok) {
|
|
const setupData = (await setupResponse.json()) as {
|
|
setupCompleted: boolean;
|
|
enabled: boolean;
|
|
};
|
|
|
|
// If setup is not completed or auth is disabled, don't verify
|
|
if (!setupData.setupCompleted || !setupData.enabled) {
|
|
setIsAuthenticated(false);
|
|
setUsername(null);
|
|
setExpirationTime(null);
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Only verify authentication if setup is completed and auth is enabled
|
|
const response = await fetch("/api/auth/verify", {
|
|
credentials: "include", // Ensure cookies are sent
|
|
});
|
|
if (response.ok) {
|
|
const data = (await response.json()) as {
|
|
username: string;
|
|
expirationTime?: number | null;
|
|
timeUntilExpiration?: number | null;
|
|
};
|
|
setIsAuthenticated(true);
|
|
setUsername(data.username);
|
|
setExpirationTime(data.expirationTime ?? null);
|
|
} else {
|
|
setIsAuthenticated(false);
|
|
setUsername(null);
|
|
setExpirationTime(null);
|
|
|
|
// Retry logic for failed auth checks (max 2 retries)
|
|
if (retryCount < 2) {
|
|
setTimeout(() => {
|
|
void checkAuthInternal(retryCount + 1);
|
|
}, 500);
|
|
return;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error checking auth:", error);
|
|
setIsAuthenticated(false);
|
|
setUsername(null);
|
|
setExpirationTime(null);
|
|
|
|
// Retry logic for network errors (max 2 retries)
|
|
if (retryCount < 2) {
|
|
setTimeout(() => {
|
|
void checkAuthInternal(retryCount + 1);
|
|
}, 500);
|
|
return;
|
|
}
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const checkAuth = useCallback(() => {
|
|
return checkAuthInternal(0);
|
|
}, []);
|
|
|
|
const login = async (
|
|
username: string,
|
|
password: string,
|
|
): Promise<boolean> => {
|
|
try {
|
|
const response = await fetch("/api/auth/login", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ username, password }),
|
|
credentials: "include", // Ensure cookies are received
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = (await response.json()) as {
|
|
username: string;
|
|
expirationTime?: number;
|
|
};
|
|
setIsAuthenticated(true);
|
|
setUsername(data.username);
|
|
// Set expiration time from login response if available
|
|
if (data.expirationTime) {
|
|
setExpirationTime(data.expirationTime);
|
|
}
|
|
// Don't call checkAuth after login - we already know we're authenticated
|
|
// The cookie is set by the server response
|
|
return true;
|
|
} else {
|
|
const errorData = await response.json();
|
|
console.error("Login failed:", errorData.error);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
console.error("Login error:", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const logout = () => {
|
|
// Clear the auth cookie by setting it to expire
|
|
document.cookie =
|
|
"auth-token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
|
setIsAuthenticated(false);
|
|
setUsername(null);
|
|
setExpirationTime(null);
|
|
};
|
|
|
|
useEffect(() => {
|
|
void checkAuth();
|
|
}, [checkAuth]);
|
|
|
|
return (
|
|
<AuthContext.Provider
|
|
value={{
|
|
isAuthenticated,
|
|
username,
|
|
isLoading,
|
|
expirationTime,
|
|
login,
|
|
logout,
|
|
checkAuth,
|
|
}}
|
|
>
|
|
{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;
|
|
}
|