Molodetz blogrol volgens DPP-template
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
@@ -0,0 +1,24 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from molodetz.constants import MEDIA_KINDS
|
||||
from molodetz.gallery import gallery_items
|
||||
from molodetz.responses import respond
|
||||
from molodetz.schemas.gallery import GalleryOut
|
||||
from molodetz.seo import base_seo_context
|
||||
from molodetz.seo_meta_text import description_for
|
||||
from molodetz.templating import static_url
|
||||
|
||||
|
||||
def gallery_page(request, kind, slug):
|
||||
title = MEDIA_KINDS[kind]
|
||||
items = gallery_items(kind)
|
||||
for item in items:
|
||||
item["url"] = static_url(item["source_path"])
|
||||
first = items[0]["url"] if items else None
|
||||
seo = base_seo_context(
|
||||
request,
|
||||
title,
|
||||
description_for(slug),
|
||||
og_image=first,
|
||||
breadcrumbs=[("Molodetz", "/"), (title, f"/{slug}")],
|
||||
)
|
||||
return respond(request, "gallery.html", {"kind": kind, "title": title, "items": items, "seo": seo}, model=GalleryOut)
|
||||
@@ -0,0 +1,25 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from molodetz.constants import TOPICS
|
||||
from molodetz.database import published_posts
|
||||
from molodetz.responses import respond
|
||||
from molodetz.schemas.posts import PostListOut
|
||||
from molodetz.seo import base_seo_context, website_schema
|
||||
from molodetz.seo_meta_text import description_for
|
||||
|
||||
|
||||
def topic_listing(request, topic):
|
||||
before = request.query_params.get("before")
|
||||
posts, next_cursor = published_posts(topic=topic, before=before, limit=25)
|
||||
title = TOPICS[topic]
|
||||
next_url = f"{request.url.path}?before={next_cursor}" if next_cursor else None
|
||||
seo = base_seo_context(
|
||||
request,
|
||||
title,
|
||||
description_for(topic),
|
||||
breadcrumbs=[("Molodetz", "/"), (title, f"/{topic}")],
|
||||
schemas=[website_schema(request)],
|
||||
next_url=next_url,
|
||||
robots="noindex,follow" if before else "index,follow",
|
||||
)
|
||||
ctx = {"title": title, "topic": topic, "posts": posts, "next_cursor": next_cursor, "next_url": next_url, "seo": seo}
|
||||
return respond(request, "listing.html", ctx, model=PostListOut)
|
||||
@@ -0,0 +1,13 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from molodetz.routers.admin import audit, backups, joins, posts, services, settings, stats, trash, users
|
||||
from molodetz.routers.admin.index import router
|
||||
|
||||
router.include_router(posts.router)
|
||||
router.include_router(joins.router)
|
||||
router.include_router(services.router)
|
||||
router.include_router(trash.router)
|
||||
router.include_router(settings.router)
|
||||
router.include_router(users.router)
|
||||
router.include_router(audit.router)
|
||||
router.include_router(backups.router)
|
||||
router.include_router(stats.router)
|
||||
@@ -0,0 +1,6 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from molodetz.seo import base_seo_context
|
||||
|
||||
|
||||
def admin_seo(request, title):
|
||||
return base_seo_context(request, title, robots="noindex,nofollow")
|
||||
@@ -0,0 +1,17 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.database import list_audit
|
||||
from molodetz.responses import respond
|
||||
from molodetz.routers.admin._shared import admin_seo
|
||||
from molodetz.schemas.admin import AuditOut
|
||||
from molodetz.utils.auth import require_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/audit")
|
||||
async def audit_index(request: Request):
|
||||
require_admin(request)
|
||||
category = request.query_params.get("category") or None
|
||||
return respond(request, "admin/audit.html", {"rows": list_audit(200, category), "category": category, "seo": admin_seo(request, "Auditlog")}, model=AuditOut)
|
||||
@@ -0,0 +1,60 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
import aiofiles
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from molodetz.attachments import format_size
|
||||
from molodetz.database import enqueue_backup, get_backup, list_backups
|
||||
from molodetz.dependencies import json_or_form
|
||||
from molodetz.models import BackupForm
|
||||
from molodetz.responses import action_result, respond
|
||||
from molodetz.routers.admin._shared import admin_seo
|
||||
from molodetz.schemas.admin import BackupsOut
|
||||
from molodetz.services.backup import backup_path
|
||||
from molodetz.utils.audit import record
|
||||
from molodetz.utils.auth import is_primary_admin, not_found, require_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/backups")
|
||||
async def backups_index(request: Request):
|
||||
user = require_admin(request)
|
||||
rows = [{**row, "size_label": format_size(row.get("size_bytes"))} for row in list_backups()]
|
||||
ctx = {"backups": rows, "can_download": is_primary_admin(user), "seo": admin_seo(request, "Back-ups")}
|
||||
return respond(request, "admin/backups.html", ctx, model=BackupsOut)
|
||||
|
||||
|
||||
@router.post("/backups")
|
||||
async def backups_enqueue(request: Request):
|
||||
user = require_admin(request)
|
||||
data = await json_or_form(request, BackupForm)
|
||||
row = enqueue_backup(data.target, user["uid"])
|
||||
record(request, "backup.enqueue", payload={"target": data.target}, targets=[("backup", row["uid"])], user=user)
|
||||
return action_result(request, "/admin/backups", data={"uid": row["uid"]})
|
||||
|
||||
|
||||
@router.get("/backups/{uid}/download")
|
||||
async def backups_download(request: Request, uid: str):
|
||||
user = require_admin(request)
|
||||
if not is_primary_admin(user):
|
||||
record(request, "backup.download", result="denied", targets=[("backup", uid)], user=user)
|
||||
raise HTTPException(403, "Alleen de primaire beheerder")
|
||||
row = get_backup(uid)
|
||||
if row is None or row.get("status") != "done":
|
||||
not_found("Back-up niet gevonden")
|
||||
path = backup_path(row)
|
||||
if not path.exists():
|
||||
not_found("Archief ontbreekt")
|
||||
record(request, "backup.download", targets=[("backup", uid)], user=user)
|
||||
return StreamingResponse(
|
||||
_stream_file(path),
|
||||
media_type="application/octet-stream",
|
||||
headers={"Content-Disposition": f'attachment; filename="{row["stored_name"]}"', "Content-Length": str(path.stat().st_size)},
|
||||
)
|
||||
|
||||
|
||||
async def _stream_file(path, chunk_size=1024 * 256):
|
||||
async with aiofiles.open(path, "rb") as handle:
|
||||
while chunk := await handle.read(chunk_size):
|
||||
yield chunk
|
||||
@@ -0,0 +1,24 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.constants import SOFT_DELETE_TABLES
|
||||
from molodetz.database import all_posts, count_deleted, count_open_join_requests, count_users
|
||||
from molodetz.responses import respond
|
||||
from molodetz.routers.admin._shared import admin_seo
|
||||
from molodetz.schemas.admin import AdminIndexOut
|
||||
from molodetz.utils.auth import require_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def admin_index(request: Request):
|
||||
require_admin(request)
|
||||
ctx = {
|
||||
"post_count": len(all_posts()),
|
||||
"open_joins": count_open_join_requests(),
|
||||
"user_count": count_users(),
|
||||
"deleted_count": sum(count_deleted(name) for name in SOFT_DELETE_TABLES),
|
||||
"seo": admin_seo(request, "Beheer"),
|
||||
}
|
||||
return respond(request, "admin/index.html", ctx, model=AdminIndexOut)
|
||||
@@ -0,0 +1,43 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.database import get_join_request, list_join_requests, update_join_request
|
||||
from molodetz.dependencies import json_or_form
|
||||
from molodetz.link_check import check_repo_link
|
||||
from molodetz.models import JoinStatusForm
|
||||
from molodetz.responses import action_result, respond
|
||||
from molodetz.routers.admin._shared import admin_seo
|
||||
from molodetz.schemas.joins import JoinListOut
|
||||
from molodetz.utils.audit import record
|
||||
from molodetz.utils.auth import not_found, require_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/joins")
|
||||
async def joins_index(request: Request):
|
||||
require_admin(request)
|
||||
return respond(request, "admin/joins.html", {"requests": list_join_requests(), "seo": admin_seo(request, "Aanmeldingen")}, model=JoinListOut)
|
||||
|
||||
|
||||
@router.post("/joins/{uid}/status")
|
||||
async def joins_status(request: Request, uid: str):
|
||||
user = require_admin(request)
|
||||
row = get_join_request(uid)
|
||||
if row is None:
|
||||
not_found("Aanmelding niet gevonden")
|
||||
data = await json_or_form(request, JoinStatusForm)
|
||||
update_join_request(uid, status=data.status)
|
||||
record(request, "join.request.status", payload={"status": data.status}, targets=[("join_request", uid)], user=user)
|
||||
return action_result(request, "/admin/joins", data={"uid": uid, "status": data.status})
|
||||
|
||||
|
||||
@router.post("/joins/{uid}/check")
|
||||
async def joins_check(request: Request, uid: str):
|
||||
user = require_admin(request)
|
||||
row = get_join_request(uid)
|
||||
if row is None:
|
||||
not_found("Aanmelding niet gevonden")
|
||||
result = await check_repo_link(row)
|
||||
record(request, "join.request.check", payload=result, targets=[("join_request", uid)], user=user)
|
||||
return action_result(request, "/admin/joins", data=result)
|
||||
@@ -0,0 +1,88 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
|
||||
from molodetz.constants import TOPICS
|
||||
from molodetz.content import create_post, delete_post, edit_post
|
||||
from molodetz.database import all_posts, get_post
|
||||
from molodetz.dependencies import json_or_form
|
||||
from molodetz.models import PostForm
|
||||
from molodetz.responses import action_result, respond
|
||||
from molodetz.routers.admin._shared import admin_seo
|
||||
from molodetz.schemas.admin import AdminPostsOut
|
||||
from molodetz.schemas.posts import PostDetailOut
|
||||
from molodetz.utils.audit import record
|
||||
from molodetz.utils.auth import is_admin, is_owner, not_found, require_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/posts")
|
||||
async def posts_index(request: Request):
|
||||
require_admin(request)
|
||||
return respond(request, "admin/posts.html", {"posts": all_posts(), "seo": admin_seo(request, "Berichten")}, model=AdminPostsOut)
|
||||
|
||||
|
||||
@router.get("/posts/new")
|
||||
async def posts_new(request: Request):
|
||||
require_admin(request)
|
||||
ctx = {"post": None, "topics": TOPICS, "errors": [], "seo": admin_seo(request, "Nieuw bericht")}
|
||||
return respond(request, "admin/post_form.html", ctx, model=PostDetailOut)
|
||||
|
||||
|
||||
@router.post("/posts")
|
||||
async def posts_create(request: Request):
|
||||
user = require_admin(request)
|
||||
data = await json_or_form(request, PostForm)
|
||||
row = create_post(user, data.title, data.body, data.topic, data.status, data.is_placeholder, request=request)
|
||||
return action_result(request, "/admin/posts", data={"uid": row["uid"], "slug": row["slug"]})
|
||||
|
||||
|
||||
@router.get("/posts/{uid}/edit")
|
||||
async def posts_edit(request: Request, uid: str):
|
||||
require_admin(request)
|
||||
post = get_post(uid)
|
||||
if post is None:
|
||||
not_found("Bericht niet gevonden")
|
||||
ctx = {"post": post, "topics": TOPICS, "errors": [], "seo": admin_seo(request, "Bericht bewerken")}
|
||||
return respond(request, "admin/post_form.html", ctx, model=PostDetailOut)
|
||||
|
||||
|
||||
def _owned_post(request, user, uid):
|
||||
post = get_post(uid)
|
||||
if post is None:
|
||||
not_found("Bericht niet gevonden")
|
||||
if not is_owner(post, user):
|
||||
record(request, "post.edit.denied", result="denied", targets=[("post", uid)], user=user)
|
||||
raise HTTPException(403, "Alleen de auteur mag dit bericht bewerken")
|
||||
return post
|
||||
|
||||
|
||||
@router.post("/posts/{uid}")
|
||||
async def posts_update(request: Request, uid: str):
|
||||
user = require_admin(request)
|
||||
post = _owned_post(request, user, uid)
|
||||
data = await json_or_form(request, PostForm)
|
||||
edit_post(post, user, data.title, data.body, data.topic, data.status, data.is_placeholder, request=request)
|
||||
return action_result(request, "/admin/posts", data={"uid": uid})
|
||||
|
||||
|
||||
@router.post("/posts/{uid}/publish")
|
||||
async def posts_publish(request: Request, uid: str):
|
||||
user = require_admin(request)
|
||||
post = _owned_post(request, user, uid)
|
||||
status = "draft" if post["status"] == "published" else "published"
|
||||
edit_post(post, user, post["title"], post["body"], post["topic"], status, post.get("is_placeholder"), request=request)
|
||||
return action_result(request, "/admin/posts", data={"uid": uid, "status": status})
|
||||
|
||||
|
||||
@router.post("/posts/{uid}/delete")
|
||||
async def posts_delete(request: Request, uid: str):
|
||||
user = require_admin(request)
|
||||
post = get_post(uid)
|
||||
if post is None:
|
||||
not_found("Bericht niet gevonden")
|
||||
if not (is_owner(post, user) or is_admin(user)):
|
||||
record(request, "post.delete.denied", result="denied", targets=[("post", uid)], user=user)
|
||||
raise HTTPException(403, "Niet toegestaan")
|
||||
stamp = delete_post(post, user, request=request)
|
||||
return action_result(request, "/admin/posts", data={"uid": uid, "stamp": stamp})
|
||||
@@ -0,0 +1,69 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
|
||||
from molodetz.responses import action_result, respond
|
||||
from molodetz.routers.admin._shared import admin_seo
|
||||
from molodetz.schemas.admin import ServiceDetailOut, ServicesOut
|
||||
from molodetz.services.base import manager
|
||||
from molodetz.utils.audit import record
|
||||
from molodetz.utils.auth import not_found, require_admin
|
||||
|
||||
router = APIRouter()
|
||||
VERBS = ("start", "stop", "run", "clear")
|
||||
|
||||
|
||||
def _service(name):
|
||||
service = manager.get(name)
|
||||
if service is None:
|
||||
not_found("Onbekende service")
|
||||
return service
|
||||
|
||||
|
||||
@router.get("/services")
|
||||
async def services_index(request: Request):
|
||||
require_admin(request)
|
||||
return respond(request, "admin/services.html", {"services": manager.describe_all(), "seo": admin_seo(request, "Services")}, model=ServicesOut)
|
||||
|
||||
|
||||
@router.get("/services/{name}")
|
||||
async def services_detail(request: Request, name: str):
|
||||
require_admin(request)
|
||||
service = _service(name)
|
||||
ctx = {"service": manager.describe(service), "seo": admin_seo(request, service.title)}
|
||||
return respond(request, "admin/service.html", ctx, model=ServiceDetailOut)
|
||||
|
||||
|
||||
async def _payload(request):
|
||||
if request.headers.get("content-type", "").startswith("application/json"):
|
||||
data = await request.json()
|
||||
return data if isinstance(data, dict) else {}
|
||||
form = await request.form()
|
||||
return {key: value for key, value in form.items()}
|
||||
|
||||
|
||||
@router.post("/services/{name}/command")
|
||||
async def services_command(request: Request, name: str):
|
||||
user = require_admin(request)
|
||||
service = _service(name)
|
||||
verb = (await _payload(request)).get("verb", "")
|
||||
if verb not in VERBS:
|
||||
raise HTTPException(400, "Onbekend commando")
|
||||
if verb in ("start", "stop"):
|
||||
manager.set_enabled(service.name, verb == "start")
|
||||
else:
|
||||
manager.send_command(service.name, verb)
|
||||
record(request, f"service.command.{verb}", targets=[("service", service.name)], user=user)
|
||||
return action_result(request, f"/admin/services/{service.name}", data={"verb": verb})
|
||||
|
||||
|
||||
@router.post("/services/{name}/config")
|
||||
async def services_config(request: Request, name: str):
|
||||
user = require_admin(request)
|
||||
service = _service(name)
|
||||
values = await _payload(request)
|
||||
try:
|
||||
updates = manager.save_config(service.name, values)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, str(exc)) from exc
|
||||
record(request, "service.config.save", payload={"keys": sorted(updates)}, targets=[("service", service.name)], user=user)
|
||||
return action_result(request, f"/admin/services/{service.name}", data={"saved": sorted(updates)})
|
||||
@@ -0,0 +1,43 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.database import all_settings, get_setting, set_setting
|
||||
from molodetz.dependencies import json_or_form
|
||||
from molodetz.models import SettingsForm
|
||||
from molodetz.responses import action_result, respond
|
||||
from molodetz.routers.admin._shared import admin_seo
|
||||
from molodetz.schemas.admin import SettingsOut
|
||||
from molodetz.utils.audit import record
|
||||
from molodetz.utils.auth import require_admin
|
||||
|
||||
router = APIRouter()
|
||||
NUMERIC = ("rate_limit_per_minute", "rate_limit_window_seconds", "session_max_age_days", "session_remember_days", "audit_retention_days")
|
||||
BOOLEAN = ("maintenance_mode", "friendly_404")
|
||||
|
||||
|
||||
@router.get("/settings")
|
||||
async def settings_index(request: Request):
|
||||
require_admin(request)
|
||||
return respond(request, "admin/settings.html", {"settings": all_settings(), "seo": admin_seo(request, "Instellingen")}, model=SettingsOut)
|
||||
|
||||
|
||||
@router.post("/settings")
|
||||
async def settings_save(request: Request):
|
||||
user = require_admin(request)
|
||||
data = await json_or_form(request, SettingsForm)
|
||||
changed = []
|
||||
for key, value in data.model_dump(exclude_none=True).items():
|
||||
if value == "":
|
||||
continue
|
||||
if key in NUMERIC:
|
||||
try:
|
||||
value = str(max(1, int(value)))
|
||||
except ValueError:
|
||||
continue
|
||||
if key in BOOLEAN and value not in ("0", "1"):
|
||||
continue
|
||||
if get_setting(key) != value:
|
||||
set_setting(key, value)
|
||||
changed.append(key)
|
||||
record(request, "admin.settings.save", payload={"keys": changed}, user=user)
|
||||
return action_result(request, "/admin/settings", data={"changed": changed})
|
||||
@@ -0,0 +1,17 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.database import daily_totals, top_paths
|
||||
from molodetz.responses import respond
|
||||
from molodetz.routers.admin._shared import admin_seo
|
||||
from molodetz.schemas.admin import StatsOut
|
||||
from molodetz.utils.auth import require_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def stats_index(request: Request):
|
||||
require_admin(request)
|
||||
ctx = {"top_paths": [dict(row) for row in top_paths()], "daily": [dict(row) for row in daily_totals()], "seo": admin_seo(request, "Statistieken")}
|
||||
return respond(request, "admin/stats.html", ctx, model=StatsOut)
|
||||
@@ -0,0 +1,62 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
|
||||
from molodetz.constants import SOFT_DELETE_TABLES
|
||||
from molodetz.database import list_deleted, purge_event, restore_event
|
||||
from molodetz.responses import action_result, respond
|
||||
from molodetz.routers.admin._shared import admin_seo
|
||||
from molodetz.schemas.admin import TrashOut
|
||||
from molodetz.utils.audit import record
|
||||
from molodetz.utils.auth import require_admin
|
||||
|
||||
router = APIRouter()
|
||||
LABEL_FIELDS = ("title", "name", "username", "message", "filename", "token")
|
||||
|
||||
|
||||
def _label(row):
|
||||
for key in LABEL_FIELDS:
|
||||
if row.get(key):
|
||||
return str(row[key])[:24] if key == "token" else str(row[key])[:120]
|
||||
return row.get("uid", "")
|
||||
|
||||
|
||||
@router.get("/trash")
|
||||
async def trash_index(request: Request):
|
||||
require_admin(request)
|
||||
items = []
|
||||
for table in SOFT_DELETE_TABLES:
|
||||
if table == "sessions":
|
||||
continue
|
||||
for row in list_deleted(table):
|
||||
items.append({"table": table, "uid": row.get("uid"), "label": _label(row), "deleted_at": row.get("deleted_at"), "deleted_by": row.get("deleted_by")})
|
||||
items.sort(key=lambda item: item["deleted_at"] or "", reverse=True)
|
||||
return respond(request, "admin/trash.html", {"items": items, "seo": admin_seo(request, "Prullenbak")}, model=TrashOut)
|
||||
|
||||
|
||||
async def _stamp(request):
|
||||
if request.headers.get("content-type", "").startswith("application/json"):
|
||||
data = await request.json()
|
||||
stamp = data.get("stamp") if isinstance(data, dict) else None
|
||||
else:
|
||||
stamp = (await request.form()).get("stamp")
|
||||
if not stamp:
|
||||
raise HTTPException(400, "stamp ontbreekt")
|
||||
return str(stamp)
|
||||
|
||||
|
||||
@router.post("/trash/restore")
|
||||
async def trash_restore(request: Request):
|
||||
user = require_admin(request)
|
||||
stamp = await _stamp(request)
|
||||
tables = restore_event(stamp)
|
||||
record(request, "admin.trash.restore", payload={"stamp": stamp}, user=user)
|
||||
return action_result(request, "/admin/trash", data={"tables": tables})
|
||||
|
||||
|
||||
@router.post("/trash/purge")
|
||||
async def trash_purge(request: Request):
|
||||
user = require_admin(request)
|
||||
stamp = await _stamp(request)
|
||||
purged = purge_event(stamp)
|
||||
record(request, "admin.trash.purge", payload={"stamp": stamp, "tables": [name for name, _ in purged]}, user=user)
|
||||
return action_result(request, "/admin/trash", data={"purged": [name for name, _ in purged]})
|
||||
@@ -0,0 +1,35 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
|
||||
from molodetz.database import get_user, list_users, set_role
|
||||
from molodetz.dependencies import json_or_form
|
||||
from molodetz.models import RoleForm
|
||||
from molodetz.responses import action_result, respond
|
||||
from molodetz.routers.admin._shared import admin_seo
|
||||
from molodetz.schemas.admin import AdminUsersOut
|
||||
from molodetz.utils.audit import record
|
||||
from molodetz.utils.auth import invalidate_user_cache, is_senior_admin, not_found, require_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/users")
|
||||
async def users_index(request: Request):
|
||||
require_admin(request)
|
||||
return respond(request, "admin/users.html", {"users": list_users(), "seo": admin_seo(request, "Accounts")}, model=AdminUsersOut)
|
||||
|
||||
|
||||
@router.post("/users/{uid}/role")
|
||||
async def users_role(request: Request, uid: str):
|
||||
actor = require_admin(request)
|
||||
target = get_user(uid)
|
||||
if target is None:
|
||||
not_found("Account niet gevonden")
|
||||
data = await json_or_form(request, RoleForm)
|
||||
if is_senior_admin(actor, target) or (target["uid"] == actor["uid"] and data.role != "Admin"):
|
||||
record(request, "admin.user.role", result="denied", targets=[("user", uid)], user=actor)
|
||||
raise HTTPException(403, "Je kunt een senior beheerder niet beheren")
|
||||
set_role(uid, data.role)
|
||||
invalidate_user_cache(propagate=True)
|
||||
record(request, "admin.user.role", payload={"role": data.role}, targets=[("user", uid)], user=actor)
|
||||
return action_result(request, "/admin/users", data={"uid": uid, "role": data.role})
|
||||
@@ -0,0 +1,70 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.database import create_session, end_session, get_int_setting, get_user_by_email, get_user_by_username
|
||||
from molodetz.dependencies import json_or_form
|
||||
from molodetz.models import LoginForm
|
||||
from molodetz.responses import action_result, json_error, respond, wants_json
|
||||
from molodetz.schemas.account import LoginPageOut
|
||||
from molodetz.seo import base_seo_context
|
||||
from molodetz.templating import templates
|
||||
from molodetz.utils.audit import record
|
||||
from molodetz.utils.auth import get_current_user, invalidate_user_cache, is_account_active, verify_password_async
|
||||
from molodetz.utils.request_meta import is_secure_request, safe_next
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _seo(request):
|
||||
return base_seo_context(request, "Inloggen", robots="noindex,nofollow")
|
||||
|
||||
|
||||
@router.get("/login")
|
||||
async def login_page(request: Request):
|
||||
next_url = safe_next(request.query_params.get("next"), "/admin")
|
||||
return respond(request, "auth/login.html", {"errors": [], "form": {}, "next": next_url, "seo": _seo(request)}, model=LoginPageOut)
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login_submit(request: Request):
|
||||
data = await json_or_form(request, LoginForm)
|
||||
user = get_user_by_username(data.username) or get_user_by_email(data.username.lower())
|
||||
valid = user is not None and is_account_active(user) and await verify_password_async(data.password, user.get("password_hash"))
|
||||
if not valid:
|
||||
record(request, "auth.login.failed", result="denied", payload={"username": data.username[:64]})
|
||||
ctx = {
|
||||
"errors": ["Onbekende combinatie van naam en wachtwoord."],
|
||||
"form": {"username": data.username},
|
||||
"next": data.next,
|
||||
"seo": _seo(request),
|
||||
}
|
||||
if wants_json(request):
|
||||
return json_error(401, "Invalid credentials")
|
||||
return templates.TemplateResponse(request, "auth/login.html", ctx, status_code=400)
|
||||
days = get_int_setting("session_remember_days", 30) if data.remember else get_int_setting("session_max_age_days", 7)
|
||||
max_age = max(1, days) * 86400
|
||||
token = create_session(user["uid"], max_age)
|
||||
record(request, "auth.login", targets=[("user", user["uid"])], user=user)
|
||||
response = action_result(request, safe_next(data.next, "/admin"))
|
||||
response.set_cookie(
|
||||
key="session",
|
||||
value=token,
|
||||
max_age=max_age,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
secure=is_secure_request(request),
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(request: Request):
|
||||
user = get_current_user(request)
|
||||
token = request.cookies.get("session")
|
||||
if token:
|
||||
end_session(token, user["uid"] if user else "guest")
|
||||
invalidate_user_cache(propagate=True)
|
||||
record(request, "auth.logout", user=user)
|
||||
response = action_result(request, "/")
|
||||
response.delete_cookie("session")
|
||||
return response
|
||||
@@ -0,0 +1,13 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import Response
|
||||
|
||||
from molodetz.avatar import render_avatar_svg
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/svg/{seed}")
|
||||
async def avatar_svg(seed: str, size: int = 64):
|
||||
svg = render_avatar_svg(seed[:128], size)
|
||||
return Response(svg, media_type="image/svg+xml", headers={"Cache-Control": "public, max-age=86400"})
|
||||
@@ -0,0 +1,42 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
import hashlib
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz import config
|
||||
from molodetz.database import admin_uids_ordered, insert_join_request
|
||||
from molodetz.dependencies import json_or_form
|
||||
from molodetz.models import JoinForm
|
||||
from molodetz.responses import action_result, respond
|
||||
from molodetz.schemas.joins import JoinPageOut
|
||||
from molodetz.seo import base_seo_context
|
||||
from molodetz.seo_meta_text import description_for
|
||||
from molodetz.utils.audit import record
|
||||
from molodetz.utils.notifications import create_notification
|
||||
from molodetz.utils.request_meta import client_ip
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _seo(request):
|
||||
return base_seo_context(request, "Binnen", description_for("binnen"), breadcrumbs=[("Molodetz", "/"), ("Binnen", "/binnen")])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def join_page(request: Request):
|
||||
submitted = request.query_params.get("ok") == "1"
|
||||
return respond(request, "binnen.html", {"submitted": submitted, "errors": [], "form": {}, "seo": _seo(request)}, model=JoinPageOut)
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def join_submit(request: Request):
|
||||
data = await json_or_form(request, JoinForm)
|
||||
if data.website:
|
||||
record(request, "join.request.honeypot", result="denied")
|
||||
return action_result(request, "/binnen?ok=1")
|
||||
ip_hash = hashlib.sha256(f"{config.SECRET_KEY}:{client_ip(request)}".encode()).hexdigest()[:24]
|
||||
row = insert_join_request(data.name, data.contact, data.repo_url, data.message, ip_hash)
|
||||
record(request, "join.request.create", payload={"uid": row["uid"]}, targets=[("join_request", row["uid"])])
|
||||
for uid in admin_uids_ordered():
|
||||
create_notification(uid, "join.request", f"{data.name} wil meeschrijven", "/admin/joins")
|
||||
return action_result(request, "/binnen?ok=1", data={"uid": row["uid"]})
|
||||
@@ -0,0 +1,76 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse
|
||||
|
||||
from molodetz.docs_api import group_by_slug, ordered_groups, substitute
|
||||
from molodetz.docs_export import export_html, export_markdown
|
||||
from molodetz.docs_live import live_services
|
||||
from molodetz.docs_prose import page_by_slug, render_page, sidebar
|
||||
from molodetz.docs_search import search
|
||||
from molodetz.seo import base_seo_context, base_url
|
||||
from molodetz.templating import templates
|
||||
from molodetz.utils.auth import get_current_user, is_admin, not_found
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _ctx(request, title, **extra):
|
||||
viewer = get_current_user(request)
|
||||
viewer_is_admin = is_admin(viewer)
|
||||
groups = [group for group in ordered_groups() if viewer_is_admin or group["slug"] != "beheer"]
|
||||
seo = base_seo_context(request, title, breadcrumbs=[("Molodetz", "/"), ("Docs", "/docs")])
|
||||
return {"sidebar": sidebar(viewer_is_admin), "api_groups": groups, "viewer_is_admin": viewer_is_admin, "seo": seo, "doc_title": title, **extra}
|
||||
|
||||
|
||||
@router.get("/docs")
|
||||
async def docs_index(request: Request):
|
||||
return templates.TemplateResponse(request, "docs/index.html", _ctx(request, "Documentatie", body=render_page("welkom")))
|
||||
|
||||
|
||||
@router.get("/docs/search")
|
||||
async def docs_search(request: Request):
|
||||
query = request.query_params.get("q", "")[:200]
|
||||
viewer_is_admin = is_admin(get_current_user(request))
|
||||
results = search(query, viewer_is_admin)
|
||||
return templates.TemplateResponse(request, "docs/search.html", _ctx(request, "Zoeken", query=query, results=results))
|
||||
|
||||
|
||||
@router.get("/docs/download.md")
|
||||
async def docs_download_md(request: Request):
|
||||
viewer_is_admin = is_admin(get_current_user(request))
|
||||
return PlainTextResponse(export_markdown(viewer_is_admin), media_type="text/markdown; charset=utf-8")
|
||||
|
||||
|
||||
@router.get("/docs/download.html")
|
||||
async def docs_download_html(request: Request):
|
||||
viewer_is_admin = is_admin(get_current_user(request))
|
||||
return HTMLResponse(export_html(viewer_is_admin))
|
||||
|
||||
|
||||
@router.get("/docs/api/{group_slug}")
|
||||
async def docs_api_group(request: Request, group_slug: str):
|
||||
group = group_by_slug(group_slug)
|
||||
viewer = get_current_user(request)
|
||||
if group is None or (group["slug"] == "beheer" and not is_admin(viewer)):
|
||||
not_found("Onbekende API-groep")
|
||||
base = base_url(request)
|
||||
username = viewer["username"] if viewer else "retoor"
|
||||
api_key = viewer["api_key"] if viewer else "JOUW_API_SLEUTEL"
|
||||
endpoints = []
|
||||
for item in group["endpoints"]:
|
||||
example = substitute(f"curl -H 'Accept: application/json' -H 'X-API-KEY: {{{{api_key}}}}' {{{{base}}}}{item['path']}", base, username, api_key)
|
||||
endpoints.append({**item, "curl": example})
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"docs/api.html",
|
||||
_ctx(request, f"API: {group['title']}", group=group, endpoints=endpoints, viewer=viewer, services=live_services() if group["slug"] == "beheer" else []),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/docs/{slug}")
|
||||
async def docs_page(request: Request, slug: str):
|
||||
page = page_by_slug(slug)
|
||||
viewer_is_admin = is_admin(get_current_user(request))
|
||||
if page is None or (page.get("admin") and not viewer_is_admin):
|
||||
not_found("Pagina niet gevonden")
|
||||
return templates.TemplateResponse(request, "docs/page.html", _ctx(request, page["title"], body=render_page(slug), page=page))
|
||||
@@ -0,0 +1,11 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.routers._gallery import gallery_page
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def flyers(request: Request):
|
||||
return gallery_page(request, "flyer", "flyers")
|
||||
@@ -0,0 +1,12 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from molodetz import config
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def health():
|
||||
return JSONResponse({"status": "ok", "version": config.APP_VERSION})
|
||||
@@ -0,0 +1,42 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz import config
|
||||
from molodetz.cache import TTLCache
|
||||
from molodetz.database import count_media, published_posts
|
||||
from molodetz.gallery import gallery_items
|
||||
from molodetz.responses import respond
|
||||
from molodetz.schemas.posts import HomeOut
|
||||
from molodetz.seo import base_seo_context, organization_schema, website_schema
|
||||
from molodetz.seo_meta_text import description_for
|
||||
|
||||
router = APIRouter()
|
||||
_landing_cache = TTLCache(ttl=config.LANDING_TTL, max_size=4)
|
||||
|
||||
|
||||
def _landing_blocks():
|
||||
cached = _landing_cache.get("blocks")
|
||||
if cached is not None:
|
||||
return cached
|
||||
posts, _ = published_posts(limit=8)
|
||||
flyers = gallery_items("flyer")
|
||||
blocks = {
|
||||
"posts": posts,
|
||||
"featured_flyer": flyers[0] if flyers else None,
|
||||
"flyer_count": count_media("flyer"),
|
||||
"meme_count": count_media("meme"),
|
||||
}
|
||||
_landing_cache.set("blocks", blocks)
|
||||
return blocks
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def index(request: Request):
|
||||
blocks = _landing_blocks()
|
||||
seo = base_seo_context(
|
||||
request,
|
||||
config.APP_TITLE,
|
||||
description_for("home"),
|
||||
schemas=[website_schema(request), organization_schema(request)],
|
||||
)
|
||||
return respond(request, "home.html", {**blocks, "seo": seo}, model=HomeOut)
|
||||
@@ -0,0 +1,11 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.routers._gallery import gallery_page
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def memes(request: Request):
|
||||
return gallery_page(request, "meme", "memes")
|
||||
@@ -0,0 +1,42 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.database import get_user_by_username, post_counts_by_author, public_people, published_posts
|
||||
from molodetz.responses import respond
|
||||
from molodetz.schemas.people import PeopleOut, ProfileOut
|
||||
from molodetz.seo import base_seo_context, profile_schema
|
||||
from molodetz.seo_meta_text import description_for
|
||||
from molodetz.utils.auth import not_found
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _with_counts(people):
|
||||
counts = post_counts_by_author([person["uid"] for person in people])
|
||||
return [{**person, "post_count": counts.get(person["uid"], 0)} for person in people]
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def people(request: Request):
|
||||
persons = _with_counts(public_people())
|
||||
seo = base_seo_context(request, "Mensen", description_for("mensen"), breadcrumbs=[("Molodetz", "/"), ("Mensen", "/mensen")])
|
||||
return respond(request, "mensen.html", {"people": persons, "seo": seo}, model=PeopleOut)
|
||||
|
||||
|
||||
@router.get("/{username}")
|
||||
async def person(request: Request, username: str):
|
||||
row = get_user_by_username(username)
|
||||
if row is None:
|
||||
not_found("Persoon niet gevonden")
|
||||
person_row = _with_counts([row])[0]
|
||||
own, _ = published_posts(limit=50, user_uid=row["uid"])
|
||||
seo = base_seo_context(
|
||||
request,
|
||||
row["username"],
|
||||
row.get("bio") or description_for("mensen"),
|
||||
og_type="profile",
|
||||
robots="index,follow" if person_row["post_count"] > 0 else "noindex,follow",
|
||||
breadcrumbs=[("Molodetz", "/"), ("Mensen", "/mensen"), (row["username"], f"/mensen/{row['username']}")],
|
||||
schemas=[profile_schema(request, row)],
|
||||
)
|
||||
return respond(request, "person.html", {"person": person_row, "posts": own, "seo": seo}, model=ProfileOut)
|
||||
@@ -0,0 +1,26 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.database import list_notifications, mark_all_read, unread_count
|
||||
from molodetz.responses import action_result, respond
|
||||
from molodetz.schemas.account import NotificationsOut
|
||||
from molodetz.seo import base_seo_context
|
||||
from molodetz.utils.auth import require_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def notifications_page(request: Request):
|
||||
user = require_user(request)
|
||||
rows = list_notifications(user["uid"])
|
||||
seo = base_seo_context(request, "Meldingen", robots="noindex,nofollow")
|
||||
ctx = {"notifications": rows, "unread": unread_count(user["uid"]), "seo": seo}
|
||||
return respond(request, "notifications.html", ctx, model=NotificationsOut)
|
||||
|
||||
|
||||
@router.post("/read")
|
||||
async def notifications_read(request: Request):
|
||||
user = require_user(request)
|
||||
mark_all_read(user["uid"])
|
||||
return action_result(request, "/notifications")
|
||||
@@ -0,0 +1,36 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from molodetz.constants import TOPICS
|
||||
from molodetz.database import get_post_by_slug, get_user
|
||||
from molodetz.rendering import content_preview, plain_title
|
||||
from molodetz.responses import respond
|
||||
from molodetz.schemas.posts import PostDetailOut
|
||||
from molodetz.seo import article_schema, base_seo_context
|
||||
from molodetz.utils.auth import get_current_user, is_admin, not_found
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/{slug}")
|
||||
async def post_detail(request: Request, slug: str):
|
||||
post = get_post_by_slug(slug)
|
||||
viewer = get_current_user(request)
|
||||
if post is None or (post["status"] != "published" and not is_admin(viewer)):
|
||||
not_found("Bericht niet gevonden")
|
||||
if slug != post["slug"]:
|
||||
return RedirectResponse(f"/posts/{post['slug']}", status_code=301)
|
||||
author = get_user(post["user_uid"])
|
||||
topic_title = TOPICS.get(post["topic"], post["topic"])
|
||||
seo = base_seo_context(
|
||||
request,
|
||||
plain_title(post["title"]),
|
||||
content_preview(post["body"], 160),
|
||||
robots="index,follow" if post["status"] == "published" else "noindex,nofollow",
|
||||
og_type="article",
|
||||
breadcrumbs=[("Molodetz", "/"), (topic_title, f"/{post['topic']}"), (post["title"], f"/posts/{post['slug']}")],
|
||||
schemas=[article_schema(request, post, author)],
|
||||
)
|
||||
ctx = {"post": post, "author": author, "topic_title": topic_title, "seo": seo}
|
||||
return respond(request, "post.html", ctx, model=PostDetailOut)
|
||||
@@ -0,0 +1,14 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from molodetz import config
|
||||
from molodetz.utils.presence import published_roster
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/roster")
|
||||
async def roster(request: Request):
|
||||
tracked = published_roster()
|
||||
return JSONResponse({"online": tracked[: config.PRESENCE_ONLINE_LIMIT], "tracked": len(tracked)})
|
||||
@@ -0,0 +1,14 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.responses import respond
|
||||
from molodetz.schemas.account import PageOut
|
||||
from molodetz.seo import base_seo_context
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def privacy(request: Request):
|
||||
seo = base_seo_context(request, "Privacy", breadcrumbs=[("Molodetz", "/"), ("Privacy", "/privacy")])
|
||||
return respond(request, "privacy.html", {"title": "Privacy", "seo": seo}, model=PageOut)
|
||||
@@ -0,0 +1,39 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.database import generate_uid, update_user
|
||||
from molodetz.responses import action_result, respond
|
||||
from molodetz.schemas.account import ApiKeyOut
|
||||
from molodetz.seo import base_seo_context
|
||||
from molodetz.utils.audit import record
|
||||
from molodetz.utils.auth import invalidate_user_cache, require_user
|
||||
from molodetz.utils.notifications import create_notification
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/api-key")
|
||||
async def api_key_page(request: Request):
|
||||
user = require_user(request)
|
||||
seo = base_seo_context(request, "API-sleutel", robots="noindex,nofollow")
|
||||
return respond(request, "profile/api_key.html", {"api_key": user["api_key"], "seo": seo}, model=ApiKeyOut)
|
||||
|
||||
|
||||
@router.post("/api-key/regenerate")
|
||||
async def api_key_regenerate(request: Request):
|
||||
user = require_user(request)
|
||||
key = generate_uid()
|
||||
update_user(user["uid"], api_key=key)
|
||||
invalidate_user_cache(propagate=True)
|
||||
record(request, "profile.apikey.regenerate", targets=[("user", user["uid"])], user=user)
|
||||
create_notification(user["uid"], "auth.apikey", "Je API-sleutel is vernieuwd.", "/profile/api-key")
|
||||
return action_result(request, "/profile/api-key", data={"api_key": key})
|
||||
|
||||
|
||||
@router.post("/avatar/regenerate")
|
||||
async def avatar_regenerate(request: Request):
|
||||
user = require_user(request)
|
||||
update_user(user["uid"], avatar_seed=generate_uid())
|
||||
invalidate_user_cache(propagate=True)
|
||||
record(request, "profile.avatar.regenerate", targets=[("user", user["uid"])], user=user)
|
||||
return action_result(request, f"/mensen/{user['username']}")
|
||||
@@ -0,0 +1,11 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.routers._listing import topic_listing
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def rol(request: Request):
|
||||
return topic_listing(request, "rol")
|
||||
@@ -0,0 +1,71 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
import logging
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
import defusedxml.ElementTree as SafeET
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import PlainTextResponse, Response
|
||||
|
||||
from molodetz import config
|
||||
from molodetz.cache import TTLCache
|
||||
from molodetz.constants import TOPICS
|
||||
from molodetz.database import db, public_people
|
||||
from molodetz.docs_prose import DOCS_PAGES
|
||||
from molodetz.seo import base_url
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
_sitemap_cache = TTLCache(ttl=config.SITEMAP_TTL, max_size=4)
|
||||
PER_TABLE_CAP = 5000
|
||||
STATIC_PATHS = ("/", "/flyers", "/memes", "/mensen", "/binnen", "/voorwaarden", "/privacy", "/docs")
|
||||
|
||||
|
||||
@router.get("/robots.txt")
|
||||
async def robots(request: Request):
|
||||
base = base_url(request)
|
||||
body = "\n".join(
|
||||
[
|
||||
"User-agent: *",
|
||||
"Disallow: /admin",
|
||||
"Disallow: /auth",
|
||||
"Disallow: /notifications",
|
||||
"Disallow: /profile",
|
||||
"Allow: /",
|
||||
f"Sitemap: {base}/sitemap.xml",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return PlainTextResponse(body)
|
||||
|
||||
|
||||
def build_sitemap(base):
|
||||
urls = [(path, None) for path in STATIC_PATHS]
|
||||
urls += [(f"/{topic}", None) for topic in TOPICS]
|
||||
if "posts" in db.tables:
|
||||
rows = list(db["posts"].find(deleted_at=None, status="published", order_by=["-published_at"], _limit=PER_TABLE_CAP + 1))
|
||||
if len(rows) > PER_TABLE_CAP:
|
||||
logger.warning("sitemap truncated posts at %d", PER_TABLE_CAP)
|
||||
rows = rows[:PER_TABLE_CAP]
|
||||
urls += [(f"/posts/{row['slug']}", row.get("updated_at")) for row in rows]
|
||||
urls += [(f"/mensen/{person['username']}", None) for person in public_people()[:PER_TABLE_CAP]]
|
||||
urls += [(f"/docs/{page['slug']}", None) for page in DOCS_PAGES if not page.get("admin")]
|
||||
parts = ['<?xml version="1.0" encoding="UTF-8"?>', '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">']
|
||||
for path, modified in urls:
|
||||
entry = f"<url><loc>{escape(base + path)}</loc>"
|
||||
if modified:
|
||||
entry += f"<lastmod>{escape(modified[:10])}</lastmod>"
|
||||
parts.append(entry + "</url>")
|
||||
parts.append("</urlset>")
|
||||
xml = "\n".join(parts)
|
||||
SafeET.fromstring(xml.encode("utf-8"))
|
||||
return xml
|
||||
|
||||
|
||||
@router.get("/sitemap.xml")
|
||||
async def sitemap(request: Request):
|
||||
base = base_url(request)
|
||||
xml = _sitemap_cache.get(base)
|
||||
if xml is None:
|
||||
xml = build_sitemap(base)
|
||||
_sitemap_cache.set(base, xml)
|
||||
return Response(xml, media_type="application/xml")
|
||||
@@ -0,0 +1,11 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.routers._listing import topic_listing
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def standaard(request: Request):
|
||||
return topic_listing(request, "standaard")
|
||||
@@ -0,0 +1,30 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from molodetz.database import get_setting, update_user
|
||||
from molodetz.responses import action_result, respond
|
||||
from molodetz.schemas.account import PageOut
|
||||
from molodetz.seo import base_seo_context
|
||||
from molodetz.utils.audit import record
|
||||
from molodetz.utils.auth import invalidate_user_cache, require_user
|
||||
from molodetz.utils.request_meta import safe_next
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def terms(request: Request):
|
||||
seo = base_seo_context(request, "Voorwaarden", breadcrumbs=[("Molodetz", "/"), ("Voorwaarden", "/voorwaarden")])
|
||||
ctx = {"title": "Voorwaarden", "terms_version": get_setting("terms_version", "1"), "next": safe_next(request.query_params.get("next"), "/"), "seo": seo}
|
||||
return respond(request, "voorwaarden.html", ctx, model=PageOut)
|
||||
|
||||
|
||||
@router.post("/accept")
|
||||
async def accept_terms(request: Request):
|
||||
user = require_user(request)
|
||||
version = get_setting("terms_version", "1")
|
||||
update_user(user["uid"], terms_version=version)
|
||||
invalidate_user_cache(propagate=True)
|
||||
record(request, "auth.terms.accept", payload={"version": version}, user=user)
|
||||
form = await request.form() if not request.headers.get("content-type", "").startswith("application/json") else {}
|
||||
return action_result(request, safe_next(form.get("next") if form else None, "/"))
|
||||
Reference in New Issue
Block a user