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
This commit is contained in:
@@ -15,6 +15,7 @@ from app.middleware.csrf import CSRFMiddleware
|
|||||||
from app.middleware.security import ContentSecurityPolicyMiddleware, RateLimitMiddleware
|
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 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.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.gitea_client import gitea
|
||||||
from app.services.webhook_outbound import init_webhook_tables
|
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(library.router)
|
||||||
app.include_router(admin.router)
|
app.include_router(admin.router)
|
||||||
app.include_router(gitea_router)
|
app.include_router(gitea_router)
|
||||||
|
app.include_router(github_router)
|
||||||
app.include_router(public_api.router)
|
app.include_router(public_api.router)
|
||||||
app.include_router(sharing.router)
|
app.include_router(sharing.router)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class CSRFMiddleware(BaseHTTPMiddleware):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
SAFE_METHODS = {"GET", "HEAD", "OPTIONS"}
|
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):
|
async def dispatch(self, request: Request, call_next):
|
||||||
# Webhook receiver, OAuth callback, and internal API are exempt
|
# Webhook receiver, OAuth callback, and internal API are exempt
|
||||||
|
|||||||
@@ -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"
|
"WHERE share_mode='anyone' OR published=1 ORDER BY updated_at DESC LIMIT 20"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
shared_pages = []
|
shared_pages = []
|
||||||
|
published_pages = []
|
||||||
for r in shared_rows:
|
for r in shared_rows:
|
||||||
shared_pages.append({
|
page_entry = {
|
||||||
"id": f"page/{r['id']}",
|
"id": f"page/{r['id']}",
|
||||||
"db_id": r["id"],
|
"db_id": r["id"],
|
||||||
"name": r["title"] or "New page",
|
"name": r["title"] or "New page",
|
||||||
@@ -301,7 +302,11 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
|
|||||||
"depth": 0,
|
"depth": 0,
|
||||||
"has_children": False,
|
"has_children": False,
|
||||||
"children": [],
|
"children": [],
|
||||||
})
|
}
|
||||||
|
if r["published"]:
|
||||||
|
published_pages.append(page_entry)
|
||||||
|
else:
|
||||||
|
shared_pages.append(page_entry)
|
||||||
|
|
||||||
# Auth method & OAuth badge data
|
# Auth method & OAuth badge data
|
||||||
auth_method = "local"
|
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",
|
"current_page": repo or "Dashboard", "last_edited": "now",
|
||||||
"recent_pages": recent, "private_pages": private_items,
|
"recent_pages": recent, "private_pages": private_items,
|
||||||
"favorite_pages": favorites, "shared_pages": shared_pages,
|
"favorite_pages": favorites, "shared_pages": shared_pages,
|
||||||
|
"published_pages": published_pages,
|
||||||
"user": user, "workspace_key": ws_key,
|
"user": user, "workspace_key": ws_key,
|
||||||
"auth_method": auth_method,
|
"auth_method": auth_method,
|
||||||
"gitea_linked": gitea_linked,
|
"gitea_linked": gitea_linked,
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ def _sidebar_data(request: Request, repos: list[dict]) -> dict:
|
|||||||
"private_pages": [],
|
"private_pages": [],
|
||||||
"favorite_pages": [],
|
"favorite_pages": [],
|
||||||
"shared_pages": [],
|
"shared_pages": [],
|
||||||
|
"published_pages": [],
|
||||||
"user": user,
|
"user": user,
|
||||||
"avatar_url": avatar_url,
|
"avatar_url": avatar_url,
|
||||||
"avatar_color": avatar_color,
|
"avatar_color": avatar_color,
|
||||||
|
|||||||
@@ -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"}
|
||||||
+24
-1
@@ -281,6 +281,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Published -->
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-section-header" @click="toggleSection('published')">
|
||||||
|
<div class="sidebar-section-title">
|
||||||
|
<span class="chevron" :class="{ open: sectionsOpen.published }" x-text="sectionsOpen.published ? '▼' : '▶'"></span>
|
||||||
|
<span>Published</span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-section-actions">
|
||||||
|
<a href="/library?tab=Published" class="library-link-btn" title="View in Library" @click.stop>→ Library</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-section-items" x-show="sectionsOpen.published" x-transition>
|
||||||
|
<ul class="sidebar-items" data-section="published">
|
||||||
|
{% for page in published_pages %}
|
||||||
|
{{ render_tree_item(page) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% if not published_pages %}
|
||||||
|
<li class="sidebar-item empty-hint"><span class="page-icon">🌐</span><span class="page-name text-dim">No published pages</span></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Private -->
|
<!-- Private -->
|
||||||
{% if auth_method != 'local' or '/' in workspace_key %}
|
{% if auth_method != 'local' or '/' in workspace_key %}
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
@@ -628,7 +651,7 @@
|
|||||||
|
|
||||||
// ── Sections collapsible ──
|
// ── Sections collapsible ──
|
||||||
sectionsOpen: (function() {
|
sectionsOpen: (function() {
|
||||||
var def = { recents: true, private: true, meetings: true, favorites: true, agents: true, shared: true, apps: true, workspace: true };
|
var def = { recents: true, private: true, meetings: true, favorites: true, agents: true, shared: true, published: true, apps: true, workspace: true };
|
||||||
try {
|
try {
|
||||||
var saved = JSON.parse(localStorage.getItem('fd_sections') || '{}');
|
var saved = JSON.parse(localStorage.getItem('fd_sections') || '{}');
|
||||||
return Object.assign(def, saved);
|
return Object.assign(def, saved);
|
||||||
|
|||||||
Reference in New Issue
Block a user