utp_service/backend/app/routers/admin.py

212 lines
7 KiB
Python

"""
Admin router — plugin management and user CRUD.
All endpoints require admin role.
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_db, require_admin
from app.models.plugin import Plugin
from app.models.ssh_key import GlobalSSHCredential
from app.models.user import User
from app.plugins.loader import BUILTIN_ACTIONS
from app.plugins.registry import action_registry
from app.schemas.admin import PluginRead, PluginUpdate, UserCreate, UserRead, UserUpdate
from app.schemas.ssh_key import GlobalSSHCredentialCreate, GlobalSSHCredentialRead
from app.services.auth import hash_password
from app.services.crypto import encrypt
router = APIRouter(prefix="/api/v1/admin", tags=["admin"])
# ── Plugins ────────────────────────────────────────────────────────────────────
@router.get("/plugins", response_model=list[PluginRead])
async def list_plugins(
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
result = await db.execute(select(Plugin).order_by(Plugin.name))
plugins = result.scalars().all()
out = []
for p in plugins:
action = BUILTIN_ACTIONS.get(p.name)
out.append(
PluginRead(
name=p.name,
display_name=p.display_name,
description=p.description,
enabled=p.enabled,
registered_at=p.registered_at,
compatible_categories=action.compatible_categories if action else [],
)
)
return out
@router.patch("/plugins/{name}", response_model=PluginRead)
async def update_plugin(
name: str,
body: PluginUpdate,
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
plugin = await db.get(Plugin, name)
if plugin is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Plugin not found")
action = BUILTIN_ACTIONS.get(name)
if body.enabled is not None:
plugin.enabled = body.enabled
if body.enabled:
if action:
action_registry.register(action)
else:
action_registry.unregister(name)
if body.compatible_categories is not None:
plugin.compatible_categories = body.compatible_categories
if action:
action.compatible_categories = body.compatible_categories
await db.flush()
return PluginRead(
name=plugin.name,
display_name=plugin.display_name,
description=plugin.description,
enabled=plugin.enabled,
registered_at=plugin.registered_at,
compatible_categories=plugin.compatible_categories,
)
# ── Users ──────────────────────────────────────────────────────────────────────
@router.get("/users", response_model=list[UserRead])
async def list_users(
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
result = await db.execute(select(User).order_by(User.username))
return result.scalars().all()
@router.post("/users", response_model=UserRead, status_code=status.HTTP_201_CREATED)
async def create_user(
body: UserCreate,
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
# Check username uniqueness
existing = await db.execute(select(User).where(User.username == body.username))
if existing.scalar_one_or_none() is not None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Username '{body.username}' is already taken",
)
user = User(
username=body.username,
email=body.email,
full_name=body.full_name,
password_hash=hash_password(body.password),
role=body.role,
is_active=True,
)
db.add(user)
await db.flush()
await db.refresh(user)
return user
@router.patch("/users/{user_id}", response_model=UserRead)
async def update_user(
user_id: int,
body: UserUpdate,
db: AsyncSession = Depends(get_db),
current_admin: User = Depends(require_admin),
):
user = await db.get(User, user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
if body.email is not None:
user.email = body.email
if body.full_name is not None:
user.full_name = body.full_name
if body.role is not None:
user.role = body.role
if body.is_active is not None:
user.is_active = body.is_active
if body.password:
user.password_hash = hash_password(body.password)
await db.flush()
await db.refresh(user)
return user
@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
user_id: int,
db: AsyncSession = Depends(get_db),
current_admin: User = Depends(require_admin),
):
if user_id == current_admin.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot delete your own account",
)
user = await db.get(User, user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
await db.delete(user)
# ── Global SSH Credentials ─────────────────────────────────────────────────────
@router.get("/global-ssh-credentials", response_model=list[GlobalSSHCredentialRead])
async def list_global_ssh_credentials(
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
result = await db.execute(
select(GlobalSSHCredential).order_by(GlobalSSHCredential.priority, GlobalSSHCredential.id)
)
return result.scalars().all()
@router.post("/global-ssh-credentials", response_model=GlobalSSHCredentialRead, status_code=status.HTTP_201_CREATED)
async def create_global_ssh_credential(
body: GlobalSSHCredentialCreate,
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
cred = GlobalSSHCredential(
type=body.type,
username=body.username,
password_enc=encrypt(body.password) if body.password else None,
ssh_key_id=body.ssh_key_id,
priority=body.priority,
description=body.description,
)
db.add(cred)
await db.flush()
await db.refresh(cred)
return cred
@router.delete("/global-ssh-credentials/{cred_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_global_ssh_credential(
cred_id: int,
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
cred = await db.get(GlobalSSHCredential, cred_id)
if cred is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Credential not found")
await db.delete(cred)