883bab87-6894-46db-972a-fb5bc303c0eb

This commit is contained in:
Z User committed 2026-10-29 22:50:51 +00:00
1 parent 7166c51680
commit e2c17aea18
3 files changed
+46 -53

No files matched your search

+22 -34
View File
@@ -214,7 +214,14 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st
// DELETE /api/aos/instances/[id] — tear down instance (idempotent, resumable)
// Removes: Dokploy domain → database → application + volumes → DNS record.
// Never deletes the Instance DB row (keeps audit record) — sets status=deleted.
//
// HARD DELETE: the Instance DB row is deleted entirely (cascade removes
// InstanceEvent + ProvisioningJob). Instances are infrastructure, not
// accounting records — they have NO GoBD retention requirement. Only the
// accounting module (muhasebe/buchhaltung: invoices, expenses, kassenbuch,
// belege, buchungssätze, licenses) is exempt from hard-deletion for GoBD.
// The cross-cutting AuditLog entry (separate table, string entityId) is the
// persistent record that this action happened.
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params
@@ -222,9 +229,6 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
if (!instance) {
return NextResponse.json({ error: 'instance not found' }, { status: 404 })
}
if (instance.status === 'deleted') {
return NextResponse.json({ ok: true, message: 'already deleted', steps: [] })
}
await db.instance.update({ where: { id }, data: { status: 'deleting' } })
await db.instanceEvent.create({
@@ -285,8 +289,8 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
}
// 3. Decide final state.
// - Both succeeded → 'deleted' (clear all IDs + secrets)
// - Either failed → 'failed' (keep IDs for reconciler retry)
// - Both succeeded → hard-delete the instance row (no GoBD on infra)
// - Either failed → keep row as 'failed' (retain IDs for reconciler retry)
const now = new Date()
if (dokployFailed || dnsFailed) {
await db.instance.update({
@@ -319,44 +323,28 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
}, { status: 500 })
}
// 4. Both succeeded → mark deleted (keep row for audit — GoBD)
await db.instance.update({
where: { id },
data: {
status: 'deleted',
deletedAt: now,
dokployAppId: null,
dokployDbId: null,
dnsRecordId: null,
dokployDbPassword: null, // clear secrets
adminLoginToken: null,
},
})
// 5. Unlink from license (keep license, just remove instance ref)
if (instance.licenseId) {
await db.license.update({
where: { id: instance.licenseId },
data: { instance: { disconnect: true } },
})
}
await db.instanceEvent.create({
data: { instanceId: id, type: 'delete.ok', step: 'done', result: 'ok', details: JSON.stringify({ steps, deletedAt: now }) },
})
// 4. Both succeeded → hard-delete the instance row.
// Instances are infrastructure (not accounting), so there is no GoBD
// retention requirement on the row itself. Cascade deletes the
// InstanceEvent + ProvisioningJob rows. The AuditLog entry (separate
// table) is the durable record of this action.
await db.auditLog.create({
data: {
actor: 'admin:ui',
action: 'instance.delete',
entity: 'Instance',
entityId: id,
after: JSON.stringify({ subdomain: instance.subdomain, steps }),
before: JSON.stringify({ subdomain: instance.subdomain, status: instance.status, product: instance.product, companyId: instance.companyId }),
after: JSON.stringify({ subdomain: instance.subdomain, steps, deletedAt: now }),
hash: 'sha256:' + Math.random().toString(36).slice(2),
},
})
return NextResponse.json({ ok: true, status: 'deleted', steps, deletedAt: now })
// Hard-delete: License 1:1 reverse relation is automatically cleared
// (the FK lives on the Instance row). Cascade removes events + jobs.
await db.instance.delete({ where: { id } })
return NextResponse.json({ ok: true, deleted: true, steps, deletedAt: now })
} catch (err) {
const msg = err instanceof Error ? err.message : 'internal server error'
return NextResponse.json({ error: msg }, { status: 500 })
+21 -16
View File
@@ -8,9 +8,12 @@ export const maxDuration = 300
// POST /api/aos/instances/cleanup
// Cron job: run daily. Finds instances whose grace period has elapsed and
// tears them down (Dokploy + DNS + volumes). Also finds expired demos and
// moves them to grace. Idempotent — safe to call multiple times.
// tears them down (Dokploy + DNS + volumes), then HARD-DELETES the instance
// row. Also finds expired demos and moves them to grace. Idempotent — safe
// to call multiple times.
//
// Instances are infrastructure, not accounting records — no GoBD retention.
// The cross-cutting AuditLog entry (separate table) records each deletion.
// In production, this is triggered by a cron job (e.g. via Dokploy's cron
// or a GitHub Action hitting this endpoint with a shared secret).
export async function POST(req: Request) {
@@ -126,21 +129,23 @@ export async function POST(req: Request) {
results.failed++
results.details.push(`${inst.subdomain}: PARTIAL (${steps.join(', ')}) — retry next run`)
} else {
// Both succeeded → mark deleted (clear IDs + secrets)
await db.instance.update({
where: { id: inst.id },
// Both succeeded → hard-delete the instance row.
// Instances are infrastructure (not accounting) — no GoBD retention.
// Cascade removes InstanceEvent + ProvisioningJob. The AuditLog
// entry (separate table) is the durable record.
await db.auditLog.create({
data: {
status: 'deleted',
deletedAt: now,
dokployAppId: null,
dokployDbId: null,
dokployDbPassword: null,
dnsRecordId: null,
adminLoginToken: null,
actor: 'cron:cleanup',
action: 'instance.delete',
entity: 'Instance',
entityId: inst.id,
before: JSON.stringify({ subdomain: inst.subdomain, status: inst.status, product: inst.product }),
after: JSON.stringify({ subdomain: inst.subdomain, steps, deletedAt: now }),
hash: 'sha256:' + Math.random().toString(36).slice(2),
},
})
// Unlink license
// Unlink license first (1:1 reverse relation) then hard-delete.
if (inst.licenseId) {
await db.license.update({
where: { id: inst.licenseId },
@@ -148,9 +153,9 @@ export async function POST(req: Request) {
})
}
await db.instanceEvent.create({
data: { instanceId: inst.id, type: 'cleanup.delete.ok', step: 'reconciler', result: 'ok', details: JSON.stringify({ steps }) },
})
await db.instance.delete({ where: { id: inst.id } })
// NOTE: no post-delete InstanceEvent — the row is gone and cascade
// would wipe it anyway. The AuditLog entry above is the durable record.
results.deleted++
results.details.push(`${inst.subdomain}: deleted (${steps.join(', ')})`)
}