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 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
@@ -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
@@ -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
@@ -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 |
@@ -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