v0.3.0: OAuth2, CSRF, rate limiting, issue CRUD, webhooks, card detail, checklists
- 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
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
"""FlowDeck — CSRF protection middleware."""
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.requests import Request
|
||||
|
||||
|
||||
class CSRFMiddleware(BaseHTTPMiddleware):
|
||||
"""Lightweight CSRF protection for state-changing requests.
|
||||
|
||||
All POST/PUT/PATCH/DELETE requests must include X-CSRF-Token
|
||||
header matching the csrf_token cookie.
|
||||
"""
|
||||
|
||||
SAFE_METHODS = {"GET", "HEAD", "OPTIONS"}
|
||||
EXCLUDED_PATHS = {"/api/webhook", "/auth/callback"}
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
# Webhook receiver and OAuth callback are exempt
|
||||
if request.url.path in self.EXCLUDED_PATHS:
|
||||
return await call_next(request)
|
||||
|
||||
if request.method in self.SAFE_METHODS:
|
||||
response = await call_next(request)
|
||||
# Set CSRF cookie if not present
|
||||
if "csrf_token" not in request.cookies:
|
||||
response.set_cookie(
|
||||
"csrf_token",
|
||||
secrets.token_hex(32),
|
||||
httponly=False, # Must be readable by JS
|
||||
samesite="lax",
|
||||
max_age=86400,
|
||||
)
|
||||
return response
|
||||
|
||||
# Validate CSRF for state-changing methods
|
||||
csrf_cookie = request.cookies.get("csrf_token", "")
|
||||
csrf_header = request.headers.get("X-CSRF-Token", "")
|
||||
|
||||
if not csrf_cookie or not csrf_header or not secrets.compare_digest(csrf_cookie, csrf_header):
|
||||
return JSONResponse(
|
||||
{"detail": "CSRF validation failed"},
|
||||
status_code=403,
|
||||
)
|
||||
|
||||
return await call_next(request)
|
||||
Reference in New Issue
Block a user