feat: implement Phase 0 auth system, setup wizard, and Docker config
- Add users table to migration, User ORM model - Implement JWT+TOTP auth API (login, refresh, me, change-password) - Add first-run setup wizard with rate-limited admin creation, TOTP QR enrollment (secret saved only after verification), and NAS path verification — served as Jinja2 single-page HTML - Add setup redirect middleware (bypasses /health, /docs, /openapi.json) - Mount config.yaml, scripts, logs volumes in docker-compose - Route API vs frontend traffic in Caddyfile - Include admin seed script as CLI fallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
81
app/main.py
81
app/main.py
@@ -2,21 +2,26 @@
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy import func, select, text
|
||||
|
||||
from api.auth import router as auth_router
|
||||
from api.setup import router as setup_router
|
||||
from core.config import settings
|
||||
from core.database import init_db
|
||||
from core.database import async_session, engine, init_db
|
||||
from models.user import User
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""앱 시작/종료 시 실행되는 lifespan 핸들러"""
|
||||
# 시작: DB 연결, 스케줄러 등록
|
||||
# 시작: DB 연결 확인
|
||||
await init_db()
|
||||
# TODO: APScheduler 시작 (Phase 3)
|
||||
yield
|
||||
# 종료: 리소스 정리
|
||||
# TODO: 스케줄러 종료, DB 연결 해제
|
||||
# 종료: DB 엔진 정리
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
@@ -26,16 +31,68 @@ app = FastAPI(
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# ─── 라우터 등록 ───
|
||||
app.include_router(setup_router, prefix="/api/setup", tags=["setup"])
|
||||
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "ok", "version": "2.0.0"}
|
||||
|
||||
|
||||
# TODO: 라우터 등록 (Phase 0~2)
|
||||
# from api import documents, search, tasks, dashboard, export
|
||||
# TODO: Phase 2에서 추가
|
||||
# app.include_router(documents.router, prefix="/api/documents", tags=["documents"])
|
||||
# app.include_router(search.router, prefix="/api/search", tags=["search"])
|
||||
# app.include_router(tasks.router, prefix="/api/tasks", tags=["tasks"])
|
||||
# app.include_router(dashboard.router, prefix="/api/dashboard", tags=["dashboard"])
|
||||
# app.include_router(export.router, prefix="/api/export", tags=["export"])
|
||||
|
||||
|
||||
# ─── 셋업 미들웨어: 유저 0명이면 /setup으로 리다이렉트 ───
|
||||
SETUP_BYPASS_PREFIXES = (
|
||||
"/api/setup", "/setup", "/health", "/docs", "/openapi.json", "/redoc",
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def setup_redirect_middleware(request: Request, call_next):
|
||||
path = request.url.path
|
||||
# 바이패스 경로는 항상 통과
|
||||
if any(path.startswith(p) for p in SETUP_BYPASS_PREFIXES):
|
||||
return await call_next(request)
|
||||
|
||||
# 유저 존재 여부 확인
|
||||
try:
|
||||
async with async_session() as session:
|
||||
result = await session.execute(select(func.count(User.id)))
|
||||
user_count = result.scalar()
|
||||
if user_count == 0:
|
||||
return RedirectResponse(url="/setup")
|
||||
except Exception:
|
||||
pass # DB 연결 실패 시 통과 (health에서 확인 가능)
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
# ─── 셋업 페이지 라우트 (API가 아닌 HTML 페이지) ───
|
||||
@app.get("/setup")
|
||||
async def setup_page_redirect(request: Request):
|
||||
"""셋업 위자드 페이지로 포워딩"""
|
||||
from api.setup import setup_page
|
||||
from core.database import get_session
|
||||
|
||||
async for session in get_session():
|
||||
return await setup_page(request, session)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""헬스체크 — DB 연결 상태 포함"""
|
||||
db_ok = False
|
||||
try:
|
||||
async with engine.connect() as conn:
|
||||
await conn.execute(text("SELECT 1"))
|
||||
db_ok = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"status": "ok" if db_ok else "degraded",
|
||||
"version": "2.0.0",
|
||||
"database": "connected" if db_ok else "disconnected",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user