mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Merge branch 'feat/graceful-degradation' into feat/on-demand-ai-features
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { formatHeaders } from "@/lib/api";
|
||||
import { useConnectionStore } from "@/stores/connection-store";
|
||||
|
||||
interface AuthState {
|
||||
loading: boolean;
|
||||
@@ -36,35 +37,37 @@ export function useAuth() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
// Check if auth is enabled
|
||||
const configRes = await fetch("/api/v1/config/auth");
|
||||
const config = await configRes.json();
|
||||
|
||||
if (!config.authEnabled) {
|
||||
setState({
|
||||
loading: false,
|
||||
authEnabled: false,
|
||||
isAuthenticated: true,
|
||||
mustChangePassword: false,
|
||||
role: "admin",
|
||||
permissions: ALL_PERMISSIONS,
|
||||
});
|
||||
if (!cancelled)
|
||||
setState({
|
||||
loading: false,
|
||||
authEnabled: false,
|
||||
isAuthenticated: true,
|
||||
mustChangePassword: false,
|
||||
role: "admin",
|
||||
permissions: ALL_PERMISSIONS,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Auth is enabled — check if we have a valid session
|
||||
const token = localStorage.getItem("ashim-token");
|
||||
if (!token) {
|
||||
setState({
|
||||
loading: false,
|
||||
authEnabled: true,
|
||||
isAuthenticated: false,
|
||||
mustChangePassword: false,
|
||||
role: null,
|
||||
permissions: [],
|
||||
});
|
||||
if (!cancelled)
|
||||
setState({
|
||||
loading: false,
|
||||
authEnabled: true,
|
||||
isAuthenticated: false,
|
||||
mustChangePassword: false,
|
||||
role: null,
|
||||
permissions: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,39 +78,45 @@ export function useAuth() {
|
||||
if (sessionRes.ok) {
|
||||
const session = await sessionRes.json();
|
||||
const mustChange = session.user?.mustChangePassword === true;
|
||||
setState({
|
||||
loading: false,
|
||||
authEnabled: true,
|
||||
isAuthenticated: true,
|
||||
mustChangePassword: mustChange,
|
||||
role: session.user?.role ?? null,
|
||||
permissions: session.user?.permissions ?? [],
|
||||
});
|
||||
if (!cancelled)
|
||||
setState({
|
||||
loading: false,
|
||||
authEnabled: true,
|
||||
isAuthenticated: true,
|
||||
mustChangePassword: mustChange,
|
||||
role: session.user?.role ?? null,
|
||||
permissions: session.user?.permissions ?? [],
|
||||
});
|
||||
} else {
|
||||
localStorage.removeItem("ashim-token");
|
||||
setState({
|
||||
loading: false,
|
||||
authEnabled: true,
|
||||
isAuthenticated: false,
|
||||
mustChangePassword: false,
|
||||
role: null,
|
||||
permissions: [],
|
||||
});
|
||||
if (!cancelled)
|
||||
setState({
|
||||
loading: false,
|
||||
authEnabled: true,
|
||||
isAuthenticated: false,
|
||||
mustChangePassword: false,
|
||||
role: null,
|
||||
permissions: [],
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Can't reach API — assume no auth needed (dev mode)
|
||||
setState({
|
||||
loading: false,
|
||||
authEnabled: false,
|
||||
isAuthenticated: true,
|
||||
mustChangePassword: false,
|
||||
role: "admin",
|
||||
permissions: ALL_PERMISSIONS,
|
||||
});
|
||||
// API unreachable — stay in loading state.
|
||||
// ConnectionBanner explains the outage. AuthGuard shows spinner.
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useEffect } from "react";
|
||||
import { useConnectionStore } from "@/stores/connection-store";
|
||||
|
||||
export function useConnectionMonitor() {
|
||||
useEffect(() => {
|
||||
const store = useConnectionStore;
|
||||
|
||||
const handleOffline = () => store.getState().setOffline();
|
||||
const handleOnline = () => {
|
||||
const prev = store.getState().status;
|
||||
store.getState().setOnline();
|
||||
if (prev === "offline") {
|
||||
store.getState().startPolling();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("offline", handleOffline);
|
||||
window.addEventListener("online", handleOnline);
|
||||
|
||||
store.getState().checkHealth();
|
||||
|
||||
const unsubscribe = store.subscribe((state, prev) => {
|
||||
if (state.status === prev.status) return;
|
||||
|
||||
if (state.status === "disconnected") {
|
||||
store.getState().startPolling();
|
||||
}
|
||||
|
||||
if (state.status === "reconnected") {
|
||||
store.getState().stopPolling();
|
||||
store
|
||||
.getState()
|
||||
.refreshStaleData()
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
if (store.getState().status === "reconnected") {
|
||||
store.setState({ status: "connected" });
|
||||
}
|
||||
}, 2500);
|
||||
});
|
||||
}
|
||||
|
||||
if (state.status === "offline") {
|
||||
store.getState().stopPolling();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("offline", handleOffline);
|
||||
window.removeEventListener("online", handleOnline);
|
||||
store.getState().stopPolling();
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
@@ -261,7 +261,7 @@ export function useToolProcessor(toolId: string) {
|
||||
eventSourceRef.current.close();
|
||||
eventSourceRef.current = null;
|
||||
}
|
||||
setError("Network error - check your connection");
|
||||
setError("Processing was interrupted \u2014 retry when reconnected");
|
||||
setProcessing(false);
|
||||
setProgress(IDLE_PROGRESS);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user