2026-10-05 09:36:20 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
2026-10-06 02:56:08 +02:00
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
2026-10-05 09:36:20 +02:00
|
|
|
|
2026-10-06 02:56:08 +02:00
|
|
|
from molodetz.database import (
|
|
|
|
|
create_invite,
|
|
|
|
|
get_int_setting,
|
|
|
|
|
get_join_request,
|
|
|
|
|
list_join_requests,
|
|
|
|
|
open_invite_for_join,
|
|
|
|
|
revoke_open_invites,
|
|
|
|
|
suggest_username,
|
|
|
|
|
update_join_request,
|
|
|
|
|
)
|
2026-10-05 09:36:20 +02:00
|
|
|
from molodetz.dependencies import json_or_form
|
|
|
|
|
from molodetz.link_check import check_repo_link
|
|
|
|
|
from molodetz.models import JoinStatusForm
|
2026-10-06 02:56:08 +02:00
|
|
|
from molodetz.responses import action_result, respond, wants_json
|
2026-10-05 09:36:20 +02:00
|
|
|
from molodetz.routers.admin._shared import admin_seo
|
|
|
|
|
from molodetz.schemas.joins import JoinListOut
|
2026-10-06 02:56:08 +02:00
|
|
|
from molodetz.templating import templates
|
2026-10-05 09:36:20 +02:00
|
|
|
from molodetz.utils.audit import record
|
|
|
|
|
from molodetz.utils.auth import not_found, require_admin
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
2026-10-06 02:56:08 +02:00
|
|
|
def _invite_contact_email(contact):
|
|
|
|
|
contact = (contact or "").strip()
|
|
|
|
|
if "@" in contact and " " not in contact and "." in contact.split("@")[-1]:
|
|
|
|
|
return contact.lower()
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
2026-10-05 09:36:20 +02:00
|
|
|
@router.get("/joins")
|
|
|
|
|
async def joins_index(request: Request):
|
|
|
|
|
require_admin(request)
|
2026-10-06 02:56:08 +02:00
|
|
|
rows = []
|
|
|
|
|
for row in list_join_requests():
|
|
|
|
|
item = dict(row)
|
|
|
|
|
invite = open_invite_for_join(row["uid"])
|
|
|
|
|
item["invite_open"] = invite is not None
|
|
|
|
|
item["invite_expires_at"] = invite["expires_at"] if invite else None
|
|
|
|
|
rows.append(item)
|
|
|
|
|
return respond(request, "admin/joins.html", {"requests": rows, "seo": admin_seo(request, "Join requests")}, model=JoinListOut)
|
2026-10-05 09:36:20 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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:
|
2026-10-05 11:25:41 +02:00
|
|
|
not_found("Join request not found")
|
2026-10-05 09:36:20 +02:00
|
|
|
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:
|
2026-10-05 11:25:41 +02:00
|
|
|
not_found("Join request not found")
|
2026-10-05 09:36:20 +02:00
|
|
|
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)
|
2026-10-06 02:56:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/joins/{uid}/invite")
|
|
|
|
|
async def joins_invite(request: Request, uid: str):
|
|
|
|
|
user = require_admin(request)
|
|
|
|
|
row = get_join_request(uid)
|
|
|
|
|
if row is None:
|
|
|
|
|
not_found("Join request not found")
|
|
|
|
|
if row.get("status") == "declined":
|
|
|
|
|
raise HTTPException(409, "Cannot invite a declined request")
|
|
|
|
|
days = get_int_setting("invite_expiry_days", 7)
|
|
|
|
|
invite, token = create_invite(
|
|
|
|
|
uid,
|
|
|
|
|
_invite_contact_email(row.get("contact")),
|
|
|
|
|
suggest_username(row.get("name")),
|
|
|
|
|
user["uid"],
|
|
|
|
|
days=days,
|
|
|
|
|
)
|
|
|
|
|
claim_url = str(request.base_url).rstrip("/") + f"/invite/{token}"
|
|
|
|
|
record(
|
|
|
|
|
request,
|
|
|
|
|
"join.invite.issued",
|
|
|
|
|
payload={"expires_at": invite["expires_at"]},
|
|
|
|
|
targets=[("join_request", uid)],
|
|
|
|
|
user=user,
|
|
|
|
|
)
|
|
|
|
|
data = {"claim_url": claim_url, "expires_at": invite["expires_at"]}
|
|
|
|
|
if wants_json(request):
|
|
|
|
|
return action_result(request, "/admin/joins", data=data)
|
|
|
|
|
return templates.TemplateResponse(
|
|
|
|
|
request,
|
|
|
|
|
"admin/invite_issued.html",
|
|
|
|
|
{"join": row, "claim_url": claim_url, "expires_at": invite["expires_at"], "seo": admin_seo(request, "Invite issued")},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/joins/{uid}/invite/revoke")
|
|
|
|
|
async def joins_invite_revoke(request: Request, uid: str):
|
|
|
|
|
user = require_admin(request)
|
|
|
|
|
row = get_join_request(uid)
|
|
|
|
|
if row is None:
|
|
|
|
|
not_found("Join request not found")
|
|
|
|
|
revoked = revoke_open_invites(uid, user["uid"])
|
|
|
|
|
record(request, "join.invite.revoked", payload={"revoked": revoked}, targets=[("join_request", uid)], user=user)
|
|
|
|
|
return action_result(request, "/admin/joins", data={"revoked": revoked})
|