diff --git a/docs/content/dev/public_api.yml b/docs/content/dev/public_api.yml index 17192fc8d..2a081e888 100644 --- a/docs/content/dev/public_api.yml +++ b/docs/content/dev/public_api.yml @@ -828,11 +828,6 @@ components: password: type: string format: password - OpenIdLogin: - type: object - properties: - openId: - type: string ServerStatus: type: object properties: diff --git a/frontend/cypress/e2e/deleteNote.spec.ts b/frontend/cypress/e2e/deleteNote.spec.ts index a32b654e5..bc81a5137 100644 --- a/frontend/cypress/e2e/deleteNote.spec.ts +++ b/frontend/cypress/e2e/deleteNote.spec.ts @@ -23,6 +23,9 @@ describe('Delete note', () => { }) it('displays an error notification if something goes wrong', () => { + cy.intercept('DELETE', `api/private/notes/${testNoteId}`, { + statusCode: 400 + }) cy.getByCypressId('sidebar.deleteNote.button').click() cy.getByCypressId('sidebar.deleteNote.modal').should('be.visible') cy.getByCypressId('sidebar.deleteNote.modal.noteTitle').should('be.visible').text().should('eq', '') diff --git a/frontend/locales/de.json b/frontend/locales/de.json index ca905ee61..cc0e0e162 100644 --- a/frontend/locales/de.json +++ b/frontend/locales/de.json @@ -192,7 +192,6 @@ "password": "Passwort", "username": "Benutzername", "error": { - "openIdLogin": "OpenID nicht korrekt", "usernamePassword": "Benutzername oder Passwort nicht korrekt" } } diff --git a/frontend/locales/en.json b/frontend/locales/en.json index cc00edf7b..311f15797 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -545,7 +545,6 @@ "password": "Password", "username": "Username", "error": { - "openIdLogin": "Invalid OpenID provided", "usernamePassword": "Invalid username or password", "loginDisabled": "The login is disabled", "other": "There was an error logging you in." diff --git a/frontend/src/api/alias/index.ts b/frontend/src/api/alias/index.ts index 1662a94af..efe8733df 100644 --- a/frontend/src/api/alias/index.ts +++ b/frontend/src/api/alias/index.ts @@ -17,7 +17,7 @@ import type { Alias, NewAliasDto, PrimaryAliasDto } from './types' * @throws {Error} when the api request wasn't successfull */ export const addAlias = async (noteIdOrAlias: string, newAlias: string): Promise => { - const response = await new PostApiRequestBuilder('alias') + const response = await new PostApiRequestBuilder('alias', 'alias') .withJsonBody({ noteIdOrAlias, newAlias @@ -35,7 +35,7 @@ export const addAlias = async (noteIdOrAlias: string, newAlias: string): Promise * @throws {Error} when the api request wasn't successfull */ export const markAliasAsPrimary = async (alias: string): Promise => { - const response = await new PutApiRequestBuilder('alias/' + alias) + const response = await new PutApiRequestBuilder('alias/' + alias, 'alias') .withJsonBody({ primaryAlias: true }) @@ -50,5 +50,5 @@ export const markAliasAsPrimary = async (alias: string): Promise => { * @throws {Error} when the api request wasn't successful. */ export const deleteAlias = async (alias: string): Promise => { - await new DeleteApiRequestBuilder('alias/' + alias).sendRequest() + await new DeleteApiRequestBuilder('alias/' + alias, 'alias').sendRequest() } diff --git a/frontend/src/api/auth/index.ts b/frontend/src/api/auth/index.ts index b8a4f74ae..b17d9bd18 100644 --- a/frontend/src/api/auth/index.ts +++ b/frontend/src/api/auth/index.ts @@ -11,5 +11,5 @@ import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-ap * @throws {Error} if logout is not possible. */ export const doLogout = async (): Promise => { - await new DeleteApiRequestBuilder('auth/logout').sendRequest() + await new DeleteApiRequestBuilder('auth/logout', 'auth').sendRequest() } diff --git a/frontend/src/api/auth/ldap.ts b/frontend/src/api/auth/ldap.ts index ea1250922..953839022 100644 --- a/frontend/src/api/auth/ldap.ts +++ b/frontend/src/api/auth/ldap.ts @@ -5,7 +5,6 @@ */ import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder' import type { LoginDto } from './types' -import { AuthError } from './types' /** * Requests to log in a user via LDAP credentials. @@ -13,17 +12,13 @@ import { AuthError } from './types' * @param provider The identifier of the LDAP provider with which to login. * @param username The username with which to try the login. * @param password The password of the user. - * @throws {AuthError.INVALID_CREDENTIALS} if the LDAP provider denied the given credentials. * @throws {Error} when the api request wasn't successfull */ export const doLdapLogin = async (provider: string, username: string, password: string): Promise => { - await new PostApiRequestBuilder('auth/ldap/' + provider) + await new PostApiRequestBuilder('auth/ldap/' + provider, 'auth') .withJsonBody({ username: username, password: password }) - .withStatusCodeErrorMapping({ - 401: AuthError.INVALID_CREDENTIALS - }) .sendRequest() } diff --git a/frontend/src/api/auth/local.ts b/frontend/src/api/auth/local.ts index c2af57f58..d1727b8b2 100644 --- a/frontend/src/api/auth/local.ts +++ b/frontend/src/api/auth/local.ts @@ -6,7 +6,6 @@ import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder' import { PutApiRequestBuilder } from '../common/api-request-builder/put-api-request-builder' import type { ChangePasswordDto, LoginDto, RegisterDto } from './types' -import { AuthError, RegisterError } from './types' /** * Requests to do a local login with a provided username and password. @@ -18,15 +17,11 @@ import { AuthError, RegisterError } from './types' * @throws {Error} when the api request wasn't successful. */ export const doLocalLogin = async (username: string, password: string): Promise => { - await new PostApiRequestBuilder('auth/local/login') + await new PostApiRequestBuilder('auth/local/login', 'auth') .withJsonBody({ username, password }) - .withStatusCodeErrorMapping({ - 400: AuthError.LOGIN_DISABLED, - 401: AuthError.INVALID_CREDENTIALS - }) .sendRequest() } @@ -42,17 +37,12 @@ export const doLocalLogin = async (username: string, password: string): Promise< * @throws {Error} when the api request wasn't successful. */ export const doLocalRegister = async (username: string, displayName: string, password: string): Promise => { - await new PostApiRequestBuilder('auth/local') + await new PostApiRequestBuilder('auth/local', 'auth') .withJsonBody({ username, displayName, password }) - .withStatusCodeErrorMapping({ - 400: RegisterError.PASSWORD_TOO_WEAK, - 403: RegisterError.REGISTRATION_DISABLED, - 409: RegisterError.USERNAME_EXISTING - }) .sendRequest() } @@ -64,14 +54,10 @@ export const doLocalRegister = async (username: string, displayName: string, pas * @throws {AuthError.LOGIN_DISABLED} when local login is disabled on the backend. */ export const doLocalPasswordChange = async (currentPassword: string, newPassword: string): Promise => { - await new PutApiRequestBuilder('auth/local') + await new PutApiRequestBuilder('auth/local', 'auth') .withJsonBody({ currentPassword, newPassword }) - .withStatusCodeErrorMapping({ - 400: AuthError.LOGIN_DISABLED, - 401: AuthError.INVALID_CREDENTIALS - }) .sendRequest() } diff --git a/frontend/src/api/auth/types.ts b/frontend/src/api/auth/types.ts index b7c549ccc..ca0820cbc 100644 --- a/frontend/src/api/auth/types.ts +++ b/frontend/src/api/auth/types.ts @@ -3,19 +3,6 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -export enum AuthError { - INVALID_CREDENTIALS = 'invalidCredentials', - LOGIN_DISABLED = 'loginDisabled', - OPENID_ERROR = 'openIdError', - OTHER = 'other' -} - -export enum RegisterError { - USERNAME_EXISTING = 'usernameExisting', - PASSWORD_TOO_WEAK = 'passwordTooWeak', - REGISTRATION_DISABLED = 'registrationDisabled', - OTHER = 'other' -} export interface LoginDto { username: string diff --git a/frontend/src/api/common/api-error-response.ts b/frontend/src/api/common/api-error-response.ts new file mode 100644 index 000000000..452ad043e --- /dev/null +++ b/frontend/src/api/common/api-error-response.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export interface ApiErrorResponse { + message: string + error: string +} diff --git a/frontend/src/api/common/api-error.ts b/frontend/src/api/common/api-error.ts new file mode 100644 index 000000000..2a3270b47 --- /dev/null +++ b/frontend/src/api/common/api-error.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ApiError extends Error { + constructor( + public readonly statusCode: number, + statusText: string, + i18nNamespace: string, + public readonly apiErrorName: string | undefined + ) { + super(`api.error.${i18nNamespace}.${statusText}`) + } +} diff --git a/frontend/src/api/common/api-request-builder/api-request-builder.ts b/frontend/src/api/common/api-request-builder/api-request-builder.ts index 0e7975b4f..208e43467 100644 --- a/frontend/src/api/common/api-request-builder/api-request-builder.ts +++ b/frontend/src/api/common/api-request-builder/api-request-builder.ts @@ -3,6 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { ApiError } from '../api-error' +import type { ApiErrorResponse } from '../api-error-response' import { ApiResponse } from '../api-response' import { defaultConfig, defaultHeaders } from '../default-config' import deepmerge from 'deepmerge' @@ -14,10 +16,8 @@ import deepmerge from 'deepmerge' */ export abstract class ApiRequestBuilder { private readonly targetUrl: string - private overrideExpectedResponseStatus: number | undefined private customRequestOptions = defaultConfig private customRequestHeaders = new Headers(defaultHeaders) - private customStatusCodeErrorMapping: Record | undefined protected requestBody: BodyInit | undefined /** @@ -25,14 +25,11 @@ export abstract class ApiRequestBuilder { * * @param endpoint The target endpoint without a leading slash. */ - constructor(endpoint: string) { + constructor(endpoint: string, private apiI18nKey: string) { this.targetUrl = `api/private/${endpoint}` } - protected async sendRequestAndVerifyResponse( - httpMethod: RequestInit['method'], - defaultExpectedStatus: number - ): Promise> { + protected async sendRequestAndVerifyResponse(httpMethod: RequestInit['method']): Promise> { const response = await fetch(this.targetUrl, { ...this.customRequestOptions, method: httpMethod, @@ -40,20 +37,19 @@ export abstract class ApiRequestBuilder { body: this.requestBody }) - if (this.customStatusCodeErrorMapping && this.customStatusCodeErrorMapping[response.status]) { - throw new Error(this.customStatusCodeErrorMapping[response.status]) - } - - const expectedStatus = this.overrideExpectedResponseStatus - ? this.overrideExpectedResponseStatus - : defaultExpectedStatus - if (response.status !== expectedStatus) { - throw new Error(`Expected response status code ${expectedStatus} but received ${response.status}.`) + if (response.status >= 400) { + const apiErrorResponse = await this.readApiErrorResponseFromBody(response) + const statusText = response.status === 400 ? apiErrorResponse?.error ?? 'unknown' : response.statusText + throw new ApiError(response.status, statusText, this.apiI18nKey, apiErrorResponse?.error) } return new ApiResponse(response) } + private async readApiErrorResponseFromBody(response: Response): Promise { + return response.json().catch(() => undefined) as Promise + } + /** * Adds an HTTP header to the API request. Previous headers with the same name will get overridden on subsequent calls * with the same name. @@ -78,30 +74,6 @@ export abstract class ApiRequestBuilder { return this } - /** - * Adds a mapping from response status codes to error messages. An error with the specified message will be thrown - * when the status code of the response matches one of the defined ones. - * - * @param mapping The mapping from response status codes to error messages. - * @return The API request instance itself for chaining. - */ - withStatusCodeErrorMapping(mapping: Record): this { - this.customStatusCodeErrorMapping = mapping - return this - } - - /** - * Sets the expected status code of the response. Can be used to override the default expected status code. - * An error will be thrown when the status code of the response does not match the expected one. - * - * @param expectedCode The expected status code of the response. - * @return The API request instance itself for chaining. - */ - withExpectedStatusCode(expectedCode: number): this { - this.overrideExpectedResponseStatus = expectedCode - return this - } - /** * Send the prepared API call as a GET request. A default status code of 200 is expected. * diff --git a/frontend/src/api/common/api-request-builder/delete-api-request-builder.test.ts b/frontend/src/api/common/api-request-builder/delete-api-request-builder.test.ts index 74be815ef..eef433288 100644 --- a/frontend/src/api/common/api-request-builder/delete-api-request-builder.test.ts +++ b/frontend/src/api/common/api-request-builder/delete-api-request-builder.test.ts @@ -3,6 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { ApiError } from '../api-error' +import type { ApiErrorResponse } from '../api-error-response' import { DeleteApiRequestBuilder } from './delete-api-request-builder' import { expectFetch } from './test-utils/expect-fetch' @@ -19,7 +21,7 @@ describe('DeleteApiRequestBuilder', () => { describe('sendRequest without body', () => { it('without headers', async () => { expectFetch('api/private/test', 204, { method: 'DELETE' }) - await new DeleteApiRequestBuilder('test').sendRequest() + await new DeleteApiRequestBuilder('test', 'test').sendRequest() }) it('with single header', async () => { @@ -29,7 +31,7 @@ describe('DeleteApiRequestBuilder', () => { method: 'DELETE', headers: expectedHeaders }) - await new DeleteApiRequestBuilder('test').withHeader('test', 'true').sendRequest() + await new DeleteApiRequestBuilder('test', 'test').withHeader('test', 'true').sendRequest() }) it('with overriding single header', async () => { @@ -39,7 +41,7 @@ describe('DeleteApiRequestBuilder', () => { method: 'DELETE', headers: expectedHeaders }) - await new DeleteApiRequestBuilder('test') + await new DeleteApiRequestBuilder('test', 'test') .withHeader('test', 'true') .withHeader('test', 'false') .sendRequest() @@ -53,7 +55,7 @@ describe('DeleteApiRequestBuilder', () => { method: 'DELETE', headers: expectedHeaders }) - await new DeleteApiRequestBuilder('test') + await new DeleteApiRequestBuilder('test', 'test') .withHeader('test', 'true') .withHeader('test2', 'false') .sendRequest() @@ -69,7 +71,7 @@ describe('DeleteApiRequestBuilder', () => { headers: expectedHeaders, body: '{"test":true,"foo":"bar"}' }) - await new DeleteApiRequestBuilder('test') + await new DeleteApiRequestBuilder('test', 'test') .withJsonBody({ test: true, foo: 'bar' @@ -82,12 +84,7 @@ describe('DeleteApiRequestBuilder', () => { method: 'DELETE', body: 'HedgeDoc' }) - await new DeleteApiRequestBuilder('test').withBody('HedgeDoc').sendRequest() - }) - - it('sendRequest with expected status code', async () => { - expectFetch('api/private/test', 200, { method: 'DELETE' }) - await new DeleteApiRequestBuilder('test').withExpectedStatusCode(200).sendRequest() + await new DeleteApiRequestBuilder('test', 'test').withBody('HedgeDoc').sendRequest() }) describe('sendRequest with custom options', () => { @@ -96,7 +93,7 @@ describe('DeleteApiRequestBuilder', () => { method: 'DELETE', cache: 'force-cache' }) - await new DeleteApiRequestBuilder('test') + await new DeleteApiRequestBuilder('test', 'test') .withCustomOptions({ cache: 'force-cache' }) @@ -108,7 +105,7 @@ describe('DeleteApiRequestBuilder', () => { method: 'DELETE', cache: 'no-store' }) - await new DeleteApiRequestBuilder('test') + await new DeleteApiRequestBuilder('test', 'test') .withCustomOptions({ cache: 'force-cache' }) @@ -124,7 +121,7 @@ describe('DeleteApiRequestBuilder', () => { cache: 'force-cache', integrity: 'test' }) - await new DeleteApiRequestBuilder('test') + await new DeleteApiRequestBuilder('test', 'test') .withCustomOptions({ cache: 'force-cache', integrity: 'test' @@ -133,37 +130,29 @@ describe('DeleteApiRequestBuilder', () => { }) }) - describe('sendRequest with custom error map', () => { - it('for valid status code', async () => { - expectFetch('api/private/test', 204, { method: 'DELETE' }) - await new DeleteApiRequestBuilder('test') - .withStatusCodeErrorMapping({ - 400: 'noooooo', - 401: 'not you!' - }) - .sendRequest() - }) - - it('for invalid status code 1', async () => { + describe('failing sendRequest', () => { + it('with bad request without api error name', async () => { expectFetch('api/private/test', 400, { method: 'DELETE' }) - const request = new DeleteApiRequestBuilder('test') - .withStatusCodeErrorMapping({ - 400: 'noooooo', - 401: 'not you!' - }) - .sendRequest() - await expect(request).rejects.toThrow('noooooo') + const request = new DeleteApiRequestBuilder('test', 'test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, 'unknown', 'test', 'testExplosion')) }) - it('for invalid status code 2', async () => { - expectFetch('api/private/test', 401, { method: 'DELETE' }) - const request = new DeleteApiRequestBuilder('test') - .withStatusCodeErrorMapping({ - 400: 'noooooo', - 401: 'not you!' - }) - .sendRequest() - await expect(request).rejects.toThrow('not you!') + it('with bad request with api error name', async () => { + expectFetch('api/private/test', 400, { method: 'DELETE' }, { + message: 'The API has exploded!', + error: 'testExplosion' + } as ApiErrorResponse) + const request = new DeleteApiRequestBuilder('test', 'test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'test', 'testExplosion')) + }) + + it('with non bad request error', async () => { + expectFetch('api/private/test', 401, { method: 'DELETE' }, { + message: 'The API has exploded!', + error: 'testExplosion' + } as ApiErrorResponse) + const request = new DeleteApiRequestBuilder('test', 'test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(401, 'forbidden', 'test', 'testExplosion')) }) }) }) diff --git a/frontend/src/api/common/api-request-builder/delete-api-request-builder.ts b/frontend/src/api/common/api-request-builder/delete-api-request-builder.ts index b1c37247b..aaf5394f5 100644 --- a/frontend/src/api/common/api-request-builder/delete-api-request-builder.ts +++ b/frontend/src/api/common/api-request-builder/delete-api-request-builder.ts @@ -21,6 +21,6 @@ export class DeleteApiRequestBuilder> { - return this.sendRequestAndVerifyResponse('DELETE', 204) + return this.sendRequestAndVerifyResponse('DELETE') } } diff --git a/frontend/src/api/common/api-request-builder/get-api-request-builder.test.ts b/frontend/src/api/common/api-request-builder/get-api-request-builder.test.ts index 201e3319a..e807109a1 100644 --- a/frontend/src/api/common/api-request-builder/get-api-request-builder.test.ts +++ b/frontend/src/api/common/api-request-builder/get-api-request-builder.test.ts @@ -3,6 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { ApiError } from '../api-error' +import type { ApiErrorResponse } from '../api-error-response' import { GetApiRequestBuilder } from './get-api-request-builder' import { expectFetch } from './test-utils/expect-fetch' @@ -20,7 +22,7 @@ describe('GetApiRequestBuilder', () => { describe('sendRequest', () => { it('without headers', async () => { expectFetch('api/private/test', 200, { method: 'GET' }) - await new GetApiRequestBuilder('test').sendRequest() + await new GetApiRequestBuilder('test', 'test').sendRequest() }) it('with single header', async () => { @@ -30,7 +32,7 @@ describe('GetApiRequestBuilder', () => { method: 'GET', headers: expectedHeaders }) - await new GetApiRequestBuilder('test').withHeader('test', 'true').sendRequest() + await new GetApiRequestBuilder('test', 'test').withHeader('test', 'true').sendRequest() }) it('with overriding single header', async () => { @@ -40,7 +42,7 @@ describe('GetApiRequestBuilder', () => { method: 'GET', headers: expectedHeaders }) - await new GetApiRequestBuilder('test') + await new GetApiRequestBuilder('test', 'test') .withHeader('test', 'true') .withHeader('test', 'false') .sendRequest() @@ -54,25 +56,20 @@ describe('GetApiRequestBuilder', () => { method: 'GET', headers: expectedHeaders }) - await new GetApiRequestBuilder('test') + await new GetApiRequestBuilder('test', 'test') .withHeader('test', 'true') .withHeader('test2', 'false') .sendRequest() }) }) - it('sendRequest with expected status code', async () => { - expectFetch('api/private/test', 200, { method: 'GET' }) - await new GetApiRequestBuilder('test').withExpectedStatusCode(200).sendRequest() - }) - describe('sendRequest with custom options', () => { it('with one option', async () => { expectFetch('api/private/test', 200, { method: 'GET', cache: 'force-cache' }) - await new GetApiRequestBuilder('test') + await new GetApiRequestBuilder('test', 'test') .withCustomOptions({ cache: 'force-cache' }) @@ -84,7 +81,7 @@ describe('GetApiRequestBuilder', () => { method: 'GET', cache: 'no-store' }) - await new GetApiRequestBuilder('test') + await new GetApiRequestBuilder('test', 'test') .withCustomOptions({ cache: 'force-cache' }) @@ -100,7 +97,7 @@ describe('GetApiRequestBuilder', () => { cache: 'force-cache', integrity: 'test' }) - await new GetApiRequestBuilder('test') + await new GetApiRequestBuilder('test', 'test') .withCustomOptions({ cache: 'force-cache', integrity: 'test' @@ -109,37 +106,29 @@ describe('GetApiRequestBuilder', () => { }) }) - describe('sendRequest with custom error map', () => { - it('for valid status code', async () => { - expectFetch('api/private/test', 200, { method: 'GET' }) - await new GetApiRequestBuilder('test') - .withStatusCodeErrorMapping({ - 400: 'noooooo', - 401: 'not you!' - }) - .sendRequest() - }) - - it('for invalid status code 1', async () => { + describe('failing sendRequest', () => { + it('with bad request without api error name', async () => { expectFetch('api/private/test', 400, { method: 'GET' }) - const request = new GetApiRequestBuilder('test') - .withStatusCodeErrorMapping({ - 400: 'noooooo', - 401: 'not you!' - }) - .sendRequest() - await expect(request).rejects.toThrow('noooooo') + const request = new GetApiRequestBuilder('test', 'test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, 'unknown', 'test', 'testExplosion')) }) - it('for invalid status code 2', async () => { - expectFetch('api/private/test', 401, { method: 'GET' }) - const request = new GetApiRequestBuilder('test') - .withStatusCodeErrorMapping({ - 400: 'noooooo', - 401: 'not you!' - }) - .sendRequest() - await expect(request).rejects.toThrow('not you!') + it('with bad request with api error name', async () => { + expectFetch('api/private/test', 400, { method: 'GET' }, { + message: 'The API has exploded!', + error: 'testExplosion' + } as ApiErrorResponse) + const request = new GetApiRequestBuilder('test', 'test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'test', 'testExplosion')) + }) + + it('with non bad request error', async () => { + expectFetch('api/private/test', 401, { method: 'GET' }, { + message: 'The API has exploded!', + error: 'testExplosion' + } as ApiErrorResponse) + const request = new GetApiRequestBuilder('test', 'test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(401, 'forbidden', 'test', 'testExplosion')) }) }) }) diff --git a/frontend/src/api/common/api-request-builder/get-api-request-builder.ts b/frontend/src/api/common/api-request-builder/get-api-request-builder.ts index fe7e966ea..bc4c16dcc 100644 --- a/frontend/src/api/common/api-request-builder/get-api-request-builder.ts +++ b/frontend/src/api/common/api-request-builder/get-api-request-builder.ts @@ -17,6 +17,6 @@ export class GetApiRequestBuilder extends ApiRequestBuilder> { - return this.sendRequestAndVerifyResponse('GET', 200) + return this.sendRequestAndVerifyResponse('GET') } } diff --git a/frontend/src/api/common/api-request-builder/post-api-request-builder.test.ts b/frontend/src/api/common/api-request-builder/post-api-request-builder.test.ts index b58fb794b..055428aec 100644 --- a/frontend/src/api/common/api-request-builder/post-api-request-builder.test.ts +++ b/frontend/src/api/common/api-request-builder/post-api-request-builder.test.ts @@ -3,6 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { ApiError } from '../api-error' +import type { ApiErrorResponse } from '../api-error-response' import { PostApiRequestBuilder } from './post-api-request-builder' import { expectFetch } from './test-utils/expect-fetch' @@ -20,7 +22,7 @@ describe('PostApiRequestBuilder', () => { describe('sendRequest without body', () => { it('without headers', async () => { expectFetch('api/private/test', 201, { method: 'POST' }) - await new PostApiRequestBuilder('test').sendRequest() + await new PostApiRequestBuilder('test', 'test').sendRequest() }) it('with single header', async () => { @@ -30,7 +32,7 @@ describe('PostApiRequestBuilder', () => { method: 'POST', headers: expectedHeaders }) - await new PostApiRequestBuilder('test').withHeader('test', 'true').sendRequest() + await new PostApiRequestBuilder('test', 'test').withHeader('test', 'true').sendRequest() }) it('with overriding single header', async () => { @@ -40,7 +42,7 @@ describe('PostApiRequestBuilder', () => { method: 'POST', headers: expectedHeaders }) - await new PostApiRequestBuilder('test') + await new PostApiRequestBuilder('test', 'test') .withHeader('test', 'true') .withHeader('test', 'false') .sendRequest() @@ -54,7 +56,7 @@ describe('PostApiRequestBuilder', () => { method: 'POST', headers: expectedHeaders }) - await new PostApiRequestBuilder('test') + await new PostApiRequestBuilder('test', 'test') .withHeader('test', 'true') .withHeader('test2', 'false') .sendRequest() @@ -70,7 +72,7 @@ describe('PostApiRequestBuilder', () => { headers: expectedHeaders, body: '{"test":true,"foo":"bar"}' }) - await new PostApiRequestBuilder('test') + await new PostApiRequestBuilder('test', 'test') .withJsonBody({ test: true, foo: 'bar' @@ -83,12 +85,7 @@ describe('PostApiRequestBuilder', () => { method: 'POST', body: 'HedgeDoc' }) - await new PostApiRequestBuilder('test').withBody('HedgeDoc').sendRequest() - }) - - it('sendRequest with expected status code', async () => { - expectFetch('api/private/test', 200, { method: 'POST' }) - await new PostApiRequestBuilder('test').withExpectedStatusCode(200).sendRequest() + await new PostApiRequestBuilder('test', 'test').withBody('HedgeDoc').sendRequest() }) describe('sendRequest with custom options', () => { @@ -97,7 +94,7 @@ describe('PostApiRequestBuilder', () => { method: 'POST', cache: 'force-cache' }) - await new PostApiRequestBuilder('test') + await new PostApiRequestBuilder('test', 'test') .withCustomOptions({ cache: 'force-cache' }) @@ -109,7 +106,7 @@ describe('PostApiRequestBuilder', () => { method: 'POST', cache: 'no-store' }) - await new PostApiRequestBuilder('test') + await new PostApiRequestBuilder('test', 'test') .withCustomOptions({ cache: 'force-cache' }) @@ -125,7 +122,7 @@ describe('PostApiRequestBuilder', () => { cache: 'force-cache', integrity: 'test' }) - await new PostApiRequestBuilder('test') + await new PostApiRequestBuilder('test', 'test') .withCustomOptions({ cache: 'force-cache', integrity: 'test' @@ -134,37 +131,29 @@ describe('PostApiRequestBuilder', () => { }) }) - describe('sendRequest with custom error map', () => { - it('for valid status code', async () => { - expectFetch('api/private/test', 201, { method: 'POST' }) - await new PostApiRequestBuilder('test') - .withStatusCodeErrorMapping({ - 400: 'noooooo', - 401: 'not you!' - }) - .sendRequest() - }) - - it('for invalid status code 1', async () => { + describe('failing sendRequest', () => { + it('with bad request without api error name', async () => { expectFetch('api/private/test', 400, { method: 'POST' }) - const request = new PostApiRequestBuilder('test') - .withStatusCodeErrorMapping({ - 400: 'noooooo', - 401: 'not you!' - }) - .sendRequest() - await expect(request).rejects.toThrow('noooooo') + const request = new PostApiRequestBuilder('test', 'test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, 'unknown', 'test', 'testExplosion')) }) - it('for invalid status code 2', async () => { - expectFetch('api/private/test', 401, { method: 'POST' }) - const request = new PostApiRequestBuilder('test') - .withStatusCodeErrorMapping({ - 400: 'noooooo', - 401: 'not you!' - }) - .sendRequest() - await expect(request).rejects.toThrow('not you!') + it('with bad request with api error name', async () => { + expectFetch('api/private/test', 400, { method: 'POST' }, { + message: 'The API has exploded!', + error: 'testExplosion' + } as ApiErrorResponse) + const request = new PostApiRequestBuilder('test', 'test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'test', 'testExplosion')) + }) + + it('with non bad request error', async () => { + expectFetch('api/private/test', 401, { method: 'POST' }, { + message: 'The API has exploded!', + error: 'testExplosion' + } as ApiErrorResponse) + const request = new PostApiRequestBuilder('test', 'test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(401, 'forbidden', 'test', 'testExplosion')) }) }) }) diff --git a/frontend/src/api/common/api-request-builder/post-api-request-builder.ts b/frontend/src/api/common/api-request-builder/post-api-request-builder.ts index 8c7b6b16d..4f4cf829f 100644 --- a/frontend/src/api/common/api-request-builder/post-api-request-builder.ts +++ b/frontend/src/api/common/api-request-builder/post-api-request-builder.ts @@ -21,6 +21,6 @@ export class PostApiRequestBuilder extends ApiReq * @see ApiRequestBuilder#sendRequest */ sendRequest(): Promise> { - return this.sendRequestAndVerifyResponse('POST', 201) + return this.sendRequestAndVerifyResponse('POST') } } diff --git a/frontend/src/api/common/api-request-builder/put-api-request-builder.test.ts b/frontend/src/api/common/api-request-builder/put-api-request-builder.test.ts index d09ade111..0ef9d55b8 100644 --- a/frontend/src/api/common/api-request-builder/put-api-request-builder.test.ts +++ b/frontend/src/api/common/api-request-builder/put-api-request-builder.test.ts @@ -3,6 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { ApiError } from '../api-error' +import type { ApiErrorResponse } from '../api-error-response' import { PutApiRequestBuilder } from './put-api-request-builder' import { expectFetch } from './test-utils/expect-fetch' @@ -20,7 +22,7 @@ describe('PutApiRequestBuilder', () => { describe('sendRequest without body', () => { it('without headers', async () => { expectFetch('api/private/test', 200, { method: 'PUT' }) - await new PutApiRequestBuilder('test').sendRequest() + await new PutApiRequestBuilder('test', 'test').sendRequest() }) it('with single header', async () => { @@ -30,7 +32,7 @@ describe('PutApiRequestBuilder', () => { method: 'PUT', headers: expectedHeaders }) - await new PutApiRequestBuilder('test').withHeader('test', 'true').sendRequest() + await new PutApiRequestBuilder('test', 'test').withHeader('test', 'true').sendRequest() }) it('with overriding single header', async () => { @@ -40,7 +42,7 @@ describe('PutApiRequestBuilder', () => { method: 'PUT', headers: expectedHeaders }) - await new PutApiRequestBuilder('test') + await new PutApiRequestBuilder('test', 'test') .withHeader('test', 'true') .withHeader('test', 'false') .sendRequest() @@ -54,7 +56,7 @@ describe('PutApiRequestBuilder', () => { method: 'PUT', headers: expectedHeaders }) - await new PutApiRequestBuilder('test') + await new PutApiRequestBuilder('test', 'test') .withHeader('test', 'true') .withHeader('test2', 'false') .sendRequest() @@ -70,7 +72,7 @@ describe('PutApiRequestBuilder', () => { headers: expectedHeaders, body: '{"test":true,"foo":"bar"}' }) - await new PutApiRequestBuilder('test') + await new PutApiRequestBuilder('test', 'test') .withJsonBody({ test: true, foo: 'bar' @@ -83,12 +85,7 @@ describe('PutApiRequestBuilder', () => { method: 'PUT', body: 'HedgeDoc' }) - await new PutApiRequestBuilder('test').withBody('HedgeDoc').sendRequest() - }) - - it('sendRequest with expected status code', async () => { - expectFetch('api/private/test', 200, { method: 'PUT' }) - await new PutApiRequestBuilder('test').withExpectedStatusCode(200).sendRequest() + await new PutApiRequestBuilder('test', 'test').withBody('HedgeDoc').sendRequest() }) describe('sendRequest with custom options', () => { @@ -97,7 +94,7 @@ describe('PutApiRequestBuilder', () => { method: 'PUT', cache: 'force-cache' }) - await new PutApiRequestBuilder('test') + await new PutApiRequestBuilder('test', 'test') .withCustomOptions({ cache: 'force-cache' }) @@ -109,7 +106,7 @@ describe('PutApiRequestBuilder', () => { method: 'PUT', cache: 'no-store' }) - await new PutApiRequestBuilder('test') + await new PutApiRequestBuilder('test', 'test') .withCustomOptions({ cache: 'force-cache' }) @@ -125,7 +122,7 @@ describe('PutApiRequestBuilder', () => { cache: 'force-cache', integrity: 'test' }) - await new PutApiRequestBuilder('test') + await new PutApiRequestBuilder('test', 'test') .withCustomOptions({ cache: 'force-cache', integrity: 'test' @@ -134,37 +131,29 @@ describe('PutApiRequestBuilder', () => { }) }) - describe('sendRequest with custom error map', () => { - it('for valid status code', async () => { - expectFetch('api/private/test', 200, { method: 'PUT' }) - await new PutApiRequestBuilder('test') - .withStatusCodeErrorMapping({ - 400: 'noooooo', - 401: 'not you!' - }) - .sendRequest() - }) - - it('for invalid status code 1', async () => { + describe('failing sendRequest', () => { + it('with bad request without api error name', async () => { expectFetch('api/private/test', 400, { method: 'PUT' }) - const request = new PutApiRequestBuilder('test') - .withStatusCodeErrorMapping({ - 400: 'noooooo', - 401: 'not you!' - }) - .sendRequest() - await expect(request).rejects.toThrow('noooooo') + const request = new PutApiRequestBuilder('test', 'test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, 'unknown', 'test', 'testExplosion')) }) - it('for invalid status code 2', async () => { - expectFetch('api/private/test', 401, { method: 'PUT' }) - const request = new PutApiRequestBuilder('test') - .withStatusCodeErrorMapping({ - 400: 'noooooo', - 401: 'not you!' - }) - .sendRequest() - await expect(request).rejects.toThrow('not you!') + it('with bad request with api error name', async () => { + expectFetch('api/private/test', 400, { method: 'PUT' }, { + message: 'The API has exploded!', + error: 'testExplosion' + } as ApiErrorResponse) + const request = new PutApiRequestBuilder('test', 'test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'test', 'testExplosion')) + }) + + it('with non bad request error', async () => { + expectFetch('api/private/test', 401, { method: 'PUT' }, { + message: 'The API has exploded!', + error: 'testExplosion' + } as ApiErrorResponse) + const request = new PutApiRequestBuilder('test', 'test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(401, 'forbidden', 'test', 'testExplosion')) }) }) }) diff --git a/frontend/src/api/common/api-request-builder/put-api-request-builder.ts b/frontend/src/api/common/api-request-builder/put-api-request-builder.ts index aa39fe223..b96acd581 100644 --- a/frontend/src/api/common/api-request-builder/put-api-request-builder.ts +++ b/frontend/src/api/common/api-request-builder/put-api-request-builder.ts @@ -21,6 +21,6 @@ export class PutApiRequestBuilder extends ApiRequ * @see ApiRequestBuilder#sendRequest */ sendRequest(): Promise> { - return this.sendRequestAndVerifyResponse('PUT', 200) + return this.sendRequestAndVerifyResponse('PUT') } } diff --git a/frontend/src/api/common/api-request-builder/test-utils/expect-fetch.ts b/frontend/src/api/common/api-request-builder/test-utils/expect-fetch.ts index d3e192213..b671c0a88 100644 --- a/frontend/src/api/common/api-request-builder/test-utils/expect-fetch.ts +++ b/frontend/src/api/common/api-request-builder/test-utils/expect-fetch.ts @@ -14,7 +14,12 @@ import { Mock } from 'ts-mockery' * @param requestStatusCode the status code the mocked request should return * @param expectedOptions additional options */ -export const expectFetch = (expectedUrl: string, requestStatusCode: number, expectedOptions: RequestInit): void => { +export const expectFetch = ( + expectedUrl: string, + requestStatusCode: number, + expectedOptions: RequestInit, + responseBody?: unknown +): void => { global.fetch = jest.fn((fetchUrl: RequestInfo | URL, fetchOptions?: RequestInit): Promise => { expect(fetchUrl).toEqual(expectedUrl) expect(fetchOptions).toStrictEqual({ @@ -25,8 +30,20 @@ export const expectFetch = (expectedUrl: string, requestStatusCode: number, expe }) return Promise.resolve( Mock.of({ - status: requestStatusCode + status: requestStatusCode, + statusText: mapCodeToText(requestStatusCode), + json: jest.fn(() => (responseBody ? Promise.resolve(responseBody) : Promise.reject())) }) ) }) } +const mapCodeToText = (code: number): string => { + switch (code) { + case 400: + return 'bad_request' + case 401: + return 'forbidden' + default: + return 'unknown_code' + } +} diff --git a/frontend/src/api/common/api-response.test.ts b/frontend/src/api/common/api-response.test.ts index fd3811036..383a329f2 100644 --- a/frontend/src/api/common/api-response.test.ts +++ b/frontend/src/api/common/api-response.test.ts @@ -13,17 +13,6 @@ describe('ApiResponse', () => { expect(responseObj.getResponse()).toEqual(mockResponse) }) - it('asBlob', async () => { - const mockBlob = Mock.of() - const mockResponse = Mock.of({ - blob(): Promise { - return Promise.resolve(mockBlob) - } - }) - const responseObj = new ApiResponse(mockResponse) - await expect(responseObj.asBlob()).resolves.toEqual(mockBlob) - }) - describe('asParsedJsonObject with', () => { it('invalid header', async () => { const mockHeaders = new Headers() diff --git a/frontend/src/api/common/api-response.ts b/frontend/src/api/common/api-response.ts index d8eca4ad1..ab29c4c66 100644 --- a/frontend/src/api/common/api-response.ts +++ b/frontend/src/api/common/api-response.ts @@ -28,6 +28,14 @@ export class ApiResponse { return this.response } + static isSuccessfulResponse(response: Response): boolean { + return response.status >= 400 + } + + isSuccessful(): boolean { + return ApiResponse.isSuccessfulResponse(this.response) + } + /** * Returns the response as parsed JSON. An error will be thrown if the response is not JSON encoded. * @@ -42,13 +50,4 @@ export class ApiResponse { // see https://github.com/hedgedoc/react-client/issues/1219 return (await this.response.json()) as ResponseType } - - /** - * Returns the response as a Blob. - * - * @return The response body as a blob. - */ - async asBlob(): Promise { - return await this.response.blob() - } } diff --git a/frontend/src/api/common/error-to-i18n-key-mapper.ts b/frontend/src/api/common/error-to-i18n-key-mapper.ts new file mode 100644 index 000000000..d10b05253 --- /dev/null +++ b/frontend/src/api/common/error-to-i18n-key-mapper.ts @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { ApiError } from './api-error' + +export class ErrorToI18nKeyMapper { + private foundI18nKey: string | undefined = undefined + + constructor(private apiError: Error, private i18nNamespace?: string) {} + + public withHttpCode(code: number, i18nKey: string): this { + if (this.foundI18nKey === undefined && this.apiError instanceof ApiError && this.apiError.statusCode === code) { + this.foundI18nKey = i18nKey + } + return this + } + + public withBackendErrorName(errorName: string, i18nKey: string): this { + if ( + this.foundI18nKey === undefined && + this.apiError instanceof ApiError && + this.apiError.apiErrorName === errorName + ) { + this.foundI18nKey = i18nKey + } + return this + } + + public withErrorMessage(message: string, i18nKey: string): this { + if (this.foundI18nKey === undefined && this.apiError.message === message) { + this.foundI18nKey = i18nKey + } + return this + } + + public orFallbackI18nKey(fallback?: string): typeof fallback { + const foundValue = this.foundI18nKey ?? fallback + if (foundValue !== undefined && this.i18nNamespace !== undefined) { + return `${this.i18nNamespace}.${foundValue}` + } else { + return foundValue + } + } +} diff --git a/frontend/src/api/config/index.ts b/frontend/src/api/config/index.ts index a93e5f81f..f60a2234a 100644 --- a/frontend/src/api/config/index.ts +++ b/frontend/src/api/config/index.ts @@ -13,6 +13,6 @@ import type { Config } from './types' * @throws {Error} when the api request wasn't successful. */ export const getConfig = async (): Promise => { - const response = await new GetApiRequestBuilder('config').sendRequest() + const response = await new GetApiRequestBuilder('config', 'config').sendRequest() return response.asParsedJsonObject() } diff --git a/frontend/src/api/group/index.ts b/frontend/src/api/group/index.ts index 88ed99bb5..ab35f0d39 100644 --- a/frontend/src/api/group/index.ts +++ b/frontend/src/api/group/index.ts @@ -14,6 +14,6 @@ import type { GroupInfo } from './types' * @throws {Error} when the api request wasn't successful. */ export const getGroup = async (groupName: string): Promise => { - const response = await new GetApiRequestBuilder('groups/' + groupName).sendRequest() + const response = await new GetApiRequestBuilder('groups/' + groupName, 'group').sendRequest() return response.asParsedJsonObject() } diff --git a/frontend/src/api/history/index.ts b/frontend/src/api/history/index.ts index 3ef125d11..f9273cdd0 100644 --- a/frontend/src/api/history/index.ts +++ b/frontend/src/api/history/index.ts @@ -16,7 +16,7 @@ import type { ChangePinStatusDto, HistoryEntry, HistoryEntryPutDto } from './typ * @throws {Error} when the api request wasn't successful. */ export const getRemoteHistory = async (): Promise => { - const response = await new GetApiRequestBuilder('me/history').sendRequest() + const response = await new GetApiRequestBuilder('me/history', 'history').sendRequest() return response.asParsedJsonObject() } @@ -27,7 +27,9 @@ export const getRemoteHistory = async (): Promise => { * @throws {Error} when the api request wasn't successful. */ export const setRemoteHistoryEntries = async (entries: HistoryEntryPutDto[]): Promise => { - await new PostApiRequestBuilder('me/history').withJsonBody(entries).sendRequest() + await new PostApiRequestBuilder('me/history', 'history') + .withJsonBody(entries) + .sendRequest() } /** @@ -41,7 +43,10 @@ export const updateRemoteHistoryEntryPinStatus = async ( noteIdOrAlias: string, pinStatus: boolean ): Promise => { - const response = await new PutApiRequestBuilder('me/history/' + noteIdOrAlias) + const response = await new PutApiRequestBuilder( + 'me/history/' + noteIdOrAlias, + 'history' + ) .withJsonBody({ pinStatus }) @@ -56,7 +61,7 @@ export const updateRemoteHistoryEntryPinStatus = async ( * @throws {Error} when the api request wasn't successful. */ export const deleteRemoteHistoryEntry = async (noteIdOrAlias: string): Promise => { - await new DeleteApiRequestBuilder('me/history/' + noteIdOrAlias).sendRequest() + await new DeleteApiRequestBuilder('me/history/' + noteIdOrAlias, 'history').sendRequest() } /** @@ -65,5 +70,5 @@ export const deleteRemoteHistoryEntry = async (noteIdOrAlias: string): Promise => { - await new DeleteApiRequestBuilder('me/history').sendRequest() + await new DeleteApiRequestBuilder('me/history', 'history').sendRequest() } diff --git a/frontend/src/api/me/index.ts b/frontend/src/api/me/index.ts index 7ca88e50b..7ea1fa788 100644 --- a/frontend/src/api/me/index.ts +++ b/frontend/src/api/me/index.ts @@ -16,7 +16,7 @@ import type { ChangeDisplayNameDto, LoginUserInfo } from './types' * @throws {Error} when the user is not signed-in. */ export const getMe = async (): Promise => { - const response = await new GetApiRequestBuilder('me').sendRequest() + const response = await new GetApiRequestBuilder('me', 'me').sendRequest() return response.asParsedJsonObject() } @@ -26,7 +26,7 @@ export const getMe = async (): Promise => { * @throws {Error} when the api request wasn't successful. */ export const deleteUser = async (): Promise => { - await new DeleteApiRequestBuilder('me').sendRequest() + await new DeleteApiRequestBuilder('me', 'me').sendRequest() } /** @@ -36,7 +36,7 @@ export const deleteUser = async (): Promise => { * @throws {Error} when the api request wasn't successful. */ export const updateDisplayName = async (displayName: string): Promise => { - await new PostApiRequestBuilder('me/profile') + await new PostApiRequestBuilder('me/profile', 'me') .withJsonBody({ displayName }) @@ -50,6 +50,6 @@ export const updateDisplayName = async (displayName: string): Promise => { * @throws {Error} when the api request wasn't successful. */ export const getMyMedia = async (): Promise => { - const response = await new GetApiRequestBuilder('me/media').sendRequest() + const response = await new GetApiRequestBuilder('me/media', 'me').sendRequest() return response.asParsedJsonObject() } diff --git a/frontend/src/api/media/index.ts b/frontend/src/api/media/index.ts index 1f1b9e8c8..0b76170ce 100644 --- a/frontend/src/api/media/index.ts +++ b/frontend/src/api/media/index.ts @@ -15,7 +15,7 @@ import type { ImageProxyRequestDto, ImageProxyResponse, MediaUpload } from './ty * @throws {Error} when the api request wasn't successful. */ export const getProxiedUrl = async (imageUrl: string): Promise => { - const response = await new PostApiRequestBuilder('media/proxy') + const response = await new PostApiRequestBuilder('media/proxy', 'media') .withJsonBody({ url: imageUrl }) @@ -34,7 +34,7 @@ export const getProxiedUrl = async (imageUrl: string): Promise => { const postData = new FormData() postData.append('file', media) - const response = await new PostApiRequestBuilder('media') + const response = await new PostApiRequestBuilder('media', 'media') .withHeader('HedgeDoc-Note', noteIdOrAlias) .withBody(postData) .sendRequest() @@ -48,5 +48,5 @@ export const uploadFile = async (noteIdOrAlias: string, media: Blob): Promise => { - await new DeleteApiRequestBuilder('media/' + mediaId).sendRequest() + await new DeleteApiRequestBuilder('media/' + mediaId, 'media').sendRequest() } diff --git a/frontend/src/api/notes/index.ts b/frontend/src/api/notes/index.ts index 22e898b50..840dcc79c 100644 --- a/frontend/src/api/notes/index.ts +++ b/frontend/src/api/notes/index.ts @@ -17,9 +17,7 @@ import type { Note, NoteDeletionOptions, NoteMetadata } from './types' * @throws {Error} when the api request wasn't successful. */ export const getNote = async (noteIdOrAlias: string): Promise => { - const response = await new GetApiRequestBuilder('notes/' + noteIdOrAlias) - .withStatusCodeErrorMapping({ 404: 'api.note.notFound', 403: 'api.note.forbidden' }) - .sendRequest() + const response = await new GetApiRequestBuilder('notes/' + noteIdOrAlias, 'note').sendRequest() return response.asParsedJsonObject() } @@ -30,7 +28,7 @@ export const getNote = async (noteIdOrAlias: string): Promise => { * @return Metadata of the specified note. */ export const getNoteMetadata = async (noteIdOrAlias: string): Promise => { - const response = await new GetApiRequestBuilder(`notes/${noteIdOrAlias}/metadata`).sendRequest() + const response = await new GetApiRequestBuilder(`notes/${noteIdOrAlias}/metadata`, 'note').sendRequest() return response.asParsedJsonObject() } @@ -42,7 +40,7 @@ export const getNoteMetadata = async (noteIdOrAlias: string): Promise => { - const response = await new GetApiRequestBuilder(`notes/${noteIdOrAlias}/media`).sendRequest() + const response = await new GetApiRequestBuilder(`notes/${noteIdOrAlias}/media`, 'note').sendRequest() return response.asParsedJsonObject() } @@ -54,7 +52,7 @@ export const getMediaForNote = async (noteIdOrAlias: string): Promise => { - const response = await new PostApiRequestBuilder('notes') + const response = await new PostApiRequestBuilder('notes', 'note') .withHeader('Content-Type', 'text/markdown') .withBody(markdown) .sendRequest() @@ -70,7 +68,7 @@ export const createNote = async (markdown: string): Promise => { * @throws {Error} when the api request wasn't successful. */ export const createNoteWithPrimaryAlias = async (markdown: string, primaryAlias: string): Promise => { - const response = await new PostApiRequestBuilder('notes/' + primaryAlias) + const response = await new PostApiRequestBuilder('notes/' + primaryAlias, 'note') .withHeader('Content-Type', 'text/markdown') .withBody(markdown) .sendRequest() @@ -84,7 +82,7 @@ export const createNoteWithPrimaryAlias = async (markdown: string, primaryAlias: * @throws {Error} when the api request wasn't successful. */ export const deleteNote = async (noteIdOrAlias: string): Promise => { - await new DeleteApiRequestBuilder('notes/' + noteIdOrAlias) + await new DeleteApiRequestBuilder('notes/' + noteIdOrAlias, 'note') .withJsonBody({ keepMedia: false // TODO Ask whether the user wants to keep the media uploaded to the note. diff --git a/frontend/src/api/permissions/index.ts b/frontend/src/api/permissions/index.ts index 0135397f4..c93a666f3 100644 --- a/frontend/src/api/permissions/index.ts +++ b/frontend/src/api/permissions/index.ts @@ -18,7 +18,8 @@ import type { OwnerChangeDto, PermissionSetDto } from './types' */ export const setNoteOwner = async (noteId: string, owner: string): Promise => { const response = await new PutApiRequestBuilder( - `notes/${noteId}/metadata/permissions/owner` + `notes/${noteId}/metadata/permissions/owner`, + 'permission' ) .withJsonBody({ owner @@ -42,7 +43,8 @@ export const setUserPermission = async ( canEdit: boolean ): Promise => { const response = await new PutApiRequestBuilder( - `notes/${noteId}/metadata/permissions/users/${username}` + `notes/${noteId}/metadata/permissions/users/${username}`, + 'permission' ) .withJsonBody({ canEdit @@ -66,7 +68,8 @@ export const setGroupPermission = async ( canEdit: boolean ): Promise => { const response = await new PutApiRequestBuilder( - `notes/${noteId}/metadata/permissions/groups/${groupName}` + `notes/${noteId}/metadata/permissions/groups/${groupName}`, + 'permission' ) .withJsonBody({ canEdit @@ -85,10 +88,9 @@ export const setGroupPermission = async ( */ export const removeUserPermission = async (noteId: string, username: string): Promise => { const response = await new DeleteApiRequestBuilder( - `notes/${noteId}/metadata/permissions/users/${username}` - ) - .withExpectedStatusCode(200) - .sendRequest() + `notes/${noteId}/metadata/permissions/users/${username}`, + 'permission' + ).sendRequest() return response.asParsedJsonObject() } @@ -102,9 +104,8 @@ export const removeUserPermission = async (noteId: string, username: string): Pr */ export const removeGroupPermission = async (noteId: string, groupName: string): Promise => { const response = await new DeleteApiRequestBuilder( - `notes/${noteId}/metadata/permissions/groups/${groupName}` - ) - .withExpectedStatusCode(200) - .sendRequest() + `notes/${noteId}/metadata/permissions/groups/${groupName}`, + 'permission' + ).sendRequest() return response.asParsedJsonObject() } diff --git a/frontend/src/api/revisions/index.ts b/frontend/src/api/revisions/index.ts index 62b98f941..a38c2f91f 100644 --- a/frontend/src/api/revisions/index.ts +++ b/frontend/src/api/revisions/index.ts @@ -17,7 +17,8 @@ import type { RevisionDetails, RevisionMetadata } from './types' */ export const getRevision = async (noteId: string, revisionId: number): Promise => { const response = await new GetApiRequestBuilder( - `notes/${noteId}/revisions/${revisionId}` + `notes/${noteId}/revisions/${revisionId}`, + 'revisions' ).sendRequest() return response.asParsedJsonObject() } @@ -30,7 +31,10 @@ export const getRevision = async (noteId: string, revisionId: number): Promise => { - const response = await new GetApiRequestBuilder(`notes/${noteId}/revisions`).sendRequest() + const response = await new GetApiRequestBuilder( + `notes/${noteId}/revisions`, + 'revisions' + ).sendRequest() return response.asParsedJsonObject() } @@ -41,5 +45,5 @@ export const getAllRevisions = async (noteId: string): Promise => { - await new DeleteApiRequestBuilder(`notes/${noteIdOrAlias}/revisions`).sendRequest() + await new DeleteApiRequestBuilder(`notes/${noteIdOrAlias}/revisions`, 'revisions').sendRequest() } diff --git a/frontend/src/api/tokens/index.ts b/frontend/src/api/tokens/index.ts index fd335aee9..04673b5c0 100644 --- a/frontend/src/api/tokens/index.ts +++ b/frontend/src/api/tokens/index.ts @@ -15,7 +15,7 @@ import type { AccessToken, AccessTokenWithSecret, CreateAccessTokenDto } from '. * @throws {Error} when the api request wasn't successful. */ export const getAccessTokenList = async (): Promise => { - const response = await new GetApiRequestBuilder('tokens').sendRequest() + const response = await new GetApiRequestBuilder('tokens', 'tokens').sendRequest() return response.asParsedJsonObject() } @@ -28,7 +28,7 @@ export const getAccessTokenList = async (): Promise => { * @throws {Error} when the api request wasn't successful. */ export const postNewAccessToken = async (label: string, validUntil: number): Promise => { - const response = await new PostApiRequestBuilder('tokens') + const response = await new PostApiRequestBuilder('tokens', 'tokens') .withJsonBody({ label, validUntil @@ -44,5 +44,5 @@ export const postNewAccessToken = async (label: string, validUntil: number): Pro * @throws {Error} when the api request wasn't successful. */ export const deleteAccessToken = async (keyId: string): Promise => { - await new DeleteApiRequestBuilder('tokens/' + keyId).sendRequest() + await new DeleteApiRequestBuilder('tokens/' + keyId, 'tokens').sendRequest() } diff --git a/frontend/src/api/users/index.ts b/frontend/src/api/users/index.ts index d785d2f73..ef9f9d583 100644 --- a/frontend/src/api/users/index.ts +++ b/frontend/src/api/users/index.ts @@ -14,6 +14,6 @@ import type { UserInfo } from './types' * @throws {Error} when the api request wasn't successful. */ export const getUser = async (username: string): Promise => { - const response = await new GetApiRequestBuilder('users/' + username).sendRequest() + const response = await new GetApiRequestBuilder('users/' + username, 'users').sendRequest() return response.asParsedJsonObject() } diff --git a/frontend/src/components/common/note-loading-boundary/__snapshots__/note-loading-boundary.test.tsx.snap b/frontend/src/components/common/note-loading-boundary/__snapshots__/note-loading-boundary.test.tsx.snap index 81850aab0..917fbdb0f 100644 --- a/frontend/src/components/common/note-loading-boundary/__snapshots__/note-loading-boundary.test.tsx.snap +++ b/frontend/src/components/common/note-loading-boundary/__snapshots__/note-loading-boundary.test.tsx.snap @@ -27,9 +27,6 @@ exports[`Note loading boundary shows an error 1`] = ` children: - - This is a mock for CreateNonExistingNoteHint - `; diff --git a/frontend/src/components/common/note-loading-boundary/note-loading-boundary.tsx b/frontend/src/components/common/note-loading-boundary/note-loading-boundary.tsx index 082eff093..3f155f4ef 100644 --- a/frontend/src/components/common/note-loading-boundary/note-loading-boundary.tsx +++ b/frontend/src/components/common/note-loading-boundary/note-loading-boundary.tsx @@ -32,7 +32,7 @@ export const NoteLoadingBoundary: React.FC = ({ children }) = } return ( - + diff --git a/frontend/src/components/login-page/auth/auth-error/auth-error.tsx b/frontend/src/components/login-page/auth/auth-error/auth-error.tsx deleted file mode 100644 index deac11245..000000000 --- a/frontend/src/components/login-page/auth/auth-error/auth-error.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { AuthError as AuthErrorType } from '../../../../api/auth/types' -import React, { useMemo } from 'react' -import { Alert } from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' - -export interface AuthErrorProps { - error?: AuthErrorType -} - -/** - * Renders an error message for auth fields when an error is present. - * - * @param error The error to render. Can be {@link undefined} when no error should be rendered. - */ -export const AuthError: React.FC = ({ error }) => { - useTranslation() - - const errorMessageI18nKey = useMemo(() => { - switch (error) { - case AuthErrorType.INVALID_CREDENTIALS: - return 'login.auth.error.usernamePassword' - case AuthErrorType.LOGIN_DISABLED: - return 'login.auth.error.loginDisabled' - case AuthErrorType.OPENID_ERROR: - return 'login.auth.error.openIdLogin' - default: - return 'login.auth.error.other' - } - }, [error]) - - return ( - - - - ) -} diff --git a/frontend/src/components/login-page/auth/via-ldap.tsx b/frontend/src/components/login-page/auth/via-ldap.tsx index ca5feef4e..a73656f4c 100644 --- a/frontend/src/components/login-page/auth/via-ldap.tsx +++ b/frontend/src/components/login-page/auth/via-ldap.tsx @@ -4,15 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { doLdapLogin } from '../../../api/auth/ldap' -import { AuthError as AuthErrorType } from '../../../api/auth/types' import { useOnInputChange } from '../../../hooks/common/use-on-input-change' -import { AuthError } from './auth-error/auth-error' import { PasswordField } from './fields/password-field' import { UsernameField } from './fields/username-field' import { fetchAndSetUser } from './utils' import type { FormEvent } from 'react' import React, { useCallback, useState } from 'react' -import { Button, Card, Form } from 'react-bootstrap' +import { Alert, Button, Card, Form } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' export interface ViaLdapProps { @@ -28,19 +26,13 @@ export const ViaLdap: React.FC = ({ providerName, identifier }) => const [username, setUsername] = useState('') const [password, setPassword] = useState('') - const [error, setError] = useState() + const [error, setError] = useState() const onLoginSubmit = useCallback( (event: FormEvent) => { doLdapLogin(identifier, username, password) .then(() => fetchAndSetUser()) - .catch((error: Error) => { - setError( - Object.values(AuthErrorType).includes(error.message as AuthErrorType) - ? (error.message as AuthErrorType) - : AuthErrorType.OTHER - ) - }) + .catch((error: Error) => setError(error.message)) event.preventDefault() }, [username, password, identifier] @@ -58,7 +50,9 @@ export const ViaLdap: React.FC = ({ providerName, identifier }) =>
- + + +