diff --git a/app/main.py b/app/main.py index 2d197d3..1320b3a 100644 --- a/app/main.py +++ b/app/main.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from contextlib import asynccontextmanager -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from starlette.middleware.sessions import SessionMiddleware @@ -84,3 +84,61 @@ async def pwa_manifest(): "theme_color": "#191919", "icons": [{"src": "/static/icon-192.png", "sizes": "192x192", "type": "image/png"}], } + + +# ═══════════ API aliases (v4.0.1) ═══════════ + + +@app.get("/api/pages") +async def api_pages_alias(request: Request): + """Alias /api/pages → /board/api/pages for API path consistency.""" + from fastapi.responses import RedirectResponse + qs = str(request.url.query) + target = f"/board/api/pages{'?' + qs if qs else ''}" + return RedirectResponse(url=target, status_code=307) + + +@app.post("/api/pages") +async def api_pages_post_alias(request: Request): + """Alias POST /api/pages → /board/api/pages for API path consistency.""" + from fastapi.responses import RedirectResponse + return RedirectResponse(url="/board/api/pages", status_code=307) + + +# ═══════════ Styled 404 handler ═══════════ + + +NOT_FOUND_HTML = """ + + + +404 — FlowDeck + + + +
+

404

+

This page doesn't exist or has been moved.

+← Back to FlowDeck +
+ +""" + + +@app.exception_handler(404) +async def not_found_handler(request: Request, exc): + """Styled 404 page matching the FlowDeck dark theme.""" + from fastapi.responses import HTMLResponse + # API routes should get JSON, not HTML + if request.url.path.startswith("/api/") or request.url.path.startswith("/board/api/"): + from fastapi.responses import JSONResponse + return JSONResponse({"detail": "Not found"}, status_code=404) + return HTMLResponse(NOT_FOUND_HTML, status_code=404) diff --git a/app/routers/auth.py b/app/routers/auth.py index 8d263c2..3f09d1d 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -85,6 +85,27 @@ async function handleLogin(e){e.preventDefault();const email=document.getElement """ +@router.get("/register") +async def register_page(request: Request): + """Show the registration page (local login page with register tab active).""" + return HTMLResponse(LOCAL_LOGIN_HTML.replace( + 'class="tab active" onclick="switchTab(\'login\')"', + 'class="tab" onclick="switchTab(\'login\')"' + ).replace( + 'class="tab" onclick="switchTab(\'register\')"', + 'class="tab active" onclick="switchTab(\'register\')"' + ).replace( + 'let mode=\'login\';', + 'let mode=\'register\';' + ).replace( + 'id="name-group" style="display:none"', + 'id="name-group" style="display:block"' + ).replace( + 'id="submit-btn">Login<', + 'id="submit-btn">Register<' + ), status_code=200) + + @router.get("/login") async def login(request: Request, provider: str = Query("gitea")): """Redirect to OAuth2 authorize page or show local login page.""" diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 11d20b3..9816483 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -404,11 +404,29 @@ async def dashboard( search: str = Query(default=""), show_archived: bool = Query(default=False), ): - auth = _get_user_or_redirect(request) - if isinstance(auth, RedirectResponse): - return auth - # Check if user has Gitea connected — if not, show local workspace - user_id = auth.get("id") if isinstance(auth, dict) else 1 + """Smart root route: landing for visitors, local workspace for new users, dashboard for Gitea users.""" + user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) + + # ── Not authenticated → show landing page ── + if not user: + # Allow through if DB is empty (fresh install) + try: + with get_conn() as conn: + count = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0] + if count == 0: + from jinja2 import Environment, FileSystemLoader + env = Environment(loader=FileSystemLoader("app/templates")) + template = env.get_template("landing.html") + return template.render() + except Exception: + pass + from jinja2 import Environment, FileSystemLoader + env = Environment(loader=FileSystemLoader("app/templates")) + template = env.get_template("landing.html") + return template.render() + + # ── Authenticated ── + user_id = user.get("id", 1) has_gitea = False try: with get_conn() as conn: @@ -419,8 +437,9 @@ async def dashboard( has_gitea = bool(tok) except Exception: pass + if not has_gitea: - # Check if user has any workspace — if not, send to workspaces + # Check if user has any workspace try: with get_conn() as conn: ws_count = conn.execute( @@ -431,6 +450,8 @@ async def dashboard( except Exception: pass return RedirectResponse("/local-workspace", status_code=302) + + # ── Gitea user → full dashboard ── try: repos = await gitea.get_user_repos(page=1, limit=50) if search: diff --git a/app/templates/landing.html b/app/templates/landing.html new file mode 100644 index 0000000..8e6ff15 --- /dev/null +++ b/app/templates/landing.html @@ -0,0 +1,204 @@ + + + + + + FlowDeck — All-in-one workspace + + + + + + + +
+

Your Notion-style workspace,
self-hosted & integrated

+

+ Write, organize, and collaborate — with block-based editing, + Kanban boards, databases, and deep Gitea/GitHub integration. + All on your own server. +

+
+ 🚀 Get started — it's free + I already have an account +
+
+ +
+
+
📝
+

Block Editor

+

Notion-style editor with slash commands, markdown shortcuts, headings, code blocks, to-dos, and more.

+
+
+
📊
+

Kanban & Tables

+

Visual project management with drag-drop boards, table views, and database collections.

+
+
+
🦎
+

Gitea & GitHub

+

Browse repos, edit files, commit changes — all from your workspace sidebar.

+
+
+
🌐
+

Publish to Web

+

One-click publish any page to a public URL. Share with anyone, no account needed.

+
+
+
🔒
+

Self-Hosted

+

Your data stays on your server. No vendor lock-in, full control, Docker single-container deploy.

+
+
+
⚡
+

Fast & Light

+

Alpine.js + FastAPI + SQLite. No React bloat, no Node.js needed. Sub-100ms page loads.

+
+
+ + + + +