mirror of
https://github.com/computerim/impactflow-discovery.git
synced 2026-08-27 07:10:37 +00:00
Initial commit: ImpactFlow Discovery + Google OAuth auth layer
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>
This commit is contained in:
+77
@@ -0,0 +1,77 @@
|
||||
"""FastAPI entrypoint for the ImpactFlow self-discovery module."""
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from app import models # noqa: F401 - register ORM models with Base.metadata
|
||||
from app.auth import ensure_api_key_admin
|
||||
from app.database import AsyncSessionLocal, Base, engine
|
||||
from app.routers import activity, auth as auth_router, discovery
|
||||
from app.routers.activity import prune_old_activity
|
||||
from app.tracking import ActivityTrackingMiddleware
|
||||
|
||||
# Path to the static directory, resolved relative to this file so it works
|
||||
# regardless of the current working directory.
|
||||
STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Alembic owns schema migrations in Docker; this create_all is an
|
||||
# idempotent safety net so the app also runs cleanly in local/dev where
|
||||
# migrations may not have been applied.
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
await ensure_api_key_admin(db)
|
||||
await prune_old_activity(db)
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(title="ImpactFlow Self-Discovery", lifespan=lifespan)
|
||||
|
||||
# add_middleware wraps the previous app, so the LAST call becomes the
|
||||
# OUTERMOST middleware. Register inner-to-outer:
|
||||
# tracking (innermost — sees request.state.user set by deps) ->
|
||||
# session (needs to wrap routes for the OAuth state cookie) ->
|
||||
# CORS (outermost — preflights must short-circuit before anything else).
|
||||
app.add_middleware(ActivityTrackingMiddleware)
|
||||
|
||||
_session_secret = os.getenv("JWT_SECRET") or "dev-session-secret-change-me"
|
||||
app.add_middleware(SessionMiddleware, secret_key=_session_secret)
|
||||
|
||||
_cors_origins = [
|
||||
o.strip()
|
||||
for o in os.getenv(
|
||||
"CORS_ALLOWED_ORIGINS", "http://localhost:8000"
|
||||
).split(",")
|
||||
if o.strip()
|
||||
]
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=_cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(auth_router.router)
|
||||
app.include_router(activity.router)
|
||||
app.include_router(discovery.router)
|
||||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return RedirectResponse(url="/static/discovery.html")
|
||||
Reference in New Issue
Block a user