208 lines
6.9 KiB
Python
208 lines
6.9 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")
|
|
|
|
plugin.enabled = body.enabled
|
|
|
|
# Hot-reload the registry immediately
|
|
if body.enabled:
|
|
action = BUILTIN_ACTIONS.get(name)
|
|
if action:
|
|
action_registry.register(action)
|
|
else:
|
|
action_registry.unregister(name)
|
|
|
|
await db.flush()
|
|
action = BUILTIN_ACTIONS.get(name)
|
|
return PluginRead(
|
|
name=plugin.name,
|
|
display_name=plugin.display_name,
|
|
description=plugin.description,
|
|
enabled=plugin.enabled,
|
|
registered_at=plugin.registered_at,
|
|
compatible_categories=action.compatible_categories if action else [],
|
|
)
|
|
|
|
|
|
# ── 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)
|