feat: instance admin login token + cleanup cron (GitHub Action)
Instance admin login: - Provisioning generates adminLoginToken (crypto random, 7-day expiry) - Sends instance-ready email with admin login URL to customer - Token returned in provisioning response Cleanup cron: - .github/workflows/cleanup-cron.yml: daily at 03:00 UTC - Calls POST /api/aos/instances/cleanup with CRON_SECRET - Also supports manual trigger (workflow_dispatch) - Requires GitHub secrets: AOS_URL, CRON_SECRET
This commit is contained in:
1 parent
00a3a89263
commit
ec5e6b1629
2 files changed
+56
No files matched your search
@@ -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"
|
||||||
@@ -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 ---
|
// --- Graphify: trigger background graph build ---
|
||||||
// Builds a knowledge graph from the instance's Git repo so the support
|
// 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
|
// 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: `<p>Guten Tag,</p><p>Ihre <strong>${product}</strong>-Instanz wurde erfolgreich bereitgestellt.</p><p><strong>URL:</strong> <a href="${result.url}">${result.url}</a><br><strong>Admin-Login:</strong> <a href="${adminLoginUrl}">${adminLoginUrl}</a></p>${isDemo ? `<p><strong>Demo-Laufzeit:</strong> 14 Tage (endet am ${new Date(instance.demoExpiresAt!).toLocaleDateString('de-DE')})</p>` : ''}<p>Mit freundlichen Grüßen<br>DFM Solutions</p>`,
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
instanceId: instance.id,
|
instanceId: instance.id,
|
||||||
subdomain,
|
subdomain,
|
||||||
url: result.url,
|
url: result.url,
|
||||||
status: isDemo ? 'demo_active' : 'active',
|
status: isDemo ? 'demo_active' : 'active',
|
||||||
demoExpiresAt: instance.demoExpiresAt,
|
demoExpiresAt: instance.demoExpiresAt,
|
||||||
|
adminLoginToken: adminToken,
|
||||||
|
adminLoginUrl: `${result.url}/?admin_token=${adminToken}`,
|
||||||
}, { status: 201 })
|
}, { status: 201 })
|
||||||
} catch (provErr) {
|
} catch (provErr) {
|
||||||
// Provisioning failed — mark as failed, keep the record for retry
|
// Provisioning failed — mark as failed, keep the record for retry
|
||||||
|
|||||||
Reference in new issue
Block a user