s3db.js 13.5.1 → 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 (105) hide show
  1. package/README.md +25 -10
  2. package/dist/{s3db.cjs.js → s3db.cjs} +30323 -24958
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +24026 -18654
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +216 -20
  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 +4 -0
  11. package/src/plugins/api/auth/basic-auth.js +23 -1
  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 +503 -54
  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 +23 -3
  28. package/src/plugins/api/routes/resource-routes.js +71 -29
  29. package/src/plugins/api/server.js +1017 -94
  30. package/src/plugins/api/utils/guards.js +213 -0
  31. package/src/plugins/api/utils/mime-types.js +154 -0
  32. package/src/plugins/api/utils/openapi-generator.js +44 -11
  33. package/src/plugins/api/utils/path-matcher.js +173 -0
  34. package/src/plugins/api/utils/static-filesystem.js +262 -0
  35. package/src/plugins/api/utils/static-s3.js +231 -0
  36. package/src/plugins/api/utils/template-engine.js +188 -0
  37. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  38. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  39. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  40. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  41. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  42. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  43. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  44. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  45. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  46. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  47. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  48. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  49. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  50. package/src/plugins/cloud-inventory/index.js +20 -0
  51. package/src/plugins/cloud-inventory/registry.js +146 -0
  52. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  53. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  54. package/src/plugins/concerns/plugin-dependencies.js +61 -1
  55. package/src/plugins/eventual-consistency/analytics.js +1 -0
  56. package/src/plugins/identity/README.md +335 -0
  57. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  58. package/src/plugins/identity/concerns/password.js +138 -0
  59. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  60. package/src/plugins/identity/concerns/token-generator.js +172 -0
  61. package/src/plugins/identity/email-service.js +422 -0
  62. package/src/plugins/identity/index.js +1052 -0
  63. package/src/plugins/identity/oauth2-server.js +1033 -0
  64. package/src/plugins/identity/oidc-discovery.js +285 -0
  65. package/src/plugins/identity/rsa-keys.js +323 -0
  66. package/src/plugins/identity/server.js +500 -0
  67. package/src/plugins/identity/session-manager.js +453 -0
  68. package/src/plugins/identity/ui/layouts/base.js +251 -0
  69. package/src/plugins/identity/ui/middleware.js +135 -0
  70. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  71. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  72. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  73. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  74. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  75. package/src/plugins/identity/ui/pages/consent.js +262 -0
  76. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  77. package/src/plugins/identity/ui/pages/login.js +144 -0
  78. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  79. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  80. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  81. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  82. package/src/plugins/identity/ui/pages/profile.js +361 -0
  83. package/src/plugins/identity/ui/pages/register.js +226 -0
  84. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  85. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  86. package/src/plugins/identity/ui/routes.js +2541 -0
  87. package/src/plugins/identity/ui/styles/main.css +465 -0
  88. package/src/plugins/index.js +4 -1
  89. package/src/plugins/ml/base-model.class.js +32 -7
  90. package/src/plugins/ml/classification-model.class.js +1 -1
  91. package/src/plugins/ml/timeseries-model.class.js +3 -1
  92. package/src/plugins/ml.plugin.js +124 -32
  93. package/src/plugins/shared/error-handler.js +147 -0
  94. package/src/plugins/shared/index.js +9 -0
  95. package/src/plugins/shared/middlewares/compression.js +117 -0
  96. package/src/plugins/shared/middlewares/cors.js +49 -0
  97. package/src/plugins/shared/middlewares/index.js +11 -0
  98. package/src/plugins/shared/middlewares/logging.js +54 -0
  99. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  100. package/src/plugins/shared/middlewares/security.js +158 -0
  101. package/src/plugins/shared/response-formatter.js +264 -0
  102. package/src/resource.class.js +140 -12
  103. package/src/schema.class.js +30 -1
  104. package/src/validator.class.js +57 -6
  105. package/dist/s3db.cjs.js.map +0 -1
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Admin Users Management Page
3
+ */
4
+
5
+ import { html } from 'hono/html';
6
+ import { BaseLayout } from '../../layouts/base.js';
7
+
8
+ const STATUS_STYLES = {
9
+ active: 'bg-emerald-500/20 text-emerald-200',
10
+ suspended: 'bg-red-500/20 text-red-200',
11
+ pending_verification: 'bg-amber-500/20 text-amber-200'
12
+ };
13
+
14
+ /**
15
+ * Render users list page
16
+ * @param {Object} props - Page properties
17
+ * @param {Array} props.users - List of users
18
+ * @param {Object} props.user - Current user
19
+ * @param {string} [props.error] - Error message
20
+ * @param {string} [props.success] - Success message
21
+ * @param {Object} [props.config] - UI configuration
22
+ * @returns {string} HTML string
23
+ */
24
+ export function AdminUsersPage(props = {}) {
25
+ const { users = [], user = {}, error = null, success = null, config = {} } = props;
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 text-xs 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 text-xs 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 text-xs 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 primaryButtonClass = [
44
+ 'inline-flex items-center justify-center rounded-2xl bg-gradient-to-r',
45
+ 'from-primary via-primary to-secondary px-4 py-2 text-xs font-semibold text-white',
46
+ 'transition hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-white/30'
47
+ ].join(' ');
48
+
49
+ const headerSecondaryButtonClass = [
50
+ 'inline-flex items-center justify-center rounded-2xl border border-white/15 bg-white/[0.06]',
51
+ 'px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.12]',
52
+ 'focus:outline-none focus:ring-2 focus:ring-white/20'
53
+ ].join(' ');
54
+
55
+ const summaryCards = [
56
+ {
57
+ label: 'Total Users',
58
+ value: users.length,
59
+ gradient: 'from-sky-500/90 via-blue-500/80 to-indigo-500/80'
60
+ },
61
+ {
62
+ label: 'Active',
63
+ value: users.filter(u => u.status === 'active').length,
64
+ gradient: 'from-emerald-400/90 via-green-400/80 to-teal-400/80'
65
+ },
66
+ {
67
+ label: 'Pending',
68
+ value: users.filter(u => u.status === 'pending_verification').length,
69
+ gradient: 'from-amber-400/90 via-orange-400/80 to-yellow-400/80'
70
+ },
71
+ {
72
+ label: 'Verified Emails',
73
+ value: users.filter(u => u.emailVerified).length,
74
+ gradient: 'from-fuchsia-500/90 via-rose-500/80 to-purple-500/80'
75
+ }
76
+ ];
77
+
78
+ const tableRows = users.map(current => {
79
+ const statusClass = STATUS_STYLES[current.status] || 'bg-white/10 text-slate-200';
80
+ const isCurrentUser = current.id === user.id;
81
+
82
+ const actions = [];
83
+
84
+ actions.push(html`
85
+ <a href="/admin/users/${current.id}/edit" class="${secondaryButtonClass}">
86
+ Edit
87
+ </a>
88
+ `);
89
+
90
+ if (!isCurrentUser) {
91
+ actions.push(html`
92
+ <form method="POST" action="/admin/users/${current.id}/delete" onsubmit="return confirm('Delete user ${current.email}? This cannot be undone.')">
93
+ <button type="submit" class="${dangerButtonClass}">
94
+ Delete
95
+ </button>
96
+ </form>
97
+ `);
98
+
99
+ actions.push(html`
100
+ <form method="POST" action="/admin/users/${current.id}/toggle-status">
101
+ <button type="submit" class="${current.status === 'active' ? dangerButtonClass : successButtonClass}">
102
+ ${current.status === 'active' ? '🔴 Suspend' : '🟢 Activate'}
103
+ </button>
104
+ </form>
105
+ `);
106
+
107
+ if (!current.emailVerified) {
108
+ actions.push(html`
109
+ <form method="POST" action="/admin/users/${current.id}/verify-email">
110
+ <button type="submit" class="${secondaryButtonClass}">
111
+ ✓ Mark Verified
112
+ </button>
113
+ </form>
114
+ `);
115
+ }
116
+
117
+ if (current.lockedUntil || current.failedLoginAttempts > 0) {
118
+ const isLocked = current.lockedUntil && new Date(current.lockedUntil) > new Date();
119
+ const lockInfo = isLocked
120
+ ? `Locked until ${new Date(current.lockedUntil).toLocaleString()}`
121
+ : `${current.failedLoginAttempts} failed attempts`;
122
+
123
+ actions.push(html`
124
+ <form method="POST" action="/admin/users/${current.id}/unlock-account" onsubmit="return confirm('Unlock account for ${current.email}?\\n\\n${lockInfo}')">
125
+ <button type="submit" class="${successButtonClass}">
126
+ 🔓 Unlock Account
127
+ </button>
128
+ </form>
129
+ `);
130
+ }
131
+
132
+ actions.push(html`
133
+ <form method="POST" action="/admin/users/${current.id}/reset-password" onsubmit="return confirm('Send password reset email to ${current.email}?')">
134
+ <button type="submit" class="${secondaryButtonClass}">
135
+ 🔑 Send Reset
136
+ </button>
137
+ </form>
138
+ `);
139
+
140
+ actions.push(html`
141
+ <form method="POST" action="/admin/users/${current.id}/toggle-admin" onsubmit="return confirm('${current.role === 'admin' ? 'Remove admin privileges from' : 'Grant admin privileges to'} ${current.name}?')">
142
+ <button type="submit" class="${current.role === 'admin' ? dangerButtonClass : primaryButtonClass}">
143
+ ${current.role === 'admin' ? '👤 Remove Admin' : '⚡ Make Admin'}
144
+ </button>
145
+ </form>
146
+ `);
147
+ }
148
+
149
+ return html`
150
+ <tr class="border-b border-white/10 hover:bg-white/[0.04]">
151
+ <td class="px-4 py-3 align-top">
152
+ <div class="flex flex-wrap items-center gap-2">
153
+ <span class="font-semibold text-white">${current.name}</span>
154
+ ${isCurrentUser ? html`
155
+ <span class="rounded-full bg-primary/20 px-3 py-1 text-xs font-semibold text-primary">
156
+ You
157
+ </span>
158
+ ` : ''}
159
+ </div>
160
+ </td>
161
+ <td class="px-4 py-3 align-top">
162
+ <code class="rounded-xl border border-white/10 bg-white/[0.08] px-3 py-1 text-xs text-slate-200">
163
+ ${current.email}
164
+ </code>
165
+ </td>
166
+ <td class="px-4 py-3 align-top">
167
+ <span class="inline-flex rounded-full px-3 py-1 text-xs font-semibold ${statusClass}">
168
+ ${current.status.replace('_', ' ')}
169
+ </span>
170
+ </td>
171
+ <td class="px-4 py-3 align-top">
172
+ ${current.role === 'admin' ? html`
173
+ <span class="rounded-full bg-red-500/20 px-3 py-1 text-xs font-semibold text-red-200">
174
+ Admin
175
+ </span>
176
+ ` : html`
177
+ <span class="text-xs text-slate-300">User</span>
178
+ `}
179
+ </td>
180
+ <td class="px-4 py-3 align-top">
181
+ ${current.emailVerified ? html`
182
+ <span class="text-emerald-300">✓</span>
183
+ ` : html`
184
+ <span class="text-slate-400">✗</span>
185
+ `}
186
+ </td>
187
+ <td class="px-4 py-3 align-top text-xs text-slate-400">
188
+ ${current.createdAt ? new Date(current.createdAt).toLocaleDateString() : 'Unknown'}
189
+ </td>
190
+ <td class="px-4 py-3 align-top">
191
+ <div class="flex flex-wrap justify-end gap-2">
192
+ ${actions}
193
+ </div>
194
+ </td>
195
+ </tr>
196
+ `;
197
+ });
198
+
199
+ const content = html`
200
+ <section class="mx-auto w-full max-w-7xl space-y-8 text-slate-100">
201
+ <header class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
202
+ <div>
203
+ <h1 class="text-3xl font-semibold text-white md:text-4xl">User Management</h1>
204
+ <p class="mt-1 text-sm text-slate-300">
205
+ Audit user accounts, toggle access, and elevate permissions.
206
+ </p>
207
+ </div>
208
+ <a href="/admin" class="${headerSecondaryButtonClass}">
209
+ ← Back to Dashboard
210
+ </a>
211
+ </header>
212
+
213
+ ${users.length === 0 ? html`
214
+ <div class="rounded-3xl border border-white/10 bg-white/[0.05] p-10 text-center shadow-xl shadow-black/30 backdrop-blur">
215
+ <p class="text-sm text-slate-300">
216
+ No users found.
217
+ </p>
218
+ </div>
219
+ ` : html`
220
+ <div class="rounded-3xl border border-white/10 bg-white/[0.05] shadow-xl shadow-black/30 backdrop-blur">
221
+ <div class="overflow-x-auto">
222
+ <table class="min-w-full divide-y divide-white/10 text-left text-sm text-slate-200">
223
+ <thead class="bg-white/[0.04] text-xs uppercase tracking-wide text-slate-400">
224
+ <tr>
225
+ <th class="px-4 py-3 font-medium">Name</th>
226
+ <th class="px-4 py-3 font-medium">Email</th>
227
+ <th class="px-4 py-3 font-medium">Status</th>
228
+ <th class="px-4 py-3 font-medium">Role</th>
229
+ <th class="px-4 py-3 font-medium">Verified</th>
230
+ <th class="px-4 py-3 font-medium">Joined</th>
231
+ <th class="px-4 py-3 text-right font-medium">Actions</th>
232
+ </tr>
233
+ </thead>
234
+ <tbody class="divide-y divide-white/10">
235
+ ${tableRows}
236
+ </tbody>
237
+ </table>
238
+ </div>
239
+ </div>
240
+ `}
241
+
242
+ <div class="grid gap-6 sm:grid-cols-2 xl:grid-cols-4">
243
+ ${summaryCards.map(card => html`
244
+ <div class="rounded-3xl border border-white/10 bg-gradient-to-br ${card.gradient} p-6 text-center shadow-xl shadow-black/30 backdrop-blur">
245
+ <div class="text-xs uppercase tracking-wide text-white/80">${card.label}</div>
246
+ <div class="mt-3 text-3xl font-semibold text-white">${card.value}</div>
247
+ </div>
248
+ `)}
249
+ </div>
250
+ </section>
251
+ `;
252
+
253
+ return BaseLayout({
254
+ title: 'User Management - Admin',
255
+ content,
256
+ config,
257
+ user,
258
+ error,
259
+ success
260
+ });
261
+ }
262
+
263
+ export default AdminUsersPage;
@@ -0,0 +1,262 @@
1
+ /**
2
+ * OAuth2 Consent Screen Page
3
+ */
4
+
5
+ import { html } from 'hono/html';
6
+ import { BaseLayout } from '../layouts/base.js';
7
+
8
+ /**
9
+ * Scope descriptions for display
10
+ */
11
+ const SCOPE_DESCRIPTIONS = {
12
+ openid: {
13
+ name: 'OpenID Connect',
14
+ description: 'Sign in using your identity',
15
+ icon: '🔐'
16
+ },
17
+ profile: {
18
+ name: 'Profile Information',
19
+ description: 'Access your basic profile information (name, picture)',
20
+ icon: '👤'
21
+ },
22
+ email: {
23
+ name: 'Email Address',
24
+ description: 'Access your email address',
25
+ icon: '📧'
26
+ },
27
+ offline_access: {
28
+ name: 'Offline Access',
29
+ description: 'Maintain access when you are not using the app',
30
+ icon: '🔄'
31
+ },
32
+ phone: {
33
+ name: 'Phone Number',
34
+ description: 'Access your phone number',
35
+ icon: '📱'
36
+ },
37
+ address: {
38
+ name: 'Address',
39
+ description: 'Access your address information',
40
+ icon: '🏠'
41
+ }
42
+ };
43
+
44
+ /**
45
+ * Render OAuth2 consent page
46
+ * @param {Object} props - Page properties
47
+ * @param {Object} props.client - OAuth2 client requesting access
48
+ * @param {Array} props.scopes - Requested scopes
49
+ * @param {Object} props.user - Current user
50
+ * @param {string} props.responseType - OAuth2 response_type
51
+ * @param {string} props.redirectUri - Redirect URI
52
+ * @param {string} [props.state] - OAuth2 state parameter
53
+ * @param {string} [props.codeChallenge] - PKCE code challenge
54
+ * @param {string} [props.codeChallengeMethod] - PKCE challenge method
55
+ * @param {string} [props.error] - Error message
56
+ * @param {Object} [props.config] - UI configuration
57
+ * @returns {string} HTML string
58
+ */
59
+ export function ConsentPage(props = {}) {
60
+ const {
61
+ client = {},
62
+ scopes = [],
63
+ user = {},
64
+ responseType,
65
+ redirectUri,
66
+ state = '',
67
+ codeChallenge = '',
68
+ codeChallengeMethod = 'plain',
69
+ error = null,
70
+ config = {}
71
+ } = props;
72
+
73
+ // Filter out unknown scopes and add descriptions
74
+ const scopeDetails = scopes
75
+ .map(scope => ({
76
+ scope,
77
+ ...SCOPE_DESCRIPTIONS[scope],
78
+ unknown: !SCOPE_DESCRIPTIONS[scope]
79
+ }))
80
+ .filter(s => !s.unknown);
81
+
82
+ const checkboxClasses = [
83
+ 'h-5 w-5 rounded border-white/30 bg-slate-900/70 text-primary',
84
+ 'focus:ring-2 focus:ring-primary/40 focus:ring-offset-0 focus:outline-none'
85
+ ].join(' ');
86
+
87
+ const primaryButtonClasses = [
88
+ 'inline-flex w-full items-center justify-center rounded-2xl bg-gradient-to-r',
89
+ 'from-primary via-primary to-secondary px-5 py-3 text-sm font-semibold text-white',
90
+ 'transition duration-200 hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-white/30'
91
+ ].join(' ');
92
+
93
+ const secondaryButtonClasses = [
94
+ 'inline-flex w-full items-center justify-center rounded-2xl border border-white/15 bg-white/[0.06]',
95
+ 'px-5 py-3 text-sm font-semibold text-white transition duration-200 hover:bg-white/[0.12]',
96
+ 'focus:outline-none focus:ring-2 focus:ring-white/20'
97
+ ].join(' ');
98
+
99
+ const content = html`
100
+ <section class="mx-auto w-full max-w-5xl space-y-8 text-slate-100">
101
+ <header class="space-y-4 text-center">
102
+ ${config.logoUrl ? html`
103
+ <div class="flex justify-center">
104
+ <img src="${config.logoUrl}" alt="${config.title || 'Identity Logo'}" class="h-14 w-auto" />
105
+ </div>
106
+ ` : ''}
107
+ <div>
108
+ <h1 class="text-3xl font-semibold text-white md:text-4xl">Authorize Application</h1>
109
+ <p class="mt-2 text-sm text-slate-300 md:text-base">
110
+ <span class="font-semibold text-white">${client.name || 'Application'}</span>
111
+ is requesting access to your account.
112
+ </p>
113
+ </div>
114
+ </header>
115
+
116
+ ${error ? html`
117
+ <div class="rounded-2xl border border-red-400/40 bg-red-500/10 px-4 py-3 text-sm leading-6 text-red-100 shadow-md shadow-red-900/30">
118
+ ${error}
119
+ </div>
120
+ ` : ''}
121
+
122
+ <div class="grid gap-6 lg:grid-cols-[1.1fr_0.9fr]">
123
+ <div class="space-y-6">
124
+ <div class="rounded-3xl border border-white/10 bg-white/[0.05] p-6 shadow-xl shadow-black/30 backdrop-blur">
125
+ <h2 class="text-lg font-semibold text-white">Application Information</h2>
126
+ <dl class="mt-4 space-y-4 text-sm text-slate-200">
127
+ <div class="flex flex-col gap-1">
128
+ <dt class="text-xs uppercase tracking-wide text-slate-400">Application Name</dt>
129
+ <dd class="text-base font-semibold text-white">${client.name || 'Unknown Application'}</dd>
130
+ </div>
131
+
132
+ ${client.description ? html`
133
+ <div class="flex flex-col gap-1">
134
+ <dt class="text-xs uppercase tracking-wide text-slate-400">Description</dt>
135
+ <dd>${client.description}</dd>
136
+ </div>
137
+ ` : ''}
138
+
139
+ <div class="flex flex-col gap-1">
140
+ <dt class="text-xs uppercase tracking-wide text-slate-400">Client ID</dt>
141
+ <dd>
142
+ <code class="rounded-xl border border-white/10 bg-white/[0.08] px-3 py-2 text-xs text-slate-200">
143
+ ${client.clientId}
144
+ </code>
145
+ </dd>
146
+ </div>
147
+
148
+ <div class="flex flex-col gap-1">
149
+ <dt class="text-xs uppercase tracking-wide text-slate-400">Will Redirect To</dt>
150
+ <dd>
151
+ <code class="rounded-xl border border-white/10 bg-white/[0.08] px-3 py-2 text-xs text-slate-200">
152
+ ${redirectUri}
153
+ </code>
154
+ </dd>
155
+ </div>
156
+ </dl>
157
+ </div>
158
+
159
+ <div class="rounded-3xl border border-white/10 bg-white/[0.05] p-6 shadow-xl shadow-black/30 backdrop-blur">
160
+ <h2 class="text-lg font-semibold text-white">Requested Permissions</h2>
161
+ <div class="mt-4 space-y-4">
162
+ ${scopeDetails.length === 0 ? html`
163
+ <p class="text-sm italic text-slate-300">
164
+ This application is not requesting any specific permissions.
165
+ </p>
166
+ ` : scopeDetails.map(s => html`
167
+ <div class="flex gap-4 rounded-2xl border border-white/5 bg-white/[0.03] p-4">
168
+ <div class="text-3xl leading-none">${s.icon}</div>
169
+ <div class="space-y-1">
170
+ <div class="text-sm font-semibold text-white">${s.name}</div>
171
+ <p class="text-xs text-slate-300">${s.description}</p>
172
+ </div>
173
+ </div>
174
+ `)}
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ <div class="space-y-6">
180
+ <div class="rounded-3xl border border-white/10 bg-white/[0.05] p-6 shadow-xl shadow-black/30 backdrop-blur">
181
+ <div class="flex gap-4">
182
+ <div class="text-2xl">ℹ️</div>
183
+ <div class="space-y-3 text-sm text-slate-200">
184
+ <p class="text-base font-semibold text-white">
185
+ Signed in as ${user.name}
186
+ </p>
187
+ <p>
188
+ By clicking <strong>Allow</strong>, you authorize
189
+ <strong>${client.name || 'this application'}</strong> to access your information as described above.
190
+ </p>
191
+ <p>
192
+ You can revoke this access at any time from your
193
+ <a href="/profile" class="font-semibold text-primary transition hover:text-white">profile settings</a>.
194
+ </p>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <form method="POST" action="/oauth/consent" class="space-y-6 rounded-3xl border border-white/10 bg-white/[0.05] p-6 shadow-xl shadow-black/30 backdrop-blur">
202
+ <input type="hidden" name="response_type" value="${responseType}" />
203
+ <input type="hidden" name="client_id" value="${client.clientId}" />
204
+ <input type="hidden" name="redirect_uri" value="${redirectUri}" />
205
+ <input type="hidden" name="scope" value="${scopes.join(' ')}" />
206
+ ${state ? html`<input type="hidden" name="state" value="${state}" />` : ''}
207
+ ${codeChallenge ? html`<input type="hidden" name="code_challenge" value="${codeChallenge}" />` : ''}
208
+ ${codeChallengeMethod ? html`<input type="hidden" name="code_challenge_method" value="${codeChallengeMethod}" />` : ''}
209
+
210
+ <label class="flex items-start gap-3 text-sm text-slate-300">
211
+ <input
212
+ type="checkbox"
213
+ class="${checkboxClasses} mt-0.5"
214
+ id="trust_application"
215
+ name="trust_application"
216
+ value="1"
217
+ />
218
+ <span>
219
+ Trust this application (don't ask again)<br>
220
+ <span class="text-xs text-slate-400">
221
+ You won't be asked for permission next time this application requests access with the same permissions.
222
+ </span>
223
+ </span>
224
+ </label>
225
+
226
+ <div class="grid gap-3 sm:grid-cols-2">
227
+ <button
228
+ type="submit"
229
+ name="decision"
230
+ value="deny"
231
+ class="${secondaryButtonClasses}"
232
+ >
233
+ Deny
234
+ </button>
235
+ <button
236
+ type="submit"
237
+ name="decision"
238
+ value="allow"
239
+ class="${primaryButtonClasses}"
240
+ style="box-shadow: 0 18px 45px var(--color-primary-glow);"
241
+ >
242
+ Allow
243
+ </button>
244
+ </div>
245
+ </form>
246
+
247
+ <p class="text-center text-sm text-slate-300">
248
+ Not ${user.name}? <a href="/logout" class="font-semibold text-primary transition hover:text-white">Sign out</a>
249
+ </p>
250
+ </section>
251
+ `;
252
+
253
+ return BaseLayout({
254
+ title: `Authorize ${client.name || 'Application'}`,
255
+ content,
256
+ config,
257
+ user,
258
+ error: null // Error shown in page
259
+ });
260
+ }
261
+
262
+ export default ConsentPage;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Forgot Password Page
3
+ */
4
+
5
+ import { html } from 'hono/html';
6
+ import { BaseLayout } from '../layouts/base.js';
7
+
8
+ /**
9
+ * Render forgot password page
10
+ * @param {Object} props - Page properties
11
+ * @param {string} [props.error] - Error message
12
+ * @param {string} [props.success] - Success message
13
+ * @param {string} [props.email] - Pre-filled email
14
+ * @param {Object} [props.config] - UI configuration
15
+ * @returns {string} HTML string
16
+ */
17
+ export function ForgotPasswordPage(props = {}) {
18
+ const { error = null, success = null, email = '', config = {} } = props;
19
+
20
+ const inputClasses = [
21
+ 'block w-full rounded-2xl border bg-white/[0.08] px-4 py-3 text-base text-white',
22
+ 'shadow-[0_1px_0_rgba(255,255,255,0.06)] transition placeholder:text-slate-300/70 focus:outline-none focus:ring-2',
23
+ error ? 'border-red-400/70 focus:border-red-400 focus:ring-red-400/40' : 'border-white/10 focus:border-white/40 focus:ring-white/30'
24
+ ].join(' ');
25
+
26
+ const content = html`
27
+ <section class="mx-auto w-full max-w-lg text-slate-100">
28
+ <div class="relative isolate overflow-hidden rounded-3xl border border-white/10 bg-slate-900/60 p-10 shadow-2xl shadow-slate-900/60 backdrop-blur">
29
+ <div class="pointer-events-none absolute -right-24 -top-24 h-56 w-56 rounded-full bg-primary/25 blur-3xl"></div>
30
+ <div class="pointer-events-none absolute -bottom-20 -left-28 h-52 w-52 rounded-full bg-secondary/20 blur-[120px]"></div>
31
+
32
+ <div class="relative z-10 space-y-6">
33
+ <header class="space-y-2 text-center">
34
+ <h2 class="text-2xl font-semibold tracking-tight text-white">
35
+ Reset Password
36
+ </h2>
37
+ <p class="text-sm text-slate-300">
38
+ Enter your email and we'll send instructions to reset your password.
39
+ </p>
40
+ </header>
41
+
42
+ ${success ? html`
43
+ <div class="space-y-6">
44
+ <div class="rounded-2xl border border-emerald-400/40 bg-emerald-500/10 px-4 py-4 text-sm leading-6 text-emerald-100 shadow-lg shadow-emerald-900/30">
45
+ ${success}
46
+ </div>
47
+ <div class="text-center text-sm text-slate-300">
48
+ <a href="/login" class="font-semibold text-primary transition hover:text-white">
49
+ Back to Sign In
50
+ </a>
51
+ </div>
52
+ </div>
53
+ ` : html`
54
+ <form method="POST" action="/forgot-password" class="space-y-6">
55
+ <div class="space-y-2">
56
+ <label for="email" class="text-sm font-semibold text-slate-200">
57
+ Email address
58
+ </label>
59
+ <input
60
+ type="email"
61
+ class="${inputClasses}"
62
+ id="email"
63
+ name="email"
64
+ value="${email}"
65
+ required
66
+ autofocus
67
+ autocomplete="email"
68
+ />
69
+ ${error ? html`
70
+ <p class="text-xs text-red-200">${error}</p>
71
+ ` : ''}
72
+ </div>
73
+
74
+ <button
75
+ type="submit"
76
+ class="inline-flex w-full items-center justify-center rounded-2xl bg-gradient-to-r from-primary via-primary to-secondary px-4 py-3 text-base font-semibold text-white transition duration-200 hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-white/30"
77
+ style="box-shadow: 0 18px 45px var(--color-primary-glow);"
78
+ >
79
+ Send Reset Link
80
+ </button>
81
+ </form>
82
+
83
+ <p class="text-center text-sm text-slate-300">
84
+ Remember your password?
85
+ <a href="/login" class="font-semibold text-primary transition hover:text-white">
86
+ Sign in
87
+ </a>
88
+ </p>
89
+ `}
90
+ </div>
91
+ </div>
92
+ </section>
93
+ `;
94
+
95
+ return BaseLayout({
96
+ title: 'Reset Password',
97
+ content,
98
+ config,
99
+ error: null, // Error shown in form
100
+ success: null // Success shown in form
101
+ });
102
+ }
103
+
104
+ export default ForgotPasswordPage;