@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,436 @@
|
|
|
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>Global Settings - Admin</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<style>
|
|
9
|
+
.toast {
|
|
10
|
+
animation: slideIn 0.3s ease-out;
|
|
11
|
+
}
|
|
12
|
+
@keyframes slideIn {
|
|
13
|
+
from { transform: translateX(100%); opacity: 0; }
|
|
14
|
+
to { transform: translateX(0); opacity: 1; }
|
|
15
|
+
}
|
|
16
|
+
.fade-out {
|
|
17
|
+
animation: fadeOut 0.3s ease-out forwards;
|
|
18
|
+
}
|
|
19
|
+
@keyframes fadeOut {
|
|
20
|
+
from { opacity: 1; }
|
|
21
|
+
to { opacity: 0; }
|
|
22
|
+
}
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body class="bg-gray-100">
|
|
26
|
+
<div class="min-h-screen">
|
|
27
|
+
<!-- Header -->
|
|
28
|
+
<div class="bg-white shadow">
|
|
29
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
30
|
+
<div class="flex justify-between items-center">
|
|
31
|
+
<div>
|
|
32
|
+
<h1 class="text-2xl font-bold text-gray-900">Global Settings Manager</h1>
|
|
33
|
+
<p class="text-sm text-gray-600 mt-1">Configure system-wide settings for SaaSBackend</p>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<!-- Main Content -->
|
|
40
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
41
|
+
<!-- Actions Bar -->
|
|
42
|
+
<div class="mb-6 flex justify-between items-center">
|
|
43
|
+
<button onclick="loadSettings()" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 flex items-center">
|
|
44
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
45
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
46
|
+
</svg>
|
|
47
|
+
Refresh
|
|
48
|
+
</button>
|
|
49
|
+
<button onclick="showCreateModal()" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 flex items-center">
|
|
50
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
51
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
|
52
|
+
</svg>
|
|
53
|
+
New Setting
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Settings Grid -->
|
|
58
|
+
<div id="settings-container" class="grid grid-cols-1 gap-6">
|
|
59
|
+
<div class="text-center py-12">
|
|
60
|
+
<p class="text-gray-600">Loading settings...</p>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Predefined Settings Info -->
|
|
65
|
+
<div class="mt-8 bg-blue-50 border-l-4 border-blue-500 p-4">
|
|
66
|
+
<h3 class="font-semibold text-blue-900 mb-2">📋 Available Setting Keys</h3>
|
|
67
|
+
<div class="text-sm text-blue-800 space-y-2">
|
|
68
|
+
<p><strong>RESEND_API_KEY</strong> (string) - API key for Resend email service</p>
|
|
69
|
+
<p><strong>EMAIL_FROM</strong> (string) - Default "From" address for emails (e.g., "SaaSBackend <no-reply@yourdomain.com>")</p>
|
|
70
|
+
<p><strong>FRONTEND_URL</strong> (string) - Frontend application URL (e.g., "https://app.saasbackend.com")</p>
|
|
71
|
+
<p><strong>EMAIL_PASSWORD_RESET_SUBJECT</strong> (string) - Subject line for password reset emails</p>
|
|
72
|
+
<p><strong>EMAIL_PASSWORD_RESET_HTML</strong> (html) - HTML template for password reset emails</p>
|
|
73
|
+
<p class="pl-4 text-blue-700">Available variables: <code class="bg-blue-100 px-1 rounded">{{resetUrl}}</code></p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Toast Container -->
|
|
80
|
+
<div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
|
|
81
|
+
|
|
82
|
+
<!-- Edit Modal -->
|
|
83
|
+
<div id="edit-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
|
84
|
+
<div class="relative top-20 mx-auto p-5 border w-full max-w-2xl shadow-lg rounded-md bg-white">
|
|
85
|
+
<div class="flex justify-between items-center mb-4">
|
|
86
|
+
<h3 class="text-xl font-bold" id="modal-title">Edit Setting</h3>
|
|
87
|
+
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600">
|
|
88
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
89
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
90
|
+
</svg>
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
<form id="edit-form" onsubmit="handleSubmit(event)">
|
|
94
|
+
<input type="hidden" id="edit-key">
|
|
95
|
+
<input type="hidden" id="edit-mode">
|
|
96
|
+
|
|
97
|
+
<div class="mb-4" id="key-input-container">
|
|
98
|
+
<label class="block text-sm font-medium mb-2">Key *</label>
|
|
99
|
+
<input type="text" id="new-key" class="w-full border rounded px-3 py-2" required>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="mb-4">
|
|
103
|
+
<label class="block text-sm font-medium mb-2">Value *</label>
|
|
104
|
+
<textarea id="edit-value" class="w-full border rounded px-3 py-2" rows="6" required></textarea>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="mb-4" id="type-input-container">
|
|
108
|
+
<label class="block text-sm font-medium mb-2">Type *</label>
|
|
109
|
+
<select id="edit-type" class="w-full border rounded px-3 py-2">
|
|
110
|
+
<option value="string">String</option>
|
|
111
|
+
<option value="html">HTML</option>
|
|
112
|
+
<option value="boolean">Boolean</option>
|
|
113
|
+
<option value="json">JSON</option>
|
|
114
|
+
<option value="number">Number</option>
|
|
115
|
+
<option value="encrypted">Encrypted</option>
|
|
116
|
+
</select>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="mb-4" id="desc-input-container">
|
|
120
|
+
<label class="block text-sm font-medium mb-2">Description *</label>
|
|
121
|
+
<input type="text" id="edit-description" class="w-full border rounded px-3 py-2" required>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="mb-4" id="vars-input-container">
|
|
125
|
+
<label class="block text-sm font-medium mb-2">Template Variables (comma-separated)</label>
|
|
126
|
+
<input type="text" id="edit-template-vars" class="w-full border rounded px-3 py-2" placeholder="e.g., resetUrl, username">
|
|
127
|
+
<p class="text-xs text-gray-500 mt-1">Variables that can be used in templates like {{resetUrl}}</p>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div class="mb-4" id="public-input-container">
|
|
131
|
+
<label class="flex items-center">
|
|
132
|
+
<input type="checkbox" id="edit-public" class="mr-2">
|
|
133
|
+
<span class="text-sm font-medium">Public (accessible without authentication)</span>
|
|
134
|
+
</label>
|
|
135
|
+
<p class="text-xs text-gray-500 mt-1">Public settings can be accessed via GET /api/admin/settings/public</p>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div class="flex justify-end space-x-2">
|
|
139
|
+
<button type="button" onclick="closeModal()" class="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400">
|
|
140
|
+
Cancel
|
|
141
|
+
</button>
|
|
142
|
+
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
|
|
143
|
+
Save
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
</form>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<script>
|
|
151
|
+
const BASE_PATH = "<%= (typeof baseUrl !== 'undefined' && baseUrl) ? baseUrl : '' %>";
|
|
152
|
+
const API_BASE = window.location.origin + (BASE_PATH || '');
|
|
153
|
+
let allSettings = [];
|
|
154
|
+
|
|
155
|
+
function showToast(message, type = 'success') {
|
|
156
|
+
const container = document.getElementById('toast-container');
|
|
157
|
+
const toast = document.createElement('div');
|
|
158
|
+
toast.className = `toast px-6 py-4 rounded-lg shadow-lg text-white ${
|
|
159
|
+
type === 'success' ? 'bg-green-500' : 'bg-red-500'
|
|
160
|
+
}`;
|
|
161
|
+
toast.textContent = message;
|
|
162
|
+
container.appendChild(toast);
|
|
163
|
+
|
|
164
|
+
setTimeout(() => {
|
|
165
|
+
toast.classList.add('fade-out');
|
|
166
|
+
setTimeout(() => toast.remove(), 300);
|
|
167
|
+
}, 3000);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function loadSettings() {
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch(`${API_BASE}/api/admin/settings`);
|
|
173
|
+
if (!response.ok) throw new Error('Failed to load settings');
|
|
174
|
+
|
|
175
|
+
allSettings = await response.json();
|
|
176
|
+
renderSettings();
|
|
177
|
+
showToast('Settings loaded successfully');
|
|
178
|
+
} catch (error) {
|
|
179
|
+
showToast(error.message, 'error');
|
|
180
|
+
document.getElementById('settings-container').innerHTML = `
|
|
181
|
+
<div class="text-center py-12 text-red-600">
|
|
182
|
+
<p>Error loading settings: ${error.message}</p>
|
|
183
|
+
</div>
|
|
184
|
+
`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function renderSettings() {
|
|
189
|
+
const container = document.getElementById('settings-container');
|
|
190
|
+
|
|
191
|
+
if (allSettings.length === 0) {
|
|
192
|
+
container.innerHTML = `
|
|
193
|
+
<div class="text-center py-12">
|
|
194
|
+
<p class="text-gray-600">No settings found. Create your first setting!</p>
|
|
195
|
+
</div>
|
|
196
|
+
`;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
container.innerHTML = allSettings.map(setting => `
|
|
201
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
202
|
+
<div class="flex justify-between items-start mb-4">
|
|
203
|
+
<div class="flex-1">
|
|
204
|
+
<div class="flex items-center space-x-2 mb-2">
|
|
205
|
+
<h3 class="text-lg font-bold text-gray-900">${setting.key}</h3>
|
|
206
|
+
<span class="px-2 py-1 text-xs rounded bg-gray-200 text-gray-700">${setting.type}</span>
|
|
207
|
+
${setting.public ? '<span class="px-2 py-1 text-xs rounded bg-green-100 text-green-700">🌍 Public</span>' : '<span class="px-2 py-1 text-xs rounded bg-gray-100 text-gray-600">🔒 Private</span>'}
|
|
208
|
+
</div>
|
|
209
|
+
<p class="text-sm text-gray-600 mb-3">${setting.description}</p>
|
|
210
|
+
${setting.templateVariables && setting.templateVariables.length > 0 ? `
|
|
211
|
+
<p class="text-xs text-blue-600 mb-2">
|
|
212
|
+
Available variables: ${setting.templateVariables.map(v => `<code class="bg-blue-50 px-1 rounded">{{${v}}}</code>`).join(', ')}
|
|
213
|
+
</p>
|
|
214
|
+
` : ''}
|
|
215
|
+
</div>
|
|
216
|
+
<div class="flex space-x-2 ml-4">
|
|
217
|
+
<button onclick="editSetting('${setting.key}')" class="text-blue-600 hover:text-blue-800">
|
|
218
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
219
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
|
220
|
+
</svg>
|
|
221
|
+
</button>
|
|
222
|
+
${setting.type === 'encrypted' ? `
|
|
223
|
+
<button onclick="revealSetting('${setting.key}')" class="text-gray-600 hover:text-gray-800" title="Reveal value">
|
|
224
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
225
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
226
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
227
|
+
</svg>
|
|
228
|
+
</button>
|
|
229
|
+
` : ''}
|
|
230
|
+
<button onclick="deleteSetting('${setting.key}')" class="text-red-600 hover:text-red-800">
|
|
231
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
232
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
233
|
+
</svg>
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="bg-gray-50 rounded p-3">
|
|
238
|
+
<p class="text-xs text-gray-500 mb-1">Current Value:</p>
|
|
239
|
+
${(() => {
|
|
240
|
+
const displayValue = setting.decryptedValue ?? setting.value;
|
|
241
|
+
const safeText = escapeHtml(displayValue);
|
|
242
|
+
const attrValue = escapeAttribute(displayValue);
|
|
243
|
+
return `
|
|
244
|
+
<pre class="text-sm text-gray-800 overflow-auto max-h-40">${safeText}</pre>
|
|
245
|
+
<button data-value="${attrValue}" onclick="copyToClipboard(this.dataset.value)" class="text-blue-600 hover:text-blue-800">
|
|
246
|
+
`;
|
|
247
|
+
})()}
|
|
248
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
249
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path>
|
|
250
|
+
</svg>
|
|
251
|
+
</button>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
`).join('');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function escapeHtml(text) {
|
|
258
|
+
const div = document.createElement('div');
|
|
259
|
+
div.textContent = text;
|
|
260
|
+
return div.innerHTML;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function escapeAttribute(value) {
|
|
264
|
+
return String(value)
|
|
265
|
+
.replace(/&/g, '&')
|
|
266
|
+
.replace(/"/g, '"')
|
|
267
|
+
.replace(/'/g, ''')
|
|
268
|
+
.replace(/</g, '<')
|
|
269
|
+
.replace(/>/g, '>');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function revealSetting(key) {
|
|
273
|
+
try {
|
|
274
|
+
const response = await fetch(`${API_BASE}/api/admin/settings/${key}/reveal`);
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
const data = await response.json();
|
|
277
|
+
throw new Error(data.error || 'Failed to reveal setting');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const data = await response.json();
|
|
281
|
+
const matching = allSettings.find(s => s.key === key);
|
|
282
|
+
if (matching) {
|
|
283
|
+
matching.decryptedValue = data.value;
|
|
284
|
+
}
|
|
285
|
+
renderSettings();
|
|
286
|
+
showToast(`Revealed value for ${key}`);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
showToast(error.message, 'error');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function copyToClipboard(text) {
|
|
293
|
+
if (!navigator.clipboard) {
|
|
294
|
+
const textarea = document.createElement('textarea');
|
|
295
|
+
textarea.value = text;
|
|
296
|
+
textarea.style.position = 'fixed';
|
|
297
|
+
document.body.appendChild(textarea);
|
|
298
|
+
textarea.focus();
|
|
299
|
+
textarea.select();
|
|
300
|
+
try {
|
|
301
|
+
document.execCommand('copy');
|
|
302
|
+
showToast('Copied to clipboard');
|
|
303
|
+
} catch (e) {
|
|
304
|
+
showToast('Failed to copy', 'error');
|
|
305
|
+
} finally {
|
|
306
|
+
document.body.removeChild(textarea);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
navigator.clipboard.writeText(text)
|
|
311
|
+
.then(() => showToast('Copied to clipboard'))
|
|
312
|
+
.catch(() => showToast('Failed to copy', 'error'));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function showCreateModal() {
|
|
316
|
+
document.getElementById('modal-title').textContent = 'Create New Setting';
|
|
317
|
+
document.getElementById('edit-mode').value = 'create';
|
|
318
|
+
document.getElementById('new-key').value = '';
|
|
319
|
+
document.getElementById('edit-value').value = '';
|
|
320
|
+
document.getElementById('edit-type').value = 'string';
|
|
321
|
+
document.getElementById('edit-description').value = '';
|
|
322
|
+
document.getElementById('edit-template-vars').value = '';
|
|
323
|
+
document.getElementById('edit-public').checked = false;
|
|
324
|
+
|
|
325
|
+
document.getElementById('key-input-container').style.display = 'block';
|
|
326
|
+
document.getElementById('type-input-container').style.display = 'block';
|
|
327
|
+
document.getElementById('desc-input-container').style.display = 'block';
|
|
328
|
+
document.getElementById('vars-input-container').style.display = 'block';
|
|
329
|
+
document.getElementById('public-input-container').style.display = 'block';
|
|
330
|
+
|
|
331
|
+
document.getElementById('edit-modal').classList.remove('hidden');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function editSetting(key) {
|
|
335
|
+
const setting = allSettings.find(s => s.key === key);
|
|
336
|
+
if (!setting) return;
|
|
337
|
+
|
|
338
|
+
document.getElementById('modal-title').textContent = 'Edit Setting';
|
|
339
|
+
document.getElementById('edit-mode').value = 'edit';
|
|
340
|
+
document.getElementById('edit-key').value = key;
|
|
341
|
+
document.getElementById('edit-value').value = setting.value;
|
|
342
|
+
|
|
343
|
+
document.getElementById('key-input-container').style.display = 'none';
|
|
344
|
+
document.getElementById('type-input-container').style.display = 'none';
|
|
345
|
+
document.getElementById('desc-input-container').style.display = 'none';
|
|
346
|
+
document.getElementById('vars-input-container').style.display = 'none';
|
|
347
|
+
document.getElementById('public-input-container').style.display = 'none';
|
|
348
|
+
|
|
349
|
+
document.getElementById('edit-modal').classList.remove('hidden');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function closeModal() {
|
|
353
|
+
document.getElementById('edit-modal').classList.add('hidden');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function handleSubmit(event) {
|
|
357
|
+
event.preventDefault();
|
|
358
|
+
|
|
359
|
+
const mode = document.getElementById('edit-mode').value;
|
|
360
|
+
const value = document.getElementById('edit-value').value;
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
let response;
|
|
364
|
+
|
|
365
|
+
if (mode === 'create') {
|
|
366
|
+
const key = document.getElementById('new-key').value;
|
|
367
|
+
const type = document.getElementById('edit-type').value;
|
|
368
|
+
const description = document.getElementById('edit-description').value;
|
|
369
|
+
const isPublic = document.getElementById('edit-public').checked;
|
|
370
|
+
const templateVars = document.getElementById('edit-template-vars').value
|
|
371
|
+
.split(',')
|
|
372
|
+
.map(v => v.trim())
|
|
373
|
+
.filter(v => v);
|
|
374
|
+
|
|
375
|
+
response = await fetch(`${API_BASE}/api/admin/settings`, {
|
|
376
|
+
method: 'POST',
|
|
377
|
+
headers: { 'Content-Type': 'application/json' },
|
|
378
|
+
body: JSON.stringify({ key, value, type, description, templateVariables: templateVars, public: isPublic })
|
|
379
|
+
});
|
|
380
|
+
} else {
|
|
381
|
+
const key = document.getElementById('edit-key').value;
|
|
382
|
+
response = await fetch(`${API_BASE}/api/admin/settings/${key}`, {
|
|
383
|
+
method: 'PUT',
|
|
384
|
+
headers: { 'Content-Type': 'application/json' },
|
|
385
|
+
body: JSON.stringify({ value })
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!response.ok) {
|
|
390
|
+
const data = await response.json();
|
|
391
|
+
throw new Error(data.error || 'Operation failed');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
showToast(mode === 'create' ? 'Setting created successfully' : 'Setting updated successfully');
|
|
395
|
+
closeModal();
|
|
396
|
+
await loadSettings();
|
|
397
|
+
} catch (error) {
|
|
398
|
+
showToast(error.message, 'error');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function deleteSetting(key) {
|
|
403
|
+
if (!confirm(`Are you sure you want to delete the setting "${key}"? This action cannot be undone.`)) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const response = await fetch(`${API_BASE}/api/admin/settings/${key}`, {
|
|
409
|
+
method: 'DELETE'
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
if (!response.ok) {
|
|
413
|
+
const data = await response.json();
|
|
414
|
+
throw new Error(data.error || 'Failed to delete setting');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
showToast('Setting deleted successfully');
|
|
418
|
+
await loadSettings();
|
|
419
|
+
} catch (error) {
|
|
420
|
+
showToast(error.message, 'error');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Load settings on page load
|
|
425
|
+
loadSettings();
|
|
426
|
+
</script>
|
|
427
|
+
<script>
|
|
428
|
+
window.addEventListener("keydown", (e) => {
|
|
429
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
|
|
430
|
+
e.preventDefault();
|
|
431
|
+
window.parent.postMessage({ type: "keydown", ctrlK: true }, "*");
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
</script>
|
|
435
|
+
</body>
|
|
436
|
+
</html>
|