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 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()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@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:
@@ -22,11 +31,46 @@ async def whoami(request: Request):
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
}
}
@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 inspect
from functools import wraps
from authlib.integrations.starlette_client import OAuth
@@ -37,7 +37,11 @@ def require_login(func):
if not request.session.get("user"):
request.session["redirect_after_login"] = str(request.url)
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
+23 -2
View File
@@ -1,16 +1,37 @@
# app/frontend.py
import os
from pathlib import Path
from fastapi import APIRouter, Request
from fastapi import APIRouter, Request, Depends
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from app.auth import require_login
from app.database import SessionLocal
router = APIRouter()
# Point templates_dir to "frontend/templates"
templates_dir = Path(__file__).parent.parent / "frontend" / "templates"
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)
async def serve_index(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
)
@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
app.include_router(frontend_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 %}