@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,452 @@
|
|
|
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>Users Admin</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<style>
|
|
9
|
+
.toast { animation: slideIn 0.3s ease-out; }
|
|
10
|
+
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
|
11
|
+
.fade-out { animation: fadeOut 0.3s ease-out forwards; }
|
|
12
|
+
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body class="bg-gray-100">
|
|
16
|
+
<div class="min-h-screen">
|
|
17
|
+
<div class="bg-white shadow">
|
|
18
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
19
|
+
<div class="flex justify-between items-center">
|
|
20
|
+
<div>
|
|
21
|
+
<h1 class="text-2xl font-bold text-gray-900">Users</h1>
|
|
22
|
+
<p class="text-sm text-gray-600 mt-1">Manage system users</p>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="flex items-center gap-4">
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
31
|
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
32
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
33
|
+
<p class="text-sm text-gray-500">Total Users</p>
|
|
34
|
+
<p id="stat-total" class="text-2xl font-bold text-gray-900">-</p>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
37
|
+
<p class="text-sm text-gray-500">Admins</p>
|
|
38
|
+
<p id="stat-admins" class="text-2xl font-bold text-gray-900">-</p>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
41
|
+
<p class="text-sm text-gray-500">Active Subscriptions</p>
|
|
42
|
+
<p id="stat-subscriptions" class="text-2xl font-bold text-gray-900">-</p>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
45
|
+
<p class="text-sm text-gray-500">Disabled</p>
|
|
46
|
+
<p id="stat-disabled" class="text-2xl font-bold text-gray-900">-</p>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
51
|
+
<div class="flex items-center justify-between mb-4">
|
|
52
|
+
<div>
|
|
53
|
+
<h2 class="text-lg font-semibold text-gray-900">User List</h2>
|
|
54
|
+
<p id="users-subtitle" class="text-sm text-gray-600">-</p>
|
|
55
|
+
</div>
|
|
56
|
+
<button id="btn-refresh" class="bg-gray-100 text-gray-800 px-3 py-2 rounded hover:bg-gray-200">Refresh</button>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="grid grid-cols-1 md:grid-cols-5 gap-3 mb-4">
|
|
60
|
+
<div>
|
|
61
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Search</label>
|
|
62
|
+
<input id="filter-q" type="text" class="w-full border rounded px-3 py-2" placeholder="email or name">
|
|
63
|
+
</div>
|
|
64
|
+
<div>
|
|
65
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
|
|
66
|
+
<select id="filter-role" class="w-full border rounded px-3 py-2">
|
|
67
|
+
<option value="">All</option>
|
|
68
|
+
<option value="user">user</option>
|
|
69
|
+
<option value="admin">admin</option>
|
|
70
|
+
</select>
|
|
71
|
+
</div>
|
|
72
|
+
<div>
|
|
73
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Subscription</label>
|
|
74
|
+
<select id="filter-subscription" class="w-full border rounded px-3 py-2">
|
|
75
|
+
<option value="">All</option>
|
|
76
|
+
<option value="none">none</option>
|
|
77
|
+
<option value="active">active</option>
|
|
78
|
+
<option value="cancelled">cancelled</option>
|
|
79
|
+
<option value="past_due">past_due</option>
|
|
80
|
+
<option value="trialing">trialing</option>
|
|
81
|
+
</select>
|
|
82
|
+
</div>
|
|
83
|
+
<div>
|
|
84
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Plan</label>
|
|
85
|
+
<select id="filter-plan" class="w-full border rounded px-3 py-2">
|
|
86
|
+
<option value="">All</option>
|
|
87
|
+
<option value="free">free</option>
|
|
88
|
+
<option value="creator">creator</option>
|
|
89
|
+
<option value="pro">pro</option>
|
|
90
|
+
</select>
|
|
91
|
+
</div>
|
|
92
|
+
<div>
|
|
93
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Limit</label>
|
|
94
|
+
<input id="filter-limit" type="number" min="1" max="500" class="w-full border rounded px-3 py-2" value="50">
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="flex gap-2 mb-4">
|
|
99
|
+
<button id="btn-apply" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Apply</button>
|
|
100
|
+
<button id="btn-reset" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Reset</button>
|
|
101
|
+
<div class="flex-1"></div>
|
|
102
|
+
<button id="btn-prev" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200" disabled>Prev</button>
|
|
103
|
+
<button id="btn-next" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200" disabled>Next</button>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="overflow-x-auto">
|
|
107
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
108
|
+
<thead class="bg-gray-50">
|
|
109
|
+
<tr>
|
|
110
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
|
|
111
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role</th>
|
|
112
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Plan</th>
|
|
113
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Subscription</th>
|
|
114
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
|
115
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
116
|
+
</tr>
|
|
117
|
+
</thead>
|
|
118
|
+
<tbody id="users-tbody" class="bg-white divide-y divide-gray-200"></tbody>
|
|
119
|
+
</table>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div id="modal-edit" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center">
|
|
126
|
+
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md mx-4">
|
|
127
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Edit User</h3>
|
|
128
|
+
<input type="hidden" id="edit-user-id">
|
|
129
|
+
<div class="space-y-4">
|
|
130
|
+
<div>
|
|
131
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
|
132
|
+
<input id="edit-name" type="text" class="w-full border rounded px-3 py-2">
|
|
133
|
+
</div>
|
|
134
|
+
<div>
|
|
135
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
|
|
136
|
+
<select id="edit-role" class="w-full border rounded px-3 py-2">
|
|
137
|
+
<option value="user">user</option>
|
|
138
|
+
<option value="admin">admin</option>
|
|
139
|
+
</select>
|
|
140
|
+
</div>
|
|
141
|
+
<div>
|
|
142
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Plan</label>
|
|
143
|
+
<input id="edit-plan" type="text" class="w-full border rounded px-3 py-2" placeholder="e.g. free, pro, starter">
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Subscription Status</label>
|
|
147
|
+
<select id="edit-subscription" class="w-full border rounded px-3 py-2">
|
|
148
|
+
<option value="none">none</option>
|
|
149
|
+
<option value="active">active</option>
|
|
150
|
+
<option value="cancelled">cancelled</option>
|
|
151
|
+
<option value="past_due">past_due</option>
|
|
152
|
+
<option value="trialing">trialing</option>
|
|
153
|
+
</select>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="flex justify-end gap-2 mt-6">
|
|
157
|
+
<button id="btn-modal-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
|
|
158
|
+
<button id="btn-modal-save" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Save</button>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div id="modal-notify" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center">
|
|
164
|
+
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md mx-4">
|
|
165
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Send Notification</h3>
|
|
166
|
+
<input type="hidden" id="notify-user-id">
|
|
167
|
+
<p id="notify-user-email" class="text-sm text-gray-600 mb-4"></p>
|
|
168
|
+
<div class="space-y-4">
|
|
169
|
+
<div>
|
|
170
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Type</label>
|
|
171
|
+
<select id="notify-type" class="w-full border rounded px-3 py-2">
|
|
172
|
+
<option value="info">info</option>
|
|
173
|
+
<option value="success">success</option>
|
|
174
|
+
<option value="warning">warning</option>
|
|
175
|
+
<option value="error">error</option>
|
|
176
|
+
</select>
|
|
177
|
+
</div>
|
|
178
|
+
<div>
|
|
179
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Channel</label>
|
|
180
|
+
<select id="notify-channel" class="w-full border rounded px-3 py-2">
|
|
181
|
+
<option value="in_app">In-App Only</option>
|
|
182
|
+
<option value="email">Email Only</option>
|
|
183
|
+
<option value="both">Both</option>
|
|
184
|
+
</select>
|
|
185
|
+
</div>
|
|
186
|
+
<div>
|
|
187
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Title *</label>
|
|
188
|
+
<input id="notify-title" type="text" class="w-full border rounded px-3 py-2" placeholder="Notification title">
|
|
189
|
+
</div>
|
|
190
|
+
<div>
|
|
191
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Message *</label>
|
|
192
|
+
<textarea id="notify-message" class="w-full border rounded px-3 py-2" rows="3" placeholder="Notification message"></textarea>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="flex justify-end gap-2 mt-6">
|
|
196
|
+
<button id="btn-notify-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
|
|
197
|
+
<button id="btn-notify-send" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">Send</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
|
|
203
|
+
|
|
204
|
+
<script>
|
|
205
|
+
const API_BASE = window.location.origin + "<%= baseUrl %>" || window.location.origin;
|
|
206
|
+
|
|
207
|
+
function showToast(message, type = 'success') {
|
|
208
|
+
const container = document.getElementById('toast-container');
|
|
209
|
+
const toast = document.createElement('div');
|
|
210
|
+
toast.className = `toast px-6 py-4 rounded-lg shadow-lg text-white ${type === 'success' ? 'bg-green-500' : 'bg-red-500'}`;
|
|
211
|
+
toast.textContent = message;
|
|
212
|
+
container.appendChild(toast);
|
|
213
|
+
setTimeout(() => { toast.classList.add('fade-out'); setTimeout(() => toast.remove(), 300); }, 3000);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function escapeHtml(str) {
|
|
217
|
+
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function formatDate(iso) {
|
|
221
|
+
try { return new Date(iso).toLocaleDateString(); } catch { return String(iso || ''); }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function qs(obj) {
|
|
225
|
+
const params = new URLSearchParams();
|
|
226
|
+
Object.entries(obj).forEach(([k, v]) => { if (v !== undefined && v !== null && String(v).trim()) params.set(k, String(v).trim()); });
|
|
227
|
+
const out = params.toString();
|
|
228
|
+
return out ? `?${out}` : '';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const state = { offset: 0, limit: 50, total: 0 };
|
|
232
|
+
|
|
233
|
+
async function loadStats() {
|
|
234
|
+
try {
|
|
235
|
+
const res = await fetch(`${API_BASE}/api/admin/users/stats`);
|
|
236
|
+
const data = await res.json();
|
|
237
|
+
if (res.ok) {
|
|
238
|
+
document.getElementById('stat-total').textContent = data.total ?? '-';
|
|
239
|
+
document.getElementById('stat-admins').textContent = data.admins ?? '-';
|
|
240
|
+
document.getElementById('stat-subscriptions').textContent = data.activeSubscriptions ?? '-';
|
|
241
|
+
document.getElementById('stat-disabled').textContent = data.disabled ?? '-';
|
|
242
|
+
}
|
|
243
|
+
} catch (e) { console.error('Failed to load stats:', e); }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function loadUsers() {
|
|
247
|
+
const q = document.getElementById('filter-q')?.value?.trim();
|
|
248
|
+
const role = document.getElementById('filter-role')?.value;
|
|
249
|
+
const subscriptionStatus = document.getElementById('filter-subscription')?.value;
|
|
250
|
+
const currentPlan = document.getElementById('filter-plan')?.value;
|
|
251
|
+
const limit = parseInt(document.getElementById('filter-limit')?.value || '50', 10);
|
|
252
|
+
|
|
253
|
+
state.limit = Math.min(500, Math.max(1, limit));
|
|
254
|
+
|
|
255
|
+
const subtitle = document.getElementById('users-subtitle');
|
|
256
|
+
const tbody = document.getElementById('users-tbody');
|
|
257
|
+
if (subtitle) subtitle.textContent = 'Loading...';
|
|
258
|
+
if (tbody) tbody.innerHTML = '';
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const url = `${API_BASE}/api/admin/users${qs({ q, role, subscriptionStatus, currentPlan, limit: state.limit, offset: state.offset })}`;
|
|
262
|
+
const res = await fetch(url);
|
|
263
|
+
const data = await res.json();
|
|
264
|
+
|
|
265
|
+
if (!res.ok) { showToast(data?.error || 'Failed to load users', 'error'); return; }
|
|
266
|
+
|
|
267
|
+
const users = Array.isArray(data?.users) ? data.users : [];
|
|
268
|
+
const pagination = data?.pagination || {};
|
|
269
|
+
state.total = pagination.total ?? 0;
|
|
270
|
+
state.limit = pagination.limit ?? state.limit;
|
|
271
|
+
state.offset = pagination.offset ?? state.offset;
|
|
272
|
+
|
|
273
|
+
if (subtitle) {
|
|
274
|
+
const from = state.total === 0 ? 0 : state.offset + 1;
|
|
275
|
+
const to = Math.min(state.offset + users.length, state.total);
|
|
276
|
+
subtitle.textContent = `${from}-${to} of ${state.total}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
document.getElementById('btn-prev').disabled = state.offset <= 0;
|
|
280
|
+
document.getElementById('btn-next').disabled = state.offset + state.limit >= state.total;
|
|
281
|
+
|
|
282
|
+
if (tbody) {
|
|
283
|
+
if (users.length === 0) {
|
|
284
|
+
tbody.innerHTML = '<tr><td class="px-4 py-6 text-sm text-gray-600" colspan="6">No users found.</td></tr>';
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
tbody.innerHTML = users.map(u => {
|
|
289
|
+
const disabled = u.disabled ? '<span class="text-xs text-red-500 ml-1">(disabled)</span>' : '';
|
|
290
|
+
return `
|
|
291
|
+
<tr>
|
|
292
|
+
<td class="px-4 py-3 text-sm text-gray-900">
|
|
293
|
+
<div class="font-medium">${escapeHtml(u.email)}${disabled}</div>
|
|
294
|
+
<div class="text-xs text-gray-500">${escapeHtml(u.name || '')}</div>
|
|
295
|
+
<div class="text-xs text-gray-400">${escapeHtml(u._id)}</div>
|
|
296
|
+
</td>
|
|
297
|
+
<td class="px-4 py-3 text-sm text-gray-700">${escapeHtml(u.role)}</td>
|
|
298
|
+
<td class="px-4 py-3 text-sm text-gray-700">${escapeHtml(u.currentPlan)}</td>
|
|
299
|
+
<td class="px-4 py-3 text-sm text-gray-700">${escapeHtml(u.subscriptionStatus)}</td>
|
|
300
|
+
<td class="px-4 py-3 text-sm text-gray-700">${formatDate(u.createdAt)}</td>
|
|
301
|
+
<td class="px-4 py-3 text-sm text-gray-700 whitespace-nowrap">
|
|
302
|
+
<button class="text-blue-600 hover:text-blue-800 mr-2" data-edit="${escapeHtml(u._id)}" data-name="${escapeHtml(u.name || '')}" data-role="${escapeHtml(u.role)}" data-plan="${escapeHtml(u.currentPlan)}" data-subscription="${escapeHtml(u.subscriptionStatus)}">Edit</button>
|
|
303
|
+
<button class="text-green-600 hover:text-green-800 mr-2" data-notify="${escapeHtml(u._id)}" data-email="${escapeHtml(u.email)}">Notify</button>
|
|
304
|
+
${u.disabled
|
|
305
|
+
? `<button class="text-green-600 hover:text-green-800" data-enable="${escapeHtml(u._id)}">Enable</button>`
|
|
306
|
+
: `<button class="text-red-600 hover:text-red-800" data-disable="${escapeHtml(u._id)}">Disable</button>`
|
|
307
|
+
}
|
|
308
|
+
</td>
|
|
309
|
+
</tr>
|
|
310
|
+
`;
|
|
311
|
+
}).join('');
|
|
312
|
+
|
|
313
|
+
tbody.querySelectorAll('[data-edit]').forEach(btn => {
|
|
314
|
+
btn.addEventListener('click', () => openEditModal(btn.dataset.edit, btn.dataset.name, btn.dataset.role, btn.dataset.plan, btn.dataset.subscription));
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
tbody.querySelectorAll('[data-notify]').forEach(btn => {
|
|
318
|
+
btn.addEventListener('click', () => openNotifyModal(btn.dataset.notify, btn.dataset.email));
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
tbody.querySelectorAll('[data-disable]').forEach(btn => {
|
|
322
|
+
btn.addEventListener('click', async () => {
|
|
323
|
+
if (!confirm('Disable this user?')) return;
|
|
324
|
+
await toggleUser(btn.dataset.disable, 'disable');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
tbody.querySelectorAll('[data-enable]').forEach(btn => {
|
|
329
|
+
btn.addEventListener('click', async () => {
|
|
330
|
+
if (!confirm('Enable this user?')) return;
|
|
331
|
+
await toggleUser(btn.dataset.enable, 'enable');
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
} catch (e) {
|
|
336
|
+
showToast(e.message || 'Failed to load users', 'error');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function toggleUser(userId, action) {
|
|
341
|
+
try {
|
|
342
|
+
const res = await fetch(`${API_BASE}/api/admin/users/${encodeURIComponent(userId)}/${action}`, { method: 'POST' });
|
|
343
|
+
const data = await res.json();
|
|
344
|
+
if (!res.ok) { showToast(data?.error || `Failed to ${action} user`, 'error'); return; }
|
|
345
|
+
showToast(`User ${action}d`, 'success');
|
|
346
|
+
await Promise.all([loadUsers(), loadStats()]);
|
|
347
|
+
} catch (e) { showToast(e.message, 'error'); }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function openEditModal(userId, name, role, plan, subscription) {
|
|
351
|
+
document.getElementById('edit-user-id').value = userId;
|
|
352
|
+
document.getElementById('edit-name').value = name || '';
|
|
353
|
+
document.getElementById('edit-role').value = role || 'user';
|
|
354
|
+
document.getElementById('edit-plan').value = plan || 'free';
|
|
355
|
+
document.getElementById('edit-subscription').value = subscription || 'none';
|
|
356
|
+
document.getElementById('modal-edit').classList.remove('hidden');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function closeEditModal() {
|
|
360
|
+
document.getElementById('modal-edit').classList.add('hidden');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function saveUser() {
|
|
364
|
+
const userId = document.getElementById('edit-user-id').value;
|
|
365
|
+
const name = document.getElementById('edit-name').value.trim();
|
|
366
|
+
const role = document.getElementById('edit-role').value;
|
|
367
|
+
const currentPlan = document.getElementById('edit-plan').value;
|
|
368
|
+
const subscriptionStatus = document.getElementById('edit-subscription').value;
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const res = await fetch(`${API_BASE}/api/admin/users/${encodeURIComponent(userId)}`, {
|
|
372
|
+
method: 'PATCH',
|
|
373
|
+
headers: { 'Content-Type': 'application/json' },
|
|
374
|
+
body: JSON.stringify({ name, role, currentPlan, subscriptionStatus }),
|
|
375
|
+
});
|
|
376
|
+
const data = await res.json();
|
|
377
|
+
if (!res.ok) { showToast(data?.error || 'Failed to update user', 'error'); return; }
|
|
378
|
+
showToast('User updated', 'success');
|
|
379
|
+
closeEditModal();
|
|
380
|
+
await Promise.all([loadUsers(), loadStats()]);
|
|
381
|
+
} catch (e) { showToast(e.message, 'error'); }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function openNotifyModal(userId, email) {
|
|
385
|
+
document.getElementById('notify-user-id').value = userId;
|
|
386
|
+
document.getElementById('notify-user-email').textContent = `To: ${email}`;
|
|
387
|
+
document.getElementById('notify-type').value = 'info';
|
|
388
|
+
document.getElementById('notify-channel').value = 'in_app';
|
|
389
|
+
document.getElementById('notify-title').value = '';
|
|
390
|
+
document.getElementById('notify-message').value = '';
|
|
391
|
+
document.getElementById('modal-notify').classList.remove('hidden');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function closeNotifyModal() {
|
|
395
|
+
document.getElementById('modal-notify').classList.add('hidden');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function sendNotification() {
|
|
399
|
+
const userId = document.getElementById('notify-user-id').value;
|
|
400
|
+
const type = document.getElementById('notify-type').value;
|
|
401
|
+
const channel = document.getElementById('notify-channel').value;
|
|
402
|
+
const title = document.getElementById('notify-title').value.trim();
|
|
403
|
+
const message = document.getElementById('notify-message').value.trim();
|
|
404
|
+
|
|
405
|
+
if (!title || !message) { showToast('Title and message are required', 'error'); return; }
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const res = await fetch(`${API_BASE}/api/admin/notifications/send`, {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
headers: { 'Content-Type': 'application/json' },
|
|
411
|
+
body: JSON.stringify({ userIds: [userId], type, channel, title, message }),
|
|
412
|
+
});
|
|
413
|
+
const data = await res.json();
|
|
414
|
+
if (!res.ok) { showToast(data?.error || 'Failed to send notification', 'error'); return; }
|
|
415
|
+
showToast('Notification sent', 'success');
|
|
416
|
+
closeNotifyModal();
|
|
417
|
+
} catch (e) { showToast(e.message, 'error'); }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function bindEvents() {
|
|
421
|
+
document.getElementById('btn-refresh').onclick = () => Promise.all([loadUsers(), loadStats()]);
|
|
422
|
+
document.getElementById('btn-apply').onclick = () => { state.offset = 0; loadUsers(); };
|
|
423
|
+
document.getElementById('btn-reset').onclick = () => {
|
|
424
|
+
document.getElementById('filter-q').value = '';
|
|
425
|
+
document.getElementById('filter-role').value = '';
|
|
426
|
+
document.getElementById('filter-subscription').value = '';
|
|
427
|
+
document.getElementById('filter-plan').value = '';
|
|
428
|
+
document.getElementById('filter-limit').value = '50';
|
|
429
|
+
state.offset = 0;
|
|
430
|
+
loadUsers();
|
|
431
|
+
};
|
|
432
|
+
document.getElementById('btn-prev').onclick = () => { state.offset = Math.max(0, state.offset - state.limit); loadUsers(); };
|
|
433
|
+
document.getElementById('btn-next').onclick = () => { state.offset += state.limit; loadUsers(); };
|
|
434
|
+
document.getElementById('btn-modal-cancel').onclick = closeEditModal;
|
|
435
|
+
document.getElementById('btn-modal-save').onclick = saveUser;
|
|
436
|
+
document.getElementById('btn-notify-cancel').onclick = closeNotifyModal;
|
|
437
|
+
document.getElementById('btn-notify-send').onclick = sendNotification;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
bindEvents();
|
|
441
|
+
Promise.all([loadUsers(), loadStats()]);
|
|
442
|
+
</script>
|
|
443
|
+
<script>
|
|
444
|
+
window.addEventListener("keydown", (e) => {
|
|
445
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
|
|
446
|
+
e.preventDefault();
|
|
447
|
+
window.parent.postMessage({ type: "keydown", ctrlK: true }, "*");
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
</script>
|
|
451
|
+
</body>
|
|
452
|
+
</html>
|