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,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity Provider Middleware
|
|
3
|
+
* Session validation and authentication middleware
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Session authentication middleware
|
|
8
|
+
* Validates session and attaches user data to context
|
|
9
|
+
* @param {Object} sessionManager - SessionManager instance
|
|
10
|
+
* @param {Object} options - Middleware options
|
|
11
|
+
* @param {boolean} options.required - Require authentication (redirect if not logged in)
|
|
12
|
+
* @param {boolean} options.requireAdmin - Require admin role
|
|
13
|
+
* @param {string} options.redirectTo - Redirect URL if not authenticated
|
|
14
|
+
* @returns {Function} Hono middleware function
|
|
15
|
+
*/
|
|
16
|
+
export function sessionAuth(sessionManager, options = {}) {
|
|
17
|
+
const {
|
|
18
|
+
required = false,
|
|
19
|
+
requireAdmin = false,
|
|
20
|
+
redirectTo = '/login'
|
|
21
|
+
} = options;
|
|
22
|
+
|
|
23
|
+
return async (c, next) => {
|
|
24
|
+
const sessionId = sessionManager.getSessionIdFromRequest(c.req);
|
|
25
|
+
|
|
26
|
+
let user = null;
|
|
27
|
+
let session = null;
|
|
28
|
+
|
|
29
|
+
if (sessionId) {
|
|
30
|
+
const { valid, session: validSession, reason } = await sessionManager.validateSession(sessionId);
|
|
31
|
+
|
|
32
|
+
if (valid) {
|
|
33
|
+
session = validSession;
|
|
34
|
+
user = validSession.metadata || null;
|
|
35
|
+
} else {
|
|
36
|
+
// Clear invalid session cookie
|
|
37
|
+
sessionManager.clearSessionCookie(c);
|
|
38
|
+
|
|
39
|
+
if (required) {
|
|
40
|
+
const currentUrl = c.req.url;
|
|
41
|
+
return c.redirect(`${redirectTo}?redirect=${encodeURIComponent(currentUrl)}&error=${encodeURIComponent('Your session has expired. Please log in again.')}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if authentication is required
|
|
47
|
+
if (required && !user) {
|
|
48
|
+
const currentUrl = c.req.url;
|
|
49
|
+
return c.redirect(`${redirectTo}?redirect=${encodeURIComponent(currentUrl)}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if admin is required
|
|
53
|
+
if (requireAdmin && (!user || !user.isAdmin)) {
|
|
54
|
+
return c.html('<h1>403 Forbidden</h1><p>You do not have permission to access this page.</p>', 403);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Attach user and session to context
|
|
58
|
+
c.set('user', user);
|
|
59
|
+
c.set('session', session);
|
|
60
|
+
c.set('isAuthenticated', !!user);
|
|
61
|
+
c.set('isAdmin', user?.isAdmin || false);
|
|
62
|
+
|
|
63
|
+
await next();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Admin-only middleware (shorthand)
|
|
69
|
+
* @param {Object} sessionManager - SessionManager instance
|
|
70
|
+
* @returns {Function} Hono middleware function
|
|
71
|
+
*/
|
|
72
|
+
export function adminOnly(sessionManager) {
|
|
73
|
+
return sessionAuth(sessionManager, {
|
|
74
|
+
required: true,
|
|
75
|
+
requireAdmin: true
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Optional authentication middleware
|
|
81
|
+
* Attaches user if logged in, but doesn't require it
|
|
82
|
+
* @param {Object} sessionManager - SessionManager instance
|
|
83
|
+
* @returns {Function} Hono middleware function
|
|
84
|
+
*/
|
|
85
|
+
export function optionalAuth(sessionManager) {
|
|
86
|
+
return sessionAuth(sessionManager, {
|
|
87
|
+
required: false,
|
|
88
|
+
requireAdmin: false
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* CSRF protection middleware
|
|
94
|
+
* Validates CSRF token for POST/PUT/PATCH/DELETE requests
|
|
95
|
+
* @param {Object} options - CSRF options
|
|
96
|
+
* @param {string[]} options.excludePaths - Paths to exclude from CSRF check
|
|
97
|
+
* @returns {Function} Hono middleware function
|
|
98
|
+
*/
|
|
99
|
+
export function csrfProtection(options = {}) {
|
|
100
|
+
const {
|
|
101
|
+
excludePaths = []
|
|
102
|
+
} = options;
|
|
103
|
+
|
|
104
|
+
return async (c, next) => {
|
|
105
|
+
const method = c.req.method;
|
|
106
|
+
const path = c.req.path;
|
|
107
|
+
|
|
108
|
+
// Skip CSRF check for safe methods
|
|
109
|
+
if (['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
|
110
|
+
await next();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Skip CSRF check for excluded paths
|
|
115
|
+
if (excludePaths.some(p => path.startsWith(p))) {
|
|
116
|
+
await next();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// For now, skip CSRF validation - will be implemented in polish phase
|
|
121
|
+
// TODO: Implement proper CSRF token generation and validation
|
|
122
|
+
// - Generate token on GET requests, store in session
|
|
123
|
+
// - Include token in forms as hidden field
|
|
124
|
+
// - Validate token on POST/PUT/PATCH/DELETE
|
|
125
|
+
|
|
126
|
+
await next();
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default {
|
|
131
|
+
sessionAuth,
|
|
132
|
+
adminOnly,
|
|
133
|
+
optionalAuth,
|
|
134
|
+
csrfProtection
|
|
135
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin OAuth2 Client Form Page (Create/Edit)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { html } from 'hono/html';
|
|
6
|
+
import { BaseLayout } from '../../layouts/base.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Render OAuth2 client form page
|
|
10
|
+
* @param {Object} props - Page properties
|
|
11
|
+
* @param {Object} [props.client] - Client data (for edit mode)
|
|
12
|
+
* @param {Object} props.user - Current user
|
|
13
|
+
* @param {string} [props.error] - Error message
|
|
14
|
+
* @param {Array} [props.availableScopes] - Available OAuth2 scopes
|
|
15
|
+
* @param {Array} [props.availableGrantTypes] - Available grant types
|
|
16
|
+
* @param {Object} [props.config] - UI configuration
|
|
17
|
+
* @returns {string} HTML string
|
|
18
|
+
*/
|
|
19
|
+
export function AdminClientFormPage(props = {}) {
|
|
20
|
+
const {
|
|
21
|
+
client = null,
|
|
22
|
+
user = {},
|
|
23
|
+
error = null,
|
|
24
|
+
availableScopes = [],
|
|
25
|
+
availableGrantTypes = [],
|
|
26
|
+
config = {}
|
|
27
|
+
} = props;
|
|
28
|
+
|
|
29
|
+
const isEditMode = !!client;
|
|
30
|
+
const clientData = client || {
|
|
31
|
+
name: '',
|
|
32
|
+
redirectUris: [''],
|
|
33
|
+
grantTypes: ['authorization_code', 'refresh_token'],
|
|
34
|
+
allowedScopes: ['openid', 'profile', 'email'],
|
|
35
|
+
active: true
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const inputClasses = [
|
|
39
|
+
'block w-full rounded-2xl border border-white/10 bg-white/[0.08]',
|
|
40
|
+
'px-4 py-2.5 text-sm text-white placeholder:text-slate-300/70',
|
|
41
|
+
'shadow-[0_1px_0_rgba(255,255,255,0.05)] transition focus:border-white/40 focus:outline-none focus:ring-2 focus:ring-white/30'
|
|
42
|
+
].join(' ');
|
|
43
|
+
|
|
44
|
+
const checkboxClasses = [
|
|
45
|
+
'h-4 w-4 rounded border-white/30 bg-slate-900/70 text-primary',
|
|
46
|
+
'focus:ring-2 focus:ring-primary/40 focus:ring-offset-0 focus:outline-none'
|
|
47
|
+
].join(' ');
|
|
48
|
+
|
|
49
|
+
const primaryButtonClass = [
|
|
50
|
+
'inline-flex items-center justify-center rounded-2xl bg-gradient-to-r',
|
|
51
|
+
'from-primary via-primary to-secondary px-5 py-2.5 text-sm font-semibold text-white',
|
|
52
|
+
'transition hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-white/30'
|
|
53
|
+
].join(' ');
|
|
54
|
+
|
|
55
|
+
const secondaryButtonClass = [
|
|
56
|
+
'inline-flex items-center justify-center rounded-2xl border border-white/15 bg-white/[0.06]',
|
|
57
|
+
'px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.12]',
|
|
58
|
+
'focus:outline-none focus:ring-2 focus:ring-white/20'
|
|
59
|
+
].join(' ');
|
|
60
|
+
|
|
61
|
+
const dangerButtonClass = [
|
|
62
|
+
'inline-flex items-center justify-center rounded-2xl border border-red-400/40 bg-red-500/10',
|
|
63
|
+
'px-4 py-2 text-sm font-semibold text-red-100 transition hover:bg-red-500/15 focus:outline-none focus:ring-2 focus:ring-red-400/40'
|
|
64
|
+
].join(' ');
|
|
65
|
+
|
|
66
|
+
const content = html`
|
|
67
|
+
<section class="mx-auto w-full max-w-4xl space-y-8 text-slate-100">
|
|
68
|
+
<header>
|
|
69
|
+
<a href="/admin/clients" class="text-sm font-semibold text-primary transition hover:text-white">
|
|
70
|
+
← Back to Clients
|
|
71
|
+
</a>
|
|
72
|
+
<h1 class="mt-3 text-3xl font-semibold text-white md:text-4xl">
|
|
73
|
+
${isEditMode ? 'Edit' : 'Create'} OAuth2 Client
|
|
74
|
+
</h1>
|
|
75
|
+
<p class="mt-2 text-sm text-slate-300">
|
|
76
|
+
Configure redirect URIs, grant types, and scopes available for this client.
|
|
77
|
+
</p>
|
|
78
|
+
</header>
|
|
79
|
+
|
|
80
|
+
<div class="rounded-3xl border border-white/10 bg-white/[0.05] p-8 shadow-xl shadow-black/30 backdrop-blur">
|
|
81
|
+
<form method="POST" action="${isEditMode ? `/admin/clients/${client.id}/update` : '/admin/clients/create'}" class="space-y-6">
|
|
82
|
+
<div class="space-y-2">
|
|
83
|
+
<label for="name" class="text-sm font-semibold text-slate-200">Client Name</label>
|
|
84
|
+
<input
|
|
85
|
+
type="text"
|
|
86
|
+
class="${inputClasses} ${error ? 'border-red-400/60 focus:border-red-400 focus:ring-red-400/40' : ''}"
|
|
87
|
+
id="name"
|
|
88
|
+
name="name"
|
|
89
|
+
value="${clientData.name}"
|
|
90
|
+
required
|
|
91
|
+
autofocus
|
|
92
|
+
placeholder="My Application"
|
|
93
|
+
/>
|
|
94
|
+
<p class="text-xs text-slate-400">A friendly name for this OAuth2 client</p>
|
|
95
|
+
${error ? html`<p class="text-xs text-red-200">${error}</p>` : ''}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="space-y-3">
|
|
99
|
+
<label class="text-sm font-semibold text-slate-200">Redirect URIs</label>
|
|
100
|
+
<div id="redirect-uris-container" class="space-y-2">
|
|
101
|
+
${(Array.isArray(clientData.redirectUris) ? clientData.redirectUris : ['']).map((uri, index) => html`
|
|
102
|
+
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:gap-3">
|
|
103
|
+
<input
|
|
104
|
+
type="url"
|
|
105
|
+
class="${inputClasses}"
|
|
106
|
+
name="redirectUris[]"
|
|
107
|
+
value="${uri}"
|
|
108
|
+
required
|
|
109
|
+
placeholder="https://example.com/callback"
|
|
110
|
+
/>
|
|
111
|
+
${index > 0 ? html`
|
|
112
|
+
<button type="button" class="${dangerButtonClass} shrink-0" onclick="this.parentElement.remove()">
|
|
113
|
+
✕
|
|
114
|
+
</button>
|
|
115
|
+
` : ''}
|
|
116
|
+
</div>
|
|
117
|
+
`)}
|
|
118
|
+
</div>
|
|
119
|
+
<button type="button" class="${secondaryButtonClass}" onclick="addRedirectUri()">
|
|
120
|
+
+ Add Another URI
|
|
121
|
+
</button>
|
|
122
|
+
<p class="text-xs text-slate-400">
|
|
123
|
+
Where users will be redirected after authorization.
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div class="space-y-2">
|
|
128
|
+
<label class="text-sm font-semibold text-slate-200">Grant Types</label>
|
|
129
|
+
<div class="grid gap-2 sm:grid-cols-2">
|
|
130
|
+
${(availableGrantTypes.length > 0 ? availableGrantTypes : ['authorization_code', 'refresh_token', 'client_credentials']).map(type => {
|
|
131
|
+
const isChecked = Array.isArray(clientData.grantTypes) && clientData.grantTypes.includes(type);
|
|
132
|
+
return html`
|
|
133
|
+
<label class="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-200">
|
|
134
|
+
<input
|
|
135
|
+
type="checkbox"
|
|
136
|
+
class="${checkboxClasses}"
|
|
137
|
+
id="grant_${type}"
|
|
138
|
+
name="grantTypes[]"
|
|
139
|
+
value="${type}"
|
|
140
|
+
${isChecked ? 'checked' : ''}
|
|
141
|
+
/>
|
|
142
|
+
<span><code>${type}</code></span>
|
|
143
|
+
</label>
|
|
144
|
+
`;
|
|
145
|
+
})}
|
|
146
|
+
</div>
|
|
147
|
+
<p class="text-xs text-slate-400">OAuth2 grant types this client can use.</p>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div class="space-y-2">
|
|
151
|
+
<label class="text-sm font-semibold text-slate-200">Allowed Scopes</label>
|
|
152
|
+
<div class="grid gap-2 sm:grid-cols-2">
|
|
153
|
+
${(availableScopes.length > 0 ? availableScopes : ['openid', 'profile', 'email', 'offline_access']).map(scope => {
|
|
154
|
+
const isChecked = Array.isArray(clientData.allowedScopes) && clientData.allowedScopes.includes(scope);
|
|
155
|
+
return html`
|
|
156
|
+
<label class="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-200">
|
|
157
|
+
<input
|
|
158
|
+
type="checkbox"
|
|
159
|
+
class="${checkboxClasses}"
|
|
160
|
+
id="scope_${scope}"
|
|
161
|
+
name="allowedScopes[]"
|
|
162
|
+
value="${scope}"
|
|
163
|
+
${isChecked ? 'checked' : ''}
|
|
164
|
+
/>
|
|
165
|
+
<span><code>${scope}</code></span>
|
|
166
|
+
</label>
|
|
167
|
+
`;
|
|
168
|
+
})}
|
|
169
|
+
</div>
|
|
170
|
+
<p class="text-xs text-slate-400">Scopes this client is allowed to request.</p>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<label class="flex items-start gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-200">
|
|
174
|
+
<input
|
|
175
|
+
type="checkbox"
|
|
176
|
+
class="${checkboxClasses} mt-1"
|
|
177
|
+
id="active"
|
|
178
|
+
name="active"
|
|
179
|
+
value="1"
|
|
180
|
+
${clientData.active !== false ? 'checked' : ''}
|
|
181
|
+
/>
|
|
182
|
+
<span>
|
|
183
|
+
<strong class="text-white">Active</strong>
|
|
184
|
+
<br>
|
|
185
|
+
Client can authenticate and receive tokens.
|
|
186
|
+
</span>
|
|
187
|
+
</label>
|
|
188
|
+
|
|
189
|
+
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-start">
|
|
190
|
+
<button type="submit" class="${primaryButtonClass} sm:w-auto" style="box-shadow: 0 18px 45px var(--color-primary-glow);">
|
|
191
|
+
${isEditMode ? 'Update Client' : 'Create Client'}
|
|
192
|
+
</button>
|
|
193
|
+
<a href="/admin/clients" class="${secondaryButtonClass}">
|
|
194
|
+
Cancel
|
|
195
|
+
</a>
|
|
196
|
+
</div>
|
|
197
|
+
</form>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
${isEditMode ? html`
|
|
201
|
+
<div class="rounded-3xl border border-white/15 bg-white/[0.06] px-6 py-4 text-sm text-slate-200 shadow-inner shadow-black/20">
|
|
202
|
+
<strong class="text-white">Note:</strong>
|
|
203
|
+
The client secret cannot be displayed again after creation. If you need a new secret, use the "Rotate Secret"
|
|
204
|
+
action on the clients list page.
|
|
205
|
+
</div>
|
|
206
|
+
` : ''}
|
|
207
|
+
</section>
|
|
208
|
+
|
|
209
|
+
<script>
|
|
210
|
+
const redirectInputClasses = ${JSON.stringify(inputClasses)};
|
|
211
|
+
const dangerButtonClasses = ${JSON.stringify(dangerButtonClass)};
|
|
212
|
+
|
|
213
|
+
function addRedirectUri() {
|
|
214
|
+
const container = document.getElementById('redirect-uris-container');
|
|
215
|
+
const wrapper = document.createElement('div');
|
|
216
|
+
wrapper.className = 'flex flex-col gap-2 sm:flex-row sm:items-start sm:gap-3';
|
|
217
|
+
|
|
218
|
+
const input = document.createElement('input');
|
|
219
|
+
input.type = 'url';
|
|
220
|
+
input.name = 'redirectUris[]';
|
|
221
|
+
input.required = true;
|
|
222
|
+
input.placeholder = 'https://example.com/callback';
|
|
223
|
+
input.className = redirectInputClasses;
|
|
224
|
+
|
|
225
|
+
const button = document.createElement('button');
|
|
226
|
+
button.type = 'button';
|
|
227
|
+
button.className = dangerButtonClasses + ' shrink-0';
|
|
228
|
+
button.textContent = '✕';
|
|
229
|
+
button.addEventListener('click', () => wrapper.remove());
|
|
230
|
+
|
|
231
|
+
wrapper.appendChild(input);
|
|
232
|
+
wrapper.appendChild(button);
|
|
233
|
+
container.appendChild(wrapper);
|
|
234
|
+
}
|
|
235
|
+
</script>
|
|
236
|
+
`;
|
|
237
|
+
|
|
238
|
+
return BaseLayout({
|
|
239
|
+
title: `${isEditMode ? 'Edit' : 'Create'} OAuth2 Client - Admin`,
|
|
240
|
+
content,
|
|
241
|
+
config,
|
|
242
|
+
user,
|
|
243
|
+
error: null // Error shown in form
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export default AdminClientFormPage;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin OAuth2 Clients Management Page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { html } from 'hono/html';
|
|
6
|
+
import { BaseLayout } from '../../layouts/base.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Render OAuth2 clients list page
|
|
10
|
+
* @param {Object} props - Page properties
|
|
11
|
+
* @param {Array} props.clients - List of OAuth2 clients
|
|
12
|
+
* @param {Object} props.user - Current user
|
|
13
|
+
* @param {string} [props.error] - Error message
|
|
14
|
+
* @param {string} [props.success] - Success message
|
|
15
|
+
* @param {Object} [props.config] - UI configuration
|
|
16
|
+
* @returns {string} HTML string
|
|
17
|
+
*/
|
|
18
|
+
export function AdminClientsPage(props = {}) {
|
|
19
|
+
const { clients = [], user = {}, error = null, success = null, config = {} } = props;
|
|
20
|
+
|
|
21
|
+
const primaryButtonClass = [
|
|
22
|
+
'inline-flex items-center justify-center rounded-2xl bg-gradient-to-r',
|
|
23
|
+
'from-primary via-primary to-secondary px-4 py-2.5 text-sm font-semibold text-white',
|
|
24
|
+
'transition hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-white/30'
|
|
25
|
+
].join(' ');
|
|
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.5 text-sm 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.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'
|
|
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.5 text-sm 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 codeChipClass = 'rounded-xl border border-white/10 bg-white/[0.08] px-3 py-1 text-xs text-slate-200';
|
|
44
|
+
const badgeClass = 'rounded-full bg-primary/20 px-3 py-1 text-xs font-semibold text-primary';
|
|
45
|
+
|
|
46
|
+
const content = html`
|
|
47
|
+
<section class="mx-auto w-full max-w-6xl space-y-8 text-slate-100">
|
|
48
|
+
<header class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
49
|
+
<div>
|
|
50
|
+
<h1 class="text-3xl font-semibold text-white md:text-4xl">OAuth2 Clients</h1>
|
|
51
|
+
<p class="mt-1 text-sm text-slate-300">
|
|
52
|
+
Manage client credentials, redirect URIs, and allowed scopes.
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
<a href="/admin/clients/new" class="${primaryButtonClass}">
|
|
56
|
+
+ New Client
|
|
57
|
+
</a>
|
|
58
|
+
</header>
|
|
59
|
+
|
|
60
|
+
${clients.length === 0 ? html`
|
|
61
|
+
<div class="rounded-3xl border border-white/10 bg-white/[0.05] p-10 text-center shadow-xl shadow-black/30 backdrop-blur">
|
|
62
|
+
<p class="text-sm text-slate-300">
|
|
63
|
+
No OAuth2 clients registered yet. Create your first client to start integrating applications.
|
|
64
|
+
</p>
|
|
65
|
+
<a href="/admin/clients/new" class="${primaryButtonClass} mt-6 inline-flex" style="box-shadow: 0 18px 45px var(--color-primary-glow);">
|
|
66
|
+
Create Your First Client
|
|
67
|
+
</a>
|
|
68
|
+
</div>
|
|
69
|
+
` : html`
|
|
70
|
+
<div class="grid gap-6">
|
|
71
|
+
${clients.map(client => {
|
|
72
|
+
const grantTypes = Array.isArray(client.grantTypes) ? client.grantTypes : [];
|
|
73
|
+
const allowedScopes = Array.isArray(client.allowedScopes) ? client.allowedScopes : [];
|
|
74
|
+
const redirectUris = Array.isArray(client.redirectUris) ? client.redirectUris : [];
|
|
75
|
+
|
|
76
|
+
return html`
|
|
77
|
+
<article class="rounded-3xl border border-white/10 bg-white/[0.05] p-6 shadow-xl shadow-black/30 backdrop-blur">
|
|
78
|
+
<div class="flex flex-col gap-4 border-b border-white/10 pb-4 sm:flex-row sm:items-center sm:justify-between">
|
|
79
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
80
|
+
<h2 class="text-lg font-semibold text-white">${client.name}</h2>
|
|
81
|
+
${client.active ? '' : html`
|
|
82
|
+
<span class="rounded-full bg-red-500/20 px-3 py-1 text-xs font-semibold text-red-200">
|
|
83
|
+
Inactive
|
|
84
|
+
</span>
|
|
85
|
+
`}
|
|
86
|
+
</div>
|
|
87
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
88
|
+
<a href="/admin/clients/${client.id}/edit" class="${secondaryButtonClass}">
|
|
89
|
+
Edit
|
|
90
|
+
</a>
|
|
91
|
+
<form method="POST" action="/admin/clients/${client.id}/delete" class="inline-flex" onsubmit="return confirm('Are you sure you want to delete this client? This action cannot be undone.')">
|
|
92
|
+
<button type="submit" class="${dangerButtonClass}">
|
|
93
|
+
Delete
|
|
94
|
+
</button>
|
|
95
|
+
</form>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="mt-4 space-y-6 text-sm text-slate-200">
|
|
100
|
+
<div>
|
|
101
|
+
<div class="text-xs uppercase tracking-wide text-slate-400">Client ID</div>
|
|
102
|
+
<code class="${codeChipClass} mt-2 block">${client.clientId}</code>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
${redirectUris.length > 0 ? html`
|
|
106
|
+
<div>
|
|
107
|
+
<div class="text-xs uppercase tracking-wide text-slate-400">
|
|
108
|
+
Redirect URIs (${redirectUris.length})
|
|
109
|
+
</div>
|
|
110
|
+
<div class="mt-2 flex flex-wrap gap-2">
|
|
111
|
+
${redirectUris.map(uri => html`<code class="${codeChipClass}">${uri}</code>`)}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
` : ''}
|
|
115
|
+
|
|
116
|
+
${grantTypes.length > 0 ? html`
|
|
117
|
+
<div>
|
|
118
|
+
<div class="text-xs uppercase tracking-wide text-slate-400">Grant Types</div>
|
|
119
|
+
<div class="mt-2 flex flex-wrap gap-2">
|
|
120
|
+
${grantTypes.map(type => html`
|
|
121
|
+
<span class="${badgeClass}">
|
|
122
|
+
${type}
|
|
123
|
+
</span>
|
|
124
|
+
`)}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
` : ''}
|
|
128
|
+
|
|
129
|
+
${allowedScopes.length > 0 ? html`
|
|
130
|
+
<div>
|
|
131
|
+
<div class="text-xs uppercase tracking-wide text-slate-400">Allowed Scopes</div>
|
|
132
|
+
<div class="mt-2 flex flex-wrap gap-2">
|
|
133
|
+
${allowedScopes.map(scope => html`
|
|
134
|
+
<span class="rounded-full bg-emerald-500/20 px-3 py-1 text-xs font-semibold text-emerald-200">
|
|
135
|
+
${scope}
|
|
136
|
+
</span>
|
|
137
|
+
`)}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
` : ''}
|
|
141
|
+
|
|
142
|
+
${client.createdAt ? html`
|
|
143
|
+
<div class="text-xs text-slate-400">
|
|
144
|
+
Created ${new Date(client.createdAt).toLocaleString()}
|
|
145
|
+
</div>
|
|
146
|
+
` : ''}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div class="mt-6 flex flex-wrap gap-3 border-t border-white/10 pt-4">
|
|
150
|
+
<form method="POST" action="/admin/clients/${client.id}/rotate-secret" class="inline-flex">
|
|
151
|
+
<button type="submit" class="${secondaryButtonClass}">
|
|
152
|
+
🔄 Rotate Secret
|
|
153
|
+
</button>
|
|
154
|
+
</form>
|
|
155
|
+
<form method="POST" action="/admin/clients/${client.id}/toggle-active" class="inline-flex">
|
|
156
|
+
<button type="submit" class="${client.active ? dangerButtonClass : successButtonClass}">
|
|
157
|
+
${client.active ? '🔴 Deactivate' : '🟢 Activate'}
|
|
158
|
+
</button>
|
|
159
|
+
</form>
|
|
160
|
+
</div>
|
|
161
|
+
</article>
|
|
162
|
+
`;
|
|
163
|
+
})}
|
|
164
|
+
</div>
|
|
165
|
+
`}
|
|
166
|
+
</section>
|
|
167
|
+
`;
|
|
168
|
+
|
|
169
|
+
return BaseLayout({
|
|
170
|
+
title: 'OAuth2 Clients - Admin',
|
|
171
|
+
content,
|
|
172
|
+
config,
|
|
173
|
+
user,
|
|
174
|
+
error,
|
|
175
|
+
success
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export default AdminClientsPage;
|