Initial scaffold: FastAPI backend + Vue 3 frontend + Docker setup

Backend: config/db/security/logging core, SQLAlchemy models (Server,
Credential, UpdateJob, UpdateLog, AuditLog, User), services (winrm, ssh,
cau, audit, job_runner), REST API (auth, servers, updates, audit),
Socket.io WebSocket layer.
Frontend: Vue 3 + TS + Pinia + Tailwind, Views (Dashboard, Servers,
Updates, Audit, Login), axios + socket.io-client, nginx prod config.
This commit is contained in:
B0rbor4d
2026-07-31 23:45:31 +00:00
commit cf7c29639c
72 changed files with 10610 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
"""Update job routes: trigger, list, logs, cancel."""
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import client_ip, get_current_user
from app.core.database import get_db
from app.core.exceptions import JobNotCancellableError, NotFoundError
from app.models.server import Server
from app.models.update_job import JobStatus, UpdateJob, UpdateLog
from app.models.user import User
from app.schemas.update import JobTriggerRequest, UpdateJobRead, UpdateLogRead
from app.services.audit import AuditService
from app.services.job_runner import job_runner
router = APIRouter()
@router.post("/trigger", response_model=UpdateJobRead, status_code=201)
async def trigger_update(
payload: JobTriggerRequest,
request: Request,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
) -> UpdateJob:
server = await db.get(Server, payload.server_id)
if not server:
raise NotFoundError("Server nicht gefunden")
job = UpdateJob(
server_id=server.id,
type=payload.type,
started_by=user.username,
)
db.add(job)
await db.flush()
await AuditService(db).log(
username=user.username,
action="update.trigger",
target=server.name,
details={"job_id": job.id, "type": payload.type.value},
ip_address=client_ip(request),
)
await db.commit()
await job_runner.start(job.id)
return job
@router.get("", response_model=list[UpdateJobRead])
async def list_jobs(
status: JobStatus | None = None,
limit: int = Query(default=50, le=200),
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
) -> list[UpdateJob]:
stmt = select(UpdateJob).order_by(UpdateJob.id.desc()).limit(limit)
if status:
stmt = stmt.where(UpdateJob.status == status)
result = await db.execute(stmt)
return list(result.scalars().all())
@router.get("/{job_id}", response_model=UpdateJobRead)
async def get_job(
job_id: int,
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
) -> UpdateJob:
job = await db.get(UpdateJob, job_id)
if not job:
raise NotFoundError("Job nicht gefunden")
return job
@router.get("/{job_id}/logs", response_model=list[UpdateLogRead])
async def get_job_logs(
job_id: int,
after_id: int = 0,
limit: int = Query(default=500, le=2000),
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
) -> list[UpdateLog]:
stmt = (
select(UpdateLog)
.where(UpdateLog.job_id == job_id, UpdateLog.id > after_id)
.order_by(UpdateLog.id)
.limit(limit)
)
result = await db.execute(stmt)
return list(result.scalars().all())
@router.post("/{job_id}/cancel", response_model=UpdateJobRead)
async def cancel_job(
job_id: int,
request: Request,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
) -> UpdateJob:
job = await db.get(UpdateJob, job_id)
if not job:
raise NotFoundError("Job nicht gefunden")
if job.status not in (JobStatus.PENDING, JobStatus.RUNNING):
raise JobNotCancellableError()
cancelled = await job_runner.cancel(job_id)
if not cancelled:
job.status = JobStatus.CANCELLED
await AuditService(db).log(
username=user.username,
action="update.cancel",
target=f"job:{job_id}",
ip_address=client_ip(request),
)
return job
@router.get("/stats/summary")
async def job_stats(
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
) -> dict:
total = await db.scalar(select(func.count(UpdateJob.id)))
running = await db.scalar(
select(func.count(UpdateJob.id)).where(UpdateJob.status == JobStatus.RUNNING)
)
failed = await db.scalar(
select(func.count(UpdateJob.id)).where(UpdateJob.status == JobStatus.FAILED)
)
return {"total": total or 0, "running": running or 0, "failed": failed or 0}