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

@@ -2,6 +2,8 @@
from __future__ import annotations
import asyncio
import inspect
from pathlib import Path
import aiosqlite
@@ -115,3 +117,34 @@ class TestGetTimezone:
session_duration_minutes=60,
)
assert await setup_service.get_timezone(db) == "America/New_York"
class TestRunSetupAsync:
"""Verify the async/non-blocking bcrypt behavior of run_setup."""
async def test_run_setup_is_coroutine_function(self) -> None:
"""run_setup must be declared as an async function."""
assert inspect.iscoroutinefunction(setup_service.run_setup)
async def test_password_hash_does_not_block_event_loop(
self, db: aiosqlite.Connection
) -> None:
"""run_setup completes without blocking; other coroutines can interleave."""
async def noop() -> str:
"""A trivial coroutine that should run concurrently with setup."""
await asyncio.sleep(0)
return "ok"
setup_coro = setup_service.run_setup(
db,
master_password="mypassword1",
database_path="bangui.db",
fail2ban_socket="/var/run/fail2ban/fail2ban.sock",
timezone="UTC",
session_duration_minutes=60,
)
# Both tasks should finish without error.
results = await asyncio.gather(setup_coro, noop())
assert results[1] == "ok"
assert await setup_service.is_setup_complete(db) is True