Merge pull request #20 from christianlouis/file-view
added the /files view and auth for api and files
This commit is contained in:
+52
-8
@@ -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
@@ -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
@@ -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
@@ -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 |
@@ -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">
|
||||
← Go Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user