@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,410 @@
|
|
|
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
|
+
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
|
7
|
+
<meta http-equiv="Pragma" content="no-cache" />
|
|
8
|
+
<meta http-equiv="Expires" content="0" />
|
|
9
|
+
<title>JSON Configs - Admin</title>
|
|
10
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
11
|
+
<style>
|
|
12
|
+
.toast {
|
|
13
|
+
animation: slideIn 0.3s ease-out;
|
|
14
|
+
}
|
|
15
|
+
@keyframes slideIn {
|
|
16
|
+
from {
|
|
17
|
+
transform: translateX(100%);
|
|
18
|
+
opacity: 0;
|
|
19
|
+
}
|
|
20
|
+
to {
|
|
21
|
+
transform: translateX(0);
|
|
22
|
+
opacity: 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
.fade-out {
|
|
26
|
+
animation: fadeOut 0.3s ease-out forwards;
|
|
27
|
+
}
|
|
28
|
+
@keyframes fadeOut {
|
|
29
|
+
from {
|
|
30
|
+
opacity: 1;
|
|
31
|
+
}
|
|
32
|
+
to {
|
|
33
|
+
opacity: 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body class="bg-gray-100">
|
|
39
|
+
<div class="min-h-screen">
|
|
40
|
+
<div class="bg-white shadow">
|
|
41
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
42
|
+
<div class="flex justify-between items-center">
|
|
43
|
+
<div>
|
|
44
|
+
<h1 class="text-2xl font-bold text-gray-900">JSON Configs</h1>
|
|
45
|
+
<p class="text-sm text-gray-600 mt-1">Manage validated JSON blobs with optional public access and TTL caching.</p>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="flex items-center gap-4">
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
54
|
+
<div class="mb-6 flex flex-wrap justify-between items-center gap-4">
|
|
55
|
+
<button id="btnReload" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 flex items-center">
|
|
56
|
+
Refresh
|
|
57
|
+
</button>
|
|
58
|
+
<button id="btnNew" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 flex items-center">
|
|
59
|
+
New Config
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
64
|
+
<div class="bg-white rounded-lg shadow">
|
|
65
|
+
<div class="px-4 py-3 border-b">
|
|
66
|
+
<div class="flex items-center justify-between">
|
|
67
|
+
<div>
|
|
68
|
+
<h2 class="text-lg font-semibold text-gray-900">Configs</h2>
|
|
69
|
+
<p class="text-sm text-gray-600">Click a row to edit.</p>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="overflow-x-auto">
|
|
75
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
76
|
+
<thead class="bg-gray-50">
|
|
77
|
+
<tr>
|
|
78
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Title</th>
|
|
79
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Slug</th>
|
|
80
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Alias</th>
|
|
81
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Public</th>
|
|
82
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">TTL</th>
|
|
83
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Updated</th>
|
|
84
|
+
</tr>
|
|
85
|
+
</thead>
|
|
86
|
+
<tbody id="rows" class="bg-white divide-y divide-gray-200"></tbody>
|
|
87
|
+
</table>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="bg-white rounded-lg shadow">
|
|
92
|
+
<div class="px-4 py-3 border-b">
|
|
93
|
+
<div class="flex justify-between items-center">
|
|
94
|
+
<h2 class="text-lg font-semibold text-gray-900" id="editorTitle">Editor</h2>
|
|
95
|
+
<div class="flex gap-2">
|
|
96
|
+
<button class="px-3 py-2 bg-red-600 text-white rounded hover:bg-red-700" id="btnDelete" style="display:none;">Delete</button>
|
|
97
|
+
<button class="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" id="btnSave">Save</button>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="p-4">
|
|
103
|
+
<div class="mb-4">
|
|
104
|
+
<label for="title" class="block text-sm font-medium mb-2">Title</label>
|
|
105
|
+
<input id="title" type="text" class="w-full border rounded px-3 py-2" placeholder="e.g. Homepage copy" />
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="mb-4">
|
|
109
|
+
<label for="alias" class="block text-sm font-medium mb-2">Alias (optional)</label>
|
|
110
|
+
<input id="alias" type="text" class="w-full border rounded px-3 py-2" placeholder="e.g. my-app-config" />
|
|
111
|
+
<p class="text-xs text-gray-500 mt-1">Custom slug. Must be unique and will be normalized to lowercase with hyphens.</p>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="mb-4">
|
|
115
|
+
<label class="block text-sm font-medium mb-2">Slug (readonly)</label>
|
|
116
|
+
<div class="flex gap-2">
|
|
117
|
+
<input id="slug" type="text" class="w-full border rounded px-3 py-2" readonly />
|
|
118
|
+
<button class="px-3 py-2 bg-gray-800 text-white rounded hover:bg-gray-900 whitespace-nowrap" id="btnRegenerate" title="Regenerate slug">Regenerate</button>
|
|
119
|
+
</div>
|
|
120
|
+
<p class="text-xs text-gray-500 mt-1">Slug stays stable unless you click regenerate.</p>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div class="mb-4">
|
|
124
|
+
<label class="flex items-center">
|
|
125
|
+
<input id="publicEnabled" type="checkbox" class="mr-2" />
|
|
126
|
+
<span class="text-sm font-medium">Public endpoint enabled</span>
|
|
127
|
+
</label>
|
|
128
|
+
<div class="text-xs text-gray-600 mt-1" id="publicUrl" style="display:none;"></div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="mb-4">
|
|
132
|
+
<label for="cacheTtlSeconds" class="block text-sm font-medium mb-2">Cache TTL (seconds)</label>
|
|
133
|
+
<div class="flex gap-2">
|
|
134
|
+
<input id="cacheTtlSeconds" type="number" min="0" step="1" value="0" class="w-full border rounded px-3 py-2" />
|
|
135
|
+
<button class="px-3 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 whitespace-nowrap" id="btnClearCache">Clear cache</button>
|
|
136
|
+
</div>
|
|
137
|
+
<p class="text-xs text-gray-500 mt-1">0 disables server-side cache (default).</p>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="mb-4">
|
|
141
|
+
<label for="jsonRaw" class="block text-sm font-medium mb-2">JSON (validated on save)</label>
|
|
142
|
+
<textarea id="jsonRaw" spellcheck="false" class="w-full border rounded px-3 py-2 font-mono text-sm" rows="10" placeholder="{\n \"hello\": \"world\"\n}"></textarea>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div>
|
|
146
|
+
<div class="flex justify-between items-center mb-2">
|
|
147
|
+
<label class="block text-sm font-medium">Internal helper snippet</label>
|
|
148
|
+
<button class="px-3 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300" id="btnCopy">Copy</button>
|
|
149
|
+
</div>
|
|
150
|
+
<pre id="snippet" class="bg-gray-50 rounded p-3 text-sm overflow-auto"></pre>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
|
|
159
|
+
|
|
160
|
+
<script>
|
|
161
|
+
const API_BASE = window.location.origin + "<%= baseUrl %>" || window.location.origin;
|
|
162
|
+
let items = [];
|
|
163
|
+
let selectedId = null;
|
|
164
|
+
|
|
165
|
+
function $(id) { return document.getElementById(id); }
|
|
166
|
+
|
|
167
|
+
function showToast(message, type = 'success') {
|
|
168
|
+
const container = document.getElementById('toast-container');
|
|
169
|
+
const toast = document.createElement('div');
|
|
170
|
+
toast.className = `toast px-6 py-4 rounded-lg shadow-lg text-white ${
|
|
171
|
+
type === 'success' ? 'bg-green-500' : 'bg-red-500'
|
|
172
|
+
}`;
|
|
173
|
+
toast.textContent = message;
|
|
174
|
+
container.appendChild(toast);
|
|
175
|
+
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
toast.classList.add('fade-out');
|
|
178
|
+
setTimeout(() => toast.remove(), 300);
|
|
179
|
+
}, 2500);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatDate(iso) {
|
|
183
|
+
if (!iso) return '';
|
|
184
|
+
try {
|
|
185
|
+
const d = new Date(iso);
|
|
186
|
+
return d.toLocaleString();
|
|
187
|
+
} catch {
|
|
188
|
+
return String(iso);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function setSelected(item) {
|
|
193
|
+
selectedId = item ? item._id : null;
|
|
194
|
+
$('editorTitle').textContent = item ? `Editing: ${item.title}` : 'New config';
|
|
195
|
+
$('btnDelete').style.display = item ? 'inline-block' : 'none';
|
|
196
|
+
|
|
197
|
+
$('title').value = item?.title || '';
|
|
198
|
+
$('slug').value = item?.slug || '';
|
|
199
|
+
$('alias').value = item?.alias || '';
|
|
200
|
+
console.log('setSelected - alias value:', $('alias').value, 'item.alias:', item?.alias);
|
|
201
|
+
$('publicEnabled').checked = Boolean(item?.publicEnabled);
|
|
202
|
+
$('cacheTtlSeconds').value = String(item?.cacheTtlSeconds ?? 0);
|
|
203
|
+
$('jsonRaw').value = item?.jsonRaw || '';
|
|
204
|
+
|
|
205
|
+
updatePublicUrl();
|
|
206
|
+
updateSnippet();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function updatePublicUrl() {
|
|
210
|
+
const enabled = $('publicEnabled').checked;
|
|
211
|
+
const slug = $('slug').value;
|
|
212
|
+
const alias = $('alias').value;
|
|
213
|
+
const el = $('publicUrl');
|
|
214
|
+
if (!enabled || !slug) {
|
|
215
|
+
el.style.display = 'none';
|
|
216
|
+
el.textContent = '';
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const slugUrl = `${API_BASE}/api/json-configs/${encodeURIComponent(slug)}`;
|
|
220
|
+
let urls = `<a href="${slugUrl}" class="text-blue-600 hover:text-blue-800" rel="noreferrer">${slugUrl}</a>`;
|
|
221
|
+
if (alias) {
|
|
222
|
+
const aliasUrl = `${API_BASE}/api/json-configs/${encodeURIComponent(alias)}`;
|
|
223
|
+
urls += ` <span class="text-gray-500">or</span> <a href="${aliasUrl}" class="text-blue-600 hover:text-blue-800" rel="noreferrer">${aliasUrl}</a>`;
|
|
224
|
+
}
|
|
225
|
+
el.style.display = 'block';
|
|
226
|
+
el.innerHTML = `<span class="text-gray-500">Public URL(s):</span> ${urls}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function updateSnippet() {
|
|
230
|
+
const slug = $('slug').value || '<slug>';
|
|
231
|
+
const snippet =
|
|
232
|
+
`const saasbackend = require('saasbackend');\n\n// Works when your host app mounts saasbackend.middleware(...) and shares the same process/DB connection\nconst { getJsonConfig } = saasbackend.services.jsonConfigs;\n\nconst cfg = await getJsonConfig('${slug}', {\n bypassCache: false,\n});\n`;
|
|
233
|
+
$('snippet').textContent = snippet;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function loadList() {
|
|
237
|
+
try {
|
|
238
|
+
const res = await fetch(`${API_BASE}/api/admin/json-configs`);
|
|
239
|
+
const data = await res.json();
|
|
240
|
+
if (!res.ok) throw new Error(data?.error || 'Failed to load');
|
|
241
|
+
|
|
242
|
+
items = Array.isArray(data?.items) ? data.items : [];
|
|
243
|
+
renderList();
|
|
244
|
+
} catch (e) {
|
|
245
|
+
showToast(e.message, 'error');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function renderList() {
|
|
250
|
+
const tbody = $('rows');
|
|
251
|
+
tbody.innerHTML = '';
|
|
252
|
+
|
|
253
|
+
if (!items.length) {
|
|
254
|
+
const tr = document.createElement('tr');
|
|
255
|
+
tr.innerHTML = `<td colspan="6" class="px-4 py-4 text-sm text-gray-500">No configs yet.</td>`;
|
|
256
|
+
tbody.appendChild(tr);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (const it of items) {
|
|
261
|
+
const tr = document.createElement('tr');
|
|
262
|
+
tr.style.cursor = 'pointer';
|
|
263
|
+
tr.className = 'hover:bg-gray-50';
|
|
264
|
+
tr.innerHTML = `
|
|
265
|
+
<td class="px-4 py-2 text-sm text-gray-900">${escapeHtml(it.title || '')}</td>
|
|
266
|
+
<td class="px-4 py-2 text-sm"><code class="text-gray-800">${escapeHtml(it.slug || '')}</code></td>
|
|
267
|
+
<td class="px-4 py-2 text-sm"><code class="text-gray-600">${escapeHtml(it.alias || '')}</code></td>
|
|
268
|
+
<td class="px-4 py-2 text-sm text-gray-900">${it.publicEnabled ? 'yes' : 'no'}</td>
|
|
269
|
+
<td class="px-4 py-2 text-sm text-gray-900">${Number(it.cacheTtlSeconds || 0)}</td>
|
|
270
|
+
<td class="px-4 py-2 text-sm text-gray-500">${escapeHtml(formatDate(it.updatedAt))}</td>
|
|
271
|
+
`;
|
|
272
|
+
tr.addEventListener('click', () => openEditor(it._id));
|
|
273
|
+
tbody.appendChild(tr);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function escapeHtml(str) {
|
|
278
|
+
return String(str)
|
|
279
|
+
.replace(/&/g, '&')
|
|
280
|
+
.replace(/</g, '<')
|
|
281
|
+
.replace(/>/g, '>')
|
|
282
|
+
.replace(/"/g, '"')
|
|
283
|
+
.replace(/'/g, ''');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function openEditor(id) {
|
|
287
|
+
try {
|
|
288
|
+
const res = await fetch(`${API_BASE}/api/admin/json-configs/${encodeURIComponent(id)}`);
|
|
289
|
+
const data = await res.json();
|
|
290
|
+
if (!res.ok) throw new Error(data?.error || 'Failed to load');
|
|
291
|
+
setSelected(data.item);
|
|
292
|
+
} catch (e) {
|
|
293
|
+
showToast(e.message, 'error');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function save() {
|
|
298
|
+
const aliasField = $('alias');
|
|
299
|
+
console.log('Alias field element:', aliasField);
|
|
300
|
+
console.log('Alias field value:', aliasField ? aliasField.value : 'FIELD NOT FOUND');
|
|
301
|
+
|
|
302
|
+
const body = {
|
|
303
|
+
title: $('title').value,
|
|
304
|
+
alias: aliasField ? aliasField.value || null : null,
|
|
305
|
+
publicEnabled: $('publicEnabled').checked,
|
|
306
|
+
cacheTtlSeconds: Number($('cacheTtlSeconds').value || 0),
|
|
307
|
+
jsonRaw: $('jsonRaw').value,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
console.log('Saving with body:', body);
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const res = await fetch(`${API_BASE}/api/admin/json-configs${selectedId ? `/${encodeURIComponent(selectedId)}` : ''}`, {
|
|
314
|
+
method: selectedId ? 'PUT' : 'POST',
|
|
315
|
+
headers: { 'Content-Type': 'application/json' },
|
|
316
|
+
body: JSON.stringify(body),
|
|
317
|
+
});
|
|
318
|
+
const data = await res.json();
|
|
319
|
+
if (!res.ok) throw new Error(data?.error || 'Save failed');
|
|
320
|
+
|
|
321
|
+
showToast('Saved');
|
|
322
|
+
setSelected(data.item);
|
|
323
|
+
await loadList();
|
|
324
|
+
} catch (e) {
|
|
325
|
+
showToast(e.message, 'error');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function regenerateSlug() {
|
|
330
|
+
if (!selectedId) return;
|
|
331
|
+
if (!confirm('Regenerate slug? This changes the public URL and internal access key.')) return;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const res = await fetch(`${API_BASE}/api/admin/json-configs/${encodeURIComponent(selectedId)}/regenerate-slug`, { method: 'POST' });
|
|
335
|
+
const data = await res.json();
|
|
336
|
+
if (!res.ok) throw new Error(data?.error || 'Failed to regenerate');
|
|
337
|
+
|
|
338
|
+
showToast('Slug regenerated');
|
|
339
|
+
setSelected(data.item);
|
|
340
|
+
await loadList();
|
|
341
|
+
} catch (e) {
|
|
342
|
+
showToast(e.message, 'error');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function clearCache() {
|
|
347
|
+
if (!selectedId) return;
|
|
348
|
+
try {
|
|
349
|
+
const res = await fetch(`${API_BASE}/api/admin/json-configs/${encodeURIComponent(selectedId)}/clear-cache`, { method: 'POST' });
|
|
350
|
+
const data = await res.json();
|
|
351
|
+
if (!res.ok) throw new Error(data?.error || 'Failed to clear cache');
|
|
352
|
+
showToast('Cache cleared');
|
|
353
|
+
} catch (e) {
|
|
354
|
+
showToast(e.message, 'error');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function remove() {
|
|
359
|
+
if (!selectedId) return;
|
|
360
|
+
if (!confirm('Delete this JSON config? This cannot be undone.')) return;
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const res = await fetch(`${API_BASE}/api/admin/json-configs/${encodeURIComponent(selectedId)}`, { method: 'DELETE' });
|
|
364
|
+
const data = await res.json();
|
|
365
|
+
if (!res.ok) throw new Error(data?.error || 'Failed to delete');
|
|
366
|
+
|
|
367
|
+
showToast('Deleted');
|
|
368
|
+
setSelected(null);
|
|
369
|
+
await loadList();
|
|
370
|
+
} catch (e) {
|
|
371
|
+
showToast(e.message, 'error');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function copySnippet() {
|
|
376
|
+
try {
|
|
377
|
+
await navigator.clipboard.writeText($('snippet').textContent);
|
|
378
|
+
showToast('Copied');
|
|
379
|
+
} catch {
|
|
380
|
+
showToast('Copy failed', 'error');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
$('btnReload').addEventListener('click', loadList);
|
|
385
|
+
$('btnNew').addEventListener('click', () => setSelected(null));
|
|
386
|
+
$('btnSave').addEventListener('click', save);
|
|
387
|
+
$('btnDelete').addEventListener('click', remove);
|
|
388
|
+
$('btnRegenerate').addEventListener('click', regenerateSlug);
|
|
389
|
+
$('btnClearCache').addEventListener('click', clearCache);
|
|
390
|
+
$('btnCopy').addEventListener('click', copySnippet);
|
|
391
|
+
|
|
392
|
+
$('publicEnabled').addEventListener('change', updatePublicUrl);
|
|
393
|
+
$('slug').addEventListener('input', () => { updatePublicUrl(); updateSnippet(); });
|
|
394
|
+
$('alias').addEventListener('input', updatePublicUrl);
|
|
395
|
+
|
|
396
|
+
(async function init() {
|
|
397
|
+
setSelected(null);
|
|
398
|
+
await loadList();
|
|
399
|
+
})();
|
|
400
|
+
</script>
|
|
401
|
+
<script>
|
|
402
|
+
window.addEventListener("keydown", (e) => {
|
|
403
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
|
|
404
|
+
e.preventDefault();
|
|
405
|
+
window.parent.postMessage({ type: "keydown", ctrlK: true }, "*");
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
</script>
|
|
409
|
+
</body>
|
|
410
|
+
</html>
|