From 6ca68126ede03fa6a965c8028c004c441d141fcc Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 11:34:22 +0100 Subject: [PATCH 01/22] added Authentik config --- app/auth.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++ app/main.py | 34 +++++++++++++++----------------- 2 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 app/auth.py diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 00000000..6415af9b --- /dev/null +++ b/app/auth.py @@ -0,0 +1,56 @@ +# app/auth.py +import os +from authlib.integrations.starlette_client import OAuth +from starlette.config import Config +from starlette.middleware.sessions import SessionMiddleware +from fastapi import APIRouter, Request +from starlette.responses import RedirectResponse + +config = Config(".env") +oauth = OAuth(config) + +oauth.register( + name="authentik", + client_id=config("AUTHENTIK_CLIENT_ID"), + client_secret=config("AUTHENTIK_CLIENT_SECRET"), + server_metadata_url=config("AUTHENTIK_CONFIG_URL"), + client_kwargs={ + "scope": "openid profile email", + }, +) + +router = APIRouter() + +def get_current_user(request: Request): + return request.session.get("user") + +def require_login(func): + """Decorator to require login for particular endpoints.""" + async def wrapper(request: Request, *args, **kwargs): + if not request.session.get("user"): + return RedirectResponse(url="/login") + return await func(request, *args, **kwargs) + return wrapper + +@router.get("/login") +async def login(request: Request): + redirect_uri = request.url_for("auth") + return await oauth.authentik.authorize_redirect(request, redirect_uri) + +@router.get("/auth") +async def auth(request: Request): + token = await oauth.authentik.authorize_access_token(request) + userinfo = token.get("userinfo") + request.session["user"] = dict(userinfo) + return RedirectResponse(url="/ui") + +@router.get("/logout") +async def logout(request: Request): + request.session.pop("user", None) + return RedirectResponse(url="/") + +@router.get("/private") +@require_login +async def private_page(request: Request): + user = request.session.get("user") + return {"message": f"This is a protected page. Hello {user['email']}!"} diff --git a/app/main.py b/app/main.py index 9d03538f..432cb805 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - import os from fastapi import FastAPI, HTTPException, UploadFile, File from app.config import settings @@ -9,9 +8,19 @@ from app.tasks.upload_to_paperless import upload_to_paperless from app.tasks.upload_to_nextcloud import upload_to_nextcloud from app.tasks.send_to_all import send_to_all_destinations from app.frontend import router as frontend_router +from app.auth import router as auth_router +from starlette.middleware.sessions import SessionMiddleware +from starlette.config import Config + +# Load configuration from .env for the session key +config = Config(".env") +SESSION_SECRET = config("SESSION_SECRET", default="YOUR_DEFAULT_SESSION_SECRET_MUST_BE_32_CHARS_OR_MORE") app = FastAPI(title="Document Processing API") +# Add session middleware (needed for storing user sessions) +app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET) + @app.get("/") def root(): return {"message": "Document Processing API"} @@ -22,8 +31,6 @@ def process(file_path: str): API Endpoint to start document processing. This enqueues the first task (upload_to_s3), which handles the full pipeline. """ - - # If file_path is not absolute, treat it as relative to settings.workdir. if not os.path.isabs(file_path): file_path = os.path.join(settings.workdir, file_path) @@ -60,27 +67,20 @@ def send_to_nextcloud(file_path: str): task = upload_to_nextcloud.delay(file_path) return {"task_id": task.id, "status": "queued"} - @app.post("/send_to_all_destinations/") def send_to_all_destinations_endpoint(file_path: str): """ Call the aggregator task that sends this file to dropbox, nextcloud, and paperless. """ if not os.path.isabs(file_path): - # If not absolute, assume it's in processed subdir file_path = os.path.join(settings.workdir, 'processed', file_path) if not os.path.exists(file_path): - raise HTTPException( - status_code=400, - detail=f"File {file_path} not found." - ) + raise HTTPException(status_code=400, detail=f"File {file_path} not found.") task = send_to_all_destinations.delay(file_path) return {"task_id": task.id, "status": "queued", "file_path": file_path} - - @app.post("/processall") def process_all_pdfs_in_workdir(): """ @@ -102,7 +102,6 @@ def process_all_pdfs_in_workdir(): task_ids = [] for pdf in pdf_files: file_path = os.path.join(target_dir, pdf) - # Enqueue upload_to_s3 from app.tasks.upload_to_s3 import upload_to_s3 task = upload_to_s3.delay(file_path) task_ids.append(task.id) @@ -113,14 +112,10 @@ def process_all_pdfs_in_workdir(): "task_ids": task_ids } -app.include_router(frontend_router) - @app.post("/ui-upload") async def ui_upload(file: UploadFile = File(...)): - # You can store this file in your 'workdir' (like how /process does) or a tmp dir workdir = "/workdir" target_path = os.path.join(workdir, file.filename) - try: with open(target_path, "wb") as f: content = await file.read() @@ -128,6 +123,9 @@ async def ui_upload(file: UploadFile = File(...)): except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to save file: {e}") - # Now you can call your existing Celery flow: task = upload_to_s3.delay(target_path) - return {"task_id": task.id, "status": "queued"} \ No newline at end of file + return {"task_id": task.id, "status": "queued"} + +# Include the frontend and auth routers +app.include_router(frontend_router) +app.include_router(auth_router) From 1f510a18caea2d622eff10eb5e01c473a3c782a2 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 12:09:36 +0100 Subject: [PATCH 02/22] added authlib to requirements --- .env.demo | 9 ++++++++- requirements.txt | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.env.demo b/.env.demo index a24f4427..a2ad6189 100644 --- a/.env.demo +++ b/.env.demo @@ -44,4 +44,11 @@ IMAP2_SSL=true IMAP2_POLL_INTERVAL_MINUTES=10 IMAP2_DELETE_AFTER_PROCESS=false -GOTENBERG_URL=http://gotenberg:3000 \ No newline at end of file +GOTENBERG_URL=http://gotenberg:3000 + +# ** needed for Authentik ** + +SESSION_SECRET= +AUTHENTIK_CLIENT_ID= +AUTHENTIK_CLIENT_SECRET= +AUTHENTIK_CONFIG_URL= \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e61f348f..e261fe8b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,6 @@ openai pymupdf requests dropbox -azure-ai-documentintelligence \ No newline at end of file +azure-ai-documentintelligence +authlib +python-dotenv \ No newline at end of file From bf4aa95f52f36d87d78473841b33b1e407ebe525 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 12:23:21 +0100 Subject: [PATCH 03/22] added support for correct url detection --- app/main.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index 432cb805..4f590905 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,10 @@ #!/usr/bin/env python3 import os from fastapi import FastAPI, HTTPException, UploadFile, File +from starlette.middleware.sessions import SessionMiddleware +from starlette.middleware.proxy_headers import ProxyHeadersMiddleware +from starlette.config import Config + from app.config import settings from app.tasks.upload_to_s3 import upload_to_s3 from app.tasks.upload_to_dropbox import upload_to_dropbox @@ -9,8 +13,6 @@ from app.tasks.upload_to_nextcloud import upload_to_nextcloud from app.tasks.send_to_all import send_to_all_destinations from app.frontend import router as frontend_router from app.auth import router as auth_router -from starlette.middleware.sessions import SessionMiddleware -from starlette.config import Config # Load configuration from .env for the session key config = Config(".env") @@ -18,6 +20,9 @@ SESSION_SECRET = config("SESSION_SECRET", default="YOUR_DEFAULT_SESSION_SECRET_M app = FastAPI(title="Document Processing API") +# Add ProxyHeadersMiddleware so that FastAPI uses the proper forwarded headers (e.g., X-Forwarded-Proto) +app.add_middleware(ProxyHeadersMiddleware) + # Add session middleware (needed for storing user sessions) app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET) From b9be5af4074a0c0a2f7082b6cd94303414dffd36 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 12:26:33 +0100 Subject: [PATCH 04/22] adding starlette to requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e261fe8b..82bcc5fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,5 @@ requests dropbox azure-ai-documentintelligence authlib -python-dotenv \ No newline at end of file +python-dotenv +starlette \ No newline at end of file From 8aa667fd488f272a5d5f4c0d7b3b30c75309fc88 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 12:44:41 +0100 Subject: [PATCH 05/22] removed proxymiddleware and moved to uvicorn startup command --- app/main.py | 3 --- docker-compose.yaml | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index 4f590905..3ae36f36 100644 --- a/app/main.py +++ b/app/main.py @@ -2,7 +2,6 @@ import os from fastapi import FastAPI, HTTPException, UploadFile, File from starlette.middleware.sessions import SessionMiddleware -from starlette.middleware.proxy_headers import ProxyHeadersMiddleware from starlette.config import Config from app.config import settings @@ -20,8 +19,6 @@ SESSION_SECRET = config("SESSION_SECRET", default="YOUR_DEFAULT_SESSION_SECRET_M app = FastAPI(title="Document Processing API") -# Add ProxyHeadersMiddleware so that FastAPI uses the proper forwarded headers (e.g., X-Forwarded-Proto) -app.add_middleware(ProxyHeadersMiddleware) # Add session middleware (needed for storing user sessions) app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET) diff --git a/docker-compose.yaml b/docker-compose.yaml index 4e67945d..38295749 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,7 +8,7 @@ services: working_dir: /workdir # We'll run uvicorn from the container's /app code - command: ["sh", "-c", "cd /app && uvicorn app.main:app --host 0.0.0.0 --port 8000"] + command: ["sh", "-c", "cd /app && uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers"] # Environment variables environment: From 5005f65f171b5a45dacf603a9e68d416e49cc059 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 14:45:45 +0100 Subject: [PATCH 06/22] changes to the Authentik integration --- app/auth.py | 23 +++++++++++++++++---- app/main.py | 58 ++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/app/auth.py b/app/auth.py index 6415af9b..71694afb 100644 --- a/app/auth.py +++ b/app/auth.py @@ -1,9 +1,10 @@ # app/auth.py import os +from functools import wraps + from authlib.integrations.starlette_client import OAuth from starlette.config import Config -from starlette.middleware.sessions import SessionMiddleware -from fastapi import APIRouter, Request +from fastapi import APIRouter, Request, status from starlette.responses import RedirectResponse config = Config(".env") @@ -26,19 +27,31 @@ def get_current_user(request: Request): def require_login(func): """Decorator to require login for particular endpoints.""" + @wraps(func) async def wrapper(request: Request, *args, **kwargs): if not request.session.get("user"): - return RedirectResponse(url="/login") + # Not logged in => redirect to /login + return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) + # User is logged in => proceed return await func(request, *args, **kwargs) return wrapper @router.get("/login") async def login(request: Request): + """ + Initiate Authentik login flow. + This calls oauth.authentik.authorize_redirect(...) + The resulting redirect_uri must match what's in Authentik's application config. + """ redirect_uri = request.url_for("auth") return await oauth.authentik.authorize_redirect(request, redirect_uri) @router.get("/auth") async def auth(request: Request): + """ + Authentik callback endpoint. Exchanges code for token, + puts user info in session, and redirects to /ui or wherever you want. + """ token = await oauth.authentik.authorize_access_token(request) userinfo = token.get("userinfo") request.session["user"] = dict(userinfo) @@ -46,11 +59,13 @@ async def auth(request: Request): @router.get("/logout") async def logout(request: Request): + """Clears session and redirects home.""" request.session.pop("user", None) return RedirectResponse(url="/") @router.get("/private") @require_login async def private_page(request: Request): - user = request.session.get("user") + """A protected endpoint that requires login.""" + user = request.session.get("user") # e.g. {"email": "...", ...} return {"message": f"This is a protected page. Hello {user['email']}!"} diff --git a/app/main.py b/app/main.py index 3ae36f36..7a41b769 100644 --- a/app/main.py +++ b/app/main.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 import os + from fastapi import FastAPI, HTTPException, UploadFile, File from starlette.middleware.sessions import SessionMiddleware from starlette.config import Config +from starlette.middleware.trustedhost import TrustedHostMiddleware +from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware from app.config import settings from app.tasks.upload_to_s3 import upload_to_s3 @@ -15,14 +18,27 @@ from app.auth import router as auth_router # Load configuration from .env for the session key config = Config(".env") -SESSION_SECRET = config("SESSION_SECRET", default="YOUR_DEFAULT_SESSION_SECRET_MUST_BE_32_CHARS_OR_MORE") +SESSION_SECRET = config( + "SESSION_SECRET", + default="YOUR_DEFAULT_SESSION_SECRET_MUST_BE_32_CHARS_OR_MORE" +) app = FastAPI(title="Document Processing API") - -# Add session middleware (needed for storing user sessions) +# 1) Session Middleware (for request.session to work) app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET) +# 2) Respect the X-Forwarded-* headers from Traefik +# so your request.url_for(...) uses https://docparse.hosterra.net +app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*") + +# 3) (Optional but recommended) Restrict valid hosts: +app.add_middleware(TrustedHostMiddleware, allowed_hosts=[ + "docparse.hosterra.net", + "localhost", + "127.0.0.1" +]) + @app.get("/") def root(): return {"message": "Document Processing API"} @@ -37,7 +53,9 @@ def process(file_path: str): file_path = os.path.join(settings.workdir, file_path) if not os.path.exists(file_path): - raise HTTPException(status_code=400, detail=f"File {file_path} not found.") + raise HTTPException( + status_code=400, detail=f"File {file_path} not found." + ) task = upload_to_s3.delay(file_path) return {"task_id": task.id, "status": "queued"} @@ -47,7 +65,9 @@ def send_to_dropbox(file_path: str): if not os.path.isabs(file_path): file_path = os.path.join(settings.workdir, 'processed', file_path) if not os.path.exists(file_path): - raise HTTPException(status_code=400, detail=f"File {file_path} not found.") + raise HTTPException( + status_code=400, detail=f"File {file_path} not found." + ) task = upload_to_dropbox.delay(file_path) return {"task_id": task.id, "status": "queued"} @@ -56,7 +76,9 @@ def send_to_paperless(file_path: str): if not os.path.isabs(file_path): file_path = os.path.join(settings.workdir, 'processed', file_path) if not os.path.exists(file_path): - raise HTTPException(status_code=400, detail=f"File {file_path} not found.") + raise HTTPException( + status_code=400, detail=f"File {file_path} not found." + ) task = upload_to_paperless.delay(file_path) return {"task_id": task.id, "status": "queued"} @@ -65,7 +87,9 @@ def send_to_nextcloud(file_path: str): if not os.path.isabs(file_path): file_path = os.path.join(settings.workdir, 'processed', file_path) if not os.path.exists(file_path): - raise HTTPException(status_code=400, detail=f"File {file_path} not found.") + raise HTTPException( + status_code=400, detail=f"File {file_path} not found." + ) task = upload_to_nextcloud.delay(file_path) return {"task_id": task.id, "status": "queued"} @@ -78,7 +102,9 @@ def send_to_all_destinations_endpoint(file_path: str): file_path = os.path.join(settings.workdir, 'processed', file_path) if not os.path.exists(file_path): - raise HTTPException(status_code=400, detail=f"File {file_path} not found.") + raise HTTPException( + status_code=400, detail=f"File {file_path} not found." + ) task = send_to_all_destinations.delay(file_path) return {"task_id": task.id, "status": "queued", "file_path": file_path} @@ -86,12 +112,13 @@ def send_to_all_destinations_endpoint(file_path: str): @app.post("/processall") def process_all_pdfs_in_workdir(): """ - Finds all .pdf files in /processed - and enqueues them for upload_to_s3. + Finds all .pdf files in and enqueues them for upload_to_s3. """ target_dir = settings.workdir if not os.path.exists(target_dir): - raise HTTPException(status_code=400, detail=f"Directory {target_dir} does not exist.") + raise HTTPException( + status_code=400, detail=f"Directory {target_dir} does not exist." + ) pdf_files = [] for filename in os.listdir(target_dir): @@ -99,12 +126,11 @@ def process_all_pdfs_in_workdir(): pdf_files.append(filename) if not pdf_files: - return {"message": "No PDF files found in processed directory."} + return {"message": "No PDF files found in that directory."} task_ids = [] for pdf in pdf_files: file_path = os.path.join(target_dir, pdf) - from app.tasks.upload_to_s3 import upload_to_s3 task = upload_to_s3.delay(file_path) task_ids.append(task.id) @@ -116,6 +142,7 @@ def process_all_pdfs_in_workdir(): @app.post("/ui-upload") async def ui_upload(file: UploadFile = File(...)): + """Endpoint to accept a user-uploaded file and enqueue it to S3.""" workdir = "/workdir" target_path = os.path.join(workdir, file.filename) try: @@ -123,7 +150,10 @@ async def ui_upload(file: UploadFile = File(...)): content = await file.read() f.write(content) except Exception as e: - raise HTTPException(status_code=500, detail=f"Failed to save file: {e}") + raise HTTPException( + status_code=500, + detail=f"Failed to save file: {e}" + ) task = upload_to_s3.delay(target_path) return {"task_id": task.id, "status": "queued"} From 6beaa30e25df3d6925f16be51afbbd27769a3c73 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 14:56:21 +0100 Subject: [PATCH 07/22] improved redirect upon login --- app/auth.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/auth.py b/app/auth.py index 71694afb..ba938ba9 100644 --- a/app/auth.py +++ b/app/auth.py @@ -26,13 +26,12 @@ def get_current_user(request: Request): return request.session.get("user") def require_login(func): - """Decorator to require login for particular endpoints.""" @wraps(func) async def wrapper(request: Request, *args, **kwargs): if not request.session.get("user"): - # Not logged in => redirect to /login + # Save original URL in session before redirecting to login + request.session["redirect_after_login"] = str(request.url) return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) - # User is logged in => proceed return await func(request, *args, **kwargs) return wrapper @@ -55,7 +54,10 @@ async def auth(request: Request): token = await oauth.authentik.authorize_access_token(request) userinfo = token.get("userinfo") request.session["user"] = dict(userinfo) - return RedirectResponse(url="/ui") + + # Get original destination or fallback + redirect_url = request.session.pop("redirect_after_login", "/ui") + return RedirectResponse(url=redirect_url) @router.get("/logout") async def logout(request: Request): From d00ee8cc3dbad17b371b8a148e89c56b339813a1 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 16:22:59 +0100 Subject: [PATCH 08/22] added auth flag and auth to /upload --- .env.demo | 2 +- app/auth.py | 70 ++++++++++++++++++++++++------------------------- app/frontend.py | 5 +++- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/.env.demo b/.env.demo index a2ad6189..144b4ee4 100644 --- a/.env.demo +++ b/.env.demo @@ -47,7 +47,7 @@ IMAP2_DELETE_AFTER_PROCESS=false GOTENBERG_URL=http://gotenberg:3000 # ** needed for Authentik ** - +AUTH_ENABLED=true SESSION_SECRET= AUTHENTIK_CLIENT_ID= AUTHENTIK_CLIENT_SECRET= diff --git a/app/auth.py b/app/auth.py index ba938ba9..db3df500 100644 --- a/app/auth.py +++ b/app/auth.py @@ -1,4 +1,5 @@ # app/auth.py +# app/auth.py import os from functools import wraps @@ -10,60 +11,57 @@ from starlette.responses import RedirectResponse config = Config(".env") oauth = OAuth(config) -oauth.register( - name="authentik", - client_id=config("AUTHENTIK_CLIENT_ID"), - client_secret=config("AUTHENTIK_CLIENT_SECRET"), - server_metadata_url=config("AUTHENTIK_CONFIG_URL"), - client_kwargs={ - "scope": "openid profile email", - }, -) +AUTH_ENABLED = config("AUTH_ENABLED", cast=bool, default=True) + +if AUTH_ENABLED: + oauth.register( + name="authentik", + client_id=config("AUTHENTIK_CLIENT_ID"), + client_secret=config("AUTHENTIK_CLIENT_SECRET"), + server_metadata_url=config("AUTHENTIK_CONFIG_URL"), + client_kwargs={"scope": "openid profile email"}, + ) router = APIRouter() + def get_current_user(request: Request): return request.session.get("user") + def require_login(func): + if not AUTH_ENABLED: + return func # no-op + @wraps(func) async def wrapper(request: Request, *args, **kwargs): if not request.session.get("user"): - # Save original URL in session before redirecting to login request.session["redirect_after_login"] = str(request.url) return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) return await func(request, *args, **kwargs) + return wrapper -@router.get("/login") -async def login(request: Request): - """ - Initiate Authentik login flow. - This calls oauth.authentik.authorize_redirect(...) - The resulting redirect_uri must match what's in Authentik's application config. - """ - redirect_uri = request.url_for("auth") - return await oauth.authentik.authorize_redirect(request, redirect_uri) -@router.get("/auth") -async def auth(request: Request): - """ - Authentik callback endpoint. Exchanges code for token, - puts user info in session, and redirects to /ui or wherever you want. - """ - token = await oauth.authentik.authorize_access_token(request) - userinfo = token.get("userinfo") - request.session["user"] = dict(userinfo) +if AUTH_ENABLED: + @router.get("/login") + async def login(request: Request): + redirect_uri = request.url_for("auth") + return await oauth.authentik.authorize_redirect(request, redirect_uri) - # Get original destination or fallback - redirect_url = request.session.pop("redirect_after_login", "/ui") - return RedirectResponse(url=redirect_url) + @router.get("/auth") + async def auth(request: Request): + token = await oauth.authentik.authorize_access_token(request) + userinfo = token.get("userinfo") + request.session["user"] = dict(userinfo) + redirect_url = request.session.pop("redirect_after_login", "/upload") + return RedirectResponse(url=redirect_url) + + @router.get("/logout") + async def logout(request: Request): + request.session.pop("user", None) + return RedirectResponse(url="/") -@router.get("/logout") -async def logout(request: Request): - """Clears session and redirects home.""" - request.session.pop("user", None) - return RedirectResponse(url="/") @router.get("/private") @require_login diff --git a/app/frontend.py b/app/frontend.py index 9cec1c42..f278fb4f 100644 --- a/app/frontend.py +++ b/app/frontend.py @@ -2,6 +2,7 @@ from fastapi import APIRouter from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles +from app.auth import require_login import os router = APIRouter() @@ -14,7 +15,9 @@ frontend_folder = os.path.join(os.path.dirname(__file__), "..", "frontend") router.mount("/static", StaticFiles(directory=frontend_folder), name="static") # 2) For the root route ("/"), return the index.html -@router.get("/ui", response_class=FileResponse) + +@router.get("/upload", response_class=FileResponse) +@require_login def serve_ui(): return os.path.join(frontend_folder, "index.html") From 6139a168d00fdf6310527e8de647be078301016b Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 17:31:27 +0100 Subject: [PATCH 09/22] added fixes to frontend.py --- app/frontend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/frontend.py b/app/frontend.py index f278fb4f..a0e1afef 100644 --- a/app/frontend.py +++ b/app/frontend.py @@ -1,5 +1,5 @@ # app/frontend.py (new file or inline in main.py) -from fastapi import APIRouter +from fastapi import APIRouter, Request from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from app.auth import require_login @@ -18,7 +18,7 @@ router.mount("/static", StaticFiles(directory=frontend_folder), name="static") @router.get("/upload", response_class=FileResponse) @require_login -def serve_ui(): +def serve_ui(request: Request): return os.path.join(frontend_folder, "index.html") # 3) Serve favicon.ico from the frontend folder From ff12e733eaa8ac00e5642f0652e1ae2dbde468ef Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 17:35:00 +0100 Subject: [PATCH 10/22] made function async to include auth --- app/frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend.py b/app/frontend.py index a0e1afef..7a3a8f39 100644 --- a/app/frontend.py +++ b/app/frontend.py @@ -18,7 +18,7 @@ router.mount("/static", StaticFiles(directory=frontend_folder), name="static") @router.get("/upload", response_class=FileResponse) @require_login -def serve_ui(request: Request): +async def serve_upload(request: Request): return os.path.join(frontend_folder, "index.html") # 3) Serve favicon.ico from the frontend folder From b95d13df1fb11bc51805d98825f2b0aabff615d6 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 21:16:02 +0100 Subject: [PATCH 11/22] add 404.html and new upload page --- app/frontend.py | 8 +++- frontend/404.html | 21 +++++++++ frontend/index.html | 107 +++++++++++++++++++++++++++++++++----------- 3 files changed, 107 insertions(+), 29 deletions(-) create mode 100644 frontend/404.html diff --git a/app/frontend.py b/app/frontend.py index 7a3a8f39..920ce22a 100644 --- a/app/frontend.py +++ b/app/frontend.py @@ -1,5 +1,5 @@ # app/frontend.py (new file or inline in main.py) -from fastapi import APIRouter, Request +from fastapi import APIRouter, Request, status from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from app.auth import require_login @@ -24,4 +24,8 @@ async def serve_upload(request: Request): # 3) Serve favicon.ico from the frontend folder @router.get("/favicon.ico", response_class=FileResponse) def favicon(): - return os.path.join(frontend_folder, "favicon.ico") \ No newline at end of file + return os.path.join(frontend_folder, "favicon.ico") + +@app.exception_handler(404) +async def custom_404_handler(request: Request, exc): + return FileResponse("frontend/404.html", status_code=status.HTTP_404_NOT_FOUND) \ No newline at end of file diff --git a/frontend/404.html b/frontend/404.html new file mode 100644 index 00000000..20086e75 --- /dev/null +++ b/frontend/404.html @@ -0,0 +1,21 @@ + + + + + 404 - Page not found + + + + +

