From 0b18491237686be627edc52fc953d37e9624371d Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Tue, 4 Jul 2023 10:31:54 +0200 Subject: [PATCH] docs: Auth tag (#4126) ## What This adds openapi documentation for the Auth tagged operations and connected schemas. ## Discussion points Our user schema seems to be exposing quite a bit of internal fields, I flagged the isApi field as deprecated, I can imagine quite a few of these fields also being deprecated to prepare for removal in next major version, but I was unsure which ones were safe to do so with. ## Observation We have some technical debt around the shape of the schema we're claiming we're returning and what we actually are returning. I believe @gastonfournier also observed this when we turned on validation for our endpoints. --------- Co-authored-by: Thomas Heartman --- src/lib/openapi/meta-schema-rules.test.ts | 14 - .../openapi/spec/change-password-schema.ts | 7 + src/lib/openapi/spec/email-schema.ts | 4 + src/lib/openapi/spec/login-schema.ts | 5 + src/lib/openapi/spec/role-schema.ts | 14 +- src/lib/openapi/spec/token-user-schema.ts | 14 +- src/lib/openapi/spec/user-schema.ts | 40 +- .../openapi/spec/validate-password-schema.ts | 4 + .../routes/auth/reset-password-controller.ts | 37 +- .../routes/auth/simple-password-provider.ts | 5 + src/lib/services/user-service.ts | 19 +- .../__snapshots__/openapi.e2e.test.ts.snap | 385 +++++++++++++++++- .../e2e/services/user-service.e2e.test.ts | 7 +- 13 files changed, 518 insertions(+), 37 deletions(-) diff --git a/src/lib/openapi/meta-schema-rules.test.ts b/src/lib/openapi/meta-schema-rules.test.ts index 841311117f..9435b0ffdc 100644 --- a/src/lib/openapi/meta-schema-rules.test.ts +++ b/src/lib/openapi/meta-schema-rules.test.ts @@ -92,13 +92,11 @@ const metaRules: Rule[] = [ knownExceptions: [ 'batchFeaturesSchema', 'batchStaleSchema', - 'changePasswordSchema', 'cloneFeatureSchema', 'createApiTokenSchema', 'createFeatureSchema', 'createInvitedUserSchema', 'createUserSchema', - 'emailSchema', 'environmentsSchema', 'environmentsProjectSchema', 'eventSchema', @@ -118,7 +116,6 @@ const metaRules: Rule[] = [ 'groupsSchema', 'groupUserModelSchema', 'idSchema', - 'loginSchema', 'maintenanceSchema', 'toggleMaintenanceSchema', 'meSchema', @@ -135,7 +132,6 @@ const metaRules: Rule[] = [ 'pushVariantsSchema', 'resetPasswordSchema', 'requestsPerSecondSchema', - 'roleSchema', 'sdkContextSchema', 'searchEventsSchema', 'setUiConfigSchema', @@ -145,7 +141,6 @@ const metaRules: Rule[] = [ 'tagTypeSchema', 'tagTypesSchema', 'tagWithVersionSchema', - 'tokenUserSchema', 'uiConfigSchema', 'updateApiTokenSchema', 'updateFeatureSchema', @@ -154,11 +149,9 @@ const metaRules: Rule[] = [ 'updateUserSchema', 'upsertContextFieldSchema', 'upsertStrategySchema', - 'userSchema', 'usersGroupsBaseSchema', 'usersSchema', 'validateEdgeTokensSchema', - 'validatePasswordSchema', 'validateTagTypeSchema', 'variantFlagSchema', 'versionSchema', @@ -183,7 +176,6 @@ const metaRules: Rule[] = [ 'applicationsSchema', 'batchFeaturesSchema', 'batchStaleSchema', - 'changePasswordSchema', 'cloneFeatureSchema', 'createApiTokenSchema', 'createFeatureSchema', @@ -191,7 +183,6 @@ const metaRules: Rule[] = [ 'createInvitedUserSchema', 'createUserSchema', 'dateSchema', - 'emailSchema', 'environmentsSchema', 'eventSchema', 'eventsSchema', @@ -209,7 +200,6 @@ const metaRules: Rule[] = [ 'groupsSchema', 'groupUserModelSchema', 'idSchema', - 'loginSchema', 'maintenanceSchema', 'toggleMaintenanceSchema', 'meSchema', @@ -229,7 +219,6 @@ const metaRules: Rule[] = [ 'resetPasswordSchema', 'requestsPerSecondSchema', 'requestsPerSecondSegmentedSchema', - 'roleSchema', 'setStrategySortOrderSchema', 'setUiConfigSchema', 'sortOrderSchema', @@ -238,7 +227,6 @@ const metaRules: Rule[] = [ 'tagTypeSchema', 'tagTypesSchema', 'tagWithVersionSchema', - 'tokenUserSchema', 'uiConfigSchema', 'updateApiTokenSchema', 'updateFeatureSchema', @@ -247,12 +235,10 @@ const metaRules: Rule[] = [ 'updateUserSchema', 'upsertContextFieldSchema', 'upsertStrategySchema', - 'userSchema', 'usersGroupsBaseSchema', 'usersSchema', 'usersSearchSchema', 'validateEdgeTokensSchema', - 'validatePasswordSchema', 'validateTagTypeSchema', 'variantFlagSchema', 'variantsSchema', diff --git a/src/lib/openapi/spec/change-password-schema.ts b/src/lib/openapi/spec/change-password-schema.ts index 21dff83916..f6e88899d8 100644 --- a/src/lib/openapi/spec/change-password-schema.ts +++ b/src/lib/openapi/spec/change-password-schema.ts @@ -5,12 +5,19 @@ export const changePasswordSchema = { type: 'object', additionalProperties: false, required: ['token', 'password'], + description: 'Change password as long as the token is a valid token', properties: { token: { + description: + 'A reset token used to validate that the user is allowed to change the password.', type: 'string', + example: + '$2a$15$QzeW/y5/MEppCWVEkoX5euejobYOLSd4We21LQjjKlWH9l2I3wCke', }, password: { type: 'string', + description: 'The new password for the user', + example: 'correct horse battery staple', }, }, components: {}, diff --git a/src/lib/openapi/spec/email-schema.ts b/src/lib/openapi/spec/email-schema.ts index 4c79530619..0f42aec7c0 100644 --- a/src/lib/openapi/spec/email-schema.ts +++ b/src/lib/openapi/spec/email-schema.ts @@ -5,9 +5,13 @@ export const emailSchema = { type: 'object', additionalProperties: false, required: ['email'], + description: + 'Represents the email of a user. Used to send email communication (reset password, welcome mail etc)', properties: { email: { + description: 'The email address', type: 'string', + example: 'test@example.com', }, }, components: {}, diff --git a/src/lib/openapi/spec/login-schema.ts b/src/lib/openapi/spec/login-schema.ts index b632afdeab..1141ea541c 100644 --- a/src/lib/openapi/spec/login-schema.ts +++ b/src/lib/openapi/spec/login-schema.ts @@ -5,12 +5,17 @@ export const loginSchema = { type: 'object', additionalProperties: false, required: ['username', 'password'], + description: 'A username/password login request', properties: { username: { + description: 'The username trying to log in', type: 'string', + example: 'user', }, password: { + description: 'The password of the user trying to log in', type: 'string', + example: 'hunter2', }, }, components: {}, diff --git a/src/lib/openapi/spec/role-schema.ts b/src/lib/openapi/spec/role-schema.ts index 63bb94437b..818723d1c6 100644 --- a/src/lib/openapi/spec/role-schema.ts +++ b/src/lib/openapi/spec/role-schema.ts @@ -3,20 +3,32 @@ import { FromSchema } from 'json-schema-to-ts'; export const roleSchema = { $id: '#/components/schemas/roleSchema', type: 'object', + description: + 'A role holds permissions to allow Unleash to decide what actions a role holder is allowed to perform', additionalProperties: false, required: ['id', 'type', 'name'], properties: { id: { - type: 'number', + type: 'integer', + description: 'The role id', + example: 9, + minimum: 0, }, type: { + description: + 'A role can either be a global role (applies to all projects) or a project role', type: 'string', + example: 'global', }, name: { + description: `The name of the role`, type: 'string', + example: 'Editor', }, description: { + description: `A more detailed description of the role and what use it's intended for`, type: 'string', + example: `Users with the editor role have access to most features in Unleash but can not manage users and roles in the global scope. Editors will be added as project owners when creating projects and get superuser rights within the context of these projects. Users with the editor role will also get access to most permissions on the default project by default.`, }, }, components: {}, diff --git a/src/lib/openapi/spec/token-user-schema.ts b/src/lib/openapi/spec/token-user-schema.ts index 50fa38a122..48cda4d22c 100644 --- a/src/lib/openapi/spec/token-user-schema.ts +++ b/src/lib/openapi/spec/token-user-schema.ts @@ -5,23 +5,35 @@ export const tokenUserSchema = { $id: '#/components/schemas/tokenUserSchema', type: 'object', additionalProperties: false, + description: 'A user identified by a token', required: ['id', 'name', 'email', 'token', 'createdBy', 'role'], properties: { id: { - type: 'number', + type: 'integer', + description: 'The user id', + example: 7, }, name: { + description: 'The name of the user', type: 'string', + example: 'Test McTest', }, email: { + description: 'The email of the user', type: 'string', + example: 'test@example.com', }, token: { + description: 'A token uniquely identifying a user', type: 'string', + example: 'user:xyzrandomstring', }, createdBy: { + description: + 'A username or email identifying which user created this token', type: 'string', nullable: true, + example: 'admin@example.com', }, role: { $ref: '#/components/schemas/roleSchema', diff --git a/src/lib/openapi/spec/user-schema.ts b/src/lib/openapi/spec/user-schema.ts index 7a34b813c1..574091bf1a 100644 --- a/src/lib/openapi/spec/user-schema.ts +++ b/src/lib/openapi/spec/user-schema.ts @@ -5,50 +5,84 @@ export const userSchema = { $id: '#/components/schemas/userSchema', type: 'object', additionalProperties: false, + description: 'An Unleash user', required: ['id'], properties: { id: { - type: 'number', + description: 'The user id', + type: 'integer', + minimum: 0, + example: 123, }, isAPI: { + description: + '(Deprecated): Used internally to know which operations the user should be allowed to perform', type: 'boolean', + example: true, + deprecated: true, }, name: { + description: 'Name of the user', type: 'string', + example: 'User', }, email: { + description: 'Email of the user', type: 'string', + example: 'user@example.com', }, username: { + description: 'A unique username for the user', type: 'string', + example: 'hunter', }, imageUrl: { + description: `URL used for the userprofile image`, type: 'string', + example: 'https://example.com/242x200.png', }, inviteLink: { + description: `If the user is actively inviting other users, this is the link that can be shared with other users`, type: 'string', + example: 'http://localhost:4242/invite-link/some-secret', }, loginAttempts: { - type: 'number', + description: + 'How many unsuccessful attempts at logging in has the user made', + type: 'integer', + minimum: 0, + example: 3, }, emailSent: { + description: 'Is the welcome email sent to the user or not', type: 'boolean', + example: false, }, rootRole: { - type: 'number', + description: + 'Which [root role](https://docs.getunleash.io/reference/rbac#standard-roles) this user is assigned', + type: 'integer', + example: 1, + minimum: 0, }, seenAt: { + description: 'The last time this user logged in', type: 'string', format: 'date-time', nullable: true, + example: '2023-06-30T11:42:00.345Z', }, createdAt: { + description: 'The user was created at this time', type: 'string', format: 'date-time', + example: '2023-06-30T11:41:00.123Z', }, accountType: { + description: 'A user is either an actual User or a Service Account', type: 'string', enum: AccountTypes, + example: 'User', }, }, components: {}, diff --git a/src/lib/openapi/spec/validate-password-schema.ts b/src/lib/openapi/spec/validate-password-schema.ts index bdf7b34aaf..7f12b95f30 100644 --- a/src/lib/openapi/spec/validate-password-schema.ts +++ b/src/lib/openapi/spec/validate-password-schema.ts @@ -5,9 +5,13 @@ export const validatePasswordSchema = { type: 'object', additionalProperties: false, required: ['password'], + description: + 'Used to validate passwords obeying [Unleash password guidelines](https://docs.getunleash.io/reference/deploy/securing-unleash#password-requirements)', properties: { password: { + description: 'The password to validate', type: 'string', + example: 'hunter2', }, }, components: {}, diff --git a/src/lib/routes/auth/reset-password-controller.ts b/src/lib/routes/auth/reset-password-controller.ts index 3465960e12..69e52597e7 100644 --- a/src/lib/routes/auth/reset-password-controller.ts +++ b/src/lib/routes/auth/reset-password-controller.ts @@ -13,7 +13,10 @@ import { TokenUserSchema, } from '../../openapi/spec/token-user-schema'; import { EmailSchema } from '../../openapi/spec/email-schema'; -import { emptyResponse } from '../../openapi/util/standard-responses'; +import { + emptyResponse, + getStandardResponses, +} from '../../openapi/util/standard-responses'; interface IValidateQuery { token: string; @@ -56,9 +59,15 @@ class ResetPasswordController extends Controller { permission: NONE, middleware: [ openApiService.validPath({ + summary: 'Validates a token', + description: + 'If the token is valid returns the user that owns the token', tags: ['Auth'], operationId: 'validateToken', - responses: { 200: createResponseSchema('tokenUserSchema') }, + responses: { + 200: createResponseSchema('tokenUserSchema'), + ...getStandardResponses(401, 415), + }, }), ], }); @@ -70,9 +79,15 @@ class ResetPasswordController extends Controller { middleware: [ openApiService.validPath({ tags: ['Auth'], + summary: `Changes a user password`, + description: + 'Allows users with a valid reset token to reset their password without remembering their old password', operationId: 'changePassword', requestBody: createRequestSchema('changePasswordSchema'), - responses: { 200: emptyResponse }, + responses: { + 200: emptyResponse, + ...getStandardResponses(401, 403, 415), + }, }), ], }); @@ -84,9 +99,15 @@ class ResetPasswordController extends Controller { middleware: [ openApiService.validPath({ tags: ['Auth'], + summary: 'Validates password', + description: + 'Verifies that the password adheres to the [Unleash password guidelines](https://docs.getunleash.io/reference/deploy/securing-unleash#password-requirements)', operationId: 'validatePassword', requestBody: createRequestSchema('validatePasswordSchema'), - responses: { 200: emptyResponse }, + responses: { + 200: emptyResponse, + ...getStandardResponses(400, 415), + }, }), ], }); @@ -98,9 +119,15 @@ class ResetPasswordController extends Controller { middleware: [ openApiService.validPath({ tags: ['Auth'], + summary: 'Reset password', + description: + 'Requests a password reset email for the user. This email can be used to reset the password for a user that has forgotten their password', operationId: 'sendResetPasswordEmail', requestBody: createRequestSchema('emailSchema'), - responses: { 200: emptyResponse }, + responses: { + 200: emptyResponse, + ...getStandardResponses(401, 404, 415), + }, }), ], }); diff --git a/src/lib/routes/auth/simple-password-provider.ts b/src/lib/routes/auth/simple-password-provider.ts index 99628380b2..80dec1a2c9 100644 --- a/src/lib/routes/auth/simple-password-provider.ts +++ b/src/lib/routes/auth/simple-password-provider.ts @@ -12,6 +12,7 @@ import { createResponseSchema } from '../../openapi/util/create-response-schema' import { userSchema, UserSchema } from '../../openapi/spec/user-schema'; import { LoginSchema } from '../../openapi/spec/login-schema'; import { serializeDates } from '../../types/serialize-dates'; +import { getStandardResponses } from '../../openapi'; export class SimplePasswordProvider extends Controller { private logger: Logger; @@ -40,10 +41,14 @@ export class SimplePasswordProvider extends Controller { middleware: [ openApiService.validPath({ tags: ['Auth'], + summary: 'Log in', + description: + 'Logs in the user and creates an active session', operationId: 'login', requestBody: createRequestSchema('loginSchema'), responses: { 200: createResponseSchema('userSchema'), + ...getStandardResponses(401), }, }), ], diff --git a/src/lib/services/user-service.ts b/src/lib/services/user-service.ts index 11a6daeb7e..45833d2c09 100644 --- a/src/lib/services/user-service.ts +++ b/src/lib/services/user-service.ts @@ -295,15 +295,20 @@ class UserService { const idQuery = isEmail(usernameOrEmail) ? { email: usernameOrEmail } : { username: usernameOrEmail }; - const user = await this.store.getByQuery(idQuery); - const passwordHash = await this.store.getPasswordHash(user.id); - const match = await bcrypt.compare(password, passwordHash); - if (match) { - await this.store.successfullyLogin(user); - return user; + let user; + try { + user = await this.store.getByQuery(idQuery); + } catch (error) {} + if (user) { + const passwordHash = await this.store.getPasswordHash(user.id); + + const match = await bcrypt.compare(password, passwordHash); + if (match) { + await this.store.successfullyLogin(user); + return user; + } } - throw new PasswordMismatch( `The combination of password and username you provided is invalid. If you have forgotten your password, visit ${this.baseUriPath}/forgotten-password or get in touch with your instance administrator.`, ); 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 b12ec92513..2ba7a97c3a 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 @@ -1227,11 +1227,16 @@ The provider you choose for your addon dictates what properties the \`parameters }, "changePasswordSchema": { "additionalProperties": false, + "description": "Change password as long as the token is a valid token", "properties": { "password": { + "description": "The new password for the user", + "example": "correct horse battery staple", "type": "string", }, "token": { + "description": "A reset token used to validate that the user is allowed to change the password.", + "example": "$2a$15$QzeW/y5/MEppCWVEkoX5euejobYOLSd4We21LQjjKlWH9l2I3wCke", "type": "string", }, }, @@ -2082,8 +2087,11 @@ The provider you choose for your addon dictates what properties the \`parameters }, "emailSchema": { "additionalProperties": false, + "description": "Represents the email of a user. Used to send email communication (reset password, welcome mail etc)", "properties": { "email": { + "description": "The email address", + "example": "test@example.com", "type": "string", }, }, @@ -3503,11 +3511,16 @@ The provider you choose for your addon dictates what properties the \`parameters }, "loginSchema": { "additionalProperties": false, + "description": "A username/password login request", "properties": { "password": { + "description": "The password of the user trying to log in", + "example": "hunter2", "type": "string", }, "username": { + "description": "The username trying to log in", + "example": "user", "type": "string", }, }, @@ -4829,17 +4842,27 @@ Stats are divided into current and previous **windows**. }, "roleSchema": { "additionalProperties": false, + "description": "A role holds permissions to allow Unleash to decide what actions a role holder is allowed to perform", "properties": { "description": { + "description": "A more detailed description of the role and what use it's intended for", + "example": "Users with the editor role have access to most features in Unleash but can not manage users and roles in the global scope. Editors will be added as project owners when creating projects and get superuser rights within the context of these projects. Users with the editor role will also get access to most permissions on the default project by default.", "type": "string", }, "id": { - "type": "number", + "description": "The role id", + "example": 9, + "minimum": 0, + "type": "integer", }, "name": { + "description": "The name of the role", + "example": "Editor", "type": "string", }, "type": { + "description": "A role can either be a global role (applies to all projects) or a project role", + "example": "global", "type": "string", }, }, @@ -5395,24 +5418,35 @@ Stats are divided into current and previous **windows**. }, "tokenUserSchema": { "additionalProperties": false, + "description": "A user identified by a token", "properties": { "createdBy": { + "description": "A username or email identifying which user created this token", + "example": "admin@example.com", "nullable": true, "type": "string", }, "email": { + "description": "The email of the user", + "example": "test@example.com", "type": "string", }, "id": { - "type": "number", + "description": "The user id", + "example": 7, + "type": "integer", }, "name": { + "description": "The name of the user", + "example": "Test McTest", "type": "string", }, "role": { "$ref": "#/components/schemas/roleSchema", }, "token": { + "description": "A token uniquely identifying a user", + "example": "user:xyzrandomstring", "type": "string", }, }, @@ -5769,47 +5803,78 @@ Stats are divided into current and previous **windows**. }, "userSchema": { "additionalProperties": false, + "description": "An Unleash user", "properties": { "accountType": { + "description": "A user is either an actual User or a Service Account", + "example": "User", "type": "string", }, "createdAt": { + "description": "The user was created at this time", + "example": "2023-06-30T11:41:00.123Z", "format": "date-time", "type": "string", }, "email": { + "description": "Email of the user", + "example": "user@example.com", "type": "string", }, "emailSent": { + "description": "Is the welcome email sent to the user or not", + "example": false, "type": "boolean", }, "id": { - "type": "number", + "description": "The user id", + "example": 123, + "minimum": 0, + "type": "integer", }, "imageUrl": { + "description": "URL used for the userprofile image", + "example": "https://example.com/242x200.png", "type": "string", }, "inviteLink": { + "description": "If the user is actively inviting other users, this is the link that can be shared with other users", + "example": "http://localhost:4242/invite-link/some-secret", "type": "string", }, "isAPI": { + "deprecated": true, + "description": "(Deprecated): Used internally to know which operations the user should be allowed to perform", + "example": true, "type": "boolean", }, "loginAttempts": { - "type": "number", + "description": "How many unsuccessful attempts at logging in has the user made", + "example": 3, + "minimum": 0, + "type": "integer", }, "name": { + "description": "Name of the user", + "example": "User", "type": "string", }, "rootRole": { - "type": "number", + "description": "Which [root role](https://docs.getunleash.io/reference/rbac#standard-roles) this user is assigned", + "example": 1, + "minimum": 0, + "type": "integer", }, "seenAt": { + "description": "The last time this user logged in", + "example": "2023-06-30T11:42:00.345Z", "format": "date-time", "nullable": true, "type": "string", }, "username": { + "description": "A unique username for the user", + "example": "hunter", "type": "string", }, }, @@ -5865,8 +5930,11 @@ Stats are divided into current and previous **windows**. }, "validatePasswordSchema": { "additionalProperties": false, + "description": "Used to validate passwords obeying [Unleash password guidelines](https://docs.getunleash.io/reference/deploy/securing-unleash#password-requirements)", "properties": { "password": { + "description": "The password to validate", + "example": "hunter2", "type": "string", }, }, @@ -14793,6 +14861,7 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9 }, "/auth/reset/password": { "post": { + "description": "Allows users with a valid reset token to reset their password without remembering their old password", "operationId": "changePassword", "requestBody": { "content": { @@ -14809,7 +14878,89 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9 "200": { "description": "This response has no body.", }, + "401": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The ID of the error instance", + "example": "9c40958a-daac-400e-98fb-3bb438567008", + "type": "string", + }, + "message": { + "description": "A description of what went wrong.", + "example": "You must log in to use Unleash. Your request had no authorization header, so we could not authorize you. Try logging in at /auth/simple/login.", + "type": "string", + }, + "name": { + "description": "The name of the error kind", + "example": "AuthenticationRequired", + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "description": "Authorization information is missing or invalid. Provide a valid API token as the \`authorization\` header, e.g. \`authorization:*.*.my-admin-token\`.", + }, + "403": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The ID of the error instance", + "example": "9c40958a-daac-400e-98fb-3bb438567008", + "type": "string", + }, + "message": { + "description": "A description of what went wrong.", + "example": "You need the "UPDATE_ADDON" permission to perform this action in the "development" environment.", + "type": "string", + }, + "name": { + "description": "The name of the error kind", + "example": "NoAccessError", + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "description": "User credentials are valid but does not have enough privileges to execute this operation", + }, + "415": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The ID of the error instance", + "example": "9c40958a-daac-400e-98fb-3bb438567008", + "type": "string", + }, + "message": { + "description": "A description of what went wrong.", + "example": "We do not accept the content-type you provided (application/xml). Try using one of the content-types we do accept instead (application/json) and make sure the body is in the corresponding format.", + "type": "string", + }, + "name": { + "description": "The name of the error kind", + "example": "ContentTypeerror", + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "description": "The operation does not support request payloads of the provided type. Please ensure that you're using one of the listed payload types and that you have specified the right content type in the "content-type" header.", + }, }, + "summary": "Changes a user password", "tags": [ "Auth", ], @@ -14817,6 +14968,7 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9 }, "/auth/reset/password-email": { "post": { + "description": "Requests a password reset email for the user. This email can be used to reset the password for a user that has forgotten their password", "operationId": "sendResetPasswordEmail", "requestBody": { "content": { @@ -14833,7 +14985,89 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9 "200": { "description": "This response has no body.", }, + "401": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The ID of the error instance", + "example": "9c40958a-daac-400e-98fb-3bb438567008", + "type": "string", + }, + "message": { + "description": "A description of what went wrong.", + "example": "You must log in to use Unleash. Your request had no authorization header, so we could not authorize you. Try logging in at /auth/simple/login.", + "type": "string", + }, + "name": { + "description": "The name of the error kind", + "example": "AuthenticationRequired", + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "description": "Authorization information is missing or invalid. Provide a valid API token as the \`authorization\` header, e.g. \`authorization:*.*.my-admin-token\`.", + }, + "404": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The ID of the error instance", + "example": "9c40958a-daac-400e-98fb-3bb438567008", + "type": "string", + }, + "message": { + "description": "A description of what went wrong.", + "example": "Could not find the addon with ID "12345".", + "type": "string", + }, + "name": { + "description": "The name of the error kind", + "example": "NotFoundError", + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "description": "The requested resource was not found.", + }, + "415": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The ID of the error instance", + "example": "9c40958a-daac-400e-98fb-3bb438567008", + "type": "string", + }, + "message": { + "description": "A description of what went wrong.", + "example": "We do not accept the content-type you provided (application/xml). Try using one of the content-types we do accept instead (application/json) and make sure the body is in the corresponding format.", + "type": "string", + }, + "name": { + "description": "The name of the error kind", + "example": "ContentTypeerror", + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "description": "The operation does not support request payloads of the provided type. Please ensure that you're using one of the listed payload types and that you have specified the right content type in the "content-type" header.", + }, }, + "summary": "Reset password", "tags": [ "Auth", ], @@ -14841,6 +15075,7 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9 }, "/auth/reset/validate": { "get": { + "description": "If the token is valid returns the user that owns the token", "operationId": "validateToken", "responses": { "200": { @@ -14853,7 +15088,62 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9 }, "description": "tokenUserSchema", }, + "401": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The ID of the error instance", + "example": "9c40958a-daac-400e-98fb-3bb438567008", + "type": "string", + }, + "message": { + "description": "A description of what went wrong.", + "example": "You must log in to use Unleash. Your request had no authorization header, so we could not authorize you. Try logging in at /auth/simple/login.", + "type": "string", + }, + "name": { + "description": "The name of the error kind", + "example": "AuthenticationRequired", + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "description": "Authorization information is missing or invalid. Provide a valid API token as the \`authorization\` header, e.g. \`authorization:*.*.my-admin-token\`.", + }, + "415": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The ID of the error instance", + "example": "9c40958a-daac-400e-98fb-3bb438567008", + "type": "string", + }, + "message": { + "description": "A description of what went wrong.", + "example": "We do not accept the content-type you provided (application/xml). Try using one of the content-types we do accept instead (application/json) and make sure the body is in the corresponding format.", + "type": "string", + }, + "name": { + "description": "The name of the error kind", + "example": "ContentTypeerror", + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "description": "The operation does not support request payloads of the provided type. Please ensure that you're using one of the listed payload types and that you have specified the right content type in the "content-type" header.", + }, }, + "summary": "Validates a token", "tags": [ "Auth", ], @@ -14861,6 +15151,7 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9 }, "/auth/reset/validate-password": { "post": { + "description": "Verifies that the password adheres to the [Unleash password guidelines](https://docs.getunleash.io/reference/deploy/securing-unleash#password-requirements)", "operationId": "validatePassword", "requestBody": { "content": { @@ -14877,7 +15168,62 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9 "200": { "description": "This response has no body.", }, + "400": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The ID of the error instance", + "example": "9c40958a-daac-400e-98fb-3bb438567008", + "type": "string", + }, + "message": { + "description": "A description of what went wrong.", + "example": "The request payload you provided doesn't conform to the schema. The .parameters property should be object. You sent [].", + "type": "string", + }, + "name": { + "description": "The name of the error kind", + "example": "ValidationError", + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "description": "The request data does not match what we expect.", + }, + "415": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The ID of the error instance", + "example": "9c40958a-daac-400e-98fb-3bb438567008", + "type": "string", + }, + "message": { + "description": "A description of what went wrong.", + "example": "We do not accept the content-type you provided (application/xml). Try using one of the content-types we do accept instead (application/json) and make sure the body is in the corresponding format.", + "type": "string", + }, + "name": { + "description": "The name of the error kind", + "example": "ContentTypeerror", + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "description": "The operation does not support request payloads of the provided type. Please ensure that you're using one of the listed payload types and that you have specified the right content type in the "content-type" header.", + }, }, + "summary": "Validates password", "tags": [ "Auth", ], @@ -14885,6 +15231,7 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9 }, "/auth/simple/login": { "post": { + "description": "Logs in the user and creates an active session", "operationId": "login", "requestBody": { "content": { @@ -14908,7 +15255,35 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9 }, "description": "userSchema", }, + "401": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "The ID of the error instance", + "example": "9c40958a-daac-400e-98fb-3bb438567008", + "type": "string", + }, + "message": { + "description": "A description of what went wrong.", + "example": "You must log in to use Unleash. Your request had no authorization header, so we could not authorize you. Try logging in at /auth/simple/login.", + "type": "string", + }, + "name": { + "description": "The name of the error kind", + "example": "AuthenticationRequired", + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "description": "Authorization information is missing or invalid. Provide a valid API token as the \`authorization\` header, e.g. \`authorization:*.*.my-admin-token\`.", + }, }, + "summary": "Log in", "tags": [ "Auth", ], diff --git a/src/test/e2e/services/user-service.e2e.test.ts b/src/test/e2e/services/user-service.e2e.test.ts index 81a9e35805..bf0d8775d4 100644 --- a/src/test/e2e/services/user-service.e2e.test.ts +++ b/src/test/e2e/services/user-service.e2e.test.ts @@ -16,6 +16,7 @@ import { addDays, minutesToMilliseconds } from 'date-fns'; import { GroupService } from '../../../lib/services/group-service'; import { randomId } from '../../../lib/util/random-id'; import { BadDataError } from '../../../lib/error'; +import PasswordMismatch from '../../../lib/error/password-mismatch'; let db; let stores; @@ -114,7 +115,11 @@ test('should not be able to login with deleted user', async () => { await expect( userService.loginUser('deleted_user', 'unleash4all'), - ).rejects.toThrow(new NotFoundError(`No user found`)); + ).rejects.toThrow( + new PasswordMismatch( + `The combination of password and username you provided is invalid. If you have forgotten your password, visit /forgotten-password or get in touch with your instance administrator.`, + ), + ); }); test('should not login user if simple auth is disabled', async () => {