mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-03 01:18:43 +02:00
fix: Remove idNumberMiddleware
and change to use parameters
in validPath
method instead (#8734)
## About the changes - Remove `idNumberMiddleware` method and change to use `parameters` field in `openApiService.validPath` method for the flexibility. - Remove unnecessary `Number` type converting method and change them to use `<{id: number}>` to specify the type. ### Reference The changed response looks like the one below. ```JSON { "id":"8174a692-7427-4d35-b7b9-6543b9d3db6e", "name":"BadDataError", "message":"Request validation failed: your request body or params contain invalid data. Refer to the `details` list for more information.", "details":[ { "message":"The `/params/id` property must be integer. You sent undefined.", "path":"/params/id" } ] } ``` I think it might be better to customize the error response, especially `"You sent undefined."`, on another pull request if this one is accepted. I prefer to separate jobs to divide the context and believe that it helps reviewer easier to understand.
This commit is contained in:
parent
18591dd017
commit
695873132e
@ -204,14 +204,8 @@ test('should create segments', async () => {
|
|||||||
expect(segments.map((s) => s.name)).toEqual(['a', 'b', 'c']);
|
expect(segments.map((s) => s.name)).toEqual(['a', 'b', 'c']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return 400 for non numeeric segment ID', async () => {
|
test('should return 400 for non numeric segment ID', async () => {
|
||||||
const { body } = await app.request
|
await app.request.get(`${SEGMENTS_BASE_PATH}/stringName`).expect(400);
|
||||||
.get(`${SEGMENTS_BASE_PATH}/stringName`)
|
|
||||||
.expect(400);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
message:
|
|
||||||
'Request validation failed: your request body or params contain invalid data: ID should be an integer',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should update segments', async () => {
|
test('should update segments', async () => {
|
||||||
|
@ -41,7 +41,6 @@ import {
|
|||||||
|
|
||||||
import { anonymiseKeys, extractUserIdFromUser } from '../../util';
|
import { anonymiseKeys, extractUserIdFromUser } from '../../util';
|
||||||
import { BadDataError } from '../../error';
|
import { BadDataError } from '../../error';
|
||||||
import idNumberMiddleware from '../../middleware/id-number-middleware';
|
|
||||||
|
|
||||||
type IUpdateFeatureStrategySegmentsRequest = IAuthRequest<
|
type IUpdateFeatureStrategySegmentsRequest = IAuthRequest<
|
||||||
{},
|
{},
|
||||||
@ -156,11 +155,21 @@ export class SegmentsController extends Controller {
|
|||||||
summary: 'Get strategies that reference segment',
|
summary: 'Get strategies that reference segment',
|
||||||
description:
|
description:
|
||||||
'Retrieve all strategies that reference the specified segment.',
|
'Retrieve all strategies that reference the specified segment.',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
description: 'a segment id',
|
||||||
|
},
|
||||||
|
],
|
||||||
responses: {
|
responses: {
|
||||||
200: createResponseSchema('segmentStrategiesSchema'),
|
200: createResponseSchema('segmentStrategiesSchema'),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
idNumberMiddleware(),
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -175,6 +184,17 @@ export class SegmentsController extends Controller {
|
|||||||
summary: 'Deletes a segment by id',
|
summary: 'Deletes a segment by id',
|
||||||
description:
|
description:
|
||||||
'Deletes a segment by its id, if not found returns a 409 error',
|
'Deletes a segment by its id, if not found returns a 409 error',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
description: 'a segment id',
|
||||||
|
},
|
||||||
|
],
|
||||||
tags: ['Segments'],
|
tags: ['Segments'],
|
||||||
operationId: 'removeSegment',
|
operationId: 'removeSegment',
|
||||||
responses: {
|
responses: {
|
||||||
@ -182,7 +202,6 @@ export class SegmentsController extends Controller {
|
|||||||
...getStandardResponses(401, 403, 409),
|
...getStandardResponses(401, 403, 409),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
idNumberMiddleware(),
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -198,13 +217,23 @@ export class SegmentsController extends Controller {
|
|||||||
'Updates the content of the segment with the provided payload. Requires `name` and `constraints` to be present. If `project` is not present, it will be set to `null`. Any other fields not specified will be left untouched.',
|
'Updates the content of the segment with the provided payload. Requires `name` and `constraints` to be present. If `project` is not present, it will be set to `null`. Any other fields not specified will be left untouched.',
|
||||||
tags: ['Segments'],
|
tags: ['Segments'],
|
||||||
operationId: 'updateSegment',
|
operationId: 'updateSegment',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
description: 'a segment id',
|
||||||
|
},
|
||||||
|
],
|
||||||
requestBody: createRequestSchema('upsertSegmentSchema'),
|
requestBody: createRequestSchema('upsertSegmentSchema'),
|
||||||
responses: {
|
responses: {
|
||||||
204: emptyResponse,
|
204: emptyResponse,
|
||||||
...getStandardResponses(400, 401, 403, 409, 415),
|
...getStandardResponses(400, 401, 403, 409, 415),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
idNumberMiddleware(),
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -219,12 +248,22 @@ export class SegmentsController extends Controller {
|
|||||||
description: 'Retrieves a segment based on its ID.',
|
description: 'Retrieves a segment based on its ID.',
|
||||||
tags: ['Segments'],
|
tags: ['Segments'],
|
||||||
operationId: 'getSegment',
|
operationId: 'getSegment',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
description: 'a segment id',
|
||||||
|
},
|
||||||
|
],
|
||||||
responses: {
|
responses: {
|
||||||
200: createResponseSchema('adminSegmentSchema'),
|
200: createResponseSchema('adminSegmentSchema'),
|
||||||
...getStandardResponses(404),
|
...getStandardResponses(404),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
idNumberMiddleware(),
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -353,7 +392,7 @@ export class SegmentsController extends Controller {
|
|||||||
req: IAuthRequest<{ id: number }>,
|
req: IAuthRequest<{ id: number }>,
|
||||||
res: Response<SegmentStrategiesSchema>,
|
res: Response<SegmentStrategiesSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const id = Number(req.params.id);
|
const id = req.params.id;
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
const strategies = await this.segmentService.getVisibleStrategies(
|
const strategies = await this.segmentService.getVisibleStrategies(
|
||||||
id,
|
id,
|
||||||
@ -386,10 +425,10 @@ export class SegmentsController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeSegment(
|
async removeSegment(
|
||||||
req: IAuthRequest<{ id: string }>,
|
req: IAuthRequest<{ id: number }>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const id = Number(req.params.id);
|
const id = req.params.id;
|
||||||
|
|
||||||
let segmentIsInUse = false;
|
let segmentIsInUse = false;
|
||||||
segmentIsInUse = await this.segmentService.isInUse(id);
|
segmentIsInUse = await this.segmentService.isInUse(id);
|
||||||
@ -403,10 +442,10 @@ export class SegmentsController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateSegment(
|
async updateSegment(
|
||||||
req: IAuthRequest<{ id: string }>,
|
req: IAuthRequest<{ id: number }>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const id = Number(req.params.id);
|
const id = req.params.id;
|
||||||
const updateRequest: UpsertSegmentSchema = {
|
const updateRequest: UpsertSegmentSchema = {
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
description: req.body.description,
|
description: req.body.description,
|
||||||
@ -423,10 +462,10 @@ export class SegmentsController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSegment(
|
async getSegment(
|
||||||
req: Request<{ id: string }>,
|
req: Request<{ id: number }>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const id = Number(req.params.id);
|
const id = req.params.id;
|
||||||
const segment = await this.segmentService.get(id);
|
const segment = await this.segmentService.get(id);
|
||||||
if (this.flagResolver.isEnabled('anonymiseEventLog')) {
|
if (this.flagResolver.isEnabled('anonymiseEventLog')) {
|
||||||
res.json(anonymiseKeys(segment, ['createdBy']));
|
res.json(anonymiseKeys(segment, ['createdBy']));
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import supertest from 'supertest';
|
|
||||||
import idNumberMiddleware from './id-number-middleware';
|
|
||||||
|
|
||||||
describe('idNumberMiddleware', () => {
|
|
||||||
it('should pass when id is a valid integer', async () => {
|
|
||||||
const app = express();
|
|
||||||
app.use('/:id', idNumberMiddleware());
|
|
||||||
app.get('/:id', (req, res) => {
|
|
||||||
res.status(200).send('Valid ID');
|
|
||||||
});
|
|
||||||
|
|
||||||
await supertest(app)
|
|
||||||
.get('/123')
|
|
||||||
.expect(200)
|
|
||||||
.expect((res) => {
|
|
||||||
expect(res.text).toBe('Valid ID');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should throw BadDataError when id is not a valid integer', async () => {
|
|
||||||
const app = express();
|
|
||||||
app.use('/:id', idNumberMiddleware());
|
|
||||||
app.get('/:id', (req, res) => {
|
|
||||||
res.status(200).send('This should not be executed');
|
|
||||||
});
|
|
||||||
|
|
||||||
const { body } = await supertest(app).get('/abc').expect(400);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
details: [{ message: 'ID should be an integer' }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,16 +0,0 @@
|
|||||||
import { BadDataError } from '../error';
|
|
||||||
|
|
||||||
const idNumberMiddleware = (): any => {
|
|
||||||
return async (req, res, next) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
if (!Number.isInteger(Number(id))) {
|
|
||||||
res.status(400).send(
|
|
||||||
new BadDataError('ID should be an integer').toJSON(),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default idNumberMiddleware;
|
|
@ -55,7 +55,6 @@ import {
|
|||||||
type CreateUserResponseSchema,
|
type CreateUserResponseSchema,
|
||||||
} from '../../openapi/spec/create-user-response-schema';
|
} from '../../openapi/spec/create-user-response-schema';
|
||||||
import type { IRoleWithPermissions } from '../../types/stores/access-store';
|
import type { IRoleWithPermissions } from '../../types/stores/access-store';
|
||||||
import idNumberMiddleware from '../../middleware/id-number-middleware';
|
|
||||||
|
|
||||||
export default class UserAdminController extends Controller {
|
export default class UserAdminController extends Controller {
|
||||||
private flagResolver: IFlagResolver;
|
private flagResolver: IFlagResolver;
|
||||||
@ -144,6 +143,17 @@ export default class UserAdminController extends Controller {
|
|||||||
operationId: 'changeUserPassword',
|
operationId: 'changeUserPassword',
|
||||||
summary: 'Change password for a user',
|
summary: 'Change password for a user',
|
||||||
description: 'Change password for a user as an admin',
|
description: 'Change password for a user as an admin',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
description: 'a user id',
|
||||||
|
},
|
||||||
|
],
|
||||||
requestBody: createRequestSchema('passwordSchema'),
|
requestBody: createRequestSchema('passwordSchema'),
|
||||||
responses: {
|
responses: {
|
||||||
200: emptyResponse,
|
200: emptyResponse,
|
||||||
@ -256,6 +266,15 @@ export default class UserAdminController extends Controller {
|
|||||||
description:
|
description:
|
||||||
'Gets a list of permissions for a user, additional project and environment can be specified.',
|
'Gets a list of permissions for a user, additional project and environment can be specified.',
|
||||||
parameters: [
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
description: 'a user id',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'project',
|
name: 'project',
|
||||||
in: 'query',
|
in: 'query',
|
||||||
@ -341,12 +360,22 @@ export default class UserAdminController extends Controller {
|
|||||||
operationId: 'getUser',
|
operationId: 'getUser',
|
||||||
summary: 'Get user',
|
summary: 'Get user',
|
||||||
description: 'Will return a single user by id',
|
description: 'Will return a single user by id',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
description: 'a user id',
|
||||||
|
},
|
||||||
|
],
|
||||||
responses: {
|
responses: {
|
||||||
200: createResponseSchema('userSchema'),
|
200: createResponseSchema('userSchema'),
|
||||||
...getStandardResponses(400, 401, 404),
|
...getStandardResponses(400, 401, 404),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
idNumberMiddleware(),
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -367,9 +396,11 @@ export default class UserAdminController extends Controller {
|
|||||||
{
|
{
|
||||||
name: 'id',
|
name: 'id',
|
||||||
in: 'path',
|
in: 'path',
|
||||||
|
required: true,
|
||||||
schema: {
|
schema: {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
},
|
},
|
||||||
|
description: 'a user id',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
responses: {
|
responses: {
|
||||||
@ -377,7 +408,6 @@ export default class UserAdminController extends Controller {
|
|||||||
...getStandardResponses(400, 401, 403, 404),
|
...getStandardResponses(400, 401, 403, 404),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
idNumberMiddleware(),
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -393,12 +423,22 @@ export default class UserAdminController extends Controller {
|
|||||||
operationId: 'deleteUser',
|
operationId: 'deleteUser',
|
||||||
summary: 'Delete a user',
|
summary: 'Delete a user',
|
||||||
description: 'Deletes the user with the given userId',
|
description: 'Deletes the user with the given userId',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
description: 'a user id',
|
||||||
|
},
|
||||||
|
],
|
||||||
responses: {
|
responses: {
|
||||||
200: emptyResponse,
|
200: emptyResponse,
|
||||||
...getStandardResponses(401, 403, 404),
|
...getStandardResponses(401, 403, 404),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
idNumberMiddleware(),
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -507,9 +547,12 @@ export default class UserAdminController extends Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser(req: Request, res: Response<UserSchema>): Promise<void> {
|
async getUser(
|
||||||
|
req: Request<{ id: number }>,
|
||||||
|
res: Response<UserSchema>,
|
||||||
|
): Promise<void> {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const user = await this.userService.getUser(Number(id));
|
const user = await this.userService.getUser(id);
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
200,
|
200,
|
||||||
@ -568,7 +611,7 @@ export default class UserAdminController extends Controller {
|
|||||||
|
|
||||||
async updateUser(
|
async updateUser(
|
||||||
req: IAuthRequest<
|
req: IAuthRequest<
|
||||||
{ id: string },
|
{ id: number },
|
||||||
CreateUserResponseSchema,
|
CreateUserResponseSchema,
|
||||||
UpdateUserSchema
|
UpdateUserSchema
|
||||||
>,
|
>,
|
||||||
@ -578,14 +621,14 @@ export default class UserAdminController extends Controller {
|
|||||||
const { id } = params;
|
const { id } = params;
|
||||||
const { name, email, rootRole } = body;
|
const { name, email, rootRole } = body;
|
||||||
|
|
||||||
await this.throwIfScimUser({ id: Number(id) });
|
await this.throwIfScimUser({ id });
|
||||||
const normalizedRootRole = Number.isInteger(Number(rootRole))
|
const normalizedRootRole = Number.isInteger(Number(rootRole))
|
||||||
? Number(rootRole)
|
? Number(rootRole)
|
||||||
: (rootRole as RoleName);
|
: (rootRole as RoleName);
|
||||||
|
|
||||||
const updateUser = await this.userService.updateUser(
|
const updateUser = await this.userService.updateUser(
|
||||||
{
|
{
|
||||||
id: Number(id),
|
id,
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
rootRole: normalizedRootRole,
|
rootRole: normalizedRootRole,
|
||||||
@ -604,11 +647,14 @@ export default class UserAdminController extends Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUser(req: IAuthRequest, res: Response): Promise<void> {
|
async deleteUser(
|
||||||
|
req: IAuthRequest<{ id: number }>,
|
||||||
|
res: Response,
|
||||||
|
): Promise<void> {
|
||||||
const { user, params } = req;
|
const { user, params } = req;
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
await this.throwIfScimUser({ id: Number(id) });
|
await this.throwIfScimUser({ id });
|
||||||
|
|
||||||
await this.userService.deleteUser(+id, req.audit);
|
await this.userService.deleteUser(+id, req.audit);
|
||||||
res.status(200).send();
|
res.status(200).send();
|
||||||
@ -625,13 +671,13 @@ export default class UserAdminController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async changeUserPassword(
|
async changeUserPassword(
|
||||||
req: IAuthRequest<{ id: string }, unknown, PasswordSchema>,
|
req: IAuthRequest<{ id: number }, unknown, PasswordSchema>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { password } = req.body;
|
const { password } = req.body;
|
||||||
|
|
||||||
await this.throwIfScimUser({ id: Number(id) });
|
await this.throwIfScimUser({ id });
|
||||||
|
|
||||||
await this.userService.changePassword(+id, password);
|
await this.userService.changePassword(+id, password);
|
||||||
res.status(200).send();
|
res.status(200).send();
|
||||||
|
@ -28,7 +28,6 @@ import {
|
|||||||
createPatSchema,
|
createPatSchema,
|
||||||
} from '../../../openapi/spec/create-pat-schema';
|
} from '../../../openapi/spec/create-pat-schema';
|
||||||
import { ForbiddenError, NotFoundError } from '../../../error';
|
import { ForbiddenError, NotFoundError } from '../../../error';
|
||||||
import idNumberMiddleware from '../../../middleware/id-number-middleware';
|
|
||||||
|
|
||||||
export default class PatController extends Controller {
|
export default class PatController extends Controller {
|
||||||
private patService: PatService;
|
private patService: PatService;
|
||||||
@ -107,12 +106,22 @@ export default class PatController extends Controller {
|
|||||||
'Delete a personal access token (PAT) for the current user.',
|
'Delete a personal access token (PAT) for the current user.',
|
||||||
description:
|
description:
|
||||||
'Deletes a [personal access token](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens) (PAT) belonging to the current user.',
|
'Deletes a [personal access token](https://docs.getunleash.io/how-to/how-to-create-personal-access-tokens) (PAT) belonging to the current user.',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
schema: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
description: 'a personal access token id',
|
||||||
|
},
|
||||||
|
],
|
||||||
responses: {
|
responses: {
|
||||||
200: emptyResponse,
|
200: emptyResponse,
|
||||||
...getStandardResponses(401, 403, 404),
|
...getStandardResponses(401, 403, 404),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
idNumberMiddleware(),
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user