From 89d0c3f3f1258ebe68be90e918f2f9073c8ccb36 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sat, 27 Dec 2025 00:59:47 +0100 Subject: [PATCH] feat(auth): enhance error handling in requireAuth and add authError state in AuthProvider --- backend/src/plugins/auth.ts | 19 +++++++++++---- backend/src/plugins/env.ts | 36 +++++++++++++++++++++++----- frontend/src/App.tsx | 41 ++++++++++++++++++++++++++++++-- frontend/src/components/Auth.tsx | 9 ++++++- 4 files changed, 91 insertions(+), 14 deletions(-) diff --git a/backend/src/plugins/auth.ts b/backend/src/plugins/auth.ts index aecf411..cc81972 100644 --- a/backend/src/plugins/auth.ts +++ b/backend/src/plugins/auth.ts @@ -78,7 +78,8 @@ export async function requireAuth(request: FastifyRequest, reply: FastifyReply) const token = request.cookies.access_token; if (!token) { - return reply.status(401).send({ error: "Authentication required", code: "AUTH_REQUIRED" }); + reply.status(401).send({ error: "Authentication required", code: "AUTH_REQUIRED" }); + throw new Error("AUTH_REQUIRED"); } try { @@ -86,19 +87,27 @@ export async function requireAuth(request: FastifyRequest, reply: FastifyReply) const [user] = await db.select().from(users).where(sql`${users.id} = ${decoded.sub}`); if (!user) { - return reply.status(401).send({ error: "User not found", code: "USER_NOT_FOUND" }); + reply.status(401).send({ error: "User not found", code: "USER_NOT_FOUND" }); + throw new Error("USER_NOT_FOUND"); } if (!user.isActive) { - return reply.status(401).send({ error: "Account disabled", code: "ACCOUNT_DISABLED" }); + reply.status(401).send({ error: "Account disabled", code: "ACCOUNT_DISABLED" }); + throw new Error("ACCOUNT_DISABLED"); } request.user = { id: user.id, username: user.username, }; - } catch { - return reply.status(401).send({ error: "Invalid or expired token", code: "INVALID_TOKEN" }); + } catch (err: any) { + // Re-throw our own errors + if (err?.message === "AUTH_REQUIRED" || err?.message === "USER_NOT_FOUND" || err?.message === "ACCOUNT_DISABLED") { + throw err; + } + // JWT verification failed + reply.status(401).send({ error: "Invalid or expired token", code: "INVALID_TOKEN" }); + throw new Error("INVALID_TOKEN"); } } diff --git a/backend/src/plugins/env.ts b/backend/src/plugins/env.ts index 910e21d..47afbcd 100644 --- a/backend/src/plugins/env.ts +++ b/backend/src/plugins/env.ts @@ -28,15 +28,39 @@ const EnvSchema = z.object({ export type Env = z.infer; // Parse and validate -const parsed = EnvSchema.parse(process.env); +let parsed: z.infer; +try { + parsed = EnvSchema.parse(process.env); +} catch (err) { + console.error("=".repeat(60)); + console.error("ENVIRONMENT CONFIGURATION ERROR"); + console.error("=".repeat(60)); + console.error(err); + console.error("\nPlease check your .env file or environment variables."); + console.error("=".repeat(60)); + process.exit(1); +} // Validate that secrets are provided when auth is enabled if (parsed.AUTH_ENABLED) { - if (!parsed.JWT_SECRET || !parsed.REFRESH_SECRET || !parsed.COOKIE_SECRET) { - throw new Error( - "AUTH_ENABLED=true requires JWT_SECRET, REFRESH_SECRET, and COOKIE_SECRET to be set. " + - "Generate them with: openssl rand -hex 32" - ); + const missing: string[] = []; + if (!parsed.JWT_SECRET) missing.push("JWT_SECRET"); + if (!parsed.REFRESH_SECRET) missing.push("REFRESH_SECRET"); + if (!parsed.COOKIE_SECRET) missing.push("COOKIE_SECRET"); + + if (missing.length > 0) { + console.error("=".repeat(60)); + console.error("AUTHENTICATION CONFIGURATION ERROR"); + console.error("=".repeat(60)); + console.error(`AUTH_ENABLED=true but missing required secrets: ${missing.join(", ")}`); + console.error(""); + console.error("To fix this, either:"); + console.error(" 1. Set these environment variables with secure random values:"); + console.error(" Generate with: openssl rand -hex 32"); + console.error(""); + console.error(" 2. Or disable authentication by removing AUTH_ENABLED=true"); + console.error("=".repeat(60)); + process.exit(1); } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0a6e3be..0890feb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -96,7 +96,7 @@ export default function App() { } function AppRouter() { - const { user, authState, loading } = useAuth(); + const { user, authState, loading, authError } = useAuth(); const location = useLocation(); const navigate = useNavigate(); @@ -112,8 +112,45 @@ function AppRouter() { ); } + // Show error if we couldn't connect to the server + if (authError) { + return ( +
+
+

💊 MedAssist

+
+ Connection Error
+ {authError} +
+

+ Please check if the server is running and try again. +

+ +
+
+ ); + } + + // If auth state is null (shouldn't happen after loading, but be safe) + if (!authState) { + return ( +
+
+

💊 MedAssist

+

Initializing...

+
+
+ ); + } + // If auth is enabled - if (authState?.authEnabled) { + if (authState.authEnabled) { // Need to register first user if (authState.needsSetup) { return ; diff --git a/frontend/src/components/Auth.tsx b/frontend/src/components/Auth.tsx index 14d224b..98ac9f5 100644 --- a/frontend/src/components/Auth.tsx +++ b/frontend/src/components/Auth.tsx @@ -21,6 +21,7 @@ interface AuthContextType { user: User | null; authState: AuthState | null; loading: boolean; + authError: string | null; login: (username: string, password: string) => Promise; register: (username: string, password: string) => Promise; logout: () => Promise; @@ -48,6 +49,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [authState, setAuthState] = useState(null); const [loading, setLoading] = useState(true); + const [authError, setAuthError] = useState(null); // Fetch auth state on mount useEffect(() => { @@ -56,7 +58,11 @@ export function AuthProvider({ children }: { children: ReactNode }) { async function fetchAuthState() { try { + setAuthError(null); const res = await fetch("/api/auth/state"); + if (!res.ok) { + throw new Error(`Server error: ${res.status}`); + } const state = await res.json(); setAuthState(state); @@ -66,6 +72,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { } } catch (err) { console.error("Failed to fetch auth state:", err); + setAuthError(err instanceof Error ? err.message : "Failed to connect to server"); } finally { setLoading(false); } @@ -147,7 +154,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { } return ( - + {children} );