feat(auth): enhance error handling in requireAuth and add authError state in AuthProvider
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,15 +28,39 @@ const EnvSchema = z.object({
|
||||
export type Env = z.infer<typeof EnvSchema>;
|
||||
|
||||
// Parse and validate
|
||||
const parsed = EnvSchema.parse(process.env);
|
||||
let parsed: z.infer<typeof EnvSchema>;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+39
-2
@@ -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 (
|
||||
<div className="auth-container">
|
||||
<div className="auth-card" style={{ textAlign: "center" }}>
|
||||
<h1 className="auth-title">💊 MedAssist</h1>
|
||||
<div className="auth-error" style={{ marginBottom: "1rem" }}>
|
||||
<strong>Connection Error</strong><br />
|
||||
{authError}
|
||||
</div>
|
||||
<p style={{ fontSize: "0.9rem", color: "var(--text-muted)" }}>
|
||||
Please check if the server is running and try again.
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => window.location.reload()}
|
||||
style={{ marginTop: "1rem" }}
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If auth state is null (shouldn't happen after loading, but be safe)
|
||||
if (!authState) {
|
||||
return (
|
||||
<div className="auth-container">
|
||||
<div className="auth-card" style={{ textAlign: "center" }}>
|
||||
<h1 className="auth-title">💊 MedAssist</h1>
|
||||
<p>Initializing...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If auth is enabled
|
||||
if (authState?.authEnabled) {
|
||||
if (authState.authEnabled) {
|
||||
// Need to register first user
|
||||
if (authState.needsSetup) {
|
||||
return <AuthPage />;
|
||||
|
||||
@@ -21,6 +21,7 @@ interface AuthContextType {
|
||||
user: User | null;
|
||||
authState: AuthState | null;
|
||||
loading: boolean;
|
||||
authError: string | null;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
register: (username: string, password: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
@@ -48,6 +49,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [authState, setAuthState] = useState<AuthState | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [authError, setAuthError] = useState<string | null>(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 (
|
||||
<AuthContext.Provider value={{ user, authState, loading, login, register, logout, refreshUser, updateProfile }}>
|
||||
<AuthContext.Provider value={{ user, authState, loading, authError, login, register, logout, refreshUser, updateProfile }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user