数据隔离:销售只可见本人发起的评估(行级权限)

- listPage 支持 assessorId 过滤;列表端点对已鉴权销售强制按 JWT.uid 过滤(防伪造)
- 看板历史与待办对销售按本人 user.id 过滤;风控/管理层仍可见全部
- 前端 fetchAssessmentsPage 支持 assessorId 参数
This commit is contained in:
freedakgmail
2026-06-13 19:36:28 +08:00
parent 8a1afb0c29
commit a3906fc1b6
4 changed files with 27 additions and 4 deletions
+6
View File
@@ -52,6 +52,8 @@ export interface ListPageOptions {
q?: string;
/** 归档过滤:'active'(默认,仅未归档)| 'archived'(仅归档)| 'all'(全部)。 */
archived?: 'active' | 'archived' | 'all';
/** 仅返回该发起人(用户ID)的评估(销售按本人过滤)。 */
assessorId?: string;
}
/** 列表行(SQL 分页返回)。 */
@@ -245,6 +247,10 @@ export class PgAssessmentStore implements AssessmentStore {
`(a.assessment->>'projectDescription' ILIKE $${i} OR a.assessment->>'businessType' ILIKE $${i} OR a.assessment->>'industry' ILIKE $${i} OR a.assessment->>'assessorId' ILIKE $${i})`,
);
}
if (opts.assessorId !== undefined && opts.assessorId !== '') {
params.push(opts.assessorId);
where.push(`a.assessment->>'assessorId' = $${params.length}`);
}
const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
const countRes = await this.pool.query(
+10
View File
@@ -1586,6 +1586,12 @@ app.get('/api/assessments', async (c) => {
const pageSize = Number(c.req.query('pageSize') ?? 20);
const statusFilter = c.req.query('status');
const q = c.req.query('q');
let assessorFilter = c.req.query('assessorId');
// 行级隔离:已鉴权的销售只能看本人发起的评估(强制按 JWT 的 uid 过滤,忽略前端传值)。
const authUser = (c as import('hono').Context).get('user') as AuthPayload | undefined;
if (authUser?.role === '商务/销售' && authUser.uid) {
assessorFilter = authUser.uid;
}
const archivedRaw = c.req.query('archived');
const archived: 'active' | 'archived' | 'all' =
archivedRaw === 'archived' ? 'archived' : archivedRaw === 'all' ? 'all' : 'active';
@@ -1633,6 +1639,7 @@ app.get('/api/assessments', async (c) => {
archived,
...(statusFilter !== undefined ? { status: statusFilter } : {}),
...(q !== undefined ? { q } : {}),
...(assessorFilter !== undefined && assessorFilter !== '' ? { assessorId: assessorFilter } : {}),
});
const nameMap = await userIdNameMap();
const items2 = items.map((it) => ({
@@ -1652,6 +1659,9 @@ app.get('/api/assessments', async (c) => {
if (statusFilter !== undefined && statusFilter !== '' && statusFilter !== 'all') {
rows = rows.filter((r) => r.status === statusFilter);
}
if (assessorFilter !== undefined && assessorFilter !== '') {
rows = rows.filter((r) => r.assessorId === assessorFilter);
}
if (q !== undefined && q.trim() !== '') {
const kw = q.trim();
rows = rows.filter(