s3db.js 13.4.0 → 13.6.0

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.
Files changed (110) hide show
  1. package/README.md +25 -10
  2. package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +38653 -32291
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +218 -22
  7. package/src/concerns/id.js +90 -6
  8. package/src/concerns/index.js +2 -1
  9. package/src/concerns/password-hashing.js +150 -0
  10. package/src/database.class.js +6 -2
  11. package/src/plugins/api/auth/basic-auth.js +40 -10
  12. package/src/plugins/api/auth/index.js +49 -3
  13. package/src/plugins/api/auth/oauth2-auth.js +171 -0
  14. package/src/plugins/api/auth/oidc-auth.js +789 -0
  15. package/src/plugins/api/auth/oidc-client.js +462 -0
  16. package/src/plugins/api/auth/path-auth-matcher.js +284 -0
  17. package/src/plugins/api/concerns/event-emitter.js +134 -0
  18. package/src/plugins/api/concerns/failban-manager.js +651 -0
  19. package/src/plugins/api/concerns/guards-helpers.js +402 -0
  20. package/src/plugins/api/concerns/metrics-collector.js +346 -0
  21. package/src/plugins/api/index.js +510 -57
  22. package/src/plugins/api/middlewares/failban.js +305 -0
  23. package/src/plugins/api/middlewares/rate-limit.js +301 -0
  24. package/src/plugins/api/middlewares/request-id.js +74 -0
  25. package/src/plugins/api/middlewares/security-headers.js +120 -0
  26. package/src/plugins/api/middlewares/session-tracking.js +194 -0
  27. package/src/plugins/api/routes/auth-routes.js +119 -78
  28. package/src/plugins/api/routes/resource-routes.js +73 -30
  29. package/src/plugins/api/server.js +1139 -45
  30. package/src/plugins/api/utils/custom-routes.js +102 -0
  31. package/src/plugins/api/utils/guards.js +213 -0
  32. package/src/plugins/api/utils/mime-types.js +154 -0
  33. package/src/plugins/api/utils/openapi-generator.js +91 -12
  34. package/src/plugins/api/utils/path-matcher.js +173 -0
  35. package/src/plugins/api/utils/static-filesystem.js +262 -0
  36. package/src/plugins/api/utils/static-s3.js +231 -0
  37. package/src/plugins/api/utils/template-engine.js +188 -0
  38. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  39. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  40. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  41. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  42. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  43. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  44. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  45. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  46. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  47. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  48. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  49. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  50. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  51. package/src/plugins/cloud-inventory/index.js +20 -0
  52. package/src/plugins/cloud-inventory/registry.js +146 -0
  53. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  54. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  55. package/src/plugins/concerns/plugin-dependencies.js +62 -2
  56. package/src/plugins/eventual-consistency/analytics.js +1 -0
  57. package/src/plugins/eventual-consistency/consolidation.js +2 -2
  58. package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
  59. package/src/plugins/eventual-consistency/install.js +2 -2
  60. package/src/plugins/identity/README.md +335 -0
  61. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  62. package/src/plugins/identity/concerns/password.js +138 -0
  63. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  64. package/src/plugins/identity/concerns/token-generator.js +172 -0
  65. package/src/plugins/identity/email-service.js +422 -0
  66. package/src/plugins/identity/index.js +1052 -0
  67. package/src/plugins/identity/oauth2-server.js +1033 -0
  68. package/src/plugins/identity/oidc-discovery.js +285 -0
  69. package/src/plugins/identity/rsa-keys.js +323 -0
  70. package/src/plugins/identity/server.js +500 -0
  71. package/src/plugins/identity/session-manager.js +453 -0
  72. package/src/plugins/identity/ui/layouts/base.js +251 -0
  73. package/src/plugins/identity/ui/middleware.js +135 -0
  74. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  75. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  76. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  77. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  78. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  79. package/src/plugins/identity/ui/pages/consent.js +262 -0
  80. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  81. package/src/plugins/identity/ui/pages/login.js +144 -0
  82. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  83. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  84. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  85. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  86. package/src/plugins/identity/ui/pages/profile.js +361 -0
  87. package/src/plugins/identity/ui/pages/register.js +226 -0
  88. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  89. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  90. package/src/plugins/identity/ui/routes.js +2541 -0
  91. package/src/plugins/identity/ui/styles/main.css +465 -0
  92. package/src/plugins/index.js +4 -1
  93. package/src/plugins/ml/base-model.class.js +65 -16
  94. package/src/plugins/ml/classification-model.class.js +1 -1
  95. package/src/plugins/ml/timeseries-model.class.js +3 -1
  96. package/src/plugins/ml.plugin.js +584 -31
  97. package/src/plugins/shared/error-handler.js +147 -0
  98. package/src/plugins/shared/index.js +9 -0
  99. package/src/plugins/shared/middlewares/compression.js +117 -0
  100. package/src/plugins/shared/middlewares/cors.js +49 -0
  101. package/src/plugins/shared/middlewares/index.js +11 -0
  102. package/src/plugins/shared/middlewares/logging.js +54 -0
  103. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  104. package/src/plugins/shared/middlewares/security.js +158 -0
  105. package/src/plugins/shared/response-formatter.js +264 -0
  106. package/src/plugins/state-machine.plugin.js +57 -2
  107. package/src/resource.class.js +140 -12
  108. package/src/schema.class.js +30 -1
  109. package/src/validator.class.js +57 -6
  110. package/dist/s3db.cjs.js.map +0 -1
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Identity Provider Middleware
3
+ * Session validation and authentication middleware
4
+ */
5
+
6
+ /**
7
+ * Session authentication middleware
8
+ * Validates session and attaches user data to context
9
+ * @param {Object} sessionManager - SessionManager instance
10
+ * @param {Object} options - Middleware options
11
+ * @param {boolean} options.required - Require authentication (redirect if not logged in)
12
+ * @param {boolean} options.requireAdmin - Require admin role
13
+ * @param {string} options.redirectTo - Redirect URL if not authenticated
14
+ * @returns {Function} Hono middleware function
15
+ */
16
+ export function sessionAuth(sessionManager, options = {}) {
17
+ const {
18
+ required = false,
19
+ requireAdmin = false,
20
+ redirectTo = '/login'
21
+ } = options;
22
+
23
+ return async (c, next) => {
24
+ const sessionId = sessionManager.getSessionIdFromRequest(c.req);
25
+
26
+ let user = null;
27
+ let session = null;
28
+
29
+ if (sessionId) {
30
+ const { valid, session: validSession, reason } = await sessionManager.validateSession(sessionId);
31
+
32
+ if (valid) {
33
+ session = validSession;
34
+ user = validSession.metadata || null;
35
+ } else {
36
+ // Clear invalid session cookie
37
+ sessionManager.clearSessionCookie(c);
38
+
39
+ if (required) {
40
+ const currentUrl = c.req.url;
41
+ return c.redirect(`${redirectTo}?redirect=${encodeURIComponent(currentUrl)}&error=${encodeURIComponent('Your session has expired. Please log in again.')}`);
42
+ }
43
+ }
44
+ }
45
+
46
+ // Check if authentication is required
47
+ if (required && !user) {
48
+ const currentUrl = c.req.url;
49
+ return c.redirect(`${redirectTo}?redirect=${encodeURIComponent(currentUrl)}`);
50
+ }
51
+
52
+ // Check if admin is required
53
+ if (requireAdmin && (!user || !user.isAdmin)) {
54
+ return c.html('<h1>403 Forbidden</h1><p>You do not have permission to access this page.</p>', 403);
55
+ }
56
+
57
+ // Attach user and session to context
58
+ c.set('user', user);
59
+ c.set('session', session);
60
+ c.set('isAuthenticated', !!user);
61
+ c.set('isAdmin', user?.isAdmin || false);
62
+
63
+ await next();
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Admin-only middleware (shorthand)
69
+ * @param {Object} sessionManager - SessionManager instance
70
+ * @returns {Function} Hono middleware function
71
+ */
72
+ export function adminOnly(sessionManager) {
73
+ return sessionAuth(sessionManager, {
74
+ required: true,
75
+ requireAdmin: true
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Optional authentication middleware
81
+ * Attaches user if logged in, but doesn't require it
82
+ * @param {Object} sessionManager - SessionManager instance
83
+ * @returns {Function} Hono middleware function
84
+ */
85
+ export function optionalAuth(sessionManager) {
86
+ return sessionAuth(sessionManager, {
87
+ required: false,
88
+ requireAdmin: false
89
+ });
90
+ }
91
+
92
+ /**
93
+ * CSRF protection middleware
94
+ * Validates CSRF token for POST/PUT/PATCH/DELETE requests
95
+ * @param {Object} options - CSRF options
96
+ * @param {string[]} options.excludePaths - Paths to exclude from CSRF check
97
+ * @returns {Function} Hono middleware function
98
+ */
99
+ export function csrfProtection(options = {}) {
100
+ const {
101
+ excludePaths = []
102
+ } = options;
103
+
104
+ return async (c, next) => {
105
+ const method = c.req.method;
106
+ const path = c.req.path;
107
+
108
+ // Skip CSRF check for safe methods
109
+ if (['GET', 'HEAD', 'OPTIONS'].includes(method)) {
110
+ await next();
111
+ return;
112
+ }
113
+
114
+ // Skip CSRF check for excluded paths
115
+ if (excludePaths.some(p => path.startsWith(p))) {
116
+ await next();
117
+ return;
118
+ }
119
+
120
+ // For now, skip CSRF validation - will be implemented in polish phase
121
+ // TODO: Implement proper CSRF token generation and validation
122
+ // - Generate token on GET requests, store in session
123
+ // - Include token in forms as hidden field
124
+ // - Validate token on POST/PUT/PATCH/DELETE
125
+
126
+ await next();
127
+ };
128
+ }
129
+
130
+ export default {
131
+ sessionAuth,
132
+ adminOnly,
133
+ optionalAuth,
134
+ csrfProtection
135
+ };
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Admin OAuth2 Client Form Page (Create/Edit)
3
+ */
4
+
5
+ import { html } from 'hono/html';
6
+ import { BaseLayout } from '../../layouts/base.js';
7
+
8
+ /**
9
+ * Render OAuth2 client form page
10
+ * @param {Object} props - Page properties
11
+ * @param {Object} [props.client] - Client data (for edit mode)
12
+ * @param {Object} props.user - Current user
13
+ * @param {string} [props.error] - Error message
14
+ * @param {Array} [props.availableScopes] - Available OAuth2 scopes
15
+ * @param {Array} [props.availableGrantTypes] - Available grant types
16
+ * @param {Object} [props.config] - UI configuration
17
+ * @returns {string} HTML string
18
+ */
19
+ export function AdminClientFormPage(props = {}) {
20
+ const {
21
+ client = null,
22
+ user = {},
23
+ error = null,
24
+ availableScopes = [],
25
+ availableGrantTypes = [],
26
+ config = {}
27
+ } = props;
28
+
29
+ const isEditMode = !!client;
30
+ const clientData = client || {
31
+ name: '',
32
+ redirectUris: [''],
33
+ grantTypes: ['authorization_code', 'refresh_token'],
34
+ allowedScopes: ['openid', 'profile', 'email'],
35
+ active: true
36
+ };
37
+
38
+ const inputClasses = [
39
+ 'block w-full rounded-2xl border border-white/10 bg-white/[0.08]',
40
+ 'px-4 py-2.5 text-sm text-white placeholder:text-slate-300/70',
41
+ 'shadow-[0_1px_0_rgba(255,255,255,0.05)] transition focus:border-white/40 focus:outline-none focus:ring-2 focus:ring-white/30'
42
+ ].join(' ');
43
+
44
+ const checkboxClasses = [
45
+ 'h-4 w-4 rounded border-white/30 bg-slate-900/70 text-primary',
46
+ 'focus:ring-2 focus:ring-primary/40 focus:ring-offset-0 focus:outline-none'
47
+ ].join(' ');
48
+
49
+ const primaryButtonClass = [
50
+ 'inline-flex items-center justify-center rounded-2xl bg-gradient-to-r',
51
+ 'from-primary via-primary to-secondary px-5 py-2.5 text-sm font-semibold text-white',
52
+ 'transition hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-white/30'
53
+ ].join(' ');
54
+
55
+ const secondaryButtonClass = [
56
+ 'inline-flex items-center justify-center rounded-2xl border border-white/15 bg-white/[0.06]',
57
+ 'px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.12]',
58
+ 'focus:outline-none focus:ring-2 focus:ring-white/20'
59
+ ].join(' ');
60
+
61
+ const dangerButtonClass = [
62
+ 'inline-flex items-center justify-center rounded-2xl border border-red-400/40 bg-red-500/10',
63
+ 'px-4 py-2 text-sm font-semibold text-red-100 transition hover:bg-red-500/15 focus:outline-none focus:ring-2 focus:ring-red-400/40'
64
+ ].join(' ');
65
+
66
+ const content = html`
67
+ <section class="mx-auto w-full max-w-4xl space-y-8 text-slate-100">
68
+ <header>
69
+ <a href="/admin/clients" class="text-sm font-semibold text-primary transition hover:text-white">
70
+ ← Back to Clients
71
+ </a>
72
+ <h1 class="mt-3 text-3xl font-semibold text-white md:text-4xl">
73
+ ${isEditMode ? 'Edit' : 'Create'} OAuth2 Client
74
+ </h1>
75
+ <p class="mt-2 text-sm text-slate-300">
76
+ Configure redirect URIs, grant types, and scopes available for this client.
77
+ </p>
78
+ </header>
79
+
80
+ <div class="rounded-3xl border border-white/10 bg-white/[0.05] p-8 shadow-xl shadow-black/30 backdrop-blur">
81
+ <form method="POST" action="${isEditMode ? `/admin/clients/${client.id}/update` : '/admin/clients/create'}" class="space-y-6">
82
+ <div class="space-y-2">
83
+ <label for="name" class="text-sm font-semibold text-slate-200">Client Name</label>
84
+ <input
85
+ type="text"
86
+ class="${inputClasses} ${error ? 'border-red-400/60 focus:border-red-400 focus:ring-red-400/40' : ''}"
87
+ id="name"
88
+ name="name"
89
+ value="${clientData.name}"
90
+ required
91
+ autofocus
92
+ placeholder="My Application"
93
+ />
94
+ <p class="text-xs text-slate-400">A friendly name for this OAuth2 client</p>
95
+ ${error ? html`<p class="text-xs text-red-200">${error}</p>` : ''}
96
+ </div>
97
+
98
+ <div class="space-y-3">
99
+ <label class="text-sm font-semibold text-slate-200">Redirect URIs</label>
100
+ <div id="redirect-uris-container" class="space-y-2">
101
+ ${(Array.isArray(clientData.redirectUris) ? clientData.redirectUris : ['']).map((uri, index) => html`
102
+ <div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:gap-3">
103
+ <input
104
+ type="url"
105
+ class="${inputClasses}"
106
+ name="redirectUris[]"
107
+ value="${uri}"
108
+ required
109
+ placeholder="https://example.com/callback"
110
+ />
111
+ ${index > 0 ? html`
112
+ <button type="button" class="${dangerButtonClass} shrink-0" onclick="this.parentElement.remove()">
113
+
114
+ </button>
115
+ ` : ''}
116
+ </div>
117
+ `)}
118
+ </div>
119
+ <button type="button" class="${secondaryButtonClass}" onclick="addRedirectUri()">
120
+ + Add Another URI
121
+ </button>
122
+ <p class="text-xs text-slate-400">
123
+ Where users will be redirected after authorization.
124
+ </p>
125
+ </div>
126
+
127
+ <div class="space-y-2">
128
+ <label class="text-sm font-semibold text-slate-200">Grant Types</label>
129
+ <div class="grid gap-2 sm:grid-cols-2">
130
+ ${(availableGrantTypes.length > 0 ? availableGrantTypes : ['authorization_code', 'refresh_token', 'client_credentials']).map(type => {
131
+ const isChecked = Array.isArray(clientData.grantTypes) && clientData.grantTypes.includes(type);
132
+ return html`
133
+ <label class="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-200">
134
+ <input
135
+ type="checkbox"
136
+ class="${checkboxClasses}"
137
+ id="grant_${type}"
138
+ name="grantTypes[]"
139
+ value="${type}"
140
+ ${isChecked ? 'checked' : ''}
141
+ />
142
+ <span><code>${type}</code></span>
143
+ </label>
144
+ `;
145
+ })}
146
+ </div>
147
+ <p class="text-xs text-slate-400">OAuth2 grant types this client can use.</p>
148
+ </div>
149
+
150
+ <div class="space-y-2">
151
+ <label class="text-sm font-semibold text-slate-200">Allowed Scopes</label>
152
+ <div class="grid gap-2 sm:grid-cols-2">
153
+ ${(availableScopes.length > 0 ? availableScopes : ['openid', 'profile', 'email', 'offline_access']).map(scope => {
154
+ const isChecked = Array.isArray(clientData.allowedScopes) && clientData.allowedScopes.includes(scope);
155
+ return html`
156
+ <label class="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-200">
157
+ <input
158
+ type="checkbox"
159
+ class="${checkboxClasses}"
160
+ id="scope_${scope}"
161
+ name="allowedScopes[]"
162
+ value="${scope}"
163
+ ${isChecked ? 'checked' : ''}
164
+ />
165
+ <span><code>${scope}</code></span>
166
+ </label>
167
+ `;
168
+ })}
169
+ </div>
170
+ <p class="text-xs text-slate-400">Scopes this client is allowed to request.</p>
171
+ </div>
172
+
173
+ <label class="flex items-start gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-200">
174
+ <input
175
+ type="checkbox"
176
+ class="${checkboxClasses} mt-1"
177
+ id="active"
178
+ name="active"
179
+ value="1"
180
+ ${clientData.active !== false ? 'checked' : ''}
181
+ />
182
+ <span>
183
+ <strong class="text-white">Active</strong>
184
+ <br>
185
+ Client can authenticate and receive tokens.
186
+ </span>
187
+ </label>
188
+
189
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-start">
190
+ <button type="submit" class="${primaryButtonClass} sm:w-auto" style="box-shadow: 0 18px 45px var(--color-primary-glow);">
191
+ ${isEditMode ? 'Update Client' : 'Create Client'}
192
+ </button>
193
+ <a href="/admin/clients" class="${secondaryButtonClass}">
194
+ Cancel
195
+ </a>
196
+ </div>
197
+ </form>
198
+ </div>
199
+
200
+ ${isEditMode ? html`
201
+ <div class="rounded-3xl border border-white/15 bg-white/[0.06] px-6 py-4 text-sm text-slate-200 shadow-inner shadow-black/20">
202
+ <strong class="text-white">Note:</strong>
203
+ The client secret cannot be displayed again after creation. If you need a new secret, use the "Rotate Secret"
204
+ action on the clients list page.
205
+ </div>
206
+ ` : ''}
207
+ </section>
208
+
209
+ <script>
210
+ const redirectInputClasses = ${JSON.stringify(inputClasses)};
211
+ const dangerButtonClasses = ${JSON.stringify(dangerButtonClass)};
212
+
213
+ function addRedirectUri() {
214
+ const container = document.getElementById('redirect-uris-container');
215
+ const wrapper = document.createElement('div');
216
+ wrapper.className = 'flex flex-col gap-2 sm:flex-row sm:items-start sm:gap-3';
217
+
218
+ const input = document.createElement('input');
219
+ input.type = 'url';
220
+ input.name = 'redirectUris[]';
221
+ input.required = true;
222
+ input.placeholder = 'https://example.com/callback';
223
+ input.className = redirectInputClasses;
224
+
225
+ const button = document.createElement('button');
226
+ button.type = 'button';
227
+ button.className = dangerButtonClasses + ' shrink-0';
228
+ button.textContent = '✕';
229
+ button.addEventListener('click', () => wrapper.remove());
230
+
231
+ wrapper.appendChild(input);
232
+ wrapper.appendChild(button);
233
+ container.appendChild(wrapper);
234
+ }
235
+ </script>
236
+ `;
237
+
238
+ return BaseLayout({
239
+ title: `${isEditMode ? 'Edit' : 'Create'} OAuth2 Client - Admin`,
240
+ content,
241
+ config,
242
+ user,
243
+ error: null // Error shown in form
244
+ });
245
+ }
246
+
247
+ export default AdminClientFormPage;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Admin OAuth2 Clients Management Page
3
+ */
4
+
5
+ import { html } from 'hono/html';
6
+ import { BaseLayout } from '../../layouts/base.js';
7
+
8
+ /**
9
+ * Render OAuth2 clients list page
10
+ * @param {Object} props - Page properties
11
+ * @param {Array} props.clients - List of OAuth2 clients
12
+ * @param {Object} props.user - Current user
13
+ * @param {string} [props.error] - Error message
14
+ * @param {string} [props.success] - Success message
15
+ * @param {Object} [props.config] - UI configuration
16
+ * @returns {string} HTML string
17
+ */
18
+ export function AdminClientsPage(props = {}) {
19
+ const { clients = [], user = {}, error = null, success = null, config = {} } = props;
20
+
21
+ const primaryButtonClass = [
22
+ 'inline-flex items-center justify-center rounded-2xl bg-gradient-to-r',
23
+ 'from-primary via-primary to-secondary px-4 py-2.5 text-sm font-semibold text-white',
24
+ 'transition hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-white/30'
25
+ ].join(' ');
26
+
27
+ const secondaryButtonClass = [
28
+ 'inline-flex items-center justify-center rounded-2xl border border-white/15 bg-white/[0.06]',
29
+ 'px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.12]',
30
+ 'focus:outline-none focus:ring-2 focus:ring-white/20'
31
+ ].join(' ');
32
+
33
+ const dangerButtonClass = [
34
+ 'inline-flex items-center justify-center rounded-2xl border border-red-400/40 bg-red-500/10',
35
+ 'px-4 py-2.5 text-sm font-semibold text-red-100 transition hover:bg-red-500/15 focus:outline-none focus:ring-2 focus:ring-red-400/40'
36
+ ].join(' ');
37
+
38
+ const successButtonClass = [
39
+ 'inline-flex items-center justify-center rounded-2xl border border-emerald-400/40 bg-emerald-500/10',
40
+ 'px-4 py-2.5 text-sm font-semibold text-emerald-100 transition hover:bg-emerald-500/15 focus:outline-none focus:ring-2 focus:ring-emerald-400/40'
41
+ ].join(' ');
42
+
43
+ const codeChipClass = 'rounded-xl border border-white/10 bg-white/[0.08] px-3 py-1 text-xs text-slate-200';
44
+ const badgeClass = 'rounded-full bg-primary/20 px-3 py-1 text-xs font-semibold text-primary';
45
+
46
+ const content = html`
47
+ <section class="mx-auto w-full max-w-6xl space-y-8 text-slate-100">
48
+ <header class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
49
+ <div>
50
+ <h1 class="text-3xl font-semibold text-white md:text-4xl">OAuth2 Clients</h1>
51
+ <p class="mt-1 text-sm text-slate-300">
52
+ Manage client credentials, redirect URIs, and allowed scopes.
53
+ </p>
54
+ </div>
55
+ <a href="/admin/clients/new" class="${primaryButtonClass}">
56
+ + New Client
57
+ </a>
58
+ </header>
59
+
60
+ ${clients.length === 0 ? html`
61
+ <div class="rounded-3xl border border-white/10 bg-white/[0.05] p-10 text-center shadow-xl shadow-black/30 backdrop-blur">
62
+ <p class="text-sm text-slate-300">
63
+ No OAuth2 clients registered yet. Create your first client to start integrating applications.
64
+ </p>
65
+ <a href="/admin/clients/new" class="${primaryButtonClass} mt-6 inline-flex" style="box-shadow: 0 18px 45px var(--color-primary-glow);">
66
+ Create Your First Client
67
+ </a>
68
+ </div>
69
+ ` : html`
70
+ <div class="grid gap-6">
71
+ ${clients.map(client => {
72
+ const grantTypes = Array.isArray(client.grantTypes) ? client.grantTypes : [];
73
+ const allowedScopes = Array.isArray(client.allowedScopes) ? client.allowedScopes : [];
74
+ const redirectUris = Array.isArray(client.redirectUris) ? client.redirectUris : [];
75
+
76
+ return html`
77
+ <article class="rounded-3xl border border-white/10 bg-white/[0.05] p-6 shadow-xl shadow-black/30 backdrop-blur">
78
+ <div class="flex flex-col gap-4 border-b border-white/10 pb-4 sm:flex-row sm:items-center sm:justify-between">
79
+ <div class="flex flex-wrap items-center gap-3">
80
+ <h2 class="text-lg font-semibold text-white">${client.name}</h2>
81
+ ${client.active ? '' : html`
82
+ <span class="rounded-full bg-red-500/20 px-3 py-1 text-xs font-semibold text-red-200">
83
+ Inactive
84
+ </span>
85
+ `}
86
+ </div>
87
+ <div class="flex flex-wrap items-center gap-2">
88
+ <a href="/admin/clients/${client.id}/edit" class="${secondaryButtonClass}">
89
+ Edit
90
+ </a>
91
+ <form method="POST" action="/admin/clients/${client.id}/delete" class="inline-flex" onsubmit="return confirm('Are you sure you want to delete this client? This action cannot be undone.')">
92
+ <button type="submit" class="${dangerButtonClass}">
93
+ Delete
94
+ </button>
95
+ </form>
96
+ </div>
97
+ </div>
98
+
99
+ <div class="mt-4 space-y-6 text-sm text-slate-200">
100
+ <div>
101
+ <div class="text-xs uppercase tracking-wide text-slate-400">Client ID</div>
102
+ <code class="${codeChipClass} mt-2 block">${client.clientId}</code>
103
+ </div>
104
+
105
+ ${redirectUris.length > 0 ? html`
106
+ <div>
107
+ <div class="text-xs uppercase tracking-wide text-slate-400">
108
+ Redirect URIs (${redirectUris.length})
109
+ </div>
110
+ <div class="mt-2 flex flex-wrap gap-2">
111
+ ${redirectUris.map(uri => html`<code class="${codeChipClass}">${uri}</code>`)}
112
+ </div>
113
+ </div>
114
+ ` : ''}
115
+
116
+ ${grantTypes.length > 0 ? html`
117
+ <div>
118
+ <div class="text-xs uppercase tracking-wide text-slate-400">Grant Types</div>
119
+ <div class="mt-2 flex flex-wrap gap-2">
120
+ ${grantTypes.map(type => html`
121
+ <span class="${badgeClass}">
122
+ ${type}
123
+ </span>
124
+ `)}
125
+ </div>
126
+ </div>
127
+ ` : ''}
128
+
129
+ ${allowedScopes.length > 0 ? html`
130
+ <div>
131
+ <div class="text-xs uppercase tracking-wide text-slate-400">Allowed Scopes</div>
132
+ <div class="mt-2 flex flex-wrap gap-2">
133
+ ${allowedScopes.map(scope => html`
134
+ <span class="rounded-full bg-emerald-500/20 px-3 py-1 text-xs font-semibold text-emerald-200">
135
+ ${scope}
136
+ </span>
137
+ `)}
138
+ </div>
139
+ </div>
140
+ ` : ''}
141
+
142
+ ${client.createdAt ? html`
143
+ <div class="text-xs text-slate-400">
144
+ Created ${new Date(client.createdAt).toLocaleString()}
145
+ </div>
146
+ ` : ''}
147
+ </div>
148
+
149
+ <div class="mt-6 flex flex-wrap gap-3 border-t border-white/10 pt-4">
150
+ <form method="POST" action="/admin/clients/${client.id}/rotate-secret" class="inline-flex">
151
+ <button type="submit" class="${secondaryButtonClass}">
152
+ 🔄 Rotate Secret
153
+ </button>
154
+ </form>
155
+ <form method="POST" action="/admin/clients/${client.id}/toggle-active" class="inline-flex">
156
+ <button type="submit" class="${client.active ? dangerButtonClass : successButtonClass}">
157
+ ${client.active ? '🔴 Deactivate' : '🟢 Activate'}
158
+ </button>
159
+ </form>
160
+ </div>
161
+ </article>
162
+ `;
163
+ })}
164
+ </div>
165
+ `}
166
+ </section>
167
+ `;
168
+
169
+ return BaseLayout({
170
+ title: 'OAuth2 Clients - Admin',
171
+ content,
172
+ config,
173
+ user,
174
+ error,
175
+ success
176
+ });
177
+ }
178
+
179
+ export default AdminClientsPage;