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:
2026-03-01 19:16:49 +01:00
parent 1cdc97a729
commit c097e55222
13 changed files with 347 additions and 394 deletions

View 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;
}