feat: implementar Fases 3, 4 y 5 del roadmap (Planificacion Avanzada, Historial de Corridas y Copia Local Duplicada)
This commit is contained in:
+14
-2
@@ -10,12 +10,24 @@ from app.services.event_service import log_event
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["Authentication"])
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
async def login(credentials: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(User).where(User.email == credentials.email.strip().lower()))
|
||||
email_clean = credentials.email.strip().lower()
|
||||
result = await db.execute(select(User).where(User.email == email_clean))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not verify_password(credentials.password, user.hashed_password):
|
||||
if not user:
|
||||
logger.warning(f"Login failed: User not found with email '{email_clean}'")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password"
|
||||
)
|
||||
|
||||
if not verify_password(credentials.password, user.hashed_password):
|
||||
logger.warning(f"Login failed: Password mismatch for email '{email_clean}' (sent password length: {len(credentials.password)})")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password"
|
||||
|
||||
+91
-8
@@ -5,8 +5,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, desc
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.models import User, Client, BackupJob, BackupFile
|
||||
from app.schemas.schemas import JobCreate, JobUpdate, JobResponse, AgentJobRegister, PurgeOrphansRequest
|
||||
from app.models.models import User, Client, BackupJob, BackupFile, JobRun
|
||||
from app.schemas.schemas import (
|
||||
JobCreate, JobUpdate, JobResponse, AgentJobRegister, PurgeOrphansRequest,
|
||||
JobRunResponse, JobRunStartRequest, JobRunCompleteRequest
|
||||
)
|
||||
from app.api.deps import get_current_user, require_admin, get_current_client
|
||||
from app.services.event_service import log_event
|
||||
from app.ws.manager import ws_manager
|
||||
@@ -46,9 +49,9 @@ async def agent_register_job(
|
||||
current_client: Client = Depends(get_current_client)
|
||||
):
|
||||
"""Called by the Windows Agent to register a new local job/folder on the server."""
|
||||
count_res = await db.execute(select(func.count(BackupJob.id)))
|
||||
job_count = count_res.scalar() or 0
|
||||
job_code = f"JOB-{job_count + 1:03d}"
|
||||
max_id_res = await db.execute(select(func.max(BackupJob.id)))
|
||||
max_id = max_id_res.scalar() or 0
|
||||
job_code = f"JOB-{max_id + 1:03d}"
|
||||
|
||||
job = BackupJob(
|
||||
job_code=job_code,
|
||||
@@ -61,6 +64,7 @@ async def agent_register_job(
|
||||
skip_system=payload.skip_system,
|
||||
skip_readonly=payload.skip_readonly,
|
||||
sync_deletions=payload.sync_deletions,
|
||||
local_destination_path=payload.local_destination_path,
|
||||
schedule_cron=payload.schedule_cron,
|
||||
keep_daily=payload.keep_daily,
|
||||
keep_weekly=payload.keep_weekly,
|
||||
@@ -124,9 +128,9 @@ async def create_job(
|
||||
if not client:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Client not found")
|
||||
|
||||
count_res = await db.execute(select(func.count(BackupJob.id)))
|
||||
job_count = count_res.scalar() or 0
|
||||
job_code = f"JOB-{job_count + 1:03d}"
|
||||
max_id_res = await db.execute(select(func.max(BackupJob.id)))
|
||||
max_id = max_id_res.scalar() or 0
|
||||
job_code = f"JOB-{max_id + 1:03d}"
|
||||
|
||||
job = BackupJob(
|
||||
job_code=job_code,
|
||||
@@ -139,6 +143,7 @@ async def create_job(
|
||||
skip_system=payload.skip_system,
|
||||
skip_readonly=payload.skip_readonly,
|
||||
sync_deletions=payload.sync_deletions,
|
||||
local_destination_path=payload.local_destination_path,
|
||||
schedule_cron=payload.schedule_cron,
|
||||
keep_daily=payload.keep_daily,
|
||||
keep_weekly=payload.keep_weekly,
|
||||
@@ -322,3 +327,81 @@ async def purge_orphans(
|
||||
"freed_bytes": total_freed_bytes,
|
||||
"message": f"Successfully purged {purged_count} orphan files from server."
|
||||
}
|
||||
|
||||
@router.post("/{job_id}/runs/start", response_model=JobRunResponse)
|
||||
async def start_job_run(
|
||||
job_id: int,
|
||||
payload: JobRunStartRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_client: Client = Depends(get_current_client)
|
||||
):
|
||||
"""Called by the Windows Agent to register the start of a scheduled or manual job run."""
|
||||
res = await db.execute(select(BackupJob).where(BackupJob.id == job_id, BackupJob.client_id == current_client.id))
|
||||
job = res.scalar_one_or_none()
|
||||
if not job:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job not found or not owned by client")
|
||||
|
||||
job.status = "RUNNING"
|
||||
job.last_run_at = datetime.now(timezone.utc)
|
||||
|
||||
run = JobRun(
|
||||
job_id=job.id,
|
||||
client_id=current_client.id,
|
||||
started_at=datetime.now(timezone.utc),
|
||||
status="RUNNING"
|
||||
)
|
||||
db.add(run)
|
||||
await db.commit()
|
||||
await db.refresh(run)
|
||||
|
||||
return run
|
||||
|
||||
@router.post("/{job_id}/runs/{run_id}/complete", response_model=JobRunResponse)
|
||||
async def complete_job_run(
|
||||
job_id: int,
|
||||
run_id: int,
|
||||
payload: JobRunCompleteRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_client: Client = Depends(get_current_client)
|
||||
):
|
||||
"""Called by the Windows Agent to record completion metrics for a run."""
|
||||
res = await db.execute(select(JobRun).where(JobRun.id == run_id, JobRun.job_id == job_id, JobRun.client_id == current_client.id))
|
||||
run = res.scalar_one_or_none()
|
||||
if not run:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job run not found")
|
||||
|
||||
job_res = await db.execute(select(BackupJob).where(BackupJob.id == job_id))
|
||||
job = job_res.scalar_one_or_none()
|
||||
|
||||
run.completed_at = datetime.now(timezone.utc)
|
||||
run.status = payload.status
|
||||
run.files_scanned = payload.files_scanned
|
||||
run.files_copied = payload.files_copied
|
||||
run.files_skipped = payload.files_skipped
|
||||
run.errors_count = payload.errors_count
|
||||
run.bytes_transferred = payload.bytes_transferred
|
||||
run.error_summary = payload.error_summary
|
||||
|
||||
if job:
|
||||
job.status = payload.status
|
||||
job.last_run_at = run.completed_at
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(run)
|
||||
|
||||
return run
|
||||
|
||||
@router.get("/{job_id}/runs", response_model=List[JobRunResponse])
|
||||
async def list_job_runs(
|
||||
job_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Called by the Web UI to retrieve execution history for a job."""
|
||||
result = await db.execute(
|
||||
select(JobRun)
|
||||
.where(JobRun.job_id == job_id)
|
||||
.order_by(desc(JobRun.started_at))
|
||||
.limit(50)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
Reference in New Issue
Block a user