auth: replace UUID-based login with JWT credential verification
Login now requires login/email + password verified against DB via /api/auth/login. Only approved registrations can access the app. Signal endpoint accepts JWT Bearer tokens alongside legacy api_key auth. Old UUID-only registration flow removed from frontend. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1adcabf3a6
commit
04f7bd79e2
8 changed files with 173 additions and 128 deletions
|
|
@ -222,9 +222,9 @@ def test_html_loads_app_js() -> None:
|
|||
assert "/app.js" in _html()
|
||||
|
||||
|
||||
def test_html_has_name_input() -> None:
|
||||
"""index.html must have name input field for onboarding."""
|
||||
assert 'id="name-input"' in _html()
|
||||
def test_html_has_login_input() -> None:
|
||||
"""index.html must have login input field for onboarding."""
|
||||
assert 'id="login-input"' in _html()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -316,31 +316,19 @@ def _app_js() -> str:
|
|||
return (FRONTEND / "app.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_app_uses_crypto_random_uuid() -> None:
|
||||
"""app.js must generate UUID via crypto.randomUUID()."""
|
||||
assert "crypto.randomUUID()" in _app_js()
|
||||
def test_app_posts_to_auth_login() -> None:
|
||||
"""app.js must send POST to /api/auth/login during login."""
|
||||
assert "/api/auth/login" in _app_js()
|
||||
|
||||
|
||||
def test_app_posts_to_api_register() -> None:
|
||||
"""app.js must send POST to /api/register during onboarding."""
|
||||
assert "/api/register" in _app_js()
|
||||
def test_app_posts_to_auth_register() -> None:
|
||||
"""app.js must send POST to /api/auth/register during registration."""
|
||||
assert "/api/auth/register" in _app_js()
|
||||
|
||||
|
||||
def test_app_register_sends_uuid() -> None:
|
||||
"""app.js must include uuid in the /api/register request body."""
|
||||
app = _app_js()
|
||||
# The register call must include uuid in the payload
|
||||
register_section = re.search(
|
||||
r"_apiPost\(['\"]\/api\/register['\"].*?\)", app, re.DOTALL
|
||||
)
|
||||
assert register_section, "No _apiPost('/api/register') call found"
|
||||
assert "uuid" in register_section.group(0), \
|
||||
"uuid not included in /api/register call"
|
||||
|
||||
|
||||
def test_app_uuid_saved_to_storage() -> None:
|
||||
"""app.js must persist UUID to storage (baton_user_id key)."""
|
||||
assert "baton_user_id" in _app_js()
|
||||
def test_app_stores_auth_token() -> None:
|
||||
"""app.js must persist JWT token to storage."""
|
||||
assert "baton_auth_token" in _app_js()
|
||||
assert "setItem" in _app_js()
|
||||
|
||||
|
||||
|
|
@ -434,16 +422,14 @@ def test_app_posts_to_api_signal() -> None:
|
|||
assert "/api/signal" in _app_js()
|
||||
|
||||
|
||||
def test_app_signal_sends_user_id() -> None:
|
||||
"""app.js must include user_id (UUID) in the /api/signal request body."""
|
||||
def test_app_signal_sends_auth_header() -> None:
|
||||
"""app.js must include Authorization Bearer header in /api/signal request."""
|
||||
app = _app_js()
|
||||
# The signal body may be built in a variable before passing to _apiPost
|
||||
# Look for user_id key in the context around /api/signal
|
||||
signal_area = re.search(
|
||||
r"user_id.*?_apiPost\(['\"]\/api\/signal", app, re.DOTALL
|
||||
r"_apiPost\(['\"]\/api\/signal['\"].*Authorization.*Bearer", app, re.DOTALL
|
||||
)
|
||||
assert signal_area, \
|
||||
"user_id must be set in the request body before calling _apiPost('/api/signal')"
|
||||
"Authorization Bearer header must be set in _apiPost('/api/signal') call"
|
||||
|
||||
|
||||
def test_app_sos_button_click_calls_handle_signal() -> None:
|
||||
|
|
@ -456,15 +442,15 @@ def test_app_sos_button_click_calls_handle_signal() -> None:
|
|||
"btn-sos must be connected to _handleSignal"
|
||||
|
||||
|
||||
def test_app_signal_uses_uuid_from_storage() -> None:
|
||||
"""app.js must retrieve UUID from storage (_getOrCreateUserId) before sending signal."""
|
||||
def test_app_signal_uses_token_from_storage() -> None:
|
||||
"""app.js must retrieve auth token from storage before sending signal."""
|
||||
app = _app_js()
|
||||
handle_signal = re.search(
|
||||
r"async function _handleSignal\(\).*?^}", app, re.MULTILINE | re.DOTALL
|
||||
)
|
||||
assert handle_signal, "_handleSignal function not found"
|
||||
assert "_getOrCreateUserId" in handle_signal.group(0), \
|
||||
"_handleSignal must call _getOrCreateUserId() to get UUID"
|
||||
assert "_getAuthToken" in handle_signal.group(0), \
|
||||
"_handleSignal must call _getAuthToken() to get JWT token"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -102,10 +102,10 @@ def test_register_request_rejects_old_placeholder_uuid(bad_uuid: str) -> None:
|
|||
"agg-uuid-001",
|
||||
"create-uuid-001",
|
||||
])
|
||||
def test_signal_request_rejects_old_placeholder_uuid(bad_uuid: str) -> None:
|
||||
"""SignalRequest.user_id must reject old-style placeholder strings."""
|
||||
with pytest.raises(ValidationError):
|
||||
SignalRequest(user_id=bad_uuid, timestamp=1700000000000)
|
||||
def test_signal_request_accepts_any_user_id_string(bad_uuid: str) -> None:
|
||||
"""SignalRequest.user_id is optional (no pattern) — validation is at endpoint level."""
|
||||
req = SignalRequest(user_id=bad_uuid, timestamp=1700000000000)
|
||||
assert req.user_id == bad_uuid
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -152,17 +152,16 @@ def test_register_request_rejects_uuid_v3_version_digit() -> None:
|
|||
RegisterRequest(uuid="550e8400-e29b-31d4-a716-446655440000", name="Test")
|
||||
|
||||
|
||||
def test_signal_request_rejects_uuid_wrong_variant_bits() -> None:
|
||||
"""UUID with invalid variant bits (0xxx in fourth group) must be rejected."""
|
||||
with pytest.raises(ValidationError):
|
||||
# fourth group starts with '0' — not 8/9/a/b variant
|
||||
SignalRequest(user_id="550e8400-e29b-41d4-0716-446655440000", timestamp=1700000000000)
|
||||
def test_signal_request_accepts_any_variant_bits() -> None:
|
||||
"""SignalRequest.user_id is now optional and unvalidated (JWT auth doesn't use it)."""
|
||||
req = SignalRequest(user_id="550e8400-e29b-41d4-0716-446655440000", timestamp=1700000000000)
|
||||
assert req.user_id is not None
|
||||
|
||||
|
||||
def test_signal_request_rejects_uuid_wrong_variant_c() -> None:
|
||||
"""UUID with variant 'c' (1100 bits) must be rejected — only 8/9/a/b allowed."""
|
||||
with pytest.raises(ValidationError):
|
||||
SignalRequest(user_id="550e8400-e29b-41d4-c716-446655440000", timestamp=1700000000000)
|
||||
def test_signal_request_without_user_id() -> None:
|
||||
"""SignalRequest works without user_id (JWT auth mode)."""
|
||||
req = SignalRequest(timestamp=1700000000000)
|
||||
assert req.user_id is None
|
||||
|
||||
|
||||
def test_register_request_accepts_all_valid_v4_variants() -> None:
|
||||
|
|
|
|||
|
|
@ -123,14 +123,16 @@ def test_signal_request_no_geo():
|
|||
assert req.geo is None
|
||||
|
||||
|
||||
def test_signal_request_missing_user_id():
|
||||
with pytest.raises(ValidationError):
|
||||
SignalRequest(timestamp=1742478000000) # type: ignore[call-arg]
|
||||
def test_signal_request_without_user_id():
|
||||
"""user_id is optional (JWT auth sends signals without it)."""
|
||||
req = SignalRequest(timestamp=1742478000000)
|
||||
assert req.user_id is None
|
||||
|
||||
|
||||
def test_signal_request_empty_user_id():
|
||||
with pytest.raises(ValidationError):
|
||||
SignalRequest(user_id="", timestamp=1742478000000)
|
||||
"""Empty string user_id is accepted (treated as None at endpoint level)."""
|
||||
req = SignalRequest(user_id="", timestamp=1742478000000)
|
||||
assert req.user_id == ""
|
||||
|
||||
|
||||
def test_signal_request_timestamp_zero():
|
||||
|
|
|
|||
|
|
@ -78,14 +78,14 @@ async def test_signal_without_geo_success():
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_signal_missing_user_id_returns_422():
|
||||
"""Missing user_id field must return 422."""
|
||||
async def test_signal_missing_auth_returns_401():
|
||||
"""Missing Authorization header must return 401."""
|
||||
async with make_app_client() as client:
|
||||
resp = await client.post(
|
||||
"/api/signal",
|
||||
json={"timestamp": 1742478000000},
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue