4 Commits

Author SHA1 Message Date
0ec1beac8f Merge pull request 'Migrate JWT verification from HS256 to JWKS' (#75) from feature/migrate-jwt-verification-to-jwks into develop
All checks were successful
CI / backend-tests (push) Successful in 29s
CI / frontend-tests (push) Successful in 28s
Reviewed-on: #75
2026-03-22 09:26:22 +01:00
d23e24b826 Merge branch 'feature/migrate-jwt-verification-to-jwks' of https://gitea.nerdboden.de/pokemon/nuzlocke-tracker into feature/migrate-jwt-verification-to-jwks
All checks were successful
CI / backend-tests (pull_request) Successful in 31s
CI / frontend-tests (pull_request) Successful in 29s
2026-03-22 09:25:05 +01:00
e9eccc5b21 feat: migrate JWT verification from HS256 shared secret to JWKS
Replace symmetric HS256 JWT verification with asymmetric RS256 using JWKS.
Backend now fetches and caches public keys from Supabase's JWKS endpoint
instead of using a shared secret.

- Add cryptography dependency for RS256 support
- Use PyJWKClient to fetch/cache JWKS from {SUPABASE_URL}/.well-known/jwks.json
- Remove SUPABASE_JWT_SECRET from config, docker-compose, deploy workflow, .env
- Update tests to use RS256 tokens with mocked JWKS client

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 09:24:41 +01:00
177c02006a feat: migrate JWT verification from HS256 shared secret to JWKS
All checks were successful
CI / backend-tests (pull_request) Successful in 28s
CI / frontend-tests (pull_request) Successful in 28s
Replace symmetric HS256 JWT verification with asymmetric RS256 using JWKS.
Backend now fetches and caches public keys from Supabase's JWKS endpoint
instead of using a shared secret.

- Add cryptography dependency for RS256 support
- Use PyJWKClient to fetch/cache JWKS from {SUPABASE_URL}/.well-known/jwks.json
- Remove SUPABASE_JWT_SECRET from config, docker-compose, deploy workflow, .env
- Update tests to use RS256 tokens with mocked JWKS client

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 14:01:36 +01:00
11 changed files with 226 additions and 140 deletions

View File

@@ -1,21 +1,11 @@
--- ---
# nuzlocke-tracker-t9aj # nuzlocke-tracker-t9aj
title: Migrate JWT verification from HS256 shared secret to asymmetric keys (JWKS) title: Migrate JWT verification from HS256 shared secret to asymmetric keys (JWKS)
<<<<<<< Updated upstream
status: todo
type: task
priority: low
created_at: 2026-03-21T11:14:29Z
updated_at: 2026-03-21T13:01:46Z
---
The backend currently verifies Supabase JWTs using an HS256 shared secret (`SUPABASE_JWT_SECRET`). Supabase recommends migrating to asymmetric keys (RS256) for better security.\n\nInstead of storing a shared secret, the backend would fetch public keys from Supabase's JWKS endpoint (`https://<project>.supabase.co/.well-known/jwks.json`) and verify tokens against those.\n\n## Changes needed\n\n- [ ] Update `backend/src/app/core/auth.py` to fetch and cache JWKS public keys\n- [ ] Change `jwt.decode` from `HS256` to `RS256` with the fetched public key\n- [ ] Remove `SUPABASE_JWT_SECRET` from config, docker-compose, deploy workflow, and .env files\n- [ ] Update tests\n\n## References\n\n- https://supabase.com/docs/guides/auth/signing-keys\n- https://supabase.com/docs/guides/auth/jwts
=======
status: completed status: completed
type: task type: task
priority: low priority: low
created_at: 2026-03-21T11:14:29Z created_at: 2026-03-21T11:14:29Z
updated_at: 2026-03-22T08:14:34Z updated_at: 2026-03-21T13:01:33Z
--- ---
The backend currently verifies Supabase JWTs using an HS256 shared secret (`SUPABASE_JWT_SECRET`). Supabase recommends migrating to asymmetric keys (RS256) for better security.\n\nInstead of storing a shared secret, the backend would fetch public keys from Supabase's JWKS endpoint (`https://<project>.supabase.co/.well-known/jwks.json`) and verify tokens against those.\n\n## Changes needed\n\n- [x] Update `backend/src/app/core/auth.py` to fetch and cache JWKS public keys\n- [x] Change `jwt.decode` from `HS256` to `RS256` with the fetched public key\n- [x] Remove `SUPABASE_JWT_SECRET` from config, docker-compose, deploy workflow, and .env files\n- [x] Update tests\n\n## References\n\n- https://supabase.com/docs/guides/auth/signing-keys\n- https://supabase.com/docs/guides/auth/jwts The backend currently verifies Supabase JWTs using an HS256 shared secret (`SUPABASE_JWT_SECRET`). Supabase recommends migrating to asymmetric keys (RS256) for better security.\n\nInstead of storing a shared secret, the backend would fetch public keys from Supabase's JWKS endpoint (`https://<project>.supabase.co/.well-known/jwks.json`) and verify tokens against those.\n\n## Changes needed\n\n- [x] Update `backend/src/app/core/auth.py` to fetch and cache JWKS public keys\n- [x] Change `jwt.decode` from `HS256` to `RS256` with the fetched public key\n- [x] Remove `SUPABASE_JWT_SECRET` from config, docker-compose, deploy workflow, and .env files\n- [x] Update tests\n\n## References\n\n- https://supabase.com/docs/guides/auth/signing-keys\n- https://supabase.com/docs/guides/auth/jwts
@@ -23,13 +13,14 @@ The backend currently verifies Supabase JWTs using an HS256 shared secret (`SUPA
## Summary of Changes ## Summary of Changes
Migrated JWT verification from HS256 shared secret to RS256 asymmetric key verification using JWKS: - Added `cryptography==45.0.3` dependency for RS256 support
- Updated `auth.py` to use `PyJWKClient` for fetching and caching JWKS public keys from `{SUPABASE_URL}/.well-known/jwks.json`
- Changed JWT verification from HS256 to RS256
- Removed `supabase_jwt_secret` from config.py
- Updated docker-compose.yml: removed `SUPABASE_JWT_SECRET`, backend now uses JWKS from GoTrue URL
- Updated docker-compose.prod.yml: replaced `SUPABASE_JWT_SECRET` with `SUPABASE_URL`
- Updated deploy.yml: deploy workflow now writes `SUPABASE_URL` instead of `SUPABASE_JWT_SECRET`
- Updated .env.example files: removed `SUPABASE_JWT_SECRET` references
- Rewrote tests to use RS256 tokens with mocked JWKS client
- **auth.py**: Added `PyJWKClient` that fetches and caches public keys from Supabase's JWKS endpoint (`SUPABASE_URL/.well-known/jwks.json`). Keys are cached for 1 hour. **Note:** For production, add `SUPABASE_URL` to your GitHub secrets (should point to your Supabase project URL like `https://your-project.supabase.co`).
- **config.py**: Removed `supabase_jwt_secret` setting
- **pyproject.toml**: Changed `PyJWT` to `PyJWT[crypto]` for RS256 support
- **docker-compose.yml**: Configured local GoTrue for RS256 with mounted dev key
- **docker-compose.prod.yml**: Replaced `SUPABASE_JWT_SECRET` with `SUPABASE_URL`
- **deploy.yml**: Updated to pass `SUPABASE_URL` instead of `SUPABASE_JWT_SECRET`
- **tests**: Updated to use mocked JWKS client with RSA key pairs
>>>>>>> Stashed changes

View File

@@ -2,15 +2,13 @@
DEBUG=true DEBUG=true
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/nuzlocke DATABASE_URL=postgresql://postgres:postgres@localhost:5432/nuzlocke
# Supabase Auth (backend) # Supabase Auth (backend uses JWKS from this URL for JWT verification)
# For local dev with GoTrue container: # For local dev with GoTrue container:
SUPABASE_URL=http://localhost:9999 SUPABASE_URL=http://localhost:9999
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc0MDQwNjEzLCJleHAiOjIwODk0MDA2MTN9.EV6tRj7gLqoiT-l2vDFw_67myqRjwpcZTuRb3Xs1nr4 SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc0MDQwNjEzLCJleHAiOjIwODk0MDA2MTN9.EV6tRj7gLqoiT-l2vDFw_67myqRjwpcZTuRb3Xs1nr4
SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
# For production, replace with your Supabase cloud values: # For production, replace with your Supabase cloud values:
# SUPABASE_URL=https://your-project.supabase.co # SUPABASE_URL=https://your-project.supabase.co
# SUPABASE_ANON_KEY=your-anon-key # SUPABASE_ANON_KEY=your-anon-key
# SUPABASE_JWT_SECRET=your-jwt-secret
# Frontend settings (used by Vite) # Frontend settings (used by Vite)
VITE_API_URL=http://localhost:8000 VITE_API_URL=http://localhost:8000

View File

@@ -47,7 +47,7 @@ jobs:
# Write .env from secrets (overwrites any existing file) # Write .env from secrets (overwrites any existing file)
printf '%s\n' \ printf '%s\n' \
"POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" \ "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" \
"SUPABASE_JWT_SECRET=${{ secrets.SUPABASE_JWT_SECRET }}" \ "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" \
| $SSH_CMD "cat > '${DEPLOY_DIR}/.env'" | $SSH_CMD "cat > '${DEPLOY_DIR}/.env'"
$SCP_CMD docker-compose.prod.yml "root@192.168.1.10:${DEPLOY_DIR}/docker-compose.yml" $SCP_CMD docker-compose.prod.yml "root@192.168.1.10:${DEPLOY_DIR}/docker-compose.yml"

View File

@@ -8,7 +8,6 @@ API_V1_PREFIX="/api/v1"
# Database settings # Database settings
DATABASE_URL="sqlite:///./nuzlocke.db" DATABASE_URL="sqlite:///./nuzlocke.db"
# Supabase Auth # Supabase Auth (JWKS used for JWT verification)
SUPABASE_URL=https://your-project.supabase.co SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key SUPABASE_ANON_KEY=your-anon-key
SUPABASE_JWT_SECRET=your-jwt-secret

View File

@@ -14,6 +14,7 @@ dependencies = [
"asyncpg==0.31.0", "asyncpg==0.31.0",
"alembic==1.18.4", "alembic==1.18.4",
"PyJWT==2.12.1", "PyJWT==2.12.1",
"cryptography==45.0.3",
] ]
[project.optional-dependencies] [project.optional-dependencies]

View File

@@ -3,6 +3,7 @@ from uuid import UUID
import jwt import jwt
from fastapi import Depends, HTTPException, Request, status from fastapi import Depends, HTTPException, Request, status
from jwt import PyJWKClient, PyJWKClientError
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -11,6 +12,8 @@ from app.core.database import get_session
from app.models.nuzlocke_run import NuzlockeRun from app.models.nuzlocke_run import NuzlockeRun
from app.models.user import User from app.models.user import User
_jwks_client: PyJWKClient | None = None
@dataclass @dataclass
class AuthUser: class AuthUser:
@@ -21,6 +24,15 @@ class AuthUser:
role: str | None = None role: str | None = None
def _get_jwks_client() -> PyJWKClient | None:
"""Get or create a cached JWKS client."""
global _jwks_client
if _jwks_client is None and settings.supabase_url:
jwks_url = f"{settings.supabase_url.rstrip('/')}/.well-known/jwks.json"
_jwks_client = PyJWKClient(jwks_url, cache_jwk_set=True, lifespan=300)
return _jwks_client
def _extract_token(request: Request) -> str | None: def _extract_token(request: Request) -> str | None:
"""Extract Bearer token from Authorization header.""" """Extract Bearer token from Authorization header."""
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")
@@ -33,14 +45,16 @@ def _extract_token(request: Request) -> str | None:
def _verify_jwt(token: str) -> dict | None: def _verify_jwt(token: str) -> dict | None:
"""Verify JWT against Supabase JWT secret. Returns payload or None.""" """Verify JWT using JWKS public key. Returns payload or None."""
if not settings.supabase_jwt_secret: client = _get_jwks_client()
if not client:
return None return None
try: try:
signing_key = client.get_signing_key_from_jwt(token)
payload = jwt.decode( payload = jwt.decode(
token, token,
settings.supabase_jwt_secret, signing_key.key,
algorithms=["HS256"], algorithms=["RS256"],
audience="authenticated", audience="authenticated",
) )
return payload return payload
@@ -48,6 +62,8 @@ def _verify_jwt(token: str) -> dict | None:
return None return None
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
return None return None
except PyJWKClientError:
return None
def get_current_user(request: Request) -> AuthUser | None: def get_current_user(request: Request) -> AuthUser | None:

View File

@@ -20,7 +20,6 @@ class Settings(BaseSettings):
# Supabase Auth # Supabase Auth
supabase_url: str | None = None supabase_url: str | None = None
supabase_anon_key: str | None = None supabase_anon_key: str | None = None
supabase_jwt_secret: str | None = None
settings = Settings() settings = Settings()

View File

@@ -1,25 +1,29 @@
import time import time
from unittest.mock import MagicMock, patch
from uuid import UUID from uuid import UUID
import jwt import jwt
import pytest import pytest
from cryptography.hazmat.primitives.asymmetric import rsa
from httpx import ASGITransport, AsyncClient from httpx import ASGITransport, AsyncClient
from app.core.auth import AuthUser, get_current_user, require_admin, require_auth from app.core.auth import AuthUser, get_current_user, require_admin, require_auth
from app.core.config import settings
from app.main import app from app.main import app
from app.models.user import User from app.models.user import User
@pytest.fixture @pytest.fixture(scope="module")
def jwt_secret(): def rsa_key_pair():
"""Provide a test JWT secret.""" """Generate RSA key pair for testing."""
return "test-jwt-secret-for-testing-only" private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
return private_key, public_key
@pytest.fixture @pytest.fixture
def valid_token(jwt_secret): def valid_token(rsa_key_pair):
"""Generate a valid JWT token.""" """Generate a valid RS256 JWT token."""
private_key, _ = rsa_key_pair
payload = { payload = {
"sub": "user-123", "sub": "user-123",
"email": "test@example.com", "email": "test@example.com",
@@ -27,12 +31,13 @@ def valid_token(jwt_secret):
"aud": "authenticated", "aud": "authenticated",
"exp": int(time.time()) + 3600, "exp": int(time.time()) + 3600,
} }
return jwt.encode(payload, jwt_secret, algorithm="HS256") return jwt.encode(payload, private_key, algorithm="RS256")
@pytest.fixture @pytest.fixture
def expired_token(jwt_secret): def expired_token(rsa_key_pair):
"""Generate an expired JWT token.""" """Generate an expired RS256 JWT token."""
private_key, _ = rsa_key_pair
payload = { payload = {
"sub": "user-123", "sub": "user-123",
"email": "test@example.com", "email": "test@example.com",
@@ -40,12 +45,13 @@ def expired_token(jwt_secret):
"aud": "authenticated", "aud": "authenticated",
"exp": int(time.time()) - 3600, # Expired 1 hour ago "exp": int(time.time()) - 3600, # Expired 1 hour ago
} }
return jwt.encode(payload, jwt_secret, algorithm="HS256") return jwt.encode(payload, private_key, algorithm="RS256")
@pytest.fixture @pytest.fixture
def invalid_token(): def invalid_token():
"""Generate a token signed with wrong secret.""" """Generate a token signed with wrong key."""
wrong_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
payload = { payload = {
"sub": "user-123", "sub": "user-123",
"email": "test@example.com", "email": "test@example.com",
@@ -53,28 +59,23 @@ def invalid_token():
"aud": "authenticated", "aud": "authenticated",
"exp": int(time.time()) + 3600, "exp": int(time.time()) + 3600,
} }
return jwt.encode(payload, "wrong-secret", algorithm="HS256") return jwt.encode(payload, wrong_key, algorithm="RS256")
@pytest.fixture @pytest.fixture
def auth_client(db_session, jwt_secret, valid_token, monkeypatch): def mock_jwks_client(rsa_key_pair):
"""Client with valid auth token and configured JWT secret.""" """Create a mock JWKS client that returns our test public key."""
monkeypatch.setattr(settings, "supabase_jwt_secret", jwt_secret) _, public_key = rsa_key_pair
mock_client = MagicMock()
async def _get_client(): mock_signing_key = MagicMock()
async with AsyncClient( mock_signing_key.key = public_key
transport=ASGITransport(app=app), mock_client.get_signing_key_from_jwt.return_value = mock_signing_key
base_url="http://test", return mock_client
headers={"Authorization": f"Bearer {valid_token}"},
) as ac:
yield ac
return _get_client
async def test_get_current_user_valid_token(jwt_secret, valid_token, monkeypatch): async def test_get_current_user_valid_token(valid_token, mock_jwks_client):
"""Test get_current_user returns user for valid token.""" """Test get_current_user returns user for valid token."""
monkeypatch.setattr(settings, "supabase_jwt_secret", jwt_secret) with patch("app.core.auth._get_jwks_client", return_value=mock_jwks_client):
class MockRequest: class MockRequest:
headers = {"Authorization": f"Bearer {valid_token}"} headers = {"Authorization": f"Bearer {valid_token}"}
@@ -86,9 +87,9 @@ async def test_get_current_user_valid_token(jwt_secret, valid_token, monkeypatch
assert user.role == "authenticated" assert user.role == "authenticated"
async def test_get_current_user_no_token(jwt_secret, monkeypatch): async def test_get_current_user_no_token(mock_jwks_client):
"""Test get_current_user returns None when no token.""" """Test get_current_user returns None when no token."""
monkeypatch.setattr(settings, "supabase_jwt_secret", jwt_secret) with patch("app.core.auth._get_jwks_client", return_value=mock_jwks_client):
class MockRequest: class MockRequest:
headers = {} headers = {}
@@ -97,9 +98,9 @@ async def test_get_current_user_no_token(jwt_secret, monkeypatch):
assert user is None assert user is None
async def test_get_current_user_expired_token(jwt_secret, expired_token, monkeypatch): async def test_get_current_user_expired_token(expired_token, mock_jwks_client):
"""Test get_current_user returns None for expired token.""" """Test get_current_user returns None for expired token."""
monkeypatch.setattr(settings, "supabase_jwt_secret", jwt_secret) with patch("app.core.auth._get_jwks_client", return_value=mock_jwks_client):
class MockRequest: class MockRequest:
headers = {"Authorization": f"Bearer {expired_token}"} headers = {"Authorization": f"Bearer {expired_token}"}
@@ -108,9 +109,9 @@ async def test_get_current_user_expired_token(jwt_secret, expired_token, monkeyp
assert user is None assert user is None
async def test_get_current_user_invalid_token(jwt_secret, invalid_token, monkeypatch): async def test_get_current_user_invalid_token(invalid_token, mock_jwks_client):
"""Test get_current_user returns None for invalid token.""" """Test get_current_user returns None for invalid token."""
monkeypatch.setattr(settings, "supabase_jwt_secret", jwt_secret) with patch("app.core.auth._get_jwks_client", return_value=mock_jwks_client):
class MockRequest: class MockRequest:
headers = {"Authorization": f"Bearer {invalid_token}"} headers = {"Authorization": f"Bearer {invalid_token}"}
@@ -119,9 +120,9 @@ async def test_get_current_user_invalid_token(jwt_secret, invalid_token, monkeyp
assert user is None assert user is None
async def test_get_current_user_malformed_header(jwt_secret, monkeypatch): async def test_get_current_user_malformed_header(mock_jwks_client):
"""Test get_current_user returns None for malformed auth header.""" """Test get_current_user returns None for malformed auth header."""
monkeypatch.setattr(settings, "supabase_jwt_secret", jwt_secret) with patch("app.core.auth._get_jwks_client", return_value=mock_jwks_client):
class MockRequest: class MockRequest:
headers = {"Authorization": "NotBearer token"} headers = {"Authorization": "NotBearer token"}
@@ -158,11 +159,10 @@ async def test_protected_endpoint_without_token(db_session):
async def test_protected_endpoint_with_expired_token( async def test_protected_endpoint_with_expired_token(
db_session, jwt_secret, expired_token, monkeypatch db_session, expired_token, mock_jwks_client
): ):
"""Test that write endpoint returns 401 with expired token.""" """Test that write endpoint returns 401 with expired token."""
monkeypatch.setattr(settings, "supabase_jwt_secret", jwt_secret) with patch("app.core.auth._get_jwks_client", return_value=mock_jwks_client):
async with AsyncClient( async with AsyncClient(
transport=ASGITransport(app=app), transport=ASGITransport(app=app),
base_url="http://test", base_url="http://test",
@@ -231,7 +231,7 @@ async def test_require_admin_user_not_in_db(db_session):
async def test_admin_endpoint_returns_403_for_non_admin( async def test_admin_endpoint_returns_403_for_non_admin(
db_session, jwt_secret, monkeypatch db_session, rsa_key_pair, mock_jwks_client
): ):
"""Test that admin endpoint returns 403 for authenticated non-admin user.""" """Test that admin endpoint returns 403 for authenticated non-admin user."""
user_id = "44444444-4444-4444-4444-444444444444" user_id = "44444444-4444-4444-4444-444444444444"
@@ -243,7 +243,7 @@ async def test_admin_endpoint_returns_403_for_non_admin(
db_session.add(regular_user) db_session.add(regular_user)
await db_session.commit() await db_session.commit()
monkeypatch.setattr(settings, "supabase_jwt_secret", jwt_secret) private_key, _ = rsa_key_pair
token = jwt.encode( token = jwt.encode(
{ {
"sub": user_id, "sub": user_id,
@@ -252,10 +252,11 @@ async def test_admin_endpoint_returns_403_for_non_admin(
"aud": "authenticated", "aud": "authenticated",
"exp": int(time.time()) + 3600, "exp": int(time.time()) + 3600,
}, },
jwt_secret, private_key,
algorithm="HS256", algorithm="RS256",
) )
with patch("app.core.auth._get_jwks_client", return_value=mock_jwks_client):
async with AsyncClient( async with AsyncClient(
transport=ASGITransport(app=app), transport=ASGITransport(app=app),
base_url="http://test", base_url="http://test",
@@ -275,7 +276,9 @@ async def test_admin_endpoint_returns_403_for_non_admin(
assert response.json()["detail"] == "Admin access required" assert response.json()["detail"] == "Admin access required"
async def test_admin_endpoint_succeeds_for_admin(db_session, jwt_secret, monkeypatch): async def test_admin_endpoint_succeeds_for_admin(
db_session, rsa_key_pair, mock_jwks_client
):
"""Test that admin endpoint succeeds for authenticated admin user.""" """Test that admin endpoint succeeds for authenticated admin user."""
user_id = "55555555-5555-5555-5555-555555555555" user_id = "55555555-5555-5555-5555-555555555555"
admin_user = User( admin_user = User(
@@ -286,7 +289,7 @@ async def test_admin_endpoint_succeeds_for_admin(db_session, jwt_secret, monkeyp
db_session.add(admin_user) db_session.add(admin_user)
await db_session.commit() await db_session.commit()
monkeypatch.setattr(settings, "supabase_jwt_secret", jwt_secret) private_key, _ = rsa_key_pair
token = jwt.encode( token = jwt.encode(
{ {
"sub": user_id, "sub": user_id,
@@ -295,10 +298,11 @@ async def test_admin_endpoint_succeeds_for_admin(db_session, jwt_secret, monkeyp
"aud": "authenticated", "aud": "authenticated",
"exp": int(time.time()) + 3600, "exp": int(time.time()) + 3600,
}, },
jwt_secret, private_key,
algorithm="HS256", algorithm="RS256",
) )
with patch("app.core.auth._get_jwks_client", return_value=mock_jwks_client):
async with AsyncClient( async with AsyncClient(
transport=ASGITransport(app=app), transport=ASGITransport(app=app),
base_url="http://test", base_url="http://test",

79
backend/uv.lock generated
View File

@@ -41,6 +41,7 @@ source = { editable = "." }
dependencies = [ dependencies = [
{ name = "alembic" }, { name = "alembic" },
{ name = "asyncpg" }, { name = "asyncpg" },
{ name = "cryptography" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
@@ -63,6 +64,7 @@ dev = [
requires-dist = [ requires-dist = [
{ name = "alembic", specifier = "==1.18.4" }, { name = "alembic", specifier = "==1.18.4" },
{ name = "asyncpg", specifier = "==0.31.0" }, { name = "asyncpg", specifier = "==0.31.0" },
{ name = "cryptography", specifier = "==45.0.3" },
{ name = "fastapi", specifier = "==0.135.1" }, { name = "fastapi", specifier = "==0.135.1" },
{ name = "httpx", marker = "extra == 'dev'", specifier = "==0.28.1" }, { name = "httpx", marker = "extra == 'dev'", specifier = "==0.28.1" },
{ name = "pydantic", specifier = "==2.12.5" }, { name = "pydantic", specifier = "==2.12.5" },
@@ -123,6 +125,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
] ]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.3.1" version = "8.3.1"
@@ -144,6 +179,41 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
] ]
[[package]]
name = "cryptography"
version = "45.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/13/1f/9fa001e74a1993a9cadd2333bb889e50c66327b8594ac538ab8a04f915b7/cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899", size = 744738, upload-time = "2025-05-25T14:17:24.777Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/b2/2345dc595998caa6f68adf84e8f8b50d18e9fc4638d32b22ea8daedd4b7a/cryptography-45.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71", size = 7056239, upload-time = "2025-05-25T14:16:12.22Z" },
{ url = "https://files.pythonhosted.org/packages/71/3d/ac361649a0bfffc105e2298b720d8b862330a767dab27c06adc2ddbef96a/cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b", size = 4205541, upload-time = "2025-05-25T14:16:14.333Z" },
{ url = "https://files.pythonhosted.org/packages/70/3e/c02a043750494d5c445f769e9c9f67e550d65060e0bfce52d91c1362693d/cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f", size = 4433275, upload-time = "2025-05-25T14:16:16.421Z" },
{ url = "https://files.pythonhosted.org/packages/40/7a/9af0bfd48784e80eef3eb6fd6fde96fe706b4fc156751ce1b2b965dada70/cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942", size = 4209173, upload-time = "2025-05-25T14:16:18.163Z" },
{ url = "https://files.pythonhosted.org/packages/31/5f/d6f8753c8708912df52e67969e80ef70b8e8897306cd9eb8b98201f8c184/cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9", size = 3898150, upload-time = "2025-05-25T14:16:20.34Z" },
{ url = "https://files.pythonhosted.org/packages/8b/50/f256ab79c671fb066e47336706dc398c3b1e125f952e07d54ce82cf4011a/cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56", size = 4466473, upload-time = "2025-05-25T14:16:22.605Z" },
{ url = "https://files.pythonhosted.org/packages/62/e7/312428336bb2df0848d0768ab5a062e11a32d18139447a76dfc19ada8eed/cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca", size = 4211890, upload-time = "2025-05-25T14:16:24.738Z" },
{ url = "https://files.pythonhosted.org/packages/e7/53/8a130e22c1e432b3c14896ec5eb7ac01fb53c6737e1d705df7e0efb647c6/cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1", size = 4466300, upload-time = "2025-05-25T14:16:26.768Z" },
{ url = "https://files.pythonhosted.org/packages/ba/75/6bb6579688ef805fd16a053005fce93944cdade465fc92ef32bbc5c40681/cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578", size = 4332483, upload-time = "2025-05-25T14:16:28.316Z" },
{ url = "https://files.pythonhosted.org/packages/2f/11/2538f4e1ce05c6c4f81f43c1ef2bd6de7ae5e24ee284460ff6c77e42ca77/cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497", size = 4573714, upload-time = "2025-05-25T14:16:30.474Z" },
{ url = "https://files.pythonhosted.org/packages/f5/bb/e86e9cf07f73a98d84a4084e8fd420b0e82330a901d9cac8149f994c3417/cryptography-45.0.3-cp311-abi3-win32.whl", hash = "sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710", size = 2934752, upload-time = "2025-05-25T14:16:32.204Z" },
{ url = "https://files.pythonhosted.org/packages/c7/75/063bc9ddc3d1c73e959054f1fc091b79572e716ef74d6caaa56e945b4af9/cryptography-45.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490", size = 3412465, upload-time = "2025-05-25T14:16:33.888Z" },
{ url = "https://files.pythonhosted.org/packages/71/9b/04ead6015229a9396890d7654ee35ef630860fb42dc9ff9ec27f72157952/cryptography-45.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06", size = 7031892, upload-time = "2025-05-25T14:16:36.214Z" },
{ url = "https://files.pythonhosted.org/packages/46/c7/c7d05d0e133a09fc677b8a87953815c522697bdf025e5cac13ba419e7240/cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57", size = 4196181, upload-time = "2025-05-25T14:16:37.934Z" },
{ url = "https://files.pythonhosted.org/packages/08/7a/6ad3aa796b18a683657cef930a986fac0045417e2dc428fd336cfc45ba52/cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716", size = 4423370, upload-time = "2025-05-25T14:16:39.502Z" },
{ url = "https://files.pythonhosted.org/packages/4f/58/ec1461bfcb393525f597ac6a10a63938d18775b7803324072974b41a926b/cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8", size = 4197839, upload-time = "2025-05-25T14:16:41.322Z" },
{ url = "https://files.pythonhosted.org/packages/d4/3d/5185b117c32ad4f40846f579369a80e710d6146c2baa8ce09d01612750db/cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc", size = 3886324, upload-time = "2025-05-25T14:16:43.041Z" },
{ url = "https://files.pythonhosted.org/packages/67/85/caba91a57d291a2ad46e74016d1f83ac294f08128b26e2a81e9b4f2d2555/cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342", size = 4450447, upload-time = "2025-05-25T14:16:44.759Z" },
{ url = "https://files.pythonhosted.org/packages/ae/d1/164e3c9d559133a38279215c712b8ba38e77735d3412f37711b9f8f6f7e0/cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b", size = 4200576, upload-time = "2025-05-25T14:16:46.438Z" },
{ url = "https://files.pythonhosted.org/packages/71/7a/e002d5ce624ed46dfc32abe1deff32190f3ac47ede911789ee936f5a4255/cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782", size = 4450308, upload-time = "2025-05-25T14:16:48.228Z" },
{ url = "https://files.pythonhosted.org/packages/87/ad/3fbff9c28cf09b0a71e98af57d74f3662dea4a174b12acc493de00ea3f28/cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65", size = 4325125, upload-time = "2025-05-25T14:16:49.844Z" },
{ url = "https://files.pythonhosted.org/packages/f5/b4/51417d0cc01802304c1984d76e9592f15e4801abd44ef7ba657060520bf0/cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b", size = 4560038, upload-time = "2025-05-25T14:16:51.398Z" },
{ url = "https://files.pythonhosted.org/packages/80/38/d572f6482d45789a7202fb87d052deb7a7b136bf17473ebff33536727a2c/cryptography-45.0.3-cp37-abi3-win32.whl", hash = "sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab", size = 2924070, upload-time = "2025-05-25T14:16:53.472Z" },
{ url = "https://files.pythonhosted.org/packages/91/5a/61f39c0ff4443651cc64e626fa97ad3099249152039952be8f344d6b0c86/cryptography-45.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2", size = 3395005, upload-time = "2025-05-25T14:16:55.134Z" },
]
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.135.1" version = "0.135.1"
@@ -315,6 +385,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
] ]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.12.5" version = "2.12.5"

View File

@@ -6,7 +6,7 @@ services:
environment: environment:
- DEBUG=false - DEBUG=false
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/nuzlocke - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/nuzlocke
- SUPABASE_JWT_SECRET=${SUPABASE_JWT_SECRET} - SUPABASE_URL=${SUPABASE_URL}
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy

View File

@@ -12,9 +12,8 @@ services:
environment: environment:
- DEBUG=true - DEBUG=true
- DATABASE_URL=postgresql://postgres:postgres@db:5432/nuzlocke - DATABASE_URL=postgresql://postgres:postgres@db:5432/nuzlocke
# Auth - must match GoTrue's JWT secret # Auth - uses JWKS from GoTrue for JWT verification
- SUPABASE_URL=http://gotrue:9999 - SUPABASE_URL=http://gotrue:9999
- SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy