mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: stop granting admin access when API is unreachable
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { formatHeaders } from "@/lib/api";
|
import { formatHeaders } from "@/lib/api";
|
||||||
|
import { useConnectionStore } from "@/stores/connection-store";
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -36,13 +37,15 @@ export function useAuth() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
async function checkAuth() {
|
async function checkAuth() {
|
||||||
try {
|
try {
|
||||||
// Check if auth is enabled
|
|
||||||
const configRes = await fetch("/api/v1/config/auth");
|
const configRes = await fetch("/api/v1/config/auth");
|
||||||
const config = await configRes.json();
|
const config = await configRes.json();
|
||||||
|
|
||||||
if (!config.authEnabled) {
|
if (!config.authEnabled) {
|
||||||
|
if (!cancelled)
|
||||||
setState({
|
setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
authEnabled: false,
|
authEnabled: false,
|
||||||
@@ -54,9 +57,9 @@ export function useAuth() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth is enabled — check if we have a valid session
|
|
||||||
const token = localStorage.getItem("ashim-token");
|
const token = localStorage.getItem("ashim-token");
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
if (!cancelled)
|
||||||
setState({
|
setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
authEnabled: true,
|
authEnabled: true,
|
||||||
@@ -75,6 +78,7 @@ export function useAuth() {
|
|||||||
if (sessionRes.ok) {
|
if (sessionRes.ok) {
|
||||||
const session = await sessionRes.json();
|
const session = await sessionRes.json();
|
||||||
const mustChange = session.user?.mustChangePassword === true;
|
const mustChange = session.user?.mustChangePassword === true;
|
||||||
|
if (!cancelled)
|
||||||
setState({
|
setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
authEnabled: true,
|
authEnabled: true,
|
||||||
@@ -85,6 +89,7 @@ export function useAuth() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem("ashim-token");
|
localStorage.removeItem("ashim-token");
|
||||||
|
if (!cancelled)
|
||||||
setState({
|
setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
authEnabled: true,
|
authEnabled: true,
|
||||||
@@ -95,19 +100,23 @@ export function useAuth() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Can't reach API — assume no auth needed (dev mode)
|
// API unreachable — stay in loading state.
|
||||||
setState({
|
// ConnectionBanner explains the outage. AuthGuard shows spinner.
|
||||||
loading: false,
|
|
||||||
authEnabled: false,
|
|
||||||
isAuthenticated: true,
|
|
||||||
mustChangePassword: false,
|
|
||||||
role: "admin",
|
|
||||||
permissions: ALL_PERMISSIONS,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkAuth();
|
checkAuth();
|
||||||
|
|
||||||
|
const unsubscribe = useConnectionStore.subscribe((curr, prev) => {
|
||||||
|
if (prev.status !== "reconnected" && curr.status === "reconnected") {
|
||||||
|
checkAuth();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const hasPermission = (permission: string) => state.permissions.includes(permission);
|
const hasPermission = (permission: string) => state.permissions.includes(permission);
|
||||||
|
|||||||
@@ -799,3 +799,30 @@ describe("API lib", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// useAuth security
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
describe("useAuth security — network error", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetchMock.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT grant admin when API is unreachable", async () => {
|
||||||
|
fetchMock.mockRejectedValue(new TypeError("Failed to fetch"));
|
||||||
|
|
||||||
|
const { renderHook, act } = await import("@testing-library/react");
|
||||||
|
const { useAuth } = await import("@/hooks/use-auth");
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAuth());
|
||||||
|
|
||||||
|
// Flush the async checkAuth() call
|
||||||
|
await act(async () => {});
|
||||||
|
|
||||||
|
// loading stays true — setState was never called
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
expect(result.current.isAuthenticated).toBe(false);
|
||||||
|
expect(result.current.role).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user