fix(auth): 补齐主管权限范围字段

This commit is contained in:
WuYongJie 2026-07-06 19:14:13 +08:00
parent cf54f94ba5
commit 50b8f61a69
5 changed files with 163 additions and 93 deletions

View File

@ -55,6 +55,10 @@ interface WorkspaceShellUser extends WorkspacePermissionUser {
avatar: string | null
department: { id: string; name: string; fullPath?: string | null } | null
departmentPaths?: string[]
departmentId?: string | null
teamId?: string | null
managedDepartmentId?: string | null
managedTeamId?: string | null
}
interface AuthMeResponse {

View File

@ -1,6 +1,6 @@
import React from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import { AdminGuard } from '@/components/auth/PermissionGuard'
import { AdminGuard, PermissionGuard } from '@/components/auth/PermissionGuard'
import { RequireAuth } from '@/components/auth/RequireAuth'
import { AuthProvider, useAuth } from '@/hooks/useAuth'
@ -80,6 +80,58 @@ describe('AuthProvider', () => {
expect(pushMock).not.toHaveBeenCalled()
})
it('主管带部门范围时,可以通过用户权限管理守卫', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: async () => ({
success: true,
data: {
authenticated: true,
user: {
id: 'supervisor-1',
email: 'supervisor@test.com',
name: '苏适',
role: 'SUPERVISOR',
status: 'ACTIVE',
isFirstLogin: false,
departmentId: 'dept-operation-b',
teamId: null,
managedDepartmentId: null,
managedTeamId: null,
department: {
id: 'dept-operation-b',
name: '运营中心B',
fullPath: '江苏简尚品牌管理有限公司 > 运营中心B',
},
team: null,
avatar: null,
lastLoginAt: null,
},
},
}),
} as Response)
render(
<AuthProvider>
<RequireAuth>
<PermissionGuard
roles={['SUPER_ADMIN', 'ADMIN', 'SUPERVISOR_DEPARTMENT', 'SUPERVISOR_TEAM']}
fallback={<div data-testid="blocked">blocked</div>}
>
<div data-testid="allowed">allowed</div>
</PermissionGuard>
</RequireAuth>
</AuthProvider>
)
await waitFor(() => {
expect(screen.getByTestId('allowed')).toBeInTheDocument()
})
expect(screen.queryByTestId('blocked')).not.toBeInTheDocument()
expect(pushMock).not.toHaveBeenCalled()
})
it('服务端认证失败时,不使用本地缓存用户放行认证和权限', async () => {
localStorage.setItem(
'user',

View File

@ -43,10 +43,14 @@ export interface User {
name: string;
fullPath?: string;
} | null;
departmentId?: string | null;
team?: {
id: string;
name: string;
} | null;
teamId?: string | null;
managedDepartmentId?: string | null;
managedTeamId?: string | null;
avatar?: string | null;
lastLoginAt?: string | null;
}

View File

@ -69,6 +69,8 @@ function buildAuthUser(overrides: Record<string, unknown> = {}) {
activationRequired: true,
employeeDirectoryId: 'emp-1',
isFirstLogin: false,
departmentId: 'dept-purchase',
teamId: null,
department: {
id: 'dept-purchase',
name: '供应链中心',
@ -126,6 +128,43 @@ describe('authService employee auto activation', () => {
expect(result.user.businessRoles).toEqual(['PURCHASING'])
})
it('登录结果会返回前端权限判断所需的部门和管理范围字段', async () => {
const activeSupervisor = buildAuthUser({
name: '苏适',
role: 'SUPERVISOR',
status: 'ACTIVE',
departmentId: 'dept-operation-b',
department: {
id: 'dept-operation-b',
name: '运营中心B',
fullPath: '江苏简尚品牌管理有限公司 > 运营中心B',
},
managedDepartment: {
id: 'dept-operation-b',
name: '运营中心B',
fullPath: '江苏简尚品牌管理有限公司 > 运营中心B',
},
})
mockAuthRepository.findByIdentifier.mockResolvedValue(activeSupervisor as never)
mockAuthRepository.getEmployeeAutoActivationContext.mockResolvedValue(null as never)
mockAuthRepository.findByIdWithRelations.mockResolvedValue(activeSupervisor as never)
const result = await authService.login({
identifier: '苏适',
password: 'password-1',
})
expect(result.user.departmentId).toBe('dept-operation-b')
expect(result.user.teamId).toBeNull()
expect(result.user.managedDepartmentId).toBe('dept-operation-b')
expect(result.user.managedTeamId).toBeNull()
expect(mockGenerateToken).toHaveBeenCalledWith(
expect.objectContaining({
departmentId: 'dept-operation-b',
}),
)
})
it('密码登录时,业务支持/快递/供应商/龙晨品牌中心不自动激活为内部可用账号', async () => {
const pendingUser = buildAuthUser({
name: '供应商甲',

View File

@ -47,21 +47,7 @@ export interface FirstLoginUpdateInput {
export interface FirstLoginUpdateResult {
token: string
user: {
id: string
email: string
name: string
role: Role
businessRoles: BusinessAccountType[]
status: UserStatus
supplierCode: string | null
supplierName: string | null
departmentId: string | null
teamId: string | null
isFirstLogin: false
workspaceAccess?: WorkspaceAccessLevel
workspaceAllowedPathPrefixes?: string[]
}
user: AuthUserDTO
}
export type CurrentUserCheckResult =
@ -91,9 +77,13 @@ export interface AuthUserDTO {
isFirstLogin: boolean
department: { id: string; name: string; fullPath: string } | null
departmentPaths: string[]
departmentId: string | null
team: { id: string; name: string } | null
teamId: string | null
managedDepartment: { id: string; name: string; fullPath: string } | null
managedDepartmentId: string | null
managedTeam: { id: string; name: string } | null
managedTeamId: string | null
avatar: string | null
lastLoginAt: Date | null
workspaceAccess?: WorkspaceAccessLevel
@ -150,6 +140,46 @@ async function buildWorkspaceTokenPermissions(user: WorkspaceTokenPermissionUser
}
}
async function buildAuthUserDTO(
user: AuthUserWithRelations,
options: { activationRequired?: boolean } = {},
): Promise<AuthUserDTO> {
const departmentPaths = await getUserDepartmentPathsById(user.id)
const workspacePermissions = await buildWorkspaceTokenPermissions({
role: user.role,
businessRoles: user.businessRoles,
department: user.department,
departmentPaths,
})
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
businessRoles: user.businessRoles,
status: user.status,
supplierCode: user.supplierCode,
supplierName: user.supplierName,
loginSource: user.loginSource,
activationRequired: options.activationRequired ?? user.status === 'PENDING',
employeeDirectoryId: user.employeeDirectoryId,
isFirstLogin: user.isFirstLogin,
department: user.department,
departmentPaths,
departmentId: user.departmentId,
team: user.team,
teamId: user.teamId,
managedDepartment: user.managedDepartment,
managedDepartmentId: user.managedDepartment?.id ?? null,
managedTeam: user.managedTeam,
managedTeamId: user.managedTeam?.id ?? null,
avatar: user.avatar,
lastLoginAt: user.lastLoginAt,
...workspacePermissions,
}
}
// ============ 服务类 ============
class AuthService {
@ -281,41 +311,32 @@ class AuthService {
oldEmail: currentUser.email,
oldName: currentUser.name,
})
const departmentPaths = await getUserDepartmentPathsById(updatedUser.id)
const effectiveUser = await authRepository.findByIdWithRelations(updatedUser.id)
if (!effectiveUser) {
throw new AppError(ErrorCode.USER_NOT_FOUND, '用户不存在')
}
const workspacePermissions = await buildWorkspaceTokenPermissions({
role: updatedUser.role,
businessRoles: updatedUser.businessRoles,
departmentPaths,
role: effectiveUser.role,
businessRoles: effectiveUser.businessRoles,
department: effectiveUser.department,
departmentPaths: await getUserDepartmentPathsById(effectiveUser.id),
})
const token = generateToken({
userId: updatedUser.id,
email: updatedUser.email,
name: updatedUser.name,
role: updatedUser.role,
status: updatedUser.status,
userId: effectiveUser.id,
email: effectiveUser.email,
name: effectiveUser.name,
role: effectiveUser.role,
status: effectiveUser.status,
isFirstLogin: false,
departmentId: updatedUser.departmentId || undefined,
teamId: updatedUser.teamId || undefined,
departmentId: effectiveUser.departmentId || undefined,
teamId: effectiveUser.teamId || undefined,
...workspacePermissions,
})
return {
token,
user: {
id: updatedUser.id,
email: updatedUser.email,
name: updatedUser.name,
role: updatedUser.role,
businessRoles: updatedUser.businessRoles,
status: updatedUser.status,
supplierCode: null,
supplierName: null,
departmentId: updatedUser.departmentId,
teamId: updatedUser.teamId,
isFirstLogin: false,
...workspacePermissions,
},
user: await buildAuthUserDTO(effectiveUser, { activationRequired: false }),
}
}
@ -364,38 +385,9 @@ class AuthService {
}
}
const departmentPaths = await getUserDepartmentPathsById(user.id)
const workspacePermissions = await buildWorkspaceTokenPermissions({
role: user.role,
businessRoles: user.businessRoles,
department: user.department,
departmentPaths,
})
return {
authenticated: true,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
businessRoles: user.businessRoles,
status: user.status,
supplierCode: user.supplierCode,
supplierName: user.supplierName,
loginSource: user.loginSource,
activationRequired: false,
employeeDirectoryId: user.employeeDirectoryId,
isFirstLogin: user.isFirstLogin,
department: user.department,
departmentPaths,
team: user.team,
managedDepartment: user.managedDepartment,
managedTeam: user.managedTeam,
avatar: user.avatar,
lastLoginAt: user.lastLoginAt,
...workspacePermissions,
},
user: await buildAuthUserDTO(user, { activationRequired: false }),
}
}
@ -524,28 +516,7 @@ class AuthService {
return {
token,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
businessRoles: user.businessRoles,
status: user.status,
supplierCode: user.supplierCode,
supplierName: user.supplierName,
loginSource: user.loginSource,
activationRequired: user.status === 'PENDING',
employeeDirectoryId: user.employeeDirectoryId,
isFirstLogin: user.isFirstLogin,
department: user.department,
departmentPaths,
team: user.team,
managedDepartment: user.managedDepartment,
managedTeam: user.managedTeam,
avatar: user.avatar,
lastLoginAt: user.lastLoginAt,
...workspacePermissions,
},
user: await buildAuthUserDTO(user),
isFirstLogin: user.isFirstLogin,
}
}