feat: Intervals.icu MCP portal (FastAPI + Authentik OIDC)
build-image / test (push) Successful in 37s
build-image / build (push) Successful in 25s

- 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:
2026-07-04 20:51:46 -04:00
commit bb5e9d518c
21 changed files with 2096 additions and 0 deletions
@@ -0,0 +1,47 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Intervals.icu MCP portal</title>
<style>
:root { color-scheme: light dark; }
body { font-family: system-ui, sans-serif; max-width: 760px; margin: 0 auto; padding: 1rem; line-height: 1.5; }
header { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
border-bottom: 1px solid #8884; padding-bottom: .5rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
nav a { margin-left: 1rem; }
a { color: #3b82f6; text-decoration: none; }
a:hover { text-decoration: underline; }
.banner { padding: .6rem .9rem; border-radius: 8px; margin: 1rem 0; }
.ok { background: #16a34a22; border: 1px solid #16a34a88; }
.warn { background: #d9770622; border: 1px solid #d9770688; }
.err { background: #dc262622; border: 1px solid #dc262688; }
label { display: block; margin-top: 1rem; font-weight: 600; }
input[type=text], input[type=password] { width: 100%; padding: .5rem; margin-top: .25rem;
border: 1px solid #8886; border-radius: 6px; box-sizing: border-box; }
button { margin-top: 1rem; padding: .5rem 1rem; border-radius: 6px; border: 1px solid #8886;
background: #3b82f6; color: #fff; cursor: pointer; }
button.secondary { background: transparent; color: inherit; }
table { border-collapse: collapse; width: 100%; margin-top: 1rem; font-size: .92rem; }
th, td { text-align: left; padding: .4rem .5rem; border-bottom: 1px solid #8883; }
code { background: #8882; padding: .1rem .35rem; border-radius: 4px; }
.muted { color: #8889; }
form.inline { display: inline; }
</style>
</head>
<body>
<header>
<strong>Intervals.icu&nbsp;MCP</strong>
{% if user %}
<nav>
<a href="/account">My account</a>
{% if user.is_admin %}<a href="/admin">Admin</a>{% endif %}
<a href="/logout">Sign out</a>
</nav>
{% endif %}
</header>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>