1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01:00

Task/open api reset password (#1740)

* task: add openapi for reset password

* fix: add respondWithValidation and remove email from tests

* fix: change tags to other
This commit is contained in:
sellinjaanus 2022-06-22 14:31:41 +03:00 committed by GitHub
parent 66452e2860
commit cecca59f65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 556 additions and 12 deletions

View File

@ -56,6 +56,11 @@ import { addonTypeSchema } from './spec/addon-type-schema';
import { applicationSchema } from './spec/application-schema';
import { applicationsSchema } from './spec/applications-schema';
import { tagWithVersionSchema } from './spec/tag-with-version-schema';
import { tokenUserSchema } from './spec/token-user-schema';
import { roleDescriptionSchema } from './spec/role-description-schema';
import { changePasswordSchema } from './spec/change-password-schema';
import { validatePasswordSchema } from './spec/validate-password-schema';
import { resetPasswordSchema } from './spec/reset-password-schema';
import { featureStrategySegmentSchema } from './spec/feature-strategy-segment-schema';
import { segmentSchema } from './spec/segment-schema';
import { stateSchema } from './spec/state-schema';
@ -73,6 +78,7 @@ export const schemas = {
applicationSchema,
applicationsSchema,
cloneFeatureSchema,
changePasswordSchema,
constraintSchema,
contextFieldSchema,
contextFieldsSchema,
@ -104,6 +110,8 @@ export const schemas = {
projectEnvironmentSchema,
projectSchema,
projectsSchema,
resetPasswordSchema,
roleDescriptionSchema,
segmentSchema,
sortOrderSchema,
splashSchema,
@ -114,12 +122,14 @@ export const schemas = {
tagsSchema,
tagTypeSchema,
tagTypesSchema,
tokenUserSchema,
uiConfigSchema,
updateFeatureSchema,
updateStrategySchema,
updateApiTokenSchema,
updateTagTypeSchema,
upsertContextFieldSchema,
validatePasswordSchema,
validateTagTypeSchema,
variantSchema,
variantsSchema,

View File

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`changePasswordSchema empty 1`] = `
Object {
"data": Object {},
"errors": Array [
Object {
"instancePath": "",
"keyword": "required",
"message": "must have required property 'token'",
"params": Object {
"missingProperty": "token",
},
"schemaPath": "#/required",
},
],
"schema": "#/components/schemas/changePasswordSchema",
}
`;

View File

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`resetPasswordSchema empty 1`] = `
Object {
"data": Object {},
"errors": Array [
Object {
"instancePath": "",
"keyword": "required",
"message": "must have required property 'email'",
"params": Object {
"missingProperty": "email",
},
"schemaPath": "#/required",
},
],
"schema": "#/components/schemas/resetPasswordSchema",
}
`;

View File

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`roleDescriptionSchema empty 1`] = `
Object {
"data": Object {},
"errors": Array [
Object {
"instancePath": "",
"keyword": "required",
"message": "must have required property 'description'",
"params": Object {
"missingProperty": "description",
},
"schemaPath": "#/required",
},
],
"schema": "#/components/schemas/roleDescriptionSchema",
}
`;

View File

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`tokenUserSchema empty 1`] = `
Object {
"data": Object {},
"errors": Array [
Object {
"instancePath": "",
"keyword": "required",
"message": "must have required property 'createdBy'",
"params": Object {
"missingProperty": "createdBy",
},
"schemaPath": "#/required",
},
],
"schema": "#/components/schemas/tokenUserSchema",
}
`;

View File

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`validatePasswordSchema empty 1`] = `
Object {
"data": Object {},
"errors": Array [
Object {
"instancePath": "",
"keyword": "required",
"message": "must have required property 'password'",
"params": Object {
"missingProperty": "password",
},
"schemaPath": "#/required",
},
],
"schema": "#/components/schemas/validatePasswordSchema",
}
`;

View File

