@intranefr/superbackend 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.commiat +4 -0
- package/.env.example +47 -0
- package/README.md +110 -0
- package/index.js +94 -0
- package/package.json +67 -0
- package/public/css/styles.css +139 -0
- package/public/js/animations.js +41 -0
- package/sdk/error-tracking/browser/package.json +16 -0
- package/sdk/error-tracking/browser/src/core.js +270 -0
- package/sdk/error-tracking/browser/src/embed.js +18 -0
- package/sdk/error-tracking/browser/src/index.js +1 -0
- package/server.js +5 -0
- package/src/admin/endpointRegistry.js +300 -0
- package/src/controllers/admin.controller.js +321 -0
- package/src/controllers/adminAssets.controller.js +530 -0
- package/src/controllers/adminAssetsStorage.controller.js +260 -0
- package/src/controllers/adminEjsVirtual.controller.js +354 -0
- package/src/controllers/adminFeatureFlags.controller.js +155 -0
- package/src/controllers/adminHeadless.controller.js +1071 -0
- package/src/controllers/adminI18n.controller.js +604 -0
- package/src/controllers/adminJsonConfigs.controller.js +97 -0
- package/src/controllers/adminLlm.controller.js +273 -0
- package/src/controllers/adminMigration.controller.js +257 -0
- package/src/controllers/adminSeoConfig.controller.js +515 -0
- package/src/controllers/adminStats.controller.js +121 -0
- package/src/controllers/adminUploadNamespaces.controller.js +208 -0
- package/src/controllers/assets.controller.js +248 -0
- package/src/controllers/auth.controller.js +93 -0
- package/src/controllers/billing.controller.js +223 -0
- package/src/controllers/featureFlags.controller.js +35 -0
- package/src/controllers/forms.controller.js +217 -0
- package/src/controllers/globalSettings.controller.js +252 -0
- package/src/controllers/headlessCrud.controller.js +126 -0
- package/src/controllers/i18n.controller.js +12 -0
- package/src/controllers/invite.controller.js +249 -0
- package/src/controllers/jsonConfigs.controller.js +19 -0
- package/src/controllers/metrics.controller.js +149 -0
- package/src/controllers/notificationAdmin.controller.js +264 -0
- package/src/controllers/notifications.controller.js +131 -0
- package/src/controllers/org.controller.js +357 -0
- package/src/controllers/orgAdmin.controller.js +491 -0
- package/src/controllers/stripeAdmin.controller.js +410 -0
- package/src/controllers/user.controller.js +361 -0
- package/src/controllers/userAdmin.controller.js +277 -0
- package/src/controllers/waitingList.controller.js +167 -0
- package/src/controllers/webhook.controller.js +200 -0
- package/src/middleware/auth.js +66 -0
- package/src/middleware/errorCapture.js +170 -0
- package/src/middleware/headlessApiTokenAuth.js +57 -0
- package/src/middleware/org.js +108 -0
- package/src/middleware.js +901 -0
- package/src/models/ActionEvent.js +31 -0
- package/src/models/ActivityLog.js +41 -0
- package/src/models/Asset.js +84 -0
- package/src/models/AuditEvent.js +93 -0
- package/src/models/EmailLog.js +28 -0
- package/src/models/ErrorAggregate.js +72 -0
- package/src/models/FormSubmission.js +41 -0
- package/src/models/GlobalSetting.js +38 -0
- package/src/models/HeadlessApiToken.js +24 -0
- package/src/models/HeadlessModelDefinition.js +41 -0
- package/src/models/I18nEntry.js +77 -0
- package/src/models/I18nLocale.js +33 -0
- package/src/models/Invite.js +70 -0
- package/src/models/JsonConfig.js +46 -0
- package/src/models/Notification.js +60 -0
- package/src/models/Organization.js +57 -0
- package/src/models/OrganizationMember.js +43 -0
- package/src/models/StripeCatalogItem.js +77 -0
- package/src/models/StripeWebhookEvent.js +57 -0
- package/src/models/User.js +89 -0
- package/src/models/VirtualEjsFile.js +60 -0
- package/src/models/VirtualEjsFileVersion.js +43 -0
- package/src/models/VirtualEjsGroupChange.js +32 -0
- package/src/models/WaitingList.js +41 -0
- package/src/models/Webhook.js +63 -0
- package/src/models/Workflow.js +29 -0
- package/src/models/WorkflowExecution.js +12 -0
- package/src/routes/admin.routes.js +26 -0
- package/src/routes/adminAssets.routes.js +28 -0
- package/src/routes/adminAssetsStorage.routes.js +13 -0
- package/src/routes/adminAudit.routes.js +196 -0
- package/src/routes/adminEjsVirtual.routes.js +17 -0
- package/src/routes/adminErrors.routes.js +164 -0
- package/src/routes/adminFeatureFlags.routes.js +12 -0
- package/src/routes/adminHeadless.routes.js +38 -0
- package/src/routes/adminI18n.routes.js +22 -0
- package/src/routes/adminJsonConfigs.routes.js +15 -0
- package/src/routes/adminLlm.routes.js +12 -0
- package/src/routes/adminMigration.routes.js +81 -0
- package/src/routes/adminSeoConfig.routes.js +20 -0
- package/src/routes/adminUploadNamespaces.routes.js +13 -0
- package/src/routes/assets.routes.js +21 -0
- package/src/routes/auth.routes.js +12 -0
- package/src/routes/billing.routes.js +11 -0
- package/src/routes/errorTracking.routes.js +31 -0
- package/src/routes/featureFlags.routes.js +9 -0
- package/src/routes/forms.routes.js +9 -0
- package/src/routes/formsAdmin.routes.js +13 -0
- package/src/routes/globalSettings.routes.js +18 -0
- package/src/routes/headless.routes.js +15 -0
- package/src/routes/i18n.routes.js +8 -0
- package/src/routes/invite.routes.js +9 -0
- package/src/routes/jsonConfigs.routes.js +8 -0
- package/src/routes/log.routes.js +111 -0
- package/src/routes/metrics.routes.js +9 -0
- package/src/routes/notificationAdmin.routes.js +15 -0
- package/src/routes/notifications.routes.js +12 -0
- package/src/routes/org.routes.js +31 -0
- package/src/routes/orgAdmin.routes.js +20 -0
- package/src/routes/publicAssets.routes.js +7 -0
- package/src/routes/stripeAdmin.routes.js +20 -0
- package/src/routes/user.routes.js +22 -0
- package/src/routes/userAdmin.routes.js +15 -0
- package/src/routes/waitingList.routes.js +13 -0
- package/src/routes/waitingListAdmin.routes.js +9 -0
- package/src/routes/webhook.routes.js +32 -0
- package/src/routes/workflowWebhook.routes.js +54 -0
- package/src/routes/workflows.routes.js +110 -0
- package/src/services/assets.service.js +110 -0
- package/src/services/audit.service.js +62 -0
- package/src/services/auditLogger.js +165 -0
- package/src/services/ejsVirtual.service.js +614 -0
- package/src/services/email.service.js +351 -0
- package/src/services/errorLogger.js +221 -0
- package/src/services/featureFlags.service.js +202 -0
- package/src/services/forms.service.js +214 -0
- package/src/services/globalSettings.service.js +49 -0
- package/src/services/headlessApiTokens.service.js +158 -0
- package/src/services/headlessCrypto.service.js +31 -0
- package/src/services/headlessModels.service.js +356 -0
- package/src/services/i18n.service.js +314 -0
- package/src/services/i18nInferredKeys.service.js +337 -0
- package/src/services/jsonConfigs.service.js +392 -0
- package/src/services/llm.service.js +749 -0
- package/src/services/migration.service.js +581 -0
- package/src/services/migrationAssets/fsLocal.js +58 -0
- package/src/services/migrationAssets/index.js +134 -0
- package/src/services/migrationAssets/s3.js +75 -0
- package/src/services/migrationAssets/sftp.js +92 -0
- package/src/services/notification.service.js +212 -0
- package/src/services/objectStorage.service.js +514 -0
- package/src/services/seoConfig.service.js +402 -0
- package/src/services/storage.js +150 -0
- package/src/services/stripe.service.js +185 -0
- package/src/services/stripeHelper.service.js +264 -0
- package/src/services/uploadNamespaces.service.js +326 -0
- package/src/services/webhook.service.js +157 -0
- package/src/services/workflow.service.js +271 -0
- package/src/utils/asyncHandler.js +5 -0
- package/src/utils/encryption.js +80 -0
- package/src/utils/jwt.js +40 -0
- package/src/utils/orgRoles.js +156 -0
- package/src/utils/validation.js +26 -0
- package/src/utils/webhookRetry.js +93 -0
- package/views/admin-assets.ejs +444 -0
- package/views/admin-audit.ejs +283 -0
- package/views/admin-coolify-deploy.ejs +207 -0
- package/views/admin-dashboard-home.ejs +291 -0
- package/views/admin-dashboard.ejs +397 -0
- package/views/admin-ejs-virtual.ejs +280 -0
- package/views/admin-errors.ejs +368 -0
- package/views/admin-feature-flags.ejs +390 -0
- package/views/admin-forms.ejs +526 -0
- package/views/admin-global-settings.ejs +436 -0
- package/views/admin-headless.ejs +2020 -0
- package/views/admin-i18n-locales.ejs +221 -0
- package/views/admin-i18n.ejs +728 -0
- package/views/admin-json-configs.ejs +410 -0
- package/views/admin-llm.ejs +884 -0
- package/views/admin-metrics.ejs +274 -0
- package/views/admin-migration.ejs +814 -0
- package/views/admin-notifications.ejs +430 -0
- package/views/admin-organizations.ejs +984 -0
- package/views/admin-seo-config.ejs +673 -0
- package/views/admin-stripe-pricing.ejs +558 -0
- package/views/admin-test.ejs +342 -0
- package/views/admin-users.ejs +452 -0
- package/views/admin-waiting-list.ejs +547 -0
- package/views/admin-webhooks.ejs +329 -0
- package/views/admin-workflows.ejs +310 -0
- package/views/partials/admin-assets-script.ejs +2022 -0
- package/views/partials/admin-test-sidebar.ejs +14 -0
- package/views/partials/dashboard/nav-items.ejs +66 -0
- package/views/partials/dashboard/palette.ejs +63 -0
- package/views/partials/dashboard/sidebar.ejs +21 -0
- package/views/partials/dashboard/tab-bar.ejs +26 -0
- package/views/partials/footer.ejs +3 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Audit Log - Admin</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body class="bg-gray-100">
|
|
10
|
+
<div class="min-h-screen">
|
|
11
|
+
<div class="bg-white shadow">
|
|
12
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
13
|
+
<div class="flex justify-between items-center">
|
|
14
|
+
<div>
|
|
15
|
+
<h1 class="text-2xl font-bold text-gray-900">Audit Log</h1>
|
|
16
|
+
<p class="text-sm text-gray-600 mt-1">Admin + user actions with request context</p>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="flex items-center gap-4">
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
25
|
+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
|
26
|
+
<div class="flex gap-4 text-sm">
|
|
27
|
+
<div class="bg-white border border-gray-200 rounded-lg px-4 py-2">
|
|
28
|
+
<span class="text-gray-500">Last 24h:</span>
|
|
29
|
+
<span id="stat-24h" class="text-blue-600 font-semibold ml-1">—</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="bg-white border border-gray-200 rounded-lg px-4 py-2">
|
|
32
|
+
<span class="text-gray-500">Failures 24h:</span>
|
|
33
|
+
<span id="stat-failures" class="text-orange-600 font-semibold ml-1">—</span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<button onclick="refreshAll()" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Refresh</button>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="bg-white border border-gray-200 rounded-xl p-4 mb-6">
|
|
40
|
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
|
|
41
|
+
<select id="filter-actor-type" class="border rounded px-3 py-2 text-sm">
|
|
42
|
+
<option value="">All Actor Types</option>
|
|
43
|
+
<option value="admin">admin</option>
|
|
44
|
+
<option value="user">user</option>
|
|
45
|
+
<option value="system">system</option>
|
|
46
|
+
</select>
|
|
47
|
+
<select id="filter-outcome" class="border rounded px-3 py-2 text-sm">
|
|
48
|
+
<option value="">All Outcomes</option>
|
|
49
|
+
<option value="success">success</option>
|
|
50
|
+
<option value="failure">failure</option>
|
|
51
|
+
</select>
|
|
52
|
+
<select id="filter-action" class="border rounded px-3 py-2 text-sm">
|
|
53
|
+
<option value="">All Actions</option>
|
|
54
|
+
</select>
|
|
55
|
+
<input id="filter-search" type="text" placeholder="Search..." class="border rounded px-3 py-2 text-sm" />
|
|
56
|
+
</div>
|
|
57
|
+
<div class="mt-3 flex justify-end">
|
|
58
|
+
<button id="btn-filter" class="bg-gray-900 text-white px-4 py-2 rounded hover:bg-gray-800 text-sm">Filter</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="bg-white border border-gray-200 rounded-xl overflow-hidden">
|
|
63
|
+
<table class="w-full">
|
|
64
|
+
<thead class="bg-gray-50">
|
|
65
|
+
<tr class="text-left text-xs uppercase tracking-wide text-gray-500">
|
|
66
|
+
<th class="px-4 py-3 w-56">At</th>
|
|
67
|
+
<th class="px-4 py-3">Action</th>
|
|
68
|
+
<th class="px-4 py-3 w-32">Actor</th>
|
|
69
|
+
<th class="px-4 py-3 w-24">Outcome</th>
|
|
70
|
+
<th class="px-4 py-3 w-48">Target</th>
|
|
71
|
+
<th class="px-4 py-3 w-24"></th>
|
|
72
|
+
</tr>
|
|
73
|
+
</thead>
|
|
74
|
+
<tbody id="audit-table" class="divide-y divide-gray-100">
|
|
75
|
+
<tr><td colspan="6" class="px-4 py-8 text-center text-gray-500">Loading...</td></tr>
|
|
76
|
+
</tbody>
|
|
77
|
+
</table>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="flex items-center justify-between mt-4 text-sm text-gray-600">
|
|
81
|
+
<div id="pagination-info"></div>
|
|
82
|
+
<div class="flex gap-2">
|
|
83
|
+
<button id="btn-prev" class="px-3 py-1 bg-white border border-gray-300 rounded hover:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed">Prev</button>
|
|
84
|
+
<button id="btn-next" class="px-3 py-1 bg-white border border-gray-300 rounded hover:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed">Next</button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div id="modal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
|
|
91
|
+
<div class="bg-white rounded-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
|
92
|
+
<div class="flex items-center justify-between px-6 py-4 border-b">
|
|
93
|
+
<h2 id="modal-title" class="text-xl font-bold text-gray-900">Audit Event</h2>
|
|
94
|
+
<button id="modal-close" class="text-gray-500 hover:text-gray-800">Close</button>
|
|
95
|
+
</div>
|
|
96
|
+
<div id="modal-content" class="flex-1 overflow-y-auto p-6"></div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<script>
|
|
101
|
+
const API_BASE = window.location.origin + "<%= baseUrl %>" || window.location.origin;
|
|
102
|
+
let currentPage = 1;
|
|
103
|
+
const pageSize = 50;
|
|
104
|
+
let totalPages = 1;
|
|
105
|
+
|
|
106
|
+
async function loadStats() {
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch(`${API_BASE}/api/admin/audit/stats`);
|
|
109
|
+
if (!res.ok) return;
|
|
110
|
+
const stats = await res.json();
|
|
111
|
+
document.getElementById('stat-24h').textContent = stats.last24h ?? 0;
|
|
112
|
+
document.getElementById('stat-failures').textContent = stats.failures24h ?? 0;
|
|
113
|
+
} catch (e) {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function loadActions() {
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetch(`${API_BASE}/api/admin/audit/actions`);
|
|
119
|
+
if (!res.ok) return;
|
|
120
|
+
const data = await res.json();
|
|
121
|
+
const select = document.getElementById('filter-action');
|
|
122
|
+
(data.actions || []).forEach(action => {
|
|
123
|
+
const opt = document.createElement('option');
|
|
124
|
+
opt.value = action;
|
|
125
|
+
opt.textContent = action;
|
|
126
|
+
select.appendChild(opt);
|
|
127
|
+
});
|
|
128
|
+
} catch (e) {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function loadEvents() {
|
|
132
|
+
const actorType = document.getElementById('filter-actor-type').value;
|
|
133
|
+
const outcome = document.getElementById('filter-outcome').value;
|
|
134
|
+
const action = document.getElementById('filter-action').value;
|
|
135
|
+
const q = document.getElementById('filter-search').value;
|
|
136
|
+
|
|
137
|
+
const params = new URLSearchParams();
|
|
138
|
+
if (actorType) params.set('actorType', actorType);
|
|
139
|
+
if (outcome) params.set('outcome', outcome);
|
|
140
|
+
if (action) params.set('action', action);
|
|
141
|
+
if (q) params.set('q', q);
|
|
142
|
+
params.set('page', currentPage);
|
|
143
|
+
params.set('pageSize', pageSize);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const res = await fetch(`${API_BASE}/api/admin/audit?` + params.toString());
|
|
147
|
+
if (!res.ok) throw new Error('Failed to load events');
|
|
148
|
+
const data = await res.json();
|
|
149
|
+
totalPages = data.totalPages || 1;
|
|
150
|
+
renderEvents(data.events || []);
|
|
151
|
+
renderPagination(data);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
document.getElementById('audit-table').innerHTML = '<tr><td colspan="6" class="px-4 py-8 text-center text-red-600">Failed to load events</td></tr>';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderEvents(events) {
|
|
158
|
+
const tbody = document.getElementById('audit-table');
|
|
159
|
+
if (!events.length) {
|
|
160
|
+
tbody.innerHTML = '<tr><td colspan="6" class="px-4 py-8 text-center text-gray-500">No audit events found</td></tr>';
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
tbody.innerHTML = events.map(evt => {
|
|
165
|
+
const actorBadge = evt.actorType === 'admin'
|
|
166
|
+
? '<span class="px-2 py-1 text-xs rounded bg-purple-100 text-purple-700">admin</span>'
|
|
167
|
+
: (evt.actorType === 'user'
|
|
168
|
+
? '<span class="px-2 py-1 text-xs rounded bg-blue-100 text-blue-700">user</span>'
|
|
169
|
+
: '<span class="px-2 py-1 text-xs rounded bg-gray-100 text-gray-700">system</span>');
|
|
170
|
+
|
|
171
|
+
const outcomeColor = evt.outcome === 'success' ? 'text-green-600' : 'text-orange-600';
|
|
172
|
+
const targetText = evt.targetType || evt.entityType || '';
|
|
173
|
+
const targetId = evt.targetId || evt.entityId || '';
|
|
174
|
+
|
|
175
|
+
return `
|
|
176
|
+
<tr class="hover:bg-gray-50 cursor-pointer" onclick="showDetails('${evt._id}')">
|
|
177
|
+
<td class="px-4 py-3 text-sm text-gray-600">${formatDate(evt.createdAt)}</td>
|
|
178
|
+
<td class="px-4 py-3">
|
|
179
|
+
<div class="text-gray-900 font-medium">${escapeHtml(evt.action || '')}</div>
|
|
180
|
+
${evt.context?.path ? `<div class="text-gray-500 text-xs">${escapeHtml(evt.context.method || 'GET')} ${escapeHtml(evt.context.path)}</div>` : ''}
|
|
181
|
+
</td>
|
|
182
|
+
<td class="px-4 py-3">${actorBadge}</td>
|
|
183
|
+
<td class="px-4 py-3 text-sm ${outcomeColor}">${escapeHtml(evt.outcome || 'success')}</td>
|
|
184
|
+
<td class="px-4 py-3 text-sm text-gray-700">${escapeHtml(targetText)} ${targetId ? `<span class="text-xs text-gray-500">${escapeHtml(String(targetId).slice(-8))}</span>` : ''}</td>
|
|
185
|
+
<td class="px-4 py-3"><button onclick="event.stopPropagation(); showDetails('${evt._id}')" class="text-blue-600 hover:underline text-sm">View</button></td>
|
|
186
|
+
</tr>
|
|
187
|
+
`;
|
|
188
|
+
}).join('');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderPagination(data) {
|
|
192
|
+
document.getElementById('pagination-info').textContent = `Page ${data.page} of ${data.totalPages} (${data.total} total)`;
|
|
193
|
+
document.getElementById('btn-prev').disabled = data.page <= 1;
|
|
194
|
+
document.getElementById('btn-next').disabled = data.page >= data.totalPages;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function showDetails(id) {
|
|
198
|
+
try {
|
|
199
|
+
const res = await fetch(`${API_BASE}/api/admin/audit/${id}`);
|
|
200
|
+
if (!res.ok) throw new Error('Failed to load event details');
|
|
201
|
+
const evt = await res.json();
|
|
202
|
+
renderModal(evt);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
alert('Failed to load event details');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function renderModal(evt) {
|
|
209
|
+
document.getElementById('modal-title').textContent = evt.action || 'Audit Event';
|
|
210
|
+
document.getElementById('modal-content').innerHTML = `
|
|
211
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-6 text-sm">
|
|
212
|
+
<div><span class="text-gray-500">Actor Type:</span> <span class="ml-2 text-gray-900">${escapeHtml(evt.actorType || '')}</span></div>
|
|
213
|
+
<div><span class="text-gray-500">Actor Id:</span> <span class="ml-2 text-gray-900">${escapeHtml(evt.actorId || '')}</span></div>
|
|
214
|
+
<div><span class="text-gray-500">Outcome:</span> <span class="ml-2 text-gray-900">${escapeHtml(evt.outcome || '')}</span></div>
|
|
215
|
+
<div><span class="text-gray-500">Entity:</span> <span class="ml-2 text-gray-900">${escapeHtml(evt.entityType || '')} ${escapeHtml(evt.entityId || '')}</span></div>
|
|
216
|
+
<div><span class="text-gray-500">Target:</span> <span class="ml-2 text-gray-900">${escapeHtml(evt.targetType || '')} ${escapeHtml(evt.targetId || '')}</span></div>
|
|
217
|
+
<div><span class="text-gray-500">Request Id:</span> <span class="ml-2 text-gray-900">${escapeHtml(evt.context?.requestId || '')}</span></div>
|
|
218
|
+
<div><span class="text-gray-500">Path:</span> <span class="ml-2 text-gray-900">${escapeHtml(evt.context?.path || '')}</span></div>
|
|
219
|
+
<div><span class="text-gray-500">IP:</span> <span class="ml-2 text-gray-900">${escapeHtml(evt.context?.ip || '')}</span></div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div class="mb-4">
|
|
223
|
+
<div class="text-gray-500 text-sm">Before</div>
|
|
224
|
+
<pre class="mt-1 p-3 bg-gray-50 rounded text-xs overflow-x-auto">${escapeHtml(JSON.stringify(evt.before ?? null, null, 2))}</pre>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div class="mb-4">
|
|
228
|
+
<div class="text-gray-500 text-sm">After</div>
|
|
229
|
+
<pre class="mt-1 p-3 bg-gray-50 rounded text-xs overflow-x-auto">${escapeHtml(JSON.stringify(evt.after ?? null, null, 2))}</pre>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div class="mb-4">
|
|
233
|
+
<div class="text-gray-500 text-sm">Meta</div>
|
|
234
|
+
<pre class="mt-1 p-3 bg-gray-50 rounded text-xs overflow-x-auto">${escapeHtml(JSON.stringify(evt.meta ?? null, null, 2))}</pre>
|
|
235
|
+
</div>
|
|
236
|
+
`;
|
|
237
|
+
|
|
238
|
+
document.getElementById('modal').classList.remove('hidden');
|
|
239
|
+
document.getElementById('modal').classList.add('flex');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function closeModal() {
|
|
243
|
+
document.getElementById('modal').classList.add('hidden');
|
|
244
|
+
document.getElementById('modal').classList.remove('flex');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function escapeHtml(str) {
|
|
248
|
+
const div = document.createElement('div');
|
|
249
|
+
div.textContent = str || '';
|
|
250
|
+
return div.innerHTML;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function formatDate(dateStr) {
|
|
254
|
+
if (!dateStr) return '—';
|
|
255
|
+
const d = new Date(dateStr);
|
|
256
|
+
return d.toLocaleString();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function refreshAll() {
|
|
260
|
+
loadStats();
|
|
261
|
+
loadEvents();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
document.getElementById('btn-filter').addEventListener('click', () => { currentPage = 1; loadEvents(); });
|
|
265
|
+
document.getElementById('filter-search').addEventListener('keypress', (e) => { if (e.key === 'Enter') { currentPage = 1; loadEvents(); } });
|
|
266
|
+
document.getElementById('btn-prev').addEventListener('click', () => { if (currentPage > 1) { currentPage--; loadEvents(); } });
|
|
267
|
+
document.getElementById('btn-next').addEventListener('click', () => { if (currentPage < totalPages) { currentPage++; loadEvents(); } });
|
|
268
|
+
document.getElementById('modal-close').addEventListener('click', closeModal);
|
|
269
|
+
document.getElementById('modal').addEventListener('click', (e) => { if (e.target.id === 'modal') closeModal(); });
|
|
270
|
+
|
|
271
|
+
loadActions();
|
|
272
|
+
refreshAll();
|
|
273
|
+
</script>
|
|
274
|
+
<script>
|
|
275
|
+
window.addEventListener("keydown", (e) => {
|
|
276
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
|
|
277
|
+
e.preventDefault();
|
|
278
|
+
window.parent.postMessage({ type: "keydown", ctrlK: true }, "*");
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
</script>
|
|
282
|
+
</body>
|
|
283
|
+
</html>
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Coolify Headless Deploy - SaaSBackend</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
9
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
|
10
|
+
<style>
|
|
11
|
+
[v-cloak] { display: none; }
|
|
12
|
+
</style>
|
|
13
|
+
</head>
|
|
14
|
+
<body class="bg-gray-50 p-6">
|
|
15
|
+
<div id="app" v-cloak>
|
|
16
|
+
<div class="max-w-6xl mx-auto space-y-6">
|
|
17
|
+
<!-- Header -->
|
|
18
|
+
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
|
19
|
+
<div>
|
|
20
|
+
<h2 class="text-2xl font-bold text-gray-900 flex items-center gap-2">
|
|
21
|
+
<i class="ti ti-rocket text-blue-600"></i>
|
|
22
|
+
Coolify Headless Deploy
|
|
23
|
+
</h2>
|
|
24
|
+
<p class="text-gray-600 mt-1">An opinionated way to deploy via SSH/rsync to remote servers supporting Traefik dynamic configs.</p>
|
|
25
|
+
</div>
|
|
26
|
+
<button
|
|
27
|
+
@click="provisionScript"
|
|
28
|
+
:disabled="loading.provisioning"
|
|
29
|
+
class="bg-blue-600 text-white px-6 py-2.5 rounded-lg hover:bg-blue-700 transition font-semibold shadow-sm flex items-center gap-2 disabled:opacity-50"
|
|
30
|
+
>
|
|
31
|
+
<i v-if="loading.provisioning" class="ti ti-loader-2 animate-spin"></i>
|
|
32
|
+
<i v-else class="ti ti-file-code"></i>
|
|
33
|
+
Provision manage.sh
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
38
|
+
<!-- Required Environment Variables -->
|
|
39
|
+
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200 border-t-4 border-blue-500">
|
|
40
|
+
<h3 class="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
|
|
41
|
+
<i class="ti ti-variable"></i>
|
|
42
|
+
Required .env Variables
|
|
43
|
+
</h3>
|
|
44
|
+
<div class="bg-gray-900 rounded-lg p-4 font-mono text-sm text-blue-400 overflow-x-auto relative group">
|
|
45
|
+
<button @click="copyToClipboard(envTemplate)" class="absolute top-2 right-2 p-2 hover:bg-gray-800 rounded text-gray-400 opacity-0 group-hover:opacity-100 transition">
|
|
46
|
+
<i class="ti ti-copy"></i>
|
|
47
|
+
</button>
|
|
48
|
+
<pre class="whitespace-pre text-xs">REMOTE_HOST_USER=root
|
|
49
|
+
REMOTE_HOST=188.245.71.48
|
|
50
|
+
REMOTE_HOST_PORT=22
|
|
51
|
+
REMOTE_HOST_PATH=/apps/superlandings
|
|
52
|
+
REMOTE_DOMAIN_HOST=188.245.71.48
|
|
53
|
+
REMOTE_SERVICE_IP=http://superlandings:3000
|
|
54
|
+
APP_NAME=superlandings
|
|
55
|
+
REMOTE_SYNC_EXCLUDES=data
|
|
56
|
+
REMOTE_DOMAIN_CONFIG_FILENAME=superlandings.yml</pre>
|
|
57
|
+
</div>
|
|
58
|
+
<ul class="mt-4 space-y-2 text-sm text-gray-600">
|
|
59
|
+
<li class="flex gap-2">
|
|
60
|
+
<span class="font-bold text-gray-900 min-w-[120px]">REMOTE_HOST:</span>
|
|
61
|
+
The IP address of your remote Coolify or Traefik server.
|
|
62
|
+
</li>
|
|
63
|
+
<li class="flex gap-2">
|
|
64
|
+
<span class="font-bold text-gray-900 min-w-[120px]">REMOTE_PATH:</span>
|
|
65
|
+
Target directory on the remote server.
|
|
66
|
+
</li>
|
|
67
|
+
<li class="flex gap-2">
|
|
68
|
+
<span class="font-bold text-gray-900 min-w-[120px]">SERVICE_IP:</span>
|
|
69
|
+
Internal container URL (e.g. http://app:3000).
|
|
70
|
+
</li>
|
|
71
|
+
</ul>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<!-- Usage & Commands -->
|
|
75
|
+
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200 border-t-4 border-emerald-500">
|
|
76
|
+
<h3 class="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
|
|
77
|
+
<i class="ti ti-terminal-2"></i>
|
|
78
|
+
CLI Usage & Commands
|
|
79
|
+
</h3>
|
|
80
|
+
<div class="space-y-4">
|
|
81
|
+
<div>
|
|
82
|
+
<p class="text-sm font-semibold text-gray-700 mb-2">1. Deploy Application</p>
|
|
83
|
+
<div class="bg-gray-100 p-2 rounded font-mono text-xs flex justify-between items-center">
|
|
84
|
+
<code>./manage.sh deploy</code>
|
|
85
|
+
<button @click="copyToClipboard('./manage.sh deploy')" class="p-1 hover:text-blue-600"><i class="ti ti-copy"></i></button>
|
|
86
|
+
</div>
|
|
87
|
+
<p class="text-xs text-gray-500 mt-1">Syncs files via rsync and runs docker compose remotely.</p>
|
|
88
|
+
</div>
|
|
89
|
+
<div>
|
|
90
|
+
<p class="text-sm font-semibold text-gray-700 mb-2">2. Setup Traefik Proxy</p>
|
|
91
|
+
<div class="bg-gray-100 p-2 rounded font-mono text-xs flex justify-between items-center">
|
|
92
|
+
<code>./manage.sh proxy</code>
|
|
93
|
+
<button @click="copyToClipboard('./manage.sh proxy')" class="p-1 hover:text-blue-600"><i class="ti ti-copy"></i></button>
|
|
94
|
+
</div>
|
|
95
|
+
<p class="text-xs text-gray-500 mt-1">Generates the Traefik YAML configuration file locally.</p>
|
|
96
|
+
</div>
|
|
97
|
+
<div>
|
|
98
|
+
<p class="text-sm font-semibold text-gray-700 mb-2">3. Deploy Domain</p>
|
|
99
|
+
<div class="bg-gray-100 p-2 rounded font-mono text-xs flex justify-between items-center">
|
|
100
|
+
<code>./manage.sh domain</code>
|
|
101
|
+
<button @click="copyToClipboard('./manage.sh domain')" class="p-1 hover:text-blue-600"><i class="ti ti-copy"></i></button>
|
|
102
|
+
</div>
|
|
103
|
+
<p class="text-xs text-gray-500 mt-1">Uploads the Traefik YAML to the gateway server.</p>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="mt-6 p-4 bg-amber-50 rounded-lg border border-amber-200">
|
|
108
|
+
<p class="text-xs text-amber-800 flex items-center gap-2">
|
|
109
|
+
<i class="ti ti-alert-triangle text-base"></i>
|
|
110
|
+
<strong>Dependencies:</strong> Ensure <code>rsync</code> and <code>ssh</code> are installed locally and remotely.
|
|
111
|
+
</p>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- Toast Notification -->
|
|
118
|
+
<div v-if="toast" class="fixed bottom-6 right-6 px-4 py-2 rounded-lg shadow-lg text-white transition-all duration-300" :class="toast.type === 'success' ? 'bg-emerald-600' : 'bg-red-600'">
|
|
119
|
+
{{ toast.message }}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<script>
|
|
124
|
+
const { createApp, ref } = Vue;
|
|
125
|
+
|
|
126
|
+
createApp({
|
|
127
|
+
setup() {
|
|
128
|
+
const baseUrl = '<%= baseUrl %>';
|
|
129
|
+
const loading = ref({ provisioning: false });
|
|
130
|
+
const toast = ref(null);
|
|
131
|
+
const envTemplate = `REMOTE_HOST_USER=root
|
|
132
|
+
REMOTE_HOST=188.245.71.48
|
|
133
|
+
REMOTE_HOST_PORT=22
|
|
134
|
+
REMOTE_HOST_PATH=/apps/superlandings
|
|
135
|
+
REMOTE_DOMAIN_HOST=188.245.71.48
|
|
136
|
+
REMOTE_SERVICE_IP=http://superlandings:3000
|
|
137
|
+
APP_NAME=superlandings
|
|
138
|
+
REMOTE_SYNC_EXCLUDES=data
|
|
139
|
+
REMOTE_DOMAIN_CONFIG_FILENAME=superlandings.yml`;
|
|
140
|
+
|
|
141
|
+
const showToast = (message, type = 'success') => {
|
|
142
|
+
toast.value = { message, type };
|
|
143
|
+
setTimeout(() => { toast.value = null; }, 3000);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const provisionScript = async (overwrite = false) => {
|
|
147
|
+
if (overwrite) {
|
|
148
|
+
if (!confirm('Are you sure you want to overwrite the existing manage.sh?')) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
loading.value.provisioning = true;
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(baseUrl + '/api/admin/coolify-headless-deploy/provision', {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: {
|
|
158
|
+
'Content-Type': 'application/json'
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify({ overwrite })
|
|
161
|
+
});
|
|
162
|
+
const data = await res.json();
|
|
163
|
+
|
|
164
|
+
if (data.requiresConfirmation) {
|
|
165
|
+
provisionScript(true);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (data.success) {
|
|
170
|
+
showToast(data.message);
|
|
171
|
+
} else {
|
|
172
|
+
showToast(data.error || 'Provisioning failed', 'error');
|
|
173
|
+
}
|
|
174
|
+
} catch (e) {
|
|
175
|
+
showToast('Network error during provisioning', 'error');
|
|
176
|
+
} finally {
|
|
177
|
+
loading.value.provisioning = false;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const copyToClipboard = (text) => {
|
|
182
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
183
|
+
showToast('Copied to clipboard');
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
baseUrl,
|
|
189
|
+
loading,
|
|
190
|
+
toast,
|
|
191
|
+
envTemplate,
|
|
192
|
+
provisionScript,
|
|
193
|
+
copyToClipboard
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}).mount('#app');
|
|
197
|
+
</script>
|
|
198
|
+
<script>
|
|
199
|
+
window.addEventListener("keydown", (e) => {
|
|
200
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
window.parent.postMessage({ type: "keydown", ctrlK: true }, "*");
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
</script>
|
|
206
|
+
</body>
|
|
207
|
+
</html>
|