Feat: Implement JWT bearer token authentication

Protects image upload, delete, and tag-update endpoints behind
Bearer token auth. Public read endpoints remain open. Angular SPA
gains a login page, auth interceptor, and route guard for /upload.

- JWTAuthProvider (HS256, sub/iat/exp, secrets.compare_digest)
- POST /api/v1/auth/token login endpoint
- require_auth FastAPI dependency on all write routes
- AuthService, LoginComponent, authInterceptor, authGuard
- Detail page hides write controls for unauthenticated visitors
- 43 unit tests passing; integration tests require Docker stack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-03 19:12:38 +00:00
parent d91a65abe5
commit 5fbbc1e67f
36 changed files with 3998 additions and 42 deletions

View File

@@ -0,0 +1,51 @@
import pytest
_VALID_CREDS = {"username": "testowner", "password": "testpassword"}
@pytest.mark.asyncio
async def test_login_success(authed_client):
client, _ = authed_client
response = await client.post("/api/v1/auth/token", json=_VALID_CREDS)
assert response.status_code == 200
body = response.json()
assert isinstance(body.get("access_token"), str)
assert len(body["access_token"]) > 0
assert body.get("token_type") == "bearer"
assert body.get("expires_in", 0) > 0
@pytest.mark.asyncio
async def test_login_wrong_password(authed_client):
client, _ = authed_client
response = await client.post(
"/api/v1/auth/token",
json={"username": "testowner", "password": "wrongpassword"},
)
assert response.status_code == 401
assert response.json().get("code") == "invalid_credentials"
@pytest.mark.asyncio
async def test_login_wrong_username(authed_client):
client, _ = authed_client
response = await client.post(
"/api/v1/auth/token",
json={"username": "notowner", "password": "testpassword"},
)
assert response.status_code == 401
assert response.json().get("code") == "invalid_credentials"
@pytest.mark.asyncio
async def test_login_missing_password(authed_client):
client, _ = authed_client
response = await client.post("/api/v1/auth/token", json={"username": "testowner"})
assert response.status_code == 422
@pytest.mark.asyncio
async def test_login_missing_username(authed_client):
client, _ = authed_client
response = await client.post("/api/v1/auth/token", json={"password": "testpassword"})
assert response.status_code == 422