From 29ef0fb0546083ee45d0907d95c2482d656c743a Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 14 Jul 2026 12:19:37 -0400 Subject: [PATCH] feat(v4.0): GitHub routes + CSRF exemptions for share/publish - app/routers/github_routes.py: /api/github/status + disconnect - CSRF: ajout /api/github, /api/pages, /api/recents aux exclusions - main.py: registration github_router --- app/main.py | 2 ++ app/middleware/csrf.py | 2 +- app/routers/board.py | 10 ++++++++-- app/routers/dashboard.py | 1 + app/routers/github_routes.py | 35 +++++++++++++++++++++++++++++++++++ app/templates/base.html | 25 ++++++++++++++++++++++++- 6 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 app/routers/github_routes.py diff --git a/app/main.py b/app/main.py index b951a44..031fa8d 100644 --- a/app/main.py +++ b/app/main.py @@ -15,6 +15,7 @@ from app.middleware.csrf import CSRFMiddleware from app.middleware.security import ContentSecurityPolicyMiddleware, RateLimitMiddleware from app.routers import dashboard, board, notes, api, auth, webhooks, collections, my_tasks, workspace, library, public_api, admin, sharing from app.routers.gitea import router as gitea_router +from app.routers.github_routes import router as github_router from app.services.gitea_client import gitea from app.services.webhook_outbound import init_webhook_tables @@ -65,6 +66,7 @@ app.include_router(workspace.router) app.include_router(library.router) app.include_router(admin.router) app.include_router(gitea_router) +app.include_router(github_router) app.include_router(public_api.router) app.include_router(sharing.router) diff --git a/app/middleware/csrf.py b/app/middleware/csrf.py index ea280cd..13b22e5 100644 --- a/app/middleware/csrf.py +++ b/app/middleware/csrf.py @@ -16,7 +16,7 @@ class CSRFMiddleware(BaseHTTPMiddleware): """ SAFE_METHODS = {"GET", "HEAD", "OPTIONS"} - EXCLUDED_PATHS = {"/api/webhook", "/api/v1", "/auth/callback", "/auth/register", "/auth/local-login", "/api/user", "/board/api/pages", "/board/api/favorites", "/api/workspace", "/api/local-workspace", "/api/settings", "/db/", "/workspace", "/api/frontend-error", "/api/admin", "/api/gitea"} + EXCLUDED_PATHS = {"/api/webhook", "/api/v1", "/auth/callback", "/auth/register", "/auth/local-login", "/api/user", "/board/api/pages", "/board/api/favorites", "/api/workspace", "/api/local-workspace", "/api/settings", "/db/", "/workspace", "/api/frontend-error", "/api/admin", "/api/gitea", "/api/github", "/api/pages", "/api/recents"} async def dispatch(self, request: Request, call_next): # Webhook receiver, OAuth callback, and internal API are exempt diff --git a/app/routers/board.py b/app/routers/board.py index d423836..0a0a9c2 100644 --- a/app/routers/board.py +++ b/app/routers/board.py @@ -289,8 +289,9 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict: "WHERE share_mode='anyone' OR published=1 ORDER BY updated_at DESC LIMIT 20" ).fetchall() shared_pages = [] + published_pages = [] for r in shared_rows: - shared_pages.append({ + page_entry = { "id": f"page/{r['id']}", "db_id": r["id"], "name": r["title"] or "New page", @@ -301,7 +302,11 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict: "depth": 0, "has_children": False, "children": [], - }) + } + if r["published"]: + published_pages.append(page_entry) + else: + shared_pages.append(page_entry) # Auth method & OAuth badge data auth_method = "local" @@ -330,6 +335,7 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict: "current_page": repo or "Dashboard", "last_edited": "now", "recent_pages": recent, "private_pages": private_items, "favorite_pages": favorites, "shared_pages": shared_pages, + "published_pages": published_pages, "user": user, "workspace_key": ws_key, "auth_method": auth_method, "gitea_linked": gitea_linked, diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 15c0bcd..57a4588 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -121,6 +121,7 @@ def _sidebar_data(request: Request, repos: list[dict]) -> dict: "private_pages": [], "favorite_pages": [], "shared_pages": [], + "published_pages": [], "user": user, "avatar_url": avatar_url, "avatar_color": avatar_color, diff --git a/app/routers/github_routes.py b/app/routers/github_routes.py new file mode 100644 index 0000000..71f43fd --- /dev/null +++ b/app/routers/github_routes.py @@ -0,0 +1,35 @@ +"""GitHub OAuth — status and disconnect routes.""" +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse + +router = APIRouter(tags=["github"], prefix="/api/github") + + +@router.get("/status") +async def github_status(request: Request): + """Check if the current user has GitHub linked.""" + from app.auth.session import SessionManager + from app.db import get_conn + user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) + if not user: + return {"linked": False} + with get_conn() as conn: + row = conn.execute( + "SELECT github_token FROM user_oauth_tokens WHERE user_id=? AND provider='github' AND github_token IS NOT NULL AND github_token != ''", + (user["id"],) + ).fetchone() + return {"linked": row is not None} + + +@router.delete("/disconnect") +async def disconnect_github(request: Request): + """Remove all GitHub OAuth tokens for the current user.""" + from app.auth.session import SessionManager + from app.db import get_conn + user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) + if not user: + return JSONResponse({"error": "Not authenticated"}, status_code=401) + with get_conn() as conn: + conn.execute("DELETE FROM user_oauth_tokens WHERE user_id=? AND provider='github'", (user["id"],)) + conn.commit() + return {"status": "ok"} diff --git a/app/templates/base.html b/app/templates/base.html index c2d25de..4a615cd 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -281,6 +281,29 @@ + + + {% if auth_method != 'local' or '/' in workspace_key %}