s3db.js 13.6.1 → 14.0.2
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 +56 -15
- package/dist/s3db.cjs +72446 -39022
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72172 -38790
- package/dist/s3db.es.js.map +1 -1
- package/mcp/lib/base-handler.js +157 -0
- package/mcp/lib/handlers/connection-handler.js +280 -0
- package/mcp/lib/handlers/query-handler.js +533 -0
- package/mcp/lib/handlers/resource-handler.js +428 -0
- package/mcp/lib/tool-registry.js +336 -0
- package/mcp/lib/tools/connection-tools.js +161 -0
- package/mcp/lib/tools/query-tools.js +267 -0
- package/mcp/lib/tools/resource-tools.js +404 -0
- package/package.json +85 -50
- package/src/clients/memory-client.class.js +346 -191
- package/src/clients/memory-storage.class.js +300 -84
- package/src/clients/s3-client.class.js +7 -6
- package/src/concerns/geo-encoding.js +19 -2
- package/src/concerns/ip.js +59 -9
- package/src/concerns/money.js +8 -1
- package/src/concerns/password-hashing.js +49 -8
- package/src/concerns/plugin-storage.js +186 -18
- package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
- package/src/database.class.js +139 -29
- package/src/errors.js +332 -42
- package/src/plugins/api/auth/oidc-auth.js +66 -17
- package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
- package/src/plugins/api/auth/strategies/factory.class.js +63 -0
- package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
- package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
- package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
- package/src/plugins/api/concerns/failban-manager.js +106 -57
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/index.js +168 -40
- package/src/plugins/api/routes/auth-routes.js +198 -30
- package/src/plugins/api/routes/resource-routes.js +19 -4
- package/src/plugins/api/server/health-manager.class.js +163 -0
- package/src/plugins/api/server/middleware-chain.class.js +310 -0
- package/src/plugins/api/server/router.class.js +472 -0
- package/src/plugins/api/server.js +280 -1303
- package/src/plugins/api/utils/custom-routes.js +17 -5
- package/src/plugins/api/utils/guards.js +76 -17
- package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
- package/src/plugins/api/utils/openapi-generator.js +7 -6
- package/src/plugins/audit.plugin.js +30 -8
- package/src/plugins/backup.plugin.js +110 -14
- package/src/plugins/cache/cache.class.js +22 -5
- package/src/plugins/cache/filesystem-cache.class.js +116 -19
- package/src/plugins/cache/memory-cache.class.js +211 -57
- package/src/plugins/cache/multi-tier-cache.class.js +371 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
- package/src/plugins/cache/redis-cache.class.js +552 -0
- package/src/plugins/cache/s3-cache.class.js +17 -8
- package/src/plugins/cache.plugin.js +176 -61
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
- package/src/plugins/cloud-inventory/index.js +29 -8
- package/src/plugins/cloud-inventory/registry.js +64 -42
- package/src/plugins/cloud-inventory.plugin.js +240 -138
- package/src/plugins/concerns/plugin-dependencies.js +54 -0
- package/src/plugins/concerns/resource-names.js +100 -0
- package/src/plugins/consumers/index.js +10 -2
- package/src/plugins/consumers/sqs-consumer.js +12 -2
- package/src/plugins/cookie-farm-suite.plugin.js +278 -0
- package/src/plugins/cookie-farm.errors.js +73 -0
- package/src/plugins/cookie-farm.plugin.js +869 -0
- package/src/plugins/costs.plugin.js +7 -1
- package/src/plugins/eventual-consistency/analytics.js +94 -19
- package/src/plugins/eventual-consistency/config.js +15 -7
- package/src/plugins/eventual-consistency/consolidation.js +29 -11
- package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
- package/src/plugins/eventual-consistency/helpers.js +39 -14
- package/src/plugins/eventual-consistency/install.js +21 -2
- package/src/plugins/eventual-consistency/utils.js +32 -10
- package/src/plugins/fulltext.plugin.js +38 -11
- package/src/plugins/geo.plugin.js +61 -9
- package/src/plugins/identity/concerns/config.js +61 -0
- package/src/plugins/identity/concerns/mfa-manager.js +15 -2
- package/src/plugins/identity/concerns/rate-limit.js +124 -0
- package/src/plugins/identity/concerns/resource-schemas.js +9 -1
- package/src/plugins/identity/concerns/token-generator.js +29 -4
- package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
- package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
- package/src/plugins/identity/drivers/index.js +18 -0
- package/src/plugins/identity/drivers/password-driver.js +122 -0
- package/src/plugins/identity/email-service.js +17 -2
- package/src/plugins/identity/index.js +413 -69
- package/src/plugins/identity/oauth2-server.js +413 -30
- package/src/plugins/identity/oidc-discovery.js +16 -8
- package/src/plugins/identity/rsa-keys.js +115 -35
- package/src/plugins/identity/server.js +166 -45
- package/src/plugins/identity/session-manager.js +53 -7
- package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
- package/src/plugins/identity/ui/routes.js +363 -255
- package/src/plugins/importer/index.js +153 -20
- package/src/plugins/index.js +9 -2
- package/src/plugins/kubernetes-inventory/index.js +6 -0
- package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
- package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
- package/src/plugins/kubernetes-inventory.plugin.js +980 -0
- package/src/plugins/metrics.plugin.js +64 -16
- package/src/plugins/ml/base-model.class.js +25 -15
- package/src/plugins/ml/regression-model.class.js +1 -1
- package/src/plugins/ml.errors.js +57 -25
- package/src/plugins/ml.plugin.js +28 -4
- package/src/plugins/namespace.js +210 -0
- package/src/plugins/plugin.class.js +180 -8
- package/src/plugins/puppeteer/console-monitor.js +729 -0
- package/src/plugins/puppeteer/cookie-manager.js +492 -0
- package/src/plugins/puppeteer/network-monitor.js +816 -0
- package/src/plugins/puppeteer/performance-manager.js +746 -0
- package/src/plugins/puppeteer/proxy-manager.js +478 -0
- package/src/plugins/puppeteer/stealth-manager.js +556 -0
- package/src/plugins/puppeteer.errors.js +81 -0
- package/src/plugins/puppeteer.plugin.js +1327 -0
- package/src/plugins/queue-consumer.plugin.js +69 -14
- package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
- package/src/plugins/recon/concerns/command-runner.js +148 -0
- package/src/plugins/recon/concerns/diff-detector.js +372 -0
- package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
- package/src/plugins/recon/concerns/process-manager.js +338 -0
- package/src/plugins/recon/concerns/report-generator.js +478 -0
- package/src/plugins/recon/concerns/security-analyzer.js +571 -0
- package/src/plugins/recon/concerns/target-normalizer.js +68 -0
- package/src/plugins/recon/config/defaults.js +321 -0
- package/src/plugins/recon/config/resources.js +370 -0
- package/src/plugins/recon/index.js +778 -0
- package/src/plugins/recon/managers/dependency-manager.js +174 -0
- package/src/plugins/recon/managers/scheduler-manager.js +179 -0
- package/src/plugins/recon/managers/storage-manager.js +745 -0
- package/src/plugins/recon/managers/target-manager.js +274 -0
- package/src/plugins/recon/stages/asn-stage.js +314 -0
- package/src/plugins/recon/stages/certificate-stage.js +84 -0
- package/src/plugins/recon/stages/dns-stage.js +107 -0
- package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
- package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
- package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
- package/src/plugins/recon/stages/http-stage.js +89 -0
- package/src/plugins/recon/stages/latency-stage.js +148 -0
- package/src/plugins/recon/stages/massdns-stage.js +302 -0
- package/src/plugins/recon/stages/osint-stage.js +1373 -0
- package/src/plugins/recon/stages/ports-stage.js +169 -0
- package/src/plugins/recon/stages/screenshot-stage.js +94 -0
- package/src/plugins/recon/stages/secrets-stage.js +514 -0
- package/src/plugins/recon/stages/subdomains-stage.js +295 -0
- package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
- package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
- package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
- package/src/plugins/recon/stages/whois-stage.js +349 -0
- package/src/plugins/recon.plugin.js +75 -0
- package/src/plugins/recon.plugin.js.backup +2635 -0
- package/src/plugins/relation.errors.js +87 -14
- package/src/plugins/replicator.plugin.js +514 -137
- package/src/plugins/replicators/base-replicator.class.js +89 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
- package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mysql-replicator.class.js +52 -17
- package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
- package/src/plugins/replicators/postgres-replicator.class.js +62 -27
- package/src/plugins/replicators/s3db-replicator.class.js +25 -18
- package/src/plugins/replicators/schema-sync.helper.js +3 -3
- package/src/plugins/replicators/sqs-replicator.class.js +8 -2
- package/src/plugins/replicators/turso-replicator.class.js +23 -3
- package/src/plugins/replicators/webhook-replicator.class.js +42 -4
- package/src/plugins/s3-queue.plugin.js +464 -65
- package/src/plugins/scheduler.plugin.js +20 -6
- package/src/plugins/state-machine.plugin.js +40 -9
- package/src/plugins/tfstate/base-driver.js +28 -4
- package/src/plugins/tfstate/errors.js +65 -10
- package/src/plugins/tfstate/filesystem-driver.js +52 -8
- package/src/plugins/tfstate/index.js +163 -90
- package/src/plugins/tfstate/s3-driver.js +64 -6
- package/src/plugins/ttl.plugin.js +72 -17
- package/src/plugins/vector/distances.js +18 -12
- package/src/plugins/vector/kmeans.js +26 -4
- package/src/resource.class.js +115 -19
- package/src/testing/factory.class.js +20 -3
- package/src/testing/seeder.class.js +7 -1
- package/src/clients/memory-client.md +0 -917
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
|
@@ -25,6 +25,7 @@ import { generateAuthCode } from '../oidc-discovery.js';
|
|
|
25
25
|
import { tryFn } from '../../../concerns/try-fn.js';
|
|
26
26
|
import { sessionAuth, adminOnly } from './middleware.js';
|
|
27
27
|
import { idGenerator } from '../../../concerns/id.js';
|
|
28
|
+
import { createRedirectRateLimitMiddleware } from '../concerns/rate-limit.js';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Get page component (custom or default)
|
|
@@ -47,6 +48,38 @@ export function registerUIRoutes(app, plugin) {
|
|
|
47
48
|
const customPages = config.ui.customPages || {};
|
|
48
49
|
const failbanConfig = config.failban || {};
|
|
49
50
|
const accountLockoutConfig = config.accountLockout || {};
|
|
51
|
+
const userAttributes = plugin.config?.resources?.users?.mergedConfig?.attributes || {};
|
|
52
|
+
const supportsStatusField = Object.prototype.hasOwnProperty.call(userAttributes, 'status');
|
|
53
|
+
const keyManager = plugin.oauth2Server?.keyManager || null;
|
|
54
|
+
|
|
55
|
+
const createMfaChallengeToken = (user, rememberFlag) => {
|
|
56
|
+
if (!keyManager) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return keyManager.createToken({
|
|
61
|
+
type: 'mfa_challenge',
|
|
62
|
+
userId: user.id,
|
|
63
|
+
email: user.email,
|
|
64
|
+
remember: rememberFlag === '1'
|
|
65
|
+
}, '5m');
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const verifyMfaChallengeToken = async (token) => {
|
|
69
|
+
if (!keyManager || !token) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const verified = await keyManager.verifyToken(token);
|
|
75
|
+
if (!verified || verified.payload?.type !== 'mfa_challenge') {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return verified.payload;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
50
83
|
|
|
51
84
|
// Create UI config object with registration settings
|
|
52
85
|
const uiConfig = {
|
|
@@ -101,63 +134,105 @@ export function registerUIRoutes(app, plugin) {
|
|
|
101
134
|
}));
|
|
102
135
|
});
|
|
103
136
|
|
|
137
|
+
const getClientIp = (c) =>
|
|
138
|
+
c.get('clientIp') ||
|
|
139
|
+
c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ||
|
|
140
|
+
c.req.header('x-real-ip') ||
|
|
141
|
+
'unknown';
|
|
142
|
+
|
|
143
|
+
const buildRateLimitRedirect = (retryAfter) => {
|
|
144
|
+
const seconds = Math.max(retryAfter, 1);
|
|
145
|
+
const message = seconds > 60
|
|
146
|
+
? `Too many login attempts. Please wait ${Math.ceil(seconds / 60)} minute(s) and try again.`
|
|
147
|
+
: `Too many login attempts. Please wait ${seconds} second(s) and try again.`;
|
|
148
|
+
return `/login?error=${encodeURIComponent(message)}`;
|
|
149
|
+
};
|
|
150
|
+
|
|
104
151
|
// ============================================================================
|
|
105
152
|
// POST /login - Handle login form submission
|
|
106
153
|
// ============================================================================
|
|
107
|
-
|
|
154
|
+
const loginHandler = async (c) => {
|
|
108
155
|
try {
|
|
109
156
|
const body = await c.req.parseBody();
|
|
110
|
-
const {
|
|
157
|
+
const {
|
|
158
|
+
email,
|
|
159
|
+
password,
|
|
160
|
+
remember,
|
|
161
|
+
mfa_token,
|
|
162
|
+
backup_code,
|
|
163
|
+
mfa_challenge
|
|
164
|
+
} = body;
|
|
165
|
+
|
|
166
|
+
const clientIp = getClientIp(c);
|
|
167
|
+
const userAgent = c.req.header('user-agent') || 'unknown';
|
|
111
168
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
'unknown';
|
|
169
|
+
const usingChallenge = Boolean(mfa_challenge);
|
|
170
|
+
let normalizedEmail = email ? email.toLowerCase().trim() : '';
|
|
171
|
+
let rememberChoice = remember === '1' ? '1' : '0';
|
|
172
|
+
let challengePayload = null;
|
|
117
173
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
174
|
+
if (usingChallenge) {
|
|
175
|
+
challengePayload = await verifyMfaChallengeToken(mfa_challenge);
|
|
176
|
+
if (!challengePayload) {
|
|
177
|
+
return c.redirect(`/login?error=${encodeURIComponent('Your login session expired. Please sign in again.')}`);
|
|
178
|
+
}
|
|
179
|
+
normalizedEmail = (challengePayload.email || '').toLowerCase();
|
|
180
|
+
rememberChoice = challengePayload.remember ? '1' : '0';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!normalizedEmail) {
|
|
121
184
|
if (failbanManager && failbanConfig.endpoints.login) {
|
|
122
185
|
await failbanManager.recordViolation(clientIp, 'invalid_login_request', {
|
|
123
186
|
path: '/login',
|
|
124
|
-
userAgent
|
|
187
|
+
userAgent
|
|
125
188
|
});
|
|
126
189
|
}
|
|
127
|
-
|
|
128
|
-
return c.redirect(`/login?error=${encodeURIComponent('Email and password are required')}&email=${encodeURIComponent(email || '')}`);
|
|
190
|
+
return c.redirect(`/login?error=${encodeURIComponent('Email and password are required')}`);
|
|
129
191
|
}
|
|
130
192
|
|
|
131
|
-
|
|
132
|
-
const [okQuery, errQuery, users] = await tryFn(() =>
|
|
133
|
-
usersResource.query({ email: email.toLowerCase().trim() })
|
|
134
|
-
);
|
|
193
|
+
let user = null;
|
|
135
194
|
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
195
|
+
if (usingChallenge) {
|
|
196
|
+
const [okUser, errUser, challengeUser] = await tryFn(() =>
|
|
197
|
+
usersResource.get(challengePayload.userId)
|
|
198
|
+
);
|
|
139
199
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
email
|
|
146
|
-
});
|
|
200
|
+
if (!okUser || !challengeUser || challengeUser.email.toLowerCase() !== normalizedEmail) {
|
|
201
|
+
if (config.verbose) {
|
|
202
|
+
console.error('[Identity Plugin] MFA challenge verification failed:', errUser?.message || 'challenge mismatch');
|
|
203
|
+
}
|
|
204
|
+
return c.redirect(`/login?error=${encodeURIComponent('Your login session expired. Please sign in again.')}`);
|
|
147
205
|
}
|
|
148
206
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
userAgent: c.req.header('user-agent')
|
|
155
|
-
});
|
|
207
|
+
user = challengeUser;
|
|
208
|
+
} else {
|
|
209
|
+
const [okQuery, errQuery, users] = await tryFn(() =>
|
|
210
|
+
usersResource.query({ email: normalizedEmail })
|
|
211
|
+
);
|
|
156
212
|
|
|
157
|
-
|
|
158
|
-
|
|
213
|
+
if (!okQuery || users.length === 0) {
|
|
214
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
159
215
|
|
|
160
|
-
|
|
216
|
+
if (failbanManager && failbanConfig.endpoints.login) {
|
|
217
|
+
await failbanManager.recordViolation(clientIp, 'failed_login', {
|
|
218
|
+
path: '/login',
|
|
219
|
+
userAgent,
|
|
220
|
+
email
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await logAudit('login_failed', {
|
|
225
|
+
email,
|
|
226
|
+
reason: 'user_not_found',
|
|
227
|
+
ipAddress: clientIp,
|
|
228
|
+
userAgent
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return c.redirect(`/login?error=${encodeURIComponent('Invalid email or password')}&email=${encodeURIComponent(email || '')}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
user = users[0];
|
|
235
|
+
}
|
|
161
236
|
|
|
162
237
|
// 🔒 ACCOUNT LOCKOUT CHECK
|
|
163
238
|
if (accountLockoutConfig.enabled && user.lockedUntil) {
|
|
@@ -165,204 +240,213 @@ export function registerUIRoutes(app, plugin) {
|
|
|
165
240
|
const lockedUntilTime = new Date(user.lockedUntil).getTime();
|
|
166
241
|
|
|
167
242
|
if (lockedUntilTime > now) {
|
|
168
|
-
// Account is still locked
|
|
169
243
|
const remainingMinutes = Math.ceil((lockedUntilTime - now) / 60000);
|
|
170
244
|
const message = `Your account has been locked due to too many failed login attempts. Please try again in ${remainingMinutes} minute${remainingMinutes > 1 ? 's' : ''} or contact support.`;
|
|
245
|
+
return c.redirect(`/login?error=${encodeURIComponent(message)}&email=${encodeURIComponent(normalizedEmail)}`);
|
|
246
|
+
}
|
|
171
247
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
await usersResource.update(user.id, {
|
|
180
|
-
lockedUntil: null,
|
|
181
|
-
failedLoginAttempts: 0,
|
|
182
|
-
lastFailedLogin: null
|
|
183
|
-
});
|
|
248
|
+
// Auto-unlock expired locks
|
|
249
|
+
await usersResource.update(user.id, {
|
|
250
|
+
lockedUntil: null,
|
|
251
|
+
failedLoginAttempts: 0,
|
|
252
|
+
lastFailedLogin: null
|
|
253
|
+
});
|
|
254
|
+
}
|
|
184
255
|
|
|
185
|
-
|
|
186
|
-
|
|
256
|
+
// Verify password (initial step only)
|
|
257
|
+
if (!usingChallenge) {
|
|
258
|
+
if (!password) {
|
|
259
|
+
if (failbanManager && failbanConfig.endpoints.login) {
|
|
260
|
+
await failbanManager.recordViolation(clientIp, 'invalid_login_request', {
|
|
261
|
+
path: '/login',
|
|
262
|
+
userAgent,
|
|
263
|
+
email
|
|
264
|
+
});
|
|
187
265
|
}
|
|
266
|
+
return c.redirect(`/login?error=${encodeURIComponent('Email and password are required')}&email=${encodeURIComponent(email || '')}`);
|
|
188
267
|
}
|
|
189
|
-
}
|
|
190
268
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
269
|
+
const authResult = await plugin.authenticateWithPassword({
|
|
270
|
+
email: normalizedEmail,
|
|
271
|
+
password,
|
|
272
|
+
user
|
|
273
|
+
});
|
|
195
274
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const now = new Date().toISOString();
|
|
275
|
+
if (!authResult.success) {
|
|
276
|
+
if (authResult.statusCode >= 500 && config.verbose) {
|
|
277
|
+
console.error('[Identity Plugin] Password driver error:', authResult.error);
|
|
278
|
+
}
|
|
201
279
|
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
const
|
|
280
|
+
if (accountLockoutConfig.enabled) {
|
|
281
|
+
const failedAttempts = (user.failedLoginAttempts || 0) + 1;
|
|
282
|
+
const nowIso = new Date().toISOString();
|
|
205
283
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
lockedUntil: lockoutUntil,
|
|
209
|
-
lastFailedLogin: now
|
|
210
|
-
});
|
|
284
|
+
if (failedAttempts >= accountLockoutConfig.maxAttempts) {
|
|
285
|
+
const lockoutUntil = new Date(Date.now() + accountLockoutConfig.lockoutDuration).toISOString();
|
|
211
286
|
|
|
212
|
-
|
|
213
|
-
|
|
287
|
+
await usersResource.update(user.id, {
|
|
288
|
+
failedLoginAttempts: failedAttempts,
|
|
289
|
+
lockedUntil: lockoutUntil,
|
|
290
|
+
lastFailedLogin: nowIso
|
|
291
|
+
});
|
|
214
292
|
|
|
215
|
-
|
|
216
|
-
|
|
293
|
+
const lockoutMinutes = Math.ceil(accountLockoutConfig.lockoutDuration / 60000);
|
|
294
|
+
const message = `Too many failed login attempts. Your account has been locked for ${lockoutMinutes} minutes. Please contact support if you need assistance.`;
|
|
295
|
+
return c.redirect(`/login?error=${encodeURIComponent(message)}&email=${encodeURIComponent(normalizedEmail)}`);
|
|
217
296
|
}
|
|
218
297
|
|
|
219
|
-
return c.redirect(`/login?error=${encodeURIComponent(message)}&email=${encodeURIComponent(email)}`);
|
|
220
|
-
} else {
|
|
221
|
-
// Increment failed attempts counter
|
|
222
298
|
await usersResource.update(user.id, {
|
|
223
299
|
failedLoginAttempts: failedAttempts,
|
|
224
|
-
lastFailedLogin:
|
|
300
|
+
lastFailedLogin: nowIso
|
|
225
301
|
});
|
|
302
|
+
}
|
|
226
303
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
304
|
+
if (failbanManager && failbanConfig.endpoints.login) {
|
|
305
|
+
await failbanManager.recordViolation(clientIp, 'failed_login', {
|
|
306
|
+
path: '/login',
|
|
307
|
+
userAgent,
|
|
308
|
+
email: normalizedEmail,
|
|
309
|
+
userId: user.id
|
|
310
|
+
});
|
|
230
311
|
}
|
|
231
|
-
}
|
|
232
312
|
|
|
233
|
-
|
|
234
|
-
if (failbanManager && failbanConfig.endpoints.login) {
|
|
235
|
-
await failbanManager.recordViolation(clientIp, 'failed_login', {
|
|
236
|
-
path: '/login',
|
|
237
|
-
userAgent: c.req.header('user-agent'),
|
|
238
|
-
email,
|
|
239
|
-
userId: user.id
|
|
240
|
-
});
|
|
313
|
+
return c.redirect(`/login?error=${encodeURIComponent('Invalid email or password')}&email=${encodeURIComponent(normalizedEmail)}`);
|
|
241
314
|
}
|
|
242
315
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
failedLoginAttempts: 0,
|
|
251
|
-
lockedUntil: null,
|
|
252
|
-
lastFailedLogin: null
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
if (config.verbose) {
|
|
256
|
-
console.log(`[Account Lockout] Reset counters for user ${user.email} after successful login`);
|
|
316
|
+
if (accountLockoutConfig.enabled && accountLockoutConfig.resetOnSuccess) {
|
|
317
|
+
if (user.failedLoginAttempts > 0 || user.lockedUntil) {
|
|
318
|
+
await usersResource.update(user.id, {
|
|
319
|
+
failedLoginAttempts: 0,
|
|
320
|
+
lockedUntil: null,
|
|
321
|
+
lastFailedLogin: null
|
|
322
|
+
});
|
|
257
323
|
}
|
|
258
324
|
}
|
|
325
|
+
|
|
326
|
+
user = authResult.user || user;
|
|
259
327
|
}
|
|
260
328
|
|
|
261
|
-
//
|
|
262
|
-
if (user.status !== 'active') {
|
|
329
|
+
// Account active status
|
|
330
|
+
if (supportsStatusField && user.status && user.status !== 'active') {
|
|
263
331
|
const message = user.status === 'suspended'
|
|
264
332
|
? 'Your account has been suspended. Please contact support.'
|
|
265
|
-
: 'Your account is inactive. Please
|
|
266
|
-
return c.redirect(`/login?error=${encodeURIComponent(message)}&email=${encodeURIComponent(
|
|
333
|
+
: 'Your account is inactive. Please contact support.';
|
|
334
|
+
return c.redirect(`/login?error=${encodeURIComponent(message)}&email=${encodeURIComponent(normalizedEmail)}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!supportsStatusField && user.active === false) {
|
|
338
|
+
return c.redirect(`/login?error=${encodeURIComponent('Your account is inactive. Please contact support.')}&email=${encodeURIComponent(normalizedEmail)}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (config.registration.requireEmailVerification && !user.emailVerified) {
|
|
342
|
+
const message = 'Please verify your email address before signing in.';
|
|
343
|
+
return c.redirect(`/login?error=${encodeURIComponent(message)}&email=${encodeURIComponent(normalizedEmail)}`);
|
|
267
344
|
}
|
|
268
345
|
|
|
269
|
-
// 🔐 MFA
|
|
346
|
+
// 🔐 MFA logic
|
|
347
|
+
let hasMFA = false;
|
|
348
|
+
let mfaDevices = [];
|
|
270
349
|
if (config.mfa.enabled && plugin.mfaDevicesResource) {
|
|
271
|
-
|
|
272
|
-
const [okMFA, errMFA, mfaDevices] = await tryFn(() =>
|
|
350
|
+
const [okMFA, errMFA, devices] = await tryFn(() =>
|
|
273
351
|
plugin.mfaDevicesResource.query({ userId: user.id, verified: true })
|
|
274
352
|
);
|
|
353
|
+
if (!okMFA && config.verbose) {
|
|
354
|
+
console.error('[Identity Plugin] Failed to load MFA devices:', errMFA);
|
|
355
|
+
}
|
|
356
|
+
if (okMFA && devices && devices.length > 0) {
|
|
357
|
+
hasMFA = true;
|
|
358
|
+
mfaDevices = devices;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
275
361
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (hasMFA || mfaRequired) {
|
|
280
|
-
// User has MFA enabled or MFA is mandatory
|
|
281
|
-
const { mfa_token, backup_code } = body;
|
|
282
|
-
|
|
283
|
-
if (!mfa_token && !backup_code) {
|
|
284
|
-
// Redirect to MFA verification page
|
|
285
|
-
// Create a temporary token to verify the user has passed password auth
|
|
286
|
-
// Store the password for re-submission (it's already verified at this point)
|
|
287
|
-
const tempToken = Buffer.from(JSON.stringify({
|
|
288
|
-
userId: user.id,
|
|
289
|
-
email: user.email,
|
|
290
|
-
password: password, // Store to re-submit after MFA
|
|
291
|
-
timestamp: Date.now()
|
|
292
|
-
})).toString('base64');
|
|
293
|
-
|
|
294
|
-
return c.redirect(`/login/mfa?token=${tempToken}&remember=${remember || ''}`);
|
|
295
|
-
}
|
|
362
|
+
if (config.mfa.required && !hasMFA) {
|
|
363
|
+
return c.redirect(`/login?error=${encodeURIComponent('Multi-factor authentication is required for your account. Please contact support to complete enrollment.')}&email=${encodeURIComponent(normalizedEmail)}`);
|
|
364
|
+
}
|
|
296
365
|
|
|
297
|
-
|
|
298
|
-
let mfaVerified = false;
|
|
366
|
+
const needsMfa = hasMFA || config.mfa.required;
|
|
299
367
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
368
|
+
if (!usingChallenge && needsMfa) {
|
|
369
|
+
const challengeToken = createMfaChallengeToken(user, rememberChoice);
|
|
370
|
+
if (!challengeToken) {
|
|
371
|
+
return c.redirect(`/login?error=${encodeURIComponent('Unable to start MFA verification. Please contact support.')}`);
|
|
372
|
+
}
|
|
303
373
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
374
|
+
const params = new URLSearchParams({ challenge: challengeToken });
|
|
375
|
+
if (rememberChoice === '1') {
|
|
376
|
+
params.set('remember', '1');
|
|
377
|
+
}
|
|
378
|
+
return c.redirect(`/login/mfa?${params.toString()}`);
|
|
379
|
+
}
|
|
309
380
|
|
|
310
|
-
|
|
311
|
-
|
|
381
|
+
if (usingChallenge && needsMfa) {
|
|
382
|
+
if (!mfa_token && !backup_code) {
|
|
383
|
+
const retryChallenge = createMfaChallengeToken(user, rememberChoice) || mfa_challenge;
|
|
384
|
+
const params = new URLSearchParams({ challenge: retryChallenge });
|
|
385
|
+
params.set('error', 'Multi-factor authentication is required.');
|
|
386
|
+
if (rememberChoice === '1') {
|
|
387
|
+
params.set('remember', '1');
|
|
388
|
+
}
|
|
389
|
+
return c.redirect(`/login/mfa?${params.toString()}`);
|
|
390
|
+
}
|
|
312
391
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (matchIndex !== null && matchIndex >= 0) {
|
|
325
|
-
// Remove used backup code
|
|
326
|
-
const updatedCodes = [...mfaDevices[0].backupCodes];
|
|
327
|
-
updatedCodes.splice(matchIndex, 1);
|
|
328
|
-
|
|
329
|
-
await plugin.mfaDevicesResource.patch(mfaDevices[0].id, {
|
|
330
|
-
backupCodes: updatedCodes,
|
|
331
|
-
lastUsedAt: new Date().toISOString()
|
|
332
|
-
});
|
|
392
|
+
let mfaVerified = false;
|
|
393
|
+
|
|
394
|
+
if (mfa_token && hasMFA) {
|
|
395
|
+
mfaVerified = plugin.mfaManager.verifyTOTP(mfaDevices[0].secret, mfa_token);
|
|
396
|
+
if (mfaVerified) {
|
|
397
|
+
await plugin.mfaDevicesResource.patch(mfaDevices[0].id, {
|
|
398
|
+
lastUsedAt: new Date().toISOString()
|
|
399
|
+
});
|
|
400
|
+
await logAudit('mfa_verified', { userId: user.id, method: 'totp' });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
333
403
|
|
|
334
|
-
|
|
404
|
+
if (!mfaVerified && backup_code && hasMFA) {
|
|
405
|
+
const matchIndex = await plugin.mfaManager.verifyBackupCode(
|
|
406
|
+
backup_code,
|
|
407
|
+
mfaDevices[0].backupCodes
|
|
408
|
+
);
|
|
335
409
|
|
|
336
|
-
|
|
337
|
-
|
|
410
|
+
if (matchIndex !== null && matchIndex >= 0) {
|
|
411
|
+
const updatedCodes = [...mfaDevices[0].backupCodes];
|
|
412
|
+
updatedCodes.splice(matchIndex, 1);
|
|
338
413
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
414
|
+
await plugin.mfaDevicesResource.patch(mfaDevices[0].id, {
|
|
415
|
+
backupCodes: updatedCodes,
|
|
416
|
+
lastUsedAt: new Date().toISOString()
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
mfaVerified = true;
|
|
420
|
+
await logAudit('mfa_verified', { userId: user.id, method: 'backup_code' });
|
|
343
421
|
}
|
|
422
|
+
}
|
|
344
423
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
await logAudit('mfa_failed', { userId: user.id, reason: 'invalid_token' });
|
|
424
|
+
if (!mfaVerified) {
|
|
425
|
+
await logAudit('mfa_failed', { userId: user.id, reason: 'invalid_token' });
|
|
348
426
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
427
|
+
if (failbanManager && failbanConfig.endpoints.login) {
|
|
428
|
+
await failbanManager.recordViolation(clientIp, 'failed_mfa', {
|
|
429
|
+
path: '/login',
|
|
430
|
+
userAgent,
|
|
431
|
+
email: normalizedEmail,
|
|
432
|
+
userId: user.id
|
|
433
|
+
});
|
|
434
|
+
}
|
|
352
435
|
|
|
353
|
-
|
|
436
|
+
const retryChallenge = createMfaChallengeToken(user, rememberChoice) || mfa_challenge;
|
|
437
|
+
const params = new URLSearchParams({
|
|
438
|
+
challenge: retryChallenge,
|
|
439
|
+
error: 'Invalid MFA code. Please try again.'
|
|
440
|
+
});
|
|
441
|
+
if (rememberChoice === '1') {
|
|
442
|
+
params.set('remember', '1');
|
|
354
443
|
}
|
|
444
|
+
return c.redirect(`/login/mfa?${params.toString()}`);
|
|
355
445
|
}
|
|
356
446
|
}
|
|
357
447
|
|
|
358
|
-
// Get request metadata
|
|
359
|
-
const ipAddress = c.req.header('x-forwarded-for')?.split(',')[0].trim() ||
|
|
360
|
-
c.req.header('x-real-ip') ||
|
|
361
|
-
'unknown';
|
|
362
|
-
const userAgent = c.req.header('user-agent') || 'unknown';
|
|
363
|
-
|
|
364
448
|
// Create session
|
|
365
|
-
const
|
|
449
|
+
const sessionDuration = rememberChoice === '1' ? '30d' : config.session.sessionExpiry;
|
|
366
450
|
const [okSession, errSession, session] = await tryFn(() =>
|
|
367
451
|
sessionManager.createSession({
|
|
368
452
|
userId: user.id,
|
|
@@ -371,9 +455,9 @@ export function registerUIRoutes(app, plugin) {
|
|
|
371
455
|
name: user.name,
|
|
372
456
|
isAdmin: user.isAdmin || false
|
|
373
457
|
},
|
|
374
|
-
ipAddress,
|
|
458
|
+
ipAddress: clientIp,
|
|
375
459
|
userAgent,
|
|
376
|
-
|
|
460
|
+
duration: sessionDuration
|
|
377
461
|
})
|
|
378
462
|
);
|
|
379
463
|
|
|
@@ -381,21 +465,18 @@ export function registerUIRoutes(app, plugin) {
|
|
|
381
465
|
if (config.verbose) {
|
|
382
466
|
console.error('[Identity Plugin] Failed to create session:', errSession);
|
|
383
467
|
}
|
|
384
|
-
return c.redirect(`/login?error=${encodeURIComponent('Failed to create session. Please try again.')}&email=${encodeURIComponent(
|
|
468
|
+
return c.redirect(`/login?error=${encodeURIComponent('Failed to create session. Please try again.')}&email=${encodeURIComponent(normalizedEmail)}`);
|
|
385
469
|
}
|
|
386
470
|
|
|
387
|
-
|
|
388
|
-
sessionManager.setSessionCookie(c, session.id, session.expiresAt);
|
|
471
|
+
sessionManager.setSessionCookie(c, session.sessionId, session.expiresAt);
|
|
389
472
|
|
|
390
|
-
// Update last login timestamp
|
|
391
473
|
await tryFn(() =>
|
|
392
474
|
usersResource.patch(user.id, {
|
|
393
475
|
lastLoginAt: new Date().toISOString(),
|
|
394
|
-
lastLoginIp:
|
|
476
|
+
lastLoginIp: clientIp
|
|
395
477
|
})
|
|
396
478
|
);
|
|
397
479
|
|
|
398
|
-
// Redirect to original destination or profile
|
|
399
480
|
const redirectTo = c.req.query('redirect') || '/profile';
|
|
400
481
|
return c.redirect(redirectTo);
|
|
401
482
|
|
|
@@ -405,7 +486,19 @@ export function registerUIRoutes(app, plugin) {
|
|
|
405
486
|
}
|
|
406
487
|
return c.redirect(`/login?error=${encodeURIComponent('An error occurred. Please try again.')}`);
|
|
407
488
|
}
|
|
408
|
-
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const loginLimiter = plugin.rateLimiters?.login;
|
|
492
|
+
if (loginLimiter) {
|
|
493
|
+
const loginRateMiddleware = createRedirectRateLimitMiddleware(
|
|
494
|
+
loginLimiter,
|
|
495
|
+
getClientIp,
|
|
496
|
+
buildRateLimitRedirect
|
|
497
|
+
);
|
|
498
|
+
app.post('/login', loginRateMiddleware, loginHandler);
|
|
499
|
+
} else {
|
|
500
|
+
app.post('/login', loginHandler);
|
|
501
|
+
}
|
|
409
502
|
|
|
410
503
|
// ============================================================================
|
|
411
504
|
// GET /login/mfa - Show MFA verification page (two-factor authentication)
|
|
@@ -416,35 +509,34 @@ export function registerUIRoutes(app, plugin) {
|
|
|
416
509
|
}
|
|
417
510
|
|
|
418
511
|
try {
|
|
419
|
-
const
|
|
512
|
+
const challenge = c.req.query('challenge');
|
|
420
513
|
const remember = c.req.query('remember');
|
|
421
514
|
const error = c.req.query('error');
|
|
422
515
|
|
|
423
|
-
if (!
|
|
516
|
+
if (!challenge) {
|
|
424
517
|
return c.redirect(`/login?error=${encodeURIComponent('Invalid MFA session')}`);
|
|
425
518
|
}
|
|
426
519
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
userData = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
|
|
431
|
-
} catch (err) {
|
|
432
|
-
return c.redirect(`/login?error=${encodeURIComponent('Invalid MFA session')}`);
|
|
520
|
+
const payload = await verifyMfaChallengeToken(challenge);
|
|
521
|
+
if (!payload) {
|
|
522
|
+
return c.redirect(`/login?error=${encodeURIComponent('MFA session expired. Please login again.')}`);
|
|
433
523
|
}
|
|
434
524
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
525
|
+
const [okUser, , challengeUser] = await tryFn(() =>
|
|
526
|
+
usersResource.get(payload.userId)
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
if (!okUser || !challengeUser) {
|
|
530
|
+
return c.redirect(`/login?error=${encodeURIComponent('Your account could not be found. Please login again.')}`);
|
|
439
531
|
}
|
|
440
532
|
|
|
441
|
-
|
|
442
|
-
const mfaToken = JSON.stringify({ email: userData.email, password: userData.password });
|
|
533
|
+
const effectiveRemember = payload.remember || remember === '1';
|
|
443
534
|
|
|
444
535
|
return c.html(MFAVerificationPage({
|
|
445
536
|
error: error ? decodeURIComponent(error) : null,
|
|
446
|
-
|
|
447
|
-
remember:
|
|
537
|
+
email: challengeUser.email,
|
|
538
|
+
remember: effectiveRemember ? '1' : '0',
|
|
539
|
+
challenge,
|
|
448
540
|
config: uiConfig
|
|
449
541
|
}));
|
|
450
542
|
|
|
@@ -554,23 +646,28 @@ export function registerUIRoutes(app, plugin) {
|
|
|
554
646
|
}
|
|
555
647
|
|
|
556
648
|
// Get request metadata
|
|
557
|
-
const ipAddress = c
|
|
558
|
-
|
|
559
|
-
|
|
649
|
+
const ipAddress = getClientIp(c);
|
|
650
|
+
|
|
651
|
+
const initialActive = !config.registration.requireEmailVerification;
|
|
652
|
+
const userRecord = {
|
|
653
|
+
email: normalizedEmail,
|
|
654
|
+
name: name.trim(),
|
|
655
|
+
password, // Auto-encrypted with 'password' type
|
|
656
|
+
isAdmin: false,
|
|
657
|
+
emailVerified: config.registration.requireEmailVerification ? false : true,
|
|
658
|
+
active: initialActive,
|
|
659
|
+
registrationIp: ipAddress,
|
|
660
|
+
lastLoginAt: null,
|
|
661
|
+
lastLoginIp: null
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
if (supportsStatusField) {
|
|
665
|
+
userRecord.status = initialActive ? 'active' : 'pending_verification';
|
|
666
|
+
}
|
|
560
667
|
|
|
561
668
|
// Create user (password will be auto-encrypted by S3DB)
|
|
562
669
|
const [okUser, errUser, user] = await tryFn(() =>
|
|
563
|
-
usersResource.insert(
|
|
564
|
-
email: normalizedEmail,
|
|
565
|
-
name: name.trim(),
|
|
566
|
-
password: password, // Auto-encrypted with 'secret' type
|
|
567
|
-
status: 'pending_verification', // Requires email verification
|
|
568
|
-
isAdmin: false,
|
|
569
|
-
emailVerified: false,
|
|
570
|
-
registrationIp: ipAddress,
|
|
571
|
-
lastLoginAt: null,
|
|
572
|
-
lastLoginIp: null
|
|
573
|
-
})
|
|
670
|
+
usersResource.insert(userRecord)
|
|
574
671
|
);
|
|
575
672
|
|
|
576
673
|
if (!okUser) {
|
|
@@ -580,34 +677,35 @@ export function registerUIRoutes(app, plugin) {
|
|
|
580
677
|
return c.redirect(`/register?error=${encodeURIComponent('Failed to create account. Please try again.')}&email=${encodeURIComponent(email)}&name=${encodeURIComponent(name)}`);
|
|
581
678
|
}
|
|
582
679
|
|
|
583
|
-
|
|
584
|
-
const verificationToken = generatePasswordResetToken();
|
|
585
|
-
const verificationExpiry = calculateExpiration(24); // 24 hours
|
|
680
|
+
let successMessage = 'Account created successfully! You can sign in now.';
|
|
586
681
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
emailVerificationExpiry: verificationExpiry
|
|
591
|
-
});
|
|
682
|
+
if (config.registration.requireEmailVerification) {
|
|
683
|
+
const verificationToken = generatePasswordResetToken();
|
|
684
|
+
const verificationExpiry = calculateExpiration('24h'); // 24 hours
|
|
592
685
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
686
|
+
await usersResource.update(user.id, {
|
|
687
|
+
emailVerificationToken: verificationToken,
|
|
688
|
+
emailVerificationExpiry: verificationExpiry
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
if (plugin.emailService) {
|
|
692
|
+
try {
|
|
693
|
+
await plugin.emailService.sendEmailVerificationEmail({
|
|
694
|
+
to: normalizedEmail,
|
|
695
|
+
name: name.trim(),
|
|
696
|
+
verificationToken
|
|
697
|
+
});
|
|
698
|
+
} catch (emailError) {
|
|
699
|
+
if (config.verbose) {
|
|
700
|
+
console.error('[Identity Plugin] Failed to send verification email:', emailError);
|
|
701
|
+
}
|
|
604
702
|
}
|
|
605
|
-
// Don't fail registration if email fails - user can resend later
|
|
606
703
|
}
|
|
704
|
+
|
|
705
|
+
successMessage = 'Account created successfully! Please check your email to verify your account.';
|
|
607
706
|
}
|
|
608
707
|
|
|
609
|
-
|
|
610
|
-
return c.redirect(`/login?success=${encodeURIComponent('Account created successfully! Please check your email to verify your account.')}&email=${encodeURIComponent(normalizedEmail)}`);
|
|
708
|
+
return c.redirect(`/login?success=${encodeURIComponent(successMessage)}&email=${encodeURIComponent(normalizedEmail)}`);
|
|
611
709
|
|
|
612
710
|
} catch (error) {
|
|
613
711
|
if (config.verbose) {
|
|
@@ -1425,8 +1523,8 @@ export function registerUIRoutes(app, plugin) {
|
|
|
1425
1523
|
const now = new Date();
|
|
1426
1524
|
const stats = {
|
|
1427
1525
|
totalUsers: users.length,
|
|
1428
|
-
activeUsers: users.filter(u => u.status === 'active').length,
|
|
1429
|
-
pendingUsers: users.filter(u => u.status === 'pending_verification').length,
|
|
1526
|
+
activeUsers: users.filter(u => supportsStatusField ? u.status === 'active' : u.active !== false).length,
|
|
1527
|
+
pendingUsers: users.filter(u => supportsStatusField ? u.status === 'pending_verification' : (u.active === false && !u.emailVerified)).length,
|
|
1430
1528
|
totalClients: clients.length,
|
|
1431
1529
|
activeClients: clients.filter(c => c.active !== false).length,
|
|
1432
1530
|
activeSessions: sessions.filter(s => new Date(s.expiresAt) > now).length,
|
|
@@ -1840,7 +1938,7 @@ export function registerUIRoutes(app, plugin) {
|
|
|
1840
1938
|
|
|
1841
1939
|
// Only allow status/role changes if not self-editing
|
|
1842
1940
|
if (!isSelfEdit) {
|
|
1843
|
-
if (status) {
|
|
1941
|
+
if (supportsStatusField && status) {
|
|
1844
1942
|
updates.status = status;
|
|
1845
1943
|
}
|
|
1846
1944
|
if (role) {
|
|
@@ -1921,6 +2019,10 @@ export function registerUIRoutes(app, plugin) {
|
|
|
1921
2019
|
const { status } = body;
|
|
1922
2020
|
const currentUser = c.get('user');
|
|
1923
2021
|
|
|
2022
|
+
if (!supportsStatusField) {
|
|
2023
|
+
return c.redirect(`/admin/users?error=${encodeURIComponent('Status management is not supported for this deployment')}`);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1924
2026
|
// Prevent self-status change
|
|
1925
2027
|
if (userId === currentUser.id) {
|
|
1926
2028
|
return c.redirect(`/admin/users?error=${encodeURIComponent('You cannot change your own status')}`);
|
|
@@ -2404,13 +2506,19 @@ export function registerUIRoutes(app, plugin) {
|
|
|
2404
2506
|
}
|
|
2405
2507
|
|
|
2406
2508
|
// Verify the email
|
|
2509
|
+
const verificationUpdate = {
|
|
2510
|
+
emailVerified: true,
|
|
2511
|
+
emailVerificationToken: null,
|
|
2512
|
+
emailVerificationExpiry: null,
|
|
2513
|
+
active: true
|
|
2514
|
+
};
|
|
2515
|
+
|
|
2516
|
+
if (supportsStatusField) {
|
|
2517
|
+
verificationUpdate.status = 'active';
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2407
2520
|
const [okUpdate, errUpdate] = await tryFn(() =>
|
|
2408
|
-
usersResource.update(user.id,
|
|
2409
|
-
emailVerified: true,
|
|
2410
|
-
emailVerificationToken: null,
|
|
2411
|
-
emailVerificationExpiry: null,
|
|
2412
|
-
status: 'active' // Activate account on email verification
|
|
2413
|
-
})
|
|
2521
|
+
usersResource.update(user.id, verificationUpdate)
|
|
2414
2522
|
);
|
|
2415
2523
|
|
|
2416
2524
|
if (!okUpdate) {
|
|
@@ -2479,7 +2587,7 @@ export function registerUIRoutes(app, plugin) {
|
|
|
2479
2587
|
|
|
2480
2588
|
// Generate new verification token
|
|
2481
2589
|
const verificationToken = generatePasswordResetToken(); // Reuse token generator
|
|
2482
|
-
const verificationExpiry = calculateExpiration(
|
|
2590
|
+
const verificationExpiry = calculateExpiration('24h'); // 24 hours
|
|
2483
2591
|
|
|
2484
2592
|
// Update user with new token
|
|
2485
2593
|
const [okUpdate, errUpdate] = await tryFn(() =>
|