404 - Page not found

+

+ Sorry, we couldn’t find the page you’re looking for. +

+ + ← Back to home + + + diff --git a/frontend/index.html b/frontend/index.html index b22c3d7d..444e61d4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,45 +1,78 @@ - Document Processor - Upload + - - -

Upload a File

+ -
-

- Drag & drop a file here, or click to select a file. -

- -
+ + -
+ +
+

Upload a File

+
+

+ Drag & drop a file here, or click to select a file. +

+ +
+ +
+
+ + +
+
+ © 2025 Document Processor - All rights reserved. +
+
+ + From 2c7a1853155a71c2e42710c8c1706f631568ca9a Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 21:18:25 +0100 Subject: [PATCH 12/22] changed 404.html code --- app/frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend.py b/app/frontend.py index 920ce22a..5d8565ec 100644 --- a/app/frontend.py +++ b/app/frontend.py @@ -26,6 +26,6 @@ async def serve_upload(request: Request): def favicon(): return os.path.join(frontend_folder, "favicon.ico") -@app.exception_handler(404) +@router.exception_handler(404) async def custom_404_handler(request: Request, exc): return FileResponse("frontend/404.html", status_code=status.HTTP_404_NOT_FOUND) \ No newline at end of file From 657861db26d69ddbdc0a4980b61562cb987cb38c Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 21:20:32 +0100 Subject: [PATCH 13/22] removed fancy 404 --- app/frontend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/frontend.py b/app/frontend.py index 5d8565ec..219ee874 100644 --- a/app/frontend.py +++ b/app/frontend.py @@ -26,6 +26,6 @@ async def serve_upload(request: Request): def favicon(): return os.path.join(frontend_folder, "favicon.ico") -@router.exception_handler(404) +""" @router.exception_handler(404) async def custom_404_handler(request: Request, exc): - return FileResponse("frontend/404.html", status_code=status.HTTP_404_NOT_FOUND) \ No newline at end of file + return FileResponse("frontend/404.html", status_code=status.HTTP_404_NOT_FOUND) """ \ No newline at end of file From e4101f690b809e685fa537cb885ef99f0d8e8b63 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 21:31:32 +0100 Subject: [PATCH 14/22] added gravatar to user and added /api/whoami call --- app/api.py | 32 ++++++++++++++++++++++++++++++++ app/main.py | 19 ++++++++++++++++++- frontend/index.html | 8 ++++++-- 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 app/api.py diff --git a/app/api.py b/app/api.py new file mode 100644 index 00000000..f768e26a --- /dev/null +++ b/app/api.py @@ -0,0 +1,32 @@ +from fastapi import APIRouter, Request, HTTPException, status +from hashlib import md5 + +router = APIRouter() + +@router.get("/whoami") +async def whoami(request: Request): + """ + Returns user info if logged in, else 401. + Example response: + { + "email": "someone@example.com", + "picture": "https://www.gravatar.com/avatar/..." + } + """ + user = request.session.get("user") + if not user: + raise HTTPException(status_code=401, detail="Not logged in") + + email = user.get("email") + if not email: + raise HTTPException(status_code=400, detail="User has no email in session") + + # Generate Gravatar URL from email + # For more options, see: https://en.gravatar.com/site/implement/images/ + email_hash = md5(email.strip().lower().encode()).hexdigest() + gravatar_url = f"https://www.gravatar.com/avatar/{email_hash}?d=identicon" + + return { + "email": email, + "picture": gravatar_url + } \ No newline at end of file diff --git a/app/main.py b/app/main.py index b2d43b0c..654e6cbf 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import os -from fastapi import FastAPI, HTTPException, UploadFile, File +from fastapi import FastAPI, HTTPException, UploadFile, File, status from starlette.middleware.sessions import SessionMiddleware from starlette.config import Config from starlette.middleware.trustedhost import TrustedHostMiddleware @@ -13,9 +13,17 @@ from app.tasks.upload_to_dropbox import upload_to_dropbox from app.tasks.upload_to_paperless import upload_to_paperless from app.tasks.upload_to_nextcloud import upload_to_nextcloud from app.tasks.send_to_all import send_to_all_destinations +from pathlib import Path + +from app.api import router as api_router from app.frontend import router as frontend_router from app.auth import router as auth_router +BASE_DIR = Path(__file__).resolve().parent # this is /app in many Docker setups +FRONTEND_DIR = BASE_DIR / "frontend" + + + # Load configuration from .env for the session key config = Config(".env") SESSION_SECRET = config( @@ -163,6 +171,15 @@ async def ui_upload(file: UploadFile = File(...)): task = upload_to_s3.delay(target_path) return {"task_id": task.id, "status": "queued"} +@app.exception_handler(404) +async def custom_404_handler(request: Request, exc: HTTPException): + return FileResponse( + FRONTEND_DIR / "404.html", + status_code=status.HTTP_404_NOT_FOUND + ) + # Include the frontend and auth routers app.include_router(frontend_router) app.include_router(auth_router) +app.include_router(api_router, prefix="/api") + diff --git a/frontend/index.html b/frontend/index.html index 444e61d4..9f8854ef 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -124,8 +124,12 @@ const resp = await fetch("/api/whoami"); if (resp.ok) { const data = await resp.json(); - document.getElementById("authSection").innerHTML = - `Logged in as ${data.email} Logout`; + // data.picture is the Gravatar URL + document.getElementById("authSection").innerHTML = ` + User Avatar + Logged in as ${data.email} + Logout + `; } else { // If 401 or other non-OK status => show Login link document.getElementById("authSection").innerHTML = From 0003daadb1883f85febb17b98e3a3d7da193b63d Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 21:33:55 +0100 Subject: [PATCH 15/22] fixed import for Request in main.py --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 654e6cbf..8c1f95e1 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import os -from fastapi import FastAPI, HTTPException, UploadFile, File, status +from fastapi import FastAPI, HTTPException, UploadFile, File, status, Request from starlette.middleware.sessions import SessionMiddleware from starlette.config import Config from starlette.middleware.trustedhost import TrustedHostMiddleware From 0f82defc0180379f665313f2ed6e1fa5be32ab16 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 21:57:48 +0100 Subject: [PATCH 16/22] added more pages and changed project name to DocuNova --- app/frontend.py | 12 ++- app/main.py | 6 +- frontend/404.html | 79 +++++++++++--- frontend/about.html | 115 +++++++++++++++++++++ frontend/index.html | 238 +++++++++++++++++-------------------------- frontend/upload.html | 133 ++++++++++++++++++++++++ 6 files changed, 419 insertions(+), 164 deletions(-) create mode 100644 frontend/about.html create mode 100644 frontend/upload.html diff --git a/app/frontend.py b/app/frontend.py index 219ee874..f6859d79 100644 --- a/app/frontend.py +++ b/app/frontend.py @@ -19,7 +19,7 @@ router.mount("/static", StaticFiles(directory=frontend_folder), name="static") @router.get("/upload", response_class=FileResponse) @require_login async def serve_upload(request: Request): - return os.path.join(frontend_folder, "index.html") + return os.path.join(frontend_folder, "upload.html") # 3) Serve favicon.ico from the frontend folder @router.get("/favicon.ico", response_class=FileResponse) @@ -28,4 +28,12 @@ def favicon(): """ @router.exception_handler(404) async def custom_404_handler(request: Request, exc): - return FileResponse("frontend/404.html", status_code=status.HTTP_404_NOT_FOUND) """ \ No newline at end of file + return FileResponse("frontend/404.html", status_code=status.HTTP_404_NOT_FOUND) """ + +@router.get("/", response_class=FileResponse) +async def serve_upload(request: Request): + return os.path.join(frontend_folder, "index.html") + +@router.get("/about", response_class=FileResponse) +async def serve_upload(request: Request): + return os.path.join(frontend_folder, "about.html") \ No newline at end of file diff --git a/app/main.py b/app/main.py index 8c1f95e1..e252b71d 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import os -from fastapi import FastAPI, HTTPException, UploadFile, File, status, Request +from fastapi import FastAPI, HTTPException, UploadFile, File, status, Request, FileResponse from starlette.middleware.sessions import SessionMiddleware from starlette.config import Config from starlette.middleware.trustedhost import TrustedHostMiddleware @@ -52,9 +52,7 @@ app.add_middleware(TrustedHostMiddleware, allowed_hosts=[ def on_startup(): init_db() # Create tables if they don't exist -@app.get("/") -def root(): - return {"message": "Document Processing API"} + @app.post("/process/") def process(file_path: str): diff --git a/frontend/404.html b/frontend/404.html index 20086e75..d953dd52 100644 --- a/frontend/404.html +++ b/frontend/404.html @@ -2,20 +2,75 @@ - 404 - Page not found + 404 - Oops, DocuNova Lost the Page + - -