@ -0,0 +1,19 @@
import { validateSchema } from '../validate';
import { ChangePasswordSchema } from './change-password-schema';
test('changePasswordSchema', () => {
const data: ChangePasswordSchema = {
token: '',
password: '',
};
expect(
validateSchema('#/components/schemas/changePasswordSchema', data),
).toBeUndefined();
});
test('changePasswordSchema empty', () => {
expect(
validateSchema('#/components/schemas/changePasswordSchema', {}),
).toMatchSnapshot();
});

View File

@ -0,0 +1,19 @@
import { FromSchema } from 'json-schema-to-ts';
export const changePasswordSchema = {
$id: '#/components/schemas/changePasswordSchema',
type: 'object',
additionalProperties: false,
required: ['token', 'password'],
properties: {
token: {
type: 'string',
},
password: {
type: 'string',
},
},
components: {},
} as const;
export type ChangePasswordSchema = FromSchema<typeof changePasswordSchema>;

View File

@ -0,0 +1,18 @@
import { validateSchema } from '../validate';
import { ResetPasswordSchema } from './reset-password-schema';
test('resetPasswordSchema', () => {
const data: ResetPasswordSchema = {
email: '',
};
expect(
validateSchema('#/components/schemas/resetPasswordSchema', data),
).toBeUndefined();
});
test('resetPasswordSchema empty', () => {
expect(
validateSchema('#/components/schemas/resetPasswordSchema', {}),
).toMatchSnapshot();
});

View File

@ -0,0 +1,16 @@
import { FromSchema } from 'json-schema-to-ts';
export const resetPasswordSchema = {
$id: '#/components/schemas/resetPasswordSchema',
type: 'object',
additionalProperties: false,
required: ['email'],
properties: {
email: {
type: 'string',
},
},
components: {},
} as const;
export type ResetPasswordSchema = FromSchema<typeof resetPasswordSchema>;

View File

@ -0,0 +1,20 @@
import { validateSchema } from '../validate';
import { RoleDescriptionSchema } from './role-description-schema';
test('roleDescriptionSchema', () => {
const data: RoleDescriptionSchema = {
description: '',
name: '',
type: '',
};
expect(
validateSchema('#/components/schemas/roleDescriptionSchema', data),
).toBeUndefined();
});
test('roleDescriptionSchema empty', () => {
expect(
validateSchema('#/components/schemas/roleDescriptionSchema', {}),
).toMatchSnapshot();
});

View File

@ -0,0 +1,22 @@
import { FromSchema } from 'json-schema-to-ts';
export const roleDescriptionSchema = {
$id: '#/components/schemas/roleDescriptionSchema',
type: 'object',
additionalProperties: false,
required: ['description', 'name', 'type'],
properties: {
description: {
type: 'string',
},
name: {
type: 'string',
},
type: {
type: 'string',
},
},
components: {},
} as const;
export type RoleDescriptionSchema = FromSchema<typeof roleDescriptionSchema>;

View File

@ -0,0 +1,24 @@
import { validateSchema } from '../validate';
import { TokenUserSchema } from './token-user-schema';
test('tokenUserSchema', () => {
const data: TokenUserSchema = {
createdBy: '',
token: '',
role: {
description: '',
name: '',
type: '',
},
};
expect(
validateSchema('#/components/schemas/tokenUserSchema', data),
).toBeUndefined();
});
test('tokenUserSchema empty', () => {
expect(
validateSchema('#/components/schemas/tokenUserSchema', {}),
).toMatchSnapshot();
});

View File

@ -0,0 +1,27 @@
import { FromSchema } from 'json-schema-to-ts';
import { roleDescriptionSchema } from './role-description-schema';
export const tokenUserSchema = {
$id: '#/components/schemas/tokenUserSchema',
type: 'object',
additionalProperties: false,
required: ['createdBy', 'token', 'role'],
properties: {
createdBy: {
type: 'string',
},
token: {
type: 'string',
},
role: {
$ref: '#/components/schemas/roleDescriptionSchema',
},
},
components: {
schemas: {
roleDescriptionSchema,
},
},
} as const;
export type TokenUserSchema = FromSchema<typeof tokenUserSchema>;

