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,181 @@
1
+ /**
2
+ * Admin Dashboard Page
3
+ */
4
+
5
+ import { html } from 'hono/html';
6
+ import { BaseLayout } from '../../layouts/base.js';
7
+
8
+ /**
9
+ * Render admin dashboard page
10
+ * @param {Object} props - Page properties
11
+ * @param {Object} props.stats - Dashboard statistics
12
+ * @param {Object} props.user - Current user
13
+ * @param {Object} [props.config] - UI configuration
14
+ * @returns {string} HTML string
15
+ */
16
+ export function AdminDashboardPage(props = {}) {
17
+ const { stats = {}, user = {}, config = {} } = props;
18
+
19
+ const formatNumber = value => Number(value || 0).toLocaleString();
20
+
21
+ const statCards = [
22
+ {
23
+ title: 'Total Users',
24
+ value: formatNumber(stats.totalUsers),
25
+ description: `${formatNumber(stats.activeUsers)} active · ${formatNumber(stats.pendingUsers)} pending`,
26
+ gradient: 'from-sky-500/90 via-blue-500/80 to-indigo-500/80'
27
+ },
28
+ {
29
+ title: 'OAuth2 Clients',
30
+ value: formatNumber(stats.totalClients),
31
+ description: `${formatNumber(stats.activeClients)} active`,
32
+ gradient: 'from-fuchsia-500/90 via-rose-500/80 to-orange-500/80'
33
+ },
34
+ {
35
+ title: 'Active Sessions',
36
+ value: formatNumber(stats.activeSessions),
37
+ description: `${formatNumber(stats.uniqueUsers)} unique users`,
38
+ gradient: 'from-cyan-400/90 via-blue-400/80 to-sky-400/80'
39
+ },
40
+ {
41
+ title: 'Auth Codes',
42
+ value: formatNumber(stats.totalAuthCodes),
43
+ description: `${formatNumber(stats.unusedAuthCodes)} unused`,
44
+ gradient: 'from-emerald-400/90 via-teal-400/80 to-green-400/80'
45
+ }
46
+ ];
47
+
48
+ const quickLinks = [
49
+ { href: '/admin/clients', label: '📱 Manage Clients' },
50
+ { href: '/admin/users', label: '👥 Manage Users' },
51
+ { href: '/admin/sessions', label: '🔐 View Sessions' },
52
+ { href: '/admin/auth-codes', label: '🎫 Auth Codes' }
53
+ ];
54
+
55
+ const recentUsers = Array.isArray(stats.recentUsers) ? stats.recentUsers : [];
56
+
57
+ const content = html`
58
+ <section class="mx-auto w-full max-w-6xl space-y-8 text-slate-100">
59
+ <header class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
60
+ <div>
61
+ <h1 class="text-3xl font-semibold text-white md:text-4xl">Admin Dashboard</h1>
62
+ <p class="mt-1 text-sm text-slate-300">
63
+ Overview of identity activity, clients, and health metrics.
64
+ </p>
65
+ </div>
66
+ <div class="rounded-2xl border border-white/15 bg-white/[0.06] px-4 py-3 text-xs text-slate-300">
67
+ <div class="text-sm font-semibold text-white">${user.email || 'admin@s3db.identity'}</div>
68
+ <div class="mt-1 flex flex-wrap items-center gap-2">
69
+ <span class="rounded-full bg-emerald-500/20 px-3 py-1 text-xs font-semibold text-emerald-200">
70
+ Administrator
71
+ </span>
72
+ ${stats.serverUptime ? html`
73
+ <span class="text-xs text-slate-400">
74
+ Uptime: ${stats.serverUptime}
75
+ </span>
76
+ ` : ''}
77
+ </div>
78
+ </div>
79
+ </header>
80
+
81
+ <div class="grid gap-6 sm:grid-cols-2 xl:grid-cols-4">
82
+ ${statCards.map(card => html`
83
+ <div class="rounded-3xl border border-white/10 bg-gradient-to-br ${card.gradient} p-6 shadow-xl shadow-black/30 backdrop-blur">
84
+ <div class="text-xs uppercase tracking-wide text-white/80">${card.title}</div>
85
+ <div class="mt-3 text-3xl font-semibold text-white">${card.value}</div>
86
+ <div class="mt-2 text-sm text-white/80">${card.description}</div>
87
+ </div>
88
+ `)}
89
+ </div>
90
+
91
+ <div class="rounded-3xl border border-white/10 bg-white/[0.05] p-6 shadow-xl shadow-black/30 backdrop-blur">
92
+ <h2 class="text-lg font-semibold text-white">Quick Actions</h2>
93
+ <div class="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
94
+ ${quickLinks.map(link => html`
95
+ <a
96
+ href="${link.href}"
97
+ class="flex items-center justify-center rounded-2xl border border-white/15 bg-white/[0.06] px-4 py-3 text-sm font-semibold text-white transition hover:-translate-y-0.5 hover:bg-white/[0.12] focus:outline-none focus:ring-2 focus:ring-white/20"
98
+ >
99
+ ${link.label}
100
+ </a>
101
+ `)}
102
+ </div>
103
+ </div>
104
+
105
+ ${recentUsers.length > 0 ? html`
106
+ <div class="rounded-3xl border border-white/10 bg-white/[0.05] p-6 shadow-xl shadow-black/30 backdrop-blur">
107
+ <h2 class="text-lg font-semibold text-white">Recent Users</h2>
108
+ <div class="mt-4 overflow-hidden rounded-2xl border border-white/10">
109
+ <table class="min-w-full divide-y divide-white/10 text-left text-sm text-slate-200">
110
+ <thead class="bg-white/[0.04] text-xs uppercase tracking-wide text-slate-400">
111
+ <tr>
112
+ <th class="px-4 py-3 font-medium">Email</th>
113
+ <th class="px-4 py-3 font-medium">Name</th>
114
+ <th class="px-4 py-3 font-medium">Status</th>
115
+ <th class="px-4 py-3 font-medium">Created</th>
116
+ </tr>
117
+ </thead>
118
+ <tbody class="divide-y divide-white/5">
119
+ ${recentUsers.map(recentUser => {
120
+ const statusClass = recentUser.status === 'active'
121
+ ? 'bg-emerald-500/20 text-emerald-200'
122
+ : recentUser.status === 'suspended'
123
+ ? 'bg-red-500/20 text-red-200'
124
+ : 'bg-amber-500/20 text-amber-200';
125
+
126
+ return html`
127
+ <tr class="hover:bg-white/[0.04]">
128
+ <td class="px-4 py-3">${recentUser.email}</td>
129
+ <td class="px-4 py-3">${recentUser.name}</td>
130
+ <td class="px-4 py-3">
131
+ <span class="rounded-full px-3 py-1 text-xs font-semibold ${statusClass}">
132
+ ${recentUser.status}
133
+ </span>
134
+ </td>
135
+ <td class="px-4 py-3 text-slate-400">
136
+ ${new Date(recentUser.createdAt).toLocaleDateString()}
137
+ </td>
138
+ </tr>
139
+ `;
140
+ })}
141
+ </tbody>
142
+ </table>
143
+ </div>
144
+ </div>
145
+ ` : ''}
146
+
147
+ <div class="rounded-3xl border border-white/10 bg-white/[0.05] p-6 shadow-xl shadow-black/30 backdrop-blur">
148
+ <h2 class="text-lg font-semibold text-white">System Information</h2>
149
+ <dl class="mt-4 divide-y divide-white/10 text-sm text-slate-200">
150
+ <div class="flex flex-col gap-1 py-3 sm:flex-row sm:items-center sm:justify-between">
151
+ <dt class="text-slate-400">Identity Provider</dt>
152
+ <dd class="font-medium text-white">${config.title || 'S3DB Identity'}</dd>
153
+ </div>
154
+ <div class="flex flex-col gap-1 py-3 sm:flex-row sm:items-center sm:justify-between">
155
+ <dt class="text-slate-400">Your Role</dt>
156
+ <dd class="font-medium text-primary">Administrator</dd>
157
+ </div>
158
+ ${stats.serverUptime ? html`
159
+ <div class="flex flex-col gap-1 py-3 sm:flex-row sm:items-center sm:justify-between">
160
+ <dt class="text-slate-400">Server Uptime</dt>
161
+ <dd>${stats.serverUptime}</dd>
162
+ </div>
163
+ ` : ''}
164
+ <div class="flex flex-col gap-1 py-3 sm:flex-row sm:items-center sm:justify-between">
165
+ <dt class="text-slate-400">Database Type</dt>
166
+ <dd>S3DB (S3-based Document Database)</dd>
167
+ </div>
168
+ </dl>
169
+ </div>
170
+ </section>
171
+ `;
172
+
173
+ return BaseLayout({
174
+ title: 'Admin Dashboard',
175
+ content,
176
+ config,
177
+ user
178
+ });
179
+ }
180
+
181
+ export default AdminDashboardPage;
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Admin User Edit Form Page
3
+ */
4
+
5
+ import { html } from 'hono/html';
6
+ import { BaseLayout } from '../../layouts/base.js';
7
+
8
+ /**
9
+ * Render user edit form page
10
+ * @param {Object} props - Page properties
11
+ * @param {Object} props.editUser - User being edited
12
+ * @param {Object} props.user - Current user
13
+ * @param {string} [props.error] - Error message
14
+ * @param {Object} [props.config] - UI configuration
15
+ * @returns {string} HTML string
16
+ */
17
+ export function AdminUserFormPage(props = {}) {
18
+ const { editUser = {}, user = {}, error = null, config = {} } = props;
19
+
20
+ const isCurrentUser = editUser.id === user.id;
21
+
22
+ const inputClasses = [
23
+ 'block w-full rounded-2xl border border-white/10 bg-white/[0.08]',
24
+ 'px-4 py-2.5 text-sm text-white placeholder:text-slate-300/70',
25
+ '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'
26
+ ].join(' ');
27
+
28
+ const checkboxClasses = [
29
+ 'h-4 w-4 rounded border-white/30 bg-slate-900/70 text-primary',
30
+ 'focus:ring-2 focus:ring-primary/40 focus:ring-offset-0 focus:outline-none'
31
+ ].join(' ');
32
+
33
+ const radioClasses = [
34
+ 'h-4 w-4 border-white/30 text-primary focus:ring-2 focus:ring-primary/40 focus:ring-offset-0 focus:outline-none'
35
+ ].join(' ');
36
+
37
+ const primaryButtonClass = [
38
+ 'inline-flex items-center justify-center rounded-2xl bg-gradient-to-r',
39
+ 'from-primary via-primary to-secondary px-5 py-2.5 text-sm font-semibold text-white',
40
+ 'transition hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-white/30'
41
+ ].join(' ');
42
+
43
+ const secondaryButtonClass = [
44
+ 'inline-flex items-center justify-center rounded-2xl border border-white/15 bg-white/[0.06]',
45
+ 'px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.12]',
46
+ 'focus:outline-none focus:ring-2 focus:ring-white/20'
47
+ ].join(' ');
48
+
49
+ const dangerButtonClass = [
50
+ 'inline-flex items-center justify-center rounded-2xl border border-red-400/40 bg-red-500/10',
51
+ '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'
52
+ ].join(' ');
53
+
54
+ const statusOptions = [
55
+ {
56
+ value: 'active',
57
+ title: 'Active',
58
+ description: 'User can log in and access services.'
59
+ },
60
+ {
61
+ value: 'suspended',
62
+ title: 'Suspended',
63
+ description: 'User cannot log in.'
64
+ },
65
+ {
66
+ value: 'pending_verification',
67
+ title: 'Pending Verification',
68
+ description: 'Awaiting email verification.'
69
+ }
70
+ ];
71
+
72
+ const roleOptions = [
73
+ {
74
+ value: 'user',
75
+ title: 'User',
76
+ description: 'Standard access to identity provider services.'
77
+ },
78
+ {
79
+ value: 'admin',
80
+ title: 'Administrator',
81
+ description: 'Full administrative access.'
82
+ }
83
+ ];
84
+
85
+ const content = html`
86
+ <section class="mx-auto w-full max-w-4xl space-y-8 text-slate-100">
87
+ <header>
88
+ <a href="/admin/users" class="text-sm font-semibold text-primary transition hover:text-white">
89
+ ← Back to Users
90
+ </a>
91
+ <h1 class="mt-3 text-3xl font-semibold text-white md:text-4xl">
92
+ Edit User: ${editUser.name}
93
+ </h1>
94
+ <p class="mt-2 text-sm text-slate-300">
95
+ Update profile details, status, and permissions for this user.
96
+ </p>
97
+ </header>
98
+
99
+ <div class="rounded-3xl border border-white/10 bg-white/[0.05] p-8 shadow-xl shadow-black/30 backdrop-blur">
100
+ <form method="POST" action="/admin/users/${editUser.id}/update" class="space-y-6">
101
+ <div class="space-y-2">
102
+ <label for="name" class="text-sm font-semibold text-slate-200">Full Name</label>
103
+ <input
104
+ type="text"
105
+ class="${inputClasses} ${error ? 'border-red-400/60 focus:border-red-400 focus:ring-red-400/40' : ''}"
106
+ id="name"
107
+ name="name"
108
+ value="${editUser.name}"
109
+ required
110
+ autofocus
111
+ placeholder="John Doe"
112
+ />
113
+ <p class="text-xs text-slate-400">User's display name.</p>
114
+ ${error ? html`<p class="text-xs text-red-200">${error}</p>` : ''}
115
+ </div>
116
+
117
+ <div class="space-y-2">
118
+ <label for="email" class="text-sm font-semibold text-slate-200">Email Address</label>
119
+ <input
120
+ type="email"
121
+ class="${inputClasses}"
122
+ id="email"
123
+ name="email"
124
+ value="${editUser.email}"
125
+ required
126
+ placeholder="user@example.com"
127
+ />
128
+ <p class="text-xs text-slate-400">Email address used for login and notifications.</p>
129
+ </div>
130
+
131
+ <div class="space-y-3">
132
+ <span class="text-sm font-semibold text-slate-200">Account Status</span>
133
+ <div class="grid gap-3">
134
+ ${statusOptions.map(option => html`
135
+ <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">
136
+ <input
137
+ type="radio"
138
+ class="${radioClasses} mt-1"
139
+ id="status_${option.value}"
140
+ name="status"
141
+ value="${option.value}"
142
+ ${editUser.status === option.value ? 'checked' : ''}
143
+ ${isCurrentUser ? 'disabled' : ''}
144
+ />
145
+ <span>
146
+ <strong class="text-white">${option.title}</strong><br>
147
+ <span class="text-xs text-slate-400">${option.description}</span>
148
+ </span>
149
+ </label>
150
+ `)}
151
+ </div>
152
+ ${isCurrentUser ? html`
153
+ <p class="text-xs text-amber-300">You cannot change your own status.</p>
154
+ ` : ''}
155
+ </div>
156
+
157
+ <div class="space-y-3">
158
+ <span class="text-sm font-semibold text-slate-200">Role</span>
159
+ <div class="grid gap-3">
160
+ ${roleOptions.map(option => html`
161
+ <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">
162
+ <input
163
+ type="radio"
164
+ class="${radioClasses} mt-1"
165
+ id="role_${option.value}"
166
+ name="role"
167
+ value="${option.value}"
168
+ ${editUser.role === option.value ? 'checked' : ''}
169
+ ${isCurrentUser ? 'disabled' : ''}
170
+ />
171
+ <span>
172
+ <strong class="text-white">${option.title}</strong><br>
173
+ <span class="text-xs text-slate-400">${option.description}</span>
174
+ </span>
175
+ </label>
176
+ `)}
177
+ </div>
178
+ ${isCurrentUser ? html`
179
+ <p class="text-xs text-amber-300">You cannot change your own role.</p>
180
+ ` : ''}
181
+ </div>
182
+
183
+ <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">
184
+ <input
185
+ type="checkbox"
186
+ class="${checkboxClasses} mt-1"
187
+ id="emailVerified"
188
+ name="emailVerified"
189
+ value="1"
190
+ ${editUser.emailVerified ? 'checked' : ''}
191
+ />
192
+ <span>
193
+ <strong class="text-white">Email Verified</strong><br>
194
+ <span class="text-xs text-slate-400">User has confirmed their email address.</span>
195
+ </span>
196
+ </label>
197
+
198
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-start">
199
+ <button type="submit" class="${primaryButtonClass} sm:w-auto" style="box-shadow: 0 18px 45px var(--color-primary-glow);">
200
+ Update User
201
+ </button>
202
+ <a href="/admin/users" class="${secondaryButtonClass}">
203
+ Cancel
204
+ </a>
205
+ </div>
206
+ </form>
207
+ </div>
208
+
209
+ <div class="rounded-3xl border border-white/10 bg-white/[0.05] p-6 shadow-xl shadow-black/30 backdrop-blur">
210
+ <h2 class="text-lg font-semibold text-white">User Information</h2>
211
+ <dl class="mt-4 divide-y divide-white/10 text-sm text-slate-200">
212
+ <div class="flex flex-col gap-1 py-3 sm:flex-row sm:items-center sm:justify-between">
213
+ <dt class="text-slate-400">User ID</dt>
214
+ <dd>
215
+ <code class="rounded-xl border border-white/10 bg-white/[0.08] px-3 py-1 text-xs text-slate-200">
216
+ ${editUser.id}
217
+ </code>
218
+ </dd>
219
+ </div>
220
+ ${editUser.createdAt ? html`
221
+ <div class="flex flex-col gap-1 py-3 sm:flex-row sm:items-center sm:justify-between">
222
+ <dt class="text-slate-400">Joined</dt>
223
+ <dd>${new Date(editUser.createdAt).toLocaleString()}</dd>
224
+ </div>
225
+ ` : ''}
226
+ ${editUser.updatedAt ? html`
227
+ <div class="flex flex-col gap-1 py-3 sm:flex-row sm:items-center sm:justify-between">
228
+ <dt class="text-slate-400">Last Updated</dt>
229
+ <dd>${new Date(editUser.updatedAt).toLocaleString()}</dd>
230
+ </div>
231
+ ` : ''}
232
+ <div class="flex flex-col gap-1 py-3 sm:flex-row sm:items-center sm:justify-between">
233
+ <dt class="text-slate-400">Last Login</dt>
234
+ <dd>${editUser.lastLoginAt ? new Date(editUser.lastLoginAt).toLocaleString() : html`<span class="text-slate-400">Never</span>`}</dd>
235
+ </div>
236
+ </dl>
237
+ </div>
238
+
239
+ ${!isCurrentUser ? html`
240
+ <div class="rounded-3xl border border-red-500/40 bg-red-500/5 shadow-xl shadow-black/30 backdrop-blur">
241
+ <div class="rounded-t-3xl border-b border-red-500/40 bg-red-500/20 px-6 py-4 text-white">
242
+ <h2 class="text-lg font-semibold">Danger Zone</h2>
243
+ </div>
244
+ <div class="space-y-6 px-6 py-6 text-sm text-slate-100">
245
+ <div>
246
+ <h3 class="text-base font-semibold text-white">Send Password Reset Email</h3>
247
+ <p class="mt-2 text-xs text-red-100">
248
+ Send a password reset link to ${editUser.email}.
249
+ </p>
250
+ <form method="POST" action="/admin/users/${editUser.id}/reset-password" onsubmit="return confirm('Send password reset email to ${editUser.email}?')">
251
+ <button type="submit" class="${secondaryButtonClass} mt-3">
252
+ 🔑 Send Password Reset
253
+ </button>
254
+ </form>
255
+ </div>
256
+
257
+ <div class="border-t border-red-500/30 pt-6">
258
+ <h3 class="text-base font-semibold text-white">Delete User Account</h3>
259
+ <p class="mt-2 text-xs text-red-100">
260
+ Permanently delete this user account. This action cannot be undone.
261
+ </p>
262
+ <form method="POST" action="/admin/users/${editUser.id}/delete" onsubmit="return confirm('Are you sure you want to delete ${editUser.name}? This action cannot be undone.')">
263
+ <button type="submit" class="${dangerButtonClass} mt-3">
264
+ 🗑️ Delete User
265
+ </button>
266
+ </form>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ ` : ''}
271
+ </section>
272
+ `;
273
+
274
+ return BaseLayout({
275
+ title: `Edit User: ${editUser.name} - Admin`,
276
+ content,
277
+ config,
278
+ user,
279
+ error: null // Error shown in form
280
+ });
281
+ }
282
+
283
+ export default AdminUserFormPage;