@h-ai/iam 0.1.0-alpha.59 → 0.1.0-alpha.61
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.en.md +212 -55
- package/README.ja.md +212 -57
- package/README.md +225 -57
- package/dist/index.d.ts +104 -9
- package/dist/index.js +665 -339
- package/dist/index.js.map +1 -1
- package/package.json +20 -6
package/dist/index.js
CHANGED
|
@@ -7,12 +7,13 @@ import { randomBytes } from 'crypto';
|
|
|
7
7
|
import { audit } from '@h-ai/audit';
|
|
8
8
|
|
|
9
9
|
// src/iam-config.ts
|
|
10
|
+
var WebUrlSchema = z.url().refine((value) => ["http:", "https:"].includes(new URL(value).protocol));
|
|
10
11
|
var AuthStrategyTypeSchema = z.enum(["password", "otp", "ldap", "apikey"]);
|
|
11
12
|
var PasswordConfigSchema = z.object({
|
|
12
13
|
/** 最小长度(默认 8) */
|
|
13
14
|
minLength: z.number().int().min(1).default(8),
|
|
14
15
|
/** 最大长度(默认 128) */
|
|
15
|
-
maxLength: z.number().int().max(256).default(128),
|
|
16
|
+
maxLength: z.number().int().min(1).max(256).default(128),
|
|
16
17
|
/** 需要大写字母 */
|
|
17
18
|
requireUppercase: z.boolean().default(true),
|
|
18
19
|
/** 需要小写字母 */
|
|
@@ -23,8 +24,11 @@ var PasswordConfigSchema = z.object({
|
|
|
23
24
|
requireSpecialChar: z.boolean().default(false),
|
|
24
25
|
/** 密码过期天数(0 表示不过期) */
|
|
25
26
|
expirationDays: z.number().int().min(0).default(0)
|
|
26
|
-
});
|
|
27
|
+
}).refine((config) => config.maxLength >= config.minLength, { message: "maxLength must be >= minLength", path: ["maxLength"] });
|
|
27
28
|
var OtpConfigSchema = z.object({
|
|
29
|
+
/** 分别启用邮件、短信验证码 */
|
|
30
|
+
email: z.boolean().default(true),
|
|
31
|
+
sms: z.boolean().default(true),
|
|
28
32
|
/** 验证码长度(默认 6) */
|
|
29
33
|
length: z.number().int().min(4).max(8).default(6),
|
|
30
34
|
/** 验证码过期时间(秒,默认 300) */
|
|
@@ -35,6 +39,12 @@ var OtpConfigSchema = z.object({
|
|
|
35
39
|
resendInterval: z.number().int().min(30).default(60)
|
|
36
40
|
});
|
|
37
41
|
var PasswordResetConfigSchema = z.object({
|
|
42
|
+
/** 重置通知渠道,phone 使用 password_reset_sms 模板。 */
|
|
43
|
+
mode: z.enum(["email", "phone"]).default("email"),
|
|
44
|
+
/** 重置页面的绝对地址,通知模板可使用 resetUrl。 */
|
|
45
|
+
resetUrl: WebUrlSchema.optional(),
|
|
46
|
+
/** 是否启用密码重置 */
|
|
47
|
+
enabled: z.boolean().default(true),
|
|
38
48
|
/** 重置令牌有效期(秒,默认 3600 = 1小时) */
|
|
39
49
|
tokenExpiresIn: z.number().int().min(300).default(3600),
|
|
40
50
|
/** 最大验证尝试次数(默认 3) */
|
|
@@ -42,7 +52,7 @@ var PasswordResetConfigSchema = z.object({
|
|
|
42
52
|
});
|
|
43
53
|
var LdapConfigSchema = z.object({
|
|
44
54
|
/** LDAP 服务器 URL */
|
|
45
|
-
url: z.
|
|
55
|
+
url: z.url().refine((value) => ["ldap:", "ldaps:"].includes(new URL(value).protocol)),
|
|
46
56
|
/** 绑定 DN */
|
|
47
57
|
bindDn: z.string(),
|
|
48
58
|
/** 绑定密码 */
|
|
@@ -75,8 +85,8 @@ var LoginConfigSchema = z.object({
|
|
|
75
85
|
password: z.boolean().default(true),
|
|
76
86
|
/** 是否启用 OTP 登录 */
|
|
77
87
|
otp: z.boolean().default(true),
|
|
78
|
-
/** 是否启用 LDAP
|
|
79
|
-
ldap: z.boolean().default(
|
|
88
|
+
/** 是否启用 LDAP 登录;需要显式开启并提供 ldap 配置。 */
|
|
89
|
+
ldap: z.boolean().default(false),
|
|
80
90
|
/** 是否启用 API Key 登录 */
|
|
81
91
|
apikey: z.boolean().default(false)
|
|
82
92
|
});
|
|
@@ -93,15 +103,29 @@ var SecurityConfigSchema = z.object({
|
|
|
93
103
|
lockoutDuration: z.number().int().min(60).default(900)
|
|
94
104
|
});
|
|
95
105
|
var AgreementConfigSchema = z.object({
|
|
106
|
+
/** 用户协议 Markdown 正文;同时配置 URL 时优先显示正文。 */
|
|
107
|
+
userAgreementMarkdown: z.string().min(1).optional(),
|
|
108
|
+
/** 隐私协议 Markdown 正文;YAML 使用 | 保留段落和换行。 */
|
|
109
|
+
privacyPolicyMarkdown: z.string().min(1).optional(),
|
|
96
110
|
/** 用户协议 URL */
|
|
97
|
-
userAgreementUrl:
|
|
111
|
+
userAgreementUrl: WebUrlSchema.optional(),
|
|
98
112
|
/** 隐私协议 URL */
|
|
99
|
-
privacyPolicyUrl:
|
|
113
|
+
privacyPolicyUrl: WebUrlSchema.optional(),
|
|
100
114
|
/** 注册时展示协议 */
|
|
101
115
|
showOnRegister: z.boolean().default(true),
|
|
102
116
|
/** 登录时展示协议 */
|
|
103
117
|
showOnLogin: z.boolean().default(false)
|
|
104
118
|
});
|
|
119
|
+
var AuthImageSchema = z.string().refine((value) => /^https?:\/\//.test(value) || value.startsWith("/") && !value.startsWith("//"));
|
|
120
|
+
var AuthUiConfigSchema = z.object({
|
|
121
|
+
variant: z.enum(["card", "split"]).default("card"),
|
|
122
|
+
title: z.string().optional(),
|
|
123
|
+
subtitle: z.string().optional(),
|
|
124
|
+
brandTitle: z.string().optional(),
|
|
125
|
+
brandText: z.string().optional(),
|
|
126
|
+
logoUrl: AuthImageSchema.optional(),
|
|
127
|
+
illustrationUrl: AuthImageSchema.optional()
|
|
128
|
+
});
|
|
105
129
|
var SessionConfigSchema = z.object({
|
|
106
130
|
/** 会话超时时间(秒,默认 86400 = 24小时) */
|
|
107
131
|
maxAge: z.number().int().min(60).default(86400),
|
|
@@ -121,6 +145,8 @@ var RbacConfigSchema = z.object({
|
|
|
121
145
|
defaultRole: z.string().default("user")
|
|
122
146
|
});
|
|
123
147
|
var IamConfigSchema = z.object({
|
|
148
|
+
/** 认证页外观 */
|
|
149
|
+
ui: AuthUiConfigSchema.optional(),
|
|
124
150
|
// ─── 认证策略配置 ───
|
|
125
151
|
/** 密码配置 */
|
|
126
152
|
password: PasswordConfigSchema.optional(),
|
|
@@ -136,7 +162,7 @@ var IamConfigSchema = z.object({
|
|
|
136
162
|
login: LoginConfigSchema.default({
|
|
137
163
|
password: true,
|
|
138
164
|
otp: true,
|
|
139
|
-
ldap:
|
|
165
|
+
ldap: false,
|
|
140
166
|
apikey: false
|
|
141
167
|
}),
|
|
142
168
|
/** 注册配置 */
|
|
@@ -159,7 +185,7 @@ var IamConfigSchema = z.object({
|
|
|
159
185
|
session: SessionConfigSchema.optional(),
|
|
160
186
|
// ─── 授权配置 ───
|
|
161
187
|
/** RBAC 配置 */
|
|
162
|
-
rbac: RbacConfigSchema.
|
|
188
|
+
rbac: RbacConfigSchema.default(() => RbacConfigSchema.parse({})),
|
|
163
189
|
// ─── 运行时选项 ───
|
|
164
190
|
/** 是否初始化默认角色和权限(默认 true) */
|
|
165
191
|
seedDefaultData: z.boolean().default(true)
|
|
@@ -168,6 +194,10 @@ var IamConfigSchema = z.object({
|
|
|
168
194
|
// messages/en-US.json
|
|
169
195
|
var en_US_default = {
|
|
170
196
|
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
197
|
+
iam_securityStateFailed: "Unable to update authentication security state. Please try again later.",
|
|
198
|
+
iam_securityBusy: "Too many authentication requests. Please try again later.",
|
|
199
|
+
iam_passwordResetDisabled: "Password reset is disabled or delivery is not configured.",
|
|
200
|
+
iam_passwordResetDeliveryFailed: "Unable to deliver the reset notification. Please try again later.",
|
|
171
201
|
iam_passwordMinLength: "Password must be at least {minLength} characters",
|
|
172
202
|
iam_passwordMaxLength: "Password cannot exceed {maxLength} characters",
|
|
173
203
|
iam_passwordNeedUppercase: "Password must contain uppercase letters",
|
|
@@ -290,7 +320,7 @@ var en_US_default = {
|
|
|
290
320
|
iam_queryOtpFailed: "Failed to query OTP: {message}",
|
|
291
321
|
iam_updateOtpAttemptsFailed: "Failed to update OTP attempts: {message}",
|
|
292
322
|
iam_deleteOtpFailed: "Failed to delete OTP: {message}",
|
|
293
|
-
iam_otpSendFailed: "Failed to send
|
|
323
|
+
iam_otpSendFailed: "Failed to send verification code. Please try again later.",
|
|
294
324
|
iam_resetTokenInvalid: "Invalid or expired password reset token",
|
|
295
325
|
iam_resetTokenMaxAttempts: "Too many verification attempts, please request a new reset token",
|
|
296
326
|
iam_saveResetTokenFailed: "Failed to save password reset token: {message}",
|
|
@@ -351,6 +381,10 @@ var en_US_default = {
|
|
|
351
381
|
// messages/zh-CN.json
|
|
352
382
|
var zh_CN_default = {
|
|
353
383
|
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
384
|
+
iam_securityStateFailed: "\u65E0\u6CD5\u66F4\u65B0\u8BA4\u8BC1\u5B89\u5168\u72B6\u6001\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
385
|
+
iam_securityBusy: "\u8BA4\u8BC1\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
386
|
+
iam_passwordResetDisabled: "\u5BC6\u7801\u91CD\u7F6E\u672A\u542F\u7528\u6216\u901A\u77E5\u6E20\u9053\u672A\u914D\u7F6E",
|
|
387
|
+
iam_passwordResetDeliveryFailed: "\u91CD\u7F6E\u901A\u77E5\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
354
388
|
iam_passwordMinLength: "\u5BC6\u7801\u957F\u5EA6\u81F3\u5C11\u4E3A {minLength} \u4E2A\u5B57\u7B26",
|
|
355
389
|
iam_passwordMaxLength: "\u5BC6\u7801\u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 {maxLength} \u4E2A\u5B57\u7B26",
|
|
356
390
|
iam_passwordNeedUppercase: "\u5BC6\u7801\u5FC5\u987B\u5305\u542B\u5927\u5199\u5B57\u6BCD",
|
|
@@ -473,7 +507,7 @@ var zh_CN_default = {
|
|
|
473
507
|
iam_queryOtpFailed: "\u67E5\u8BE2 OTP \u5931\u8D25: {message}",
|
|
474
508
|
iam_updateOtpAttemptsFailed: "\u66F4\u65B0 OTP \u5C1D\u8BD5\u6B21\u6570\u5931\u8D25: {message}",
|
|
475
509
|
iam_deleteOtpFailed: "\u5220\u9664 OTP \u5931\u8D25: {message}",
|
|
476
|
-
iam_otpSendFailed: "\u9A8C\u8BC1\u7801\u53D1\u9001\u5931\u8D25
|
|
510
|
+
iam_otpSendFailed: "\u9A8C\u8BC1\u7801\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
477
511
|
iam_resetTokenInvalid: "\u5BC6\u7801\u91CD\u7F6E\u4EE4\u724C\u65E0\u6548\u6216\u5DF2\u8FC7\u671F",
|
|
478
512
|
iam_resetTokenMaxAttempts: "\u9A8C\u8BC1\u6B21\u6570\u8FC7\u591A\uFF0C\u8BF7\u91CD\u65B0\u7533\u8BF7\u91CD\u7F6E\u4EE4\u724C",
|
|
479
513
|
iam_saveResetTokenFailed: "\u4FDD\u5B58\u5BC6\u7801\u91CD\u7F6E\u4EE4\u724C\u5931\u8D25: {message}",
|
|
@@ -559,6 +593,8 @@ var IamErrorInfo = {
|
|
|
559
593
|
RESET_TOKEN_EXPIRED: "020:400",
|
|
560
594
|
RESET_TOKEN_MAX_ATTEMPTS: "021:429",
|
|
561
595
|
OTP_EXPIRED: "022:400",
|
|
596
|
+
SECURITY_BUSY: "023:429",
|
|
597
|
+
PASSWORD_RESET_DISABLED: "024:403",
|
|
562
598
|
SESSION_NOT_FOUND: "101:401",
|
|
563
599
|
SESSION_EXPIRED: "102:401",
|
|
564
600
|
SESSION_INVALID: "103:401",
|
|
@@ -792,9 +828,11 @@ var DbApiKeyRepository = class extends BaseReldbCrudRepository {
|
|
|
792
828
|
function buildAgreementDisplay(config, enabled) {
|
|
793
829
|
if (!enabled)
|
|
794
830
|
return void 0;
|
|
795
|
-
if (!config.userAgreementUrl && !config.privacyPolicyUrl)
|
|
831
|
+
if (!config.userAgreementMarkdown && !config.privacyPolicyMarkdown && !config.userAgreementUrl && !config.privacyPolicyUrl)
|
|
796
832
|
return void 0;
|
|
797
833
|
return {
|
|
834
|
+
userAgreementMarkdown: config.userAgreementMarkdown,
|
|
835
|
+
privacyPolicyMarkdown: config.privacyPolicyMarkdown,
|
|
798
836
|
userAgreementUrl: config.userAgreementUrl,
|
|
799
837
|
privacyPolicyUrl: config.privacyPolicyUrl,
|
|
800
838
|
showOnRegister: config.showOnRegister,
|
|
@@ -962,6 +1000,40 @@ var DbUserRepository = class extends BaseReldbCrudRepository {
|
|
|
962
1000
|
fields: USER_FIELDS
|
|
963
1001
|
});
|
|
964
1002
|
}
|
|
1003
|
+
async recordLoginFailure(userId, policy) {
|
|
1004
|
+
const sqlite = reldb.config?.type === "sqlite";
|
|
1005
|
+
const now = sqlite ? Date.now() : /* @__PURE__ */ new Date();
|
|
1006
|
+
const deadline = Date.now() + policy.lockoutDuration * 1e3;
|
|
1007
|
+
const lockedUntil = sqlite ? deadline : new Date(deadline);
|
|
1008
|
+
const expired = await reldb.sql.execute(`UPDATE ${TABLE_NAME2} SET login_failed_count = 0, locked_until = NULL, last_login_failed_at = NULL WHERE id = ? AND locked_until <= ?`, [userId, now]);
|
|
1009
|
+
if (!expired.success)
|
|
1010
|
+
return this.buildQueryError(expired.error, expired.error);
|
|
1011
|
+
const result = await reldb.sql.execute(
|
|
1012
|
+
`UPDATE ${TABLE_NAME2} SET locked_until = CASE WHEN COALESCE(login_failed_count, 0) + 1 >= ? THEN ? ELSE locked_until END, login_failed_count = COALESCE(login_failed_count, 0) + 1, last_login_failed_at = ? WHERE id = ? AND locked_until IS NULL`,
|
|
1013
|
+
[policy.maxLoginAttempts, lockedUntil, now, userId]
|
|
1014
|
+
);
|
|
1015
|
+
if (!result.success)
|
|
1016
|
+
return this.buildQueryError(result.error, result.error);
|
|
1017
|
+
return ok(void 0);
|
|
1018
|
+
}
|
|
1019
|
+
async resetLoginFailures(userId) {
|
|
1020
|
+
const result = await reldb.sql.execute(
|
|
1021
|
+
`UPDATE ${TABLE_NAME2} SET login_failed_count = 0, last_login_failed_at = NULL, locked_until = NULL WHERE id = ? AND enabled = ? AND (locked_until IS NULL OR locked_until <= ?)`,
|
|
1022
|
+
[userId, reldb.config?.type === "postgresql" ? true : 1, reldb.config?.type === "sqlite" ? Date.now() : /* @__PURE__ */ new Date()]
|
|
1023
|
+
);
|
|
1024
|
+
if (!result.success)
|
|
1025
|
+
return this.buildQueryError(result.error, result.error);
|
|
1026
|
+
if (result.data.changes === 0) {
|
|
1027
|
+
const user = await this.findById(userId);
|
|
1028
|
+
if (!user.success)
|
|
1029
|
+
return user;
|
|
1030
|
+
if (!user.data?.enabled)
|
|
1031
|
+
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
1032
|
+
if (user.data.lockedUntil && user.data.lockedUntil > /* @__PURE__ */ new Date())
|
|
1033
|
+
return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
|
|
1034
|
+
}
|
|
1035
|
+
return ok(void 0);
|
|
1036
|
+
}
|
|
965
1037
|
/** 根据用户名查找用户 */
|
|
966
1038
|
async findByUsername(username, tx) {
|
|
967
1039
|
return this.findOneBy("username = ?", [username], tx);
|
|
@@ -1064,24 +1136,10 @@ function isAccountLocked(user) {
|
|
|
1064
1136
|
return /* @__PURE__ */ new Date() < user.lockedUntil;
|
|
1065
1137
|
}
|
|
1066
1138
|
async function recordLoginFailure(userRepository, user, policy) {
|
|
1067
|
-
|
|
1068
|
-
const updateData = {
|
|
1069
|
-
loginFailedCount: failedCount,
|
|
1070
|
-
lastLoginFailedAt: /* @__PURE__ */ new Date()
|
|
1071
|
-
};
|
|
1072
|
-
if (failedCount >= policy.maxLoginAttempts) {
|
|
1073
|
-
updateData.lockedUntil = new Date(Date.now() + policy.lockoutDuration * 1e3);
|
|
1074
|
-
}
|
|
1075
|
-
await userRepository.updateById(user.id, updateData);
|
|
1139
|
+
return userRepository.recordLoginFailure(user.id, policy);
|
|
1076
1140
|
}
|
|
1077
1141
|
async function resetLoginFailures(userRepository, user) {
|
|
1078
|
-
|
|
1079
|
-
await userRepository.updateById(user.id, {
|
|
1080
|
-
loginFailedCount: 0,
|
|
1081
|
-
lastLoginFailedAt: void 0,
|
|
1082
|
-
lockedUntil: void 0
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1142
|
+
return userRepository.resetLoginFailures(user.id);
|
|
1085
1143
|
}
|
|
1086
1144
|
|
|
1087
1145
|
// src/authn/apikey/iam-authn-apikey-strategy.ts
|
|
@@ -1228,6 +1286,60 @@ function createApiKeyStrategy(config) {
|
|
|
1228
1286
|
};
|
|
1229
1287
|
return { strategy, apiKeyFunctions };
|
|
1230
1288
|
}
|
|
1289
|
+
async function loadLdapClientFactory() {
|
|
1290
|
+
const { Client } = await import('ldapts');
|
|
1291
|
+
return async (config) => {
|
|
1292
|
+
const client = new Client({ url: config.url, connectTimeout: config.connectTimeout, timeout: config.connectTimeout });
|
|
1293
|
+
try {
|
|
1294
|
+
if (config.useTls && config.url.startsWith("ldap://"))
|
|
1295
|
+
await client.startTLS({ minVersion: "TLSv1.2" });
|
|
1296
|
+
return ok({
|
|
1297
|
+
async bind(dn, password) {
|
|
1298
|
+
if (!password)
|
|
1299
|
+
return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
|
|
1300
|
+
try {
|
|
1301
|
+
await client.bind(dn, password);
|
|
1302
|
+
return ok(void 0);
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
return err(HaiIamError.LDAP_BIND_FAILED, iamM("iam_passwordWrong"), error);
|
|
1305
|
+
}
|
|
1306
|
+
},
|
|
1307
|
+
async search(base, filter, attributes) {
|
|
1308
|
+
try {
|
|
1309
|
+
const result = await client.search(base, { scope: "sub", filter, attributes });
|
|
1310
|
+
return ok(result.searchEntries.map((entry) => {
|
|
1311
|
+
const values = {};
|
|
1312
|
+
for (const attribute of attributes) {
|
|
1313
|
+
const value = entry[attribute];
|
|
1314
|
+
if (typeof value === "string")
|
|
1315
|
+
values[attribute] = value;
|
|
1316
|
+
else if (Array.isArray(value))
|
|
1317
|
+
values[attribute] = value.map((item) => String(item));
|
|
1318
|
+
else if (value !== void 0)
|
|
1319
|
+
values[attribute] = String(value);
|
|
1320
|
+
}
|
|
1321
|
+
return { dn: entry.dn, attributes: values };
|
|
1322
|
+
}));
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
return err(HaiIamError.LDAP_SEARCH_FAILED, iamM("iam_ldapSearchFailed"), error);
|
|
1325
|
+
}
|
|
1326
|
+
},
|
|
1327
|
+
async unbind() {
|
|
1328
|
+
try {
|
|
1329
|
+
await client.unbind();
|
|
1330
|
+
return ok(void 0);
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
return err(HaiIamError.LDAP_CONNECTION_FAILED, iamM("iam_ldapConnectionFailed"), error);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
await client.unbind().catch(() => {
|
|
1338
|
+
});
|
|
1339
|
+
return err(HaiIamError.LDAP_CONNECTION_FAILED, iamM("iam_ldapConnectionFailed"), error);
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1231
1343
|
var logger2 = core.logger.child({ module: "iam", scope: "ldap-strategy" });
|
|
1232
1344
|
function escapeLdapFilterValue(value) {
|
|
1233
1345
|
return value.replace(/[\\*()/\0]/g, (ch) => {
|
|
@@ -1276,6 +1388,8 @@ function createLdapStrategy(config) {
|
|
|
1276
1388
|
return credentialResult;
|
|
1277
1389
|
}
|
|
1278
1390
|
const { username, password } = credentialResult.data;
|
|
1391
|
+
if (!username.trim() || !password)
|
|
1392
|
+
return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
|
|
1279
1393
|
const clientResult = await ldapClientFactory(ldapConfig);
|
|
1280
1394
|
if (!clientResult.success) {
|
|
1281
1395
|
return err(
|
|
@@ -1340,7 +1454,9 @@ function createLdapStrategy(config) {
|
|
|
1340
1454
|
const userBindResult = await client.bind(entry.dn, password);
|
|
1341
1455
|
if (!userBindResult.success) {
|
|
1342
1456
|
if (storedUser) {
|
|
1343
|
-
await recordLoginFailure(userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
|
|
1457
|
+
const failureResult = await recordLoginFailure(userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
|
|
1458
|
+
if (!failureResult.success)
|
|
1459
|
+
return failureResult;
|
|
1344
1460
|
}
|
|
1345
1461
|
logger2.warn("LDAP authentication failed", { username });
|
|
1346
1462
|
return err(
|
|
@@ -1353,7 +1469,7 @@ function createLdapStrategy(config) {
|
|
|
1353
1469
|
username: ldapUsername,
|
|
1354
1470
|
email: ldapEmail,
|
|
1355
1471
|
displayName: ldapDisplayName,
|
|
1356
|
-
enabled: true,
|
|
1472
|
+
enabled: config.defaultEnabled ?? true,
|
|
1357
1473
|
emailVerified: !!ldapEmail,
|
|
1358
1474
|
metadata: {
|
|
1359
1475
|
ldapDn: entry.dn,
|
|
@@ -1374,7 +1490,11 @@ function createLdapStrategy(config) {
|
|
|
1374
1490
|
storedUser = createdResult.data;
|
|
1375
1491
|
if (storedUser && config.onUserAutoRegistered) {
|
|
1376
1492
|
try {
|
|
1377
|
-
await config.onUserAutoRegistered(storedUser.id);
|
|
1493
|
+
const assigned = await config.onUserAutoRegistered(storedUser.id);
|
|
1494
|
+
if (!assigned.success) {
|
|
1495
|
+
await userRepository.deleteById(storedUser.id);
|
|
1496
|
+
return assigned;
|
|
1497
|
+
}
|
|
1378
1498
|
} catch (callbackError) {
|
|
1379
1499
|
logger2.warn("onUserAutoRegistered callback failed", { userId: storedUser?.id, error: callbackError });
|
|
1380
1500
|
}
|
|
@@ -1400,7 +1520,9 @@ function createLdapStrategy(config) {
|
|
|
1400
1520
|
const ldapUser = buildLdapUser(entry, ldapUsername, ldapEmail, ldapDisplayName);
|
|
1401
1521
|
return ok(ldapUser);
|
|
1402
1522
|
}
|
|
1403
|
-
await resetLoginFailures(userRepository, storedUser);
|
|
1523
|
+
const resetResult = await resetLoginFailures(userRepository, storedUser);
|
|
1524
|
+
if (!resetResult.success)
|
|
1525
|
+
return resetResult;
|
|
1404
1526
|
logger2.info("LDAP authentication succeeded", { userId: storedUser.id });
|
|
1405
1527
|
return ok(toUser(storedUser));
|
|
1406
1528
|
} finally {
|
|
@@ -1455,7 +1577,9 @@ function createCacheOtpRepository() {
|
|
|
1455
1577
|
result.error
|
|
1456
1578
|
);
|
|
1457
1579
|
}
|
|
1458
|
-
await cache.kv.del(buildOtpAttemptsKey(identifier));
|
|
1580
|
+
const reset = await cache.kv.del(buildOtpAttemptsKey(identifier));
|
|
1581
|
+
if (!reset.success)
|
|
1582
|
+
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), reset.error);
|
|
1459
1583
|
return ok(void 0);
|
|
1460
1584
|
},
|
|
1461
1585
|
async fetchOtp(identifier) {
|
|
@@ -1471,6 +1595,8 @@ function createCacheOtpRepository() {
|
|
|
1471
1595
|
return ok(null);
|
|
1472
1596
|
}
|
|
1473
1597
|
const attemptsResult = await cache.kv.get(buildOtpAttemptsKey(identifier));
|
|
1598
|
+
if (!attemptsResult.success)
|
|
1599
|
+
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), attemptsResult.error);
|
|
1474
1600
|
const attempts = attemptsResult.success && typeof attemptsResult.data === "number" ? attemptsResult.data : 0;
|
|
1475
1601
|
const restored = restoreOtpDates(result.data);
|
|
1476
1602
|
return ok({ ...restored, attempts });
|
|
@@ -1498,8 +1624,12 @@ function createCacheOtpRepository() {
|
|
|
1498
1624
|
);
|
|
1499
1625
|
}
|
|
1500
1626
|
const ttlResult = await cache.kv.ttl(otpKey);
|
|
1627
|
+
if (!ttlResult.success)
|
|
1628
|
+
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), ttlResult.error);
|
|
1501
1629
|
const ttl = ttlResult.success && ttlResult.data > 0 ? ttlResult.data : 1;
|
|
1502
|
-
await cache.kv.expire(attemptsKey, ttl);
|
|
1630
|
+
const expire = await cache.kv.expire(attemptsKey, ttl);
|
|
1631
|
+
if (!expire.success)
|
|
1632
|
+
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), expire.error);
|
|
1503
1633
|
return ok(incrResult.data);
|
|
1504
1634
|
},
|
|
1505
1635
|
async removeOtp(identifier) {
|
|
@@ -1511,13 +1641,108 @@ function createCacheOtpRepository() {
|
|
|
1511
1641
|
result.error
|
|
1512
1642
|
);
|
|
1513
1643
|
}
|
|
1514
|
-
await cache.kv.del(buildOtpAttemptsKey(identifier));
|
|
1644
|
+
const removed = await cache.kv.del(buildOtpAttemptsKey(identifier));
|
|
1645
|
+
if (!removed.success)
|
|
1646
|
+
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), removed.error);
|
|
1515
1647
|
return ok(void 0);
|
|
1516
1648
|
}
|
|
1517
1649
|
};
|
|
1518
1650
|
otpRepoInstance = repo;
|
|
1519
1651
|
return repo;
|
|
1520
1652
|
}
|
|
1653
|
+
|
|
1654
|
+
// src/session/iam-session-utils.ts
|
|
1655
|
+
var BASE64_PLUS_REGEX = /\+/g;
|
|
1656
|
+
var BASE64_SLASH_REGEX = /\//g;
|
|
1657
|
+
var BASE64_TRAILING_PADDING_REGEX = /=+$/;
|
|
1658
|
+
function generateToken() {
|
|
1659
|
+
const bytes = new Uint8Array(32);
|
|
1660
|
+
crypto.getRandomValues(bytes);
|
|
1661
|
+
const base64 = btoa(String.fromCharCode(...bytes));
|
|
1662
|
+
return base64.replace(BASE64_PLUS_REGEX, "-").replace(BASE64_SLASH_REGEX, "_").replace(BASE64_TRAILING_PADDING_REGEX, "");
|
|
1663
|
+
}
|
|
1664
|
+
function buildSession(options, now, sessionTtl, accessToken) {
|
|
1665
|
+
return {
|
|
1666
|
+
userId: options.userId,
|
|
1667
|
+
username: options.username,
|
|
1668
|
+
displayName: options.displayName,
|
|
1669
|
+
avatarUrl: options.avatarUrl,
|
|
1670
|
+
roles: options.roles ?? [],
|
|
1671
|
+
permissions: options.permissions ?? [],
|
|
1672
|
+
source: options.source,
|
|
1673
|
+
accessToken,
|
|
1674
|
+
createdAt: now,
|
|
1675
|
+
lastActiveAt: now,
|
|
1676
|
+
expiresAt: new Date(now.getTime() + sessionTtl * 1e3),
|
|
1677
|
+
data: options.data
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function getSessionTtl(session, now = Date.now()) {
|
|
1681
|
+
return Math.max(0, Math.floor((session.expiresAt.getTime() - now) / 1e3));
|
|
1682
|
+
}
|
|
1683
|
+
function applySessionPatch(session, patch) {
|
|
1684
|
+
const nextSession = { ...session };
|
|
1685
|
+
if (patch.data !== void 0) {
|
|
1686
|
+
nextSession.data = { ...nextSession.data, ...patch.data };
|
|
1687
|
+
}
|
|
1688
|
+
if (patch.roles !== void 0) {
|
|
1689
|
+
nextSession.roles = patch.roles;
|
|
1690
|
+
}
|
|
1691
|
+
if (patch.permissions !== void 0) {
|
|
1692
|
+
nextSession.permissions = patch.permissions;
|
|
1693
|
+
}
|
|
1694
|
+
if (patch.username !== void 0) {
|
|
1695
|
+
nextSession.username = patch.username;
|
|
1696
|
+
}
|
|
1697
|
+
if (patch.displayName !== void 0) {
|
|
1698
|
+
nextSession.displayName = patch.displayName;
|
|
1699
|
+
}
|
|
1700
|
+
if (patch.avatarUrl !== void 0) {
|
|
1701
|
+
nextSession.avatarUrl = patch.avatarUrl;
|
|
1702
|
+
}
|
|
1703
|
+
if (patch.source !== void 0) {
|
|
1704
|
+
nextSession.source = patch.source;
|
|
1705
|
+
}
|
|
1706
|
+
nextSession.lastActiveAt = /* @__PURE__ */ new Date();
|
|
1707
|
+
return nextSession;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// src/iam-security.ts
|
|
1711
|
+
async function withIamLock(key, operation) {
|
|
1712
|
+
const owner = generateToken();
|
|
1713
|
+
const lockKey = `hai:iam:security:${key}`;
|
|
1714
|
+
for (let attempt = 0; attempt < 500; attempt++) {
|
|
1715
|
+
const acquired = await cache.lock.acquire(lockKey, { owner, ttl: 30 });
|
|
1716
|
+
if (!acquired.success)
|
|
1717
|
+
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), acquired.error);
|
|
1718
|
+
if (acquired.data) {
|
|
1719
|
+
let leaseValid = true;
|
|
1720
|
+
const renewal = setInterval(() => {
|
|
1721
|
+
void cache.lock.extend(lockKey, 30, owner).then((result) => {
|
|
1722
|
+
if (!result.success || !result.data)
|
|
1723
|
+
leaseValid = false;
|
|
1724
|
+
}).catch(() => {
|
|
1725
|
+
leaseValid = false;
|
|
1726
|
+
});
|
|
1727
|
+
}, 1e4);
|
|
1728
|
+
try {
|
|
1729
|
+
const result = await operation();
|
|
1730
|
+
if (!leaseValid)
|
|
1731
|
+
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"));
|
|
1732
|
+
return result;
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
return err(HaiIamError.INTERNAL_ERROR, iamM("iam_securityStateFailed"), error);
|
|
1735
|
+
} finally {
|
|
1736
|
+
clearInterval(renewal);
|
|
1737
|
+
await cache.lock.release(lockKey, owner);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1741
|
+
}
|
|
1742
|
+
return err(HaiIamError.SECURITY_BUSY, iamM("iam_securityBusy"));
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// src/authn/otp/iam-authn-otp-strategy.ts
|
|
1521
1746
|
var logger3 = core.logger.child({ module: "iam", scope: "otp-strategy" });
|
|
1522
1747
|
var PHONE_IDENTIFIER_REGEX = /^\+?\d{8,}$/;
|
|
1523
1748
|
var PHONE_SEPARATOR_REGEX = /[\s-]/g;
|
|
@@ -1559,98 +1784,136 @@ function createOtpStrategy(config) {
|
|
|
1559
1784
|
if (!credentialResult.success) {
|
|
1560
1785
|
return credentialResult;
|
|
1561
1786
|
}
|
|
1562
|
-
const
|
|
1563
|
-
const
|
|
1564
|
-
if (!
|
|
1565
|
-
return
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
1571
|
-
}
|
|
1572
|
-
if (isAccountLocked(storedUser)) {
|
|
1573
|
-
return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
|
|
1787
|
+
const identifier = normalizeOtpIdentifier(credentialResult.data.identifier);
|
|
1788
|
+
const { code } = credentialResult.data;
|
|
1789
|
+
if (!isChannelEnabled(identifier))
|
|
1790
|
+
return err(HaiIamError.LOGIN_DISABLED, iamM("iam_identifierTypeNotSupported"));
|
|
1791
|
+
return withIamLock(`otp:${identifier}`, async () => {
|
|
1792
|
+
const userResult = await config.userRepository.findByIdentifier(identifier);
|
|
1793
|
+
if (!userResult.success) {
|
|
1794
|
+
return userResult;
|
|
1574
1795
|
}
|
|
1575
|
-
|
|
1576
|
-
const storedOtpResult = await config.otpRepository.fetchOtp(identifier);
|
|
1577
|
-
if (!storedOtpResult.success) {
|
|
1578
|
-
return storedOtpResult;
|
|
1579
|
-
}
|
|
1580
|
-
const storedOtp = storedOtpResult.data;
|
|
1581
|
-
if (!storedOtp) {
|
|
1582
|
-
return err(HaiIamError.OTP_INVALID, iamM("iam_otpNotExistOrExpired"));
|
|
1583
|
-
}
|
|
1584
|
-
if (storedOtp.attempts >= otpConfig.maxAttempts) {
|
|
1585
|
-
await config.otpRepository.removeOtp(identifier);
|
|
1796
|
+
let storedUser = userResult.data;
|
|
1586
1797
|
if (storedUser) {
|
|
1587
|
-
|
|
1798
|
+
if (!storedUser.enabled) {
|
|
1799
|
+
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
1800
|
+
}
|
|
1801
|
+
if (isAccountLocked(storedUser)) {
|
|
1802
|
+
return err(HaiIamError.USER_LOCKED, iamM("iam_accountLocked"));
|
|
1803
|
+
}
|
|
1588
1804
|
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
await config.otpRepository.incrementOtpAttempts(identifier);
|
|
1593
|
-
if (storedUser) {
|
|
1594
|
-
await recordLoginFailure(config.userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
|
|
1805
|
+
const storedOtpResult = await config.otpRepository.fetchOtp(identifier);
|
|
1806
|
+
if (!storedOtpResult.success) {
|
|
1807
|
+
return storedOtpResult;
|
|
1595
1808
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
if (!storedUser && allowAutoRegister) {
|
|
1600
|
-
const type = identifierType(identifier);
|
|
1601
|
-
const createResult = await config.userRepository.create({
|
|
1602
|
-
username: identifier,
|
|
1603
|
-
email: type === "email" ? identifier : void 0,
|
|
1604
|
-
phone: type === "phone" ? identifier : void 0,
|
|
1605
|
-
enabled: defaultEnabled,
|
|
1606
|
-
emailVerified: type === "email",
|
|
1607
|
-
phoneVerified: type === "phone"
|
|
1608
|
-
});
|
|
1609
|
-
if (!createResult.success) {
|
|
1610
|
-
return err(
|
|
1611
|
-
HaiIamError.REPOSITORY_ERROR,
|
|
1612
|
-
iamM("iam_createUserFailed", { params: { message: createResult.error.message } }),
|
|
1613
|
-
createResult.error
|
|
1614
|
-
);
|
|
1809
|
+
const storedOtp = storedOtpResult.data;
|
|
1810
|
+
if (!storedOtp) {
|
|
1811
|
+
return err(HaiIamError.OTP_INVALID, iamM("iam_otpNotExistOrExpired"));
|
|
1615
1812
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1813
|
+
if (storedOtp.attempts >= otpConfig.maxAttempts) {
|
|
1814
|
+
const removed2 = await config.otpRepository.removeOtp(identifier);
|
|
1815
|
+
if (!removed2.success)
|
|
1816
|
+
return removed2;
|
|
1817
|
+
if (storedUser) {
|
|
1818
|
+
const failureResult = await recordLoginFailure(config.userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
|
|
1819
|
+
if (!failureResult.success)
|
|
1820
|
+
return failureResult;
|
|
1821
|
+
}
|
|
1822
|
+
return err(HaiIamError.OTP_INVALID, iamM("iam_otpInvalid"));
|
|
1619
1823
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1824
|
+
if (!core.string.constantTimeEqual(storedOtp.code, code)) {
|
|
1825
|
+
const attemptResult = await config.otpRepository.incrementOtpAttempts(identifier);
|
|
1826
|
+
if (!attemptResult.success)
|
|
1827
|
+
return attemptResult;
|
|
1828
|
+
if (attemptResult.data >= otpConfig.maxAttempts) {
|
|
1829
|
+
const removed2 = await config.otpRepository.removeOtp(identifier);
|
|
1830
|
+
if (!removed2.success)
|
|
1831
|
+
return removed2;
|
|
1832
|
+
}
|
|
1833
|
+
if (storedUser) {
|
|
1834
|
+
const failureResult = await recordLoginFailure(config.userRepository, storedUser, { maxLoginAttempts, lockoutDuration });
|
|
1835
|
+
if (!failureResult.success)
|
|
1836
|
+
return failureResult;
|
|
1626
1837
|
}
|
|
1838
|
+
return err(HaiIamError.OTP_INVALID, iamM("iam_otpWrong"));
|
|
1627
1839
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1840
|
+
const removed = await config.otpRepository.removeOtp(identifier);
|
|
1841
|
+
if (!removed.success)
|
|
1842
|
+
return removed;
|
|
1843
|
+
if (!storedUser && allowAutoRegister) {
|
|
1844
|
+
const type = identifierType(identifier);
|
|
1845
|
+
const createResult = await config.userRepository.create({
|
|
1846
|
+
username: identifier,
|
|
1847
|
+
email: type === "email" ? identifier : void 0,
|
|
1848
|
+
phone: type === "phone" ? identifier : void 0,
|
|
1849
|
+
enabled: defaultEnabled,
|
|
1850
|
+
emailVerified: type === "email",
|
|
1851
|
+
phoneVerified: type === "phone"
|
|
1852
|
+
});
|
|
1853
|
+
if (!createResult.success) {
|
|
1854
|
+
return err(
|
|
1855
|
+
HaiIamError.REPOSITORY_ERROR,
|
|
1856
|
+
iamM("iam_createUserFailed", { params: { message: createResult.error.message } }),
|
|
1857
|
+
createResult.error
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
const createdResult = await config.userRepository.findByIdentifier(identifier);
|
|
1861
|
+
if (!createdResult.success) {
|
|
1862
|
+
return createdResult;
|
|
1863
|
+
}
|
|
1864
|
+
storedUser = createdResult.data;
|
|
1865
|
+
if (storedUser && config.onUserAutoRegistered) {
|
|
1866
|
+
try {
|
|
1867
|
+
const assigned = await config.onUserAutoRegistered(storedUser.id);
|
|
1868
|
+
if (!assigned.success) {
|
|
1869
|
+
await config.userRepository.deleteById(storedUser.id);
|
|
1870
|
+
return assigned;
|
|
1871
|
+
}
|
|
1872
|
+
} catch (callbackError) {
|
|
1873
|
+
logger3.warn("onUserAutoRegistered callback failed", { userId: storedUser.id, error: callbackError });
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (!storedUser) {
|
|
1878
|
+
return err(HaiIamError.USER_NOT_FOUND, iamM("iam_userNotExist"));
|
|
1879
|
+
}
|
|
1880
|
+
if (!storedUser.enabled)
|
|
1881
|
+
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
1882
|
+
const resetResult = await resetLoginFailures(config.userRepository, storedUser);
|
|
1883
|
+
if (!resetResult.success)
|
|
1884
|
+
return resetResult;
|
|
1885
|
+
logger3.info("OTP authentication succeeded", { userId: storedUser.id });
|
|
1886
|
+
return ok(toUser(storedUser));
|
|
1887
|
+
});
|
|
1635
1888
|
}
|
|
1636
1889
|
};
|
|
1890
|
+
function isChannelEnabled(identifier) {
|
|
1891
|
+
const type = identifierType(identifier);
|
|
1892
|
+
return type === "email" && otpConfig.email && !!config.onOtpSendEmail || type === "phone" && otpConfig.sms && !!config.onOtpSendSms;
|
|
1893
|
+
}
|
|
1637
1894
|
async function challenge(identifier) {
|
|
1638
|
-
|
|
1639
|
-
if (
|
|
1640
|
-
|
|
1641
|
-
if (elapsedSeconds < otpConfig.resendInterval) {
|
|
1642
|
-
return err(
|
|
1643
|
-
HaiIamError.OTP_RESEND_TOO_FAST,
|
|
1644
|
-
iamM("iam_otpResendTooFast", { params: { seconds: otpConfig.resendInterval - elapsedSeconds } })
|
|
1645
|
-
);
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1895
|
+
identifier = normalizeOtpIdentifier(identifier);
|
|
1896
|
+
if (!isChannelEnabled(identifier))
|
|
1897
|
+
return err(HaiIamError.LOGIN_DISABLED, iamM("iam_identifierTypeNotSupported"));
|
|
1648
1898
|
const code = generateOtpCode(otpConfig.length);
|
|
1649
1899
|
const expiresAt = new Date(Date.now() + otpConfig.expiresIn * 1e3);
|
|
1650
|
-
const
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1900
|
+
const saved = await withIamLock(`otp:${identifier}`, async () => {
|
|
1901
|
+
const cooldownKey = `hai:iam:otp:cooldown:${identifier}`;
|
|
1902
|
+
const cooldown = await cache.kv.get(cooldownKey);
|
|
1903
|
+
if (!cooldown.success)
|
|
1904
|
+
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), cooldown.error);
|
|
1905
|
+
if (cooldown.data && cooldown.data > Date.now())
|
|
1906
|
+
return err(HaiIamError.OTP_RESEND_TOO_FAST, iamM("iam_otpResendTooFast", { params: { seconds: Math.ceil((cooldown.data - Date.now()) / 1e3) } }));
|
|
1907
|
+
const storeResult = await config.otpRepository.saveOtp(identifier, code, otpConfig.expiresIn);
|
|
1908
|
+
if (!storeResult.success)
|
|
1909
|
+
return storeResult;
|
|
1910
|
+
const limited = await cache.kv.set(cooldownKey, Date.now() + otpConfig.resendInterval * 1e3, { ex: otpConfig.resendInterval });
|
|
1911
|
+
if (!limited.success)
|
|
1912
|
+
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"), limited.error);
|
|
1913
|
+
return ok(void 0);
|
|
1914
|
+
});
|
|
1915
|
+
if (!saved.success)
|
|
1916
|
+
return saved;
|
|
1654
1917
|
const type = identifierType(identifier);
|
|
1655
1918
|
if (type === "email" && config.onOtpSendEmail) {
|
|
1656
1919
|
try {
|
|
@@ -1658,7 +1921,7 @@ function createOtpStrategy(config) {
|
|
|
1658
1921
|
} catch (error) {
|
|
1659
1922
|
return err(
|
|
1660
1923
|
HaiIamError.INTERNAL_ERROR,
|
|
1661
|
-
iamM("iam_otpSendFailed"
|
|
1924
|
+
iamM("iam_otpSendFailed"),
|
|
1662
1925
|
error
|
|
1663
1926
|
);
|
|
1664
1927
|
}
|
|
@@ -1668,7 +1931,7 @@ function createOtpStrategy(config) {
|
|
|
1668
1931
|
} catch (error) {
|
|
1669
1932
|
return err(
|
|
1670
1933
|
HaiIamError.INTERNAL_ERROR,
|
|
1671
|
-
iamM("iam_otpSendFailed"
|
|
1934
|
+
iamM("iam_otpSendFailed"),
|
|
1672
1935
|
error
|
|
1673
1936
|
);
|
|
1674
1937
|
}
|
|
@@ -1765,7 +2028,9 @@ function createPasswordStrategy(config) {
|
|
|
1765
2028
|
return mapPasswordError(verifyResult.error.message);
|
|
1766
2029
|
}
|
|
1767
2030
|
if (!verifyResult.data) {
|
|
1768
|
-
await recordLoginFailure(config.userRepository, user, { maxLoginAttempts, lockoutDuration });
|
|
2031
|
+
const failureResult = await recordLoginFailure(config.userRepository, user, { maxLoginAttempts, lockoutDuration });
|
|
2032
|
+
if (!failureResult.success)
|
|
2033
|
+
return failureResult;
|
|
1769
2034
|
logger4.warn("Password verification failed", { userId: user.id });
|
|
1770
2035
|
return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
|
|
1771
2036
|
}
|
|
@@ -1797,7 +2062,9 @@ function createPasswordStrategy(config) {
|
|
|
1797
2062
|
if (isPasswordExpired(storedUser)) {
|
|
1798
2063
|
return err(HaiIamError.PASSWORD_EXPIRED, iamM("iam_passwordExpired"));
|
|
1799
2064
|
}
|
|
1800
|
-
await resetLoginFailures(config.userRepository, storedUser);
|
|
2065
|
+
const resetResult = await resetLoginFailures(config.userRepository, storedUser);
|
|
2066
|
+
if (!resetResult.success)
|
|
2067
|
+
return resetResult;
|
|
1801
2068
|
logger4.info("Password authentication succeeded", { userId: storedUser.id });
|
|
1802
2069
|
return ok(toUser(storedUser));
|
|
1803
2070
|
}
|
|
@@ -1826,12 +2093,14 @@ async function createAuthnOperations(deps) {
|
|
|
1826
2093
|
const securityConfig = SecurityConfigSchema.parse(config.security ?? {});
|
|
1827
2094
|
const loginConfig = LoginConfigSchema.parse(config.login ?? {});
|
|
1828
2095
|
async function onUserAutoRegistered(userId) {
|
|
1829
|
-
if (!config.rbac?.defaultRole)
|
|
1830
|
-
return;
|
|
2096
|
+
if (!config.rbac?.enabled || !config.rbac.defaultRole)
|
|
2097
|
+
return ok(void 0);
|
|
1831
2098
|
const roleResult = await authzFunctions.getRoleByCode(config.rbac.defaultRole);
|
|
1832
|
-
if (roleResult.success
|
|
1833
|
-
|
|
1834
|
-
|
|
2099
|
+
if (!roleResult.success)
|
|
2100
|
+
return roleResult;
|
|
2101
|
+
if (!roleResult.data)
|
|
2102
|
+
return err(HaiIamError.ROLE_NOT_FOUND, iamM("iam_roleNotExist"));
|
|
2103
|
+
return authzFunctions.assignRole(userId, roleResult.data.id);
|
|
1835
2104
|
}
|
|
1836
2105
|
const passwordResult = createPasswordStrategy({
|
|
1837
2106
|
passwordConfig: config.password,
|
|
@@ -1857,12 +2126,14 @@ async function createAuthnOperations(deps) {
|
|
|
1857
2126
|
});
|
|
1858
2127
|
}
|
|
1859
2128
|
let ldapStrategy;
|
|
1860
|
-
if (loginConfig.ldap && config.ldap
|
|
2129
|
+
if (loginConfig.ldap && config.ldap) {
|
|
2130
|
+
const clientFactory = ldapClientFactory ?? await loadLdapClientFactory();
|
|
1861
2131
|
ldapStrategy = createLdapStrategy({
|
|
1862
2132
|
ldapConfig: config.ldap,
|
|
1863
2133
|
userRepository,
|
|
1864
|
-
ldapClientFactory,
|
|
2134
|
+
ldapClientFactory: clientFactory,
|
|
1865
2135
|
syncUser: ldapSyncUser ?? true,
|
|
2136
|
+
defaultEnabled: config.register.defaultEnabled,
|
|
1866
2137
|
maxLoginAttempts: securityConfig.maxLoginAttempts,
|
|
1867
2138
|
lockoutDuration: securityConfig.lockoutDuration,
|
|
1868
2139
|
onUserAutoRegistered
|
|
@@ -1892,7 +2163,7 @@ async function createAuthnOperations(deps) {
|
|
|
1892
2163
|
config
|
|
1893
2164
|
});
|
|
1894
2165
|
logger5.info("Authn sub-feature initialized");
|
|
1895
|
-
return ok({ authn: operations, passwordStrategyResult: passwordResult, apiKeyFunctions });
|
|
2166
|
+
return ok({ authn: operations.authn, loginRegisteredUser: operations.loginRegisteredUser, passwordStrategyResult: passwordResult, apiKeyFunctions });
|
|
1896
2167
|
} catch (error) {
|
|
1897
2168
|
logger5.error("Authn sub-feature initialization failed", { error });
|
|
1898
2169
|
return err(
|
|
@@ -1960,6 +2231,8 @@ function buildAuthnOperations(deps) {
|
|
|
1960
2231
|
);
|
|
1961
2232
|
}
|
|
1962
2233
|
async function buildAuthResult(user) {
|
|
2234
|
+
if (!user.enabled)
|
|
2235
|
+
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
1963
2236
|
const [roleCodesResult, permCodesResult] = await Promise.all([
|
|
1964
2237
|
resolveUserRoleCodes(user.id),
|
|
1965
2238
|
resolveUserPermissionCodes(user.id)
|
|
@@ -2015,7 +2288,7 @@ function buildAuthnOperations(deps) {
|
|
|
2015
2288
|
}
|
|
2016
2289
|
return result;
|
|
2017
2290
|
}
|
|
2018
|
-
return {
|
|
2291
|
+
return { loginRegisteredUser: buildAuthResult, authn: {
|
|
2019
2292
|
async login(credentials) {
|
|
2020
2293
|
return loginWithStrategy("password", credentials, passwordStrategy);
|
|
2021
2294
|
},
|
|
@@ -2056,7 +2329,7 @@ function buildAuthnOperations(deps) {
|
|
|
2056
2329
|
}
|
|
2057
2330
|
return otpChallenge(identifier);
|
|
2058
2331
|
}
|
|
2059
|
-
};
|
|
2332
|
+
} };
|
|
2060
2333
|
}
|
|
2061
2334
|
var TABLE_NAME3 = "hai_iam_permissions";
|
|
2062
2335
|
var PERMISSION_FIELDS = [
|
|
@@ -3461,6 +3734,62 @@ function createRbacManager(config) {
|
|
|
3461
3734
|
}
|
|
3462
3735
|
};
|
|
3463
3736
|
}
|
|
3737
|
+
|
|
3738
|
+
// src/iam-notifications.ts
|
|
3739
|
+
async function resolveIamNotifications(input) {
|
|
3740
|
+
const callbacks = { onOtpSendEmail: input.onOtpSendEmail, onOtpSendSms: input.onOtpSendSms, onPasswordResetRequest: input.onPasswordResetRequest };
|
|
3741
|
+
const reach = input.reach;
|
|
3742
|
+
if (!reach?.isInitialized)
|
|
3743
|
+
return callbacks;
|
|
3744
|
+
const needsEmail = input.login?.otp !== false && !!input.otp && input.otp.email !== false && !callbacks.onOtpSendEmail;
|
|
3745
|
+
const needsSms = input.login?.otp !== false && !!input.otp && input.otp.sms !== false && !callbacks.onOtpSendSms;
|
|
3746
|
+
const needsReset = input.passwordReset?.enabled !== false && !callbacks.onPasswordResetRequest;
|
|
3747
|
+
if (!needsEmail && !needsSms && !needsReset)
|
|
3748
|
+
return callbacks;
|
|
3749
|
+
const templates = await reach.template.list();
|
|
3750
|
+
if (!templates.success)
|
|
3751
|
+
throw new Error(iamM("iam_securityStateFailed"));
|
|
3752
|
+
const configured = new Map(templates.data.map((template) => [template.name, template.provider]));
|
|
3753
|
+
async function send(template, to, vars) {
|
|
3754
|
+
const provider = configured.get(template);
|
|
3755
|
+
if (!reach?.isInitialized || !provider || !to)
|
|
3756
|
+
throw new Error(iamM("iam_passwordResetDeliveryFailed"));
|
|
3757
|
+
const result = await reach.send({ provider, to, template, vars });
|
|
3758
|
+
if (!result.success || !result.data.success)
|
|
3759
|
+
throw new Error(iamM("iam_passwordResetDeliveryFailed"));
|
|
3760
|
+
}
|
|
3761
|
+
if (needsEmail && configured.has("otp_email"))
|
|
3762
|
+
callbacks.onOtpSendEmail = (email, code) => send("otp_email", email, { code });
|
|
3763
|
+
if (needsSms && configured.has("otp_sms"))
|
|
3764
|
+
callbacks.onOtpSendSms = (phone, code) => send("otp_sms", phone, { code });
|
|
3765
|
+
const resetTemplate = input.passwordReset?.mode === "phone" ? "password_reset_sms" : "password_reset";
|
|
3766
|
+
if (needsReset && configured.has(resetTemplate)) {
|
|
3767
|
+
callbacks.onPasswordResetRequest = (user, token, expiresAt) => {
|
|
3768
|
+
const url = input.passwordReset?.resetUrl ? new URL(input.passwordReset.resetUrl) : null;
|
|
3769
|
+
url?.searchParams.set("token", token);
|
|
3770
|
+
const vars = { token, expiresAt: expiresAt.toISOString(), resetUrl: url?.toString() ?? token };
|
|
3771
|
+
return send(resetTemplate, input.passwordReset?.mode === "phone" ? user.phone ?? "" : user.email ?? "", vars);
|
|
3772
|
+
};
|
|
3773
|
+
}
|
|
3774
|
+
return callbacks;
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
// src/iam-public-config.ts
|
|
3778
|
+
function buildIamPublicConfig(config, runtime) {
|
|
3779
|
+
const otp = OtpConfigSchema.parse(config.otp ?? {});
|
|
3780
|
+
const email = config.login.otp && !!config.otp && otp.email && !!runtime.onOtpSendEmail;
|
|
3781
|
+
const sms = config.login.otp && !!config.otp && otp.sms && !!runtime.onOtpSendSms;
|
|
3782
|
+
return {
|
|
3783
|
+
password: PasswordConfigSchema.parse(config.password ?? {}),
|
|
3784
|
+
register: { enabled: config.register.enabled },
|
|
3785
|
+
login: { password: config.login.password, otp: email || sms, ldap: config.login.ldap && !!config.ldap, email, sms },
|
|
3786
|
+
passwordReset: { enabled: config.passwordReset?.enabled !== false && !!runtime.onPasswordResetRequest, mode: config.passwordReset?.mode ?? "email" },
|
|
3787
|
+
otp: { length: otp.length, resendInterval: otp.resendInterval },
|
|
3788
|
+
agreements: { ...config.agreements },
|
|
3789
|
+
ui: AuthUiConfigSchema.parse(config.ui ?? {}),
|
|
3790
|
+
session: { maxAge: SessionConfigSchema.parse(config.session ?? {}).maxAge }
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3464
3793
|
var logger7 = core.logger.child({ module: "iam", scope: "seed" });
|
|
3465
3794
|
var DEFAULT_ROLES = [
|
|
3466
3795
|
{ code: "admin", name: () => iamM("iam_seedRoleAdminName"), description: () => iamM("iam_seedRoleAdminDesc"), isSystem: true },
|
|
@@ -3572,64 +3901,6 @@ async function seedIamData(authz) {
|
|
|
3572
3901
|
);
|
|
3573
3902
|
}
|
|
3574
3903
|
}
|
|
3575
|
-
|
|
3576
|
-
// src/session/iam-session-utils.ts
|
|
3577
|
-
var BASE64_PLUS_REGEX = /\+/g;
|
|
3578
|
-
var BASE64_SLASH_REGEX = /\//g;
|
|
3579
|
-
var BASE64_TRAILING_PADDING_REGEX = /=+$/;
|
|
3580
|
-
function generateToken() {
|
|
3581
|
-
const bytes = new Uint8Array(32);
|
|
3582
|
-
crypto.getRandomValues(bytes);
|
|
3583
|
-
const base64 = btoa(String.fromCharCode(...bytes));
|
|
3584
|
-
return base64.replace(BASE64_PLUS_REGEX, "-").replace(BASE64_SLASH_REGEX, "_").replace(BASE64_TRAILING_PADDING_REGEX, "");
|
|
3585
|
-
}
|
|
3586
|
-
function buildSession(options, now, sessionTtl, accessToken) {
|
|
3587
|
-
return {
|
|
3588
|
-
userId: options.userId,
|
|
3589
|
-
username: options.username,
|
|
3590
|
-
displayName: options.displayName,
|
|
3591
|
-
avatarUrl: options.avatarUrl,
|
|
3592
|
-
roles: options.roles ?? [],
|
|
3593
|
-
permissions: options.permissions ?? [],
|
|
3594
|
-
source: options.source,
|
|
3595
|
-
accessToken,
|
|
3596
|
-
createdAt: now,
|
|
3597
|
-
lastActiveAt: now,
|
|
3598
|
-
expiresAt: new Date(now.getTime() + sessionTtl * 1e3),
|
|
3599
|
-
data: options.data
|
|
3600
|
-
};
|
|
3601
|
-
}
|
|
3602
|
-
function getSessionTtl(session, now = Date.now()) {
|
|
3603
|
-
return Math.max(0, Math.floor((session.expiresAt.getTime() - now) / 1e3));
|
|
3604
|
-
}
|
|
3605
|
-
function applySessionPatch(session, patch) {
|
|
3606
|
-
const nextSession = { ...session };
|
|
3607
|
-
if (patch.data !== void 0) {
|
|
3608
|
-
nextSession.data = { ...nextSession.data, ...patch.data };
|
|
3609
|
-
}
|
|
3610
|
-
if (patch.roles !== void 0) {
|
|
3611
|
-
nextSession.roles = patch.roles;
|
|
3612
|
-
}
|
|
3613
|
-
if (patch.permissions !== void 0) {
|
|
3614
|
-
nextSession.permissions = patch.permissions;
|
|
3615
|
-
}
|
|
3616
|
-
if (patch.username !== void 0) {
|
|
3617
|
-
nextSession.username = patch.username;
|
|
3618
|
-
}
|
|
3619
|
-
if (patch.displayName !== void 0) {
|
|
3620
|
-
nextSession.displayName = patch.displayName;
|
|
3621
|
-
}
|
|
3622
|
-
if (patch.avatarUrl !== void 0) {
|
|
3623
|
-
nextSession.avatarUrl = patch.avatarUrl;
|
|
3624
|
-
}
|
|
3625
|
-
if (patch.source !== void 0) {
|
|
3626
|
-
nextSession.source = patch.source;
|
|
3627
|
-
}
|
|
3628
|
-
nextSession.lastActiveAt = /* @__PURE__ */ new Date();
|
|
3629
|
-
return nextSession;
|
|
3630
|
-
}
|
|
3631
|
-
|
|
3632
|
-
// src/session/iam-session-repository-cache.ts
|
|
3633
3904
|
var TOKEN_KEY_PREFIX = "hai:iam:token:";
|
|
3634
3905
|
var USER_TOKENS_KEY_PREFIX = "hai:iam:user:";
|
|
3635
3906
|
var REFRESH_TOKEN_PREFIX = "hai:iam:refresh:";
|
|
@@ -4203,61 +4474,82 @@ function createCacheResetTokenRepository() {
|
|
|
4203
4474
|
}
|
|
4204
4475
|
const repo = {
|
|
4205
4476
|
async saveToken(token, userId, expiresAt) {
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4477
|
+
return withIamLock(`reset:${userId}`, async () => {
|
|
4478
|
+
const hashResult = hashResetToken(token);
|
|
4479
|
+
if (!hashResult.success) {
|
|
4480
|
+
return hashResult;
|
|
4481
|
+
}
|
|
4482
|
+
const hashedToken = hashResult.data;
|
|
4483
|
+
const ttlSeconds = Math.max(1, Math.ceil((expiresAt.getTime() - Date.now()) / 1e3));
|
|
4484
|
+
const userTokenKey = buildUserTokenKey(userId);
|
|
4485
|
+
const existingTokenResult = await cache.kv.get(userTokenKey);
|
|
4486
|
+
if (!existingTokenResult.success) {
|
|
4487
|
+
return buildResetTokenRepositoryError(
|
|
4488
|
+
"iam_queryResetTokenFailed",
|
|
4489
|
+
existingTokenResult.error.message,
|
|
4490
|
+
existingTokenResult.error
|
|
4491
|
+
);
|
|
4492
|
+
}
|
|
4493
|
+
if (existingTokenResult.data && existingTokenResult.data !== hashedToken) {
|
|
4494
|
+
const removeExistingTokenResult = await cache.kv.del(buildResetTokenKey(existingTokenResult.data));
|
|
4495
|
+
if (!removeExistingTokenResult.success) {
|
|
4496
|
+
return buildResetTokenRepositoryError(
|
|
4497
|
+
"iam_saveResetTokenFailed",
|
|
4498
|
+
removeExistingTokenResult.error.message,
|
|
4499
|
+
removeExistingTokenResult.error
|
|
4500
|
+
);
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
const setResult = await cache.kv.set(buildResetTokenKey(hashedToken), userId, { ex: ttlSeconds });
|
|
4504
|
+
if (!setResult.success) {
|
|
4505
|
+
return err(
|
|
4506
|
+
HaiIamError.REPOSITORY_ERROR,
|
|
4507
|
+
iamM("iam_saveResetTokenFailed", { params: { message: setResult.error.message } }),
|
|
4508
|
+
setResult.error
|
|
4509
|
+
);
|
|
4510
|
+
}
|
|
4511
|
+
const saveUserTokenResult = await cache.kv.set(userTokenKey, hashedToken, { ex: ttlSeconds });
|
|
4512
|
+
if (!saveUserTokenResult.success) {
|
|
4513
|
+
await cache.kv.del(buildResetTokenKey(hashedToken));
|
|
4224
4514
|
return buildResetTokenRepositoryError(
|
|
4225
4515
|
"iam_saveResetTokenFailed",
|
|
4226
|
-
|
|
4227
|
-
|
|
4516
|
+
saveUserTokenResult.error.message,
|
|
4517
|
+
saveUserTokenResult.error
|
|
4228
4518
|
);
|
|
4229
4519
|
}
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
return
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
);
|
|
4259
|
-
|
|
4260
|
-
|
|
4520
|
+
const attemptsResult = await cache.kv.set(buildAttemptsKey(userId), 0, { ex: ttlSeconds });
|
|
4521
|
+
if (!attemptsResult.success) {
|
|
4522
|
+
await Promise.all([
|
|
4523
|
+
cache.kv.del(buildResetTokenKey(hashedToken)),
|
|
4524
|
+
cache.kv.del(userTokenKey)
|
|
4525
|
+
]);
|
|
4526
|
+
return buildResetTokenRepositoryError(
|
|
4527
|
+
"iam_saveResetTokenFailed",
|
|
4528
|
+
attemptsResult.error.message,
|
|
4529
|
+
attemptsResult.error
|
|
4530
|
+
);
|
|
4531
|
+
}
|
|
4532
|
+
return ok(void 0);
|
|
4533
|
+
});
|
|
4534
|
+
},
|
|
4535
|
+
async useToken(token, maxAttempts, operation) {
|
|
4536
|
+
const hash = hashResetToken(token);
|
|
4537
|
+
if (!hash.success)
|
|
4538
|
+
return hash;
|
|
4539
|
+
const user = await cache.kv.get(buildResetTokenKey(hash.data));
|
|
4540
|
+
if (!user.success)
|
|
4541
|
+
return buildResetTokenRepositoryError("iam_queryResetTokenFailed", user.error.message, user.error);
|
|
4542
|
+
if (!user.data)
|
|
4543
|
+
return err(HaiIamError.RESET_TOKEN_INVALID, iamM("iam_resetTokenInvalid"));
|
|
4544
|
+
return withIamLock(`reset:${user.data}`, async () => {
|
|
4545
|
+
const verified = await repo.tryGetUserByToken(token, maxAttempts);
|
|
4546
|
+
if (!verified.success)
|
|
4547
|
+
return verified;
|
|
4548
|
+
const result = await operation(verified.data);
|
|
4549
|
+
if (!result.success)
|
|
4550
|
+
return result;
|
|
4551
|
+
return repo.removeToken(token);
|
|
4552
|
+
});
|
|
4261
4553
|
},
|
|
4262
4554
|
async tryGetUserByToken(token, maxAttempts) {
|
|
4263
4555
|
const hashResult = hashResetToken(token);
|
|
@@ -4281,6 +4573,11 @@ function createCacheResetTokenRepository() {
|
|
|
4281
4573
|
);
|
|
4282
4574
|
}
|
|
4283
4575
|
const userId = result.data;
|
|
4576
|
+
const current = await cache.kv.get(buildUserTokenKey(userId));
|
|
4577
|
+
if (!current.success)
|
|
4578
|
+
return buildResetTokenRepositoryError("iam_queryResetTokenFailed", current.error.message, current.error);
|
|
4579
|
+
if (current.data !== hashedToken)
|
|
4580
|
+
return err(HaiIamError.RESET_TOKEN_INVALID, iamM("iam_resetTokenInvalid"));
|
|
4284
4581
|
const attemptsKey = buildAttemptsKey(userId);
|
|
4285
4582
|
const incrResult = await cache.kv.incr(attemptsKey);
|
|
4286
4583
|
if (!incrResult.success) {
|
|
@@ -4427,16 +4724,12 @@ function buildRegistrationOps(ctx) {
|
|
|
4427
4724
|
const { userRepository, authzFunctions, config, registerConfig, agreementConfig } = ctx;
|
|
4428
4725
|
const { validatePassword, hashPassword } = ctx;
|
|
4429
4726
|
async function validateRegisterPreconditions(options) {
|
|
4430
|
-
if (!registerConfig.enabled) {
|
|
4431
|
-
return err(
|
|
4432
|
-
HaiIamError.REGISTER_DISABLED,
|
|
4433
|
-
iamM("iam_registerDisabled")
|
|
4434
|
-
);
|
|
4435
|
-
}
|
|
4436
4727
|
const validateResult = validatePassword(options.password);
|
|
4437
4728
|
if (!validateResult.success)
|
|
4438
4729
|
return validateResult;
|
|
4439
4730
|
const existsResult = await userRepository.existsByUsername(options.username);
|
|
4731
|
+
if (!existsResult.success)
|
|
4732
|
+
return existsResult;
|
|
4440
4733
|
if (existsResult.success && existsResult.data) {
|
|
4441
4734
|
return err(
|
|
4442
4735
|
HaiIamError.USER_ALREADY_EXISTS,
|
|
@@ -4445,6 +4738,8 @@ function buildRegistrationOps(ctx) {
|
|
|
4445
4738
|
}
|
|
4446
4739
|
if (options.email) {
|
|
4447
4740
|
const emailExistsResult = await userRepository.existsByEmail(options.email);
|
|
4741
|
+
if (!emailExistsResult.success)
|
|
4742
|
+
return emailExistsResult;
|
|
4448
4743
|
if (emailExistsResult.success && emailExistsResult.data) {
|
|
4449
4744
|
return err(
|
|
4450
4745
|
HaiIamError.USER_ALREADY_EXISTS,
|
|
@@ -4455,61 +4750,74 @@ function buildRegistrationOps(ctx) {
|
|
|
4455
4750
|
return ok(void 0);
|
|
4456
4751
|
}
|
|
4457
4752
|
async function assignDefaultRole(userId) {
|
|
4458
|
-
if (!config.rbac?.defaultRole)
|
|
4459
|
-
return;
|
|
4753
|
+
if (!config.rbac?.enabled || !config.rbac.defaultRole)
|
|
4754
|
+
return ok(void 0);
|
|
4460
4755
|
const roleResult = await authzFunctions.getRoleByCode(config.rbac.defaultRole);
|
|
4461
|
-
if (roleResult.success
|
|
4462
|
-
|
|
4756
|
+
if (!roleResult.success)
|
|
4757
|
+
return roleResult;
|
|
4758
|
+
if (!roleResult.data)
|
|
4759
|
+
return err(HaiIamError.ROLE_NOT_FOUND, iamM("iam_roleNotExist"));
|
|
4760
|
+
return authzFunctions.assignRole(userId, roleResult.data.id);
|
|
4761
|
+
}
|
|
4762
|
+
async function createUser(options, enabled) {
|
|
4763
|
+
const preResult = await validateRegisterPreconditions(options);
|
|
4764
|
+
if (!preResult.success)
|
|
4765
|
+
return preResult;
|
|
4766
|
+
const hashResult = hashPassword(options.password);
|
|
4767
|
+
if (!hashResult.success)
|
|
4768
|
+
return hashResult;
|
|
4769
|
+
const txResult = await reldb.tx.begin();
|
|
4770
|
+
if (!txResult.success) {
|
|
4771
|
+
return mapRepositoryError("iam_createUserFailed", txResult.error.message);
|
|
4463
4772
|
}
|
|
4773
|
+
const tx = txResult.data;
|
|
4774
|
+
const createResult = await userRepository.create({
|
|
4775
|
+
username: options.username,
|
|
4776
|
+
email: options.email,
|
|
4777
|
+
phone: options.phone,
|
|
4778
|
+
displayName: options.displayName,
|
|
4779
|
+
enabled,
|
|
4780
|
+
emailVerified: false,
|
|
4781
|
+
phoneVerified: false,
|
|
4782
|
+
passwordHash: hashResult.data,
|
|
4783
|
+
passwordUpdatedAt: /* @__PURE__ */ new Date(),
|
|
4784
|
+
metadata: options.metadata
|
|
4785
|
+
}, tx);
|
|
4786
|
+
if (!createResult.success) {
|
|
4787
|
+
await tx.rollback();
|
|
4788
|
+
return mapUpdateErrorAsDomainError(createResult.error.message);
|
|
4789
|
+
}
|
|
4790
|
+
const createdUserResult = await userRepository.findByUsername(options.username, tx);
|
|
4791
|
+
if (!createdUserResult.success || !createdUserResult.data) {
|
|
4792
|
+
await tx.rollback();
|
|
4793
|
+
return err(
|
|
4794
|
+
HaiIamError.USER_NOT_FOUND,
|
|
4795
|
+
iamM("iam_userNotExist")
|
|
4796
|
+
);
|
|
4797
|
+
}
|
|
4798
|
+
const commitResult = await tx.commit();
|
|
4799
|
+
if (!commitResult.success) {
|
|
4800
|
+
return mapRepositoryError("iam_createUserFailed", commitResult.error.message);
|
|
4801
|
+
}
|
|
4802
|
+
const createdUser = createdUserResult.data;
|
|
4803
|
+
const assigned = await assignDefaultRole(createdUser.id);
|
|
4804
|
+
if (!assigned.success) {
|
|
4805
|
+
await authzFunctions.syncRoles(createdUser.id, []);
|
|
4806
|
+
await userRepository.deleteById(createdUser.id);
|
|
4807
|
+
return assigned;
|
|
4808
|
+
}
|
|
4809
|
+
logger10.info("User registered", { userId: createdUser.id, username: options.username });
|
|
4810
|
+
return ok({
|
|
4811
|
+
user: toUser(createdUser),
|
|
4812
|
+
agreements: buildAgreementDisplay(agreementConfig, agreementConfig.showOnRegister)
|
|
4813
|
+
});
|
|
4464
4814
|
}
|
|
4465
4815
|
return {
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
if (!
|
|
4469
|
-
return
|
|
4470
|
-
|
|
4471
|
-
if (!hashResult.success)
|
|
4472
|
-
return hashResult;
|
|
4473
|
-
const txResult = await reldb.tx.begin();
|
|
4474
|
-
if (!txResult.success) {
|
|
4475
|
-
return mapRepositoryError("iam_createUserFailed", txResult.error.message);
|
|
4476
|
-
}
|
|
4477
|
-
const tx = txResult.data;
|
|
4478
|
-
const createResult = await userRepository.create({
|
|
4479
|
-
username: options.username,
|
|
4480
|
-
email: options.email,
|
|
4481
|
-
phone: options.phone,
|
|
4482
|
-
displayName: options.displayName,
|
|
4483
|
-
enabled: registerConfig.defaultEnabled,
|
|
4484
|
-
emailVerified: false,
|
|
4485
|
-
phoneVerified: false,
|
|
4486
|
-
passwordHash: hashResult.data,
|
|
4487
|
-
passwordUpdatedAt: /* @__PURE__ */ new Date(),
|
|
4488
|
-
metadata: options.metadata
|
|
4489
|
-
}, tx);
|
|
4490
|
-
if (!createResult.success) {
|
|
4491
|
-
await tx.rollback();
|
|
4492
|
-
return mapUpdateErrorAsDomainError(createResult.error.message);
|
|
4493
|
-
}
|
|
4494
|
-
const createdUserResult = await userRepository.findByUsername(options.username, tx);
|
|
4495
|
-
if (!createdUserResult.success || !createdUserResult.data) {
|
|
4496
|
-
await tx.rollback();
|
|
4497
|
-
return err(
|
|
4498
|
-
HaiIamError.USER_NOT_FOUND,
|
|
4499
|
-
iamM("iam_userNotExist")
|
|
4500
|
-
);
|
|
4501
|
-
}
|
|
4502
|
-
const commitResult = await tx.commit();
|
|
4503
|
-
if (!commitResult.success) {
|
|
4504
|
-
return mapRepositoryError("iam_createUserFailed", commitResult.error.message);
|
|
4505
|
-
}
|
|
4506
|
-
const createdUser = createdUserResult.data;
|
|
4507
|
-
await assignDefaultRole(createdUser.id);
|
|
4508
|
-
logger10.info("User registered", { userId: createdUser.id, username: options.username });
|
|
4509
|
-
return ok({
|
|
4510
|
-
user: toUser(createdUser),
|
|
4511
|
-
agreements: buildAgreementDisplay(agreementConfig, agreementConfig.showOnRegister)
|
|
4512
|
-
});
|
|
4816
|
+
createUser: (options) => createUser(options, true),
|
|
4817
|
+
register(options) {
|
|
4818
|
+
if (!registerConfig.enabled)
|
|
4819
|
+
return Promise.resolve(err(HaiIamError.REGISTER_DISABLED, iamM("iam_registerDisabled")));
|
|
4820
|
+
return createUser(options, registerConfig.defaultEnabled);
|
|
4513
4821
|
},
|
|
4514
4822
|
validatePassword(password) {
|
|
4515
4823
|
return validatePassword(password);
|
|
@@ -4806,16 +5114,20 @@ function buildPasswordChangeOps(ctx) {
|
|
|
4806
5114
|
}
|
|
4807
5115
|
const verifyResult = crypto$1.password.verify(oldPassword, user.passwordHash);
|
|
4808
5116
|
if (!verifyResult.success || !verifyResult.data) {
|
|
4809
|
-
await recordLoginFailure(userRepository, user, {
|
|
5117
|
+
const failureResult = await recordLoginFailure(userRepository, user, {
|
|
4810
5118
|
maxLoginAttempts: ctx.loginConfig.maxLoginAttempts,
|
|
4811
5119
|
lockoutDuration: ctx.loginConfig.lockoutDuration
|
|
4812
5120
|
});
|
|
5121
|
+
if (!failureResult.success)
|
|
5122
|
+
return failureResult;
|
|
4813
5123
|
return err(
|
|
4814
5124
|
HaiIamError.INVALID_CREDENTIALS,
|
|
4815
5125
|
iamM("iam_originalPasswordWrong")
|
|
4816
5126
|
);
|
|
4817
5127
|
}
|
|
4818
|
-
await resetLoginFailures(userRepository, user);
|
|
5128
|
+
const resetResult = await resetLoginFailures(userRepository, user);
|
|
5129
|
+
if (!resetResult.success)
|
|
5130
|
+
return resetResult;
|
|
4819
5131
|
const validateResult = validatePassword(newPassword);
|
|
4820
5132
|
if (!validateResult.success)
|
|
4821
5133
|
return validateResult;
|
|
@@ -4857,6 +5169,8 @@ function buildPasswordResetOps(ctx) {
|
|
|
4857
5169
|
async requestPasswordReset(identifier) {
|
|
4858
5170
|
logger10.debug("Password reset requested", { identifier });
|
|
4859
5171
|
const resetConfig = ctx.resetConfig;
|
|
5172
|
+
if (!resetConfig.enabled || !onPasswordResetRequest)
|
|
5173
|
+
return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
|
|
4860
5174
|
const userResult = await userRepository.findByIdentifier(identifier);
|
|
4861
5175
|
if (!userResult.success) {
|
|
4862
5176
|
logger10.warn("Failed to look up user for password reset", { identifier });
|
|
@@ -4867,20 +5181,19 @@ function buildPasswordResetOps(ctx) {
|
|
|
4867
5181
|
return ok(void 0);
|
|
4868
5182
|
}
|
|
4869
5183
|
const user = toUser(userResult.data);
|
|
5184
|
+
if (!user.enabled)
|
|
5185
|
+
return ok(void 0);
|
|
4870
5186
|
const token = generateToken();
|
|
4871
5187
|
const expiresAt = new Date(Date.now() + resetConfig.tokenExpiresIn * 1e3);
|
|
4872
5188
|
const saveResult = await resetTokenRepository.saveToken(token, user.id, expiresAt);
|
|
4873
5189
|
if (!saveResult.success) {
|
|
4874
5190
|
return saveResult;
|
|
4875
5191
|
}
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
}
|
|
4882
|
-
} else {
|
|
4883
|
-
logger10.warn("No password reset callback configured, token will not be delivered to user", { userId: user.id });
|
|
5192
|
+
try {
|
|
5193
|
+
await onPasswordResetRequest(user, token, expiresAt);
|
|
5194
|
+
} catch {
|
|
5195
|
+
logger10.error("Password reset callback failed", { userId: user.id });
|
|
5196
|
+
return err(HaiIamError.INTERNAL_ERROR, iamM("iam_passwordResetDeliveryFailed"));
|
|
4884
5197
|
}
|
|
4885
5198
|
logger10.info("Password reset token generated", { userId: user.id });
|
|
4886
5199
|
return ok(void 0);
|
|
@@ -4888,38 +5201,43 @@ function buildPasswordResetOps(ctx) {
|
|
|
4888
5201
|
async confirmPasswordReset(token, newPassword) {
|
|
4889
5202
|
logger10.debug("Confirming password reset");
|
|
4890
5203
|
const resetConfig = ctx.resetConfig;
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
)
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
5204
|
+
if (!resetConfig.enabled)
|
|
5205
|
+
return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
|
|
5206
|
+
return resetTokenRepository.useToken(token, resetConfig.maxAttempts, async (userId) => {
|
|
5207
|
+
const validateResult = validatePassword(newPassword);
|
|
5208
|
+
if (!validateResult.success)
|
|
5209
|
+
return validateResult;
|
|
5210
|
+
const userResult = await userRepository.findById(userId);
|
|
5211
|
+
if (!userResult.success) {
|
|
5212
|
+
return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
|
|
5213
|
+
}
|
|
5214
|
+
if (!userResult.data) {
|
|
5215
|
+
return err(
|
|
5216
|
+
HaiIamError.USER_NOT_FOUND,
|
|
5217
|
+
iamM("iam_userNotExist")
|
|
5218
|
+
);
|
|
5219
|
+
}
|
|
5220
|
+
if (!userResult.data.enabled)
|
|
5221
|
+
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
5222
|
+
const hashResult = hashPassword(newPassword);
|
|
5223
|
+
if (!hashResult.success)
|
|
5224
|
+
return hashResult;
|
|
5225
|
+
const updateResult = await userRepository.updateById(userId, {
|
|
5226
|
+
passwordHash: hashResult.data,
|
|
5227
|
+
passwordUpdatedAt: /* @__PURE__ */ new Date(),
|
|
5228
|
+
loginFailedCount: 0,
|
|
5229
|
+
lockedUntil: null,
|
|
5230
|
+
lastLoginFailedAt: null
|
|
5231
|
+
});
|
|
5232
|
+
if (!updateResult.success) {
|
|
5233
|
+
return mapRepositoryError("iam_updateUserFailed", updateResult.error.message);
|
|
5234
|
+
}
|
|
5235
|
+
const revoked = await sessionFunctions.deleteByUserId(userId);
|
|
5236
|
+
if (!revoked.success)
|
|
5237
|
+
return revoked;
|
|
5238
|
+
logger10.info("Password reset confirmed", { userId });
|
|
5239
|
+
return ok(void 0);
|
|
4915
5240
|
});
|
|
4916
|
-
if (!updateResult.success) {
|
|
4917
|
-
return mapRepositoryError("iam_updateUserFailed", updateResult.error.message);
|
|
4918
|
-
}
|
|
4919
|
-
await resetTokenRepository.removeToken(token);
|
|
4920
|
-
await sessionFunctions.deleteByUserId(userId);
|
|
4921
|
-
logger10.info("Password reset confirmed", { userId });
|
|
4922
|
-
return ok(void 0);
|
|
4923
5241
|
}
|
|
4924
5242
|
};
|
|
4925
5243
|
}
|
|
@@ -4947,6 +5265,7 @@ function buildUserFunctions(deps) {
|
|
|
4947
5265
|
var logger11 = core.logger.child({ module: "iam", scope: "main" });
|
|
4948
5266
|
var initInProgress = false;
|
|
4949
5267
|
var currentConfig = null;
|
|
5268
|
+
var currentPublicConfig = null;
|
|
4950
5269
|
var currentAuth = null;
|
|
4951
5270
|
var currentUser = null;
|
|
4952
5271
|
var currentAuthz = null;
|
|
@@ -4984,7 +5303,7 @@ var iam = {
|
|
|
4984
5303
|
logger11.warn("IAM module is already initialized, reinitializing");
|
|
4985
5304
|
await iam.close();
|
|
4986
5305
|
}
|
|
4987
|
-
const { ldapClientFactory, ldapSyncUser
|
|
5306
|
+
const { ldapClientFactory, ldapSyncUser } = config;
|
|
4988
5307
|
logger11.info("Initializing IAM module");
|
|
4989
5308
|
if (!reldb.isInitialized) {
|
|
4990
5309
|
return err(
|
|
@@ -5008,7 +5327,7 @@ var iam = {
|
|
|
5008
5327
|
);
|
|
5009
5328
|
}
|
|
5010
5329
|
}
|
|
5011
|
-
const parseResult = IamConfigSchema.safeParse(
|
|
5330
|
+
const parseResult = IamConfigSchema.safeParse(config);
|
|
5012
5331
|
if (!parseResult.success) {
|
|
5013
5332
|
logger11.error("IAM config validation failed", { error: parseResult.error.message });
|
|
5014
5333
|
return err(
|
|
@@ -5018,6 +5337,8 @@ var iam = {
|
|
|
5018
5337
|
);
|
|
5019
5338
|
}
|
|
5020
5339
|
const parsed = parseResult.data;
|
|
5340
|
+
const { onPasswordResetRequest, onOtpSendEmail, onOtpSendSms } = await resolveIamNotifications(config);
|
|
5341
|
+
logger11.info("IAM notification channels resolved", { email: Boolean(onOtpSendEmail), sms: Boolean(onOtpSendSms), passwordReset: Boolean(onPasswordResetRequest) });
|
|
5021
5342
|
const sessionResult = await createSessionOperations({ config: parsed });
|
|
5022
5343
|
if (!sessionResult.success) {
|
|
5023
5344
|
return sessionResult;
|
|
@@ -5058,6 +5379,7 @@ var iam = {
|
|
|
5058
5379
|
currentAuthz = authzResult.data;
|
|
5059
5380
|
currentUser = userResult.data;
|
|
5060
5381
|
currentConfig = parsed;
|
|
5382
|
+
currentPublicConfig = buildIamPublicConfig(parsed, { ...config, onPasswordResetRequest, onOtpSendEmail, onOtpSendSms });
|
|
5061
5383
|
currentApiKey = authnResult.data.apiKeyFunctions;
|
|
5062
5384
|
currentTicket = createTicketOperations();
|
|
5063
5385
|
const authn = authnResult.data.authn;
|
|
@@ -5068,7 +5390,7 @@ var iam = {
|
|
|
5068
5390
|
if (!regResult.success) {
|
|
5069
5391
|
return regResult;
|
|
5070
5392
|
}
|
|
5071
|
-
return
|
|
5393
|
+
return authnResult.data.loginRegisteredUser(regResult.data.user);
|
|
5072
5394
|
}
|
|
5073
5395
|
};
|
|
5074
5396
|
logger11.info("IAM module initialized");
|
|
@@ -5105,6 +5427,9 @@ var iam = {
|
|
|
5105
5427
|
get config() {
|
|
5106
5428
|
return currentConfig ? core.sanitize.sanitizeSensitiveFields(currentConfig) : null;
|
|
5107
5429
|
},
|
|
5430
|
+
get publicConfig() {
|
|
5431
|
+
return currentPublicConfig ? structuredClone(currentPublicConfig) : null;
|
|
5432
|
+
},
|
|
5108
5433
|
get isInitialized() {
|
|
5109
5434
|
return currentConfig !== null;
|
|
5110
5435
|
},
|
|
@@ -5124,6 +5449,7 @@ var iam = {
|
|
|
5124
5449
|
currentApiKey = null;
|
|
5125
5450
|
currentTicket = null;
|
|
5126
5451
|
currentConfig = null;
|
|
5452
|
+
currentPublicConfig = null;
|
|
5127
5453
|
resetApiKeyRepoSingleton();
|
|
5128
5454
|
resetOtpRepoSingleton();
|
|
5129
5455
|
resetUserRepoSingleton();
|
|
@@ -5134,6 +5460,6 @@ var iam = {
|
|
|
5134
5460
|
}
|
|
5135
5461
|
};
|
|
5136
5462
|
|
|
5137
|
-
export { AgreementConfigSchema, ApiKeyConfigSchema, AuthStrategyTypeSchema, HaiIamError, IamConfigSchema, LdapConfigSchema, LoginConfigSchema, OtpConfigSchema, PasswordConfigSchema, PasswordResetConfigSchema, RbacConfigSchema, RegisterConfigSchema, SecurityConfigSchema, SessionConfigSchema, iam };
|
|
5463
|
+
export { AgreementConfigSchema, ApiKeyConfigSchema, AuthStrategyTypeSchema, AuthUiConfigSchema, HaiIamError, IamConfigSchema, LdapConfigSchema, LoginConfigSchema, OtpConfigSchema, PasswordConfigSchema, PasswordResetConfigSchema, RbacConfigSchema, RegisterConfigSchema, SecurityConfigSchema, SessionConfigSchema, iam };
|
|
5138
5464
|
//# sourceMappingURL=index.js.map
|
|
5139
5465
|
//# sourceMappingURL=index.js.map
|