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.
Files changed (110) hide show
  1. package/README.md +25 -10
  2. package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +38653 -32291
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +218 -22
  7. package/src/concerns/id.js +90 -6
  8. package/src/concerns/index.js +2 -1
  9. package/src/concerns/password-hashing.js +150 -0
  10. package/src/database.class.js +6 -2
  11. package/src/plugins/api/auth/basic-auth.js +40 -10
  12. package/src/plugins/api/auth/index.js +49 -3
  13. package/src/plugins/api/auth/oauth2-auth.js +171 -0
  14. package/src/plugins/api/auth/oidc-auth.js +789 -0
  15. package/src/plugins/api/auth/oidc-client.js +462 -0
  16. package/src/plugins/api/auth/path-auth-matcher.js +284 -0
  17. package/src/plugins/api/concerns/event-emitter.js +134 -0
  18. package/src/plugins/api/concerns/failban-manager.js +651 -0
  19. package/src/plugins/api/concerns/guards-helpers.js +402 -0
  20. package/src/plugins/api/concerns/metrics-collector.js +346 -0
  21. package/src/plugins/api/index.js +510 -57
  22. package/src/plugins/api/middlewares/failban.js +305 -0
  23. package/src/plugins/api/middlewares/rate-limit.js +301 -0
  24. package/src/plugins/api/middlewares/request-id.js +74 -0
  25. package/src/plugins/api/middlewares/security-headers.js +120 -0
  26. package/src/plugins/api/middlewares/session-tracking.js +194 -0
  27. package/src/plugins/api/routes/auth-routes.js +119 -78
  28. package/src/plugins/api/routes/resource-routes.js +73 -30
  29. package/src/plugins/api/server.js +1139 -45
  30. package/src/plugins/api/utils/custom-routes.js +102 -0
  31. package/src/plugins/api/utils/guards.js +213 -0
  32. package/src/plugins/api/utils/mime-types.js +154 -0
  33. package/src/plugins/api/utils/openapi-generator.js +91 -12
  34. package/src/plugins/api/utils/path-matcher.js +173 -0
  35. package/src/plugins/api/utils/static-filesystem.js +262 -0
  36. package/src/plugins/api/utils/static-s3.js +231 -0
  37. package/src/plugins/api/utils/template-engine.js +188 -0
  38. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  39. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  40. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  41. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  42. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  43. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  44. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  45. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  46. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  47. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  48. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  49. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  50. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  51. package/src/plugins/cloud-inventory/index.js +20 -0
  52. package/src/plugins/cloud-inventory/registry.js +146 -0
  53. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  54. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  55. package/src/plugins/concerns/plugin-dependencies.js +62 -2
  56. package/src/plugins/eventual-consistency/analytics.js +1 -0
  57. package/src/plugins/eventual-consistency/consolidation.js +2 -2
  58. package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
  59. package/src/plugins/eventual-consistency/install.js +2 -2
  60. package/src/plugins/identity/README.md +335 -0
  61. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  62. package/src/plugins/identity/concerns/password.js +138 -0
  63. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  64. package/src/plugins/identity/concerns/token-generator.js +172 -0
  65. package/src/plugins/identity/email-service.js +422 -0
  66. package/src/plugins/identity/index.js +1052 -0
  67. package/src/plugins/identity/oauth2-server.js +1033 -0
  68. package/src/plugins/identity/oidc-discovery.js +285 -0
  69. package/src/plugins/identity/rsa-keys.js +323 -0
  70. package/src/plugins/identity/server.js +500 -0
  71. package/src/plugins/identity/session-manager.js +453 -0
  72. package/src/plugins/identity/ui/layouts/base.js +251 -0
  73. package/src/plugins/identity/ui/middleware.js +135 -0
  74. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  75. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  76. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  77. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  78. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  79. package/src/plugins/identity/ui/pages/consent.js +262 -0
  80. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  81. package/src/plugins/identity/ui/pages/login.js +144 -0
  82. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  83. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  84. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  85. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  86. package/src/plugins/identity/ui/pages/profile.js +361 -0
  87. package/src/plugins/identity/ui/pages/register.js +226 -0
  88. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  89. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  90. package/src/plugins/identity/ui/routes.js +2541 -0
  91. package/src/plugins/identity/ui/styles/main.css +465 -0
  92. package/src/plugins/index.js +4 -1
  93. package/src/plugins/ml/base-model.class.js +65 -16
  94. package/src/plugins/ml/classification-model.class.js +1 -1
  95. package/src/plugins/ml/timeseries-model.class.js +3 -1
  96. package/src/plugins/ml.plugin.js +584 -31
  97. package/src/plugins/shared/error-handler.js +147 -0
  98. package/src/plugins/shared/index.js +9 -0
  99. package/src/plugins/shared/middlewares/compression.js +117 -0
  100. package/src/plugins/shared/middlewares/cors.js +49 -0
  101. package/src/plugins/shared/middlewares/index.js +11 -0
  102. package/src/plugins/shared/middlewares/logging.js +54 -0
  103. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  104. package/src/plugins/shared/middlewares/security.js +158 -0
  105. package/src/plugins/shared/response-formatter.js +264 -0
  106. package/src/plugins/state-machine.plugin.js +57 -2
  107. package/src/resource.class.js +140 -12
  108. package/src/schema.class.js +30 -1
  109. package/src/validator.class.js +57 -6
  110. 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} usersResource - s3db.js users resource
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(usersResource, config = {}) {
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 { username, password, email, role = 'user' } = data;
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: 'username', message: 'Username is required' },
40
- { field: 'password', message: 'Password is required' }
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: 'password', message: 'Password must be at least 8 characters' }
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 existing = await usersResource.query({ username });
58
+ const queryFilter = { [usernameField]: username };
59
+ const existing = await authResource.query(queryFilter);
54
60
  if (existing && existing.length > 0) {
55
- const response = formatter.error('Username already exists', {
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
- const user = await usersResource.insert({
64
- username,
65
- password, // Will be auto-encrypted by schema (secret field)
66
- email,
67
- role,
68
- active: true,
69
- apiKey: generateApiKey(),
70
- createdAt: new Date().toISOString()
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
- // Generate JWT token
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
- { userId: user.id, username: user.username, role: user.role },
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 { password: _, ...userWithoutPassword } = user;
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
- app.post('/login', asyncHandler(async (c) => {
97
- const data = await c.req.json();
98
- const { username, password } = data;
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
- // Validate input
101
- if (!username || !password) {
102
- const response = formatter.unauthorized('Username and password are required');
103
- return c.json(response, response._status);
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
- // Find user
107
- const users = await usersResource.query({ username });
108
- if (!users || users.length === 0) {
109
- const response = formatter.unauthorized('Invalid credentials');
110
- return c.json(response, response._status);
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
- const user = users[0];
134
+ const user = users[0];
114
135
 
115
- if (!user.active) {
116
- const response = formatter.unauthorized('User account is inactive');
117
- return c.json(response, response._status);
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
- // Verify password (decrypt and compare)
121
- // Note: In production, use proper password hashing (bcrypt, argon2)
122
- const [ok, err, decrypted] = await tryFn(() =>
123
- user.password // Password is already decrypted by autoDecrypt
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
- // For secret fields, we need to manually decrypt if autoDecrypt is off
127
- // But by default autoDecrypt is true, so user.password should be plain text here
128
- // Let's just compare directly since schema handles encryption/decryption
129
- const isValid = user.password === password;
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
- if (!isValid) {
132
- const response = formatter.unauthorized('Invalid credentials');
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
- // Update last login
137
- await usersResource.update(user.id, {
138
- lastLoginAt: new Date().toISOString()
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
- // Generate JWT token
142
- let token = null;
143
- if (jwtSecret) {
144
- token = createToken(
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
- // Remove sensitive data from response
152
- const { password: _, ...userWithoutPassword } = user;
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
- const response = formatter.success({
155
- user: userWithoutPassword,
156
- token,
157
- expiresIn: jwtExpiresIn
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
- return c.json(response, response._status);
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) {