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