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

@@ -6,7 +6,7 @@
* All fields use Fluent UI v9 components and inline validation.
*/
import { useState } from "react";
import { useEffect, useState } from "react";
import {
Button,
Field,
@@ -21,7 +21,8 @@ import {
import { useNavigate } from "react-router-dom";
import type { ChangeEvent, FormEvent } from "react";
import { ApiError } from "../api/client";
import { submitSetup } from "../api/setup";
import { getSetupStatus, submitSetup } from "../api/setup";
import { sha256Hex } from "../utils/crypto";
// ---------------------------------------------------------------------------
// Styles
@@ -105,6 +106,17 @@ export function SetupPage(): React.JSX.Element {
const [apiError, setApiError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
// Redirect to /login if setup has already been completed.
useEffect(() => {
getSetupStatus()
.then((res) => {
if (res.completed) navigate("/login", { replace: true });
})
.catch(() => {
/* ignore — stay on setup page */
});
}, [navigate]);
// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------
@@ -149,8 +161,11 @@ export function SetupPage(): React.JSX.Element {
setSubmitting(true);
try {
// Hash the password client-side before transmission — the plaintext
// never leaves the browser. The backend bcrypt-hashes the received hash.
const hashedPassword = await sha256Hex(values.masterPassword);
await submitSetup({
master_password: values.masterPassword,
master_password: hashedPassword,
database_path: values.databasePath,
fail2ban_socket: values.fail2banSocket,
timezone: values.timezone,