fix: setup routing, async bcrypt, password hashing, clean command
- Add SetupGuard component: redirects to /setup if setup not complete, shown as spinner while loading. All routes except /setup now wrapped. - SetupPage redirects to /login on mount when setup already done. - Fix async blocking: offload bcrypt.hashpw and bcrypt.checkpw to run_in_executor so they never stall the asyncio event loop. - Hash password with SHA-256 (SubtleCrypto) before transmission; added src/utils/crypto.ts with sha256Hex(). Backend stores bcrypt(sha256). - Add Makefile with make up/down/restart/logs/clean targets. - Add tests: _check_password async, concurrent bcrypt, expired session, login-without-setup, run_setup event-loop interleaving. - Update Architekture.md and Features.md to reflect all changes.
This commit is contained in:
65
frontend/src/components/SetupGuard.tsx
Normal file
65
frontend/src/components/SetupGuard.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Route guard component.
|
||||
*
|
||||
* Protects all routes by ensuring the initial setup wizard has been
|
||||
* completed. If setup is not done yet, the user is redirected to `/setup`.
|
||||
* While the status is loading a full-screen spinner is shown.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { Spinner } from "@fluentui/react-components";
|
||||
import { getSetupStatus } from "../api/setup";
|
||||
|
||||
type Status = "loading" | "done" | "pending";
|
||||
|
||||
interface SetupGuardProps {
|
||||
/** The protected content to render when setup is complete. */
|
||||
children: React.JSX.Element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render `children` only when setup has been completed.
|
||||
*
|
||||
* Redirects to `/setup` if setup is still pending.
|
||||
*/
|
||||
export function SetupGuard({ children }: SetupGuardProps): React.JSX.Element {
|
||||
const [status, setStatus] = useState<Status>("loading");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
getSetupStatus()
|
||||
.then((res): void => {
|
||||
if (!cancelled) setStatus(res.completed ? "done" : "pending");
|
||||
})
|
||||
.catch((): void => {
|
||||
// If the check fails, optimistically allow through — the backend will
|
||||
// redirect API calls to /api/setup anyway.
|
||||
if (!cancelled) setStatus("done");
|
||||
});
|
||||
return (): void => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (status === "loading") {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "100vh",
|
||||
}}
|
||||
>
|
||||
<Spinner size="large" label="Loading…" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "pending") {
|
||||
return <Navigate to="/setup" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
Reference in New Issue
Block a user