fix(auth): 补齐主管权限范围字段
This commit is contained in:
parent
cf54f94ba5
commit
50b8f61a69
@ -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 {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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: '供应商甲',
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user