diff --git a/backend/main.py b/backend/main.py index 597b62c..b98a7d3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,7 +3,7 @@ NoteDiscovery - Self-Hosted Markdown Knowledge Base Main FastAPI application """ -from fastapi import FastAPI, HTTPException, UploadFile, File, Request, Form, Depends +from fastapi import FastAPI, HTTPException, UploadFile, File, Request, Form, Depends, APIRouter from fastapi.staticfiles import StaticFiles from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, RedirectResponse from fastapi.middleware.cors import CORSMiddleware @@ -200,11 +200,27 @@ async def favicon(): # ============================================================================ -# Application Routes +# Routers with Authentication # ============================================================================ -@app.get("/", response_class=HTMLResponse) -async def root(request: Request, _auth: None = Depends(require_auth)): +# Create API router with authentication dependency applied globally +api_router = APIRouter( + prefix="/api", + dependencies=[Depends(require_auth)] # Apply auth to ALL routes in this router +) + +# Create pages router with authentication dependency applied globally +pages_router = APIRouter( + dependencies=[Depends(require_auth)] # Apply auth to ALL routes in this router +) + + +# ============================================================================ +# Application Routes (with auth via router dependencies) +# ============================================================================ + +@pages_router.get("/", response_class=HTMLResponse) +async def root(request: Request): """Serve the main application page""" index_path = static_path / "index.html" async with aiofiles.open(index_path, 'r', encoding='utf-8') as f: @@ -212,7 +228,7 @@ async def root(request: Request, _auth: None = Depends(require_auth)): return content -@app.get("/api") +@api_router.get("") async def api_documentation(): """API Documentation - List all available endpoints""" return { @@ -364,7 +380,7 @@ async def api_documentation(): } -@app.get("/api/config") +@api_router.get("/config") async def get_config(): """Get app configuration for frontend""" return { @@ -378,7 +394,7 @@ async def get_config(): } -@app.get("/api/themes") +@api_router.get("/themes") async def list_themes(): """Get all available themes""" themes_dir = Path(__file__).parent.parent / "themes" @@ -398,8 +414,8 @@ async def get_theme(theme_id: str): return {"css": css, "theme_id": theme_id} -@app.post("/api/folders") -async def create_new_folder(data: dict, _auth: None = Depends(require_auth)): +@api_router.post("/folders") +async def create_new_folder(data: dict): """Create a new folder""" try: folder_path = data.get('path', '') @@ -420,8 +436,8 @@ async def create_new_folder(data: dict, _auth: None = Depends(require_auth)): raise HTTPException(status_code=500, detail=str(e)) -@app.post("/api/notes/move") -async def move_note_endpoint(data: dict, _auth: None = Depends(require_auth)): +@api_router.post("/notes/move") +async def move_note_endpoint(data: dict): """Move a note to a different folder""" try: old_path = data.get('oldPath', '') @@ -448,8 +464,8 @@ async def move_note_endpoint(data: dict, _auth: None = Depends(require_auth)): raise HTTPException(status_code=500, detail=str(e)) -@app.post("/api/folders/move") -async def move_folder_endpoint(data: dict, _auth: None = Depends(require_auth)): +@api_router.post("/folders/move") +async def move_folder_endpoint(data: dict): """Move a folder to a different location""" try: old_path = data.get('oldPath', '') @@ -473,8 +489,8 @@ async def move_folder_endpoint(data: dict, _auth: None = Depends(require_auth)): raise HTTPException(status_code=500, detail=str(e)) -@app.post("/api/folders/rename") -async def rename_folder_endpoint(data: dict, _auth: None = Depends(require_auth)): +@api_router.post("/folders/rename") +async def rename_folder_endpoint(data: dict): """Rename a folder""" try: old_path = data.get('oldPath', '') @@ -498,8 +514,8 @@ async def rename_folder_endpoint(data: dict, _auth: None = Depends(require_auth) raise HTTPException(status_code=500, detail=str(e)) -@app.delete("/api/folders/{folder_path:path}") -async def delete_folder_endpoint(folder_path: str, _auth: None = Depends(require_auth)): +@api_router.delete("/folders/{folder_path:path}") +async def delete_folder_endpoint(folder_path: str): """Delete a folder and all its contents""" try: if not folder_path: @@ -519,7 +535,7 @@ async def delete_folder_endpoint(folder_path: str, _auth: None = Depends(require raise HTTPException(status_code=500, detail=str(e)) -@app.get("/api/notes") +@api_router.get("/notes") async def list_notes(_auth: None = Depends(require_auth)): """List all notes with metadata""" try: @@ -530,8 +546,8 @@ async def list_notes(_auth: None = Depends(require_auth)): raise HTTPException(status_code=500, detail=str(e)) -@app.get("/api/notes/{note_path:path}") -async def get_note(note_path: str, _auth: None = Depends(require_auth)): +@api_router.get("/notes/{note_path:path}") +async def get_note(note_path: str): """Get a specific note's content""" try: content = get_note_content(config['storage']['notes_dir'], note_path) @@ -558,8 +574,8 @@ async def get_note(note_path: str, _auth: None = Depends(require_auth)): raise HTTPException(status_code=500, detail=str(e)) -@app.post("/api/notes/{note_path:path}") -async def create_or_update_note(note_path: str, content: dict, _auth: None = Depends(require_auth)): +@api_router.post("/notes/{note_path:path}") +async def create_or_update_note(note_path: str, content: dict): """Create or update a note""" try: note_content = content.get('content', '') @@ -596,8 +612,8 @@ async def create_or_update_note(note_path: str, content: dict, _auth: None = Dep raise HTTPException(status_code=500, detail=str(e)) -@app.delete("/api/notes/{note_path:path}") -async def remove_note(note_path: str, _auth: None = Depends(require_auth)): +@api_router.delete("/notes/{note_path:path}") +async def remove_note(note_path: str): """Delete a note""" try: success = delete_note(config['storage']['notes_dir'], note_path) @@ -616,8 +632,8 @@ async def remove_note(note_path: str, _auth: None = Depends(require_auth)): raise HTTPException(status_code=500, detail=str(e)) -@app.get("/api/search") -async def search(q: str, _auth: None = Depends(require_auth)): +@api_router.get("/search") +async def search(q: str): """Search notes by content""" try: if not config['search']['enabled']: @@ -635,7 +651,7 @@ async def search(q: str, _auth: None = Depends(require_auth)): raise HTTPException(status_code=500, detail=str(e)) -@app.get("/api/graph") +@api_router.get("/graph") async def get_graph(_auth: None = Depends(require_auth)): """Get graph data for visualization""" try: @@ -665,14 +681,14 @@ async def get_graph(_auth: None = Depends(require_auth)): raise HTTPException(status_code=500, detail=str(e)) -@app.get("/api/plugins") +@api_router.get("/plugins") async def list_plugins(_auth: None = Depends(require_auth)): """List all available plugins""" return {"plugins": plugin_manager.list_plugins()} @app.get("/api/plugins/note_stats/calculate") -async def calculate_note_stats(content: str, _auth: None = Depends(require_auth)): +async def calculate_note_stats(content: str): """Calculate statistics for note content (if plugin enabled)""" try: plugin = plugin_manager.plugins.get('note_stats') @@ -685,8 +701,8 @@ async def calculate_note_stats(content: str, _auth: None = Depends(require_auth) raise HTTPException(status_code=500, detail=str(e)) -@app.post("/api/plugins/{plugin_name}/toggle") -async def toggle_plugin(plugin_name: str, enabled: dict, _auth: None = Depends(require_auth)): +@api_router.post("/plugins/{plugin_name}/toggle") +async def toggle_plugin(plugin_name: str, enabled: dict): """Enable or disable a plugin""" try: is_enabled = enabled.get('enabled', False) @@ -716,8 +732,8 @@ async def health_check(): # Catch-all route for SPA (Single Page Application) routing # This allows URLs like /folder/note to work for direct navigation -@app.get("/{full_path:path}", response_class=HTMLResponse) -async def catch_all(full_path: str, request: Request, _auth: None = Depends(require_auth)): +@pages_router.get("/{full_path:path}", response_class=HTMLResponse) +async def catch_all(full_path: str, request: Request): """ Serve index.html for all non-API routes. This enables client-side routing (e.g., /folder/note) @@ -733,6 +749,16 @@ async def catch_all(full_path: str, request: Request, _auth: None = Depends(requ return content +# ============================================================================ +# Register Routers +# ============================================================================ + +# Register routers with the main app +# Authentication is applied via router dependencies +app.include_router(api_router) +app.include_router(pages_router) + + if __name__ == "__main__": import uvicorn uvicorn.run(