- OAuth2 Gitea (login/callback/logout) with fallback admin mode
- Session management with signed cookies (itsdangerous, 7 days)
- CSRF protection middleware on all POST/PUT/PATCH/DELETE
- Rate limiting in-memory (60 req/min per IP)
- Issue creation from FlowDeck (POST /api/issues/{owner}/{repo})
- Inline issue editing (PATCH /api/issues/{owner}/{repo}/{issue_id})
- Card detail modal (double-click): description, labels, comments
- Checklists with toggleable items (CRUD endpoints + UI)
- Priority + due date editable on cards
- Webhook receiver (POST /api/webhook) with HMAC-SHA256
- Auto-register/status webhooks for repos
- GiteaClient: create_issue, update_issue, get_comments, webhooks, collaborators
- New DB tables: users, user_tokens, checklists, checklist_items
- CSS: modal, forms, checklists, comments, PR section
111 lines
3.9 KiB
Python
111 lines
3.9 KiB
Python
"""FlowDeck — Auth routes: login, callback, logout."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import secrets
|
|
|
|
from fastapi import APIRouter, Request, Query
|
|
from fastapi.responses import RedirectResponse, HTMLResponse
|
|
|
|
from app.auth.session import SessionManager
|
|
from app.auth.oauth import gitea_oauth
|
|
from app.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["auth"], prefix="/auth")
|
|
|
|
|
|
@router.get("/login")
|
|
async def login(request: Request):
|
|
"""Redirect to Gitea OAuth2 authorize page."""
|
|
if not gitea_oauth.enabled:
|
|
# Fallback: use global token, create a fake session
|
|
from app.db import get_conn
|
|
with get_conn() as conn:
|
|
user = conn.execute("SELECT * FROM users WHERE login='admin'").fetchone()
|
|
if not user:
|
|
conn.execute("INSERT INTO users (login, full_name, email, avatar_url) VALUES ('admin', 'Admin', '', '')")
|
|
conn.commit()
|
|
user = conn.execute("SELECT * FROM users WHERE login='admin'").fetchone()
|
|
user_data = dict(user)
|
|
session = SessionManager.create_session(user_data)
|
|
response = RedirectResponse(url="/", status_code=302)
|
|
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax")
|
|
return response
|
|
|
|
state = secrets.token_hex(32)
|
|
request.session["oauth_state"] = state
|
|
auth_url = gitea_oauth.get_authorize_url(state)
|
|
return RedirectResponse(url=auth_url, status_code=302)
|
|
|
|
|
|
@router.get("/callback")
|
|
async def callback(
|
|
request: Request,
|
|
code: str = Query(...),
|
|
state: str = Query(...),
|
|
):
|
|
"""Handle OAuth2 callback from Gitea."""
|
|
# Validate state
|
|
expected_state = request.session.get("oauth_state", "")
|
|
if not expected_state or state != expected_state:
|
|
return HTMLResponse("<h1>Invalid state</h1>", status_code=400)
|
|
|
|
# Exchange code for token
|
|
token_data = await gitea_oauth.exchange_code(code)
|
|
if not token_data:
|
|
return HTMLResponse("<h1>Token exchange failed</h1>", status_code=400)
|
|
|
|
access_token = token_data.get("access_token")
|
|
if not access_token:
|
|
return HTMLResponse("<h1>No access token</h1>", status_code=400)
|
|
|
|
# Get user info
|
|
user = await gitea_oauth.get_user(access_token)
|
|
if not user:
|
|
return HTMLResponse("<h1>Failed to get user</h1>", status_code=400)
|
|
|
|
# Store user in DB
|
|
from app.db import get_conn
|
|
with get_conn() as conn:
|
|
conn.execute(
|
|
"""INSERT INTO users (login, full_name, email, avatar_url)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT(login)
|
|
DO UPDATE SET full_name=excluded.full_name, email=excluded.email, avatar_url=excluded.avatar_url""",
|
|
(user["login"], user.get("full_name", ""), user.get("email", ""), user.get("avatar_url", "")),
|
|
)
|
|
conn.commit()
|
|
|
|
# Store token
|
|
SessionManager.store_token(user["id"], access_token)
|
|
|
|
# Also store user in local DB
|
|
with get_conn() as conn:
|
|
db_user = conn.execute("SELECT * FROM users WHERE login=?", (user["login"],)).fetchone()
|
|
user_data = dict(db_user) if db_user else user
|
|
|
|
# Create session
|
|
session = SessionManager.create_session(user_data)
|
|
response = RedirectResponse(url="/", status_code=302)
|
|
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax")
|
|
return response
|
|
|
|
|
|
@router.get("/logout")
|
|
async def logout():
|
|
"""Clear session and redirect to dashboard."""
|
|
response = RedirectResponse(url="/", status_code=302)
|
|
response.delete_cookie("flowdeck_session")
|
|
return response
|
|
|
|
|
|
@router.get("/user")
|
|
async def current_user(request: Request):
|
|
"""Return current user info as JSON."""
|
|
from app.auth.session import get_current_user as gcu
|
|
user = await gcu(request)
|
|
if not user:
|
|
return {"authenticated": False}
|
|
return {"authenticated": True, "user": user}
|