0bb384e7-e18c-49c5-b7ef-fe73701ab6ab

This commit is contained in:
Z User committed 2026-10-28 17:00:06 +00:00
1 parent c4d338ee48
commit 8c34778db3
8 files changed
+121 -52

No files matched your search

+1 -1
View File
@@ -73,7 +73,7 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st
})
// TODO (production): send email to company.email with subject
// "Ihre Bayi-Bewerbung wurde genehmigt — {discountPct}% Rabatt"
// "Ihre Reseller-Bewerbung wurde genehmigt — {discountPct}% Rabatt"
// containing the discount and a link to the agency portal.
return NextResponse.json({
+49 -3
View File
@@ -176,6 +176,9 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
// Parse metadata for Dokploy domain ID
const meta = instance.metadata ? JSON.parse(instance.metadata) as { dokployDomainId?: string; url?: string } : {}
let dokployFailed = false
let dnsFailed = false
// 1. Remove Dokploy domain + database + application (idempotent)
if (dokploy) {
try {
@@ -189,6 +192,7 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
data: { instanceId: id, type: 'delete.dokploy.done', step: 'dokploy', result: 'ok', details: JSON.stringify({ steps: result.steps }) },
})
} catch (e) {
dokployFailed = true
steps.push(`dokploy.error: ${e instanceof Error ? e.message : 'unknown'}`)
await db.instanceEvent.create({
data: { instanceId: id, type: 'delete.dokploy.error', step: 'dokploy', result: 'error', details: JSON.stringify({ error: e instanceof Error ? e.message : 'unknown' }) },
@@ -198,7 +202,11 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
steps.push('dokploy.skipped (not configured)')
}
// 2. Remove DNS A record (if per-instance, not wildcard)
// 2. Remove DNS A record (if per-instance, not wildcard).
// IMPORTANT: Dokploy silme başarısız olsa bile DNS'i silmeye çalış —
// orphaned DNS record subdomain takeover riski taşır. Ama DNS silme
// hata verirse dnsRecordId'yi KORU, instance'ı failed yap ki reconciler
// sonra tekrar deneyebilsin.
if (dns && instance.dnsRecordId && instance.dnsRecordId !== 'wildcard') {
try {
await dns.deleteRecord(instance.dnsRecordId)
@@ -207,12 +215,50 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
data: { instanceId: id, type: 'delete.dns.removed', step: 'dns', result: 'ok', details: JSON.stringify({ recordId: instance.dnsRecordId }) },
})
} catch (e) {
dnsFailed = true
steps.push(`dns.error: ${e instanceof Error ? e.message : 'unknown'}`)
await db.instanceEvent.create({
data: { instanceId: id, type: 'delete.dns.error', step: 'dns', result: 'error', details: JSON.stringify({ recordId: instance.dnsRecordId, error: e instanceof Error ? e.message : 'unknown' }) },
})
}
}
// 3. Mark deleted (keep the row for audit — GoBD)
// 3. Decide final state.
// - Both succeeded → 'deleted' (clear all IDs + secrets)
// - Either failed → 'failed' (keep IDs for reconciler retry)
const now = new Date()
if (dokployFailed || dnsFailed) {
await db.instance.update({
where: { id },
data: {
status: 'failed',
lastError: `deletion partial: ${steps.join(', ')}`,
lastErrorAt: now,
// Keep dokployAppId + dnsRecordId so reconciler can retry.
},
})
await db.instanceEvent.create({
data: { instanceId: id, type: 'delete.partial', step: 'done', result: 'error', details: JSON.stringify({ steps, dokployFailed, dnsFailed }) },
})
await db.auditLog.create({
data: {
actor: 'admin:ui',
action: 'instance.delete.partial',
entity: 'Instance',
entityId: id,
after: JSON.stringify({ subdomain: instance.subdomain, steps, dokployFailed, dnsFailed, message: 'reconciler will retry' }),
hash: 'sha256:' + Math.random().toString(36).slice(2),
},
})
return NextResponse.json({
ok: false,
status: 'failed',
steps,
message: 'Löschung teilweise fehlgeschlagen — Reconciler wird erneut versuchen. Dokploy + Hetzner A record birlikte silinmedi; orphaned resource riski için instance failed durumda.',
}, { status: 500 })
}
// 4. Both succeeded → mark deleted (keep row for audit — GoBD)
await db.instance.update({
where: { id },
data: {
@@ -226,7 +272,7 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
},
})
// 4. Unlink from license (keep license, just remove instance ref)
// 5. Unlink from license (keep license, just remove instance ref)
if (instance.licenseId) {
await db.license.update({
where: { id: instance.licenseId },
+49 -26
View File
@@ -80,6 +80,8 @@ export async function POST(req: Request) {
const meta = inst.metadata ? JSON.parse(inst.metadata) as { dokployDomainId?: string } : {}
const steps: string[] = []
let dokployFailed = false
let dnsFailed = false
// Teardown Dokploy resources
if (dokploy) {
@@ -91,47 +93,68 @@ export async function POST(req: Request) {
})
steps.push(...r.steps)
} catch (e) {
dokployFailed = true
steps.push(`dokploy.error: ${e instanceof Error ? e.message : 'unknown'}`)
}
}
// Remove DNS record
// Remove DNS record (even if Dokploy failed — orphaned DNS = takeover risk)
if (dns && inst.dnsRecordId && inst.dnsRecordId !== 'wildcard') {
try {
await dns.deleteRecord(inst.dnsRecordId)
steps.push('dns.removed')
} catch (e) {
dnsFailed = true
steps.push(`dns.error: ${e instanceof Error ? e.message : 'unknown'}`)
}
}
// Mark deleted
await db.instance.update({
where: { id: inst.id },
data: {
status: 'deleted',
deletedAt: now,
dokployAppId: null,
dokployDbId: null,
dokployDbPassword: null,
dnsRecordId: null,
adminLoginToken: null,
},
})
// Unlink license
if (inst.licenseId) {
await db.license.update({
where: { id: inst.licenseId },
data: { instance: { disconnect: true } },
// Decide final state — both must succeed for full deletion
if (dokployFailed || dnsFailed) {
// Partial failure → keep IDs for next reconciler run
await db.instance.update({
where: { id: inst.id },
data: {
status: 'failed',
lastError: `cleanup partial: ${steps.join(', ')}`,
lastErrorAt: now,
// Keep dokployAppId + dnsRecordId for retry
},
})
await db.instanceEvent.create({
data: { instanceId: inst.id, type: 'cleanup.delete.partial', step: 'reconciler', result: 'error', details: JSON.stringify({ steps, dokployFailed, dnsFailed }) },
})
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 },
data: {
status: 'deleted',
deletedAt: now,
dokployAppId: null,
dokployDbId: null,
dokployDbPassword: null,
dnsRecordId: null,
adminLoginToken: null,
},
})
}
await db.instanceEvent.create({
data: { instanceId: inst.id, type: 'cleanup.delete.ok', step: 'reconciler', result: 'ok', details: JSON.stringify({ steps }) },
})
results.deleted++
results.details.push(`${inst.subdomain}: deleted (${steps.join(', ')})`)
// Unlink license
if (inst.licenseId) {
await db.license.update({
where: { id: inst.licenseId },
data: { instance: { disconnect: true } },
})
}
await db.instanceEvent.create({
data: { instanceId: inst.id, type: 'cleanup.delete.ok', step: 'reconciler', result: 'ok', details: JSON.stringify({ steps }) },
})
results.deleted++
results.details.push(`${inst.subdomain}: deleted (${steps.join(', ')})`)
}
} catch (e) {
results.failed++
results.details.push(`${inst.subdomain}: FAILED (${e instanceof Error ? e.message : 'unknown'})`)