diff --git a/.github/workflows/cleanup-cron.yml b/.github/workflows/cleanup-cron.yml new file mode 100644 index 0000000..b317840 --- /dev/null +++ b/.github/workflows/cleanup-cron.yml @@ -0,0 +1,28 @@ +name: AOS Cleanup Cron + +on: + schedule: + # Run daily at 03:00 UTC (05:00 Berlin time) + - cron: '0 3 * * *' + workflow_dispatch: # Allow manual trigger + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Trigger instance cleanup + run: | + RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ + "${{ secrets.AOS_URL || 'https://dfmpanel.eu' }}/api/aos/instances/cleanup" \ + -H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \ + -H "Content-Type: application/json" \ + --max-time 300) + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + echo "HTTP $HTTP_CODE" + echo "Response: $BODY" + if [ "$HTTP_CODE" != "200" ]; then + echo "::warning::Cleanup returned HTTP $HTTP_CODE" + exit 1 + fi + echo "::notice::Cleanup completed successfully" diff --git a/src/app/api/aos/instances/route.ts b/src/app/api/aos/instances/route.ts index 1366162..1557267 100644 --- a/src/app/api/aos/instances/route.ts +++ b/src/app/api/aos/instances/route.ts @@ -253,6 +253,20 @@ export async function POST(req: Request) { }, }) + // --- Generate admin login token (one-time, 7-day expiry) --- + // This token allows the client to access their instance admin panel + // without separate credentials. Sent via email (if SMTP configured). + const crypto = await import('crypto') + const adminToken = `admin-${crypto.randomBytes(24).toString('hex')}` + const adminTokenExpires = new Date(Date.now() + 7 * 86400000) + await db.instance.update({ + where: { id: instance.id }, + data: { + adminLoginToken: adminToken, + adminLoginExpiresAt: adminTokenExpires, + }, + }) + // --- Graphify: trigger background graph build --- // Builds a knowledge graph from the instance's Git repo so the support // ticket AI can query the codebase later. Fire-and-forget: we don't @@ -295,12 +309,26 @@ export async function POST(req: Request) { }, }) + // Send instance ready email with admin login link + const { sendEmail } = await import('@/lib/email') + if (company.email) { + const adminLoginUrl = `${result.url}/?admin_token=${adminToken}` + sendEmail({ + to: company.email, + subject: `Ihre ${product}-Instanz ist bereit — ${result.url}`, + text: `Guten Tag,\n\nIhre Instanz wurde erfolgreich bereitgestellt.\n\nURL: ${result.url}\nAdmin-Login: ${adminLoginUrl}\n${isDemo ? `Demo-Laufzeit: 14 Tage (endet am ${new Date(instance.demoExpiresAt!).toLocaleDateString('de-DE')})` : ''}\n\nMit freundlichen Grüßen\nDFM Solutions`, + html: `

Guten Tag,

Ihre ${product}-Instanz wurde erfolgreich bereitgestellt.

URL: ${result.url}
Admin-Login: ${adminLoginUrl}

${isDemo ? `

Demo-Laufzeit: 14 Tage (endet am ${new Date(instance.demoExpiresAt!).toLocaleDateString('de-DE')})

` : ''}

Mit freundlichen Grüßen
DFM Solutions

`, + }).catch(() => {}) + } + return NextResponse.json({ instanceId: instance.id, subdomain, url: result.url, status: isDemo ? 'demo_active' : 'active', demoExpiresAt: instance.demoExpiresAt, + adminLoginToken: adminToken, + adminLoginUrl: `${result.url}/?admin_token=${adminToken}`, }, { status: 201 }) } catch (provErr) { // Provisioning failed — mark as failed, keep the record for retry