wip: ops admin panel + migration sync + crud/rbac phase-5 snapshot
Working-tree checkpoint of in-progress work carried across prior sessions on the feat/crud-rbac branch, committed so it lands on the remote alongside the CI changes. - Operaciones admin panel: apps/api/src/ops (ingest upload, backup / restore / re-import jobs) wired into app.module + RBAC abilities, and the apps/web/src/app/operaciones page. docker-compose gets INGEST_DIR / BACKUP_DIR volumes; .gitignore excludes migration/ingest + backups. - migration/sync.py plus transform_*.py / run_all / config / dbenv / blob_extract adjustments for the additive sync path. - crud/rbac phase-5 web bits: AppShell, api/labels/types libs, globals. - schema.prisma + PLAN/RESUME doc updates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Req,
|
||||
Res,
|
||||
StreamableFile,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from "@nestjs/common";
|
||||
import { FileInterceptor } from "@nestjs/platform-express";
|
||||
import { Request, Response } from "express";
|
||||
import { AuthenticatedGuard } from "../auth/authenticated.guard";
|
||||
import { AbilityGuard } from "../auth/ability.guard";
|
||||
import { RequireAbility } from "../auth/require-ability.decorator";
|
||||
import { AuditService } from "../common/audit.service";
|
||||
import { OpsService } from "./ops.service";
|
||||
import { StartJobDto } from "./start-job.dto";
|
||||
|
||||
/** Every route is ADMIN-only (ability "db:manage"). */
|
||||
@UseGuards(AuthenticatedGuard, AbilityGuard)
|
||||
@RequireAbility("db:manage")
|
||||
@Controller("ops")
|
||||
export class OpsController {
|
||||
constructor(
|
||||
private readonly ops: OpsService,
|
||||
private readonly audit: AuditService,
|
||||
) {}
|
||||
|
||||
private actingId(req: Request): string {
|
||||
return (req.user as { id: string }).id;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- ingest */
|
||||
|
||||
@Get("ingest")
|
||||
listIngest() {
|
||||
return this.ops.listIngest();
|
||||
}
|
||||
|
||||
@Post("ingest/:name")
|
||||
@UseInterceptors(
|
||||
FileInterceptor("file", { limits: { fileSize: 500 * 1024 * 1024 } }),
|
||||
)
|
||||
async uploadIngest(
|
||||
@Param("name") name: string,
|
||||
@UploadedFile() file: { buffer: Buffer; size: number } | undefined,
|
||||
@Req() req: Request,
|
||||
) {
|
||||
if (!file) throw new Error("No se recibió ningún archivo.");
|
||||
await this.ops.saveIngest(name, file.buffer);
|
||||
void this.audit.log(this.actingId(req), "ops.ingest.upload", {
|
||||
name,
|
||||
size: file.size,
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@Delete("ingest/:name")
|
||||
async deleteIngest(@Param("name") name: string, @Req() req: Request) {
|
||||
await this.ops.deleteIngest(name);
|
||||
void this.audit.log(this.actingId(req), "ops.ingest.delete", { name });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ backups */
|
||||
|
||||
@Get("backups")
|
||||
listBackups() {
|
||||
return this.ops.listBackups();
|
||||
}
|
||||
|
||||
@Get("backups/:name/download")
|
||||
download(
|
||||
@Param("name") name: string,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
): StreamableFile {
|
||||
const { stream, name: safe } = this.ops.backupStream(name);
|
||||
res.set({
|
||||
"Content-Type": "application/gzip",
|
||||
"Content-Disposition": `attachment; filename="${safe}"`,
|
||||
});
|
||||
return new StreamableFile(stream);
|
||||
}
|
||||
|
||||
@Delete("backups/:name")
|
||||
async deleteBackup(@Param("name") name: string, @Req() req: Request) {
|
||||
await this.ops.deleteBackup(name);
|
||||
void this.audit.log(this.actingId(req), "ops.backup.delete", { name });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- jobs */
|
||||
|
||||
@Get("jobs")
|
||||
listJobs() {
|
||||
return this.ops.listJobs();
|
||||
}
|
||||
|
||||
@Get("jobs/:id")
|
||||
getJob(@Param("id") id: string) {
|
||||
return this.ops.getJob(id);
|
||||
}
|
||||
|
||||
@Post("jobs")
|
||||
async startJob(@Body() dto: StartJobDto, @Req() req: Request) {
|
||||
const userId = this.actingId(req);
|
||||
const job = await this.ops.startJob(dto.kind, { file: dto.file }, userId);
|
||||
void this.audit.log(userId, "ops.job.start", {
|
||||
jobId: job.id,
|
||||
kind: dto.kind,
|
||||
file: dto.file,
|
||||
});
|
||||
return job;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user