883bab87-6894-46db-972a-fb5bc303c0eb
This commit is contained in:
1 parent
7166c51680
commit
e2c17aea18
3 files changed
+46
-53
No files matched your search
@@ -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 })
|
||||
|
||||
@@ -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(', ')})`)
|
||||
}
|
||||
|
||||
@@ -899,9 +899,9 @@ function InstanceDetailSheet({
|
||||
{confirmingDelete ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Die Instanz wird unwiderruflich gelöscht. Dokploy-Ressourcen, DNS-Einträge
|
||||
und Volumes werden entfernt (die Datenbank-Zeile bleibt für das Audit-Log
|
||||
erhalten).
|
||||
Die Instanz wird unwiderruflich gelöscht. Dokploy-Ressourcen, DNS-Einträge,
|
||||
Volumes und der Datenbank-Datensatz (inkl. Events & Jobs) werden entfernt.
|
||||
Nur der Audit-Log-Eintrag bleibt bestehen.
|
||||
</p>
|
||||
<SlideCommit
|
||||
label="Instanz wirklich löschen →"
|
||||
|
||||
Reference in new issue
Block a user