feat: Intervals.icu MCP portal (FastAPI + Authentik OIDC)
- OIDC login via Authentik; first login creates a disabled user (admin approval). - Users set/replace their Intervals.icu athlete ID + API key; the key is validated against Intervals.icu and stored AES-256-GCM encrypted (shared key with the MCP server). Same users table (schema owned by the MCP server's migrations). - Admin page (gated on the intervalsicu-mcp-admins group claim): list, approve, disable, delete users. - 29 tests @ 93% (OIDC routes integration-only); Dockerfile asserts templates are packaged; .gitea CI test-gates the image build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""Store operations against in-memory SQLite (mirrors the MCP server's schema)."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from intervalsicu_mcp_ui import crypto, db
|
||||
|
||||
KEY = crypto.generate_key_b64()
|
||||
|
||||
|
||||
def _run_db(monkeypatch, body):
|
||||
async def go():
|
||||
monkeypatch.setenv("INTERVALS_ENC_KEY", KEY)
|
||||
engine = create_async_engine(
|
||||
"sqlite+aiosqlite:///:memory:",
|
||||
poolclass=StaticPool,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(db.Base.metadata.create_all)
|
||||
maker = async_sessionmaker(engine, expire_on_commit=False)
|
||||
try:
|
||||
await body(maker)
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
asyncio.run(go())
|
||||
|
||||
|
||||
def test_first_login_disabled_then_credentials_encrypted(monkeypatch):
|
||||
async def body(maker):
|
||||
async with maker() as s:
|
||||
user = await db.upsert_login(s, "sub1", "a@b.com", "Alice")
|
||||
assert user.enabled is False and user.has_credentials is False
|
||||
assert await db.set_credentials(s, "sub1", "i123", "SECRET") is True
|
||||
again = await db.get_user(s, "sub1")
|
||||
assert again.athlete_id == "i123"
|
||||
assert b"SECRET" not in again.api_key_enc
|
||||
assert crypto.decrypt(again.api_key_enc) == "SECRET"
|
||||
assert again.has_credentials is True
|
||||
|
||||
_run_db(monkeypatch, body)
|
||||
|
||||
|
||||
def test_enable_disable_delete_and_list(monkeypatch):
|
||||
async def body(maker):
|
||||
async with maker() as s:
|
||||
await db.upsert_login(s, "sub1", "a@b.com", None)
|
||||
await db.upsert_login(s, "sub2", "b@b.com", None)
|
||||
assert len(await db.list_users(s)) == 2
|
||||
assert await db.set_enabled(s, "sub1", True) is True
|
||||
assert (await db.get_user(s, "sub1")).enabled is True
|
||||
assert await db.clear_credentials(s, "sub1") is True
|
||||
assert await db.delete_user(s, "sub2") is True
|
||||
assert await db.get_user(s, "sub2") is None
|
||||
assert await db.delete_user(s, "ghost") is False
|
||||
|
||||
_run_db(monkeypatch, body)
|
||||
Reference in New Issue
Block a user