feat: add Workspace feature — scoped server groups with user access and sidebar selector
This commit is contained in:
@@ -8,6 +8,7 @@ from . import logs
|
||||
from . import settings
|
||||
from . import notifications
|
||||
from . import users
|
||||
from . import workspaces
|
||||
|
||||
__all__ = [
|
||||
"auth",
|
||||
@@ -18,5 +19,6 @@ __all__ = [
|
||||
"logs",
|
||||
"settings",
|
||||
"notifications",
|
||||
"users"
|
||||
"users",
|
||||
"workspaces",
|
||||
]
|
||||
@@ -17,18 +17,52 @@ templates = Jinja2Templates(directory="app/templates")
|
||||
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get("/dashboard", response_class=HTMLResponse, include_in_schema=False)
|
||||
def dashboard(request: Request, db: Session = Depends(get_db)):
|
||||
total_servers = db.query(ProxmoxServer).count()
|
||||
online_servers = db.query(ProxmoxServer).filter(ProxmoxServer.is_online == True).count()
|
||||
offline_servers = db.query(ProxmoxServer).filter(ProxmoxServer.is_online == False).count()
|
||||
from .workspaces import get_workspace_server_ids
|
||||
|
||||
# Count distinct clusters (non-null cluster_name counts as one cluster each)
|
||||
named_clusters = db.query(func.count(distinct(ProxmoxServer.cluster_name))).filter(
|
||||
# Try to get current user for workspace filtering (best-effort)
|
||||
try:
|
||||
token = request.cookies.get("access_token") or (
|
||||
request.headers.get("Authorization", "").removeprefix("Bearer ").strip() or None
|
||||
)
|
||||
current_user_obj = None
|
||||
if token:
|
||||
from ..auth import decode_access_token
|
||||
from ..models import User as UserModel
|
||||
try:
|
||||
payload = decode_access_token(token)
|
||||
username = payload.get("sub")
|
||||
if username:
|
||||
current_user_obj = db.query(UserModel).filter(
|
||||
UserModel.username == username, UserModel.is_active == True
|
||||
).first()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
server_ids = get_workspace_server_ids(request, db, current_user_obj) if current_user_obj else None
|
||||
except Exception:
|
||||
server_ids = None
|
||||
|
||||
base_q = db.query(ProxmoxServer)
|
||||
if server_ids is not None:
|
||||
base_q = base_q.filter(ProxmoxServer.id.in_(server_ids))
|
||||
|
||||
total_servers = base_q.count()
|
||||
online_servers = base_q.filter(ProxmoxServer.is_online == True).count()
|
||||
offline_servers = base_q.filter(ProxmoxServer.is_online == False).count()
|
||||
|
||||
# Count distinct clusters within the filtered servers
|
||||
cluster_q = db.query(func.count(distinct(ProxmoxServer.cluster_name))).filter(
|
||||
ProxmoxServer.cluster_name.isnot(None),
|
||||
ProxmoxServer.cluster_name != ''
|
||||
).scalar() or 0
|
||||
standalone_count = db.query(ProxmoxServer).filter(
|
||||
)
|
||||
standalone_q = db.query(ProxmoxServer).filter(
|
||||
(ProxmoxServer.cluster_name == None) | (ProxmoxServer.cluster_name == '')
|
||||
).count()
|
||||
)
|
||||
if server_ids is not None:
|
||||
cluster_q = cluster_q.filter(ProxmoxServer.id.in_(server_ids))
|
||||
standalone_q = standalone_q.filter(ProxmoxServer.id.in_(server_ids))
|
||||
named_clusters = cluster_q.scalar() or 0
|
||||
standalone_count = standalone_q.count()
|
||||
total_clusters = named_clusters + standalone_count
|
||||
|
||||
# Alerts in last 24 hours
|
||||
@@ -47,7 +81,10 @@ def dashboard(request: Request, db: Session = Depends(get_db)):
|
||||
AuditLog.created_at.desc()
|
||||
).limit(5).all()
|
||||
|
||||
recent_servers = db.query(ProxmoxServer).order_by(ProxmoxServer.id.desc()).limit(6).all()
|
||||
recent_servers_q = db.query(ProxmoxServer)
|
||||
if server_ids is not None:
|
||||
recent_servers_q = recent_servers_q.filter(ProxmoxServer.id.in_(server_ids))
|
||||
recent_servers = recent_servers_q.order_by(ProxmoxServer.id.desc()).limit(6).all()
|
||||
|
||||
stats = {
|
||||
"total_servers": total_servers,
|
||||
|
||||
@@ -32,8 +32,36 @@ async def backups_page(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
from ...i18n import t
|
||||
from ...api.workspaces import get_workspace_server_ids
|
||||
from ...auth import get_current_user_optional
|
||||
|
||||
lang = request.cookies.get("language", "ru")
|
||||
servers = db.query(ProxmoxServer).filter(ProxmoxServer.is_online.is_(True)).all()
|
||||
|
||||
# Workspace filtering (best-effort, page may be loaded unauthenticated)
|
||||
server_ids = None
|
||||
try:
|
||||
token = request.cookies.get("access_token") or (
|
||||
request.headers.get("Authorization", "").removeprefix("Bearer ").strip() or None
|
||||
)
|
||||
if token:
|
||||
from ...auth import decode_access_token
|
||||
from ...models import User as UserModel
|
||||
payload = decode_access_token(token)
|
||||
username = payload.get("sub")
|
||||
if username:
|
||||
user_obj = db.query(UserModel).filter(
|
||||
UserModel.username == username, UserModel.is_active == True
|
||||
).first()
|
||||
if user_obj:
|
||||
server_ids = get_workspace_server_ids(request, db, user_obj)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
servers_q = db.query(ProxmoxServer).filter(ProxmoxServer.is_online.is_(True))
|
||||
if server_ids is not None:
|
||||
servers_q = servers_q.filter(ProxmoxServer.id.in_(server_ids))
|
||||
servers = servers_q.all()
|
||||
|
||||
context = {
|
||||
"request": request,
|
||||
"page_title": t("nav_backups", lang),
|
||||
@@ -57,7 +85,7 @@ def get_storages(
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
try:
|
||||
client = _get_proxmox_client(server)
|
||||
storages = client.get_storages()
|
||||
storages = client.get_cluster_storages()
|
||||
return JSONResponse(content={"storages": storages})
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing storages for server {server_id}: {e}")
|
||||
|
||||
@@ -84,11 +84,17 @@ def instance_detail_page(request: Request, server_id: int, vmid: int, type: str
|
||||
|
||||
@router.get("/api/servers", response_model=List[ProxmoxServerResponse])
|
||||
def list_proxmox_servers(
|
||||
db: Session = Depends(get_db),
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.view"))
|
||||
):
|
||||
"""Получить список всех Proxmox серверов"""
|
||||
servers = db.query(ProxmoxServer).all()
|
||||
"""Получить список всех Proxmox серверов (фильтруется по активному workspace)"""
|
||||
from ...api.workspaces import get_workspace_server_ids
|
||||
server_ids = get_workspace_server_ids(request, db, current_user)
|
||||
query = db.query(ProxmoxServer)
|
||||
if server_ids is not None:
|
||||
query = query.filter(ProxmoxServer.id.in_(server_ids))
|
||||
servers = query.all()
|
||||
return servers
|
||||
|
||||
|
||||
@@ -705,7 +711,7 @@ def get_storages(
|
||||
if not client.is_connected():
|
||||
raise HTTPException(status_code=503, detail="Failed to connect to Proxmox server")
|
||||
|
||||
storages = client.get_storages(node)
|
||||
storages = client.get_node_storages(node)
|
||||
|
||||
# Фильтрация по типу контента если указано
|
||||
if content_type and storages:
|
||||
|
||||
@@ -77,6 +77,7 @@ def sync_vms_now(
|
||||
|
||||
@router.get("/api/virtual-machines")
|
||||
def get_all_virtual_machines(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("vms.view"))
|
||||
):
|
||||
@@ -88,9 +89,16 @@ def get_all_virtual_machines(
|
||||
Для пользователей с ролью 'user' возвращаются только их собственные инстансы.
|
||||
"""
|
||||
from ...models import VMInstance
|
||||
|
||||
# Get servers for name lookup
|
||||
servers = db.query(ProxmoxServer).all()
|
||||
from ...api.workspaces import get_workspace_server_ids
|
||||
|
||||
# Workspace filtering
|
||||
workspace_server_ids = get_workspace_server_ids(request, db, current_user)
|
||||
|
||||
# Get servers for name lookup (filtered by workspace)
|
||||
servers_q = db.query(ProxmoxServer)
|
||||
if workspace_server_ids is not None:
|
||||
servers_q = servers_q.filter(ProxmoxServer.id.in_(workspace_server_ids))
|
||||
servers = servers_q.all()
|
||||
server_map = {s.id: s for s in servers}
|
||||
|
||||
# Pre-load IPAM allocations for IP lookup
|
||||
@@ -121,6 +129,10 @@ def get_all_virtual_machines(
|
||||
VMInstance.deleted_at.is_(None),
|
||||
VMInstance.is_template == False
|
||||
)
|
||||
|
||||
# Workspace filter: only show VMs from servers in the active workspace
|
||||
if workspace_server_ids is not None:
|
||||
query = query.filter(VMInstance.server_id.in_(workspace_server_ids))
|
||||
|
||||
# VPS-style user isolation: users with 'user' role see only their own instances
|
||||
# Check if user has 'vms.view' but not 'vms.view:all' (or is user role)
|
||||
|
||||
431
backend/app/api/workspaces.py
Normal file
431
backend/app/api/workspaces.py
Normal file
@@ -0,0 +1,431 @@
|
||||
"""
|
||||
Workspace API
|
||||
=============
|
||||
Endpoints for managing workspaces — named groups of Proxmox servers
|
||||
with scoped user access.
|
||||
|
||||
Admin-only: create, update, delete, assign servers/users.
|
||||
All authenticated: list (filtered to their workspaces), get detail.
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session
|
||||
from loguru import logger
|
||||
|
||||
from ..db import get_db
|
||||
from ..models import Workspace, WorkspaceServer, WorkspaceUser, ProxmoxServer, User
|
||||
from ..schemas import WorkspaceCreate, WorkspaceUpdate, WorkspaceResponse, WorkspaceDetail
|
||||
from ..auth import get_current_user, PermissionChecker
|
||||
from ..template_helpers import add_i18n_context
|
||||
from ..logging_service import LoggingService
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: resolve workspace server IDs from X-Active-Workspace header
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_workspace_server_ids(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> Optional[List[int]]:
|
||||
"""
|
||||
Read X-Active-Workspace header and return list[int] of server_ids
|
||||
that belong to that workspace AND that the current user can access.
|
||||
|
||||
Returns None when:
|
||||
- Header is absent → no workspace filter (show all, admin behaviour)
|
||||
- workspace_id == 0 → explicit "show all" (admin only)
|
||||
- workspace not found or user has no access → empty list (403-ish)
|
||||
"""
|
||||
raw = request.headers.get("X-Active-Workspace")
|
||||
if raw is None:
|
||||
return None # no filter
|
||||
|
||||
try:
|
||||
workspace_id = int(raw)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if workspace_id == 0:
|
||||
# "All workspaces" — only admins may use this
|
||||
if current_user.is_admin:
|
||||
return None
|
||||
# non-admin trying to bypass → return their union of server_ids
|
||||
workspace_id = None
|
||||
|
||||
if workspace_id is None:
|
||||
# Collect all workspaces the user belongs to
|
||||
wus = db.query(WorkspaceUser).filter(WorkspaceUser.user_id == current_user.id).all()
|
||||
ws_ids = [wu.workspace_id for wu in wus]
|
||||
if not ws_ids:
|
||||
# Fall back to Default workspace
|
||||
default_ws = db.query(Workspace).filter(Workspace.is_default == True).first()
|
||||
if default_ws:
|
||||
ws_ids = [default_ws.id]
|
||||
else:
|
||||
return []
|
||||
rows = db.query(WorkspaceServer.server_id).filter(
|
||||
WorkspaceServer.workspace_id.in_(ws_ids)
|
||||
).all()
|
||||
return [r.server_id for r in rows]
|
||||
|
||||
workspace = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not workspace:
|
||||
return []
|
||||
|
||||
# Check access: admins always have access; regular users need WorkspaceUser entry
|
||||
if not current_user.is_admin:
|
||||
access = db.query(WorkspaceUser).filter(
|
||||
WorkspaceUser.workspace_id == workspace_id,
|
||||
WorkspaceUser.user_id == current_user.id,
|
||||
).first()
|
||||
if not access:
|
||||
return []
|
||||
|
||||
rows = db.query(WorkspaceServer.server_id).filter(
|
||||
WorkspaceServer.workspace_id == workspace_id
|
||||
).all()
|
||||
return [r.server_id for r in rows]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: build WorkspaceResponse with counters
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _build_response(ws: Workspace) -> WorkspaceResponse:
|
||||
return WorkspaceResponse(
|
||||
id=ws.id,
|
||||
name=ws.name,
|
||||
description=ws.description,
|
||||
color=ws.color,
|
||||
is_default=ws.is_default,
|
||||
server_count=len(ws.workspace_servers),
|
||||
user_count=len(ws.workspace_users),
|
||||
created_at=ws.created_at,
|
||||
)
|
||||
|
||||
|
||||
def _build_detail(ws: Workspace) -> WorkspaceDetail:
|
||||
from ..schemas import WorkspaceServerItem, WorkspaceUserItem
|
||||
servers = [
|
||||
WorkspaceServerItem(
|
||||
id=wss.server.id,
|
||||
name=wss.server.name,
|
||||
ip_address=wss.server.ip_address,
|
||||
is_online=wss.server.is_online,
|
||||
cluster_name=wss.server.cluster_name,
|
||||
)
|
||||
for wss in ws.workspace_servers
|
||||
if wss.server is not None
|
||||
]
|
||||
users = [
|
||||
WorkspaceUserItem(
|
||||
id=wsu.user.id,
|
||||
username=wsu.user.username,
|
||||
email=wsu.user.email,
|
||||
full_name=wsu.user.full_name,
|
||||
)
|
||||
for wsu in ws.workspace_users
|
||||
if wsu.user is not None
|
||||
]
|
||||
return WorkspaceDetail(
|
||||
id=ws.id,
|
||||
name=ws.name,
|
||||
description=ws.description,
|
||||
color=ws.color,
|
||||
is_default=ws.is_default,
|
||||
server_count=len(servers),
|
||||
user_count=len(users),
|
||||
created_at=ws.created_at,
|
||||
servers=servers,
|
||||
users=users,
|
||||
)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# HTML Page
|
||||
# ===========================================================================
|
||||
|
||||
@router.get("/workspaces", response_class=HTMLResponse, include_in_schema=False)
|
||||
def workspaces_page(
|
||||
request: Request,
|
||||
):
|
||||
"""Admin workspace management page"""
|
||||
context = {
|
||||
"request": request,
|
||||
"page_title": "Workspaces",
|
||||
}
|
||||
from ..template_helpers import add_i18n_context
|
||||
context = add_i18n_context(request, context)
|
||||
return templates.TemplateResponse("workspaces.html", context)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# REST API
|
||||
# ===========================================================================
|
||||
|
||||
@router.get("/api/workspaces", response_model=List[WorkspaceResponse])
|
||||
def list_workspaces(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
List workspaces.
|
||||
Admins see all; regular users see only workspaces they belong to
|
||||
(plus the Default workspace).
|
||||
"""
|
||||
if current_user.is_admin:
|
||||
workspaces = db.query(Workspace).order_by(Workspace.is_default.desc(), Workspace.name).all()
|
||||
else:
|
||||
wus = db.query(WorkspaceUser).filter(WorkspaceUser.user_id == current_user.id).all()
|
||||
ws_ids = [wu.workspace_id for wu in wus]
|
||||
# Always include default workspace
|
||||
default_ws = db.query(Workspace).filter(Workspace.is_default == True).first()
|
||||
if default_ws and default_ws.id not in ws_ids:
|
||||
ws_ids.append(default_ws.id)
|
||||
workspaces = db.query(Workspace).filter(Workspace.id.in_(ws_ids)).order_by(
|
||||
Workspace.is_default.desc(), Workspace.name
|
||||
).all()
|
||||
|
||||
return [_build_response(ws) for ws in workspaces]
|
||||
|
||||
|
||||
@router.post("/api/workspaces", response_model=WorkspaceDetail, status_code=status.HTTP_201_CREATED)
|
||||
def create_workspace(
|
||||
data: WorkspaceCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("settings.panel")),
|
||||
):
|
||||
"""Create a new workspace (admin only)."""
|
||||
existing = db.query(Workspace).filter(Workspace.name == data.name).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Workspace with name '{data.name}' already exists",
|
||||
)
|
||||
|
||||
ws = Workspace(
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
color=data.color or "#667eea",
|
||||
is_default=False,
|
||||
created_by=current_user.id,
|
||||
)
|
||||
db.add(ws)
|
||||
db.flush() # get ws.id
|
||||
|
||||
# Assign servers
|
||||
for server_id in (data.server_ids or []):
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if server:
|
||||
db.add(WorkspaceServer(workspace_id=ws.id, server_id=server_id))
|
||||
|
||||
# Assign users
|
||||
for user_id in (data.user_ids or []):
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if user:
|
||||
db.add(WorkspaceUser(workspace_id=ws.id, user_id=user_id))
|
||||
|
||||
db.commit()
|
||||
db.refresh(ws)
|
||||
|
||||
LoggingService.log(
|
||||
db=db,
|
||||
level=LoggingService.INFO,
|
||||
category=LoggingService.SYSTEM,
|
||||
action="workspace_create",
|
||||
message=f"Workspace '{ws.name}' created",
|
||||
username=current_user.username,
|
||||
user_id=current_user.id,
|
||||
resource_type="workspace",
|
||||
resource_id=str(ws.id),
|
||||
resource_name=ws.name,
|
||||
success=True,
|
||||
)
|
||||
logger.info(f"User {current_user.username} created workspace: {ws.name}")
|
||||
return _build_detail(ws)
|
||||
|
||||
|
||||
@router.get("/api/workspaces/{workspace_id}", response_model=WorkspaceDetail)
|
||||
def get_workspace(
|
||||
workspace_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get workspace detail with servers and users list."""
|
||||
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not ws:
|
||||
raise HTTPException(status_code=404, detail="Workspace not found")
|
||||
|
||||
if not current_user.is_admin:
|
||||
access = db.query(WorkspaceUser).filter(
|
||||
WorkspaceUser.workspace_id == workspace_id,
|
||||
WorkspaceUser.user_id == current_user.id,
|
||||
).first()
|
||||
if not access and not ws.is_default:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
return _build_detail(ws)
|
||||
|
||||
|
||||
@router.put("/api/workspaces/{workspace_id}", response_model=WorkspaceDetail)
|
||||
def update_workspace(
|
||||
workspace_id: int,
|
||||
data: WorkspaceUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("settings.panel")),
|
||||
):
|
||||
"""Update workspace name/description/color (admin only)."""
|
||||
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not ws:
|
||||
raise HTTPException(status_code=404, detail="Workspace not found")
|
||||
|
||||
if data.name is not None:
|
||||
dup = db.query(Workspace).filter(
|
||||
Workspace.name == data.name, Workspace.id != workspace_id
|
||||
).first()
|
||||
if dup:
|
||||
raise HTTPException(status_code=400, detail=f"Name '{data.name}' already taken")
|
||||
ws.name = data.name
|
||||
if data.description is not None:
|
||||
ws.description = data.description
|
||||
if data.color is not None:
|
||||
ws.color = data.color
|
||||
|
||||
db.commit()
|
||||
db.refresh(ws)
|
||||
logger.info(f"User {current_user.username} updated workspace {ws.id}: {ws.name}")
|
||||
return _build_detail(ws)
|
||||
|
||||
|
||||
@router.delete("/api/workspaces/{workspace_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_workspace(
|
||||
workspace_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("settings.panel")),
|
||||
):
|
||||
"""Delete workspace (admin only). Default workspace cannot be deleted."""
|
||||
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not ws:
|
||||
raise HTTPException(status_code=404, detail="Workspace not found")
|
||||
if ws.is_default:
|
||||
raise HTTPException(status_code=400, detail="Cannot delete the Default workspace")
|
||||
|
||||
db.delete(ws)
|
||||
db.commit()
|
||||
logger.info(f"User {current_user.username} deleted workspace {workspace_id}: {ws.name}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Server assignment
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
from pydantic import BaseModel as _BaseModel
|
||||
|
||||
|
||||
class _IdsRequest(_BaseModel):
|
||||
ids: List[int]
|
||||
|
||||
|
||||
@router.post("/api/workspaces/{workspace_id}/servers", response_model=WorkspaceDetail)
|
||||
def assign_servers(
|
||||
workspace_id: int,
|
||||
body: _IdsRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("settings.panel")),
|
||||
):
|
||||
"""Replace the server list of a workspace (admin only)."""
|
||||
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not ws:
|
||||
raise HTTPException(status_code=404, detail="Workspace not found")
|
||||
|
||||
# Remove existing
|
||||
db.query(WorkspaceServer).filter(WorkspaceServer.workspace_id == workspace_id).delete()
|
||||
|
||||
# Add new
|
||||
for server_id in body.ids:
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if server:
|
||||
db.add(WorkspaceServer(workspace_id=workspace_id, server_id=server_id))
|
||||
|
||||
db.commit()
|
||||
db.refresh(ws)
|
||||
return _build_detail(ws)
|
||||
|
||||
|
||||
@router.delete("/api/workspaces/{workspace_id}/servers/{server_id}", response_model=WorkspaceDetail)
|
||||
def remove_server(
|
||||
workspace_id: int,
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("settings.panel")),
|
||||
):
|
||||
"""Remove a single server from a workspace (admin only)."""
|
||||
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not ws:
|
||||
raise HTTPException(status_code=404, detail="Workspace not found")
|
||||
|
||||
db.query(WorkspaceServer).filter(
|
||||
WorkspaceServer.workspace_id == workspace_id,
|
||||
WorkspaceServer.server_id == server_id,
|
||||
).delete()
|
||||
db.commit()
|
||||
db.refresh(ws)
|
||||
return _build_detail(ws)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# User assignment
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.post("/api/workspaces/{workspace_id}/users", response_model=WorkspaceDetail)
|
||||
def assign_users(
|
||||
workspace_id: int,
|
||||
body: _IdsRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("settings.panel")),
|
||||
):
|
||||
"""Replace the user list of a workspace (admin only)."""
|
||||
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not ws:
|
||||
raise HTTPException(status_code=404, detail="Workspace not found")
|
||||
|
||||
db.query(WorkspaceUser).filter(WorkspaceUser.workspace_id == workspace_id).delete()
|
||||
|
||||
for user_id in body.ids:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if user:
|
||||
db.add(WorkspaceUser(workspace_id=workspace_id, user_id=user_id))
|
||||
|
||||
db.commit()
|
||||
db.refresh(ws)
|
||||
return _build_detail(ws)
|
||||
|
||||
|
||||
@router.delete("/api/workspaces/{workspace_id}/users/{user_id}", response_model=WorkspaceDetail)
|
||||
def remove_user(
|
||||
workspace_id: int,
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("settings.panel")),
|
||||
):
|
||||
"""Remove a single user from a workspace (admin only)."""
|
||||
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not ws:
|
||||
raise HTTPException(status_code=404, detail="Workspace not found")
|
||||
|
||||
db.query(WorkspaceUser).filter(
|
||||
WorkspaceUser.workspace_id == workspace_id,
|
||||
WorkspaceUser.user_id == user_id,
|
||||
).delete()
|
||||
db.commit()
|
||||
db.refresh(ws)
|
||||
return _build_detail(ws)
|
||||
Reference in New Issue
Block a user