Commit e7c3b3eb by luoqi

refactor(auth): mockLogin 走对外契约同一路径(issueTokens 单一真理源)+ 修 ['default'] bug

第一性:模拟登录 = 模拟 host 调换票,本应产出契约同形 user 上下文(核心 orgScope)
并走与 exchangeToken **同一条**展开+签发。之前 mockLogin 把 expand+sign 抄了一遍,已飘:
- 抽出 private issueTokens(host, user):orgScope→expandOrgScope→sourceUnits+clinicIds→签 access/refresh
- exchangeToken 与 mockLogin 都委托它;mockLogin 只负责"preset → 契约 user(算 orgScope)"
- 删 exchangeToken 的 clinicIds 空→['default'] 兜底:空=不限(下游 plan 过滤 length>0 才加),
  兜底会把集团级误锁成不存在的诊所 → 啥都看不到(mockLogin 本就没这兜底,统一后契约路径一并修)

活体验证三级:集团 sourceUnits=[]/clinics=0(不限);品牌 ["瑞尔"]/3;诊所 ["瑞尔"]/2;瑞泰 ["瑞泰"]/2

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 07a32a03
......@@ -77,38 +77,8 @@ export class AuthService {
throw new BizError(ApiCode.AUTH_INVALID_CREDENTIALS);
}
const tenantId = req.user.tenantId;
// 标准数据权限:host 传 orgScope(任意层级 org 节点)→ 按 host org 树展开成 sourceUnits + clinicIds;
// 未传 orgScope 则用显式 sourceUnits/clinicIds(向后兼容)。
const orgTree = host.name === 'jvs-dw' ? this.jvsDwOrgTree() : null;
const expanded =
req.user.orgScope && orgTree ? expandOrgScope(orgTree, req.user.orgScope) : null;
const sourceUnits = expanded ? expanded.sourceUnits : (req.user.sourceUnits ?? []);
const rawClinicIds = expanded ? expanded.clinicIds : req.user.clinicIds;
const clinicIds = rawClinicIds.length > 0 ? rawClinicIds : ['default'];
const permissions = this.resolvePermissions(req.user.role);
const accessToken = await this.signAccessToken({
sub: req.user.userId,
hostId: host.id,
tenantId,
clinicIds,
sourceUnits, // 集团模型:品牌数据范围(orgScope 展开 或 显式;空=不限)
role: req.user.role,
permissions,
// 可选名字典(clinics/users id→名)透传进 JWT,前端用于 UI 显示
...(req.user.dictionary ? { dictionary: req.user.dictionary } : {}),
});
const refreshToken = await this.signRefreshToken({
sub: req.user.userId,
hostId: host.id,
tenantId,
clinicIds,
sourceUnits,
role: req.user.role,
dictionary: req.user.dictionary,
});
// 契约 user 上下文 → token,与 mockLogin 共用同一条 issueTokens(展开 + 签发)。
const { accessToken, refreshToken } = await this.issueTokens(host, req.user);
const code = randomCode(24);
const codeTtl = this.config.getOrThrow<number>('exchangeCodeTtlSeconds');
......@@ -233,6 +203,55 @@ export class AuthService {
return ROLE_PERMISSIONS[role];
}
/**
* 契约 user 上下文 → 签发 access + refresh。**单一真理源** —— exchangeToken(真 host 换票)
* 与 mockLogin(模拟 host)共用,token 怎么铸只此一处。
*
* 标准数据权限走 orgScope:host 传任意层级 org 节点 → 按 host org 树 expandOrgScope
* 展开成 sourceUnits + clinicIds;未传 orgScope 才回退显式 sourceUnits/clinicIds。
* 两维**空数组 = 不限**(集团级 / 单命名空间 host),下游过滤层按"空=不加过滤"处理
* —— 故此处不再对空 clinicIds 兜底成 ['default'](那会把集团级误锁成不存在的诊所)。
*/
private async issueTokens(
host: { id: string; name: string },
user: {
userId: string;
tenantId: string;
role: UserRole;
clinicIds: string[];
sourceUnits?: string[];
orgScope?: string[];
dictionary?: TokenDictionary;
},
): Promise<{ accessToken: string; refreshToken: string }> {
const orgTree = host.name === 'jvs-dw' ? this.jvsDwOrgTree() : null;
const expanded = user.orgScope && orgTree ? expandOrgScope(orgTree, user.orgScope) : null;
const sourceUnits = expanded ? expanded.sourceUnits : (user.sourceUnits ?? []);
const clinicIds = expanded ? expanded.clinicIds : user.clinicIds;
const permissions = this.resolvePermissions(user.role);
const accessToken = await this.signAccessToken({
sub: user.userId,
hostId: host.id,
tenantId: user.tenantId,
clinicIds,
sourceUnits, // 集团模型:品牌数据范围(orgScope 展开 或 显式;空=不限)
role: user.role,
permissions,
...(user.dictionary ? { dictionary: user.dictionary } : {}),
});
const refreshToken = await this.signRefreshToken({
sub: user.userId,
hostId: host.id,
tenantId: user.tenantId,
clinicIds,
sourceUnits,
role: user.role,
dictionary: user.dictionary,
});
return { accessToken, refreshToken };
}
// ─────────────────────────────────────────────────────────────
// Mock Login(开发 / 试部署用 — env 门控,生产关闭)
// ─────────────────────────────────────────────────────────────
......@@ -332,40 +351,26 @@ export class AuthService {
orgScope = [preset.sourceUnit];
}
const expanded = expandOrgScope(tree, orgScope);
const sourceUnits = expanded.sourceUnits;
const clinicIds = expanded.clinicIds; // 空 = 不限(集团级)
const dictionary: TokenDictionary = {
clinics: (req.orgLevel === 'group' ? allClinics : preset.clinics) as Record<string, string>,
users: { [sub]: subName },
};
const permissions = this.resolvePermissions(req.role);
const accessToken = await this.signAccessToken({
sub,
hostId: host.id,
// 第一性:模拟登录 = 模拟 host 调换票 —— 产出契约同形的 user 上下文(核心就是 orgScope),
// 走与 exchangeToken **同一条** issueTokens(展开 + 签发),不另搞一套。
const { accessToken, refreshToken } = await this.issueTokens(host, {
userId: sub,
tenantId: preset.tenantId,
clinicIds,
sourceUnits, // org-scope 展开:诊所/品牌→[品牌];集团→[](不限)
role: req.role,
permissions,
dictionary,
});
const refreshToken = await this.signRefreshToken({
sub,
hostId: host.id,
tenantId: preset.tenantId,
clinicIds,
sourceUnits,
role: req.role,
clinicIds: [], // 用 orgScope,显式 clinicIds 不参与
orgScope,
dictionary,
});
const expiresIn = this.accessExpiresInSeconds;
this.logger.log(
`mock-login: tenant=${req.tenant}(${preset.tenantNameZh}) role=${req.role} ` +
`sub=${sub}(${subName}) clinics=${clinicIds.length} hostId=${host.id}`,
`sub=${sub}(${subName}) orgScope=[${orgScope.join(',')}] hostId=${host.id}`,
);
return { accessToken, refreshToken, expiresIn };
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment