s3db.js 13.5.1 → 13.6.1
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 +89 -19
- package/dist/{s3db.cjs.js → s3db.cjs} +29780 -24384
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +24263 -18860
- package/dist/s3db.es.js.map +1 -1
- package/package.json +227 -21
- 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 +4 -0
- package/src/plugins/api/auth/basic-auth.js +23 -1
- 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/concerns/opengraph-helper.js +116 -0
- package/src/plugins/api/concerns/state-machine.js +288 -0
- package/src/plugins/api/index.js +514 -54
- 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 +23 -3
- package/src/plugins/api/routes/resource-routes.js +71 -29
- package/src/plugins/api/server.js +1017 -94
- 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 +44 -11
- 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 +262 -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 +61 -1
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- 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 +32 -7
- 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 +124 -32
- 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/tfstate/README.md +126 -126
- 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;
|
|
@@ -67,8 +67,9 @@ export function createAuthRoutes(authResource, config = {}) {
|
|
|
67
67
|
|
|
68
68
|
// Create user with dynamic fields
|
|
69
69
|
// Only include fields from request + required auth fields
|
|
70
|
+
const { id, ...dataWithoutId } = data; // Exclude id from request data
|
|
70
71
|
const userData = {
|
|
71
|
-
...
|
|
72
|
+
...dataWithoutId, // Include all fields from request except id
|
|
72
73
|
[usernameField]: username, // Override to ensure correct value
|
|
73
74
|
[passwordField]: password // Will be auto-encrypted by schema (secret field)
|
|
74
75
|
};
|
|
@@ -139,8 +140,27 @@ export function createAuthRoutes(authResource, config = {}) {
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
// Verify password (compare with password field)
|
|
142
|
-
//
|
|
143
|
-
|
|
143
|
+
// For 'password' field type (bcrypt hash), use verifyPassword
|
|
144
|
+
// For 'secret' field type (AES encryption), compare directly
|
|
145
|
+
let isValid = false;
|
|
146
|
+
|
|
147
|
+
const storedPassword = user[passwordField];
|
|
148
|
+
if (!storedPassword) {
|
|
149
|
+
const response = formatter.unauthorized('Invalid credentials');
|
|
150
|
+
return c.json(response, response._status);
|
|
151
|
+
}
|
|
152
|
+
|
|
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(':'));
|
|
155
|
+
|
|
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
|
+
}
|
|
144
164
|
|
|
145
165
|
if (!isValid) {
|
|
146
166
|
const response = formatter.unauthorized('Invalid credentials');
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { asyncHandler } from '../utils/error-handler.js';
|
|
8
8
|
import * as formatter from '../utils/response-formatter.js';
|
|
9
|
+
import { guardMiddleware } from '../utils/guards.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Parse custom route definition (e.g., "GET /healthcheck" or "async POST /custom")
|
|
@@ -59,12 +60,16 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
59
60
|
methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
|
|
60
61
|
customMiddleware = [],
|
|
61
62
|
enableValidation = true,
|
|
62
|
-
versionPrefix = '' // Empty string by default (calculated in server.js)
|
|
63
|
+
versionPrefix = '', // Empty string by default (calculated in server.js)
|
|
64
|
+
events = null // Event emitter for lifecycle hooks
|
|
63
65
|
} = config;
|
|
64
66
|
|
|
65
67
|
const resourceName = resource.name;
|
|
66
68
|
const basePath = versionPrefix ? `/${versionPrefix}/${resourceName}` : `/${resourceName}`;
|
|
67
69
|
|
|
70
|
+
// Get guards configuration from resource config
|
|
71
|
+
const guards = resource.config?.guards || null;
|
|
72
|
+
|
|
68
73
|
// Apply custom middleware
|
|
69
74
|
customMiddleware.forEach(middleware => {
|
|
70
75
|
app.use('*', middleware);
|
|
@@ -112,7 +117,7 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
112
117
|
|
|
113
118
|
// LIST - GET /{version}/{resource}
|
|
114
119
|
if (methods.includes('GET')) {
|
|
115
|
-
app.get('/', asyncHandler(async (c) => {
|
|
120
|
+
app.get('/', guardMiddleware(guards, 'list'), asyncHandler(async (c) => {
|
|
116
121
|
const query = c.req.query();
|
|
117
122
|
const limit = parseInt(query.limit) || 100;
|
|
118
123
|
const offset = parseInt(query.offset) || 0;
|
|
@@ -141,22 +146,23 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
141
146
|
|
|
142
147
|
// Use query if filters are present
|
|
143
148
|
if (Object.keys(filters).length > 0) {
|
|
144
|
-
|
|
145
|
-
items =
|
|
149
|
+
// Query with native offset support (efficient!)
|
|
150
|
+
items = await resource.query(filters, { limit, offset });
|
|
151
|
+
// Note: total is approximate (length of returned items)
|
|
152
|
+
// For exact total count with filters, would need separate count query
|
|
146
153
|
total = items.length;
|
|
147
154
|
} else if (partition && partitionValues) {
|
|
148
155
|
// Query specific partition
|
|
149
156
|
items = await resource.listPartition({
|
|
150
157
|
partition,
|
|
151
158
|
partitionValues,
|
|
152
|
-
limit
|
|
159
|
+
limit,
|
|
160
|
+
offset
|
|
153
161
|
});
|
|
154
|
-
items = items.slice(offset, offset + limit);
|
|
155
162
|
total = items.length;
|
|
156
163
|
} else {
|
|
157
164
|
// Regular list
|
|
158
|
-
items = await resource.list({ limit
|
|
159
|
-
items = items.slice(offset, offset + limit);
|
|
165
|
+
items = await resource.list({ limit, offset });
|
|
160
166
|
total = items.length;
|
|
161
167
|
}
|
|
162
168
|
|
|
@@ -177,7 +183,7 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
177
183
|
|
|
178
184
|
// GET ONE - GET /{version}/{resource}/:id
|
|
179
185
|
if (methods.includes('GET')) {
|
|
180
|
-
app.get('/:id', asyncHandler(async (c) => {
|
|
186
|
+
app.get('/:id', guardMiddleware(guards, 'get'), asyncHandler(async (c) => {
|
|
181
187
|
const id = c.req.param('id');
|
|
182
188
|
const query = c.req.query();
|
|
183
189
|
const partition = query.partition;
|
|
@@ -211,12 +217,22 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
211
217
|
|
|
212
218
|
// CREATE - POST /{version}/{resource}
|
|
213
219
|
if (methods.includes('POST')) {
|
|
214
|
-
app.post('/', asyncHandler(async (c) => {
|
|
220
|
+
app.post('/', guardMiddleware(guards, 'create'), asyncHandler(async (c) => {
|
|
215
221
|
const data = await c.req.json();
|
|
216
222
|
|
|
217
223
|
// Validation middleware will run if enabled
|
|
218
224
|
const item = await resource.insert(data);
|
|
219
225
|
|
|
226
|
+
// Emit resource:created event
|
|
227
|
+
if (events) {
|
|
228
|
+
events.emitResourceEvent('created', {
|
|
229
|
+
resource: resourceName,
|
|
230
|
+
id: item.id,
|
|
231
|
+
data: item,
|
|
232
|
+
user: c.get('user')
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
220
236
|
const location = `${basePath}/${item.id}`;
|
|
221
237
|
const response = formatter.created(item, location);
|
|
222
238
|
|
|
@@ -227,7 +243,7 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
227
243
|
|
|
228
244
|
// UPDATE (full) - PUT /{version}/{resource}/:id
|
|
229
245
|
if (methods.includes('PUT')) {
|
|
230
|
-
app.put('/:id', asyncHandler(async (c) => {
|
|
246
|
+
app.put('/:id', guardMiddleware(guards, 'update'), asyncHandler(async (c) => {
|
|
231
247
|
const id = c.req.param('id');
|
|
232
248
|
const data = await c.req.json();
|
|
233
249
|
|
|
@@ -241,6 +257,17 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
241
257
|
// Full update
|
|
242
258
|
const updated = await resource.update(id, data);
|
|
243
259
|
|
|
260
|
+
// Emit resource:updated event
|
|
261
|
+
if (events) {
|
|
262
|
+
events.emitResourceEvent('updated', {
|
|
263
|
+
resource: resourceName,
|
|
264
|
+
id: updated.id,
|
|
265
|
+
data: updated,
|
|
266
|
+
previous: existing,
|
|
267
|
+
user: c.get('user')
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
244
271
|
const response = formatter.success(updated);
|
|
245
272
|
return c.json(response, response._status);
|
|
246
273
|
}));
|
|
@@ -248,7 +275,7 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
248
275
|
|
|
249
276
|
// UPDATE (partial) - PATCH /{version}/{resource}/:id
|
|
250
277
|
if (methods.includes('PATCH')) {
|
|
251
|
-
app.patch('/:id', asyncHandler(async (c) => {
|
|
278
|
+
app.patch('/:id', guardMiddleware(guards, 'update'), asyncHandler(async (c) => {
|
|
252
279
|
const id = c.req.param('id');
|
|
253
280
|
const data = await c.req.json();
|
|
254
281
|
|
|
@@ -263,6 +290,18 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
263
290
|
const merged = { ...existing, ...data, id };
|
|
264
291
|
const updated = await resource.update(id, merged);
|
|
265
292
|
|
|
293
|
+
// Emit resource:updated event
|
|
294
|
+
if (events) {
|
|
295
|
+
events.emitResourceEvent('updated', {
|
|
296
|
+
resource: resourceName,
|
|
297
|
+
id: updated.id,
|
|
298
|
+
data: updated,
|
|
299
|
+
previous: existing,
|
|
300
|
+
partial: true,
|
|
301
|
+
user: c.get('user')
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
266
305
|
const response = formatter.success(updated);
|
|
267
306
|
return c.json(response, response._status);
|
|
268
307
|
}));
|
|
@@ -270,7 +309,7 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
270
309
|
|
|
271
310
|
// DELETE - DELETE /{version}/{resource}/:id
|
|
272
311
|
if (methods.includes('DELETE')) {
|
|
273
|
-
app.delete('/:id', asyncHandler(async (c) => {
|
|
312
|
+
app.delete('/:id', guardMiddleware(guards, 'delete'), asyncHandler(async (c) => {
|
|
274
313
|
const id = c.req.param('id');
|
|
275
314
|
|
|
276
315
|
// Check if exists
|
|
@@ -282,6 +321,16 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
282
321
|
|
|
283
322
|
await resource.delete(id);
|
|
284
323
|
|
|
324
|
+
// Emit resource:deleted event
|
|
325
|
+
if (events) {
|
|
326
|
+
events.emitResourceEvent('deleted', {
|
|
327
|
+
resource: resourceName,
|
|
328
|
+
id,
|
|
329
|
+
previous: existing,
|
|
330
|
+
user: c.get('user')
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
285
334
|
const response = formatter.noContent();
|
|
286
335
|
return c.json(response, response._status);
|
|
287
336
|
}));
|
|
@@ -292,22 +341,11 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
292
341
|
app.on('HEAD', '/', asyncHandler(async (c) => {
|
|
293
342
|
// Get statistics
|
|
294
343
|
const total = await resource.count();
|
|
344
|
+
const version = resource.config?.currentVersion || resource.version || 'v1';
|
|
295
345
|
|
|
296
|
-
//
|
|
297
|
-
// For large datasets, this might need optimization
|
|
298
|
-
const allItems = await resource.list({ limit: 1000 });
|
|
299
|
-
|
|
300
|
-
// Calculate statistics
|
|
301
|
-
const stats = {
|
|
302
|
-
total,
|
|
303
|
-
version: resource.config?.currentVersion || resource.version || 'v1'
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
// Add resource-specific stats
|
|
346
|
+
// Set resource metadata headers
|
|
307
347
|
c.header('X-Total-Count', total.toString());
|
|
308
|
-
c.header('X-Resource-Version',
|
|
309
|
-
|
|
310
|
-
// Add schema info
|
|
348
|
+
c.header('X-Resource-Version', version);
|
|
311
349
|
c.header('X-Schema-Fields', Object.keys(resource.config?.attributes || {}).length.toString());
|
|
312
350
|
|
|
313
351
|
return c.body(null, 200);
|
|
@@ -393,8 +431,12 @@ export function createRelationalRoutes(sourceResource, relationName, relationCon
|
|
|
393
431
|
|
|
394
432
|
// GET /{version}/{resource}/:id/{relation}
|
|
395
433
|
// Examples: GET /v1/users/user123/posts, GET /v1/users/user123/profile
|
|
396
|
-
|
|
397
|
-
|
|
434
|
+
// Note: The :id param comes from parent route mounting (see server.js:469)
|
|
435
|
+
app.get('/', asyncHandler(async (c) => {
|
|
436
|
+
// Get parent route's :id param
|
|
437
|
+
const pathParts = c.req.path.split('/');
|
|
438
|
+
const relationNameIndex = pathParts.lastIndexOf(relationName);
|
|
439
|
+
const id = pathParts[relationNameIndex - 1];
|
|
398
440
|
const query = c.req.query();
|
|
399
441
|
|
|
400
442
|
// Check if source resource exists
|