@intranefr/superbackend 1.5.0 → 1.5.2
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 +15 -0
- package/README.md +11 -0
- package/analysis-only.skill +0 -0
- package/index.js +23 -0
- package/package.json +8 -2
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +90 -6
- 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/adminExperiments.controller.js +200 -0
- package/src/controllers/adminHeadless.controller.js +9 -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 +126 -4
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- 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/experiments.controller.js +85 -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/internalExperiments.controller.js +17 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +80 -0
- package/src/helpers/mongooseHelper.js +258 -0
- package/src/helpers/scriptBase.js +230 -0
- package/src/helpers/scriptRunner.js +335 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +810 -48
- 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/Experiment.js +75 -0
- package/src/models/ExperimentAssignment.js +23 -0
- package/src/models/ExperimentEvent.js +26 -0
- package/src/models/ExperimentMetricBucket.js +30 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/GlobalSetting.js +1 -2
- 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 +1 -0
- package/src/models/Webhook.js +2 -0
- package/src/routes/admin.routes.js +2 -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/adminExperiments.routes.js +29 -0
- package/src/routes/adminHeadless.routes.js +2 -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/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminUiComponents.routes.js +2 -1
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/experiments.routes.js +30 -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/internalExperiments.routes.js +15 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +1 -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/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 +185 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +738 -0
- package/src/services/consoleOverride.service.js +7 -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/experiments.service.js +273 -0
- package/src/services/experimentsAggregation.service.js +308 -0
- package/src/services/experimentsCronsBootstrap.service.js +118 -0
- package/src/services/experimentsRetention.service.js +43 -0
- package/src/services/experimentsWs.service.js +134 -0
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/globalSettings.service.js +15 -0
- 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/jsonConfigs.service.js +2 -2
- 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 +215 -15
- package/src/services/uiComponentsAi.service.js +6 -19
- 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 +33 -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-dashboard.ejs +28 -8
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-experiments.ejs +91 -0
- package/views/admin-file-manager.ejs +942 -0
- 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 +163 -1
- 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 +597 -3
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-ui-components.ejs +57 -25
- 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 +12 -0
- package/views/partials/dashboard/palette.ejs +5 -3
- package/views/partials/llm-provider-model-picker.ejs +183 -0
- package/src/routes/llmUi.routes.js +0 -26
|
@@ -0,0 +1,491 @@
|
|
|
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>Proxy system - Admin</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
9
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body class="bg-gray-50">
|
|
12
|
+
<div id="app" class="max-w-7xl mx-auto px-6 py-6" v-cloak>
|
|
13
|
+
<div class="flex items-center justify-between mb-6">
|
|
14
|
+
<div>
|
|
15
|
+
<h1 class="text-2xl font-semibold text-gray-900">Proxy system</h1>
|
|
16
|
+
<div class="text-sm text-gray-500">Configure proxy entries for <code>/proxy/*</code>, including allow/deny, rate limits, caching, and transforms.</div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="flex items-center gap-2">
|
|
19
|
+
<button @click="loadAll" class="px-3 py-2 rounded bg-gray-600 text-white text-sm hover:bg-gray-700">
|
|
20
|
+
<i class="ti ti-refresh mr-1"></i> Refresh
|
|
21
|
+
</button>
|
|
22
|
+
<button @click="createEntry" class="px-3 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700">
|
|
23
|
+
<i class="ti ti-plus mr-1"></i> New entry
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="flex gap-2 mb-5">
|
|
29
|
+
<button @click="activeTab='entries'" :class="tabClass('entries')" class="px-4 py-2 rounded">Entries</button>
|
|
30
|
+
<button @click="activeTab='audit'" :class="tabClass('audit')" class="px-4 py-2 rounded">Audit</button>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div v-if="activeTab==='entries'" class="grid grid-cols-12 gap-6">
|
|
34
|
+
<div class="col-span-5 space-y-6">
|
|
35
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
36
|
+
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
37
|
+
<div class="text-sm font-semibold text-gray-800">Configured entries</div>
|
|
38
|
+
<div class="text-xs text-gray-400">Stored in Mongo</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="p-2">
|
|
41
|
+
<div class="flex items-center gap-2 p-2">
|
|
42
|
+
<input v-model="filter" class="w-full border rounded px-3 py-2 text-sm" placeholder="Filter (name / match value)" />
|
|
43
|
+
</div>
|
|
44
|
+
<div class="max-h-[420px] overflow-auto">
|
|
45
|
+
<button
|
|
46
|
+
v-for="it in filteredEntries"
|
|
47
|
+
:key="it._id"
|
|
48
|
+
class="w-full text-left px-3 py-2 border-t hover:bg-gray-50"
|
|
49
|
+
@click="selectEntry(it)"
|
|
50
|
+
>
|
|
51
|
+
<div class="flex items-center justify-between">
|
|
52
|
+
<div class="font-semibold text-sm text-gray-900">{{ it.name || '(unnamed)' }}</div>
|
|
53
|
+
<div class="text-xs" :class="it.enabled ? 'text-green-700' : 'text-gray-500'">
|
|
54
|
+
{{ it.enabled ? 'enabled' : 'disabled' }}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="text-xs text-gray-600 mt-1">
|
|
58
|
+
match: {{ it.match?.type }} {{ it.match?.applyTo }} = {{ it.match?.value }}
|
|
59
|
+
</div>
|
|
60
|
+
<div class="text-xs text-gray-400 mt-1">
|
|
61
|
+
policy: {{ it.policy?.mode || 'whitelist' }}
|
|
62
|
+
</div>
|
|
63
|
+
</button>
|
|
64
|
+
<div v-if="filteredEntries.length===0" class="p-4 text-sm text-gray-500">No entries.</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
70
|
+
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
71
|
+
<div class="text-sm font-semibold text-gray-800">Discoveries (ephemeral)</div>
|
|
72
|
+
<div class="text-xs text-gray-400">Cache Layer TTL</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="p-4">
|
|
75
|
+
<div class="text-xs text-gray-500 mb-3">
|
|
76
|
+
Unknown <code>/proxy/*</code> requests are denied, but recorded here for visibility.
|
|
77
|
+
</div>
|
|
78
|
+
<div class="max-h-[320px] overflow-auto">
|
|
79
|
+
<div v-for="d in discoveries" :key="d.key" class="border-t py-2">
|
|
80
|
+
<div class="text-xs text-gray-700 break-all"><span class="font-semibold">{{ d.host }}</span> — {{ d.path }}</div>
|
|
81
|
+
<div class="text-xs text-gray-400 break-all">{{ d.targetUrl }}</div>
|
|
82
|
+
<div class="text-xs text-gray-500 mt-1">count: {{ d.count }} | last: {{ d.lastSeenAt }}</div>
|
|
83
|
+
<div class="mt-2">
|
|
84
|
+
<button class="px-2 py-1 rounded bg-gray-100 hover:bg-gray-200 text-xs" @click="prefillFromDiscovery(d)">
|
|
85
|
+
Create entry from this
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<div v-if="discoveries.length===0" class="text-sm text-gray-500">No discoveries yet.</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="col-span-7 space-y-6">
|
|
96
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
97
|
+
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
98
|
+
<div class="text-sm font-semibold text-gray-800">Editor</div>
|
|
99
|
+
<div class="flex gap-2">
|
|
100
|
+
<button v-if="selected && selected._id" @click="deleteSelected" class="px-3 py-2 rounded bg-red-600 text-white text-sm hover:bg-red-700">Delete</button>
|
|
101
|
+
<button v-if="selected" @click="saveSelected" class="px-3 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700">Save</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div v-if="!selected" class="p-4 text-sm text-gray-500">Select an entry or click “New entry”.</div>
|
|
106
|
+
|
|
107
|
+
<div v-else class="p-4 space-y-6">
|
|
108
|
+
<div class="grid grid-cols-2 gap-4">
|
|
109
|
+
<div>
|
|
110
|
+
<label class="text-xs font-semibold text-gray-600">Name</label>
|
|
111
|
+
<input v-model="selected.name" class="mt-1 w-full border rounded px-3 py-2" />
|
|
112
|
+
</div>
|
|
113
|
+
<div class="flex items-center gap-3 mt-6">
|
|
114
|
+
<input id="enabled" type="checkbox" v-model="selected.enabled" class="w-4 h-4" />
|
|
115
|
+
<label for="enabled" class="text-sm text-gray-800">Enabled</label>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="grid grid-cols-3 gap-4">
|
|
120
|
+
<div>
|
|
121
|
+
<label class="text-xs font-semibold text-gray-600">Match type</label>
|
|
122
|
+
<select v-model="selected.match.type" class="mt-1 w-full border rounded px-3 py-2">
|
|
123
|
+
<option value="exact">exact</option>
|
|
124
|
+
<option value="contains">contains</option>
|
|
125
|
+
<option value="regexp">regexp</option>
|
|
126
|
+
</select>
|
|
127
|
+
</div>
|
|
128
|
+
<div>
|
|
129
|
+
<label class="text-xs font-semibold text-gray-600">Match field</label>
|
|
130
|
+
<select v-model="selected.match.applyTo" class="mt-1 w-full border rounded px-3 py-2">
|
|
131
|
+
<option value="host">host</option>
|
|
132
|
+
<option value="path">path</option>
|
|
133
|
+
<option value="targetUrl">targetUrl</option>
|
|
134
|
+
</select>
|
|
135
|
+
</div>
|
|
136
|
+
<div>
|
|
137
|
+
<label class="text-xs font-semibold text-gray-600">Regexp flags</label>
|
|
138
|
+
<input v-model="selected.match.flags" class="mt-1 w-full border rounded px-3 py-2" placeholder="i" />
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div>
|
|
143
|
+
<label class="text-xs font-semibold text-gray-600">Match value</label>
|
|
144
|
+
<input v-model="selected.match.value" class="mt-1 w-full border rounded px-3 py-2" placeholder="api2.com" />
|
|
145
|
+
<div class="text-xs text-gray-400 mt-1">Requests are denied unless a matching entry is enabled.</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div class="grid grid-cols-2 gap-4">
|
|
149
|
+
<div>
|
|
150
|
+
<label class="text-xs font-semibold text-gray-600">Policy mode</label>
|
|
151
|
+
<select v-model="selected.policy.mode" class="mt-1 w-full border rounded px-3 py-2">
|
|
152
|
+
<option value="whitelist">whitelist (default deny)</option>
|
|
153
|
+
<option value="blacklist">blacklist (default allow)</option>
|
|
154
|
+
<option value="allowAll">allowAll</option>
|
|
155
|
+
<option value="denyAll">denyAll</option>
|
|
156
|
+
</select>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="flex items-center gap-3 mt-6">
|
|
159
|
+
<button class="px-3 py-2 rounded bg-gray-100 hover:bg-gray-200 text-sm" @click="addRule">Add rule</button>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div class="border rounded">
|
|
164
|
+
<div class="px-3 py-2 text-xs font-semibold text-gray-600 border-b bg-gray-50">Rules</div>
|
|
165
|
+
<div class="p-3 space-y-2">
|
|
166
|
+
<div v-for="(r, idx) in selected.policy.rules" :key="idx" class="grid grid-cols-12 gap-2 items-center">
|
|
167
|
+
<div class="col-span-1">
|
|
168
|
+
<input type="checkbox" v-model="r.enabled" class="w-4 h-4" />
|
|
169
|
+
</div>
|
|
170
|
+
<div class="col-span-3">
|
|
171
|
+
<select v-model="r.type" class="w-full border rounded px-2 py-1 text-sm">
|
|
172
|
+
<option value="contains">contains</option>
|
|
173
|
+
<option value="regexp">regexp</option>
|
|
174
|
+
</select>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="col-span-3">
|
|
177
|
+
<select v-model="r.applyTo" class="w-full border rounded px-2 py-1 text-sm">
|
|
178
|
+
<option value="targetUrl">targetUrl</option>
|
|
179
|
+
<option value="host">host</option>
|
|
180
|
+
<option value="path">path</option>
|
|
181
|
+
</select>
|
|
182
|
+
</div>
|
|
183
|
+
<div class="col-span-4">
|
|
184
|
+
<input v-model="r.value" class="w-full border rounded px-2 py-1 text-sm" placeholder="api2" />
|
|
185
|
+
</div>
|
|
186
|
+
<div class="col-span-1">
|
|
187
|
+
<button class="text-xs text-red-600" @click="removeRule(idx)">Remove</button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
<div v-if="selected.policy.rules.length===0" class="text-sm text-gray-500">No rules.</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div class="grid grid-cols-2 gap-4">
|
|
195
|
+
<div class="border rounded p-3">
|
|
196
|
+
<div class="text-xs font-semibold text-gray-600">Rate limit</div>
|
|
197
|
+
<div class="mt-2 flex items-center gap-3">
|
|
198
|
+
<input type="checkbox" v-model="selected.rateLimit.enabled" class="w-4 h-4" />
|
|
199
|
+
<div class="text-sm text-gray-800">Enabled</div>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="mt-2">
|
|
202
|
+
<label class="text-xs font-semibold text-gray-600">Limiter ID</label>
|
|
203
|
+
<input v-model="selected.rateLimit.limiterId" class="mt-1 w-full border rounded px-3 py-2" placeholder="proxy:..." />
|
|
204
|
+
<div class="text-xs text-gray-400 mt-1">Configure details in Rate Limiter UI.</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div class="border rounded p-3">
|
|
209
|
+
<div class="text-xs font-semibold text-gray-600">Cache</div>
|
|
210
|
+
<div class="mt-2 flex items-center gap-3">
|
|
211
|
+
<input type="checkbox" v-model="selected.cache.enabled" class="w-4 h-4" />
|
|
212
|
+
<div class="text-sm text-gray-800">Enabled</div>
|
|
213
|
+
</div>
|
|
214
|
+
<div class="mt-2 grid grid-cols-2 gap-3">
|
|
215
|
+
<div>
|
|
216
|
+
<label class="text-xs font-semibold text-gray-600">TTL seconds</label>
|
|
217
|
+
<input type="number" v-model.number="selected.cache.ttlSeconds" class="mt-1 w-full border rounded px-3 py-2" />
|
|
218
|
+
</div>
|
|
219
|
+
<div>
|
|
220
|
+
<label class="text-xs font-semibold text-gray-600">Namespace</label>
|
|
221
|
+
<input v-model="selected.cache.namespace" class="mt-1 w-full border rounded px-3 py-2" />
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
<div class="mt-2">
|
|
225
|
+
<label class="text-xs font-semibold text-gray-600">Allowed methods (cached)</label>
|
|
226
|
+
<input v-model="cacheMethods" class="mt-1 w-full border rounded px-3 py-2" placeholder="GET,HEAD" />
|
|
227
|
+
<div class="text-xs text-gray-400 mt-1">Default: GET,HEAD. Other methods require admin enable.</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div class="border rounded p-3">
|
|
233
|
+
<div class="text-xs font-semibold text-gray-600">Headers</div>
|
|
234
|
+
<div class="mt-2 grid grid-cols-2 gap-4">
|
|
235
|
+
<label class="flex items-center gap-2 text-sm text-gray-800"><input type="checkbox" v-model="selected.headers.forwardAuthorization" class="w-4 h-4" /> Forward Authorization</label>
|
|
236
|
+
<label class="flex items-center gap-2 text-sm text-gray-800"><input type="checkbox" v-model="selected.headers.forwardCookie" class="w-4 h-4" /> Forward Cookie</label>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="mt-3 grid grid-cols-2 gap-4">
|
|
239
|
+
<div>
|
|
240
|
+
<label class="text-xs font-semibold text-gray-600">Allow list (comma-separated, optional)</label>
|
|
241
|
+
<input v-model="headersAllowList" class="mt-1 w-full border rounded px-3 py-2" placeholder="x-api-key,x-tenant" />
|
|
242
|
+
</div>
|
|
243
|
+
<div>
|
|
244
|
+
<label class="text-xs font-semibold text-gray-600">Deny list (comma-separated, optional)</label>
|
|
245
|
+
<input v-model="headersDenyList" class="mt-1 w-full border rounded px-3 py-2" placeholder="x-internal" />
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div class="border rounded p-3">
|
|
251
|
+
<div class="text-xs font-semibold text-gray-600">Transform (JS)</div>
|
|
252
|
+
<div class="mt-2 flex items-center gap-3">
|
|
253
|
+
<input type="checkbox" v-model="selected.transform.enabled" class="w-4 h-4" />
|
|
254
|
+
<div class="text-sm text-gray-800">Enabled</div>
|
|
255
|
+
</div>
|
|
256
|
+
<div class="mt-2 grid grid-cols-2 gap-4">
|
|
257
|
+
<div>
|
|
258
|
+
<label class="text-xs font-semibold text-gray-600">Timeout (ms)</label>
|
|
259
|
+
<input type="number" v-model.number="selected.transform.timeoutMs" class="mt-1 w-full border rounded px-3 py-2" />
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="mt-3">
|
|
263
|
+
<label class="text-xs font-semibold text-gray-600">Code (define <code>function transform(ctx)</code>)</label>
|
|
264
|
+
<textarea v-model="selected.transform.code" rows="10" class="mt-1 w-full border rounded px-3 py-2 font-mono text-xs" placeholder="function transform(ctx) {\n return {};\n}"></textarea>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<div v-if="error" class="p-3 rounded bg-red-50 border border-red-200 text-sm text-red-700">{{ error }}</div>
|
|
269
|
+
<div v-if="toast" class="p-3 rounded bg-green-50 border border-green-200 text-sm text-green-700">{{ toast }}</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div v-if="activeTab==='audit'" class="bg-white border border-gray-200 rounded-lg">
|
|
276
|
+
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
277
|
+
<div class="text-sm font-semibold text-gray-800">Proxy audit</div>
|
|
278
|
+
<div class="flex items-center gap-2">
|
|
279
|
+
<button @click="loadAudit" class="px-3 py-2 rounded bg-gray-100 hover:bg-gray-200 text-sm">Refresh</button>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
<div class="p-4">
|
|
283
|
+
<div class="text-xs text-gray-500 mb-3">Filtered from the global Audit Logs API by <code>action</code> prefix <code>proxy.</code>.</div>
|
|
284
|
+
<div class="max-h-[560px] overflow-auto border rounded">
|
|
285
|
+
<div v-for="evt in auditEvents" :key="evt._id" class="border-b p-3">
|
|
286
|
+
<div class="flex items-center justify-between">
|
|
287
|
+
<div class="text-sm font-semibold text-gray-900">{{ evt.action }}</div>
|
|
288
|
+
<div class="text-xs" :class="evt.outcome==='failure' ? 'text-red-700' : 'text-green-700'">{{ evt.outcome }}</div>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="text-xs text-gray-500 mt-1">{{ evt.at || evt.createdAt }} — {{ evt.context?.method }} {{ evt.context?.path }}</div>
|
|
291
|
+
<div class="text-xs text-gray-700 mt-2 break-all" v-if="evt.details && evt.details.targetUrl">target: {{ evt.details.targetUrl }}</div>
|
|
292
|
+
<details class="mt-2">
|
|
293
|
+
<summary class="text-xs text-gray-500 cursor-pointer">Details</summary>
|
|
294
|
+
<pre class="text-xs mt-2 bg-gray-50 border rounded p-2 overflow-auto">{{ JSON.stringify(evt.details || evt.meta || {}, null, 2) }}</pre>
|
|
295
|
+
</details>
|
|
296
|
+
</div>
|
|
297
|
+
<div v-if="auditEvents.length===0" class="p-4 text-sm text-gray-500">No proxy audit events found.</div>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<script>
|
|
305
|
+
const { createApp } = Vue;
|
|
306
|
+
|
|
307
|
+
createApp({
|
|
308
|
+
data() {
|
|
309
|
+
return {
|
|
310
|
+
activeTab: 'entries',
|
|
311
|
+
items: [],
|
|
312
|
+
discoveries: [],
|
|
313
|
+
selected: null,
|
|
314
|
+
filter: '',
|
|
315
|
+
error: '',
|
|
316
|
+
toast: '',
|
|
317
|
+
auditEvents: [],
|
|
318
|
+
auditPage: 1,
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
computed: {
|
|
322
|
+
filteredEntries() {
|
|
323
|
+
const q = (this.filter || '').toLowerCase().trim();
|
|
324
|
+
if (!q) return this.items;
|
|
325
|
+
return (this.items || []).filter((it) => {
|
|
326
|
+
const name = (it.name || '').toLowerCase();
|
|
327
|
+
const mv = (it.match && it.match.value ? String(it.match.value) : '').toLowerCase();
|
|
328
|
+
return name.includes(q) || mv.includes(q);
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
cacheMethods: {
|
|
332
|
+
get() {
|
|
333
|
+
const arr = (this.selected && this.selected.cache && Array.isArray(this.selected.cache.methods)) ? this.selected.cache.methods : ['GET', 'HEAD'];
|
|
334
|
+
return arr.join(',');
|
|
335
|
+
},
|
|
336
|
+
set(value) {
|
|
337
|
+
const parts = String(value || '').split(',').map((x) => x.trim().toUpperCase()).filter(Boolean);
|
|
338
|
+
if (this.selected && this.selected.cache) this.selected.cache.methods = parts.length ? parts : ['GET', 'HEAD'];
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
headersAllowList: {
|
|
342
|
+
get() {
|
|
343
|
+
const arr = (this.selected && this.selected.headers && Array.isArray(this.selected.headers.allowList)) ? this.selected.headers.allowList : [];
|
|
344
|
+
return arr.join(',');
|
|
345
|
+
},
|
|
346
|
+
set(value) {
|
|
347
|
+
const parts = String(value || '').split(',').map((x) => x.trim().toLowerCase()).filter(Boolean);
|
|
348
|
+
if (this.selected && this.selected.headers) this.selected.headers.allowList = parts;
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
headersDenyList: {
|
|
352
|
+
get() {
|
|
353
|
+
const arr = (this.selected && this.selected.headers && Array.isArray(this.selected.headers.denyList)) ? this.selected.headers.denyList : [];
|
|
354
|
+
return arr.join(',');
|
|
355
|
+
},
|
|
356
|
+
set(value) {
|
|
357
|
+
const parts = String(value || '').split(',').map((x) => x.trim().toLowerCase()).filter(Boolean);
|
|
358
|
+
if (this.selected && this.selected.headers) this.selected.headers.denyList = parts;
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
methods: {
|
|
363
|
+
tabClass(id) {
|
|
364
|
+
return this.activeTab === id ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-800 hover:bg-gray-300';
|
|
365
|
+
},
|
|
366
|
+
setToast(msg) {
|
|
367
|
+
this.toast = msg;
|
|
368
|
+
setTimeout(() => { this.toast = ''; }, 2500);
|
|
369
|
+
},
|
|
370
|
+
async loadAll() {
|
|
371
|
+
this.error = '';
|
|
372
|
+
const res = await fetch('/api/admin/proxy/entries', { headers: { 'Accept': 'application/json' } });
|
|
373
|
+
if (!res.ok) {
|
|
374
|
+
this.error = 'Failed to load proxy entries';
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const data = await res.json();
|
|
378
|
+
this.items = data.items || [];
|
|
379
|
+
this.discoveries = data.discoveries || [];
|
|
380
|
+
},
|
|
381
|
+
selectEntry(it) {
|
|
382
|
+
this.error = '';
|
|
383
|
+
this.selected = JSON.parse(JSON.stringify(it));
|
|
384
|
+
this.ensureDefaults();
|
|
385
|
+
},
|
|
386
|
+
ensureDefaults() {
|
|
387
|
+
if (!this.selected.match) this.selected.match = { type: 'contains', applyTo: 'host', value: '', flags: 'i' };
|
|
388
|
+
if (!this.selected.policy) this.selected.policy = { mode: 'whitelist', rules: [] };
|
|
389
|
+
if (!Array.isArray(this.selected.policy.rules)) this.selected.policy.rules = [];
|
|
390
|
+
if (!this.selected.rateLimit) this.selected.rateLimit = { enabled: false, limiterId: null };
|
|
391
|
+
if (!this.selected.cache) this.selected.cache = { enabled: false, ttlSeconds: 60, namespace: 'proxy', methods: ['GET','HEAD'] };
|
|
392
|
+
if (!Array.isArray(this.selected.cache.methods)) this.selected.cache.methods = ['GET','HEAD'];
|
|
393
|
+
if (!this.selected.headers) this.selected.headers = { forwardAuthorization: true, forwardCookie: true, allowList: [], denyList: [] };
|
|
394
|
+
if (!Array.isArray(this.selected.headers.allowList)) this.selected.headers.allowList = [];
|
|
395
|
+
if (!Array.isArray(this.selected.headers.denyList)) this.selected.headers.denyList = [];
|
|
396
|
+
if (!this.selected.transform) this.selected.transform = { enabled: false, timeoutMs: 200, code: 'function transform(ctx) {\n return {};\n}' };
|
|
397
|
+
},
|
|
398
|
+
createEntry() {
|
|
399
|
+
this.selected = {
|
|
400
|
+
name: '',
|
|
401
|
+
enabled: false,
|
|
402
|
+
match: { type: 'contains', applyTo: 'host', value: '', flags: 'i' },
|
|
403
|
+
policy: { mode: 'whitelist', rules: [] },
|
|
404
|
+
rateLimit: { enabled: false, limiterId: null },
|
|
405
|
+
cache: { enabled: false, ttlSeconds: 60, namespace: 'proxy', methods: ['GET','HEAD'] },
|
|
406
|
+
headers: { forwardAuthorization: true, forwardCookie: true, allowList: [], denyList: [] },
|
|
407
|
+
transform: { enabled: false, timeoutMs: 200, code: 'function transform(ctx) {\n return {};\n}' },
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
prefillFromDiscovery(d) {
|
|
411
|
+
this.createEntry();
|
|
412
|
+
this.selected.name = d.host || '';
|
|
413
|
+
this.selected.match.applyTo = 'host';
|
|
414
|
+
this.selected.match.type = 'contains';
|
|
415
|
+
this.selected.match.value = d.host || '';
|
|
416
|
+
},
|
|
417
|
+
addRule() {
|
|
418
|
+
this.selected.policy.rules.push({ enabled: true, type: 'contains', applyTo: 'targetUrl', value: '' });
|
|
419
|
+
},
|
|
420
|
+
removeRule(idx) {
|
|
421
|
+
this.selected.policy.rules.splice(idx, 1);
|
|
422
|
+
},
|
|
423
|
+
async saveSelected() {
|
|
424
|
+
this.error = '';
|
|
425
|
+
this.toast = '';
|
|
426
|
+
const payload = JSON.parse(JSON.stringify(this.selected));
|
|
427
|
+
|
|
428
|
+
if (!payload.match || !payload.match.value) {
|
|
429
|
+
this.error = 'match.value is required';
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const isUpdate = Boolean(payload._id);
|
|
434
|
+
const url = isUpdate ? `/api/admin/proxy/entries/${payload._id}` : '/api/admin/proxy/entries';
|
|
435
|
+
const method = isUpdate ? 'PUT' : 'POST';
|
|
436
|
+
|
|
437
|
+
const res = await fetch(url, {
|
|
438
|
+
method,
|
|
439
|
+
headers: { 'Content-Type': 'application/json' },
|
|
440
|
+
body: JSON.stringify(payload),
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (!res.ok) {
|
|
444
|
+
const data = await res.json().catch(() => ({}));
|
|
445
|
+
this.error = data.error || 'Save failed';
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const data = await res.json().catch(() => ({}));
|
|
450
|
+
if (data.item) {
|
|
451
|
+
this.selected = JSON.parse(JSON.stringify(data.item));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
await this.loadAll();
|
|
455
|
+
this.setToast('Saved');
|
|
456
|
+
},
|
|
457
|
+
async deleteSelected() {
|
|
458
|
+
this.error = '';
|
|
459
|
+
if (!this.selected || !this.selected._id) return;
|
|
460
|
+
|
|
461
|
+
const res = await fetch(`/api/admin/proxy/entries/${this.selected._id}`, { method: 'DELETE' });
|
|
462
|
+
if (!res.ok) {
|
|
463
|
+
this.error = 'Delete failed';
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
this.selected = null;
|
|
468
|
+
await this.loadAll();
|
|
469
|
+
this.setToast('Deleted');
|
|
470
|
+
},
|
|
471
|
+
async loadAudit() {
|
|
472
|
+
this.error = '';
|
|
473
|
+
const params = new URLSearchParams({ page: String(this.auditPage || 1), pageSize: '50', action: 'proxy\.' });
|
|
474
|
+
const res = await fetch(`/api/admin/audit?${params.toString()}`);
|
|
475
|
+
if (!res.ok) {
|
|
476
|
+
this.error = 'Failed to load audit';
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const data = await res.json();
|
|
480
|
+
const events = Array.isArray(data.events) ? data.events : [];
|
|
481
|
+
this.auditEvents = events.filter((e) => String(e.action || '').startsWith('proxy.'));
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
async mounted() {
|
|
485
|
+
await this.loadAll();
|
|
486
|
+
await this.loadAudit();
|
|
487
|
+
}
|
|
488
|
+
}).mount('#app');
|
|
489
|
+
</script>
|
|
490
|
+
</body>
|
|
491
|
+
</html>
|