404 - Page not found

-

- Sorry, we couldn’t find the page you’re looking for. -

- - ← Back to home - + + + + + +
+
+

404

+

Oops, we couldn’t find that page!

+

+ It seems DocuNova has misplaced the document you were looking for. Whether it got lost in the cloud or hidden between the files, don’t worry – we’ve got your back. +

+ + ← Return Home + +
+
+ + +
+
+ © 2025 DocuNova. All rights reserved. +
+
+ + + diff --git a/frontend/about.html b/frontend/about.html new file mode 100644 index 00000000..eb2d4658 --- /dev/null +++ b/frontend/about.html @@ -0,0 +1,115 @@ + + + + + About DocuNova + + + + + + + + + +
+

About DocuNova

+

+ Welcome to DocuNova – your modern, intelligent solution for document processing! We’ve built DocuNova to completely transform the way you handle your documents – from upload to extraction, from processing to storage. With cutting-edge technologies and a user-first design, DocuNova makes managing your documents as easy as a click. +

+ + +
+

Our Story

+

+ DocuNova was created with one goal in mind: to simplify and streamline document management for everyone, whether you’re a small startup or a large enterprise. Tired of clunky, outdated systems, we set out to design a platform that is intuitive, flexible, and packed with powerful features. +

+

+ We harness the power of OpenAI for metadata extraction and text refinement, integrate seamlessly with Dropbox, Nextcloud, and Paperless NGX for storage and indexing, leverage Azure Document Intelligence for OCR, and even use Gotenberg for file-to-PDF conversions. And while we’ve implemented AWS S3 for now, we’re always evolving! +

+
+ + +
+

Key Features

+
    +
  • Simple and secure file uploads with drag & drop support
  • +
  • Automated metadata extraction, indexing, and version control
  • +
  • Integration with popular cloud services and storage platforms
  • +
  • OCR and intelligent document processing powered by AI
  • +
  • IMAP integration for automated document fetching
  • +
  • Highly configurable via environment variables for custom workflows
  • +
  • Docker-ready for easy deployment and scalability
  • +
+
+ + +
+

Meet the Creator

+

+ DocuNova is passionately developed by Christian Krakau-Louis, a visionary committed to solving real-world challenges with innovative technology. His dedication and expertise ensure that every feature in DocuNova is crafted with you in mind. +

+
+ + +
+

Get Involved

+

+ Want to dive into the code, contribute ideas, or simply check out the magic behind DocuNova? Visit our GitHub repository to see the project in action! +

+ + GitHub Logo + View DocuNova on GitHub + +
+
+ + +
+
+ © 2025 DocuNova. All rights reserved. +
+
+ + + + + diff --git a/frontend/index.html b/frontend/index.html index 9f8854ef..06d56a91 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,146 +1,92 @@ - - - - - Document Processor - Upload - - - - - - - - - - -
-

Upload a File

- -
-

- Drag & drop a file here, or click to select a file. -

