@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.
- package/README.md +1 -0
- package/client.d.ts +2 -0
- package/client.js +1 -0
- package/dist/client/IPFilterForm.d.ts +1 -0
- package/dist/client/PasswordAttemptForm.d.ts +1 -0
- package/dist/client/PasswordStrengthSettingsForm.d.ts +2 -0
- package/dist/client/SignInFailsTable.d.ts +2 -0
- package/dist/client/UserLocksTable.d.ts +2 -0
- package/dist/client/collections/signInFails.d.ts +2 -0
- package/dist/client/collections/userLocks.d.ts +2 -0
- package/dist/client/hooks/usePasswordStrength.d.ts +11 -0
- package/dist/client/hooks/usePasswordValidator.d.ts +16 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +4 -0
- package/dist/client/locale.d.ts +6 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.js +44 -0
- package/dist/externalVersion.js +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +39 -0
- package/dist/locale/en-US.json +107 -0
- package/dist/locale/zh-CN.json +107 -0
- package/dist/node_modules/geoip-lite/LICENSE +50 -0
- package/dist/node_modules/geoip-lite/data/city.checksum +1 -0
- package/dist/node_modules/geoip-lite/data/country.checksum +1 -0
- package/dist/node_modules/geoip-lite/data/geoip-city-names.dat +0 -0
- package/dist/node_modules/geoip-lite/data/geoip-city.dat +0 -0
- package/dist/node_modules/geoip-lite/data/geoip-city6.dat +0 -0
- package/dist/node_modules/geoip-lite/data/geoip-country.dat +0 -0
- package/dist/node_modules/geoip-lite/data/geoip-country6.dat +0 -0
- package/dist/node_modules/geoip-lite/lib/fsWatcher.js +83 -0
- package/dist/node_modules/geoip-lite/lib/geoip.js +1 -0
- package/dist/node_modules/geoip-lite/lib/utils.js +98 -0
- package/dist/node_modules/geoip-lite/node_modules/.bin/rimraf +17 -0
- package/dist/node_modules/geoip-lite/package.json +1 -0
- package/dist/node_modules/geoip-lite/scripts/updatedb.js +685 -0
- package/dist/node_modules/geoip-lite/test/geo-lookup.js +56 -0
- package/dist/node_modules/geoip-lite/test/memory_usage.js +3 -0
- package/dist/node_modules/geoip-lite/test/tests.js +197 -0
- package/dist/server/actions/IpFilterController.d.ts +7 -0
- package/dist/server/actions/IpFilterController.js +124 -0
- package/dist/server/actions/PasswordAttemptController.d.ts +7 -0
- package/dist/server/actions/PasswordAttemptController.js +123 -0
- package/dist/server/actions/PasswordStrengthController.d.ts +7 -0
- package/dist/server/actions/PasswordStrengthController.js +123 -0
- package/dist/server/actions/SignInFailsController.d.ts +5 -0
- package/dist/server/actions/SignInFailsController.js +156 -0
- package/dist/server/actions/UserLocksController.d.ts +4 -0
- package/dist/server/actions/UserLocksController.js +102 -0
- package/dist/server/collections/ipFilter.d.ts +2 -0
- package/dist/server/collections/ipFilter.js +51 -0
- package/dist/server/collections/passwordAttempt.d.ts +2 -0
- package/dist/server/collections/passwordAttempt.js +55 -0
- package/dist/server/collections/passwordHistory.d.ts +2 -0
- package/dist/server/collections/passwordHistory.js +46 -0
- package/dist/server/collections/passwordStrengthConfig.d.ts +2 -0
- package/dist/server/collections/passwordStrengthConfig.js +59 -0
- package/dist/server/collections/signInFail.d.ts +2 -0
- package/dist/server/collections/signInFail.js +56 -0
- package/dist/server/collections/userLocks.d.ts +2 -0
- package/dist/server/collections/userLocks.js +51 -0
- package/dist/server/collections/users.d.ts +2 -0
- package/dist/server/collections/users.js +42 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +33 -0
- package/dist/server/plugin.d.ts +5 -0
- package/dist/server/plugin.js +129 -0
- package/dist/server/services/IPFilterService.d.ts +49 -0
- package/dist/server/services/IPFilterService.js +270 -0
- package/dist/server/services/PasswordAttemptService.d.ts +75 -0
- package/dist/server/services/PasswordAttemptService.js +595 -0
- package/dist/server/services/PasswordStrengthService.d.ts +28 -0
- package/dist/server/services/PasswordStrengthService.js +313 -0
- package/dist/types/geoip-lite.d.js +0 -0
- package/package.json +25 -0
- package/server.d.ts +2 -0
- 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
|
+
}
|