mirror of https://github.com/wg-easy/wg-easy
12 changed files with 746 additions and 5 deletions
@ -0,0 +1,29 @@ |
|||
import { createError, getValidatedRouterParams } from 'h3'; |
|||
|
|||
import Database from '#server/utils/Database'; |
|||
import { definePermissionEventHandler } from '#server/utils/handler'; |
|||
import { validateZod } from '#server/utils/types'; |
|||
import { ClientGroupGetSchema } from '#db/repositories/clientGroup/types'; |
|||
|
|||
export default definePermissionEventHandler( |
|||
'admin', |
|||
'any', |
|||
async ({ event }) => { |
|||
const { groupId } = await getValidatedRouterParams( |
|||
event, |
|||
validateZod(ClientGroupGetSchema, event) |
|||
); |
|||
|
|||
const group = await Database.clientGroups.get(groupId); |
|||
|
|||
if (!group) { |
|||
throw createError({ |
|||
statusCode: 404, |
|||
statusMessage: 'Client group not found', |
|||
}); |
|||
} |
|||
|
|||
await Database.clientGroups.delete(groupId); |
|||
return { success: true }; |
|||
} |
|||
); |
|||
@ -0,0 +1,28 @@ |
|||
import { createError, getValidatedRouterParams } from 'h3'; |
|||
|
|||
import Database from '#server/utils/Database'; |
|||
import { definePermissionEventHandler } from '#server/utils/handler'; |
|||
import { validateZod } from '#server/utils/types'; |
|||
import { ClientGroupGetSchema } from '#db/repositories/clientGroup/types'; |
|||
|
|||
export default definePermissionEventHandler( |
|||
'admin', |
|||
'any', |
|||
async ({ event }) => { |
|||
const { groupId } = await getValidatedRouterParams( |
|||
event, |
|||
validateZod(ClientGroupGetSchema, event) |
|||
); |
|||
|
|||
const group = await Database.clientGroups.getDetails(groupId); |
|||
|
|||
if (!group) { |
|||
throw createError({ |
|||
statusCode: 404, |
|||
statusMessage: 'Client group not found', |
|||
}); |
|||
} |
|||
|
|||
return group; |
|||
} |
|||
); |
|||
@ -0,0 +1,48 @@ |
|||
import { createError, getValidatedRouterParams, readValidatedBody } from 'h3'; |
|||
|
|||
import Database from '#server/utils/Database'; |
|||
import { definePermissionEventHandler } from '#server/utils/handler'; |
|||
import { validateZod } from '#server/utils/types'; |
|||
import { |
|||
ClientGroupGetSchema, |
|||
ClientGroupUpdateSchema, |
|||
} from '#db/repositories/clientGroup/types'; |
|||
|
|||
export default definePermissionEventHandler( |
|||
'admin', |
|||
'any', |
|||
async ({ event }) => { |
|||
const { groupId } = await getValidatedRouterParams( |
|||
event, |
|||
validateZod(ClientGroupGetSchema, event) |
|||
); |
|||
|
|||
const data = await readValidatedBody( |
|||
event, |
|||
validateZod(ClientGroupUpdateSchema, event) |
|||
); |
|||
|
|||
try { |
|||
const group = await Database.clientGroups.update(groupId, data); |
|||
return { success: true, group }; |
|||
} catch (error) { |
|||
if (error instanceof Error) { |
|||
if (error.message === 'Client group not found') { |
|||
throw createError({ |
|||
statusCode: 404, |
|||
statusMessage: error.message, |
|||
}); |
|||
} |
|||
|
|||
if (error.message === 'Client group already exists') { |
|||
throw createError({ |
|||
statusCode: 409, |
|||
statusMessage: error.message, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
throw error; |
|||
} |
|||
} |
|||
); |
|||
@ -0,0 +1,6 @@ |
|||
import Database from '#server/utils/Database'; |
|||
import { definePermissionEventHandler } from '#server/utils/handler'; |
|||
|
|||
export default definePermissionEventHandler('admin', 'any', async () => { |
|||
return Database.clientGroups.list(); |
|||
}); |
|||
@ -0,0 +1,34 @@ |
|||
import { createError, readValidatedBody } from 'h3'; |
|||
|
|||
import Database from '#server/utils/Database'; |
|||
import { definePermissionEventHandler } from '#server/utils/handler'; |
|||
import { validateZod } from '#server/utils/types'; |
|||
import { ClientGroupCreateSchema } from '#db/repositories/clientGroup/types'; |
|||
|
|||
export default definePermissionEventHandler( |
|||
'admin', |
|||
'any', |
|||
async ({ event }) => { |
|||
const data = await readValidatedBody( |
|||
event, |
|||
validateZod(ClientGroupCreateSchema, event) |
|||
); |
|||
|
|||
try { |
|||
const group = await Database.clientGroups.create(data); |
|||
return { success: true, group }; |
|||
} catch (error) { |
|||
if ( |
|||
error instanceof Error && |
|||
error.message === 'Client group already exists' |
|||
) { |
|||
throw createError({ |
|||
statusCode: 409, |
|||
statusMessage: error.message, |
|||
}); |
|||
} |
|||
|
|||
throw error; |
|||
} |
|||
} |
|||
); |
|||
@ -0,0 +1,32 @@ |
|||
import { createError, getValidatedRouterParams } from 'h3'; |
|||
|
|||
import Database from '#server/utils/Database'; |
|||
import { definePermissionEventHandler } from '#server/utils/handler'; |
|||
import { validateZod } from '#server/utils/types'; |
|||
import { ClientGroupClientParamsSchema } from '#db/repositories/clientGroup/types'; |
|||
|
|||
export default definePermissionEventHandler( |
|||
'admin', |
|||
'any', |
|||
async ({ event }) => { |
|||
const { clientId } = await getValidatedRouterParams( |
|||
event, |
|||
validateZod(ClientGroupClientParamsSchema, event) |
|||
); |
|||
|
|||
try { |
|||
await Database.clientGroups.getClientGroupId(clientId); |
|||
await Database.clientGroups.unassignClient(clientId); |
|||
return { success: true }; |
|||
} catch (error) { |
|||
if (error instanceof Error && error.message === 'Client not found') { |
|||
throw createError({ |
|||
statusCode: 404, |
|||
statusMessage: error.message, |
|||
}); |
|||
} |
|||
|
|||
throw error; |
|||
} |
|||
} |
|||
); |
|||
@ -0,0 +1,48 @@ |
|||
import { createError, getValidatedRouterParams, readValidatedBody } from 'h3'; |
|||
|
|||
import Database from '#server/utils/Database'; |
|||
import { definePermissionEventHandler } from '#server/utils/handler'; |
|||
import { validateZod } from '#server/utils/types'; |
|||
import { |
|||
ClientGroupAssignSchema, |
|||
ClientGroupClientParamsSchema, |
|||
} from '#db/repositories/clientGroup/types'; |
|||
|
|||
export default definePermissionEventHandler( |
|||
'admin', |
|||
'any', |
|||
async ({ event }) => { |
|||
const { clientId } = await getValidatedRouterParams( |
|||
event, |
|||
validateZod(ClientGroupClientParamsSchema, event) |
|||
); |
|||
|
|||
const { groupId } = await readValidatedBody( |
|||
event, |
|||
validateZod(ClientGroupAssignSchema, event) |
|||
); |
|||
|
|||
try { |
|||
await Database.clientGroups.assignClient(clientId, groupId); |
|||
return { success: true }; |
|||
} catch (error) { |
|||
if (error instanceof Error) { |
|||
if (error.message === 'Client not found') { |
|||
throw createError({ |
|||
statusCode: 404, |
|||
statusMessage: error.message, |
|||
}); |
|||
} |
|||
|
|||
if (error.message === 'Client group not found') { |
|||
throw createError({ |
|||
statusCode: 404, |
|||
statusMessage: error.message, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
throw error; |
|||
} |
|||
} |
|||
); |
|||
@ -0,0 +1,187 @@ |
|||
import { createEvent } from 'h3'; |
|||
import { beforeEach, describe, expect, test, vi } from 'vitest'; |
|||
|
|||
import { roles } from '../../shared/utils/permissions'; |
|||
|
|||
const mockDatabase = { |
|||
clientGroups: { |
|||
create: vi.fn(), |
|||
getDetails: vi.fn(), |
|||
list: vi.fn(), |
|||
}, |
|||
}; |
|||
|
|||
const mockGetCurrentUser = vi.fn(); |
|||
|
|||
vi.mock('#server/utils/Database', () => ({ |
|||
default: mockDatabase, |
|||
})); |
|||
|
|||
vi.mock('#server/utils/session', () => ({ |
|||
getCurrentUser: mockGetCurrentUser, |
|||
})); |
|||
|
|||
function createH3Event( |
|||
url: string, |
|||
options: RequestInit & { params?: Record<string, string> } = {} |
|||
) { |
|||
const event = createEvent(new Request(url, options)); |
|||
event.context.params = options.params; |
|||
|
|||
return event; |
|||
} |
|||
|
|||
describe('client group route handlers', () => { |
|||
beforeEach(() => { |
|||
vi.clearAllMocks(); |
|||
mockGetCurrentUser.mockResolvedValue({ |
|||
id: 1, |
|||
username: 'admin', |
|||
password: 'hash', |
|||
email: null, |
|||
name: 'Administrator', |
|||
role: roles.ADMIN, |
|||
totpKey: null, |
|||
totpVerified: false, |
|||
enabled: true, |
|||
oauthProvider: null, |
|||
oauthId: null, |
|||
createdAt: '', |
|||
updatedAt: '', |
|||
}); |
|||
}); |
|||
|
|||
test('rejects non-admin access before reaching the list handler', async () => { |
|||
mockGetCurrentUser.mockResolvedValueOnce({ |
|||
id: 2, |
|||
username: 'client', |
|||
password: 'hash', |
|||
email: null, |
|||
name: 'Client', |
|||
role: roles.CLIENT, |
|||
totpKey: null, |
|||
totpVerified: false, |
|||
enabled: true, |
|||
oauthProvider: null, |
|||
oauthId: null, |
|||
createdAt: '', |
|||
updatedAt: '', |
|||
}); |
|||
|
|||
const handler = (await import('../../server/api/client-group/index.get')) |
|||
.default; |
|||
|
|||
await expect( |
|||
handler(createH3Event('http://wg.test/api/client-group')) |
|||
).rejects.toMatchObject({ statusCode: 403 }); |
|||
expect(mockDatabase.clientGroups.list).not.toHaveBeenCalled(); |
|||
}); |
|||
|
|||
test('allows admin access to reach the list handler', async () => { |
|||
mockDatabase.clientGroups.list.mockResolvedValueOnce([]); |
|||
const handler = (await import('../../server/api/client-group/index.get')) |
|||
.default; |
|||
|
|||
await expect( |
|||
handler(createH3Event('http://wg.test/api/client-group')) |
|||
).resolves.toEqual([]); |
|||
expect(mockDatabase.clientGroups.list).toHaveBeenCalledOnce(); |
|||
}); |
|||
|
|||
test('malformed group id rejects before lookup', async () => { |
|||
const handler = ( |
|||
await import('../../server/api/client-group/[groupId]/index.get') |
|||
).default; |
|||
|
|||
await expect( |
|||
handler( |
|||
createH3Event('http://wg.test/api/client-group/not-a-number', { |
|||
params: { groupId: 'not-a-number' }, |
|||
}) |
|||
) |
|||
).rejects.toThrow(); |
|||
expect(mockDatabase.clientGroups.getDetails).not.toHaveBeenCalled(); |
|||
}); |
|||
|
|||
test('missing group returns 404', async () => { |
|||
mockDatabase.clientGroups.getDetails.mockResolvedValueOnce(undefined); |
|||
const handler = ( |
|||
await import('../../server/api/client-group/[groupId]/index.get') |
|||
).default; |
|||
|
|||
await expect( |
|||
handler( |
|||
createH3Event('http://wg.test/api/client-group/123', { |
|||
params: { groupId: '123' }, |
|||
}) |
|||
) |
|||
).rejects.toMatchObject({ |
|||
statusCode: 404, |
|||
statusMessage: 'Client group not found', |
|||
}); |
|||
}); |
|||
|
|||
test('duplicate group create returns 409', async () => { |
|||
mockDatabase.clientGroups.create.mockRejectedValueOnce( |
|||
new Error('Client group already exists') |
|||
); |
|||
const handler = (await import('../../server/api/client-group/index.post')) |
|||
.default; |
|||
|
|||
await expect( |
|||
handler( |
|||
createH3Event('http://wg.test/api/client-group', { |
|||
method: 'POST', |
|||
headers: { 'content-type': 'application/json' }, |
|||
body: JSON.stringify({ |
|||
name: 'Customers', |
|||
description: null, |
|||
allowedIps: null, |
|||
dns: null, |
|||
firewallIps: null, |
|||
}), |
|||
}) |
|||
) |
|||
).rejects.toMatchObject({ |
|||
statusCode: 409, |
|||
statusMessage: 'Client group already exists', |
|||
}); |
|||
}); |
|||
|
|||
test('successful detail response does not expose client secrets', async () => { |
|||
mockDatabase.clientGroups.getDetails.mockResolvedValueOnce({ |
|||
id: 1, |
|||
name: 'Customers', |
|||
description: null, |
|||
allowedIps: null, |
|||
dns: null, |
|||
firewallIps: null, |
|||
assignedClientCount: 1, |
|||
createdAt: new Date('2026-01-01T00:00:00.000Z'), |
|||
updatedAt: new Date('2026-01-01T00:00:00.000Z'), |
|||
clients: [ |
|||
{ |
|||
id: 7, |
|||
name: 'Phone', |
|||
enabled: true, |
|||
ipv4Address: '10.8.0.2', |
|||
ipv6Address: 'fdcc:ad94:bacf:61a4::cafe:2', |
|||
createdAt: new Date('2026-01-01T00:00:00.000Z'), |
|||
updatedAt: new Date('2026-01-01T00:00:00.000Z'), |
|||
}, |
|||
], |
|||
}); |
|||
const handler = ( |
|||
await import('../../server/api/client-group/[groupId]/index.get') |
|||
).default; |
|||
|
|||
const response = await handler( |
|||
createH3Event('http://wg.test/api/client-group/1', { |
|||
params: { groupId: '1' }, |
|||
}) |
|||
); |
|||
|
|||
expect(response.clients[0]).not.toHaveProperty('privateKey'); |
|||
expect(response.clients[0]).not.toHaveProperty('preSharedKey'); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue