Merge pull request #20 from christianlouis/file-view

added the /files view and auth for api and files
This commit is contained in:
Christian Krakau-Louis
2025-03-26 17:28:40 +01:00
committed by GitHub
7 changed files with 184 additions and 12 deletions
+52 -8
View File
@@ -1,17 +1,26 @@
from fastapi import APIRouter, Request, HTTPException, status # app/api.py
from fastapi import APIRouter, Request, HTTPException, status, Depends
from hashlib import md5 from hashlib import md5
from sqlalchemy.orm import Session
from typing import List
from app.auth import require_login
from app.database import SessionLocal
from app.models import FileRecord
router = APIRouter() router = APIRouter()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@router.get("/whoami") @router.get("/whoami")
async def whoami(request: Request): async def whoami(request: Request):
""" """
Returns user info if logged in, else 401. 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") user = request.session.get("user")
if not user: if not user:
@@ -22,11 +31,46 @@ async def whoami(request: Request):
raise HTTPException(status_code=400, detail="User has no email in session") raise HTTPException(status_code=400, detail="User has no email in session")
# Generate Gravatar URL from email # Generate Gravatar URL from email
# For more options, see: https://en.gravatar.com/site/implement/images/
email_hash = md5(email.strip().lower().encode()).hexdigest() email_hash = md5(email.strip().lower().encode()).hexdigest()
gravatar_url = f"https://www.gravatar.com/avatar/{email_hash}?d=identicon" gravatar_url = f"https://www.gravatar.com/avatar/{email_hash}?d=identicon"
return { return {
"email": email, "email": email,
"picture": gravatar_url "picture": gravatar_url
} }
@router.get("/files")
@require_login
def list_files_api(request: Request, db: Session = Depends(get_db)):
"""
Returns a JSON list of all FileRecord entries.
Protected by `@require_login`, so only logged-in sessions can access.
Example response:
[
{
"id": 123,
"filehash": "abc123...",
"original_filename": "example.pdf",
"local_filename": "/workdir/tmp/<uuid>.pdf",
"file_size": 1048576,
"mime_type": "application/pdf",
"created_at": "2025-05-01T12:34:56.789000"
},
...
]
"""
files = db.query(FileRecord).order_by(FileRecord.created_at.desc()).all()
# Return a simple list of dicts
result = []
for f in files:
result.append({
"id": f.id,
"filehash": f.filehash,
"original_filename": f.original_filename,
"local_filename": f.local_filename,
"file_size": f.file_size,
"mime_type": f.mime_type,
"created_at": f.created_at.isoformat() if f.created_at else None
})
return result
+6 -2
View File
@@ -1,5 +1,5 @@
# app/auth.py
import os import os
import inspect
from functools import wraps from functools import wraps
from authlib.integrations.starlette_client import OAuth from authlib.integrations.starlette_client import OAuth
@@ -37,7 +37,11 @@ def require_login(func):
if not request.session.get("user"): if not request.session.get("user"):
request.session["redirect_after_login"] = str(request.url) request.session["redirect_after_login"] = str(request.url)
return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
return await func(request, *args, **kwargs) # Check if the wrapped function is a coroutine function
if inspect.iscoroutinefunction(func):
return await func(request, *args, **kwargs)
else:
return func(request, *args, **kwargs)
return wrapper return wrapper
+23 -2
View File
@@ -1,16 +1,37 @@
# app/frontend.py # app/frontend.py
import os import os
from pathlib import Path from pathlib import Path
from fastapi import APIRouter, Request from fastapi import APIRouter, Request, Depends
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from app.auth import require_login from app.auth import require_login
from app.database import SessionLocal
router = APIRouter() router = APIRouter()
# Point templates_dir to "frontend/templates"
templates_dir = Path(__file__).parent.parent / "frontend" / "templates" templates_dir = Path(__file__).parent.parent / "frontend" / "templates"
templates = Jinja2Templates(directory=str(templates_dir)) templates = Jinja2Templates(directory=str(templates_dir))
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@router.get("/files")
@require_login
def files_page(request: Request):
"""
Return the 'files.html' template.
The actual file data is fetched via XHR from /api/files in the template.
"""
return templates.TemplateResponse("files.html", {"request": request})
# ... existing routes for /, /upload, /about, etc. ...
@router.get("/", include_in_schema=False) @router.get("/", include_in_schema=False)
async def serve_index(request: Request): async def serve_index(request: Request):
return templates.TemplateResponse("index.html", {"request": request}) return templates.TemplateResponse("index.html", {"request": request})
+14
View File
@@ -180,6 +180,20 @@ async def custom_404_handler(request: Request, exc: HTTPException):
status_code=status.HTTP_404_NOT_FOUND status_code=status.HTTP_404_NOT_FOUND
) )
@app.exception_handler(500)
async def custom_500_handler(request: Request, exc: Exception):
templates = Jinja2Templates(directory=str(frontend_static_dir.parent / "templates"))
# Option 1: Keep it simple, just show a funny 500 message:
return templates.TemplateResponse(
"500.html",
{"request": request, "exc": exc},
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@app.get("/test-500")
def test_500():
raise RuntimeError("Testing forced 500 error!")
# Include the frontend and auth routers # Include the frontend and auth routers
app.include_router(frontend_router) app.include_router(frontend_router)
app.include_router(auth_router) app.include_router(auth_router)
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

+43
View File
@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}Server Error - DocuNova{% endblock %}
{% block content %}
<div class="relative isolate px-6 pt-14 lg:px-8">
<!-- Hero Image -->
<div class="mx-auto max-w-3xl py-12">
<img
src="/static/explosion.jpg"
alt="Server Explosion"
class="mx-auto rounded-xl shadow-xl h-[300px] w-full object-cover"
/>
</div>
<div class="mx-auto max-w-3xl text-center">
<h1 class="text-7xl font-bold text-red-500">500</h1>
<p class="mt-4 text-4xl font-semibold tracking-tight text-gray-900">
Oops! Something Went Wrong.
</p>
<p class="mt-4 text-lg leading-8 text-gray-600">
Our servers encountered a mishap and need a moment. Maybe the hamsters spilled their coffee?
</p>
<p class="mt-2 text-lg leading-8 text-gray-600">
We're already on it—sorting files, rebooting servers, and recalibrating flux capacitors.
</p>
{% if exc %}
<details class="mt-8 bg-gray-100 rounded-lg p-4 text-left shadow-md">
<summary class="cursor-pointer font-medium text-blue-600 hover:text-blue-700">
Show Debug Info
</summary>
<pre class="mt-4 text-sm text-gray-700 overflow-auto">{{ exc }}</pre>
</details>
{% endif %}
<div class="mt-10">
<a href="/" class="inline-block bg-blue-600 px-6 py-3 rounded-md text-white font-semibold shadow-md hover:bg-blue-700 transition-colors">
&larr; Go Home
</a>
</div>
</div>
</div>
{% endblock %}
+46
View File
@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}File Records{% endblock %}
{% block head_extra %}
<!-- Include Grid.js CSS -->
<link href="https://unpkg.com/gridjs/dist/theme/mermaid.min.css" rel="stylesheet" />
{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 py-8">
<h2 class="text-3xl font-bold mb-6">File Records</h2>
<!-- Grid.js will render the table in this container -->
<div id="gridjs-wrapper"></div>
</div>
{% endblock %}
{% block scripts %}
<!-- Include Grid.js JS -->
<script src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script>
<script>
new gridjs.Grid({
columns: [
{ id: 'id', name: 'ID' },
{ id: 'original_filename', name: 'Original Filename' },
{ id: 'file_size', name: 'File Size' },
{ id: 'mime_type', name: 'Mime Type' },
{ id: 'created_at', name: 'Created At' }
],
server: {
url: '/api/files',
then: data => data.map(file => [
file.id,
file.original_filename || "",
file.file_size,
file.mime_type,
file.created_at || ""
])
},
search: true,
sort: true,
pagination: {
limit: 10
}
}).render(document.getElementById("gridjs-wrapper"));
</script>
{% endblock %}