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,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;
|