View File

@ -0,0 +1,18 @@
import { validateSchema } from '../validate';
import { ValidatePasswordSchema } from './validate-password-schema';
test('validatePasswordSchema', () => {
const data: ValidatePasswordSchema = {
password: '',
};
expect(
validateSchema('#/components/schemas/validatePasswordSchema', data),
).toBeUndefined();
});
test('validatePasswordSchema empty', () => {
expect(
validateSchema('#/components/schemas/validatePasswordSchema', {}),
).toMatchSnapshot();
});

View File

@ -0,0 +1,16 @@
import { FromSchema } from 'json-schema-to-ts';
export const validatePasswordSchema = {
$id: '#/components/schemas/validatePasswordSchema',
type: 'object',
additionalProperties: false,
required: ['password'],
properties: {
password: {
type: 'string',
},
},
components: {},
} as const;
export type ValidatePasswordSchema = FromSchema<typeof validatePasswordSchema>;

View File

@ -3,8 +3,15 @@ import Controller from '../controller';
import UserService from '../../services/user-service';
import { Logger } from '../../logger';
import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services';
import { IUnleashServices } from '../../types';
import { NONE } from '../../types/permissions';
import { createRequestSchema, createResponseSchema } from '../../openapi';
import { emptyResponse } from '../../openapi/spec/empty-response';
import { OpenApiService } from '../../services/openapi-service';
import {
tokenUserSchema,
TokenUserSchema,
} from '../../openapi/spec/token-user-schema';
interface IValidateQuery {
token: string;
@ -23,18 +30,78 @@ interface SessionRequest<PARAMS, QUERY, BODY, K>
class ResetPasswordController extends Controller {
private userService: UserService;
private openApiService: OpenApiService;
private logger: Logger;
constructor(config: IUnleashConfig, { userService }: IUnleashServices) {
constructor(
config: IUnleashConfig,
{
userService,
openApiService,
}: Pick<IUnleashServices, 'userService' | 'openApiService'>,
) {
super(config);
this.logger = config.getLogger(
'lib/routes/auth/reset-password-controller.ts',
);
this.openApiService = openApiService;
this.userService = userService;
this.get('/validate', this.validateToken);
this.post('/password', this.changePassword, NONE);
this.post('/validate-password', this.validatePassword, NONE);
this.post('/password-email', this.sendResetPasswordEmail, NONE);
this.route({
method: 'get',
path: '/validate',
handler: this.validateToken,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['other'],
operationId: 'validateToken',
responses: { 200: createResponseSchema('tokenUserSchema') },
}),
],
});
this.route({
method: 'post',
path: '/password',
handler: this.changePassword,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['other'],
operationId: 'changePassword',
requestBody: createRequestSchema('changePasswordSchema'),
responses: { 200: emptyResponse },
}),
],
});
this.route({
method: 'post',
path: '/validate-password',
handler: this.validatePassword,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['other'],
operationId: 'validatePassword',
requestBody: createRequestSchema('validatePasswordSchema'),
responses: { 200: emptyResponse },
}),
],
});
this.route({
method: 'post',
path: '/password-email',
handler: this.sendResetPasswordEmail,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['other'],
operationId: 'sendResetPasswordEmail',
requestBody: createRequestSchema('resetPasswordSchema'),
responses: { 200: emptyResponse },
}),
],
});
}
async sendResetPasswordEmail(req: Request, res: Response): Promise<void> {
@ -53,12 +120,17 @@ class ResetPasswordController extends Controller {
async validateToken(
req: Request<unknown, unknown, unknown, IValidateQuery>,
res: Response,
res: Response<TokenUserSchema>,
): Promise<void> {
const { token } = req.query;
const user = await this.userService.getUserForToken(token);
await this.logout(req);
res.status(200).json(user);
this.openApiService.respondWithValidation<TokenUserSchema>(
200,
res,
tokenUserSchema.$id,
user,
);
}
async changePassword(

View File

@ -152,7 +152,6 @@ test('Trying to reset password with same token twice does not work', async () =>
await app.request
.post('/auth/reset/password')
.send({
email: user.email,
token,
password,
})
@ -160,7 +159,6 @@ test('Trying to reset password with same token twice does not work', async () =>
await app.request
.post('/auth/reset/password')
.send({
email: user.email,
token,
password,
})
@ -222,7 +220,6 @@ test('Calling reset endpoint with already existing session should logout/destroy
await request
.post('/auth/reset/password')
.send({
email: user.email,
token,
password,
})
@ -259,7 +256,6 @@ test('Trying to change password to undefined should yield 400 without crashing t
await app.request
.post('/auth/reset/password')
.send({
email: user.email,
token,
password: undefined,
})

View File

@ -293,6 +293,22 @@ Object {
},
"type": "object",
},
"changePasswordSchema": Object {
"additionalProperties": false,
"properties": Object {
"password": Object {
"type": "string",
},
"token": Object {
"type": "string",
},
},
"required": Array [
"token",
"password",
],
"type": "object",
},
"cloneFeatureSchema": Object {
"properties": Object {
"name": Object {
@ -1089,6 +1105,38 @@ Object {
],
"type": "object",
},
"resetPasswordSchema": Object {
"additionalProperties": false,
"properties": Object {
"email": Object {
"type": "string",
},
},
"required": Array [
"email",
],
"type": "object",
},
"roleDescriptionSchema": Object {
"additionalProperties": false,
"properties": Object {
"description": Object {
"type": "string",
},
"name": Object {
"type": "string",
},
"type": Object {
"type": "string",
},
},
"required": Array [
"description",
"name",
"type",
],
"type": "object",
},
"segmentSchema": Object {
"additionalProperties": false,
"properties": Object {
@ -1330,6 +1378,26 @@ Object {
],
"type": "object",
},
"tokenUserSchema": Object {
"additionalProperties": false,
"properties": Object {
"createdBy": Object {
"type": "string",
},
"role": Object {
"$ref": "#/components/schemas/roleDescriptionSchema",
},
"token": Object {
"type": "string",
},
},
"required": Array [
"createdBy",
"token",
"role",
],
"type": "object",
},
"uiConfigSchema": Object {
"additionalProperties": false,
"properties": Object {
@ -1508,6 +1576,18 @@ Object {
],
"type": "object",
},
"validatePasswordSchema": Object {
"additionalProperties": false,
"properties": Object {
"password": Object {
"type": "string",
},
},
"required": Array [
"password",
],
"type": "object",
},
"validateTagTypeSchema": Object {
"properties": Object {
"tagType": Object {
@ -4018,6 +4098,98 @@ Object {
],
},
},
"/auth/reset/password": Object {
"post": Object {
"operationId": "changePassword",
"requestBody": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/changePasswordSchema",
},
},
},
"description": "changePasswordSchema",
"required": true,
},
"responses": Object {
"200": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"other",
],
},
},
"/auth/reset/password-email": Object {
"post": Object {
"operationId": "sendResetPasswordEmail",
"requestBody": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/resetPasswordSchema",
},
},
},
"description": "resetPasswordSchema",
"required": true,
},
"responses": Object {
"200": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"other",
],
},
},
"/auth/reset/validate": Object {
"get": Object {
"operationId": "validateToken",
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/tokenUserSchema",
},
},
},
"description": "tokenUserSchema",
},
},
"tags": Array [
"other",
],
},
},
"/auth/reset/validate-password": Object {
"post": Object {
"operationId": "validatePassword",
"requestBody": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/validatePasswordSchema",
},
},
},
"description": "validatePasswordSchema",
"required": true,
},
"responses": Object {
"200": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"other",
],
},
},
"/health": Object {
"get": Object {
"operationId": "getHealth",