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.
- package/README.md +25 -10
- package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +38653 -32291
- package/dist/s3db.es.js.map +1 -1
- package/package.json +218 -22
- package/src/concerns/id.js +90 -6
- package/src/concerns/index.js +2 -1
- package/src/concerns/password-hashing.js +150 -0
- package/src/database.class.js +6 -2
- package/src/plugins/api/auth/basic-auth.js +40 -10
- package/src/plugins/api/auth/index.js +49 -3
- package/src/plugins/api/auth/oauth2-auth.js +171 -0
- package/src/plugins/api/auth/oidc-auth.js +789 -0
- package/src/plugins/api/auth/oidc-client.js +462 -0
- package/src/plugins/api/auth/path-auth-matcher.js +284 -0
- package/src/plugins/api/concerns/event-emitter.js +134 -0
- package/src/plugins/api/concerns/failban-manager.js +651 -0
- package/src/plugins/api/concerns/guards-helpers.js +402 -0
- package/src/plugins/api/concerns/metrics-collector.js +346 -0
- package/src/plugins/api/index.js +510 -57
- package/src/plugins/api/middlewares/failban.js +305 -0
- package/src/plugins/api/middlewares/rate-limit.js +301 -0
- package/src/plugins/api/middlewares/request-id.js +74 -0
- package/src/plugins/api/middlewares/security-headers.js +120 -0
- package/src/plugins/api/middlewares/session-tracking.js +194 -0
- package/src/plugins/api/routes/auth-routes.js +119 -78
- package/src/plugins/api/routes/resource-routes.js +73 -30
- package/src/plugins/api/server.js +1139 -45
- package/src/plugins/api/utils/custom-routes.js +102 -0
- package/src/plugins/api/utils/guards.js +213 -0
- package/src/plugins/api/utils/mime-types.js +154 -0
- package/src/plugins/api/utils/openapi-generator.js +91 -12
- package/src/plugins/api/utils/path-matcher.js +173 -0
- package/src/plugins/api/utils/static-filesystem.js +262 -0
- package/src/plugins/api/utils/static-s3.js +231 -0
- package/src/plugins/api/utils/template-engine.js +188 -0
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
- package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
- package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
- package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
- package/src/plugins/cloud-inventory/index.js +20 -0
- package/src/plugins/cloud-inventory/registry.js +146 -0
- package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
- package/src/plugins/cloud-inventory.plugin.js +1333 -0
- package/src/plugins/concerns/plugin-dependencies.js +62 -2
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- package/src/plugins/eventual-consistency/consolidation.js +2 -2
- package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
- package/src/plugins/eventual-consistency/install.js +2 -2
- package/src/plugins/identity/README.md +335 -0
- package/src/plugins/identity/concerns/mfa-manager.js +204 -0
- package/src/plugins/identity/concerns/password.js +138 -0
- package/src/plugins/identity/concerns/resource-schemas.js +273 -0
- package/src/plugins/identity/concerns/token-generator.js +172 -0
- package/src/plugins/identity/email-service.js +422 -0
- package/src/plugins/identity/index.js +1052 -0
- package/src/plugins/identity/oauth2-server.js +1033 -0
- package/src/plugins/identity/oidc-discovery.js +285 -0
- package/src/plugins/identity/rsa-keys.js +323 -0
- package/src/plugins/identity/server.js +500 -0
- package/src/plugins/identity/session-manager.js +453 -0
- package/src/plugins/identity/ui/layouts/base.js +251 -0
- package/src/plugins/identity/ui/middleware.js +135 -0
- package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
- package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
- package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
- package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
- package/src/plugins/identity/ui/pages/admin/users.js +263 -0
- package/src/plugins/identity/ui/pages/consent.js +262 -0
- package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
- package/src/plugins/identity/ui/pages/login.js +144 -0
- package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
- package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
- package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
- package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
- package/src/plugins/identity/ui/pages/profile.js +361 -0
- package/src/plugins/identity/ui/pages/register.js +226 -0
- package/src/plugins/identity/ui/pages/reset-password.js +128 -0
- package/src/plugins/identity/ui/pages/verify-email.js +172 -0
- package/src/plugins/identity/ui/routes.js +2541 -0
- package/src/plugins/identity/ui/styles/main.css +465 -0
- package/src/plugins/index.js +4 -1
- package/src/plugins/ml/base-model.class.js +65 -16
- package/src/plugins/ml/classification-model.class.js +1 -1
- package/src/plugins/ml/timeseries-model.class.js +3 -1
- package/src/plugins/ml.plugin.js +584 -31
- package/src/plugins/shared/error-handler.js +147 -0
- package/src/plugins/shared/index.js +9 -0
- package/src/plugins/shared/middlewares/compression.js +117 -0
- package/src/plugins/shared/middlewares/cors.js +49 -0
- package/src/plugins/shared/middlewares/index.js +11 -0
- package/src/plugins/shared/middlewares/logging.js +54 -0
- package/src/plugins/shared/middlewares/rate-limit.js +73 -0
- package/src/plugins/shared/middlewares/security.js +158 -0
- package/src/plugins/shared/response-formatter.js +264 -0
- package/src/plugins/state-machine.plugin.js +57 -2
- package/src/resource.class.js +140 -12
- package/src/schema.class.js +30 -1
- package/src/validator.class.js +57 -6
- 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;
|