diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index 0066627a04..c64cc6be4d 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -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, diff --git a/src/lib/openapi/spec/__snapshots__/change-password-schema.test.ts.snap b/src/lib/openapi/spec/__snapshots__/change-password-schema.test.ts.snap new file mode 100644 index 0000000000..65151ce25e --- /dev/null +++ b/src/lib/openapi/spec/__snapshots__/change-password-schema.test.ts.snap @@ -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", +} +`; diff --git a/src/lib/openapi/spec/__snapshots__/reset-password-schema.test.ts.snap b/src/lib/openapi/spec/__snapshots__/reset-password-schema.test.ts.snap new file mode 100644 index 0000000000..892b501ad4 --- /dev/null +++ b/src/lib/openapi/spec/__snapshots__/reset-password-schema.test.ts.snap @@ -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", +} +`; diff --git a/src/lib/openapi/spec/__snapshots__/role-description-schema.test.ts.snap b/src/lib/openapi/spec/__snapshots__/role-description-schema.test.ts.snap new file mode 100644 index 0000000000..5fbd61e9c6 --- /dev/null +++ b/src/lib/openapi/spec/__snapshots__/role-description-schema.test.ts.snap @@ -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", +} +`; diff --git a/src/lib/openapi/spec/__snapshots__/token-user-schema.test.ts.snap b/src/lib/openapi/spec/__snapshots__/token-user-schema.test.ts.snap new file mode 100644 index 0000000000..1ffcc3b78b --- /dev/null +++ b/src/lib/openapi/spec/__snapshots__/token-user-schema.test.ts.snap @@ -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", +} +`; diff --git a/src/lib/openapi/spec/__snapshots__/validate-password-schema.test.ts.snap b/src/lib/openapi/spec/__snapshots__/validate-password-schema.test.ts.snap new file mode 100644 index 0000000000..a2572cc94b --- /dev/null +++ b/src/lib/openapi/spec/__snapshots__/validate-password-schema.test.ts.snap @@ -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", +} +`; diff --git a/src/lib/openapi/spec/change-password-schema.test.ts b/src/lib/openapi/spec/change-password-schema.test.ts new file mode 100644 index 0000000000..d96ad15937 --- /dev/null +++ b/src/lib/openapi/spec/change-password-schema.test.ts @@ -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(); +}); diff --git a/src/lib/openapi/spec/change-password-schema.ts b/src/lib/openapi/spec/change-password-schema.ts new file mode 100644 index 0000000000..21dff83916 --- /dev/null +++ b/src/lib/openapi/spec/change-password-schema.ts @@ -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; diff --git a/src/lib/openapi/spec/reset-password-schema.test.ts b/src/lib/openapi/spec/reset-password-schema.test.ts new file mode 100644 index 0000000000..fed93e4013 --- /dev/null +++ b/src/lib/openapi/spec/reset-password-schema.test.ts @@ -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(); +}); diff --git a/src/lib/openapi/spec/reset-password-schema.ts b/src/lib/openapi/spec/reset-password-schema.ts new file mode 100644 index 0000000000..cdc5b1d4a7 --- /dev/null +++ b/src/lib/openapi/spec/reset-password-schema.ts @@ -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; diff --git a/src/lib/openapi/spec/role-description-schema.test.ts b/src/lib/openapi/spec/role-description-schema.test.ts new file mode 100644 index 0000000000..61dc118f7c --- /dev/null +++ b/src/lib/openapi/spec/role-description-schema.test.ts @@ -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(); +}); diff --git a/src/lib/openapi/spec/role-description-schema.ts b/src/lib/openapi/spec/role-description-schema.ts new file mode 100644 index 0000000000..7eec103ca6 --- /dev/null +++ b/src/lib/openapi/spec/role-description-schema.ts @@ -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; diff --git a/src/lib/openapi/spec/token-user-schema.test.ts b/src/lib/openapi/spec/token-user-schema.test.ts new file mode 100644 index 0000000000..e63b1aa538 --- /dev/null +++ b/src/lib/openapi/spec/token-user-schema.test.ts @@ -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(); +}); diff --git a/src/lib/openapi/spec/token-user-schema.ts b/src/lib/openapi/spec/token-user-schema.ts new file mode 100644 index 0000000000..769d801492 --- /dev/null +++ b/src/lib/openapi/spec/token-user-schema.ts @@ -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; diff --git a/src/lib/openapi/spec/validate-password-schema.test.ts b/src/lib/openapi/spec/validate-password-schema.test.ts new file mode 100644 index 0000000000..aa3f8c0b25 --- /dev/null +++ b/src/lib/openapi/spec/validate-password-schema.test.ts @@ -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(); +}); diff --git a/src/lib/openapi/spec/validate-password-schema.ts b/src/lib/openapi/spec/validate-password-schema.ts new file mode 100644 index 0000000000..bdf7b34aaf --- /dev/null +++ b/src/lib/openapi/spec/validate-password-schema.ts @@ -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; diff --git a/src/lib/routes/auth/reset-password-controller.ts b/src/lib/routes/auth/reset-password-controller.ts index 263525902f..71d81ba3cd 100644 --- a/src/lib/routes/auth/reset-password-controller.ts +++ b/src/lib/routes/auth/reset-password-controller.ts @@ -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 class ResetPasswordController extends Controller { private userService: UserService; + private openApiService: OpenApiService; + private logger: Logger; - constructor(config: IUnleashConfig, { userService }: IUnleashServices) { + constructor( + config: IUnleashConfig, + { + userService, + openApiService, + }: Pick, + ) { 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 { @@ -53,12 +120,17 @@ class ResetPasswordController extends Controller { async validateToken( req: Request, - res: Response, + res: Response, ): Promise { const { token } = req.query; const user = await this.userService.getUserForToken(token); await this.logout(req); - res.status(200).json(user); + this.openApiService.respondWithValidation( + 200, + res, + tokenUserSchema.$id, + user, + ); } async changePassword( diff --git a/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts b/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts index 57889f3b3c..beab0d675f 100644 --- a/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts +++ b/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts @@ -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, }) diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index 232d1f1de3..a189512997 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -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",