|
# retoor <retoor@molodetz.nl>
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from devplacepy.database import db, get_table, get_users_by_uids
|
|
from devplacepy.dependencies import json_or_form
|
|
from devplacepy.models import (
|
|
EditorPrefsForm,
|
|
WorkspaceFlagForm,
|
|
WorkspaceQuotaForm,
|
|
WorkspaceSuspendForm,
|
|
)
|
|
from devplacepy.responses import action_result, json_error, respond
|
|
from devplacepy.routers.admin._shared import deny_senior, is_senior_admin
|
|
from devplacepy.schemas import AdminWorkspacesOut
|
|
from devplacepy.seo import base_seo_context
|
|
from devplacepy.services.audit import record as audit
|
|
from devplacepy.services.containers import store
|
|
from devplacepy.services.containers.workspace import (
|
|
editor,
|
|
flags,
|
|
provision,
|
|
quota,
|
|
tunnels,
|
|
)
|
|
from devplacepy.utils import create_notification, generate_uid, not_found, require_admin
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _decorate(rows: list[dict]) -> list[dict]:
|
|
owner_uids = {row.get("workspace_owner_uid") for row in rows if row.get("workspace_owner_uid")}
|
|
owners = get_users_by_uids(list(owner_uids)) if owner_uids else {}
|
|
projects = {}
|
|
if "projects" in db.tables:
|
|
project_uids = {row.get("project_uid") for row in rows if row.get("project_uid")}
|
|
for uid in project_uids:
|
|
found = get_table("projects").find_one(uid=uid)
|
|
if found:
|
|
projects[uid] = found
|
|
decorated = []
|
|
for row in rows:
|
|
view = provision.view(row)
|
|
owner = owners.get(row.get("workspace_owner_uid", "")) or {}
|
|
project = projects.get(row.get("project_uid", "")) or {}
|
|
view["owner_username"] = owner.get("username", "")
|
|
view["project_title"] = project.get("title", "")
|
|
view["project_slug"] = project.get("slug", "") or project.get("uid", "")
|
|
decorated.append(view)
|
|
return decorated
|
|
|
|
|
|
def _all_workspaces() -> list[dict]:
|
|
return list(get_table("instances").find(is_workspace=1, deleted_at=None))
|
|
|
|
|
|
def _instance_or_404(uid: str) -> dict:
|
|
instance = store.get_instance(uid)
|
|
if not instance or not instance.get("is_workspace"):
|
|
raise not_found("Workspace not found")
|
|
return instance
|
|
|
|
|
|
def _audit(request: Request, admin: dict, event_key: str, instance: dict, **extra):
|
|
audit.record(
|
|
request,
|
|
event_key,
|
|
user=admin,
|
|
target_type="instance",
|
|
target_uid=instance["uid"],
|
|
target_label=instance.get("name"),
|
|
summary=f"{admin['username']} {event_key} workspace {instance.get('name')}",
|
|
links=[audit.instance(instance["uid"], instance.get("name"))],
|
|
**extra,
|
|
)
|
|
|
|
|
|
@router.get("/workspaces")
|
|
async def admin_workspaces(request: Request):
|
|
admin = require_admin(request)
|
|
if not isinstance(admin, dict):
|
|
return admin
|
|
context = {
|
|
"workspaces": _decorate(_all_workspaces()),
|
|
"flags": flags.list_flags(),
|
|
"admin_section": "workspaces",
|
|
"user": admin,
|
|
**base_seo_context(
|
|
request,
|
|
title="Workspaces - Admin",
|
|
description="Administer dev workspaces.",
|
|
robots="noindex,nofollow",
|
|
breadcrumbs=[
|
|
{"name": "Home", "url": "/feed"},
|
|
{"name": "Admin", "url": "/admin"},
|
|
{"name": "Workspaces", "url": "/admin/workspaces"},
|
|
],
|
|
),
|
|
}
|
|
return respond(request, "admin_workspaces.html", context, model=AdminWorkspacesOut)
|
|
|
|
|
|
@router.get("/workspaces/data")
|
|
async def admin_workspaces_data(request: Request):
|
|
admin = require_admin(request)
|
|
if not isinstance(admin, dict):
|
|
return admin
|
|
return JSONResponse(
|
|
{"workspaces": _decorate(_all_workspaces()), "flags": flags.list_flags()}
|
|
)
|
|
|
|
|
|
@router.post("/workspaces/{uid}/suspend")
|
|
async def admin_workspace_suspend(
|
|
request: Request,
|
|
uid: str,
|
|
data: Annotated[WorkspaceSuspendForm, Depends(json_or_form(WorkspaceSuspendForm))],
|
|
):
|
|
admin = require_admin(request)
|
|
if not isinstance(admin, dict):
|
|
return admin
|
|
instance = _instance_or_404(uid)
|
|
reason = (data.reason or "").strip()
|
|
if not reason:
|
|
return json_error(400, "a reason is required and is shown to the owner")
|
|
provision.suspend(instance, admin["uid"], reason)
|
|
_audit(request, admin, "container.workspace.suspend", instance, metadata={"reason": reason})
|
|
owner = instance.get("workspace_owner_uid", "")
|
|
if owner:
|
|
create_notification(
|
|
owner,
|
|
"workspace",
|
|
f"Workspace {instance.get('name', '')} was suspended: {reason}",
|
|
instance["uid"],
|
|
"/projects",
|
|
)
|
|
return action_result(request, "/admin/workspaces")
|
|
|
|
|
|
@router.post("/workspaces/{uid}/unsuspend")
|
|
async def admin_workspace_unsuspend(request: Request, uid: str):
|
|
admin = require_admin(request)
|
|
if not isinstance(admin, dict):
|
|
return admin
|
|
instance = _instance_or_404(uid)
|
|
provision.unsuspend(instance)
|
|
_audit(request, admin, "container.workspace.unsuspend", instance)
|
|
owner = instance.get("workspace_owner_uid", "")
|
|
if owner:
|
|
create_notification(
|
|
owner,
|
|
"workspace",
|
|
f"Workspace {instance.get('name', '')} is available again.",
|
|
instance["uid"],
|
|
"/projects",
|
|
)
|
|
return action_result(request, "/admin/workspaces")
|
|
|
|
|
|
@router.post("/workspaces/{uid}/stop")
|
|
async def admin_workspace_stop(request: Request, uid: str):
|
|
admin = require_admin(request)
|
|
if not isinstance(admin, dict):
|
|
return admin
|
|
instance = _instance_or_404(uid)
|
|
provision.stop(instance)
|
|
_audit(request, admin, "container.workspace.stop", instance)
|
|
return action_result(request, "/admin/workspaces")
|
|
|
|
|
|
@router.post("/workspaces/{uid}/start")
|
|
async def admin_workspace_start(request: Request, uid: str):
|
|
admin = require_admin(request)
|
|
if not isinstance(admin, dict):
|
|
return admin
|
|
instance = _instance_or_404(uid)
|
|
store.update_instance(instance["uid"], {"desired_state": "running"})
|
|
_audit(request, admin, "container.workspace.resume", instance)
|
|
return action_result(request, "/admin/workspaces")
|
|
|
|
|
|
@router.post("/workspaces/{uid}/delete")
|
|
async def admin_workspace_delete(request: Request, uid: str):
|
|
admin = require_admin(request)
|
|
if not isinstance(admin, dict):
|
|
return admin
|
|
instance = _instance_or_404(uid)
|
|
for row in tunnels.list_for_instance(instance["uid"]):
|
|
tunnels.soft_delete(row["uid"], admin["uid"])
|
|
store.delete_instance(instance["uid"], admin["uid"])
|
|
_audit(request, admin, "container.workspace.delete", instance)
|
|
return action_result(request, "/admin/workspaces")
|
|
|
|
|
|
@router.post("/workspaces/{uid}/flag")
|
|
async def admin_workspace_flag(
|
|
request: Request,
|
|
uid: str,
|
|
data: Annotated[WorkspaceFlagForm, Depends(json_or_form(WorkspaceFlagForm))],
|
|
):
|
|
admin = require_admin(request)
|
|
if not isinstance(admin, dict):
|
|
return admin
|
|
instance = _instance_or_404(uid)
|
|
row = flags.raise_flag(
|
|
instance, data.kind or flags.KIND_MANUAL, data.severity, data.detail
|
|
)
|
|
_audit(request, admin, "container.workspace.flag.raise", instance,
|
|
metadata={"kind": data.kind, "severity": data.severity})
|
|
owner = instance.get("workspace_owner_uid", "")
|
|
if owner:
|
|
create_notification(
|
|
owner,
|
|
"workspace",
|
|
f"Workspace {instance.get('name', '')} was flagged: {data.detail or data.kind}",
|
|
instance["uid"],
|
|
"/projects",
|
|
)
|
|
return action_result(request, "/admin/workspaces", data=row)
|
|
|
|
|
|
@router.post("/workspaces/flags/{flag_uid}/resolve")
|
|
async def admin_flag_resolve(request: Request, flag_uid: str, status: str = "resolved"):
|
|
admin = require_admin(request)
|
|
if not isinstance(admin, dict):
|
|
return admin
|
|
if not flags.set_status(flag_uid, status, admin["uid"]):
|
|
raise not_found("Flag not found")
|
|
event = (
|
|
"container.workspace.flag.dismiss"
|
|
if status == "dismissed"
|
|
else "container.workspace.flag.resolve"
|
|
)
|
|
audit.record(
|
|
request,
|
|
event,
|
|
user=admin,
|
|
target_type="workspace_flag",
|
|
target_uid=flag_uid,
|
|
summary=f"{admin['username']} set flag {flag_uid} to {status}",
|
|
)
|
|
return action_result(request, "/admin/workspaces")
|
|
|
|
|
|
@router.post("/workspaces/{uid}/editor")
|
|
async def admin_workspace_editor(
|
|
request: Request,
|
|
uid: str,
|
|
data: Annotated[EditorPrefsForm, Depends(json_or_form(EditorPrefsForm))],
|
|
):
|
|
admin = require_admin(request)
|
|
if not isinstance(admin, dict):
|
|
return admin
|
|
instance = _instance_or_404(uid)
|
|
owner_uid = instance.get("workspace_owner_uid", "")
|
|
owner = get_users_by_uids([owner_uid]).get(owner_uid) if owner_uid else None
|
|
if is_senior_admin(admin, owner):
|
|
return deny_senior(
|
|
request,
|
|
admin,
|
|
owner_uid,
|
|
owner,
|
|
"container.workspace.editor.update",
|
|
"/admin/workspaces",
|
|
)
|
|
if not owner_uid:
|
|
return json_error(400, "this workspace has no owner")
|
|
if data.reset:
|
|
editor.reset_prefs(owner_uid, admin["uid"])
|
|
else:
|
|
editor.save_prefs(
|
|
owner_uid, data.model_dump(exclude={"reset"}, exclude_unset=True)
|
|
)
|
|
_audit(request, admin, "container.workspace.editor.update", instance)
|
|
return action_result(
|
|
request,
|
|
"/admin/workspaces",
|
|
data={"editor": editor.view(owner_uid, instance)},
|
|
)
|
|
|
|
|
|
@router.post("/workspaces/quota")
|
|
async def admin_workspace_quota(
|
|
request: Request,
|
|
data: Annotated[WorkspaceQuotaForm, Depends(json_or_form(WorkspaceQuotaForm))],
|
|
):
|
|
admin = require_admin(request)
|
|
if not isinstance(admin, dict):
|
|
return admin
|
|
if not data.owner_id:
|
|
return json_error(400, "owner_id is required")
|
|
table = get_table(quota.RULES_TABLE)
|
|
existing = table.find_one(
|
|
owner_kind="user", owner_id=data.owner_id, deleted_at=None
|
|
)
|
|
payload = {key: getattr(data, key) for key in quota.RULE_COLUMNS}
|
|
if existing:
|
|
table.update({"uid": existing["uid"], "label": data.label, **payload}, ["uid"])
|
|
uid = existing["uid"]
|
|
else:
|
|
uid = generate_uid()
|
|
table.insert(
|
|
{
|
|
"uid": uid,
|
|
"owner_kind": "user",
|
|
"owner_id": data.owner_id,
|
|
"label": data.label,
|
|
"created_at": "",
|
|
"updated_at": "",
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
**payload,
|
|
}
|
|
)
|
|
audit.record(
|
|
request,
|
|
"container.workspace.settings.update",
|
|
user=admin,
|
|
target_type="user",
|
|
target_uid=data.owner_id,
|
|
summary=f"{admin['username']} updated workspace quota",
|
|
metadata=payload,
|
|
)
|
|
return action_result(request, "/admin/workspaces", data={"uid": uid, **payload})
|