@intranefr/superbackend 1.4.4 → 1.5.1
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/.env.example +5 -0
- package/README.md +11 -0
- package/index.js +39 -1
- package/package.json +11 -3
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +111 -5
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminHeadless.controller.js +91 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +320 -0
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +366 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware/internalCronAuth.js +29 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +879 -56
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +1 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminHeadless.routes.js +8 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +30 -0
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +6 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +184 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +700 -0
- package/src/services/consoleOverride.service.js +6 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +299 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +29 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-headless.ejs +294 -24
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +528 -10
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +741 -0
- package/views/admin-users.ejs +261 -4
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +14 -0
- package/views/partials/llm-provider-model-picker.ejs +183 -0
|
@@ -0,0 +1,942 @@
|
|
|
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>File Manager - Admin</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></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-4xl 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">File Manager</h1>
|
|
16
|
+
<p class="text-sm text-gray-600 mt-1">Configure public File Manager route and UI</p>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
23
|
+
<div class="mb-6 border-b border-gray-200">
|
|
24
|
+
<nav class="-mb-px flex gap-6" aria-label="Tabs">
|
|
25
|
+
<button id="tabGeneral" class="py-2 px-1 border-b-2 font-medium text-sm">General</button>
|
|
26
|
+
<button id="tabStorage" class="py-2 px-1 border-b-2 font-medium text-sm">Storage</button>
|
|
27
|
+
</nav>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div id="sectionGeneral" class="bg-white rounded-lg shadow p-6">
|
|
31
|
+
<div class="space-y-6">
|
|
32
|
+
<div class="flex items-start justify-between">
|
|
33
|
+
<div>
|
|
34
|
+
<h2 class="text-lg font-semibold text-gray-900">Public File Manager</h2>
|
|
35
|
+
<p class="text-sm text-gray-600 mt-1">Serve the user-facing File Manager SPA at a configurable base path.</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="grid grid-cols-1 gap-6">
|
|
40
|
+
<div class="flex items-center justify-between">
|
|
41
|
+
<div>
|
|
42
|
+
<label class="text-sm font-medium text-gray-900">Enabled</label>
|
|
43
|
+
<p class="text-xs text-gray-500">Requires restart to affect public routing.</p>
|
|
44
|
+
</div>
|
|
45
|
+
<input id="enabled" type="checkbox" class="h-5 w-5" />
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div>
|
|
49
|
+
<label class="block text-sm font-medium text-gray-900 mb-2">Base Path</label>
|
|
50
|
+
<input id="basePath" type="text" class="w-full border rounded px-3 py-2" placeholder="/files" />
|
|
51
|
+
<p class="text-xs text-gray-500 mt-1">Requires restart. Must start with <code>/</code>.</p>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div>
|
|
55
|
+
<label class="block text-sm font-medium text-gray-900 mb-2">Max upload size (bytes)</label>
|
|
56
|
+
<input id="maxUploadBytes" type="number" min="1" step="1" class="w-full border rounded px-3 py-2" placeholder="1073741824" />
|
|
57
|
+
<p class="text-xs text-gray-500 mt-1">Applies immediately to uploads. Default: 1073741824 (1GB).</p>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div>
|
|
61
|
+
<label class="block text-sm font-medium text-gray-900 mb-2">Computed URL</label>
|
|
62
|
+
<div class="flex gap-2">
|
|
63
|
+
<div class="flex-1 bg-gray-50 rounded px-3 py-2 text-sm text-gray-800" id="computedUrl"></div>
|
|
64
|
+
<button id="openComputedUrl" class="px-3 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 transition-colors" title="Open in new tab">
|
|
65
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
66
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
|
67
|
+
</svg>
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="flex justify-end space-x-2">
|
|
73
|
+
<button id="refresh" class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">Refresh</button>
|
|
74
|
+
<button id="save" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Save</button>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4">
|
|
78
|
+
<p class="text-sm text-yellow-900 font-medium">Restart required</p>
|
|
79
|
+
<p class="text-sm text-yellow-800 mt-1">Changes to the File Manager public route are applied when the server restarts.</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div id="sectionStorage" class="bg-white rounded-lg shadow p-6 hidden">
|
|
86
|
+
<div class="space-y-6">
|
|
87
|
+
<div>
|
|
88
|
+
<h2 class="text-lg font-semibold text-gray-900">Storage configuration</h2>
|
|
89
|
+
<p class="text-sm text-gray-600 mt-1">Configure per-drive max upload and storage limits (user > group > org > global > default).</p>
|
|
90
|
+
<p class="text-xs text-gray-500 mt-1">Default max storage (if no match): <code>FILE_MANAGER_DEFAULT_MAX_STORAGE_BYTES</code> env, fallback 100mb.</p>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="grid grid-cols-1 gap-6">
|
|
94
|
+
<div>
|
|
95
|
+
<label class="block text-sm font-medium text-gray-900 mb-2">Global max upload size</label>
|
|
96
|
+
<input id="policyGlobalMaxUploadHuman" type="text" class="w-full border rounded px-3 py-2" placeholder="40mb" />
|
|
97
|
+
<div id="policyGlobalMaxUploadBytesHint" class="text-xs text-gray-500 mt-1"></div>
|
|
98
|
+
<input id="policyGlobalMaxUploadBytes" type="number" min="1" step="1" class="hidden" />
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div>
|
|
102
|
+
<label class="block text-sm font-medium text-gray-900 mb-2">Global max storage size</label>
|
|
103
|
+
<input id="policyGlobalMaxStorageHuman" type="text" class="w-full border rounded px-3 py-2" placeholder="1gb" />
|
|
104
|
+
<div id="policyGlobalMaxStorageBytesHint" class="text-xs text-gray-500 mt-1"></div>
|
|
105
|
+
<input id="policyGlobalMaxStorageBytes" type="number" min="1" step="1" class="hidden" />
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="border-t pt-6">
|
|
109
|
+
<label class="block text-sm font-medium text-gray-900 mb-2">Organization</label>
|
|
110
|
+
<select id="policyOrgSelect" class="w-full border rounded px-3 py-2"></select>
|
|
111
|
+
<p class="text-xs text-gray-500 mt-1">Select an org to configure org/group/user overrides.</p>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div id="policyOrgSection" class="hidden">
|
|
115
|
+
<div class="grid grid-cols-1 gap-4">
|
|
116
|
+
<div>
|
|
117
|
+
<label class="block text-sm font-medium text-gray-900 mb-2">Org max upload size</label>
|
|
118
|
+
<input id="policyOrgMaxUploadHuman" type="text" class="w-full border rounded px-3 py-2" placeholder="50mb" />
|
|
119
|
+
<div id="policyOrgMaxUploadBytesHint" class="text-xs text-gray-500 mt-1"></div>
|
|
120
|
+
<input id="policyOrgMaxUploadBytes" type="number" min="1" step="1" class="hidden" />
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div>
|
|
124
|
+
<label class="block text-sm font-medium text-gray-900 mb-2">Org max storage size</label>
|
|
125
|
+
<input id="policyOrgMaxStorageHuman" type="text" class="w-full border rounded px-3 py-2" placeholder="1gb" />
|
|
126
|
+
<div id="policyOrgMaxStorageBytesHint" class="text-xs text-gray-500 mt-1"></div>
|
|
127
|
+
<input id="policyOrgMaxStorageBytes" type="number" min="1" step="1" class="hidden" />
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div class="border-t pt-6">
|
|
131
|
+
<div class="flex items-center justify-between">
|
|
132
|
+
<div>
|
|
133
|
+
<div class="text-sm font-medium text-gray-900">Group overrides</div>
|
|
134
|
+
<div class="text-xs text-gray-500">Applies to Group drives, and to User drives as a fallback (max of matching groups).</div>
|
|
135
|
+
</div>
|
|
136
|
+
<button id="policyRefreshGroups" class="px-3 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 text-sm">Refresh groups</button>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="mt-3 overflow-x-auto border rounded">
|
|
139
|
+
<table class="min-w-full text-sm">
|
|
140
|
+
<thead class="bg-gray-50">
|
|
141
|
+
<tr>
|
|
142
|
+
<th class="text-left px-3 py-2">Group</th>
|
|
143
|
+
<th class="text-left px-3 py-2">Max upload</th>
|
|
144
|
+
<th class="text-left px-3 py-2">Max storage</th>
|
|
145
|
+
</tr>
|
|
146
|
+
</thead>
|
|
147
|
+
<tbody id="policyGroupsBody" class="divide-y"></tbody>
|
|
148
|
+
</table>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="border-t pt-6">
|
|
153
|
+
<div class="text-sm font-medium text-gray-900">User overrides (user drive)</div>
|
|
154
|
+
<div class="text-xs text-gray-500 mt-1">Pick a user from org members to set user-drive limits.</div>
|
|
155
|
+
|
|
156
|
+
<div class="mt-3 overflow-x-auto border rounded">
|
|
157
|
+
<table class="min-w-full text-sm">
|
|
158
|
+
<thead class="bg-gray-50">
|
|
159
|
+
<tr>
|
|
160
|
+
<th class="text-left px-3 py-2">User</th>
|
|
161
|
+
<th class="text-left px-3 py-2">Max upload</th>
|
|
162
|
+
<th class="text-left px-3 py-2">Max storage</th>
|
|
163
|
+
<th class="text-right px-3 py-2">Actions</th>
|
|
164
|
+
</tr>
|
|
165
|
+
</thead>
|
|
166
|
+
<tbody id="policyUsersBody" class="divide-y"></tbody>
|
|
167
|
+
</table>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div class="mt-3 flex gap-2">
|
|
171
|
+
<select id="policyUserSelect" class="flex-1 border rounded px-3 py-2"></select>
|
|
172
|
+
<button id="policyUserApply" class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">Load</button>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div id="policyUserEditor" class="hidden mt-4 grid grid-cols-1 gap-4">
|
|
176
|
+
<div>
|
|
177
|
+
<label class="block text-sm font-medium text-gray-900 mb-2">User max upload size</label>
|
|
178
|
+
<input id="policyUserMaxUploadHuman" type="text" class="w-full border rounded px-3 py-2" placeholder="100mb" />
|
|
179
|
+
<div id="policyUserMaxUploadBytesHint" class="text-xs text-gray-500 mt-1"></div>
|
|
180
|
+
<input id="policyUserMaxUploadBytes" type="number" min="1" step="1" class="hidden" />
|
|
181
|
+
</div>
|
|
182
|
+
<div>
|
|
183
|
+
<label class="block text-sm font-medium text-gray-900 mb-2">User max storage size</label>
|
|
184
|
+
<input id="policyUserMaxStorageHuman" type="text" class="w-full border rounded px-3 py-2" placeholder="10gb" />
|
|
185
|
+
<div id="policyUserMaxStorageBytesHint" class="text-xs text-gray-500 mt-1"></div>
|
|
186
|
+
<input id="policyUserMaxStorageBytes" type="number" min="1" step="1" class="hidden" />
|
|
187
|
+
</div>
|
|
188
|
+
<div class="flex justify-end">
|
|
189
|
+
<button id="policyUserClear" class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">Clear user override</button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div class="flex justify-end space-x-2 border-t pt-6">
|
|
197
|
+
<button id="policyRefresh" class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">Refresh</button>
|
|
198
|
+
<button id="policySave" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Save storage config</button>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div id="toast" class="hidden fixed bottom-4 right-4 px-4 py-3 rounded shadow text-white"></div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<script>
|
|
209
|
+
const BASE_PATH = "<%= (typeof baseUrl !== 'undefined' && baseUrl) ? baseUrl : '' %>";
|
|
210
|
+
const API_BASE = window.location.origin + (BASE_PATH || '');
|
|
211
|
+
|
|
212
|
+
const KEY_ENABLED = 'FILE_MANAGER_ENABLED';
|
|
213
|
+
const KEY_BASE_PATH = 'FILE_MANAGER_BASE_PATH';
|
|
214
|
+
const KEY_MAX_UPLOAD_BYTES = 'FILE_MANAGER_MAX_UPLOAD_BYTES';
|
|
215
|
+
const KEY_STORAGE_POLICY_JSON = 'FILE_MANAGER_STORAGE_POLICY_JSON';
|
|
216
|
+
|
|
217
|
+
const elEnabled = document.getElementById('enabled');
|
|
218
|
+
const elBasePath = document.getElementById('basePath');
|
|
219
|
+
const elMaxUploadBytes = document.getElementById('maxUploadBytes');
|
|
220
|
+
const elComputedUrl = document.getElementById('computedUrl');
|
|
221
|
+
const elOpenComputedUrl = document.getElementById('openComputedUrl');
|
|
222
|
+
const elToast = document.getElementById('toast');
|
|
223
|
+
|
|
224
|
+
const elTabGeneral = document.getElementById('tabGeneral');
|
|
225
|
+
const elTabStorage = document.getElementById('tabStorage');
|
|
226
|
+
const elSectionGeneral = document.getElementById('sectionGeneral');
|
|
227
|
+
const elSectionStorage = document.getElementById('sectionStorage');
|
|
228
|
+
|
|
229
|
+
const elPolicyGlobalMaxUploadHuman = document.getElementById('policyGlobalMaxUploadHuman');
|
|
230
|
+
const elPolicyGlobalMaxUploadBytes = document.getElementById('policyGlobalMaxUploadBytes');
|
|
231
|
+
const elPolicyGlobalMaxUploadBytesHint = document.getElementById('policyGlobalMaxUploadBytesHint');
|
|
232
|
+
const elPolicyGlobalMaxStorageHuman = document.getElementById('policyGlobalMaxStorageHuman');
|
|
233
|
+
const elPolicyGlobalMaxStorageBytes = document.getElementById('policyGlobalMaxStorageBytes');
|
|
234
|
+
const elPolicyGlobalMaxStorageBytesHint = document.getElementById('policyGlobalMaxStorageBytesHint');
|
|
235
|
+
|
|
236
|
+
const elPolicyOrgSelect = document.getElementById('policyOrgSelect');
|
|
237
|
+
const elPolicyOrgSection = document.getElementById('policyOrgSection');
|
|
238
|
+
const elPolicyOrgMaxUploadHuman = document.getElementById('policyOrgMaxUploadHuman');
|
|
239
|
+
const elPolicyOrgMaxUploadBytes = document.getElementById('policyOrgMaxUploadBytes');
|
|
240
|
+
const elPolicyOrgMaxUploadBytesHint = document.getElementById('policyOrgMaxUploadBytesHint');
|
|
241
|
+
const elPolicyOrgMaxStorageHuman = document.getElementById('policyOrgMaxStorageHuman');
|
|
242
|
+
const elPolicyOrgMaxStorageBytes = document.getElementById('policyOrgMaxStorageBytes');
|
|
243
|
+
const elPolicyOrgMaxStorageBytesHint = document.getElementById('policyOrgMaxStorageBytesHint');
|
|
244
|
+
|
|
245
|
+
const elPolicyGroupsBody = document.getElementById('policyGroupsBody');
|
|
246
|
+
const elPolicyRefreshGroups = document.getElementById('policyRefreshGroups');
|
|
247
|
+
const elPolicyUsersBody = document.getElementById('policyUsersBody');
|
|
248
|
+
const elPolicyUserSelect = document.getElementById('policyUserSelect');
|
|
249
|
+
const elPolicyUserApply = document.getElementById('policyUserApply');
|
|
250
|
+
const elPolicyUserEditor = document.getElementById('policyUserEditor');
|
|
251
|
+
const elPolicyUserMaxUploadHuman = document.getElementById('policyUserMaxUploadHuman');
|
|
252
|
+
const elPolicyUserMaxUploadBytes = document.getElementById('policyUserMaxUploadBytes');
|
|
253
|
+
const elPolicyUserMaxUploadBytesHint = document.getElementById('policyUserMaxUploadBytesHint');
|
|
254
|
+
const elPolicyUserMaxStorageHuman = document.getElementById('policyUserMaxStorageHuman');
|
|
255
|
+
const elPolicyUserMaxStorageBytes = document.getElementById('policyUserMaxStorageBytes');
|
|
256
|
+
const elPolicyUserMaxStorageBytesHint = document.getElementById('policyUserMaxStorageBytesHint');
|
|
257
|
+
const elPolicyUserClear = document.getElementById('policyUserClear');
|
|
258
|
+
|
|
259
|
+
let storagePolicy = { version: 1, global: {}, orgs: {} };
|
|
260
|
+
let orgsCache = [];
|
|
261
|
+
let groupsCache = [];
|
|
262
|
+
let orgMembersCache = [];
|
|
263
|
+
let selectedOrgId = '';
|
|
264
|
+
let selectedUserId = '';
|
|
265
|
+
|
|
266
|
+
const toPositiveIntOrNull = (value) => {
|
|
267
|
+
const n = Number(value);
|
|
268
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
269
|
+
return Math.floor(n);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const parseHumanBytes = (input) => {
|
|
273
|
+
const raw = String(input || '').trim().toLowerCase();
|
|
274
|
+
if (!raw) return null;
|
|
275
|
+
|
|
276
|
+
const m = raw.match(/^([0-9]+(?:\.[0-9]+)?)\s*(b|kb|mb|gb|tb)$/i);
|
|
277
|
+
if (!m) return null;
|
|
278
|
+
|
|
279
|
+
const num = Number(m[1]);
|
|
280
|
+
if (!Number.isFinite(num) || num <= 0) return null;
|
|
281
|
+
|
|
282
|
+
const unit = m[2].toLowerCase();
|
|
283
|
+
const mult = unit === 'tb' ? 1024 ** 4 : unit === 'gb' ? 1024 ** 3 : unit === 'mb' ? 1024 ** 2 : unit === 'kb' ? 1024 : 1;
|
|
284
|
+
const bytes = Math.floor(num * mult);
|
|
285
|
+
return bytes > 0 ? bytes : null;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const formatHumanBytes = (bytes) => {
|
|
289
|
+
const n = Number(bytes);
|
|
290
|
+
if (!Number.isFinite(n) || n <= 0) return '';
|
|
291
|
+
const units = [
|
|
292
|
+
{ u: 'tb', v: 1024 ** 4 },
|
|
293
|
+
{ u: 'gb', v: 1024 ** 3 },
|
|
294
|
+
{ u: 'mb', v: 1024 ** 2 },
|
|
295
|
+
{ u: 'kb', v: 1024 },
|
|
296
|
+
];
|
|
297
|
+
for (const { u, v } of units) {
|
|
298
|
+
if (n >= v) {
|
|
299
|
+
const val = n / v;
|
|
300
|
+
const str = val >= 10 ? String(Math.round(val)) : String(Math.round(val * 10) / 10);
|
|
301
|
+
return `${str}${u}`;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return `${Math.round(n)}b`;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const ensurePolicyShape = (p) => {
|
|
308
|
+
const out = p && typeof p === 'object' ? p : {};
|
|
309
|
+
return {
|
|
310
|
+
version: toPositiveIntOrNull(out.version) || 1,
|
|
311
|
+
global: out.global && typeof out.global === 'object' ? out.global : {},
|
|
312
|
+
orgs: out.orgs && typeof out.orgs === 'object' ? out.orgs : {},
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const getOrgPolicy = (orgId) => {
|
|
317
|
+
const key = String(orgId || '');
|
|
318
|
+
if (!key) return null;
|
|
319
|
+
if (!storagePolicy.orgs[key]) storagePolicy.orgs[key] = { groups: {}, users: {} };
|
|
320
|
+
if (!storagePolicy.orgs[key].groups) storagePolicy.orgs[key].groups = {};
|
|
321
|
+
if (!storagePolicy.orgs[key].users) storagePolicy.orgs[key].users = {};
|
|
322
|
+
return storagePolicy.orgs[key];
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const renderOrgsSelect = () => {
|
|
326
|
+
elPolicyOrgSelect.innerHTML = '';
|
|
327
|
+
const opt0 = document.createElement('option');
|
|
328
|
+
opt0.value = '';
|
|
329
|
+
opt0.textContent = 'Select organization';
|
|
330
|
+
elPolicyOrgSelect.appendChild(opt0);
|
|
331
|
+
|
|
332
|
+
for (const o of orgsCache) {
|
|
333
|
+
const opt = document.createElement('option');
|
|
334
|
+
opt.value = String(o._id || o.id);
|
|
335
|
+
opt.textContent = o.name || o.slug || String(o._id || o.id);
|
|
336
|
+
elPolicyOrgSelect.appendChild(opt);
|
|
337
|
+
}
|
|
338
|
+
elPolicyOrgSelect.value = selectedOrgId || '';
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const renderGroupsTable = () => {
|
|
342
|
+
elPolicyGroupsBody.innerHTML = '';
|
|
343
|
+
const orgGroups = groupsCache.filter((g) => String(g.orgId || '') === String(selectedOrgId || ''));
|
|
344
|
+
const orgPolicy = getOrgPolicy(selectedOrgId);
|
|
345
|
+
|
|
346
|
+
for (const g of orgGroups) {
|
|
347
|
+
const gid = String(g.id || g._id);
|
|
348
|
+
const row = document.createElement('tr');
|
|
349
|
+
|
|
350
|
+
const c1 = document.createElement('td');
|
|
351
|
+
c1.className = 'px-3 py-2';
|
|
352
|
+
c1.textContent = g.name || gid;
|
|
353
|
+
|
|
354
|
+
const c2 = document.createElement('td');
|
|
355
|
+
c2.className = 'px-3 py-2';
|
|
356
|
+
const wrap2 = document.createElement('div');
|
|
357
|
+
const in2 = document.createElement('input');
|
|
358
|
+
in2.type = 'text';
|
|
359
|
+
in2.className = 'w-56 border rounded px-2 py-1';
|
|
360
|
+
const hint2 = document.createElement('div');
|
|
361
|
+
hint2.className = 'text-xs text-gray-500 mt-1';
|
|
362
|
+
const existingUpload = orgPolicy?.groups?.[gid]?.maxUploadBytes;
|
|
363
|
+
in2.value = existingUpload ? formatHumanBytes(existingUpload) : '';
|
|
364
|
+
hint2.textContent = existingUpload ? `${existingUpload} bytes` : '';
|
|
365
|
+
in2.addEventListener('blur', () => {
|
|
366
|
+
const parsed = parseHumanBytes(in2.value);
|
|
367
|
+
if (!orgPolicy.groups[gid]) orgPolicy.groups[gid] = {};
|
|
368
|
+
if (parsed === null) {
|
|
369
|
+
delete orgPolicy.groups[gid].maxUploadBytes;
|
|
370
|
+
hint2.textContent = '';
|
|
371
|
+
} else {
|
|
372
|
+
orgPolicy.groups[gid].maxUploadBytes = parsed;
|
|
373
|
+
hint2.textContent = `${parsed} bytes`;
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
wrap2.appendChild(in2);
|
|
377
|
+
wrap2.appendChild(hint2);
|
|
378
|
+
c2.appendChild(wrap2);
|
|
379
|
+
|
|
380
|
+
const c3 = document.createElement('td');
|
|
381
|
+
c3.className = 'px-3 py-2';
|
|
382
|
+
const wrap3 = document.createElement('div');
|
|
383
|
+
const in3 = document.createElement('input');
|
|
384
|
+
in3.type = 'text';
|
|
385
|
+
in3.className = 'w-56 border rounded px-2 py-1';
|
|
386
|
+
const hint3 = document.createElement('div');
|
|
387
|
+
hint3.className = 'text-xs text-gray-500 mt-1';
|
|
388
|
+
const existingStorage = orgPolicy?.groups?.[gid]?.maxStorageBytes;
|
|
389
|
+
in3.value = existingStorage ? formatHumanBytes(existingStorage) : '';
|
|
390
|
+
hint3.textContent = existingStorage ? `${existingStorage} bytes` : '';
|
|
391
|
+
in3.addEventListener('blur', () => {
|
|
392
|
+
const parsed = parseHumanBytes(in3.value);
|
|
393
|
+
if (!orgPolicy.groups[gid]) orgPolicy.groups[gid] = {};
|
|
394
|
+
if (parsed === null) {
|
|
395
|
+
delete orgPolicy.groups[gid].maxStorageBytes;
|
|
396
|
+
hint3.textContent = '';
|
|
397
|
+
} else {
|
|
398
|
+
orgPolicy.groups[gid].maxStorageBytes = parsed;
|
|
399
|
+
hint3.textContent = `${parsed} bytes`;
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
wrap3.appendChild(in3);
|
|
403
|
+
wrap3.appendChild(hint3);
|
|
404
|
+
c3.appendChild(wrap3);
|
|
405
|
+
|
|
406
|
+
row.appendChild(c1);
|
|
407
|
+
row.appendChild(c2);
|
|
408
|
+
row.appendChild(c3);
|
|
409
|
+
elPolicyGroupsBody.appendChild(row);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const renderUsersSelect = () => {
|
|
414
|
+
elPolicyUserSelect.innerHTML = '';
|
|
415
|
+
const opt0 = document.createElement('option');
|
|
416
|
+
opt0.value = '';
|
|
417
|
+
opt0.textContent = 'Select user';
|
|
418
|
+
elPolicyUserSelect.appendChild(opt0);
|
|
419
|
+
|
|
420
|
+
for (const m of orgMembersCache) {
|
|
421
|
+
const uid = String(m.user?._id || m.userId || '');
|
|
422
|
+
if (!uid) continue;
|
|
423
|
+
const opt = document.createElement('option');
|
|
424
|
+
opt.value = uid;
|
|
425
|
+
opt.textContent = m.user?.email || m.user?.name || uid;
|
|
426
|
+
elPolicyUserSelect.appendChild(opt);
|
|
427
|
+
}
|
|
428
|
+
elPolicyUserSelect.value = selectedUserId || '';
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const renderUserOverridesTable = () => {
|
|
432
|
+
elPolicyUsersBody.innerHTML = '';
|
|
433
|
+
if (!selectedOrgId) return;
|
|
434
|
+
|
|
435
|
+
const orgPolicy = getOrgPolicy(selectedOrgId);
|
|
436
|
+
const users = orgPolicy?.users && typeof orgPolicy.users === 'object' ? orgPolicy.users : {};
|
|
437
|
+
const userIds = Object.keys(users);
|
|
438
|
+
|
|
439
|
+
const displayNameForUserId = (uid) => {
|
|
440
|
+
const match = (orgMembersCache || []).find((m) => String(m.user?._id || m.userId || '') === String(uid));
|
|
441
|
+
return match?.user?.email || match?.user?.name || String(uid);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
for (const uid of userIds) {
|
|
445
|
+
const cfg = users[uid] || {};
|
|
446
|
+
const hasUpload = Number.isFinite(Number(cfg.maxUploadBytes)) && Number(cfg.maxUploadBytes) > 0;
|
|
447
|
+
const hasStorage = Number.isFinite(Number(cfg.maxStorageBytes)) && Number(cfg.maxStorageBytes) > 0;
|
|
448
|
+
if (!hasUpload && !hasStorage) continue;
|
|
449
|
+
|
|
450
|
+
const row = document.createElement('tr');
|
|
451
|
+
|
|
452
|
+
const c1 = document.createElement('td');
|
|
453
|
+
c1.className = 'px-3 py-2';
|
|
454
|
+
c1.textContent = displayNameForUserId(uid);
|
|
455
|
+
|
|
456
|
+
const c2 = document.createElement('td');
|
|
457
|
+
c2.className = 'px-3 py-2';
|
|
458
|
+
const wrap2 = document.createElement('div');
|
|
459
|
+
const in2 = document.createElement('input');
|
|
460
|
+
in2.type = 'text';
|
|
461
|
+
in2.className = 'w-56 border rounded px-2 py-1';
|
|
462
|
+
const hint2 = document.createElement('div');
|
|
463
|
+
hint2.className = 'text-xs text-gray-500 mt-1';
|
|
464
|
+
in2.value = hasUpload ? formatHumanBytes(cfg.maxUploadBytes) : '';
|
|
465
|
+
hint2.textContent = hasUpload ? `${Number(cfg.maxUploadBytes)} bytes` : '';
|
|
466
|
+
in2.addEventListener('blur', () => {
|
|
467
|
+
const parsed = parseHumanBytes(in2.value);
|
|
468
|
+
if (!orgPolicy.users[uid]) orgPolicy.users[uid] = {};
|
|
469
|
+
if (parsed === null) {
|
|
470
|
+
delete orgPolicy.users[uid].maxUploadBytes;
|
|
471
|
+
hint2.textContent = '';
|
|
472
|
+
} else {
|
|
473
|
+
orgPolicy.users[uid].maxUploadBytes = parsed;
|
|
474
|
+
hint2.textContent = `${parsed} bytes`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const u2 = orgPolicy.users[uid] || {};
|
|
478
|
+
const stillHas = (Number(u2.maxUploadBytes) > 0) || (Number(u2.maxStorageBytes) > 0);
|
|
479
|
+
if (!stillHas) {
|
|
480
|
+
delete orgPolicy.users[uid];
|
|
481
|
+
renderUserOverridesTable();
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
wrap2.appendChild(in2);
|
|
485
|
+
wrap2.appendChild(hint2);
|
|
486
|
+
c2.appendChild(wrap2);
|
|
487
|
+
|
|
488
|
+
const c3 = document.createElement('td');
|
|
489
|
+
c3.className = 'px-3 py-2';
|
|
490
|
+
const wrap3 = document.createElement('div');
|
|
491
|
+
const in3 = document.createElement('input');
|
|
492
|
+
in3.type = 'text';
|
|
493
|
+
in3.className = 'w-56 border rounded px-2 py-1';
|
|
494
|
+
const hint3 = document.createElement('div');
|
|
495
|
+
hint3.className = 'text-xs text-gray-500 mt-1';
|
|
496
|
+
in3.value = hasStorage ? formatHumanBytes(cfg.maxStorageBytes) : '';
|
|
497
|
+
hint3.textContent = hasStorage ? `${Number(cfg.maxStorageBytes)} bytes` : '';
|
|
498
|
+
in3.addEventListener('blur', () => {
|
|
499
|
+
const parsed = parseHumanBytes(in3.value);
|
|
500
|
+
if (!orgPolicy.users[uid]) orgPolicy.users[uid] = {};
|
|
501
|
+
if (parsed === null) {
|
|
502
|
+
delete orgPolicy.users[uid].maxStorageBytes;
|
|
503
|
+
hint3.textContent = '';
|
|
504
|
+
} else {
|
|
505
|
+
orgPolicy.users[uid].maxStorageBytes = parsed;
|
|
506
|
+
hint3.textContent = `${parsed} bytes`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const u2 = orgPolicy.users[uid] || {};
|
|
510
|
+
const stillHas = (Number(u2.maxUploadBytes) > 0) || (Number(u2.maxStorageBytes) > 0);
|
|
511
|
+
if (!stillHas) {
|
|
512
|
+
delete orgPolicy.users[uid];
|
|
513
|
+
renderUserOverridesTable();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
wrap3.appendChild(in3);
|
|
517
|
+
wrap3.appendChild(hint3);
|
|
518
|
+
c3.appendChild(wrap3);
|
|
519
|
+
|
|
520
|
+
const c4 = document.createElement('td');
|
|
521
|
+
c4.className = 'px-3 py-2 text-right';
|
|
522
|
+
const btn = document.createElement('button');
|
|
523
|
+
btn.className = 'px-3 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 text-sm';
|
|
524
|
+
btn.textContent = 'Remove';
|
|
525
|
+
btn.addEventListener('click', () => {
|
|
526
|
+
delete orgPolicy.users[uid];
|
|
527
|
+
if (selectedUserId === String(uid)) {
|
|
528
|
+
selectedUserId = '';
|
|
529
|
+
elPolicyUserEditor.classList.add('hidden');
|
|
530
|
+
elPolicyUserMaxUploadBytes.value = '';
|
|
531
|
+
elPolicyUserMaxStorageBytes.value = '';
|
|
532
|
+
elPolicyUserMaxUploadHuman.value = '';
|
|
533
|
+
elPolicyUserMaxStorageHuman.value = '';
|
|
534
|
+
updateBytesHint(elPolicyUserMaxUploadBytesHint, null);
|
|
535
|
+
updateBytesHint(elPolicyUserMaxStorageBytesHint, null);
|
|
536
|
+
}
|
|
537
|
+
renderUsersSelect();
|
|
538
|
+
renderUserOverridesTable();
|
|
539
|
+
});
|
|
540
|
+
c4.appendChild(btn);
|
|
541
|
+
|
|
542
|
+
row.appendChild(c1);
|
|
543
|
+
row.appendChild(c2);
|
|
544
|
+
row.appendChild(c3);
|
|
545
|
+
row.appendChild(c4);
|
|
546
|
+
elPolicyUsersBody.appendChild(row);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const applyHumanToBytes = (humanEl, bytesEl, onApplied) => {
|
|
551
|
+
const parsed = parseHumanBytes(humanEl.value);
|
|
552
|
+
if (parsed !== null) {
|
|
553
|
+
bytesEl.value = String(parsed);
|
|
554
|
+
if (typeof onApplied === 'function') onApplied(parsed);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const updateBytesHint = (hintEl, bytes) => {
|
|
559
|
+
if (!hintEl) return;
|
|
560
|
+
const n = Number(bytes);
|
|
561
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
562
|
+
hintEl.textContent = '';
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
hintEl.textContent = `${n} bytes`;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const syncPolicyToInputs = () => {
|
|
569
|
+
const g = storagePolicy.global || {};
|
|
570
|
+
elPolicyGlobalMaxUploadBytes.value = g.maxUploadBytes ? String(g.maxUploadBytes) : '';
|
|
571
|
+
elPolicyGlobalMaxStorageBytes.value = g.maxStorageBytes ? String(g.maxStorageBytes) : '';
|
|
572
|
+
elPolicyGlobalMaxUploadHuman.value = g.maxUploadBytes ? formatHumanBytes(g.maxUploadBytes) : '';
|
|
573
|
+
elPolicyGlobalMaxStorageHuman.value = g.maxStorageBytes ? formatHumanBytes(g.maxStorageBytes) : '';
|
|
574
|
+
updateBytesHint(elPolicyGlobalMaxUploadBytesHint, elPolicyGlobalMaxUploadBytes.value);
|
|
575
|
+
updateBytesHint(elPolicyGlobalMaxStorageBytesHint, elPolicyGlobalMaxStorageBytes.value);
|
|
576
|
+
|
|
577
|
+
if (!selectedOrgId) {
|
|
578
|
+
elPolicyOrgSection.classList.add('hidden');
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
elPolicyOrgSection.classList.remove('hidden');
|
|
583
|
+
const orgPolicy = getOrgPolicy(selectedOrgId);
|
|
584
|
+
elPolicyOrgMaxUploadBytes.value = orgPolicy?.maxUploadBytes ? String(orgPolicy.maxUploadBytes) : '';
|
|
585
|
+
elPolicyOrgMaxStorageBytes.value = orgPolicy?.maxStorageBytes ? String(orgPolicy.maxStorageBytes) : '';
|
|
586
|
+
elPolicyOrgMaxUploadHuman.value = orgPolicy?.maxUploadBytes ? formatHumanBytes(orgPolicy.maxUploadBytes) : '';
|
|
587
|
+
elPolicyOrgMaxStorageHuman.value = orgPolicy?.maxStorageBytes ? formatHumanBytes(orgPolicy.maxStorageBytes) : '';
|
|
588
|
+
updateBytesHint(elPolicyOrgMaxUploadBytesHint, elPolicyOrgMaxUploadBytes.value);
|
|
589
|
+
updateBytesHint(elPolicyOrgMaxStorageBytesHint, elPolicyOrgMaxStorageBytes.value);
|
|
590
|
+
|
|
591
|
+
renderGroupsTable();
|
|
592
|
+
renderUsersSelect();
|
|
593
|
+
renderUserOverridesTable();
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
async function fetchJson(url) {
|
|
597
|
+
const res = await fetch(url);
|
|
598
|
+
if (!res.ok) {
|
|
599
|
+
const data = await res.json().catch(() => ({}));
|
|
600
|
+
throw new Error(data.error || `Request failed: ${res.status}`);
|
|
601
|
+
}
|
|
602
|
+
return await res.json();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function loadStoragePolicy() {
|
|
606
|
+
const setting = await fetchSetting(KEY_STORAGE_POLICY_JSON);
|
|
607
|
+
if (!setting || !setting.value) {
|
|
608
|
+
storagePolicy = { version: 1, global: {}, orgs: {} };
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
storagePolicy = ensurePolicyShape(typeof setting.value === 'string' ? JSON.parse(setting.value) : setting.value);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function loadAdminOrgs() {
|
|
615
|
+
const data = await fetchJson(`${API_BASE}/api/admin/orgs?limit=200`);
|
|
616
|
+
orgsCache = data.orgs || [];
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function loadAdminGroups() {
|
|
620
|
+
const data = await fetchJson(`${API_BASE}/api/admin/rbac/groups`);
|
|
621
|
+
groupsCache = data.groups || [];
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function loadAdminOrgMembers(orgId) {
|
|
625
|
+
orgMembersCache = [];
|
|
626
|
+
if (!orgId) return;
|
|
627
|
+
const data = await fetchJson(`${API_BASE}/api/admin/orgs/${encodeURIComponent(orgId)}/members?limit=200`);
|
|
628
|
+
orgMembersCache = data.members || [];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function refreshStorageConfigUI() {
|
|
632
|
+
await Promise.all([loadStoragePolicy(), loadAdminOrgs(), loadAdminGroups()]);
|
|
633
|
+
renderOrgsSelect();
|
|
634
|
+
await loadAdminOrgMembers(selectedOrgId);
|
|
635
|
+
syncPolicyToInputs();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function saveStoragePolicy() {
|
|
639
|
+
const g = storagePolicy.global || {};
|
|
640
|
+
|
|
641
|
+
const parsedGlobalUpload = parseHumanBytes(elPolicyGlobalMaxUploadHuman.value);
|
|
642
|
+
if (parsedGlobalUpload !== null) elPolicyGlobalMaxUploadBytes.value = String(parsedGlobalUpload);
|
|
643
|
+
const parsedGlobalStorage = parseHumanBytes(elPolicyGlobalMaxStorageHuman.value);
|
|
644
|
+
if (parsedGlobalStorage !== null) elPolicyGlobalMaxStorageBytes.value = String(parsedGlobalStorage);
|
|
645
|
+
|
|
646
|
+
const gUpload = toPositiveIntOrNull(elPolicyGlobalMaxUploadBytes.value);
|
|
647
|
+
const gStorage = toPositiveIntOrNull(elPolicyGlobalMaxStorageBytes.value);
|
|
648
|
+
if (gUpload === null) delete g.maxUploadBytes;
|
|
649
|
+
else g.maxUploadBytes = gUpload;
|
|
650
|
+
if (gStorage === null) delete g.maxStorageBytes;
|
|
651
|
+
else g.maxStorageBytes = gStorage;
|
|
652
|
+
storagePolicy.global = g;
|
|
653
|
+
|
|
654
|
+
if (selectedOrgId) {
|
|
655
|
+
const orgPolicy = getOrgPolicy(selectedOrgId);
|
|
656
|
+
|
|
657
|
+
const parsedOrgUpload = parseHumanBytes(elPolicyOrgMaxUploadHuman.value);
|
|
658
|
+
if (parsedOrgUpload !== null) elPolicyOrgMaxUploadBytes.value = String(parsedOrgUpload);
|
|
659
|
+
const parsedOrgStorage = parseHumanBytes(elPolicyOrgMaxStorageHuman.value);
|
|
660
|
+
if (parsedOrgStorage !== null) elPolicyOrgMaxStorageBytes.value = String(parsedOrgStorage);
|
|
661
|
+
|
|
662
|
+
const ou = toPositiveIntOrNull(elPolicyOrgMaxUploadBytes.value);
|
|
663
|
+
const os = toPositiveIntOrNull(elPolicyOrgMaxStorageBytes.value);
|
|
664
|
+
if (ou === null) delete orgPolicy.maxUploadBytes;
|
|
665
|
+
else orgPolicy.maxUploadBytes = ou;
|
|
666
|
+
if (os === null) delete orgPolicy.maxStorageBytes;
|
|
667
|
+
else orgPolicy.maxStorageBytes = os;
|
|
668
|
+
|
|
669
|
+
if (selectedUserId && !elPolicyUserEditor.classList.contains('hidden')) {
|
|
670
|
+
const parsedUserUpload = parseHumanBytes(elPolicyUserMaxUploadHuman.value);
|
|
671
|
+
if (parsedUserUpload !== null) elPolicyUserMaxUploadBytes.value = String(parsedUserUpload);
|
|
672
|
+
const parsedUserStorage = parseHumanBytes(elPolicyUserMaxStorageHuman.value);
|
|
673
|
+
if (parsedUserStorage !== null) elPolicyUserMaxStorageBytes.value = String(parsedUserStorage);
|
|
674
|
+
|
|
675
|
+
const userUpload = toPositiveIntOrNull(elPolicyUserMaxUploadBytes.value);
|
|
676
|
+
const userStorage = toPositiveIntOrNull(elPolicyUserMaxStorageBytes.value);
|
|
677
|
+
if (userUpload === null && userStorage === null) {
|
|
678
|
+
delete orgPolicy.users[selectedUserId];
|
|
679
|
+
} else {
|
|
680
|
+
if (!orgPolicy.users[selectedUserId]) orgPolicy.users[selectedUserId] = {};
|
|
681
|
+
if (userUpload === null) delete orgPolicy.users[selectedUserId].maxUploadBytes;
|
|
682
|
+
else orgPolicy.users[selectedUserId].maxUploadBytes = userUpload;
|
|
683
|
+
if (userStorage === null) delete orgPolicy.users[selectedUserId].maxStorageBytes;
|
|
684
|
+
else orgPolicy.users[selectedUserId].maxStorageBytes = userStorage;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
await upsertSetting({
|
|
690
|
+
key: KEY_STORAGE_POLICY_JSON,
|
|
691
|
+
value: JSON.stringify(ensurePolicyShape(storagePolicy)),
|
|
692
|
+
type: 'json',
|
|
693
|
+
description: 'File Manager storage policy (global/org/group/user max upload + max storage)'
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const showToast = (message, type = 'success') => {
|
|
698
|
+
elToast.classList.remove('hidden');
|
|
699
|
+
elToast.className = 'fixed bottom-4 right-4 px-4 py-3 rounded shadow text-white ' + (type === 'success' ? 'bg-green-600' : 'bg-red-600');
|
|
700
|
+
elToast.textContent = message;
|
|
701
|
+
setTimeout(() => elToast.classList.add('hidden'), 3000);
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const normalizeBasePath = (value) => {
|
|
705
|
+
const v = String(value || '').trim();
|
|
706
|
+
if (!v) return '/files';
|
|
707
|
+
return v.startsWith('/') ? v : `/${v}`;
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const updateComputedUrl = () => {
|
|
711
|
+
const basePath = normalizeBasePath(elBasePath.value);
|
|
712
|
+
elComputedUrl.textContent = `${window.location.origin}${BASE_PATH || ''}${basePath}`;
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
async function fetchSetting(key) {
|
|
716
|
+
const res = await fetch(`${API_BASE}/api/admin/settings/${encodeURIComponent(key)}`);
|
|
717
|
+
if (res.status === 404) return null;
|
|
718
|
+
if (!res.ok) throw new Error(`Failed to fetch setting ${key}`);
|
|
719
|
+
return await res.json();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function upsertSetting({ key, value, type, description }) {
|
|
723
|
+
const existing = await fetchSetting(key);
|
|
724
|
+
if (!existing) {
|
|
725
|
+
const res = await fetch(`${API_BASE}/api/admin/settings`, {
|
|
726
|
+
method: 'POST',
|
|
727
|
+
headers: { 'Content-Type': 'application/json' },
|
|
728
|
+
body: JSON.stringify({ key, value, type, description, public: false })
|
|
729
|
+
});
|
|
730
|
+
if (!res.ok) {
|
|
731
|
+
const data = await res.json().catch(() => ({}));
|
|
732
|
+
throw new Error(data.error || `Failed to create setting ${key}`);
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const res = await fetch(`${API_BASE}/api/admin/settings/${encodeURIComponent(key)}`, {
|
|
738
|
+
method: 'PUT',
|
|
739
|
+
headers: { 'Content-Type': 'application/json' },
|
|
740
|
+
body: JSON.stringify({ value })
|
|
741
|
+
});
|
|
742
|
+
if (!res.ok) {
|
|
743
|
+
const data = await res.json().catch(() => ({}));
|
|
744
|
+
throw new Error(data.error || `Failed to update setting ${key}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function load() {
|
|
749
|
+
try {
|
|
750
|
+
const enabledSetting = await fetchSetting(KEY_ENABLED);
|
|
751
|
+
const basePathSetting = await fetchSetting(KEY_BASE_PATH);
|
|
752
|
+
const maxUploadBytesSetting = await fetchSetting(KEY_MAX_UPLOAD_BYTES);
|
|
753
|
+
|
|
754
|
+
elEnabled.checked = enabledSetting ? enabledSetting.value === 'true' : false;
|
|
755
|
+
elBasePath.value = basePathSetting ? String(basePathSetting.value || '') : '/files';
|
|
756
|
+
elMaxUploadBytes.value = maxUploadBytesSetting ? String(maxUploadBytesSetting.value || '') : '1073741824';
|
|
757
|
+
updateComputedUrl();
|
|
758
|
+
|
|
759
|
+
await refreshStorageConfigUI();
|
|
760
|
+
} catch (e) {
|
|
761
|
+
showToast(e.message || 'Failed to load settings', 'error');
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async function save() {
|
|
766
|
+
try {
|
|
767
|
+
const enabledValue = elEnabled.checked ? 'true' : 'false';
|
|
768
|
+
const basePathValue = normalizeBasePath(elBasePath.value);
|
|
769
|
+
const maxUploadBytesValue = String(parseInt(String(elMaxUploadBytes.value || '1073741824'), 10) || 1073741824);
|
|
770
|
+
|
|
771
|
+
await upsertSetting({
|
|
772
|
+
key: KEY_ENABLED,
|
|
773
|
+
value: enabledValue,
|
|
774
|
+
type: 'boolean',
|
|
775
|
+
description: 'Enable public File Manager UI route (restart required)'
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
await upsertSetting({
|
|
779
|
+
key: KEY_BASE_PATH,
|
|
780
|
+
value: basePathValue,
|
|
781
|
+
type: 'string',
|
|
782
|
+
description: 'Public File Manager UI base path (restart required)'
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
await upsertSetting({
|
|
786
|
+
key: KEY_MAX_UPLOAD_BYTES,
|
|
787
|
+
value: maxUploadBytesValue,
|
|
788
|
+
type: 'number',
|
|
789
|
+
description: 'File Manager max upload size in bytes'
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
elBasePath.value = basePathValue;
|
|
793
|
+
elMaxUploadBytes.value = maxUploadBytesValue;
|
|
794
|
+
updateComputedUrl();
|
|
795
|
+
showToast('Saved. Restart required to apply route changes.');
|
|
796
|
+
} catch (e) {
|
|
797
|
+
showToast(e.message || 'Failed to save settings', 'error');
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const ADMIN_TAB_KEY = 'sb_fm_admin_file_manager_tab';
|
|
802
|
+
|
|
803
|
+
const setActiveTab = (tab) => {
|
|
804
|
+
const t = tab === 'storage' ? 'storage' : 'general';
|
|
805
|
+
localStorage.setItem(ADMIN_TAB_KEY, t);
|
|
806
|
+
|
|
807
|
+
const activeClass = 'border-blue-500 text-blue-600';
|
|
808
|
+
const inactiveClass = 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300';
|
|
809
|
+
|
|
810
|
+
elTabGeneral.className = `py-2 px-1 border-b-2 font-medium text-sm ${t === 'general' ? activeClass : inactiveClass}`;
|
|
811
|
+
elTabStorage.className = `py-2 px-1 border-b-2 font-medium text-sm ${t === 'storage' ? activeClass : inactiveClass}`;
|
|
812
|
+
|
|
813
|
+
if (t === 'general') {
|
|
814
|
+
elSectionGeneral.classList.remove('hidden');
|
|
815
|
+
elSectionStorage.classList.add('hidden');
|
|
816
|
+
} else {
|
|
817
|
+
elSectionStorage.classList.remove('hidden');
|
|
818
|
+
elSectionGeneral.classList.add('hidden');
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
elTabGeneral.addEventListener('click', () => setActiveTab('general'));
|
|
823
|
+
elTabStorage.addEventListener('click', () => setActiveTab('storage'));
|
|
824
|
+
|
|
825
|
+
setActiveTab(localStorage.getItem(ADMIN_TAB_KEY) || 'general');
|
|
826
|
+
|
|
827
|
+
elPolicyGlobalMaxUploadHuman.addEventListener('blur', () => applyHumanToBytes(elPolicyGlobalMaxUploadHuman, elPolicyGlobalMaxUploadBytes, (bytes) => updateBytesHint(elPolicyGlobalMaxUploadBytesHint, bytes)));
|
|
828
|
+
elPolicyGlobalMaxStorageHuman.addEventListener('blur', () => applyHumanToBytes(elPolicyGlobalMaxStorageHuman, elPolicyGlobalMaxStorageBytes, (bytes) => updateBytesHint(elPolicyGlobalMaxStorageBytesHint, bytes)));
|
|
829
|
+
elPolicyOrgMaxUploadHuman.addEventListener('blur', () => applyHumanToBytes(elPolicyOrgMaxUploadHuman, elPolicyOrgMaxUploadBytes, (bytes) => updateBytesHint(elPolicyOrgMaxUploadBytesHint, bytes)));
|
|
830
|
+
elPolicyOrgMaxStorageHuman.addEventListener('blur', () => applyHumanToBytes(elPolicyOrgMaxStorageHuman, elPolicyOrgMaxStorageBytes, (bytes) => updateBytesHint(elPolicyOrgMaxStorageBytesHint, bytes)));
|
|
831
|
+
|
|
832
|
+
elPolicyOrgSelect.addEventListener('change', async () => {
|
|
833
|
+
selectedOrgId = String(elPolicyOrgSelect.value || '');
|
|
834
|
+
selectedUserId = '';
|
|
835
|
+
await loadAdminOrgMembers(selectedOrgId);
|
|
836
|
+
syncPolicyToInputs();
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
elPolicyUserApply.addEventListener('click', () => {
|
|
840
|
+
selectedUserId = String(elPolicyUserSelect.value || '');
|
|
841
|
+
const orgPolicy = getOrgPolicy(selectedOrgId);
|
|
842
|
+
if (!selectedUserId) {
|
|
843
|
+
elPolicyUserEditor.classList.add('hidden');
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (!orgPolicy.users[selectedUserId]) orgPolicy.users[selectedUserId] = {};
|
|
848
|
+
const u = orgPolicy.users[selectedUserId];
|
|
849
|
+
elPolicyUserMaxUploadBytes.value = u.maxUploadBytes ? String(u.maxUploadBytes) : '';
|
|
850
|
+
elPolicyUserMaxStorageBytes.value = u.maxStorageBytes ? String(u.maxStorageBytes) : '';
|
|
851
|
+
elPolicyUserMaxUploadHuman.value = u.maxUploadBytes ? formatHumanBytes(u.maxUploadBytes) : '';
|
|
852
|
+
elPolicyUserMaxStorageHuman.value = u.maxStorageBytes ? formatHumanBytes(u.maxStorageBytes) : '';
|
|
853
|
+
updateBytesHint(elPolicyUserMaxUploadBytesHint, elPolicyUserMaxUploadBytes.value);
|
|
854
|
+
updateBytesHint(elPolicyUserMaxStorageBytesHint, elPolicyUserMaxStorageBytes.value);
|
|
855
|
+
elPolicyUserEditor.classList.remove('hidden');
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
elPolicyUserMaxUploadHuman.addEventListener('blur', () => applyHumanToBytes(elPolicyUserMaxUploadHuman, elPolicyUserMaxUploadBytes, (bytes) => {
|
|
859
|
+
updateBytesHint(elPolicyUserMaxUploadBytesHint, bytes);
|
|
860
|
+
elPolicyUserMaxUploadBytes.dispatchEvent(new Event('input'));
|
|
861
|
+
}));
|
|
862
|
+
elPolicyUserMaxStorageHuman.addEventListener('blur', () => applyHumanToBytes(elPolicyUserMaxStorageHuman, elPolicyUserMaxStorageBytes, (bytes) => {
|
|
863
|
+
updateBytesHint(elPolicyUserMaxStorageBytesHint, bytes);
|
|
864
|
+
elPolicyUserMaxStorageBytes.dispatchEvent(new Event('input'));
|
|
865
|
+
}));
|
|
866
|
+
|
|
867
|
+
elPolicyUserMaxUploadBytes.addEventListener('input', () => {
|
|
868
|
+
if (!selectedOrgId || !selectedUserId) return;
|
|
869
|
+
const orgPolicy = getOrgPolicy(selectedOrgId);
|
|
870
|
+
if (!orgPolicy.users[selectedUserId]) orgPolicy.users[selectedUserId] = {};
|
|
871
|
+
const val = toPositiveIntOrNull(elPolicyUserMaxUploadBytes.value);
|
|
872
|
+
if (val === null) delete orgPolicy.users[selectedUserId].maxUploadBytes;
|
|
873
|
+
else orgPolicy.users[selectedUserId].maxUploadBytes = val;
|
|
874
|
+
updateBytesHint(elPolicyUserMaxUploadBytesHint, val);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
elPolicyUserMaxStorageBytes.addEventListener('input', () => {
|
|
878
|
+
if (!selectedOrgId || !selectedUserId) return;
|
|
879
|
+
const orgPolicy = getOrgPolicy(selectedOrgId);
|
|
880
|
+
if (!orgPolicy.users[selectedUserId]) orgPolicy.users[selectedUserId] = {};
|
|
881
|
+
const val = toPositiveIntOrNull(elPolicyUserMaxStorageBytes.value);
|
|
882
|
+
if (val === null) delete orgPolicy.users[selectedUserId].maxStorageBytes;
|
|
883
|
+
else orgPolicy.users[selectedUserId].maxStorageBytes = val;
|
|
884
|
+
updateBytesHint(elPolicyUserMaxStorageBytesHint, val);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
elPolicyUserClear.addEventListener('click', () => {
|
|
888
|
+
if (!selectedOrgId || !selectedUserId) return;
|
|
889
|
+
const orgPolicy = getOrgPolicy(selectedOrgId);
|
|
890
|
+
delete orgPolicy.users[selectedUserId];
|
|
891
|
+
elPolicyUserEditor.classList.add('hidden');
|
|
892
|
+
elPolicyUserMaxUploadBytes.value = '';
|
|
893
|
+
elPolicyUserMaxStorageBytes.value = '';
|
|
894
|
+
elPolicyUserMaxUploadHuman.value = '';
|
|
895
|
+
elPolicyUserMaxStorageHuman.value = '';
|
|
896
|
+
updateBytesHint(elPolicyUserMaxUploadBytesHint, null);
|
|
897
|
+
updateBytesHint(elPolicyUserMaxStorageBytesHint, null);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
elPolicyRefreshGroups.addEventListener('click', async () => {
|
|
901
|
+
try {
|
|
902
|
+
await loadAdminGroups();
|
|
903
|
+
renderGroupsTable();
|
|
904
|
+
showToast('Groups refreshed.');
|
|
905
|
+
} catch (e) {
|
|
906
|
+
showToast(e.message || 'Failed to refresh groups', 'error');
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
document.getElementById('policyRefresh').addEventListener('click', async () => {
|
|
911
|
+
try {
|
|
912
|
+
await refreshStorageConfigUI();
|
|
913
|
+
showToast('Storage config refreshed.');
|
|
914
|
+
} catch (e) {
|
|
915
|
+
showToast(e.message || 'Failed to refresh storage config', 'error');
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
document.getElementById('policySave').addEventListener('click', async () => {
|
|
920
|
+
try {
|
|
921
|
+
await saveStoragePolicy();
|
|
922
|
+
showToast('Storage config saved.');
|
|
923
|
+
} catch (e) {
|
|
924
|
+
showToast(e.message || 'Failed to save storage config', 'error');
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
document.getElementById('refresh').addEventListener('click', load);
|
|
929
|
+
document.getElementById('save').addEventListener('click', save);
|
|
930
|
+
elBasePath.addEventListener('input', updateComputedUrl);
|
|
931
|
+
|
|
932
|
+
elOpenComputedUrl.addEventListener('click', () => {
|
|
933
|
+
const url = elComputedUrl.textContent;
|
|
934
|
+
if (url) {
|
|
935
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
load();
|
|
940
|
+
</script>
|
|
941
|
+
</body>
|
|
942
|
+
</html>
|