2026-03-25 09:27:12 +08:00
|
|
|
import { useEffect, useState } from "react";
|
2026-04-05 18:41:06 +08:00
|
|
|
import { formatHeaders } from "@/lib/api";
|
2026-04-20 22:07:39 +08:00
|
|
|
import { useConnectionStore } from "@/stores/connection-store";
|
2026-03-22 11:19:41 +08:00
|
|
|
|
|
|
|
|
interface AuthState {
|
|
|
|
|
loading: boolean;
|
|
|
|
|
authEnabled: boolean;
|
|
|
|
|
isAuthenticated: boolean;
|
2026-03-28 14:02:07 +08:00
|
|
|
mustChangePassword: boolean;
|
2026-04-06 15:48:51 +08:00
|
|
|
role: string | null;
|
|
|
|
|
permissions: string[];
|
2026-04-22 19:10:52 +08:00
|
|
|
analyticsEnabled: boolean | null;
|
|
|
|
|
analyticsConsentShownAt: number | null;
|
|
|
|
|
analyticsConsentRemindAt: number | null;
|
2026-03-22 11:19:41 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-21 23:38:42 +08:00
|
|
|
const USER_PERMISSIONS = [
|
2026-04-10 21:25:03 +08:00
|
|
|
"tools:use",
|
|
|
|
|
"files:own",
|
|
|
|
|
"apikeys:own",
|
|
|
|
|
"pipelines:own",
|
|
|
|
|
"settings:read",
|
|
|
|
|
];
|
|
|
|
|
|
2026-04-06 15:48:51 +08:00
|
|
|
export function useAuth() {
|
2026-03-22 11:19:41 +08:00
|
|
|
const [state, setState] = useState<AuthState>({
|
|
|
|
|
loading: true,
|
|
|
|
|
authEnabled: false,
|
|
|
|
|
isAuthenticated: false,
|
2026-03-28 14:02:07 +08:00
|
|
|
mustChangePassword: false,
|
2026-04-06 15:48:51 +08:00
|
|
|
role: null,
|
|
|
|
|
permissions: [],
|
2026-04-22 19:10:52 +08:00
|
|
|
analyticsEnabled: null,
|
|
|
|
|
analyticsConsentShownAt: null,
|
|
|
|
|
analyticsConsentRemindAt: null,
|
2026-03-22 11:19:41 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-04-20 22:07:39 +08:00
|
|
|
let cancelled = false;
|
|
|
|
|
|
2026-03-25 22:12:55 +08:00
|
|
|
async function checkAuth() {
|
|
|
|
|
try {
|
|
|
|
|
const configRes = await fetch("/api/v1/config/auth");
|
|
|
|
|
const config = await configRes.json();
|
2026-03-22 11:19:41 +08:00
|
|
|
|
2026-03-25 22:12:55 +08:00
|
|
|
if (!config.authEnabled) {
|
2026-04-20 22:07:39 +08:00
|
|
|
if (!cancelled)
|
|
|
|
|
setState({
|
|
|
|
|
loading: false,
|
|
|
|
|
authEnabled: false,
|
|
|
|
|
isAuthenticated: true,
|
|
|
|
|
mustChangePassword: false,
|
2026-04-21 23:38:42 +08:00
|
|
|
role: "user",
|
|
|
|
|
permissions: USER_PERMISSIONS,
|
2026-04-22 19:10:52 +08:00
|
|
|
analyticsEnabled: null,
|
|
|
|
|
analyticsConsentShownAt: null,
|
|
|
|
|
analyticsConsentRemindAt: null,
|
2026-04-20 22:07:39 +08:00
|
|
|
});
|
2026-03-25 22:12:55 +08:00
|
|
|
return;
|
|
|
|
|
}
|
2026-03-22 11:19:41 +08:00
|
|
|
|
2026-04-24 18:02:21 +08:00
|
|
|
const token = localStorage.getItem("snapotter-token");
|
2026-03-25 22:12:55 +08:00
|
|
|
if (!token) {
|
2026-04-20 22:07:39 +08:00
|
|
|
if (!cancelled)
|
|
|
|
|
setState({
|
|
|
|
|
loading: false,
|
|
|
|
|
authEnabled: true,
|
|
|
|
|
isAuthenticated: false,
|
|
|
|
|
mustChangePassword: false,
|
|
|
|
|
role: null,
|
|
|
|
|
permissions: [],
|
2026-04-22 19:10:52 +08:00
|
|
|
analyticsEnabled: null,
|
|
|
|
|
analyticsConsentShownAt: null,
|
|
|
|
|
analyticsConsentRemindAt: null,
|
2026-04-20 22:07:39 +08:00
|
|
|
});
|
2026-03-25 22:12:55 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sessionRes = await fetch("/api/auth/session", {
|
2026-04-05 18:41:06 +08:00
|
|
|
headers: formatHeaders(),
|
2026-03-25 22:12:55 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (sessionRes.ok) {
|
2026-03-28 14:02:07 +08:00
|
|
|
const session = await sessionRes.json();
|
|
|
|
|
const mustChange = session.user?.mustChangePassword === true;
|
2026-04-20 22:07:39 +08:00
|
|
|
if (!cancelled)
|
|
|
|
|
setState({
|
|
|
|
|
loading: false,
|
|
|
|
|
authEnabled: true,
|
|
|
|
|
isAuthenticated: true,
|
|
|
|
|
mustChangePassword: mustChange,
|
|
|
|
|
role: session.user?.role ?? null,
|
|
|
|
|
permissions: session.user?.permissions ?? [],
|
2026-04-22 19:10:52 +08:00
|
|
|
analyticsEnabled: session.user?.analyticsEnabled ?? null,
|
|
|
|
|
analyticsConsentShownAt: session.user?.analyticsConsentShownAt ?? null,
|
|
|
|
|
analyticsConsentRemindAt: session.user?.analyticsConsentRemindAt ?? null,
|
2026-04-20 22:07:39 +08:00
|
|
|
});
|
2026-03-25 22:12:55 +08:00
|
|
|
} else {
|
2026-04-24 18:02:21 +08:00
|
|
|
localStorage.removeItem("snapotter-token");
|
2026-04-20 22:07:39 +08:00
|
|
|
if (!cancelled)
|
|
|
|
|
setState({
|
|
|
|
|
loading: false,
|
|
|
|
|
authEnabled: true,
|
|
|
|
|
isAuthenticated: false,
|
|
|
|
|
mustChangePassword: false,
|
|
|
|
|
role: null,
|
|
|
|
|
permissions: [],
|
2026-04-22 19:10:52 +08:00
|
|
|
analyticsEnabled: null,
|
|
|
|
|
analyticsConsentShownAt: null,
|
|
|
|
|
analyticsConsentRemindAt: null,
|
2026-04-20 22:07:39 +08:00
|
|
|
});
|
2026-03-25 22:12:55 +08:00
|
|
|
}
|
|
|
|
|
} catch {
|
2026-04-20 22:07:39 +08:00
|
|
|
// API unreachable — stay in loading state.
|
|
|
|
|
// ConnectionBanner explains the outage. AuthGuard shows spinner.
|
2026-03-22 11:19:41 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-25 22:12:55 +08:00
|
|
|
|
|
|
|
|
checkAuth();
|
2026-04-20 22:07:39 +08:00
|
|
|
|
|
|
|
|
const unsubscribe = useConnectionStore.subscribe((curr, prev) => {
|
|
|
|
|
if (prev.status !== "reconnected" && curr.status === "reconnected") {
|
|
|
|
|
checkAuth();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
unsubscribe();
|
|
|
|
|
};
|
2026-03-25 22:12:55 +08:00
|
|
|
}, []);
|
2026-03-22 11:19:41 +08:00
|
|
|
|
2026-04-06 15:48:51 +08:00
|
|
|
const hasPermission = (permission: string) => state.permissions.includes(permission);
|
|
|
|
|
|
|
|
|
|
return { ...state, hasPermission };
|
2026-03-22 11:19:41 +08:00
|
|
|
}
|