mirror of
https://github.com/computerim/impactflow-discovery.git
synced 2026-08-27 05:00:37 +00:00
b8f176bb31
Discovery service (pre-existing): FastAPI + async SQLAlchemy + Alembic +
SQLite + Anthropic, with a five-prompt static UI that produces an Enneagram
+ Ikigai profile.
Auth implementation (this change set) follows
Impact_Flow_Auth_Plan_OAuth.html, adapted to the discovery_conversation /
discovery_profile schema:
- app/auth.py: Google OAuth registration, JWT issue/decode, dual-auth
dependency (Bearer JWT or X-API-Key), refresh-token hashing, domain
allow-list, synthetic api-key-admin user
- app/tracking.py: ActivityTrackingMiddleware + log_activity helper;
tags machine-to-machine calls source=mcp
- app/routers/auth.py: /api/auth/{login,callback,refresh,logout},
/api/me, /api/me/{stats,sessions,sessions/{id}}
- app/routers/activity.py: /api/activity, /api/activity/summary,
/api/admin/activity, plus prune_old_activity (90-day retention)
- app/routers/discovery.py: every route now user-scoped via the auth
dependency; /discovery/profile/{user_id} -> /discovery/profile/me
- alembic/versions/002_add_auth.py: users, refresh_tokens, activity_log
- tests/test_auth.py: 8 tests covering 401 paths, X-API-Key admin
resolution, JWT round-trip, admin gating, domain allow-list
- README.md: Authentication section, expanded env-var table, updated
data-model and API-reference tables
- .env.example: new GOOGLE_*, JWT_*, IMPACTFLOW_API_KEY, CORS_*,
ALLOWED_EMAIL_DOMAINS placeholders
- .gitignore: also exclude data/*.log
Tests: 19/19 pass (11 pre-existing + 8 new). smoke_test.py exercises the
full discovery flow under X-API-Key plus 401 paths, OAuth login redirect,
activity logging, and /api/me/stats.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
466 lines
21 KiB
HTML
466 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Impact Flow Auth Plan — OAuth Edition</title>
|
|
<style>
|
|
:root { color-scheme: light; }
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #fff; color: #1a1a1a; padding: 24px; line-height: 1.6; }
|
|
h1 { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
|
|
.subtitle { color: #64748b; font-size: 13px; margin-bottom: 24px; }
|
|
.phase { margin-bottom: 28px; border: 1px solid #e2e8f0; border-radius: 10px; overflow: hidden; }
|
|
.phase-header { padding: 14px 18px; display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; }
|
|
.phase-header:hover { filter: brightness(0.97); }
|
|
.phase-num { font-size: 12px; font-weight: 700; color: #fff; background: #2563eb; border-radius: 50%; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
.phase.p2 .phase-num { background: #7c3aed; }
|
|
.phase.p3 .phase-num { background: #059669; }
|
|
.phase.p4 .phase-num { background: #d97706; }
|
|
.phase-title { font-size: 15px; font-weight: 600; }
|
|
.phase-est { margin-left: auto; font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 10px; border-radius: 12px; flex-shrink: 0; }
|
|
.phase-body { padding: 0 18px 18px; display: none; }
|
|
.phase.open .phase-body { display: block; }
|
|
.chevron { margin-left: 4px; transition: transform 0.2s; font-size: 12px; color: #94a3b8; }
|
|
.phase.open .chevron { transform: rotate(90deg); }
|
|
.step { margin-bottom: 16px; }
|
|
.step-title { font-size: 13px; font-weight: 600; margin-bottom: 4px; color: #334155; }
|
|
.step p, .step li { font-size: 13px; color: #475569; }
|
|
.step ul { padding-left: 18px; margin-top: 4px; }
|
|
.step li { margin-bottom: 3px; }
|
|
.file-tag { display: inline-block; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; background: #f1f5f9; color: #475569; padding: 1px 6px; border-radius: 4px; margin: 1px 2px; }
|
|
.schema { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 12px 14px; margin: 8px 0; font-family: 'SF Mono', monospace; font-size: 11px; color: #334155; white-space: pre-wrap; line-height: 1.5; }
|
|
.warn { background: #fef3c7; border-left: 3px solid #f59e0b; padding: 8px 12px; border-radius: 0 6px 6px 0; margin: 8px 0; font-size: 12px; color: #92400e; }
|
|
.good { background: #dcfce7; border-left: 3px solid #22c55e; padding: 8px 12px; border-radius: 0 6px 6px 0; margin: 8px 0; font-size: 12px; color: #166534; }
|
|
.overview { background: #f0f4ff; border-radius: 8px; padding: 16px; margin-bottom: 24px; }
|
|
.overview h2 { font-size: 14px; font-weight: 600; margin-bottom: 8px; color: #1e40af; }
|
|
.overview p { font-size: 13px; color: #334155; }
|
|
.stack-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
|
|
.stack-chip { font-size: 11px; background: #dbeafe; color: #1e40af; padding: 2px 10px; border-radius: 12px; font-weight: 500; }
|
|
.removed { text-decoration: line-through; color: #94a3b8; }
|
|
.vs-table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 12px; }
|
|
.vs-table th { background: #f1f5f9; padding: 6px 10px; text-align: left; font-weight: 600; color: #334155; border-bottom: 2px solid #e2e8f0; }
|
|
.vs-table td { padding: 6px 10px; border-bottom: 1px solid #f1f5f9; color: #475569; }
|
|
.vs-table .yes { color: #16a34a; font-weight: 600; }
|
|
.vs-table .no { color: #dc2626; font-weight: 600; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<h1>Impact Flow: Auth & User Tracking Plan</h1>
|
|
<p class="subtitle">OAuth Edition — Google Sign-In + API Key for MCP</p>
|
|
|
|
<div class="overview">
|
|
<h2>Why OAuth over Password Auth</h2>
|
|
<table class="vs-table">
|
|
<tr><th>Concern</th><th>Password (JWT-only)</th><th>OAuth (Google)</th></tr>
|
|
<tr><td>Password storage</td><td class="no">You manage bcrypt hashes</td><td class="yes">Google handles it</td></tr>
|
|
<tr><td>Password reset flow</td><td class="no">You build it</td><td class="yes">Not needed</td></tr>
|
|
<tr><td>Email verification</td><td class="no">You build it</td><td class="yes">Google verifies</td></tr>
|
|
<tr><td>Brute-force protection</td><td class="no">You implement rate limiting</td><td class="yes">Google handles it</td></tr>
|
|
<tr><td>Account recovery</td><td class="no">You build it</td><td class="yes">Google handles it</td></tr>
|
|
<tr><td>Initial setup</td><td class="yes">No external deps</td><td>Register Google Cloud app (~15 min)</td></tr>
|
|
<tr><td>Ongoing maintenance</td><td class="no">High</td><td class="yes">Near zero</td></tr>
|
|
</table>
|
|
<div class="stack-row">
|
|
<span class="stack-chip">Google OAuth 2.0</span>
|
|
<span class="stack-chip">authlib</span>
|
|
<span class="stack-chip">FastAPI</span>
|
|
<span class="stack-chip">SQLite</span>
|
|
<span class="stack-chip">API Key (MCP)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PHASE 1 -->
|
|
<div class="phase p1 open" onclick="this.classList.toggle('open')">
|
|
<div class="phase-header">
|
|
<span class="phase-num">1</span>
|
|
<span class="phase-title">Google OAuth + Simplified User Model</span>
|
|
<span class="phase-est">~1 day</span>
|
|
<span class="chevron">▶</span>
|
|
</div>
|
|
<div class="phase-body">
|
|
|
|
<div class="step">
|
|
<div class="step-title">1.1 Google Cloud Console setup</div>
|
|
<ul>
|
|
<li>Go to <strong>console.cloud.google.com</strong> → APIs & Services → Credentials</li>
|
|
<li>Create an OAuth 2.0 Client ID (Web application)</li>
|
|
<li>Authorized redirect URI: <code>http://100.103.206.4:8000/api/auth/callback</code></li>
|
|
<li>Also add <code>http://localhost:8000/api/auth/callback</code> for local dev</li>
|
|
<li>Save the <strong>Client ID</strong> and <strong>Client Secret</strong></li>
|
|
</ul>
|
|
<div class="warn">Store these in .env, never commit them. Add .env to .gitignore if not already there.</div>
|
|
<div class="schema">GOOGLE_CLIENT_ID=xxxx.apps.googleusercontent.com
|
|
GOOGLE_CLIENT_SECRET=GOCSPX-xxxx
|
|
JWT_SECRET=generate-a-random-64-char-string
|
|
IMPACTFLOW_API_KEY=generate-another-random-64-char-string</div>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">1.2 Simplified users table (no passwords!)</div>
|
|
<div class="schema">CREATE TABLE users (
|
|
id TEXT PRIMARY KEY, -- UUID
|
|
email TEXT UNIQUE NOT NULL,
|
|
display_name TEXT NOT NULL,
|
|
avatar_url TEXT, -- from Google profile
|
|
google_id TEXT UNIQUE NOT NULL, -- Google sub claim
|
|
role TEXT DEFAULT 'user', -- user | admin
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
last_login_at TEXT
|
|
);</div>
|
|
<div class="good">No password_hash, no password reset, no email verification. Google handles all of that.</div>
|
|
<p>Files: <span class="file-tag">app/db.py</span> <span class="file-tag">app/models.py</span></p>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">1.3 Install dependencies</div>
|
|
<div class="schema">pip install authlib httpx python-jose[cryptography]</div>
|
|
<p><strong>authlib</strong> handles the OAuth dance. <strong>python-jose</strong> for JWT signing. No bcrypt/passlib needed.</p>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">1.4 OAuth flow — 3 endpoints</div>
|
|
<div class="schema">from authlib.integrations.starlette_client import OAuth
|
|
|
|
oauth = OAuth()
|
|
oauth.register(
|
|
name="google",
|
|
client_id=os.getenv("GOOGLE_CLIENT_ID"),
|
|
client_secret=os.getenv("GOOGLE_CLIENT_SECRET"),
|
|
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
|
client_kwargs={"scope": "openid email profile"},
|
|
)</div>
|
|
|
|
<p><strong>GET /api/auth/login</strong> — Redirects browser to Google consent screen</p>
|
|
<div class="schema">@app.get("/api/auth/login")
|
|
async def login(request: Request):
|
|
redirect_uri = request.url_for("auth_callback")
|
|
return await oauth.google.authorize_redirect(request, redirect_uri)</div>
|
|
|
|
<p><strong>GET /api/auth/callback</strong> — Google redirects back here with auth code</p>
|
|
<div class="schema">@app.get("/api/auth/callback")
|
|
async def auth_callback(request: Request):
|
|
token = await oauth.google.authorize_access_token(request)
|
|
user_info = token.get("userinfo")
|
|
|
|
# Find or create user
|
|
user = db.get_user_by_google_id(user_info["sub"])
|
|
if not user:
|
|
user = db.create_user(
|
|
email=user_info["email"],
|
|
display_name=user_info["name"],
|
|
avatar_url=user_info.get("picture"),
|
|
google_id=user_info["sub"],
|
|
)
|
|
|
|
# Update last login
|
|
db.update_last_login(user.id)
|
|
|
|
# Issue our own JWT
|
|
access_token = create_jwt({"sub": user.id, "email": user.email})
|
|
return {"access_token": access_token, "token_type": "bearer", "user": user}</div>
|
|
|
|
<p><strong>GET /api/auth/me</strong> — Returns current user from JWT</p>
|
|
<div class="schema">@app.get("/api/auth/me")
|
|
async def get_me(user = Depends(get_current_user)):
|
|
return user</div>
|
|
|
|
<p>Files: <span class="file-tag">app/auth.py</span> (new) <span class="file-tag">app/main.py</span></p>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">1.5 Dual auth: JWT + API Key</div>
|
|
<p>The auth dependency accepts either a valid JWT <strong>or</strong> the <code>X-API-Key</code> header. This keeps the MCP server working without OAuth.</p>
|
|
<div class="schema">async def get_current_user(
|
|
request: Request,
|
|
credentials: Optional[HTTPAuthorizationCredentials] = Depends(
|
|
HTTPBearer(auto_error=False)
|
|
),
|
|
db = Depends(get_db),
|
|
) -> User:
|
|
# Check API key first (for MCP server)
|
|
api_key = request.headers.get("x-api-key")
|
|
if api_key and api_key == os.getenv("IMPACTFLOW_API_KEY"):
|
|
return db.get_admin_user() # API key acts as admin
|
|
|
|
# Then check JWT (for browser users)
|
|
if credentials:
|
|
payload = decode_jwt(credentials.credentials)
|
|
user = db.get_user(payload["sub"])
|
|
if user:
|
|
return user
|
|
|
|
raise HTTPException(status_code=401, detail="Not authenticated")</div>
|
|
<div class="good">MCP server just adds <code>X-API-Key: {key}</code> header to every request. No OAuth dance needed for machine-to-machine.</div>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">1.6 Session middleware (for OAuth state)</div>
|
|
<p>authlib needs Starlette sessions to store the OAuth state parameter during the redirect flow. Add:</p>
|
|
<div class="schema">from starlette.middleware.sessions import SessionMiddleware
|
|
app.add_middleware(SessionMiddleware, secret_key=os.getenv("JWT_SECRET"))</div>
|
|
<div class="warn">This is only for the OAuth redirect flow. Your API auth still uses stateless JWTs, not session cookies.</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PHASE 2 -->
|
|
<div class="phase p2" onclick="this.classList.toggle('open')">
|
|
<div class="phase-header">
|
|
<span class="phase-num">2</span>
|
|
<span class="phase-title">Protect Endpoints & Multi-User Data</span>
|
|
<span class="phase-est">~0.5 day</span>
|
|
<span class="chevron">▶</span>
|
|
</div>
|
|
<div class="phase-body">
|
|
|
|
<div class="step">
|
|
<div class="step-title">2.1 Add user_id to all data tables</div>
|
|
<div class="schema">ALTER TABLE projects ADD COLUMN user_id TEXT REFERENCES users(id);
|
|
ALTER TABLE visions ADD COLUMN user_id TEXT REFERENCES users(id);
|
|
ALTER TABLE journal_entries ADD COLUMN user_id TEXT REFERENCES users(id);
|
|
|
|
CREATE INDEX idx_projects_user ON projects(user_id);
|
|
CREATE INDEX idx_visions_user ON visions(user_id);
|
|
CREATE INDEX idx_journal_user ON journal_entries(user_id);</div>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">2.2 Migration script</div>
|
|
<p>One-time migration that preserves all existing data:</p>
|
|
<ul>
|
|
<li>Create users table</li>
|
|
<li>Create your admin user (first Google login auto-promotes to admin)</li>
|
|
<li>Add user_id columns</li>
|
|
<li>Backfill existing rows with admin user_id</li>
|
|
<li>Add indexes</li>
|
|
</ul>
|
|
<div class="schema">-- Auto-promote first user to admin
|
|
-- In auth_callback, if no users exist yet:
|
|
if db.count_users() == 0:
|
|
user.role = "admin"</div>
|
|
<p>Files: <span class="file-tag">app/migrations/001_add_auth.py</span> (new)</p>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">2.3 Scope all queries</div>
|
|
<p>Every CRUD operation filters by the authenticated user:</p>
|
|
<div class="schema"># Before (no auth)
|
|
def get_projects(db):
|
|
return db.execute("SELECT * FROM projects")
|
|
|
|
# After (user-scoped)
|
|
def get_projects(db, user_id: str):
|
|
return db.execute(
|
|
"SELECT * FROM projects WHERE user_id = ?", [user_id]
|
|
)</div>
|
|
<ul>
|
|
<li>GET list endpoints: filter by user_id</li>
|
|
<li>GET detail endpoints: filter by user_id (prevents accessing other users' data)</li>
|
|
<li>POST: auto-set user_id from authenticated user</li>
|
|
<li>PUT/PATCH/DELETE: verify ownership before modifying</li>
|
|
</ul>
|
|
<p>Files: <span class="file-tag">app/db.py</span> <span class="file-tag">app/main.py</span></p>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">2.4 Route classification</div>
|
|
<table class="vs-table">
|
|
<tr><th>Route</th><th>Auth</th><th>Notes</th></tr>
|
|
<tr><td><code>/health</code></td><td>Public</td><td>Health check</td></tr>
|
|
<tr><td><code>/api/auth/login</code></td><td>Public</td><td>Initiates OAuth</td></tr>
|
|
<tr><td><code>/api/auth/callback</code></td><td>Public</td><td>OAuth callback</td></tr>
|
|
<tr><td><code>/api/auth/me</code></td><td>Protected</td><td>Current user profile</td></tr>
|
|
<tr><td><code>/api/projects/*</code></td><td>Protected</td><td>User-scoped</td></tr>
|
|
<tr><td><code>/api/visions/*</code></td><td>Protected</td><td>User-scoped</td></tr>
|
|
<tr><td><code>/api/journal/*</code></td><td>Protected</td><td>User-scoped</td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PHASE 3 -->
|
|
<div class="phase p3" onclick="this.classList.toggle('open')">
|
|
<div class="phase-header">
|
|
<span class="phase-num">3</span>
|
|
<span class="phase-title">Activity Tracking</span>
|
|
<span class="phase-est">~1 day</span>
|
|
<span class="chevron">▶</span>
|
|
</div>
|
|
<div class="phase-body">
|
|
|
|
<div class="step">
|
|
<div class="step-title">3.1 Activity log table</div>
|
|
<div class="schema">CREATE TABLE activity_log (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id),
|
|
action TEXT NOT NULL, -- create|update|delete|view|login|export
|
|
resource TEXT NOT NULL, -- project|vision|journal|auth
|
|
resource_id TEXT, -- FK to the affected record
|
|
metadata TEXT, -- JSON for extra context
|
|
source TEXT DEFAULT 'web', -- web|mcp|api
|
|
ip_address TEXT,
|
|
user_agent TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE INDEX idx_activity_user ON activity_log(user_id, created_at DESC);
|
|
CREATE INDEX idx_activity_resource ON activity_log(resource, resource_id);</div>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">3.2 Tracking middleware</div>
|
|
<div class="schema">@app.middleware("http")
|
|
async def track_activity(request: Request, call_next):
|
|
response = await call_next(request)
|
|
user = getattr(request.state, "user", None)
|
|
if user and response.status_code < 400:
|
|
# Determine source
|
|
source = "mcp" if request.headers.get("x-api-key") else "web"
|
|
log_activity(
|
|
user_id=user.id,
|
|
action=method_to_action(request.method),
|
|
resource=path_to_resource(request.url.path),
|
|
source=source,
|
|
ip=request.client.host,
|
|
ua=request.headers.get("user-agent", ""),
|
|
)
|
|
return response
|
|
|
|
def method_to_action(method: str) -> str:
|
|
return {"GET": "view", "POST": "create", "PUT": "update",
|
|
"PATCH": "update", "DELETE": "delete"}.get(method, "other")</div>
|
|
<p>Files: <span class="file-tag">app/tracking.py</span> (new)</p>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">3.3 What to track</div>
|
|
<ul>
|
|
<li><strong>Auth events:</strong> login (via OAuth callback), failed attempts</li>
|
|
<li><strong>CRUD ops:</strong> create/update/delete on projects, visions, journal</li>
|
|
<li><strong>Detail views:</strong> GET on individual records (not list endpoints)</li>
|
|
<li><strong>MCP calls:</strong> tagged with <code>source="mcp"</code> to distinguish Claude-initiated actions</li>
|
|
<li><strong>Exports:</strong> any data export or report generation</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">3.4 Query endpoints</div>
|
|
<div class="schema">GET /api/activity?page=1&limit=20
|
|
-- Paginated activity feed for current user
|
|
|
|
GET /api/activity/summary?days=30
|
|
-- Returns: { actions_per_day, top_resources, mcp_vs_web_ratio }
|
|
|
|
GET /api/admin/activity?user_id=... (admin only)
|
|
-- All users' activity</div>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">3.5 Retention</div>
|
|
<p>Auto-prune logs older than 90 days. Run on app startup:</p>
|
|
<div class="schema">@app.on_event("startup")
|
|
async def prune_old_activity():
|
|
db.execute("""
|
|
DELETE FROM activity_log
|
|
WHERE created_at < datetime('now', '-90 days')
|
|
""")</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PHASE 4 -->
|
|
<div class="phase p4" onclick="this.classList.toggle('open')">
|
|
<div class="phase-header">
|
|
<span class="phase-num">4</span>
|
|
<span class="phase-title">Discovery & Polish</span>
|
|
<span class="phase-est">~1 day</span>
|
|
<span class="chevron">▶</span>
|
|
</div>
|
|
<div class="phase-body">
|
|
|
|
<div class="step">
|
|
<div class="step-title">4.1 User profile endpoints</div>
|
|
<ul>
|
|
<li><code>GET /api/me</code> — full profile + stats</li>
|
|
<li><code>PATCH /api/me</code> — update display_name (email managed by Google)</li>
|
|
<li><code>GET /api/me/stats</code> — personal usage: projects created, journal streaks, active days</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">4.2 Token management</div>
|
|
<div class="schema">CREATE TABLE refresh_tokens (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id),
|
|
token_hash TEXT NOT NULL,
|
|
device TEXT, -- from user-agent
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
expires_at TEXT NOT NULL
|
|
);</div>
|
|
<ul>
|
|
<li><code>GET /api/me/sessions</code> — list active refresh tokens (device, created)</li>
|
|
<li><code>DELETE /api/me/sessions/{id}</code> — revoke a token</li>
|
|
<li>Issue refresh token on OAuth callback, access token from refresh</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">4.3 Update MCP server</div>
|
|
<ul>
|
|
<li>Add <code>X-API-Key</code> header to every request in MCP server config</li>
|
|
<li>New MCP tool: <code>get_activity_summary</code> — "What have I been working on?"</li>
|
|
<li>New MCP tool: <code>get_recent_changes</code> — Feed of what changed recently</li>
|
|
<li>New MCP tool: <code>get_user_profile</code> — Current user info and stats</li>
|
|
</ul>
|
|
<p>Files: <span class="file-tag">mcp_server/tools.py</span></p>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">4.4 Security hardening</div>
|
|
<ul>
|
|
<li>CORS: restrict origins to Tailscale IP + localhost</li>
|
|
<li>Rate limit OAuth callback (prevent abuse)</li>
|
|
<li>Allowed email domains: optionally restrict to specific domains</li>
|
|
<li>JWT expiry: 15 min access, 7 day refresh</li>
|
|
<li>All secrets in .env, .env in .gitignore</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">4.5 What you no longer need to build</div>
|
|
<div class="good">
|
|
<strong>Removed from scope (Google handles these):</strong><br>
|
|
<span class="removed">POST /api/auth/register</span> —
|
|
<span class="removed">Password hashing (bcrypt)</span> —
|
|
<span class="removed">Password reset flow</span> —
|
|
<span class="removed">Email verification</span> —
|
|
<span class="removed">Brute-force protection on login</span> —
|
|
<span class="removed">Account recovery</span> —
|
|
<span class="removed">passlib dependency</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-title">4.6 Future considerations</div>
|
|
<ul>
|
|
<li><strong>Additional OAuth providers:</strong> GitHub, Microsoft — authlib makes adding providers trivial (same pattern, new registration)</li>
|
|
<li><strong>Invite system:</strong> Admin generates invite links, new users must have invite to create account</li>
|
|
<li><strong>Team/org model:</strong> Shared projects between users (if Impact Flow grows beyond personal use)</li>
|
|
<li><strong>Allowed domains:</strong> Restrict sign-up to @computerim.com emails</li>
|
|
</ul>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// First phase open by default
|
|
</script>
|
|
</body>
|
|
</html>
|