- -
- -
-
- - -
-
- © 2025 Document Processor - All rights reserved. -
-
- - - - - + + + + + DocuNova - Intelligent Document Processing + + + + + + + + + +
+

Welcome to DocuNova

+

+ Your intelligent solution for processing, managing, and organizing documents effortlessly. +

+ +
+
+

Upload Documents

+

+ Quickly upload and process your files with our user-friendly interface. +

+ + Get Started → + +
+
+

Manage Your Files

+

+ View, organize, and collaborate on your processed files in one central hub. +

+ + View Files → + +
+
+
+ + +
+
+ © 2025 DocuNova. All rights reserved. +
+
+ + + + + diff --git a/frontend/upload.html b/frontend/upload.html new file mode 100644 index 00000000..2f981202 --- /dev/null +++ b/frontend/upload.html @@ -0,0 +1,133 @@ + + + + + DocuNova - Upload + + + + + + + + + +
+

Upload a File

+ +
+

+ Drag & drop a file here, or click to select a file. +

+ +
+ +
+
+ + +
+
+ © 2025 DocuNova. All rights reserved. +
+
+ + + + + From 9570d499b483a377833dfbad684ee49da2eba115 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 22:02:19 +0100 Subject: [PATCH 17/22] change import --- app/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index e252b71d..1d6697af 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 import os -from fastapi import FastAPI, HTTPException, UploadFile, File, status, Request, FileResponse +from fastapi import FastAPI, HTTPException, UploadFile, File, status, Request +from fastapi.responses import FileResponse from starlette.middleware.sessions import SessionMiddleware from starlette.config import Config from starlette.middleware.trustedhost import TrustedHostMiddleware From cd1230b8beb285b5014d3c9746cf14dc2d872797 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 22:04:40 +0100 Subject: [PATCH 18/22] Update app/frontend.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- app/frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend.py b/app/frontend.py index f6859d79..28a49734 100644 --- a/app/frontend.py +++ b/app/frontend.py @@ -31,7 +31,7 @@ async def custom_404_handler(request: Request, exc): return FileResponse("frontend/404.html", status_code=status.HTTP_404_NOT_FOUND) """ @router.get("/", response_class=FileResponse) -async def serve_upload(request: Request): +async def serve_index(request: Request): return os.path.join(frontend_folder, "index.html") @router.get("/about", response_class=FileResponse) From f50177bfbf8a9e53256c671433f312e44c2f4850 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 22:04:53 +0100 Subject: [PATCH 19/22] Update app/frontend.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- app/frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend.py b/app/frontend.py index 28a49734..9cc25798 100644 --- a/app/frontend.py +++ b/app/frontend.py @@ -35,5 +35,5 @@ async def serve_index(request: Request): return os.path.join(frontend_folder, "index.html") @router.get("/about", response_class=FileResponse) -async def serve_upload(request: Request): +async def serve_about(request: Request): return os.path.join(frontend_folder, "about.html") \ No newline at end of file From efa4c9c3cd0be9732679c322cdae1431fb06316b Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 22:05:10 +0100 Subject: [PATCH 20/22] Update frontend/upload.html Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- frontend/upload.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/upload.html b/frontend/upload.html index 2f981202..d03e3bd0 100644 --- a/frontend/upload.html +++ b/frontend/upload.html @@ -90,6 +90,12 @@ } async function uploadFile(file) { + // Check file type + const acceptedTypes = ['application/pdf']; + if (!acceptedTypes.includes(file.type)) { + statusMessage.textContent = `Error: Only PDF files are accepted.`; + return; + } statusMessage.textContent = `Uploading ${file.name}...`; try { let formData = new FormData(); From 426e3af0f6ab803bd0296b62cee2aadb4b2ec152 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 22:05:43 +0100 Subject: [PATCH 21/22] Update frontend/index.html Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- frontend/index.html | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 06d56a91..6c183692 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -73,11 +73,27 @@ if (resp.ok) { const data = await resp.json(); // data.picture is expected to be a Gravatar URL - document.getElementById("authSection").innerHTML = ` - User Avatar - Logged in as ${data.email} - Logout - `; + const authSection = document.getElementById("authSection"); + authSection.innerHTML = ''; + + const img = document.createElement('img'); + img.src = data.picture; + img.alt = 'User Avatar'; + img.className = 'inline-block h-8 w-8 rounded-full mr-2'; + + const textNode = document.createTextNode('Logged in as '); + const strong = document.createElement('strong'); + strong.textContent = data.email; + + const logoutLink = document.createElement('a'); + logoutLink.href = '/logout'; + logoutLink.className = 'ml-4 text-blue-600 hover:text-blue-800'; + logoutLink.textContent = 'Logout'; + + authSection.appendChild(img); + authSection.appendChild(textNode); + authSection.appendChild(strong); + authSection.appendChild(logoutLink); } else { document.getElementById("authSection").innerHTML = `Login`; From ff1f6e042cac320c7c09755d763a7eda766cb611 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 25 Mar 2025 22:11:44 +0100 Subject: [PATCH 22/22] fixed 404 --- app/main.py | 6 +---- frontend/404.html | 61 ++++++++++++++++++++++++++---------------- frontend/about.html | 63 ++++++++++++++++++++++++++++---------------- frontend/upload.html | 30 ++++++++++++++++----- 4 files changed, 104 insertions(+), 56 deletions(-) diff --git a/app/main.py b/app/main.py index 1d6697af..d255c451 100644 --- a/app/main.py +++ b/app/main.py @@ -20,10 +20,6 @@ from app.api import router as api_router from app.frontend import router as frontend_router from app.auth import router as auth_router -BASE_DIR = Path(__file__).resolve().parent # this is /app in many Docker setups -FRONTEND_DIR = BASE_DIR / "frontend" - - # Load configuration from .env for the session key config = Config(".env") @@ -173,7 +169,7 @@ async def ui_upload(file: UploadFile = File(...)): @app.exception_handler(404) async def custom_404_handler(request: Request, exc: HTTPException): return FileResponse( - FRONTEND_DIR / "404.html", + "/app/frontend/404.html", status_code=status.HTTP_404_NOT_FOUND ) diff --git a/frontend/404.html b/frontend/404.html index d953dd52..c085430d 100644 --- a/frontend/404.html +++ b/frontend/404.html @@ -50,27 +50,44 @@ - - + + diff --git a/frontend/about.html b/frontend/about.html index eb2d4658..718b8513 100644 --- a/frontend/about.html +++ b/frontend/about.html @@ -65,7 +65,7 @@

Meet the Creator

- DocuNova is passionately developed by Christian Krakau-Louis, a visionary committed to solving real-world challenges with innovative technology. His dedication and expertise ensure that every feature in DocuNova is crafted with you in mind. + DocuNova is passionately developed by Christian Krakau-Louis, a visionary committed to solving real-world challenges with innovative technology. His dedication and expertise ensure that every feature in DocuNova is crafted with you in mind.

@@ -89,27 +89,44 @@ - - + + diff --git a/frontend/upload.html b/frontend/upload.html index d03e3bd0..eec9ca34 100644 --- a/frontend/upload.html +++ b/frontend/upload.html @@ -114,17 +114,35 @@ } } - // Dynamic auth check using /api/whoami + + (async function checkAuth() { try { const resp = await fetch("/api/whoami"); if (resp.ok) { const data = await resp.json(); - document.getElementById("authSection").innerHTML = ` - User Avatar - Logged in as ${data.email} - Logout - `; + // data.picture is expected to be a Gravatar URL + const authSection = document.getElementById("authSection"); + authSection.innerHTML = ''; + + const img = document.createElement('img'); + img.src = data.picture; + img.alt = 'User Avatar'; + img.className = 'inline-block h-8 w-8 rounded-full mr-2'; + + const textNode = document.createTextNode('Logged in as '); + const strong = document.createElement('strong'); + strong.textContent = data.email; + + const logoutLink = document.createElement('a'); + logoutLink.href = '/logout'; + logoutLink.className = 'ml-4 text-blue-600 hover:text-blue-800'; + logoutLink.textContent = 'Logout'; + + authSection.appendChild(img); + authSection.appendChild(textNode); + authSection.appendChild(strong); + authSection.appendChild(logoutLink); } else { document.getElementById("authSection").innerHTML = `Login`;