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,225 @@
1
+ /**
2
+ * OAuth Error Page
3
+ * Shows OAuth2/OIDC error messages with proper formatting
4
+ */
5
+
6
+ import { html } from 'hono/html';
7
+ import { BaseLayout } from '../layouts/base.js';
8
+
9
+ /**
10
+ * Render OAuth error page
11
+ * @param {Object} props - Page properties
12
+ * @param {string} props.error - Error code (e.g., 'invalid_request')
13
+ * @param {string} [props.errorDescription] - Human-readable error description
14
+ * @param {string} [props.errorUri] - Link to error documentation
15
+ * @param {Object} [props.config] - UI configuration
16
+ * @returns {string} HTML string
17
+ */
18
+ export function OAuthErrorPage(props = {}) {
19
+ const {
20
+ error = 'server_error',
21
+ errorDescription = 'An error occurred during OAuth authorization.',
22
+ errorUri = null,
23
+ config = {}
24
+ } = props;
25
+
26
+ // Map error codes to friendly messages and icons
27
+ const errorInfo = {
28
+ invalid_request: {
29
+ icon: '⚠️',
30
+ title: 'Invalid Request',
31
+ color: 'amber'
32
+ },
33
+ unauthorized_client: {
34
+ icon: '🚫',
35
+ title: 'Unauthorized Client',
36
+ color: 'red'
37
+ },
38
+ access_denied: {
39
+ icon: '🔒',
40
+ title: 'Access Denied',
41
+ color: 'red'
42
+ },
43
+ unsupported_response_type: {
44
+ icon: '❌',
45
+ title: 'Unsupported Response Type',
46
+ color: 'orange'
47
+ },
48
+ invalid_scope: {
49
+ icon: '⛔',
50
+ title: 'Invalid Scope',
51
+ color: 'red'
52
+ },
53
+ server_error: {
54
+ icon: '💥',
55
+ title: 'Server Error',
56
+ color: 'red'
57
+ },
58
+ temporarily_unavailable: {
59
+ icon: '⏸️',
60
+ title: 'Temporarily Unavailable',
61
+ color: 'yellow'
62
+ },
63
+ invalid_client: {
64
+ icon: '🔑',
65
+ title: 'Invalid Client',
66
+ color: 'red'
67
+ },
68
+ invalid_grant: {
69
+ icon: '⚠️',
70
+ title: 'Invalid Grant',
71
+ color: 'amber'
72
+ }
73
+ };
74
+
75
+ const info = errorInfo[error] || errorInfo.server_error;
76
+
77
+ const content = html`
78
+ <div class="mx-auto w-full max-w-2xl px-4 py-12">
79
+ <div class="rounded-3xl border border-slate-700/50 bg-slate-900/50 p-8 shadow-2xl backdrop-blur-xl md:p-12">
80
+ <!-- Error Icon & Title -->
81
+ <div class="mb-8 text-center">
82
+ <div class="mb-4 inline-flex h-20 w-20 items-center justify-center rounded-2xl bg-red-500/20 text-4xl shadow-lg shadow-red-500/30">
83
+ ${info.icon}
84
+ </div>
85
+ <h1 class="mb-2 text-3xl font-bold text-white">
86
+ ${info.title}
87
+ </h1>
88
+ <p class="text-lg text-slate-400">
89
+ OAuth Authorization Error
90
+ </p>
91
+ </div>
92
+
93
+ <!-- Error Details -->
94
+ <div class="mb-8 space-y-4">
95
+ <!-- Error Code -->
96
+ <div class="rounded-2xl border border-slate-700/30 bg-slate-800/30 p-6">
97
+ <h2 class="mb-2 text-sm font-semibold uppercase tracking-wide text-slate-400">
98
+ Error Code
99
+ </h2>
100
+ <code class="block rounded-lg bg-slate-900/50 px-4 py-3 font-mono text-red-400">
101
+ ${error}
102
+ </code>
103
+ </div>
104
+
105
+ <!-- Error Description -->
106
+ ${errorDescription ? html`
107
+ <div class="rounded-2xl border border-slate-700/30 bg-slate-800/30 p-6">
108
+ <h2 class="mb-2 text-sm font-semibold uppercase tracking-wide text-slate-400">
109
+ Description
110
+ </h2>
111
+ <p class="text-slate-200">
112
+ ${errorDescription}
113
+ </p>
114
+ </div>
115
+ ` : ''}
116
+
117
+ <!-- Common Causes -->
118
+ <div class="rounded-2xl border border-blue-500/30 bg-blue-500/10 p-6">
119
+ <h2 class="mb-3 text-lg font-semibold text-blue-200">
120
+ Common Causes
121
+ </h2>
122
+ <ul class="space-y-2 text-sm text-blue-300/80">
123
+ ${error === 'invalid_request' ? html`
124
+ <li class="flex items-start gap-2">
125
+ <span class="mt-0.5 flex-shrink-0">•</span>
126
+ <span>Missing required parameters (client_id, redirect_uri, etc.)</span>
127
+ </li>
128
+ <li class="flex items-start gap-2">
129
+ <span class="mt-0.5 flex-shrink-0">•</span>
130
+ <span>Malformed request parameters</span>
131
+ </li>
132
+ ` : ''}
133
+ ${error === 'unauthorized_client' || error === 'invalid_client' ? html`
134
+ <li class="flex items-start gap-2">
135
+ <span class="mt-0.5 flex-shrink-0">•</span>
136
+ <span>Client ID not found or inactive</span>
137
+ </li>
138
+ <li class="flex items-start gap-2">
139
+ <span class="mt-0.5 flex-shrink-0">•</span>
140
+ <span>Invalid client credentials</span>
141
+ </li>
142
+ ` : ''}
143
+ ${error === 'invalid_scope' ? html`
144
+ <li class="flex items-start gap-2">
145
+ <span class="mt-0.5 flex-shrink-0">•</span>
146
+ <span>Requested scopes not allowed for this client</span>
147
+ </li>
148
+ <li class="flex items-start gap-2">
149
+ <span class="mt-0.5 flex-shrink-0">•</span>
150
+ <span>Unknown or unsupported scope requested</span>
151
+ </li>
152
+ ` : ''}
153
+ ${error === 'access_denied' ? html`
154
+ <li class="flex items-start gap-2">
155
+ <span class="mt-0.5 flex-shrink-0">•</span>
156
+ <span>User denied authorization request</span>
157
+ </li>
158
+ <li class="flex items-start gap-2">
159
+ <span class="mt-0.5 flex-shrink-0">•</span>
160
+ <span>Insufficient permissions for requested scopes</span>
161
+ </li>
162
+ ` : ''}
163
+ ${error === 'server_error' ? html`
164
+ <li class="flex items-start gap-2">
165
+ <span class="mt-0.5 flex-shrink-0">•</span>
166
+ <span>Internal server error occurred</span>
167
+ </li>
168
+ <li class="flex items-start gap-2">
169
+ <span class="mt-0.5 flex-shrink-0">•</span>
170
+ <span>Temporary service disruption</span>
171
+ </li>
172
+ ` : ''}
173
+ </ul>
174
+ </div>
175
+ </div>
176
+
177
+ <!-- Actions -->
178
+ <div class="space-y-3">
179
+ ${errorUri ? html`
180
+ <a
181
+ href="${errorUri}"
182
+ target="_blank"
183
+ rel="noopener noreferrer"
184
+ class="flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-blue-600 to-purple-600 px-6 py-3.5 font-semibold text-white shadow-lg shadow-blue-500/30 transition-all hover:shadow-xl hover:shadow-blue-500/40"
185
+ >
186
+ 📖 View Documentation
187
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
188
+ <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" />
189
+ </svg>
190
+ </a>
191
+ ` : ''}
192
+
193
+ <a
194
+ href="/login"
195
+ class="flex w-full items-center justify-center rounded-xl border border-slate-700/50 bg-slate-800/30 px-6 py-3.5 font-medium text-slate-300 transition-all hover:border-slate-600/50 hover:bg-slate-800/50"
196
+ >
197
+ ← Back to Login
198
+ </a>
199
+ </div>
200
+
201
+ <!-- Help Section -->
202
+ ${config.supportEmail ? html`
203
+ <div class="mt-8 rounded-xl border border-slate-700/30 bg-slate-800/20 px-6 py-4 text-center">
204
+ <p class="text-sm text-slate-400">
205
+ Need help?
206
+ <a href="mailto:${config.supportEmail}" class="font-medium text-blue-400 hover:text-blue-300">
207
+ Contact Support
208
+ </a>
209
+ </p>
210
+ </div>
211
+ ` : ''}
212
+ </div>
213
+ </div>
214
+ `;
215
+
216
+ return BaseLayout({
217
+ title: `OAuth Error: ${info.title}`,
218
+ content,
219
+ config,
220
+ error: null,
221
+ success: null
222
+ });
223
+ }
224
+
225
+ export default OAuthErrorPage;
@@ -0,0 +1,361 @@
1
+ /**
2
+ * User Profile Page
3
+ */
4
+
5
+ import { html } from 'hono/html';
6
+ import { BaseLayout } from '../layouts/base.js';
7
+
8
+ /**
9
+ * Render user profile page
10
+ * @param {Object} props - Page properties
11
+ * @param {Object} props.user - User data
12
+ * @param {Array} [props.sessions] - Active sessions
13
+ * @param {string} [props.error] - Error message
14
+ * @param {string} [props.success] - Success message
15
+ * @param {Object} [props.passwordPolicy] - Password policy configuration
16
+ * @param {Object} [props.config] - UI configuration
17
+ * @returns {string} HTML string
18
+ */
19
+ export function ProfilePage(props = {}) {
20
+ const { user = {}, sessions = [], error = null, success = null, passwordPolicy = {}, config = {} } = props;
21
+
22
+ // Extract password policy
23
+ const minLength = passwordPolicy.minLength || 8;
24
+ const maxLength = passwordPolicy.maxLength || 128;
25
+ const requireUppercase = passwordPolicy.requireUppercase !== false;
26
+ const requireLowercase = passwordPolicy.requireLowercase !== false;
27
+ const requireNumbers = passwordPolicy.requireNumbers !== false;
28
+ const requireSymbols = passwordPolicy.requireSymbols || false;
29
+
30
+ // Build password requirements text
31
+ const requirements = [];
32
+ requirements.push(`${minLength}-${maxLength} characters`);
33
+ if (requireUppercase) requirements.push('uppercase letter');
34
+ if (requireLowercase) requirements.push('lowercase letter');
35
+ if (requireNumbers) requirements.push('number');
36
+ if (requireSymbols) requirements.push('symbol');
37
+
38
+ const inputClasses = [
39
+ 'block w-full rounded-2xl border border-white/10 bg-white/[0.08] px-4 py-3 text-sm text-white',
40
+ 'shadow-[0_1px_0_rgba(255,255,255,0.05)] transition placeholder:text-slate-300/70 focus:border-white/40 focus:outline-none focus:ring-2 focus:ring-white/30'
41
+ ].join(' ');
42
+
43
+ const primaryButtonClass = [
44
+ 'inline-flex items-center justify-center rounded-2xl bg-gradient-to-r',
45
+ 'from-primary via-primary to-secondary px-5 py-2.5 text-sm font-semibold text-white',
46
+ 'transition duration-200 hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-white/30'
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 text-xs 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 dangerButtonLargeClass = [
55
+ 'inline-flex items-center justify-center rounded-2xl border border-red-400/40 bg-red-500/10',
56
+ 'px-5 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'
57
+ ].join(' ');
58
+
59
+ const panelClasses = 'rounded-3xl border border-white/10 bg-white/[0.05] p-8 shadow-xl shadow-black/30 backdrop-blur';
60
+
61
+ const accountRows = [];
62
+ accountRows.push({
63
+ label: 'Account Status',
64
+ value: user.status === 'active'
65
+ ? html`<span class="text-emerald-300">✓ Active</span>`
66
+ : user.status === 'pending_verification'
67
+ ? html`<span class="text-amber-300">⏳ Pending Verification</span>`
68
+ : user.status === 'suspended'
69
+ ? html`<span class="text-red-300">⚠ Suspended</span>`
70
+ : html`<span class="text-slate-300">Unknown</span>`
71
+ });
72
+
73
+ if (user.isAdmin) {
74
+ accountRows.push({
75
+ label: 'Role',
76
+ value: html`<span class="font-semibold text-primary">👑 Administrator</span>`
77
+ });
78
+ }
79
+
80
+ if (user.lastLoginAt) {
81
+ accountRows.push({
82
+ label: 'Last Login',
83
+ value: html`${new Date(user.lastLoginAt).toLocaleString()}`
84
+ });
85
+ }
86
+
87
+ if (user.lastLoginIp) {
88
+ accountRows.push({
89
+ label: 'Last Login IP',
90
+ value: html`${user.lastLoginIp}`
91
+ });
92
+ }
93
+
94
+ if (user.createdAt) {
95
+ accountRows.push({
96
+ label: 'Member Since',
97
+ value: html`${new Date(user.createdAt).toLocaleDateString()}`
98
+ });
99
+ }
100
+
101
+ const sessionCards = sessions.map(session => {
102
+ const isCurrentSession = session.isCurrent;
103
+ const createdAt = session.createdAt ? new Date(session.createdAt) : null;
104
+ const expiresAt = session.expiresAt ? new Date(session.expiresAt) : null;
105
+
106
+ const sessionClasses = [
107
+ 'rounded-2xl border border-white/10 px-5 py-4 transition',
108
+ isCurrentSession
109
+ ? 'bg-primary/10 ring-1 ring-primary/40'
110
+ : 'bg-white/[0.05] hover:bg-white/[0.08]'
111
+ ].join(' ');
112
+
113
+ return html`
114
+ <div class="${sessionClasses}">
115
+ <div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
116
+ <div class="space-y-3">
117
+ <div class="flex flex-wrap items-center gap-3 text-sm font-semibold text-white">
118
+ <span>${session.userAgent || 'Unknown device'}</span>
119
+ ${isCurrentSession ? html`
120
+ <span class="rounded-full bg-emerald-500/20 px-3 py-1 text-xs font-semibold text-emerald-200">
121
+ Current
122
+ </span>
123
+ ` : ''}
124
+ </div>
125
+ <dl class="grid gap-2 text-xs text-slate-300">
126
+ <div class="flex gap-3">
127
+ <dt class="w-24 text-slate-400">IP</dt>
128
+ <dd class="flex-1">${session.ipAddress || 'Unknown'}</dd>
129
+ </div>
130
+ <div class="flex gap-3">
131
+ <dt class="w-24 text-slate-400">Created</dt>
132
+ <dd class="flex-1">${createdAt ? createdAt.toLocaleString() : 'Unknown'}</dd>
133
+ </div>
134
+ <div class="flex gap-3">
135
+ <dt class="w-24 text-slate-400">Expires</dt>
136
+ <dd class="flex-1">${expiresAt ? expiresAt.toLocaleString() : 'Unknown'}</dd>
137
+ </div>
138
+ </dl>
139
+ </div>
140
+ ${!isCurrentSession ? html`
141
+ <form method="POST" action="/profile/logout-session" class="shrink-0 self-start">
142
+ <input type="hidden" name="session_id" value="${session.id}" />
143
+ <button type="submit" class="${dangerButtonClass}">
144
+ Logout
145
+ </button>
146
+ </form>
147
+ ` : html`
148
+ <span class="shrink-0 rounded-full border border-emerald-400/30 bg-emerald-500/10 px-3 py-1 text-xs font-semibold text-emerald-200">
149
+ Active Session
150
+ </span>
151
+ `}
152
+ </div>
153
+ </div>
154
+ `;
155
+ });
156
+
157
+ const sessionsSection = sessions.length === 0
158
+ ? html`<p class="text-sm text-slate-300">No active sessions</p>`
159
+ : html`
160
+ <div class="space-y-4">
161
+ <p class="text-sm text-slate-300">
162
+ You are currently logged in on these devices. If you see a session you don't recognize, log it out immediately.
163
+ </p>
164
+ ${sessionCards}
165
+ ${sessions.length > 1 ? html`
166
+ <form method="POST" action="/profile/logout-all-sessions" class="pt-2">
167
+ <button type="submit" class="${dangerButtonLargeClass}">
168
+ Logout All Other Sessions
169
+ </button>
170
+ </form>
171
+ ` : ''}
172
+ </div>
173
+ `;
174
+
175
+ const content = html`
176
+ <section class="mx-auto w-full max-w-6xl space-y-8 text-slate-100">
177
+ <header class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
178
+ <div>
179
+ <h1 class="text-3xl font-semibold text-white md:text-4xl">My Profile</h1>
180
+ <p class="mt-1 text-sm text-slate-300">
181
+ Manage your personal information, security preferences, and connected sessions.
182
+ </p>
183
+ </div>
184
+ <div class="flex items-center gap-3 self-start rounded-2xl border border-white/15 bg-white/[0.06] px-4 py-3 text-xs text-slate-300">
185
+ <span class="text-sm font-semibold text-white">${user.email || 'Unknown email'}</span>
186
+ <span class="${user.emailVerified ? 'rounded-full bg-emerald-500/20 px-3 py-1 text-xs font-semibold text-emerald-200' : 'rounded-full bg-amber-500/20 px-3 py-1 text-xs font-semibold text-amber-200'}">
187
+ ${user.emailVerified ? 'Verified' : 'Not verified'}
188
+ </span>
189
+ </div>
190
+ </header>
191
+
192
+ <div class="grid gap-8 lg:grid-cols-[1.1fr_0.9fr]">
193
+ <div class="space-y-8">
194
+ <div class="${panelClasses}">
195
+ <div class="flex items-start justify-between gap-4">
196
+ <div>
197
+ <h2 class="text-xl font-semibold text-white">Profile Information</h2>
198
+ <p class="text-sm text-slate-300">
199
+ Update your personal details and contact email.
200
+ </p>
201
+ </div>
202
+ </div>
203
+ <form method="POST" action="/profile/update" class="mt-6 space-y-6">
204
+ <div class="space-y-2">
205
+ <label for="name" class="text-sm font-semibold text-slate-200">
206
+ Full Name
207
+ </label>
208
+ <input
209
+ type="text"
210
+ class="${inputClasses}"
211
+ id="name"
212
+ name="name"
213
+ value="${user.name || ''}"
214
+ required
215
+ minlength="2"
216
+ maxlength="100"
217
+ />
218
+ </div>
219
+
220
+ <div class="space-y-2">
221
+ <label for="email" class="text-sm font-semibold text-slate-200">
222
+ Email Address
223
+ </label>
224
+ <input
225
+ type="email"
226
+ class="${inputClasses}"
227
+ id="email"
228
+ name="email"
229
+ value="${user.email || ''}"
230
+ required
231
+ autocomplete="email"
232
+ />
233
+ ${user.emailVerified
234
+ ? html`<p class="text-xs font-medium text-emerald-300">✓ Verified email address</p>`
235
+ : html`<p class="text-xs text-amber-200">
236
+ ⚠ Not verified —
237
+ <a href="/verify-email/resend" class="font-semibold text-primary transition hover:text-white">
238
+ Resend verification email
239
+ </a>
240
+ </p>`
241
+ }
242
+ </div>
243
+
244
+ <button type="submit" class="${primaryButtonClass} w-full sm:w-auto" style="box-shadow: 0 18px 45px var(--color-primary-glow);">
245
+ Save Changes
246
+ </button>
247
+ </form>
248
+ </div>
249
+
250
+ <div class="${panelClasses}">
251
+ <h2 class="text-xl font-semibold text-white">Change Password</h2>
252
+ <p class="text-sm text-slate-300">
253
+ Keep your account secure with a strong password.
254
+ </p>
255
+
256
+ <form method="POST" action="/profile/change-password" class="mt-6 space-y-6">
257
+ <div class="space-y-2">
258
+ <label for="current_password" class="text-sm font-semibold text-slate-200">
259
+ Current Password
260
+ </label>
261
+ <input
262
+ type="password"
263
+ class="${inputClasses}"
264
+ id="current_password"
265
+ name="current_password"
266
+ required
267
+ autocomplete="current-password"
268
+ />
269
+ </div>
270
+
271
+ <div class="space-y-2">
272
+ <label for="new_password" class="text-sm font-semibold text-slate-200">
273
+ New Password
274
+ </label>
275
+ <input
276
+ type="password"
277
+ class="${inputClasses}"
278
+ id="new_password"
279
+ name="new_password"
280
+ required
281
+ autocomplete="new-password"
282
+ minlength="${minLength}"
283
+ maxlength="${maxLength}"
284
+ />
285
+ <div class="rounded-2xl border border-white/10 bg-white/[0.06] px-4 py-3 text-xs text-slate-200">
286
+ <span class="font-semibold text-white/80">Must include:</span>
287
+ <ul class="mt-2 list-disc space-y-1 pl-5 text-slate-300">
288
+ ${requirements.map(req => html`<li>${req}</li>`)}
289
+ </ul>
290
+ </div>
291
+ </div>
292
+
293
+ <div class="space-y-2">
294
+ <label for="confirm_new_password" class="text-sm font-semibold text-slate-200">
295
+ Confirm New Password
296
+ </label>
297
+ <input
298
+ type="password"
299
+ class="${inputClasses}"
300
+ id="confirm_new_password"
301
+ name="confirm_new_password"
302
+ required
303
+ autocomplete="new-password"
304
+ />
305
+ </div>
306
+
307
+ <button type="submit" class="${primaryButtonClass} w-full sm:w-auto" style="box-shadow: 0 18px 45px var(--color-primary-glow);">
308
+ Change Password
309
+ </button>
310
+ </form>
311
+ </div>
312
+ </div>
313
+
314
+ <div class="space-y-8">
315
+ <div class="${panelClasses}">
316
+ <h2 class="text-xl font-semibold text-white">Account Overview</h2>
317
+ <p class="text-sm text-slate-300">
318
+ Key information about your account and access.
319
+ </p>
320
+ <dl class="mt-6 space-y-4">
321
+ ${accountRows.map(row => html`
322
+ <div class="flex flex-col gap-2 border-b border-white/10 pb-4 last:border-b-0 last:pb-0 sm:flex-row sm:items-start sm:gap-6">
323
+ <dt class="text-xs font-semibold uppercase tracking-wide text-slate-400 sm:w-40">${row.label}</dt>
324
+ <dd class="text-sm text-slate-200 sm:flex-1">${row.value}</dd>
325
+ </div>
326
+ `)}
327
+ </dl>
328
+ </div>
329
+ </div>
330
+ </div>
331
+
332
+ <div class="${panelClasses}">
333
+ <div class="flex items-center justify-between">
334
+ <div>
335
+ <h2 class="text-xl font-semibold text-white">Active Sessions</h2>
336
+ <p class="text-sm text-slate-300">
337
+ Review and manage the devices currently connected to your account.
338
+ </p>
339
+ </div>
340
+ <span class="rounded-full bg-primary/20 px-3 py-1 text-sm font-semibold text-primary">
341
+ ${sessions.length} active
342
+ </span>
343
+ </div>
344
+ <div class="mt-6 space-y-4">
345
+ ${sessionsSection}
346
+ </div>
347
+ </div>
348
+ </section>
349
+ `;
350
+
351
+ return BaseLayout({
352
+ title: 'My Profile',
353
+ content,
354
+ config,
355
+ user,
356
+ error,
357
+ success
358
+ });
359
+ }
360
+
361
+ export default ProfilePage;