@tachybase/plugin-password-policy 1.0.6

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 (77) hide show
  1. package/README.md +1 -0
  2. package/client.d.ts +2 -0
  3. package/client.js +1 -0
  4. package/dist/client/IPFilterForm.d.ts +1 -0
  5. package/dist/client/PasswordAttemptForm.d.ts +1 -0
  6. package/dist/client/PasswordStrengthSettingsForm.d.ts +2 -0
  7. package/dist/client/SignInFailsTable.d.ts +2 -0
  8. package/dist/client/UserLocksTable.d.ts +2 -0
  9. package/dist/client/collections/signInFails.d.ts +2 -0
  10. package/dist/client/collections/userLocks.d.ts +2 -0
  11. package/dist/client/hooks/usePasswordStrength.d.ts +11 -0
  12. package/dist/client/hooks/usePasswordValidator.d.ts +16 -0
  13. package/dist/client/index.d.ts +5 -0
  14. package/dist/client/index.js +4 -0
  15. package/dist/client/locale.d.ts +6 -0
  16. package/dist/constants.d.ts +11 -0
  17. package/dist/constants.js +44 -0
  18. package/dist/externalVersion.js +10 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +39 -0
  21. package/dist/locale/en-US.json +107 -0
  22. package/dist/locale/zh-CN.json +107 -0
  23. package/dist/node_modules/geoip-lite/LICENSE +50 -0
  24. package/dist/node_modules/geoip-lite/data/city.checksum +1 -0
  25. package/dist/node_modules/geoip-lite/data/country.checksum +1 -0
  26. package/dist/node_modules/geoip-lite/data/geoip-city-names.dat +0 -0
  27. package/dist/node_modules/geoip-lite/data/geoip-city.dat +0 -0
  28. package/dist/node_modules/geoip-lite/data/geoip-city6.dat +0 -0
  29. package/dist/node_modules/geoip-lite/data/geoip-country.dat +0 -0
  30. package/dist/node_modules/geoip-lite/data/geoip-country6.dat +0 -0
  31. package/dist/node_modules/geoip-lite/lib/fsWatcher.js +83 -0
  32. package/dist/node_modules/geoip-lite/lib/geoip.js +1 -0
  33. package/dist/node_modules/geoip-lite/lib/utils.js +98 -0
  34. package/dist/node_modules/geoip-lite/node_modules/.bin/rimraf +17 -0
  35. package/dist/node_modules/geoip-lite/package.json +1 -0
  36. package/dist/node_modules/geoip-lite/scripts/updatedb.js +685 -0
  37. package/dist/node_modules/geoip-lite/test/geo-lookup.js +56 -0
  38. package/dist/node_modules/geoip-lite/test/memory_usage.js +3 -0
  39. package/dist/node_modules/geoip-lite/test/tests.js +197 -0
  40. package/dist/server/actions/IpFilterController.d.ts +7 -0
  41. package/dist/server/actions/IpFilterController.js +124 -0
  42. package/dist/server/actions/PasswordAttemptController.d.ts +7 -0
  43. package/dist/server/actions/PasswordAttemptController.js +123 -0
  44. package/dist/server/actions/PasswordStrengthController.d.ts +7 -0
  45. package/dist/server/actions/PasswordStrengthController.js +123 -0
  46. package/dist/server/actions/SignInFailsController.d.ts +5 -0
  47. package/dist/server/actions/SignInFailsController.js +156 -0
  48. package/dist/server/actions/UserLocksController.d.ts +4 -0
  49. package/dist/server/actions/UserLocksController.js +102 -0
  50. package/dist/server/collections/ipFilter.d.ts +2 -0
  51. package/dist/server/collections/ipFilter.js +51 -0
  52. package/dist/server/collections/passwordAttempt.d.ts +2 -0
  53. package/dist/server/collections/passwordAttempt.js +55 -0
  54. package/dist/server/collections/passwordHistory.d.ts +2 -0
  55. package/dist/server/collections/passwordHistory.js +46 -0
  56. package/dist/server/collections/passwordStrengthConfig.d.ts +2 -0
  57. package/dist/server/collections/passwordStrengthConfig.js +59 -0
  58. package/dist/server/collections/signInFail.d.ts +2 -0
  59. package/dist/server/collections/signInFail.js +56 -0
  60. package/dist/server/collections/userLocks.d.ts +2 -0
  61. package/dist/server/collections/userLocks.js +51 -0
  62. package/dist/server/collections/users.d.ts +2 -0
  63. package/dist/server/collections/users.js +42 -0
  64. package/dist/server/index.d.ts +1 -0
  65. package/dist/server/index.js +33 -0
  66. package/dist/server/plugin.d.ts +5 -0
  67. package/dist/server/plugin.js +129 -0
  68. package/dist/server/services/IPFilterService.d.ts +49 -0
  69. package/dist/server/services/IPFilterService.js +270 -0
  70. package/dist/server/services/PasswordAttemptService.d.ts +75 -0
  71. package/dist/server/services/PasswordAttemptService.js +595 -0
  72. package/dist/server/services/PasswordStrengthService.d.ts +28 -0
  73. package/dist/server/services/PasswordStrengthService.js +313 -0
  74. package/dist/types/geoip-lite.d.js +0 -0
  75. package/package.json +25 -0
  76. package/server.d.ts +2 -0
  77. package/server.js +1 -0
@@ -0,0 +1,595 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
8
+ var __typeError = (msg) => {
9
+ throw TypeError(msg);
10
+ };
11
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
12
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
17
+ var __copyProps = (to, from, except, desc) => {
18
+ if (from && typeof from === "object" || typeof from === "function") {
19
+ for (let key of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(to, key) && key !== except)
21
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
26
+ // If the importer is in node compatibility mode or this is not an ESM
27
+ // file that has been converted to a CommonJS file using a Babel-
28
+ // compatible transform (i.e. "__esModule" has not been set), then set
29
+ // "default" to the CommonJS "module.exports" for node compatibility.
30
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
31
+ mod
32
+ ));
33
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
34
+ var __decoratorStart = (base) => [, , , __create((base == null ? void 0 : base[__knownSymbol("metadata")]) ?? null)];
35
+ var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
36
+ var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
37
+ var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) });
38
+ var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
39
+ var __runInitializers = (array, flags, self, value) => {
40
+ for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
41
+ return value;
42
+ };
43
+ var __decorateElement = (array, flags, name, decorators, target, extra) => {
44
+ var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
45
+ var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
46
+ var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
47
+ var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() {
48
+ return __privateGet(this, extra);
49
+ }, set [name](x) {
50
+ return __privateSet(this, extra, x);
51
+ } }, name));
52
+ k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
53
+ for (var i = decorators.length - 1; i >= 0; i--) {
54
+ ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
55
+ if (k) {
56
+ ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x };
57
+ if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
58
+ if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
59
+ }
60
+ it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1;
61
+ if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
62
+ else if (typeof it !== "object" || it === null) __typeError("Object expected");
63
+ else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
64
+ }
65
+ return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
66
+ };
67
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
68
+ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
69
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
70
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
71
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
72
+ var PasswordAttemptService_exports = {};
73
+ __export(PasswordAttemptService_exports, {
74
+ PasswordAttemptService: () => PasswordAttemptService
75
+ });
76
+ module.exports = __toCommonJS(PasswordAttemptService_exports);
77
+ var import_utils = require("@tachybase/utils");
78
+ var geoip = __toESM(require("geoip-lite"));
79
+ var import_constants = require("../../constants");
80
+ var _logger_dec, _app_dec, _db_dec, _PasswordAttemptService_decorators, _init;
81
+ _PasswordAttemptService_decorators = [(0, import_utils.Service)()], _db_dec = [(0, import_utils.Db)()], _app_dec = [(0, import_utils.App)()], _logger_dec = [(0, import_utils.InjectLog)()];
82
+ class PasswordAttemptService {
83
+ constructor() {
84
+ this.db = __runInitializers(_init, 8, this), __runInitializers(_init, 11, this);
85
+ this.app = __runInitializers(_init, 12, this), __runInitializers(_init, 15, this);
86
+ this.logger = __runInitializers(_init, 16, this), __runInitializers(_init, 19, this);
87
+ this.config = void 0;
88
+ // 内存缓存,用于存储最近的失败记录
89
+ this.failureRecords = /* @__PURE__ */ new Map();
90
+ // 缓存前缀
91
+ this.CACHE_PREFIX = "passwordAttempt";
92
+ // 缓存过期时间(毫秒)
93
+ this.CACHE_TTL = 5 * 60 * 1e3;
94
+ }
95
+ // 5分钟
96
+ async load() {
97
+ this.addMiddleWare();
98
+ this.app.on("afterStart", async () => {
99
+ const config = await this.db.getRepository("passwordAttempt").findOne();
100
+ await this.refreshConfig(config);
101
+ await this.initLockedUsersCache();
102
+ this.setupLockedUsersListener();
103
+ });
104
+ this.db.on("passwordAttempt.afterSave", async (model) => {
105
+ await this.refreshConfig(model);
106
+ });
107
+ }
108
+ /**
109
+ * 获取用户锁定信息的缓存键
110
+ * @param userId 用户ID
111
+ * @returns 缓存键
112
+ */
113
+ getUserLockCacheKey(userId) {
114
+ return `${this.CACHE_PREFIX}:locked:${userId}`;
115
+ }
116
+ /**
117
+ * 初始化被锁定用户的缓存
118
+ */
119
+ async initLockedUsersCache() {
120
+ try {
121
+ const now = /* @__PURE__ */ new Date();
122
+ const lockedUsersRepo = this.db.getRepository("userLocks");
123
+ const lockedUsers = await lockedUsersRepo.find({
124
+ filter: {
125
+ expireAt: {
126
+ $gt: now
127
+ }
128
+ }
129
+ });
130
+ for (const user of lockedUsers) {
131
+ const userId = user.get("userId");
132
+ const expireAt = user.get("expireAt");
133
+ await this.app.cache.set(
134
+ this.getUserLockCacheKey(userId),
135
+ {
136
+ id: userId,
137
+ expireAt,
138
+ lastChecked: now
139
+ },
140
+ this.CACHE_TTL
141
+ );
142
+ }
143
+ this.logger.info(`Loaded ${lockedUsers.length} locked users into cache`);
144
+ } catch (error) {
145
+ this.logger.error("Failed to load locked users into cache:", error);
146
+ }
147
+ }
148
+ /**
149
+ * 设置监听userLocks表变动的事件
150
+ */
151
+ setupLockedUsersListener() {
152
+ this.app.db.on("userLocks.afterCreate", async (model) => {
153
+ const userId = model.get("userId");
154
+ const expireAt = model.get("expireAt");
155
+ await this.app.cache.set(
156
+ this.getUserLockCacheKey(userId),
157
+ {
158
+ id: userId,
159
+ expireAt,
160
+ lastChecked: /* @__PURE__ */ new Date()
161
+ },
162
+ this.CACHE_TTL
163
+ );
164
+ this.logger.info(`Added user ${userId} to locked users cache`);
165
+ });
166
+ this.app.db.on("userLocks.afterUpdate", async (model) => {
167
+ const userId = model.get("userId");
168
+ if (!userId && model.previous("userId")) {
169
+ const userId2 = model.previous("userId");
170
+ await this.app.cache.del(this.getUserLockCacheKey(userId2));
171
+ await this.clearUserFailRecords(userId2);
172
+ this.logger.info(`Removed user ${userId2} from locked users cache (deleted)`);
173
+ return;
174
+ }
175
+ const expireAt = model.get("expireAt");
176
+ const now = /* @__PURE__ */ new Date();
177
+ if (expireAt > now) {
178
+ await this.app.cache.set(
179
+ this.getUserLockCacheKey(userId),
180
+ {
181
+ id: userId,
182
+ expireAt,
183
+ lastChecked: now
184
+ },
185
+ this.CACHE_TTL
186
+ );
187
+ this.logger.info(`Updated user ${userId} in locked users cache`);
188
+ } else {
189
+ await this.app.cache.del(this.getUserLockCacheKey(userId));
190
+ await this.clearUserFailRecords(userId);
191
+ this.logger.info(`Removed user ${userId} from locked users cache (expired)`);
192
+ }
193
+ });
194
+ this.app.db.on("userLocks.afterDestroy", async (model) => {
195
+ const userId = model.get("userId");
196
+ await this.app.cache.del(this.getUserLockCacheKey(userId));
197
+ await this.clearUserFailRecords(userId);
198
+ this.logger.info(`Removed user ${userId} from locked users cache (deleted)`);
199
+ });
200
+ }
201
+ /**
202
+ * 清空用户的登录失败记录
203
+ * @param userId 用户ID
204
+ */
205
+ async clearUserFailRecords(userId) {
206
+ try {
207
+ await this.db.getRepository("signInFails").update({
208
+ filter: {
209
+ userId
210
+ },
211
+ values: {
212
+ status: false
213
+ }
214
+ });
215
+ this.failureRecords.delete(userId);
216
+ this.logger.info(`Cleared sign-in failure records for user ${userId}`);
217
+ } catch (error) {
218
+ this.logger.error(`Failed to clear sign-in failure records for user ${userId}:`, error);
219
+ }
220
+ }
221
+ /**
222
+ * 检查用户是否被锁定
223
+ * @param userId 用户ID
224
+ * @returns 如果用户被锁定,返回true;否则返回false
225
+ */
226
+ async isUserLocked(userId) {
227
+ const now = /* @__PURE__ */ new Date();
228
+ const cacheKey = this.getUserLockCacheKey(userId);
229
+ const cachedInfo = await this.app.cache.get(cacheKey);
230
+ if (cachedInfo) {
231
+ const isCacheExpired = now.getTime() - cachedInfo.lastChecked.getTime() > this.CACHE_TTL;
232
+ if (isCacheExpired) {
233
+ this.refreshLockedUserCache(userId);
234
+ }
235
+ if (!cachedInfo.expireAt) {
236
+ return false;
237
+ }
238
+ if (cachedInfo.expireAt > now) {
239
+ return true;
240
+ } else {
241
+ cachedInfo.expireAt = null;
242
+ cachedInfo.lastChecked = now;
243
+ await this.app.cache.set(cacheKey, cachedInfo, this.CACHE_TTL);
244
+ await this.clearUserFailRecords(userId);
245
+ return false;
246
+ }
247
+ }
248
+ return await this.checkAndCacheLockedUser(userId);
249
+ }
250
+ /**
251
+ * 从数据库检查并缓存用户锁定状态
252
+ * @param userId 用户ID
253
+ * @returns 如果用户被锁定,返回true;否则返回false
254
+ */
255
+ async checkAndCacheLockedUser(userId) {
256
+ var _a;
257
+ try {
258
+ const now = /* @__PURE__ */ new Date();
259
+ const userRepository = this.db.getRepository("users");
260
+ const user = await userRepository.findOne({
261
+ fields: ["id"],
262
+ filter: { id: userId },
263
+ appends: ["lock"]
264
+ });
265
+ const cacheKey = this.getUserLockCacheKey(userId);
266
+ if (user && ((_a = user.lock) == null ? void 0 : _a.expireAt) && user.lock.expireAt > now) {
267
+ await this.app.cache.set(
268
+ cacheKey,
269
+ {
270
+ id: userId,
271
+ expireAt: user.lock.expireAt,
272
+ lastChecked: now
273
+ },
274
+ this.CACHE_TTL
275
+ );
276
+ return true;
277
+ } else {
278
+ await this.app.cache.set(
279
+ cacheKey,
280
+ {
281
+ id: userId,
282
+ expireAt: null,
283
+ // null表示未被锁定
284
+ lastChecked: now
285
+ },
286
+ this.CACHE_TTL
287
+ );
288
+ return false;
289
+ }
290
+ } catch (error) {
291
+ this.logger.error(`Error checking locked status for user ${userId}:`, error);
292
+ return false;
293
+ }
294
+ }
295
+ /**
296
+ * 在后台刷新用户锁定缓存
297
+ * @param userId 用户ID
298
+ */
299
+ async refreshLockedUserCache(userId) {
300
+ var _a;
301
+ try {
302
+ const now = /* @__PURE__ */ new Date();
303
+ const userRepository = this.db.getRepository("users");
304
+ const user = await userRepository.findOne({
305
+ fields: ["id"],
306
+ filter: { id: userId },
307
+ appends: ["lock"]
308
+ });
309
+ const cacheKey = this.getUserLockCacheKey(userId);
310
+ if (user && ((_a = user.lock) == null ? void 0 : _a.expireAt) && user.lock.expireAt > now) {
311
+ await this.app.cache.set(
312
+ cacheKey,
313
+ {
314
+ id: userId,
315
+ expireAt: user.lock.expireAt,
316
+ lastChecked: now
317
+ },
318
+ this.CACHE_TTL
319
+ );
320
+ } else {
321
+ const existingInfo = await this.app.cache.get(cacheKey);
322
+ if (existingInfo) {
323
+ existingInfo.expireAt = null;
324
+ existingInfo.lastChecked = now;
325
+ await this.app.cache.set(cacheKey, existingInfo, this.CACHE_TTL);
326
+ } else {
327
+ await this.app.cache.set(
328
+ cacheKey,
329
+ {
330
+ id: userId,
331
+ expireAt: null,
332
+ lastChecked: now
333
+ },
334
+ this.CACHE_TTL
335
+ );
336
+ }
337
+ }
338
+ } catch (error) {
339
+ this.logger.error(`Error refreshing locked cache for user ${userId}:`, error);
340
+ }
341
+ }
342
+ async refreshConfig(config) {
343
+ this.config = {
344
+ windowSeconds: (config == null ? void 0 : config.get("windowSeconds")) || import_constants.WINDOW_SECONDS,
345
+ // 默认5分钟
346
+ maxAttempts: (config == null ? void 0 : config.get("maxAttempts")) ?? 0,
347
+ // 默认0,表示不启用防护
348
+ lockSeconds: (config == null ? void 0 : config.get("lockSeconds")) || import_constants.LOCK_SECONDS,
349
+ // 默认30分钟
350
+ strictLock: (config == null ? void 0 : config.get("strictLock")) || false
351
+ // 锁定时候禁止任意api
352
+ };
353
+ if (this.config.maxAttempts > 0) {
354
+ await this.loadRecentRecords();
355
+ } else {
356
+ this.logger.info("Sign-in failure protection is disabled (maxAttempts = 0)");
357
+ }
358
+ }
359
+ addMiddleWare() {
360
+ this.app.resourcer.use(
361
+ async (ctx, next) => {
362
+ var _a;
363
+ const { resourceName, actionName } = ctx.action.params;
364
+ if (resourceName === "auth" && actionName === "signIn") {
365
+ const { account, password, email } = ctx.action.params.values;
366
+ if (account && password) {
367
+ const filter = email ? { email } : {
368
+ $or: [{ username: account }, { email: account }]
369
+ };
370
+ const userRepository = ctx.db.getRepository("users");
371
+ const user = await userRepository.findOne({
372
+ fields: ["id", "password"],
373
+ filter,
374
+ appends: ["lock"]
375
+ });
376
+ if (user) {
377
+ if (((_a = user.lock) == null ? void 0 : _a.expireAt) && user.lock.expireAt > /* @__PURE__ */ new Date()) {
378
+ ctx.throw(403, ctx.t("User has been locked", { ns: import_constants.NAMESPACE }));
379
+ }
380
+ const field = userRepository.collection.getField("password");
381
+ const valid = await field.verify(password, user.password);
382
+ if (!valid) {
383
+ await this.recordFailedAttempt(user, ctx.state.clientIp);
384
+ } else {
385
+ await this.resetFailedAttempts(user.id);
386
+ }
387
+ }
388
+ }
389
+ }
390
+ await next();
391
+ },
392
+ {
393
+ tag: "lockUserByPasswordPolicy",
394
+ before: "auth"
395
+ }
396
+ );
397
+ this.app.resourcer.use(
398
+ async (ctx, next) => {
399
+ if (this.config.strictLock && ctx.state.currentUser) {
400
+ const userId = ctx.state.currentUser.id;
401
+ const isLocked = await this.isUserLocked(userId);
402
+ if (isLocked) {
403
+ ctx.throw(403, ctx.t("User has been locked", { ns: import_constants.NAMESPACE }));
404
+ }
405
+ }
406
+ await next();
407
+ },
408
+ { tag: "lockAllResource", after: "auth", before: "acl" }
409
+ );
410
+ }
411
+ /**
412
+ * 从数据库加载最近的失败记录到内存
413
+ */
414
+ async loadRecentRecords() {
415
+ try {
416
+ const cutoffTime = /* @__PURE__ */ new Date();
417
+ cutoffTime.setSeconds(cutoffTime.getSeconds() - Math.max(this.config.windowSeconds, this.config.lockSeconds));
418
+ const records = await this.db.getRepository("signInFails").find({
419
+ filter: {
420
+ createdAt: {
421
+ $gt: cutoffTime
422
+ },
423
+ status: true
424
+ }
425
+ });
426
+ this.failureRecords.clear();
427
+ records.forEach((record) => {
428
+ const userId = record.get("userId");
429
+ const createdAt = record.get("createdAt");
430
+ const userRecords = this.failureRecords.get(userId) || [];
431
+ userRecords.push({ userId, createdAt });
432
+ this.failureRecords.set(userId, userRecords);
433
+ });
434
+ this.logger.info(`Loaded ${records.length} recent sign-in failure records into cache`);
435
+ } catch (error) {
436
+ this.logger.error("Failed to load recent sign-in failure records:", error);
437
+ }
438
+ }
439
+ /**
440
+ * 获取IP地址的地理位置信息
441
+ * @param ip IP地址
442
+ * @returns 地理位置信息
443
+ */
444
+ getGeoLocation(ip) {
445
+ try {
446
+ if (!ip || ip === "127.0.0.1" || ip === "localhost" || ip.startsWith("192.168.") || ip.startsWith("10.")) {
447
+ return { country: "Local", region: "Local", city: "Local" };
448
+ }
449
+ const geo = geoip.lookup(ip);
450
+ if (!geo) {
451
+ return {};
452
+ }
453
+ return {
454
+ country: geo.country,
455
+ region: geo.region,
456
+ city: geo.city
457
+ };
458
+ } catch (error) {
459
+ this.logger.error(`Failed to get geo location for IP ${ip}:`, error);
460
+ return {};
461
+ }
462
+ }
463
+ /**
464
+ * 记录登录失败
465
+ * @param username 用户名
466
+ */
467
+ async recordFailedAttempt(user, ip) {
468
+ var _a, _b;
469
+ try {
470
+ const now = /* @__PURE__ */ new Date();
471
+ if (this.config.maxAttempts === 0) {
472
+ await this.recordFailedAttemptToDb(user, ip, now, false);
473
+ return;
474
+ }
475
+ if (((_a = user.lock) == null ? void 0 : _a.expireAt) && now > user.lock.expireAt) {
476
+ await this.resetFailedAttempts(user.id);
477
+ } else if (this.getRecentFailureCount(user.id) + 1 >= this.config.maxAttempts) {
478
+ if (!((_b = user.lock) == null ? void 0 : _b.expireAt) || now > user.lock.expireAt) {
479
+ const lockExpireAt = new Date(now);
480
+ lockExpireAt.setSeconds(lockExpireAt.getSeconds() + this.config.lockSeconds);
481
+ await this.db.sequelize.transaction(async (transaction) => {
482
+ const existOne = await this.db.getRepository("userLocks").findOne({
483
+ filter: {
484
+ userId: user.id
485
+ },
486
+ transaction
487
+ });
488
+ if (existOne) {
489
+ await existOne.update(
490
+ {
491
+ expireAt: lockExpireAt
492
+ },
493
+ {
494
+ transaction
495
+ }
496
+ );
497
+ } else {
498
+ await this.db.getRepository("userLocks").create({
499
+ values: {
500
+ userId: user.id,
501
+ expireAt: lockExpireAt
502
+ },
503
+ transaction
504
+ });
505
+ }
506
+ });
507
+ await this.app.cache.set(
508
+ this.getUserLockCacheKey(user.id),
509
+ {
510
+ id: user.id,
511
+ expireAt: lockExpireAt,
512
+ lastChecked: now
513
+ },
514
+ this.CACHE_TTL
515
+ );
516
+ }
517
+ }
518
+ const record = await this.recordFailedAttemptToDb(user, ip, now, true);
519
+ if (this.failureRecords.get(user.id)) {
520
+ this.failureRecords.get(user.id).push({
521
+ userId: user.id,
522
+ createdAt: record.get("createdAt")
523
+ });
524
+ } else {
525
+ this.failureRecords.set(user.id, [
526
+ {
527
+ userId: user.id,
528
+ createdAt: record.get("createdAt")
529
+ }
530
+ ]);
531
+ }
532
+ } catch (error) {
533
+ this.logger.error("Failed to record sign-in failure:", error);
534
+ throw error;
535
+ }
536
+ }
537
+ async recordFailedAttemptToDb(user, ip, now, recordUser = false) {
538
+ let geoLocation = {};
539
+ if (ip) {
540
+ geoLocation = this.getGeoLocation(ip);
541
+ }
542
+ const address = `${geoLocation.country || ""} ${geoLocation.region || ""} ${geoLocation.city || ""}`.trim();
543
+ return this.db.getRepository("signInFails").create({
544
+ values: {
545
+ userId: user.id,
546
+ status: recordUser,
547
+ ip,
548
+ address,
549
+ createdAt: now
550
+ }
551
+ });
552
+ }
553
+ /**
554
+ * 获取最近的失败次数(从内存缓存中获取)
555
+ */
556
+ getRecentFailureCount(userId) {
557
+ if (this.config.maxAttempts === 0) {
558
+ return 0;
559
+ }
560
+ const userRecords = this.failureRecords.get(userId) || [];
561
+ const windowStart = /* @__PURE__ */ new Date();
562
+ windowStart.setSeconds(windowStart.getSeconds() - this.config.windowSeconds);
563
+ return userRecords.filter((record) => record.createdAt > windowStart).length;
564
+ }
565
+ /**
566
+ * 重置用户的失败记录
567
+ * @param username 用户名
568
+ */
569
+ async resetFailedAttempts(userId) {
570
+ if (this.config.maxAttempts === 0) {
571
+ return;
572
+ }
573
+ try {
574
+ await this.db.getRepository("userLocks").destroy({
575
+ filter: {
576
+ userId
577
+ }
578
+ });
579
+ this.logger.info(`Reset sign-in failure records for user ${userId}`);
580
+ } catch (error) {
581
+ this.logger.error(`Failed to reset sign-in failure records for user ${userId}:`, error);
582
+ throw error;
583
+ }
584
+ }
585
+ }
586
+ _init = __decoratorStart(null);
587
+ __decorateElement(_init, 5, "db", _db_dec, PasswordAttemptService);
588
+ __decorateElement(_init, 5, "app", _app_dec, PasswordAttemptService);
589
+ __decorateElement(_init, 5, "logger", _logger_dec, PasswordAttemptService);
590
+ PasswordAttemptService = __decorateElement(_init, 0, "PasswordAttemptService", _PasswordAttemptService_decorators, PasswordAttemptService);
591
+ __runInitializers(_init, 1, PasswordAttemptService);
592
+ // Annotate the CommonJS export names for ESM import in node:
593
+ 0 && (module.exports = {
594
+ PasswordAttemptService
595
+ });
@@ -0,0 +1,28 @@
1
+ import { Context } from '@tachybase/actions';
2
+ import Database from '@tachybase/database';
3
+ import { Application } from '@tachybase/server';
4
+ export declare class PasswordStrengthService {
5
+ db: Database;
6
+ app: Application;
7
+ private logger;
8
+ private config;
9
+ load(): Promise<void>;
10
+ refreshConfig(config: any): Promise<void>;
11
+ addMiddleware(): void;
12
+ /**
13
+ * 获取用户名通过ID
14
+ */
15
+ private getUsernameById;
16
+ /**
17
+ * 验证密码强度
18
+ */
19
+ validatePasswordStrength(ctx: Context, password: string, username?: string): Promise<void>;
20
+ /**
21
+ * 验证密码历史
22
+ */
23
+ private validatePasswordHistory;
24
+ /**
25
+ * 保存密码历史
26
+ */
27
+ private savePasswordHistory;
28
+ }