实现系统管理后台 V1
This commit is contained in:
@@ -79,6 +79,18 @@ function createDebugMenu(): AuthMenuResult {
|
||||
}
|
||||
}
|
||||
|
||||
function createSystemMenu(): AuthMenuResult {
|
||||
return {
|
||||
menu_code: 'SYSTEM_SETTINGS',
|
||||
menu_name: '系统设置',
|
||||
route_path: '/system',
|
||||
component_key: 'SystemSettings',
|
||||
icon_key: 'pi pi-cog',
|
||||
permission_code: 'SYSTEM_ADMIN_CONSOLE_ACCESS',
|
||||
sort_order: 40,
|
||||
}
|
||||
}
|
||||
|
||||
async function mountShell(menus = [createReservationOrdersMenu(), createReservationTasksMenu()]) {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@@ -108,6 +120,11 @@ async function mountShell(menus = [createReservationOrdersMenu(), createReservat
|
||||
component: { template: '<div />' },
|
||||
meta: { titleKey: 'debugEml.title' },
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
component: { template: '<div />' },
|
||||
meta: { titleKey: 'nav.systemSettings' },
|
||||
},
|
||||
],
|
||||
})
|
||||
const pinia = createPinia()
|
||||
@@ -158,6 +175,13 @@ describe('ReservationAppShell', () => {
|
||||
expect(wrapper.text()).toContain('Debug EML')
|
||||
})
|
||||
|
||||
it('shows System Settings only when backend menus include it', async () => {
|
||||
const wrapper = await mountShell([createReservationOrdersMenu(), createSystemMenu()])
|
||||
|
||||
expect(wrapper.find('a[href="/system"]').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('系统设置')
|
||||
})
|
||||
|
||||
it('shows current user and hotel context in the top bar', async () => {
|
||||
const wrapper = await mountShell()
|
||||
|
||||
|
||||
@@ -53,6 +53,15 @@ describe('reservation router', () => {
|
||||
expect(router.resolve('/debug/eml-superagent').name).toBe('debug-eml-superagent')
|
||||
})
|
||||
|
||||
it('exposes the System Admin V1 routes', () => {
|
||||
expect(router.resolve('/system/users').name).toBe('system-users')
|
||||
expect(router.resolve('/system/roles').name).toBe('system-roles')
|
||||
expect(router.resolve('/system/menus').name).toBe('system-menus')
|
||||
expect(router.resolve('/system/hotels').name).toBe('system-hotels')
|
||||
expect(router.resolve('/system/audits').name).toBe('system-audits')
|
||||
expect(router.resolve('/system/not-ready').name).toBe('not-found')
|
||||
})
|
||||
|
||||
it('declares permissions for protected P0 routes', () => {
|
||||
expect(router.resolve('/reservation/orders').meta.permission).toBe('RESERVATION_ORDER_READ')
|
||||
expect(router.resolve('/reservation/orders/20001').meta.permission).toBe('RESERVATION_ORDER_READ')
|
||||
@@ -62,6 +71,11 @@ describe('reservation router', () => {
|
||||
'SOURCE_MESSAGE_ORIGINAL_READ',
|
||||
)
|
||||
expect(router.resolve('/debug/eml-superagent').meta.permission).toBe('SYSTEM_DEBUG_EML_RUN')
|
||||
expect(router.resolve('/system/users').meta.permission).toBe('SYSTEM_USER_MANAGE')
|
||||
expect(router.resolve('/system/roles').meta.permission).toBe('SYSTEM_ROLE_MANAGE')
|
||||
expect(router.resolve('/system/menus').meta.permission).toBe('SYSTEM_MENU_MANAGE')
|
||||
expect(router.resolve('/system/hotels').meta.permission).toBe('HOTEL_MANAGE')
|
||||
expect(router.resolve('/system/audits').meta.permission).toBe('SYSTEM_ADMIN_CONSOLE_ACCESS')
|
||||
})
|
||||
|
||||
it('redirects anonymous users to login with the protected target as redirect', async () => {
|
||||
@@ -119,6 +133,19 @@ describe('reservation router', () => {
|
||||
expect(restored).toBe(true)
|
||||
})
|
||||
|
||||
it('redirects the System Admin entry to the first allowed management page', async () => {
|
||||
const guard = createAuthGuard(() =>
|
||||
createAuthStoreForGuard({
|
||||
hasPermission: (permission: string) =>
|
||||
permission === 'SYSTEM_ADMIN_CONSOLE_ACCESS' || permission === 'HOTEL_MANAGE',
|
||||
}),
|
||||
)
|
||||
|
||||
await expect(guard(createRoute('/system', { permission: 'SYSTEM_ADMIN_CONSOLE_ACCESS' }))).resolves.toBe(
|
||||
'/system/hotels',
|
||||
)
|
||||
})
|
||||
|
||||
it('redirects the root route to the default page when the backend menu points to root', async () => {
|
||||
const guard = createAuthGuard(() =>
|
||||
createAuthStoreForGuard({
|
||||
|
||||
238
client/src/tests/systemAdminService.spec.ts
Normal file
238
client/src/tests/systemAdminService.spec.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
assignAdminRolePermissions,
|
||||
assignAdminUserHotels,
|
||||
assignAdminUserRoles,
|
||||
createAdminHotel,
|
||||
createAdminMenu,
|
||||
createAdminRole,
|
||||
createAdminUser,
|
||||
fetchAdminAudits,
|
||||
fetchAdminHotels,
|
||||
fetchAdminMenus,
|
||||
fetchAdminPermissions,
|
||||
fetchAdminRoleDetail,
|
||||
fetchAdminRoles,
|
||||
fetchAdminUserDetail,
|
||||
fetchAdminUsers,
|
||||
resetAdminUserPassword,
|
||||
updateAdminHotel,
|
||||
updateAdminHotelStatus,
|
||||
updateAdminMenu,
|
||||
updateAdminRole,
|
||||
updateAdminUser,
|
||||
} from '@/services/systemAdminService'
|
||||
|
||||
const jsonHeaders = {
|
||||
headers: {
|
||||
get: (name: string) => (name.toLowerCase() === 'content-type' ? 'application/json' : null),
|
||||
},
|
||||
}
|
||||
|
||||
function mockJsonResponse(payload: unknown): Response {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => payload,
|
||||
text: async () => JSON.stringify(payload),
|
||||
...jsonHeaders,
|
||||
} as Response
|
||||
}
|
||||
|
||||
describe('systemAdminService', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
sessionStorage.clear()
|
||||
})
|
||||
|
||||
it('fetches admin users with filters', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
items: [],
|
||||
page: {
|
||||
page_num: 2,
|
||||
page_size: 10,
|
||||
total: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await fetchAdminUsers({
|
||||
keyword: 'admin',
|
||||
user_status: 'ACTIVE',
|
||||
page_num: 2,
|
||||
page_size: 10,
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/admin/users?keyword=admin&user_status=ACTIVE&page_num=2&page_size=10',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('fetches role, menu, hotel lists and details from admin endpoints', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockJsonResponse({ items: [], page: {} }))
|
||||
|
||||
await fetchAdminRoles({ role_status: 'DISABLED' })
|
||||
await fetchAdminMenus({ menu_status: 'ACTIVE' })
|
||||
await fetchAdminHotels({ hotel_status: 'ACTIVE' })
|
||||
await fetchAdminUserDetail('10001')
|
||||
await fetchAdminRoleDetail('20001')
|
||||
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/api/admin/roles?role_status=DISABLED',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/api/admin/menus?menu_status=ACTIVE',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'/api/admin/hotels?hotel_status=ACTIVE',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'/api/admin/users/10001',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
5,
|
||||
'/api/admin/roles/20001',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('fetches the readonly permission list', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
mockJsonResponse([
|
||||
{
|
||||
id: '1',
|
||||
permission_code: 'SYSTEM_USER_MANAGE',
|
||||
permission_name: '用户管理',
|
||||
permission_group: 'SYSTEM',
|
||||
permission_status: 'ACTIVE',
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
const result = await fetchAdminPermissions()
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/admin/permissions', expect.objectContaining({ method: 'GET' }))
|
||||
expect(result[0]?.permission_code).toBe('SYSTEM_USER_MANAGE')
|
||||
})
|
||||
|
||||
it('sends admin user write operations to backend endpoints', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockJsonResponse({ id: '10001' }))
|
||||
|
||||
await createAdminUser({
|
||||
username: 'operator',
|
||||
initial_password: 'User@123456',
|
||||
display_name: 'Operator',
|
||||
user_status: 'ACTIVE',
|
||||
role_ids: ['20001'],
|
||||
hotel_ids: ['HOTEL-TEST'],
|
||||
default_hotel_id: 'HOTEL-TEST',
|
||||
})
|
||||
await updateAdminUser('10001', {
|
||||
display_name: 'Operator A',
|
||||
user_status: 'DISABLED',
|
||||
})
|
||||
await assignAdminUserRoles('10001', {
|
||||
role_ids: ['20002'],
|
||||
})
|
||||
await assignAdminUserHotels('10001', {
|
||||
hotel_ids: ['HOTEL-TEST'],
|
||||
default_hotel_id: 'HOTEL-TEST',
|
||||
})
|
||||
await resetAdminUserPassword('10001')
|
||||
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/api/admin/users',
|
||||
expect.objectContaining({ method: 'POST' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/api/admin/users/10001',
|
||||
expect.objectContaining({ method: 'PUT' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'/api/admin/users/10001/roles',
|
||||
expect.objectContaining({ method: 'PUT' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'/api/admin/users/10001/hotels',
|
||||
expect.objectContaining({ method: 'PUT' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
5,
|
||||
'/api/admin/users/10001/password-reset',
|
||||
expect.objectContaining({ method: 'POST', body: JSON.stringify({}) }),
|
||||
)
|
||||
})
|
||||
|
||||
it('sends role, menu, hotel writes and audit queries to admin endpoints', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockJsonResponse({ id: '1', items: [], page: {} }))
|
||||
|
||||
await createAdminRole({ role_code: 'CUSTOM_ROLE', role_name: 'Custom role', role_status: 'ACTIVE' })
|
||||
await updateAdminRole('20001', { role_name: 'Custom role A', role_status: 'DISABLED' })
|
||||
await assignAdminRolePermissions('20001', { permission_ids: ['30001'] })
|
||||
await createAdminMenu({
|
||||
menu_code: 'CUSTOM_MENU',
|
||||
menu_name: 'Custom menu',
|
||||
menu_type: 'PAGE',
|
||||
visible: false,
|
||||
menu_status: 'DISABLED',
|
||||
})
|
||||
await updateAdminMenu('40001', {
|
||||
menu_name: 'Custom menu A',
|
||||
menu_type: 'PAGE',
|
||||
visible: true,
|
||||
menu_status: 'ACTIVE',
|
||||
})
|
||||
await createAdminHotel({
|
||||
hotel_id: 'HOTEL-BKK',
|
||||
hotel_name: 'Bangkok Hotel',
|
||||
time_zone: 'Asia/Bangkok',
|
||||
})
|
||||
await updateAdminHotel('HOTEL-BKK', {
|
||||
hotel_name: 'Bangkok Hotel A',
|
||||
time_zone: 'Asia/Bangkok',
|
||||
})
|
||||
await updateAdminHotelStatus('HOTEL-BKK', { hotel_status: 'ACTIVE' })
|
||||
await fetchAdminAudits({ target_type: 'PLATFORM_USER', page_num: 1, page_size: 20 })
|
||||
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/admin/roles', expect.objectContaining({ method: 'POST' }))
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/admin/roles/20001', expect.objectContaining({ method: 'PUT' }))
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'/api/admin/roles/20001/permissions',
|
||||
expect.objectContaining({ method: 'PUT' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(4, '/api/admin/menus', expect.objectContaining({ method: 'POST' }))
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(5, '/api/admin/menus/40001', expect.objectContaining({ method: 'PUT' }))
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(6, '/api/admin/hotels', expect.objectContaining({ method: 'POST' }))
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
7,
|
||||
'/api/admin/hotels/HOTEL-BKK',
|
||||
expect.objectContaining({ method: 'PUT' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
8,
|
||||
'/api/admin/hotels/HOTEL-BKK/status',
|
||||
expect.objectContaining({ method: 'PUT' }),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
9,
|
||||
'/api/admin/audits?target_type=PLATFORM_USER&page_num=1&page_size=20',
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user