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,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Headers Middleware
|
|
3
|
+
*
|
|
4
|
+
* Adds standard security headers to all responses for enhanced protection.
|
|
5
|
+
* Helps prevent common web vulnerabilities like XSS, clickjacking, and MIME sniffing.
|
|
6
|
+
*
|
|
7
|
+
* Headers included:
|
|
8
|
+
* - Content-Security-Policy (CSP): Prevents XSS and data injection attacks
|
|
9
|
+
* - Strict-Transport-Security (HSTS): Forces HTTPS connections
|
|
10
|
+
* - X-Frame-Options: Prevents clickjacking
|
|
11
|
+
* - X-Content-Type-Options: Prevents MIME sniffing
|
|
12
|
+
* - Referrer-Policy: Controls referer information
|
|
13
|
+
* - X-XSS-Protection: Legacy XSS protection (for older browsers)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* import { createSecurityHeadersMiddleware } from './middlewares/security-headers.js';
|
|
17
|
+
*
|
|
18
|
+
* const middleware = createSecurityHeadersMiddleware({
|
|
19
|
+
* headers: {
|
|
20
|
+
* csp: "default-src 'self'; script-src 'self' 'unsafe-inline'",
|
|
21
|
+
* hsts: { maxAge: 31536000, includeSubDomains: true },
|
|
22
|
+
* xFrameOptions: 'DENY'
|
|
23
|
+
* }
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* app.use('*', middleware);
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create security headers middleware
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} config - Security configuration
|
|
33
|
+
* @param {Object} config.headers - Header configuration
|
|
34
|
+
* @param {string} config.headers.csp - Content Security Policy
|
|
35
|
+
* @param {Object} config.headers.hsts - HSTS configuration
|
|
36
|
+
* @param {number} config.headers.hsts.maxAge - HSTS max age in seconds
|
|
37
|
+
* @param {boolean} config.headers.hsts.includeSubDomains - Include subdomains
|
|
38
|
+
* @param {boolean} config.headers.hsts.preload - Enable HSTS preload
|
|
39
|
+
* @param {string} config.headers.xFrameOptions - X-Frame-Options value (DENY, SAMEORIGIN, ALLOW-FROM)
|
|
40
|
+
* @param {string} config.headers.xContentTypeOptions - X-Content-Type-Options (nosniff)
|
|
41
|
+
* @param {string} config.headers.referrerPolicy - Referrer-Policy value
|
|
42
|
+
* @param {string} config.headers.xssProtection - X-XSS-Protection value
|
|
43
|
+
* @returns {Function} Hono middleware
|
|
44
|
+
*/
|
|
45
|
+
export function createSecurityHeadersMiddleware(config = {}) {
|
|
46
|
+
const defaults = {
|
|
47
|
+
csp: "default-src 'self'",
|
|
48
|
+
hsts: { maxAge: 31536000, includeSubDomains: true, preload: false },
|
|
49
|
+
xFrameOptions: 'DENY',
|
|
50
|
+
xContentTypeOptions: 'nosniff',
|
|
51
|
+
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
52
|
+
xssProtection: '1; mode=block',
|
|
53
|
+
permissionsPolicy: 'geolocation=(), microphone=(), camera=()'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const settings = {
|
|
57
|
+
...defaults,
|
|
58
|
+
...(config.headers || {})
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Merge HSTS settings
|
|
62
|
+
if (config.headers?.hsts && typeof config.headers.hsts === 'object') {
|
|
63
|
+
settings.hsts = {
|
|
64
|
+
...defaults.hsts,
|
|
65
|
+
...config.headers.hsts
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return async (c, next) => {
|
|
70
|
+
// Content Security Policy
|
|
71
|
+
if (settings.csp) {
|
|
72
|
+
c.header('Content-Security-Policy', settings.csp);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// HTTP Strict Transport Security
|
|
76
|
+
if (settings.hsts) {
|
|
77
|
+
const hsts = settings.hsts;
|
|
78
|
+
let hstsValue = `max-age=${hsts.maxAge}`;
|
|
79
|
+
|
|
80
|
+
if (hsts.includeSubDomains) {
|
|
81
|
+
hstsValue += '; includeSubDomains';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (hsts.preload) {
|
|
85
|
+
hstsValue += '; preload';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
c.header('Strict-Transport-Security', hstsValue);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// X-Frame-Options
|
|
92
|
+
if (settings.xFrameOptions) {
|
|
93
|
+
c.header('X-Frame-Options', settings.xFrameOptions);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// X-Content-Type-Options
|
|
97
|
+
if (settings.xContentTypeOptions) {
|
|
98
|
+
c.header('X-Content-Type-Options', settings.xContentTypeOptions);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Referrer-Policy
|
|
102
|
+
if (settings.referrerPolicy) {
|
|
103
|
+
c.header('Referrer-Policy', settings.referrerPolicy);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// X-XSS-Protection (legacy, but still useful for older browsers)
|
|
107
|
+
if (settings.xssProtection) {
|
|
108
|
+
c.header('X-XSS-Protection', settings.xssProtection);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Permissions-Policy (formerly Feature-Policy)
|
|
112
|
+
if (settings.permissionsPolicy) {
|
|
113
|
+
c.header('Permissions-Policy', settings.permissionsPolicy);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await next();
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default createSecurityHeadersMiddleware;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Tracking Middleware
|
|
3
|
+
*
|
|
4
|
+
* Tracks user sessions for analytics and monitoring purposes.
|
|
5
|
+
* Creates persistent session IDs stored in encrypted cookies.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Encrypted session IDs (AES-256-GCM)
|
|
9
|
+
* - Optional database storage
|
|
10
|
+
* - Auto-update on each request
|
|
11
|
+
* - Custom session enrichment
|
|
12
|
+
* - IP, User-Agent, Referer tracking
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* import { createSessionTrackingMiddleware } from './middlewares/session-tracking.js';
|
|
16
|
+
*
|
|
17
|
+
* const middleware = createSessionTrackingMiddleware({
|
|
18
|
+
* enabled: true,
|
|
19
|
+
* resource: 'sessions',
|
|
20
|
+
* cookieName: 'session_id',
|
|
21
|
+
* passphrase: process.env.SESSION_SECRET,
|
|
22
|
+
* updateOnRequest: true,
|
|
23
|
+
* enrichSession: async ({ session, context }) => ({
|
|
24
|
+
* userAgent: context.req.header('user-agent'),
|
|
25
|
+
* ip: context.req.header('x-forwarded-for')
|
|
26
|
+
* })
|
|
27
|
+
* }, db);
|
|
28
|
+
*
|
|
29
|
+
* app.use('*', middleware);
|
|
30
|
+
*
|
|
31
|
+
* // In route handlers:
|
|
32
|
+
* app.get('/r/:id', async (c) => {
|
|
33
|
+
* const sessionId = c.get('sessionId');
|
|
34
|
+
* const session = c.get('session');
|
|
35
|
+
* console.log('Session:', sessionId);
|
|
36
|
+
* });
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { encrypt, decrypt } from '../../../concerns/crypto.js';
|
|
40
|
+
import { idGenerator } from '../../../concerns/id.js';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create session tracking middleware
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} config - Session configuration
|
|
46
|
+
* @param {boolean} config.enabled - Enable session tracking (default: false)
|
|
47
|
+
* @param {string} config.resource - Resource name for DB storage (optional)
|
|
48
|
+
* @param {string} config.cookieName - Cookie name (default: 'session_id')
|
|
49
|
+
* @param {number} config.cookieMaxAge - Cookie max age in ms (default: 30 days)
|
|
50
|
+
* @param {boolean} config.cookieSecure - Secure flag (default: production mode)
|
|
51
|
+
* @param {string} config.cookieSameSite - SameSite policy (default: 'Strict')
|
|
52
|
+
* @param {boolean} config.updateOnRequest - Update session on each request (default: true)
|
|
53
|
+
* @param {string} config.passphrase - Encryption passphrase (required)
|
|
54
|
+
* @param {Function} config.enrichSession - Custom session enrichment function
|
|
55
|
+
* @param {Object} db - Database instance
|
|
56
|
+
* @returns {Function} Hono middleware
|
|
57
|
+
*/
|
|
58
|
+
export function createSessionTrackingMiddleware(config = {}, db) {
|
|
59
|
+
const {
|
|
60
|
+
enabled = false,
|
|
61
|
+
resource = null,
|
|
62
|
+
cookieName = 'session_id',
|
|
63
|
+
cookieMaxAge = 2592000000, // 30 days
|
|
64
|
+
cookieSecure = process.env.NODE_ENV === 'production',
|
|
65
|
+
cookieSameSite = 'Strict',
|
|
66
|
+
updateOnRequest = true,
|
|
67
|
+
passphrase = null,
|
|
68
|
+
enrichSession = null
|
|
69
|
+
} = config;
|
|
70
|
+
|
|
71
|
+
// If disabled, return no-op middleware
|
|
72
|
+
if (!enabled) {
|
|
73
|
+
return async (c, next) => await next();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Validate required config
|
|
77
|
+
if (!passphrase) {
|
|
78
|
+
throw new Error('sessionTracking.passphrase is required when sessionTracking.enabled = true');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Get sessions resource if configured
|
|
82
|
+
const sessionsResource = resource && db ? db.resources[resource] : null;
|
|
83
|
+
|
|
84
|
+
return async (c, next) => {
|
|
85
|
+
let session = null;
|
|
86
|
+
let sessionId = null;
|
|
87
|
+
let isNewSession = false;
|
|
88
|
+
|
|
89
|
+
// 1. Check if session cookie exists
|
|
90
|
+
const sessionCookie = c.req.cookie(cookieName);
|
|
91
|
+
|
|
92
|
+
if (sessionCookie) {
|
|
93
|
+
try {
|
|
94
|
+
// Decrypt session ID
|
|
95
|
+
sessionId = await decrypt(sessionCookie, passphrase);
|
|
96
|
+
|
|
97
|
+
// Load from DB if resource configured
|
|
98
|
+
if (sessionsResource) {
|
|
99
|
+
const exists = await sessionsResource.exists(sessionId);
|
|
100
|
+
if (exists) {
|
|
101
|
+
session = await sessionsResource.get(sessionId);
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
// No DB storage - create minimal session object
|
|
105
|
+
session = { id: sessionId };
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error('[SessionTracking] Failed to decrypt cookie:', err.message);
|
|
109
|
+
// Will create new session below
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. Create new session if needed
|
|
114
|
+
if (!session) {
|
|
115
|
+
isNewSession = true;
|
|
116
|
+
sessionId = idGenerator();
|
|
117
|
+
|
|
118
|
+
const sessionData = {
|
|
119
|
+
id: sessionId,
|
|
120
|
+
userAgent: c.req.header('user-agent') || null,
|
|
121
|
+
ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || null,
|
|
122
|
+
referer: c.req.header('referer') || null,
|
|
123
|
+
createdAt: new Date().toISOString(),
|
|
124
|
+
lastSeenAt: new Date().toISOString()
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Enrich with custom data
|
|
128
|
+
if (enrichSession && typeof enrichSession === 'function') {
|
|
129
|
+
try {
|
|
130
|
+
const enriched = await enrichSession({ session: sessionData, context: c });
|
|
131
|
+
if (enriched && typeof enriched === 'object') {
|
|
132
|
+
Object.assign(sessionData, enriched);
|
|
133
|
+
}
|
|
134
|
+
} catch (enrichErr) {
|
|
135
|
+
console.error('[SessionTracking] enrichSession failed:', enrichErr.message);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Save to DB if resource configured
|
|
140
|
+
if (sessionsResource) {
|
|
141
|
+
try {
|
|
142
|
+
session = await sessionsResource.insert(sessionData);
|
|
143
|
+
} catch (insertErr) {
|
|
144
|
+
console.error('[SessionTracking] Failed to insert session:', insertErr.message);
|
|
145
|
+
session = sessionData; // Use in-memory fallback
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
session = sessionData;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 3. Update session on each request (if enabled and not new)
|
|
153
|
+
else if (updateOnRequest && !isNewSession && sessionsResource) {
|
|
154
|
+
const updates = {
|
|
155
|
+
lastSeenAt: new Date().toISOString(),
|
|
156
|
+
lastUserAgent: c.req.header('user-agent') || null,
|
|
157
|
+
lastIp: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || null
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Fire-and-forget update (don't block request)
|
|
161
|
+
sessionsResource.update(sessionId, updates).catch((updateErr) => {
|
|
162
|
+
console.error('[SessionTracking] Failed to update session:', updateErr.message);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Update local copy
|
|
166
|
+
Object.assign(session, updates);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 4. Set/refresh cookie
|
|
170
|
+
try {
|
|
171
|
+
const encryptedSessionId = await encrypt(sessionId, passphrase);
|
|
172
|
+
|
|
173
|
+
c.header(
|
|
174
|
+
'Set-Cookie',
|
|
175
|
+
`${cookieName}=${encryptedSessionId}; ` +
|
|
176
|
+
`Max-Age=${Math.floor(cookieMaxAge / 1000)}; ` +
|
|
177
|
+
`Path=/; ` +
|
|
178
|
+
`HttpOnly; ` +
|
|
179
|
+
(cookieSecure ? 'Secure; ' : '') +
|
|
180
|
+
`SameSite=${cookieSameSite}`
|
|
181
|
+
);
|
|
182
|
+
} catch (encryptErr) {
|
|
183
|
+
console.error('[SessionTracking] Failed to encrypt session ID:', encryptErr.message);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 5. Expose to context
|
|
187
|
+
c.set('sessionId', sessionId);
|
|
188
|
+
c.set('session', session);
|
|
189
|
+
|
|
190
|
+
await next();
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export default createSessionTrackingMiddleware;
|
|
@@ -14,13 +14,16 @@ import tryFn from '../../../concerns/try-fn.js';
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Create authentication routes
|
|
17
|
-
* @param {Object}
|
|
17
|
+
* @param {Object} authResource - s3db.js resource that manages authentication
|
|
18
18
|
* @param {Object} config - Auth configuration
|
|
19
19
|
* @returns {Hono} Hono app with auth routes
|
|
20
20
|
*/
|
|
21
|
-
export function createAuthRoutes(
|
|
21
|
+
export function createAuthRoutes(authResource, config = {}) {
|
|
22
22
|
const app = new Hono();
|
|
23
23
|
const {
|
|
24
|
+
driver, // 'jwt' or 'basic'
|
|
25
|
+
usernameField = 'email', // Field name for username (default: 'email')
|
|
26
|
+
passwordField = 'password', // Field name for password (default: 'password')
|
|
24
27
|
jwtSecret,
|
|
25
28
|
jwtExpiresIn = '7d',
|
|
26
29
|
passphrase = 'secret',
|
|
@@ -31,134 +34,172 @@ export function createAuthRoutes(usersResource, config = {}) {
|
|
|
31
34
|
if (allowRegistration) {
|
|
32
35
|
app.post('/register', asyncHandler(async (c) => {
|
|
33
36
|
const data = await c.req.json();
|
|
34
|
-
const
|
|
37
|
+
const username = data[usernameField];
|
|
38
|
+
const password = data[passwordField];
|
|
39
|
+
const role = data.role || 'user';
|
|
35
40
|
|
|
36
41
|
// Validate input
|
|
37
42
|
if (!username || !password) {
|
|
38
43
|
const response = formatter.validationError([
|
|
39
|
-
{ field:
|
|
40
|
-
{ field:
|
|
44
|
+
{ field: usernameField, message: `${usernameField} is required` },
|
|
45
|
+
{ field: passwordField, message: `${passwordField} is required` }
|
|
41
46
|
]);
|
|
42
47
|
return c.json(response, response._status);
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
if (password.length < 8) {
|
|
46
51
|
const response = formatter.validationError([
|
|
47
|
-
{ field:
|
|
52
|
+
{ field: passwordField, message: 'Password must be at least 8 characters' }
|
|
48
53
|
]);
|
|
49
54
|
return c.json(response, response._status);
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
// Check if username already exists
|
|
53
|
-
const
|
|
58
|
+
const queryFilter = { [usernameField]: username };
|
|
59
|
+
const existing = await authResource.query(queryFilter);
|
|
54
60
|
if (existing && existing.length > 0) {
|
|
55
|
-
const response = formatter.error(
|
|
61
|
+
const response = formatter.error(`${usernameField} already exists`, {
|
|
56
62
|
status: 409,
|
|
57
63
|
code: 'CONFLICT'
|
|
58
64
|
});
|
|
59
65
|
return c.json(response, response._status);
|
|
60
66
|
}
|
|
61
67
|
|
|
62
|
-
// Create user
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
// Create user with dynamic fields
|
|
69
|
+
// Only include fields from request + required auth fields
|
|
70
|
+
const { id, ...dataWithoutId } = data; // Exclude id from request data
|
|
71
|
+
const userData = {
|
|
72
|
+
...dataWithoutId, // Include all fields from request except id
|
|
73
|
+
[usernameField]: username, // Override to ensure correct value
|
|
74
|
+
[passwordField]: password // Will be auto-encrypted by schema (secret field)
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Add optional fields only if not provided
|
|
78
|
+
if (!userData.role) {
|
|
79
|
+
userData.role = role;
|
|
80
|
+
}
|
|
81
|
+
if (userData.active === undefined) {
|
|
82
|
+
userData.active = true;
|
|
83
|
+
}
|
|
72
84
|
|
|
73
|
-
|
|
85
|
+
const user = await authResource.insert(userData);
|
|
86
|
+
|
|
87
|
+
// Generate JWT token (only for JWT driver)
|
|
74
88
|
let token = null;
|
|
75
|
-
if (jwtSecret) {
|
|
89
|
+
if (driver === 'jwt' && jwtSecret) {
|
|
76
90
|
token = createToken(
|
|
77
|
-
{
|
|
91
|
+
{
|
|
92
|
+
userId: user.id,
|
|
93
|
+
[usernameField]: user[usernameField],
|
|
94
|
+
role: user.role
|
|
95
|
+
},
|
|
78
96
|
jwtSecret,
|
|
79
97
|
jwtExpiresIn
|
|
80
98
|
);
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
// Remove sensitive data from response
|
|
84
|
-
const {
|
|
102
|
+
const { [passwordField]: _, ...userWithoutPassword } = user;
|
|
85
103
|
|
|
86
104
|
const response = formatter.created({
|
|
87
105
|
user: userWithoutPassword,
|
|
88
|
-
token
|
|
106
|
+
...(token && { token }) // Only include token if JWT driver
|
|
89
107
|
}, `/auth/users/${user.id}`);
|
|
90
108
|
|
|
91
109
|
return c.json(response, response._status);
|
|
92
110
|
}));
|
|
93
111
|
}
|
|
94
112
|
|
|
95
|
-
// POST /auth/login - Login with username/password
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
// POST /auth/login - Login with username/password (JWT driver only)
|
|
114
|
+
if (driver === 'jwt') {
|
|
115
|
+
app.post('/login', asyncHandler(async (c) => {
|
|
116
|
+
const data = await c.req.json();
|
|
117
|
+
const username = data[usernameField];
|
|
118
|
+
const password = data[passwordField];
|
|
99
119
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
// Validate input
|
|
121
|
+
if (!username || !password) {
|
|
122
|
+
const response = formatter.unauthorized(`${usernameField} and ${passwordField} are required`);
|
|
123
|
+
return c.json(response, response._status);
|
|
124
|
+
}
|
|
105
125
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
126
|
+
// Find user by username field
|
|
127
|
+
const queryFilter = { [usernameField]: username };
|
|
128
|
+
const users = await authResource.query(queryFilter);
|
|
129
|
+
if (!users || users.length === 0) {
|
|
130
|
+
const response = formatter.unauthorized('Invalid credentials');
|
|
131
|
+
return c.json(response, response._status);
|
|
132
|
+
}
|
|
112
133
|
|
|
113
|
-
|
|
134
|
+
const user = users[0];
|
|
114
135
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
136
|
+
// Check if user is active
|
|
137
|
+
if (user.active !== undefined && !user.active) {
|
|
138
|
+
const response = formatter.unauthorized('User account is inactive');
|
|
139
|
+
return c.json(response, response._status);
|
|
140
|
+
}
|
|
119
141
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
);
|
|
142
|
+
// Verify password (compare with password field)
|
|
143
|
+
// For 'password' field type (bcrypt hash), use verifyPassword
|
|
144
|
+
// For 'secret' field type (AES encryption), compare directly
|
|
145
|
+
let isValid = false;
|
|
125
146
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
147
|
+
const storedPassword = user[passwordField];
|
|
148
|
+
if (!storedPassword) {
|
|
149
|
+
const response = formatter.unauthorized('Invalid credentials');
|
|
150
|
+
return c.json(response, response._status);
|
|
151
|
+
}
|
|
130
152
|
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
return c.json(response, response._status);
|
|
134
|
-
}
|
|
153
|
+
// Check if it's a bcrypt hash (starts with $ or is compacted 53 chars)
|
|
154
|
+
const isBcryptHash = storedPassword.startsWith('$') || (storedPassword.length === 53 && !storedPassword.includes(':'));
|
|
135
155
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
if (isBcryptHash) {
|
|
157
|
+
// Import verifyPassword for bcrypt hashes
|
|
158
|
+
const { verifyPassword } = await import('../../../concerns/password-hashing.js');
|
|
159
|
+
isValid = await verifyPassword(password, storedPassword);
|
|
160
|
+
} else {
|
|
161
|
+
// For encrypted/secret fields, direct comparison
|
|
162
|
+
isValid = storedPassword === password;
|
|
163
|
+
}
|
|
140
164
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
{ userId: user.id, username: user.username, role: user.role },
|
|
146
|
-
jwtSecret,
|
|
147
|
-
jwtExpiresIn
|
|
148
|
-
);
|
|
149
|
-
}
|
|
165
|
+
if (!isValid) {
|
|
166
|
+
const response = formatter.unauthorized('Invalid credentials');
|
|
167
|
+
return c.json(response, response._status);
|
|
168
|
+
}
|
|
150
169
|
|
|
151
|
-
|
|
152
|
-
|
|
170
|
+
// Update last login if field exists
|
|
171
|
+
if (user.lastLoginAt !== undefined) {
|
|
172
|
+
await authResource.update(user.id, {
|
|
173
|
+
lastLoginAt: new Date().toISOString()
|
|
174
|
+
});
|
|
175
|
+
}
|
|
153
176
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
177
|
+
// Generate JWT token
|
|
178
|
+
let token = null;
|
|
179
|
+
if (jwtSecret) {
|
|
180
|
+
token = createToken(
|
|
181
|
+
{
|
|
182
|
+
userId: user.id,
|
|
183
|
+
[usernameField]: user[usernameField],
|
|
184
|
+
role: user.role
|
|
185
|
+
},
|
|
186
|
+
jwtSecret,
|
|
187
|
+
jwtExpiresIn
|
|
188
|
+
);
|
|
189
|
+
}
|
|
159
190
|
|
|
160
|
-
|
|
161
|
-
|
|
191
|
+
// Remove sensitive data from response
|
|
192
|
+
const { [passwordField]: _, ...userWithoutPassword } = user;
|
|
193
|
+
|
|
194
|
+
const response = formatter.success({
|
|
195
|
+
user: userWithoutPassword,
|
|
196
|
+
token,
|
|
197
|
+
expiresIn: jwtExpiresIn
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return c.json(response, response._status);
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
162
203
|
|
|
163
204
|
// POST /auth/token/refresh - Refresh JWT token
|
|
164
205
|
if (jwtSecret) {
|