diff --git a/app/config.py b/app/config.py index 96047b3..e94611e 100644 --- a/app/config.py +++ b/app/config.py @@ -64,7 +64,12 @@ def get_gitea_public_url() -> str: def get_gitea_server_url() -> str: - return get_gitea_public_url() or INTERNAL_GITEA_URL + return ( + read_env_value("GITEA_SERVER_URL", "") + or read_env_value("GITEA_INTERNAL_URL", "") + or get_gitea_public_url() + or INTERNAL_GITEA_URL + ).rstrip("/") def get_gitea_admin_token() -> str: diff --git a/app/db/users.py b/app/db/users.py index 1068f0a..c8d2dbd 100644 --- a/app/db/users.py +++ b/app/db/users.py @@ -154,6 +154,14 @@ def set_user_enabled(user_id: int, enabled: bool) -> None: con.close() +def delete_user(user_id: int) -> None: + run_migrations() + con = get_connection() + con.execute("DELETE FROM users WHERE id = ?", (user_id,)) + con.commit() + con.close() + + def update_gitea_sync_state( user_id: int, status: str, diff --git a/app/gitea_provisioning.py b/app/gitea_provisioning.py index 9087b68..d65c239 100644 --- a/app/gitea_provisioning.py +++ b/app/gitea_provisioning.py @@ -136,7 +136,7 @@ def _request_json(method: str, path: str, payload: dict | None = None, expected: detail = exc.read().decode("utf-8", errors="replace") raise RuntimeError(f"Gitea API returned HTTP {exc.code}: {detail}") from exc except (URLError, TimeoutError, json.JSONDecodeError) as exc: - raise RuntimeError(f"Gitea API request failed: {exc}") from exc + raise RuntimeError(f"Gitea API request failed for {base_url}{path}: {exc}") from exc def _email(user: dict) -> str: diff --git a/app/routes/users.py b/app/routes/users.py index f2f25d1..290fb88 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -6,7 +6,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse from app.auth import require_user from app.db.audit import log_audit_event -from app.db.users import ROLES, count_enabled_admins, get_user, list_users, set_user_enabled, update_user_role +from app.db.users import ROLES, count_enabled_admins, delete_user, get_user, list_users, set_user_enabled, update_user_role from app.gitea_provisioning import sync_gitea_user from app.templates.layout import page @@ -132,6 +132,9 @@ def users_page(request: Request, message: str = "", error: str = "", user=Depend
+ """ @@ -291,3 +294,40 @@ def sync_gitea_user_action(user_id: int, user=Depends(require_user)): status_code=303, ) return RedirectResponse(url="/portal/admin/users?message=" + quote("Gitea synchronized."), status_code=303) + + +@router.post("/admin/users/{user_id}/delete") +def delete_user_action(user_id: int, user=Depends(require_user)): + require_admin(user) + target = get_user(user_id) + if not target: + raise HTTPException(status_code=404, detail="User not found") + if int(user.get("id")) == user_id: + return RedirectResponse( + url="/portal/admin/users?error=" + quote("Cannot delete your own user account."), + status_code=303, + ) + if (target.get("role") or "").lower() == "admin" and count_enabled_admins(excluding_user_id=user_id) == 0: + return RedirectResponse( + url="/portal/admin/users?error=" + quote("Cannot delete the last enabled admin."), + status_code=303, + ) + + set_user_enabled(user_id, False) + target = get_user(user_id) or target + sync_result = sync_gitea_user(target) + if sync_result.get("status") == "error": + return RedirectResponse( + url="/portal/admin/users?error=" + quote(f"User disabled, but Gitea sync failed: {sync_result.get('error')}"), + status_code=303, + ) + + delete_user(user_id) + log_audit_event( + user, + action="user.deleted", + target_type="user", + target_id=user_id, + metadata={"username": target.get("username"), "role": target.get("role"), "gitea_sync_status": sync_result.get("status")}, + ) + return RedirectResponse(url="/portal/admin/users?message=" + quote("User deleted."), status_code=303)