From ca9836d691026710bf53899d607d2b08f436bc2d Mon Sep 17 00:00:00 2001 From: Erik Michelson Date: Sat, 25 Mar 2023 18:58:48 +0100 Subject: [PATCH] enhancement(auth): better error message handling Signed-off-by: Erik Michelson --- backend/src/api/utils/login-enabled.guard.ts | 10 ++--- .../api/utils/registration-enabled.guard.ts | 4 +- backend/src/errors/error-mapping.ts | 2 +- backend/src/errors/errors.ts | 4 +- backend/test/private-api/auth.e2e-spec.ts | 6 +-- frontend/locales/en.json | 9 ++--- frontend/src/api/alias/index.ts | 6 +-- frontend/src/api/auth/index.ts | 2 +- frontend/src/api/auth/ldap.ts | 2 +- frontend/src/api/auth/local.ts | 6 +-- frontend/src/api/common/api-error-response.ts | 2 +- frontend/src/api/common/api-error.ts | 7 ++-- .../api-request-builder.ts | 7 ++-- .../delete-api-request-builder.spec.ts | 40 +++++++++---------- .../get-api-request-builder.spec.ts | 36 ++++++++--------- .../post-api-request-builder.spec.ts | 40 +++++++++---------- .../put-api-request-builder.spec.ts | 40 +++++++++---------- .../api/common/error-to-i18n-key-mapper.ts | 15 +++---- frontend/src/api/config/index.ts | 2 +- frontend/src/api/group/index.ts | 2 +- frontend/src/api/history/index.ts | 15 +++---- frontend/src/api/me/index.ts | 8 ++-- frontend/src/api/media/index.ts | 6 +-- frontend/src/api/notes/index.ts | 12 +++--- frontend/src/api/permissions/index.ts | 15 +++---- frontend/src/api/revisions/index.ts | 10 ++--- frontend/src/api/tokens/index.ts | 6 +-- frontend/src/api/users/index.ts | 2 +- .../common/fields/new-password-field.tsx | 3 +- .../note-loading-boundary.spec.tsx.snap | 7 +++- .../note-loading-boundary.spec.tsx | 3 +- .../note-loading-boundary.tsx | 15 +++++-- .../components/login-page/auth/via-local.tsx | 36 ++++++++++------- .../settings/profile-change-password.tsx | 4 +- .../register-page/register-error.tsx | 4 +- frontend/src/pages/login.tsx | 2 +- frontend/src/pages/register.tsx | 16 +++----- 37 files changed, 199 insertions(+), 207 deletions(-) diff --git a/backend/src/api/utils/login-enabled.guard.ts b/backend/src/api/utils/login-enabled.guard.ts index fe1b98eb2..ce7de6ea5 100644 --- a/backend/src/api/utils/login-enabled.guard.ts +++ b/backend/src/api/utils/login-enabled.guard.ts @@ -3,14 +3,10 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { - BadRequestException, - CanActivate, - Inject, - Injectable, -} from '@nestjs/common'; +import { CanActivate, Inject, Injectable } from '@nestjs/common'; import authConfiguration, { AuthConfig } from '../../config/auth.config'; +import { FeatureDisabledError } from '../../errors/errors'; import { ConsoleLoggerService } from '../../logger/console-logger.service'; @Injectable() @@ -26,7 +22,7 @@ export class LoginEnabledGuard implements CanActivate { canActivate(): boolean { if (!this.authConfig.local.enableLogin) { this.logger.debug('Local auth is disabled.', 'canActivate'); - throw new BadRequestException('Local auth is disabled.'); + throw new FeatureDisabledError('Local auth is disabled.'); } return true; } diff --git a/backend/src/api/utils/registration-enabled.guard.ts b/backend/src/api/utils/registration-enabled.guard.ts index 8d4cce503..2158b4bbb 100644 --- a/backend/src/api/utils/registration-enabled.guard.ts +++ b/backend/src/api/utils/registration-enabled.guard.ts @@ -6,7 +6,7 @@ import { CanActivate, Inject, Injectable } from '@nestjs/common'; import authConfiguration, { AuthConfig } from '../../config/auth.config'; -import { RegistrationDisabledError } from '../../errors/errors'; +import { FeatureDisabledError } from '../../errors/errors'; import { ConsoleLoggerService } from '../../logger/console-logger.service'; @Injectable() @@ -22,7 +22,7 @@ export class RegistrationEnabledGuard implements CanActivate { canActivate(): boolean { if (!this.authConfig.local.enableRegister) { this.logger.debug('User registration is disabled.', 'canActivate'); - throw new RegistrationDisabledError(); + throw new FeatureDisabledError('User registration is disabled'); } return true; } diff --git a/backend/src/errors/error-mapping.ts b/backend/src/errors/error-mapping.ts index 6672a2c0c..d3ab64425 100644 --- a/backend/src/errors/error-mapping.ts +++ b/backend/src/errors/error-mapping.ts @@ -77,7 +77,7 @@ const mapOfHedgeDocErrorsToHttpErrors: Map = (object): HttpException => new PayloadTooLargeException(object), ], [ - 'RegistrationDisabledError', + 'FeatureDisabledError', (object): HttpException => new ForbiddenException(object), ], ]); diff --git a/backend/src/errors/errors.ts b/backend/src/errors/errors.ts index 4010d420f..7e2335c97 100644 --- a/backend/src/errors/errors.ts +++ b/backend/src/errors/errors.ts @@ -60,6 +60,6 @@ export class MaximumDocumentLengthExceededError extends Error { name = 'MaximumDocumentLengthExceededError'; } -export class RegistrationDisabledError extends Error { - name = 'RegistrationDisabledError'; +export class FeatureDisabledError extends Error { + name = 'FeatureDisabledError'; } diff --git a/backend/test/private-api/auth.e2e-spec.ts b/backend/test/private-api/auth.e2e-spec.ts index 4d1b52efe..531a43c69 100644 --- a/backend/test/private-api/auth.e2e-spec.ts +++ b/backend/test/private-api/auth.e2e-spec.ts @@ -25,7 +25,7 @@ describe('Auth', () => { let displayName: string; let password: string; - beforeAll(async () => { + beforeEach(async () => { testSetup = await TestSetupBuilder.create().build(); await testSetup.app.init(); @@ -34,7 +34,7 @@ describe('Auth', () => { password = 'test_password'; }); - afterAll(async () => { + afterEach(async () => { // Yes, this is a bad hack, but there is a race somewhere and I have // no idea how to fix it. await new Promise((resolve) => { @@ -193,7 +193,7 @@ describe('Auth', () => { .set('Content-Type', 'application/json') .set('Cookie', cookie) .send(JSON.stringify(changePasswordDto)) - .expect(400); + .expect(403); // enable login again testSetup.configService.get('authConfig').local.enableLogin = true; // new password doesn't work for login diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 67fa8336c..0d4b8814e 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -43,10 +43,8 @@ "creating": "Creating note...", "error": "Note couldn't be created.", "success": "Note has been created." - } - }, - "api": { - "note": { + }, + "error": { "notFound": { "title": "Note not found", "description": "The requested note doesn't exist." @@ -563,10 +561,11 @@ } }, "register": { + "question": "No account yet?", "title": "Register", "passwordAgain": "Password (again)", "usernameInfo": "The username is your unique identifier for login.", - "passwordInfo": "Choose a unique and secure password. It must contain at least 8 characters.", + "passwordInfo": "Choose a unique and secure password.", "infoTermsPrivacy": "With the registration of my user account I agree to the following terms:", "success": { "title": "Successfully registered", diff --git a/frontend/src/api/alias/index.ts b/frontend/src/api/alias/index.ts index efe8733df..1662a94af 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', 'alias') + const response = await new PostApiRequestBuilder('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, 'alias') + const response = await new PutApiRequestBuilder('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, 'alias').sendRequest() + await new DeleteApiRequestBuilder('alias/' + alias).sendRequest() } diff --git a/frontend/src/api/auth/index.ts b/frontend/src/api/auth/index.ts index b17d9bd18..b8a4f74ae 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', 'auth').sendRequest() + await new DeleteApiRequestBuilder('auth/logout').sendRequest() } diff --git a/frontend/src/api/auth/ldap.ts b/frontend/src/api/auth/ldap.ts index 953839022..65cdf5b37 100644 --- a/frontend/src/api/auth/ldap.ts +++ b/frontend/src/api/auth/ldap.ts @@ -15,7 +15,7 @@ import type { LoginDto } from './types' * @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, 'auth') + await new PostApiRequestBuilder('auth/ldap/' + provider) .withJsonBody({ username: username, password: password diff --git a/frontend/src/api/auth/local.ts b/frontend/src/api/auth/local.ts index d1727b8b2..ca830f811 100644 --- a/frontend/src/api/auth/local.ts +++ b/frontend/src/api/auth/local.ts @@ -17,7 +17,7 @@ import type { ChangePasswordDto, LoginDto, RegisterDto } 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', 'auth') + await new PostApiRequestBuilder('auth/local/login') .withJsonBody({ username, password @@ -37,7 +37,7 @@ 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', 'auth') + await new PostApiRequestBuilder('auth/local') .withJsonBody({ username, displayName, @@ -54,7 +54,7 @@ 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', 'auth') + await new PutApiRequestBuilder('auth/local') .withJsonBody({ currentPassword, newPassword diff --git a/frontend/src/api/common/api-error-response.ts b/frontend/src/api/common/api-error-response.ts index 452ad043e..69ca1ed0f 100644 --- a/frontend/src/api/common/api-error-response.ts +++ b/frontend/src/api/common/api-error-response.ts @@ -6,5 +6,5 @@ export interface ApiErrorResponse { message: string - error: string + name: string } diff --git a/frontend/src/api/common/api-error.ts b/frontend/src/api/common/api-error.ts index 2a3270b47..c5726cc92 100644 --- a/frontend/src/api/common/api-error.ts +++ b/frontend/src/api/common/api-error.ts @@ -7,10 +7,9 @@ export class ApiError extends Error { constructor( public readonly statusCode: number, - statusText: string, - i18nNamespace: string, - public readonly apiErrorName: string | undefined + public readonly backendErrorName: string | undefined, + public readonly backendErrorMessage: string | undefined ) { - super(`api.error.${i18nNamespace}.${statusText}`) + super() } } 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 208e43467..ebcc5d6f2 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 @@ -25,7 +25,7 @@ export abstract class ApiRequestBuilder { * * @param endpoint The target endpoint without a leading slash. */ - constructor(endpoint: string, private apiI18nKey: string) { + constructor(endpoint: string) { this.targetUrl = `api/private/${endpoint}` } @@ -38,9 +38,8 @@ export abstract class ApiRequestBuilder { }) 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) + const backendError = await this.readApiErrorResponseFromBody(response) + throw new ApiError(response.status, backendError?.name, backendError?.message) } return new ApiResponse(response) diff --git a/frontend/src/api/common/api-request-builder/delete-api-request-builder.spec.ts b/frontend/src/api/common/api-request-builder/delete-api-request-builder.spec.ts index 97ed84dc4..f44f38687 100644 --- a/frontend/src/api/common/api-request-builder/delete-api-request-builder.spec.ts +++ b/frontend/src/api/common/api-request-builder/delete-api-request-builder.spec.ts @@ -21,7 +21,7 @@ describe('DeleteApiRequestBuilder', () => { describe('sendRequest without body', () => { it('without headers', async () => { expectFetch('api/private/test', 204, { method: 'DELETE' }) - await new DeleteApiRequestBuilder('test', 'test').sendRequest() + await new DeleteApiRequestBuilder('test').sendRequest() }) it('with single header', async () => { @@ -31,7 +31,7 @@ describe('DeleteApiRequestBuilder', () => { method: 'DELETE', headers: expectedHeaders }) - await new DeleteApiRequestBuilder('test', 'test').withHeader('test', 'true').sendRequest() + await new DeleteApiRequestBuilder('test').withHeader('test', 'true').sendRequest() }) it('with overriding single header', async () => { @@ -41,7 +41,7 @@ describe('DeleteApiRequestBuilder', () => { method: 'DELETE', headers: expectedHeaders }) - await new DeleteApiRequestBuilder('test', 'test') + await new DeleteApiRequestBuilder('test') .withHeader('test', 'true') .withHeader('test', 'false') .sendRequest() @@ -55,7 +55,7 @@ describe('DeleteApiRequestBuilder', () => { method: 'DELETE', headers: expectedHeaders }) - await new DeleteApiRequestBuilder('test', 'test') + await new DeleteApiRequestBuilder('test') .withHeader('test', 'true') .withHeader('test2', 'false') .sendRequest() @@ -71,7 +71,7 @@ describe('DeleteApiRequestBuilder', () => { headers: expectedHeaders, body: '{"test":true,"foo":"bar"}' }) - await new DeleteApiRequestBuilder('test', 'test') + await new DeleteApiRequestBuilder('test') .withJsonBody({ test: true, foo: 'bar' @@ -84,7 +84,7 @@ describe('DeleteApiRequestBuilder', () => { method: 'DELETE', body: 'HedgeDoc' }) - await new DeleteApiRequestBuilder('test', 'test').withBody('HedgeDoc').sendRequest() + await new DeleteApiRequestBuilder('test').withBody('HedgeDoc').sendRequest() }) describe('sendRequest with custom options', () => { @@ -93,7 +93,7 @@ describe('DeleteApiRequestBuilder', () => { method: 'DELETE', cache: 'force-cache' }) - await new DeleteApiRequestBuilder('test', 'test') + await new DeleteApiRequestBuilder('test') .withCustomOptions({ cache: 'force-cache' }) @@ -105,7 +105,7 @@ describe('DeleteApiRequestBuilder', () => { method: 'DELETE', cache: 'no-store' }) - await new DeleteApiRequestBuilder('test', 'test') + await new DeleteApiRequestBuilder('test') .withCustomOptions({ cache: 'force-cache' }) @@ -121,7 +121,7 @@ describe('DeleteApiRequestBuilder', () => { cache: 'force-cache', integrity: 'test' }) - await new DeleteApiRequestBuilder('test', 'test') + await new DeleteApiRequestBuilder('test') .withCustomOptions({ cache: 'force-cache', integrity: 'test' @@ -131,28 +131,28 @@ describe('DeleteApiRequestBuilder', () => { }) describe('failing sendRequest', () => { - it('with bad request without api error name', async () => { + it('without backend provided error name or error message', async () => { expectFetch('api/private/test', 400, { method: 'DELETE' }) - const request = new DeleteApiRequestBuilder('test', 'test').sendRequest() - await expect(request).rejects.toEqual(new ApiError(400, 'unknown', 'test', 'testExplosion')) + const request = new DeleteApiRequestBuilder('test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, undefined, undefined)) }) - it('with bad request with api error name', async () => { + it('with backend error name and error message', async () => { expectFetch('api/private/test', 400, { method: 'DELETE' }, { message: 'The API has exploded!', - error: 'testExplosion' + name: 'testExplosion' } as ApiErrorResponse) - const request = new DeleteApiRequestBuilder('test', 'test').sendRequest() - await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'test', 'testExplosion')) + const request = new DeleteApiRequestBuilder('test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'The API has exploded!')) }) - it('with non bad request error', async () => { + it('with another status code than 400', async () => { expectFetch('api/private/test', 401, { method: 'DELETE' }, { message: 'The API has exploded!', - error: 'testExplosion' + name: 'testExplosion' } as ApiErrorResponse) - const request = new DeleteApiRequestBuilder('test', 'test').sendRequest() - await expect(request).rejects.toEqual(new ApiError(401, 'forbidden', 'test', 'testExplosion')) + const request = new DeleteApiRequestBuilder('test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(401, 'testExplosion', 'The API has exploded!')) }) }) }) diff --git a/frontend/src/api/common/api-request-builder/get-api-request-builder.spec.ts b/frontend/src/api/common/api-request-builder/get-api-request-builder.spec.ts index 8e822cbe9..17f40b2ed 100644 --- a/frontend/src/api/common/api-request-builder/get-api-request-builder.spec.ts +++ b/frontend/src/api/common/api-request-builder/get-api-request-builder.spec.ts @@ -22,7 +22,7 @@ describe('GetApiRequestBuilder', () => { describe('sendRequest', () => { it('without headers', async () => { expectFetch('api/private/test', 200, { method: 'GET' }) - await new GetApiRequestBuilder('test', 'test').sendRequest() + await new GetApiRequestBuilder('test').sendRequest() }) it('with single header', async () => { @@ -32,7 +32,7 @@ describe('GetApiRequestBuilder', () => { method: 'GET', headers: expectedHeaders }) - await new GetApiRequestBuilder('test', 'test').withHeader('test', 'true').sendRequest() + await new GetApiRequestBuilder('test').withHeader('test', 'true').sendRequest() }) it('with overriding single header', async () => { @@ -42,7 +42,7 @@ describe('GetApiRequestBuilder', () => { method: 'GET', headers: expectedHeaders }) - await new GetApiRequestBuilder('test', 'test') + await new GetApiRequestBuilder('test') .withHeader('test', 'true') .withHeader('test', 'false') .sendRequest() @@ -56,7 +56,7 @@ describe('GetApiRequestBuilder', () => { method: 'GET', headers: expectedHeaders }) - await new GetApiRequestBuilder('test', 'test') + await new GetApiRequestBuilder('test') .withHeader('test', 'true') .withHeader('test2', 'false') .sendRequest() @@ -69,7 +69,7 @@ describe('GetApiRequestBuilder', () => { method: 'GET', cache: 'force-cache' }) - await new GetApiRequestBuilder('test', 'test') + await new GetApiRequestBuilder('test') .withCustomOptions({ cache: 'force-cache' }) @@ -81,7 +81,7 @@ describe('GetApiRequestBuilder', () => { method: 'GET', cache: 'no-store' }) - await new GetApiRequestBuilder('test', 'test') + await new GetApiRequestBuilder('test') .withCustomOptions({ cache: 'force-cache' }) @@ -97,7 +97,7 @@ describe('GetApiRequestBuilder', () => { cache: 'force-cache', integrity: 'test' }) - await new GetApiRequestBuilder('test', 'test') + await new GetApiRequestBuilder('test') .withCustomOptions({ cache: 'force-cache', integrity: 'test' @@ -107,28 +107,28 @@ describe('GetApiRequestBuilder', () => { }) describe('failing sendRequest', () => { - it('with bad request without api error name', async () => { + it('without backend provided error name or error message', async () => { expectFetch('api/private/test', 400, { method: 'GET' }) - const request = new GetApiRequestBuilder('test', 'test').sendRequest() - await expect(request).rejects.toEqual(new ApiError(400, 'unknown', 'test', 'testExplosion')) + const request = new GetApiRequestBuilder('test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, undefined, undefined)) }) - it('with bad request with api error name', async () => { + it('with backend error name and error message', async () => { expectFetch('api/private/test', 400, { method: 'GET' }, { message: 'The API has exploded!', - error: 'testExplosion' + name: 'testExplosion' } as ApiErrorResponse) - const request = new GetApiRequestBuilder('test', 'test').sendRequest() - await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'test', 'testExplosion')) + const request = new GetApiRequestBuilder('test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'The API has exploded!')) }) - it('with non bad request error', async () => { + it('with another status code than 400', async () => { expectFetch('api/private/test', 401, { method: 'GET' }, { message: 'The API has exploded!', - error: 'testExplosion' + name: 'testExplosion' } as ApiErrorResponse) - const request = new GetApiRequestBuilder('test', 'test').sendRequest() - await expect(request).rejects.toEqual(new ApiError(401, 'forbidden', 'test', 'testExplosion')) + const request = new GetApiRequestBuilder('test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(401, 'testExplosion', 'The API has exploded!')) }) }) }) diff --git a/frontend/src/api/common/api-request-builder/post-api-request-builder.spec.ts b/frontend/src/api/common/api-request-builder/post-api-request-builder.spec.ts index e4ee7223e..db145d9ad 100644 --- a/frontend/src/api/common/api-request-builder/post-api-request-builder.spec.ts +++ b/frontend/src/api/common/api-request-builder/post-api-request-builder.spec.ts @@ -22,7 +22,7 @@ describe('PostApiRequestBuilder', () => { describe('sendRequest without body', () => { it('without headers', async () => { expectFetch('api/private/test', 201, { method: 'POST' }) - await new PostApiRequestBuilder('test', 'test').sendRequest() + await new PostApiRequestBuilder('test').sendRequest() }) it('with single header', async () => { @@ -32,7 +32,7 @@ describe('PostApiRequestBuilder', () => { method: 'POST', headers: expectedHeaders }) - await new PostApiRequestBuilder('test', 'test').withHeader('test', 'true').sendRequest() + await new PostApiRequestBuilder('test').withHeader('test', 'true').sendRequest() }) it('with overriding single header', async () => { @@ -42,7 +42,7 @@ describe('PostApiRequestBuilder', () => { method: 'POST', headers: expectedHeaders }) - await new PostApiRequestBuilder('test', 'test') + await new PostApiRequestBuilder('test') .withHeader('test', 'true') .withHeader('test', 'false') .sendRequest() @@ -56,7 +56,7 @@ describe('PostApiRequestBuilder', () => { method: 'POST', headers: expectedHeaders }) - await new PostApiRequestBuilder('test', 'test') + await new PostApiRequestBuilder('test') .withHeader('test', 'true') .withHeader('test2', 'false') .sendRequest() @@ -72,7 +72,7 @@ describe('PostApiRequestBuilder', () => { headers: expectedHeaders, body: '{"test":true,"foo":"bar"}' }) - await new PostApiRequestBuilder('test', 'test') + await new PostApiRequestBuilder('test') .withJsonBody({ test: true, foo: 'bar' @@ -85,7 +85,7 @@ describe('PostApiRequestBuilder', () => { method: 'POST', body: 'HedgeDoc' }) - await new PostApiRequestBuilder('test', 'test').withBody('HedgeDoc').sendRequest() + await new PostApiRequestBuilder('test').withBody('HedgeDoc').sendRequest() }) describe('sendRequest with custom options', () => { @@ -94,7 +94,7 @@ describe('PostApiRequestBuilder', () => { method: 'POST', cache: 'force-cache' }) - await new PostApiRequestBuilder('test', 'test') + await new PostApiRequestBuilder('test') .withCustomOptions({ cache: 'force-cache' }) @@ -106,7 +106,7 @@ describe('PostApiRequestBuilder', () => { method: 'POST', cache: 'no-store' }) - await new PostApiRequestBuilder('test', 'test') + await new PostApiRequestBuilder('test') .withCustomOptions({ cache: 'force-cache' }) @@ -122,7 +122,7 @@ describe('PostApiRequestBuilder', () => { cache: 'force-cache', integrity: 'test' }) - await new PostApiRequestBuilder('test', 'test') + await new PostApiRequestBuilder('test') .withCustomOptions({ cache: 'force-cache', integrity: 'test' @@ -132,28 +132,28 @@ describe('PostApiRequestBuilder', () => { }) describe('failing sendRequest', () => { - it('with bad request without api error name', async () => { + it('without backend provided error name or error message', async () => { expectFetch('api/private/test', 400, { method: 'POST' }) - const request = new PostApiRequestBuilder('test', 'test').sendRequest() - await expect(request).rejects.toEqual(new ApiError(400, 'unknown', 'test', 'testExplosion')) + const request = new PostApiRequestBuilder('test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, undefined, undefined)) }) - it('with bad request with api error name', async () => { + it('with backend error name and error message', async () => { expectFetch('api/private/test', 400, { method: 'POST' }, { message: 'The API has exploded!', - error: 'testExplosion' + name: 'testExplosion' } as ApiErrorResponse) - const request = new PostApiRequestBuilder('test', 'test').sendRequest() - await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'test', 'testExplosion')) + const request = new PostApiRequestBuilder('test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'The API has exploded!')) }) - it('with non bad request error', async () => { + it('with another status code than 400', async () => { expectFetch('api/private/test', 401, { method: 'POST' }, { message: 'The API has exploded!', - error: 'testExplosion' + name: 'testExplosion' } as ApiErrorResponse) - const request = new PostApiRequestBuilder('test', 'test').sendRequest() - await expect(request).rejects.toEqual(new ApiError(401, 'forbidden', 'test', 'testExplosion')) + const request = new PostApiRequestBuilder('test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(401, 'testExplosion', 'The API has exploded!')) }) }) }) diff --git a/frontend/src/api/common/api-request-builder/put-api-request-builder.spec.ts b/frontend/src/api/common/api-request-builder/put-api-request-builder.spec.ts index 97057ae5d..b4b8bcf6f 100644 --- a/frontend/src/api/common/api-request-builder/put-api-request-builder.spec.ts +++ b/frontend/src/api/common/api-request-builder/put-api-request-builder.spec.ts @@ -22,7 +22,7 @@ describe('PutApiRequestBuilder', () => { describe('sendRequest without body', () => { it('without headers', async () => { expectFetch('api/private/test', 200, { method: 'PUT' }) - await new PutApiRequestBuilder('test', 'test').sendRequest() + await new PutApiRequestBuilder('test').sendRequest() }) it('with single header', async () => { @@ -32,7 +32,7 @@ describe('PutApiRequestBuilder', () => { method: 'PUT', headers: expectedHeaders }) - await new PutApiRequestBuilder('test', 'test').withHeader('test', 'true').sendRequest() + await new PutApiRequestBuilder('test').withHeader('test', 'true').sendRequest() }) it('with overriding single header', async () => { @@ -42,7 +42,7 @@ describe('PutApiRequestBuilder', () => { method: 'PUT', headers: expectedHeaders }) - await new PutApiRequestBuilder('test', 'test') + await new PutApiRequestBuilder('test') .withHeader('test', 'true') .withHeader('test', 'false') .sendRequest() @@ -56,7 +56,7 @@ describe('PutApiRequestBuilder', () => { method: 'PUT', headers: expectedHeaders }) - await new PutApiRequestBuilder('test', 'test') + await new PutApiRequestBuilder('test') .withHeader('test', 'true') .withHeader('test2', 'false') .sendRequest() @@ -72,7 +72,7 @@ describe('PutApiRequestBuilder', () => { headers: expectedHeaders, body: '{"test":true,"foo":"bar"}' }) - await new PutApiRequestBuilder('test', 'test') + await new PutApiRequestBuilder('test') .withJsonBody({ test: true, foo: 'bar' @@ -85,7 +85,7 @@ describe('PutApiRequestBuilder', () => { method: 'PUT', body: 'HedgeDoc' }) - await new PutApiRequestBuilder('test', 'test').withBody('HedgeDoc').sendRequest() + await new PutApiRequestBuilder('test').withBody('HedgeDoc').sendRequest() }) describe('sendRequest with custom options', () => { @@ -94,7 +94,7 @@ describe('PutApiRequestBuilder', () => { method: 'PUT', cache: 'force-cache' }) - await new PutApiRequestBuilder('test', 'test') + await new PutApiRequestBuilder('test') .withCustomOptions({ cache: 'force-cache' }) @@ -106,7 +106,7 @@ describe('PutApiRequestBuilder', () => { method: 'PUT', cache: 'no-store' }) - await new PutApiRequestBuilder('test', 'test') + await new PutApiRequestBuilder('test') .withCustomOptions({ cache: 'force-cache' }) @@ -122,7 +122,7 @@ describe('PutApiRequestBuilder', () => { cache: 'force-cache', integrity: 'test' }) - await new PutApiRequestBuilder('test', 'test') + await new PutApiRequestBuilder('test') .withCustomOptions({ cache: 'force-cache', integrity: 'test' @@ -132,28 +132,28 @@ describe('PutApiRequestBuilder', () => { }) describe('failing sendRequest', () => { - it('with bad request without api error name', async () => { + it('without backend provided error name or error message', async () => { expectFetch('api/private/test', 400, { method: 'PUT' }) - const request = new PutApiRequestBuilder('test', 'test').sendRequest() - await expect(request).rejects.toEqual(new ApiError(400, 'unknown', 'test', 'testExplosion')) + const request = new PutApiRequestBuilder('test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, undefined, undefined)) }) - it('with bad request with api error name', async () => { + it('with backend error name and error message', async () => { expectFetch('api/private/test', 400, { method: 'PUT' }, { message: 'The API has exploded!', - error: 'testExplosion' + name: 'testExplosion' } as ApiErrorResponse) - const request = new PutApiRequestBuilder('test', 'test').sendRequest() - await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'test', 'testExplosion')) + const request = new PutApiRequestBuilder('test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'The API has exploded!')) }) - it('with non bad request error', async () => { + it('with another status code than 400', async () => { expectFetch('api/private/test', 401, { method: 'PUT' }, { message: 'The API has exploded!', - error: 'testExplosion' + name: 'testExplosion' } as ApiErrorResponse) - const request = new PutApiRequestBuilder('test', 'test').sendRequest() - await expect(request).rejects.toEqual(new ApiError(401, 'forbidden', 'test', 'testExplosion')) + const request = new PutApiRequestBuilder('test').sendRequest() + await expect(request).rejects.toEqual(new ApiError(401, 'testExplosion', 'The API has exploded!')) }) }) }) diff --git a/frontend/src/api/common/error-to-i18n-key-mapper.ts b/frontend/src/api/common/error-to-i18n-key-mapper.ts index d10b05253..d52dc6bef 100644 --- a/frontend/src/api/common/error-to-i18n-key-mapper.ts +++ b/frontend/src/api/common/error-to-i18n-key-mapper.ts @@ -5,6 +5,9 @@ */ import { ApiError } from './api-error' +/** + * Maps an error to an i18n key. + */ export class ErrorToI18nKeyMapper { private foundI18nKey: string | undefined = undefined @@ -21,7 +24,7 @@ export class ErrorToI18nKeyMapper { if ( this.foundI18nKey === undefined && this.apiError instanceof ApiError && - this.apiError.apiErrorName === errorName + this.apiError.backendErrorName === errorName ) { this.foundI18nKey = i18nKey } @@ -35,12 +38,10 @@ export class ErrorToI18nKeyMapper { 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 + public orFallbackI18nKey(fallback: T): string | T { + if (!this.foundI18nKey) { + return fallback } + return this.i18nNamespace ? `${this.i18nNamespace}.${this.foundI18nKey}` : this.foundI18nKey } } diff --git a/frontend/src/api/config/index.ts b/frontend/src/api/config/index.ts index f60a2234a..a93e5f81f 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', 'config').sendRequest() + const response = await new GetApiRequestBuilder('config').sendRequest() return response.asParsedJsonObject() } diff --git a/frontend/src/api/group/index.ts b/frontend/src/api/group/index.ts index ab35f0d39..88ed99bb5 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, 'group').sendRequest() + const response = await new GetApiRequestBuilder('groups/' + groupName).sendRequest() return response.asParsedJsonObject() } diff --git a/frontend/src/api/history/index.ts b/frontend/src/api/history/index.ts index f9273cdd0..3ef125d11 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', 'history').sendRequest() + const response = await new GetApiRequestBuilder('me/history').sendRequest() return response.asParsedJsonObject() } @@ -27,9 +27,7 @@ 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', 'history') - .withJsonBody(entries) - .sendRequest() + await new PostApiRequestBuilder('me/history').withJsonBody(entries).sendRequest() } /** @@ -43,10 +41,7 @@ export const updateRemoteHistoryEntryPinStatus = async ( noteIdOrAlias: string, pinStatus: boolean ): Promise => { - const response = await new PutApiRequestBuilder( - 'me/history/' + noteIdOrAlias, - 'history' - ) + const response = await new PutApiRequestBuilder('me/history/' + noteIdOrAlias) .withJsonBody({ pinStatus }) @@ -61,7 +56,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, 'history').sendRequest() + await new DeleteApiRequestBuilder('me/history/' + noteIdOrAlias).sendRequest() } /** @@ -70,5 +65,5 @@ export const deleteRemoteHistoryEntry = async (noteIdOrAlias: string): Promise => { - await new DeleteApiRequestBuilder('me/history', 'history').sendRequest() + await new DeleteApiRequestBuilder('me/history').sendRequest() } diff --git a/frontend/src/api/me/index.ts b/frontend/src/api/me/index.ts index 7ea1fa788..7ca88e50b 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', 'me').sendRequest() + const response = await new GetApiRequestBuilder('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', 'me').sendRequest() + await new DeleteApiRequestBuilder('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', 'me') + await new PostApiRequestBuilder('me/profile') .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', 'me').sendRequest() + const response = await new GetApiRequestBuilder('me/media').sendRequest() return response.asParsedJsonObject() } diff --git a/frontend/src/api/media/index.ts b/frontend/src/api/media/index.ts index 0b76170ce..1f1b9e8c8 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', 'media') + const response = await new PostApiRequestBuilder('media/proxy') .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', 'media') + const response = await new PostApiRequestBuilder('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, 'media').sendRequest() + await new DeleteApiRequestBuilder('media/' + mediaId).sendRequest() } diff --git a/frontend/src/api/notes/index.ts b/frontend/src/api/notes/index.ts index 840dcc79c..6684005b1 100644 --- a/frontend/src/api/notes/index.ts +++ b/frontend/src/api/notes/index.ts @@ -17,7 +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, 'note').sendRequest() + const response = await new GetApiRequestBuilder('notes/' + noteIdOrAlias).sendRequest() return response.asParsedJsonObject() } @@ -28,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`, 'note').sendRequest() + const response = await new GetApiRequestBuilder(`notes/${noteIdOrAlias}/metadata`).sendRequest() return response.asParsedJsonObject() } @@ -40,7 +40,7 @@ export const getNoteMetadata = async (noteIdOrAlias: string): Promise => { - const response = await new GetApiRequestBuilder(`notes/${noteIdOrAlias}/media`, 'note').sendRequest() + const response = await new GetApiRequestBuilder(`notes/${noteIdOrAlias}/media`).sendRequest() return response.asParsedJsonObject() } @@ -52,7 +52,7 @@ export const getMediaForNote = async (noteIdOrAlias: string): Promise => { - const response = await new PostApiRequestBuilder('notes', 'note') + const response = await new PostApiRequestBuilder('notes') .withHeader('Content-Type', 'text/markdown') .withBody(markdown) .sendRequest() @@ -68,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, 'note') + const response = await new PostApiRequestBuilder('notes/' + primaryAlias) .withHeader('Content-Type', 'text/markdown') .withBody(markdown) .sendRequest() @@ -82,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, 'note') + await new DeleteApiRequestBuilder('notes/' + noteIdOrAlias) .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 c93a666f3..d6a6c2647 100644 --- a/frontend/src/api/permissions/index.ts +++ b/frontend/src/api/permissions/index.ts @@ -18,8 +18,7 @@ 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`, - 'permission' + `notes/${noteId}/metadata/permissions/owner` ) .withJsonBody({ owner @@ -43,8 +42,7 @@ export const setUserPermission = async ( canEdit: boolean ): Promise => { const response = await new PutApiRequestBuilder( - `notes/${noteId}/metadata/permissions/users/${username}`, - 'permission' + `notes/${noteId}/metadata/permissions/users/${username}` ) .withJsonBody({ canEdit @@ -68,8 +66,7 @@ export const setGroupPermission = async ( canEdit: boolean ): Promise => { const response = await new PutApiRequestBuilder( - `notes/${noteId}/metadata/permissions/groups/${groupName}`, - 'permission' + `notes/${noteId}/metadata/permissions/groups/${groupName}` ) .withJsonBody({ canEdit @@ -88,8 +85,7 @@ export const setGroupPermission = async ( */ export const removeUserPermission = async (noteId: string, username: string): Promise => { const response = await new DeleteApiRequestBuilder( - `notes/${noteId}/metadata/permissions/users/${username}`, - 'permission' + `notes/${noteId}/metadata/permissions/users/${username}` ).sendRequest() return response.asParsedJsonObject() } @@ -104,8 +100,7 @@ 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}`, - 'permission' + `notes/${noteId}/metadata/permissions/groups/${groupName}` ).sendRequest() return response.asParsedJsonObject() } diff --git a/frontend/src/api/revisions/index.ts b/frontend/src/api/revisions/index.ts index a38c2f91f..62b98f941 100644 --- a/frontend/src/api/revisions/index.ts +++ b/frontend/src/api/revisions/index.ts @@ -17,8 +17,7 @@ import type { RevisionDetails, RevisionMetadata } from './types' */ export const getRevision = async (noteId: string, revisionId: number): Promise => { const response = await new GetApiRequestBuilder( - `notes/${noteId}/revisions/${revisionId}`, - 'revisions' + `notes/${noteId}/revisions/${revisionId}` ).sendRequest() return response.asParsedJsonObject() } @@ -31,10 +30,7 @@ export const getRevision = async (noteId: string, revisionId: number): Promise => { - const response = await new GetApiRequestBuilder( - `notes/${noteId}/revisions`, - 'revisions' - ).sendRequest() + const response = await new GetApiRequestBuilder(`notes/${noteId}/revisions`).sendRequest() return response.asParsedJsonObject() } @@ -45,5 +41,5 @@ export const getAllRevisions = async (noteId: string): Promise => { - await new DeleteApiRequestBuilder(`notes/${noteIdOrAlias}/revisions`, 'revisions').sendRequest() + await new DeleteApiRequestBuilder(`notes/${noteIdOrAlias}/revisions`).sendRequest() } diff --git a/frontend/src/api/tokens/index.ts b/frontend/src/api/tokens/index.ts index 04673b5c0..fd335aee9 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', 'tokens').sendRequest() + const response = await new GetApiRequestBuilder('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', 'tokens') + const response = await new PostApiRequestBuilder('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, 'tokens').sendRequest() + await new DeleteApiRequestBuilder('tokens/' + keyId).sendRequest() } diff --git a/frontend/src/api/users/index.ts b/frontend/src/api/users/index.ts index ef9f9d583..d785d2f73 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, 'users').sendRequest() + const response = await new GetApiRequestBuilder('users/' + username).sendRequest() return response.asParsedJsonObject() } diff --git a/frontend/src/components/common/fields/new-password-field.tsx b/frontend/src/components/common/fields/new-password-field.tsx index 92db18706..c182e787e 100644 --- a/frontend/src/components/common/fields/new-password-field.tsx +++ b/frontend/src/components/common/fields/new-password-field.tsx @@ -18,7 +18,7 @@ export const NewPasswordField: React.FC = ({ onChange, value, const { t } = useTranslation() const isValid = useMemo(() => { - return value.trim() !== '' && value.length >= 8 + return value.trim() !== '' }, [value]) return ( @@ -34,7 +34,6 @@ export const NewPasswordField: React.FC = ({ onChange, value, onChange={onChange} placeholder={t('login.auth.password') ?? undefined} className='bg-dark text-light' - minLength={8} autoComplete='new-password' required /> diff --git a/frontend/src/components/common/note-loading-boundary/__snapshots__/note-loading-boundary.spec.tsx.snap b/frontend/src/components/common/note-loading-boundary/__snapshots__/note-loading-boundary.spec.tsx.snap index 917fbdb0f..a4061f5d8 100644 --- a/frontend/src/components/common/note-loading-boundary/__snapshots__/note-loading-boundary.spec.tsx.snap +++ b/frontend/src/components/common/note-loading-boundary/__snapshots__/note-loading-boundary.spec.tsx.snap @@ -19,14 +19,17 @@ exports[`Note loading boundary shows an error 1`] = ` titleI18nKey: - api.note.notFound.title + noteLoadingBoundary.error.notFound.title descriptionI18nKey: - api.note.notFound.description + noteLoadingBoundary.error.notFound.description children: + + This is a mock for CreateNonExistingNoteHint + `; diff --git a/frontend/src/components/common/note-loading-boundary/note-loading-boundary.spec.tsx b/frontend/src/components/common/note-loading-boundary/note-loading-boundary.spec.tsx index 7dab85b89..10c676bec 100644 --- a/frontend/src/components/common/note-loading-boundary/note-loading-boundary.spec.tsx +++ b/frontend/src/components/common/note-loading-boundary/note-loading-boundary.spec.tsx @@ -3,6 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { ApiError } from '../../../api/common/api-error' import * as getNoteModule from '../../../api/notes' import type { Note } from '../../../api/notes/types' import * as LoadingScreenModule from '../../../components/application-loader/loading-screen/loading-screen' @@ -87,7 +88,7 @@ describe('Note loading boundary', () => { jest.spyOn(getNoteModule, 'getNote').mockImplementation((id) => { expect(id).toBe(mockedNoteId) return new Promise((resolve, reject) => { - setTimeout(() => reject(new Error('api.note.notFound')), 0) + setTimeout(() => reject(new ApiError(404, undefined, undefined)), 0) }) }) } 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 3f155f4ef..30f17df89 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 @@ -3,6 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { ApiError } from '../../../api/common/api-error' +import { ErrorToI18nKeyMapper } from '../../../api/common/error-to-i18n-key-mapper' import { LoadingScreen } from '../../application-loader/loading-screen/loading-screen' import { CommonErrorPage } from '../../error-pages/common-error-page' import { CustomAsyncLoadingBoundary } from '../async-loading-boundary/custom-async-loading-boundary' @@ -28,11 +30,18 @@ export const NoteLoadingBoundary: React.FC = ({ children }) = const errorComponent = useMemo(() => { if (error === undefined) { - return <> + return null } + const errorI18nKeyPrefix = new ErrorToI18nKeyMapper(error, 'noteLoadingBoundary.error') + .withHttpCode(404, 'notFound') + .withHttpCode(403, 'forbidden') + .withHttpCode(401, 'forbidden') + .orFallbackI18nKey('other') return ( - - + + diff --git a/frontend/src/components/login-page/auth/via-local.tsx b/frontend/src/components/login-page/auth/via-local.tsx index 622fa4454..3e74b6549 100644 --- a/frontend/src/components/login-page/auth/via-local.tsx +++ b/frontend/src/components/login-page/auth/via-local.tsx @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { doLocalLogin } from '../../../api/auth/local' +import { ErrorToI18nKeyMapper } from '../../../api/common/error-to-i18n-key-mapper' import { useApplicationState } from '../../../hooks/common/use-application-state' import { useOnInputChange } from '../../../hooks/common/use-on-input-change' import { ShowIf } from '../../common/show-if/show-if' @@ -30,7 +31,14 @@ export const ViaLocal: React.FC = () => { (event: FormEvent) => { doLocalLogin(username, password) .then(() => fetchAndSetUser()) - .catch((error: Error) => setError(error.message)) + .catch((error: Error) => { + const errorI18nKey = new ErrorToI18nKeyMapper(error, 'login.auth.error') + .withHttpCode(404, 'usernamePassword') + .withHttpCode(401, 'usernamePassword') + .withBackendErrorName('FeatureDisabledError', 'loginDisabled') + .orFallbackI18nKey('other') + setError(errorI18nKey) + }) event.preventDefault() }, [username, password] @@ -45,25 +53,23 @@ export const ViaLocal: React.FC = () => { -
+ - -
- - - - - - -
+ + + + + + + diff --git a/frontend/src/components/profile-page/settings/profile-change-password.tsx b/frontend/src/components/profile-page/settings/profile-change-password.tsx index 65180cd59..d2724d732 100644 --- a/frontend/src/components/profile-page/settings/profile-change-password.tsx +++ b/frontend/src/components/profile-page/settings/profile-change-password.tsx @@ -37,8 +37,8 @@ export const ProfileChangePassword: React.FC = () => { } catch (error) { const foundI18nKey = new ErrorToI18nKeyMapper(error as Error, 'login.auth.error') .withHttpCode(401, 'invalidCredentials') - .withBackendErrorName('loginDisabled', 'loginDisabled') - .withBackendErrorName('passwordTooWeak', 'passwordTooWeak') + .withBackendErrorName('FeatureDisabledError', 'loginDisabled') + .withBackendErrorName('PasswordTooWeakError', 'passwordTooWeak') .orFallbackI18nKey('other') return Promise.reject(foundI18nKey) } finally { diff --git a/frontend/src/components/register-page/register-error.tsx b/frontend/src/components/register-page/register-error.tsx index 243893367..ca52bced6 100644 --- a/frontend/src/components/register-page/register-error.tsx +++ b/frontend/src/components/register-page/register-error.tsx @@ -21,8 +21,8 @@ export const RegisterError: React.FC = ({ error }) => { } return new ErrorToI18nKeyMapper(error, 'login.register.error') .withHttpCode(409, 'usernameExisting') - .withBackendErrorName('registrationDisabled', 'registrationDisabled') - .withBackendErrorName('passwordTooWeak', 'passwordTooWeak') + .withBackendErrorName('FeatureDisabledError', 'registrationDisabled') + .withBackendErrorName('PasswordTooWeakError', 'passwordTooWeak') .orFallbackI18nKey('other') }, [error]) diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index dcb0cac5c..3539b3170 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -60,7 +60,7 @@ export const LoginPage: React.FC = () => { return (
- + 0 || localLoginEnabled}> diff --git a/frontend/src/pages/register.tsx b/frontend/src/pages/register.tsx index e4862c7ed..bb999d893 100644 --- a/frontend/src/pages/register.tsx +++ b/frontend/src/pages/register.tsx @@ -54,17 +54,11 @@ export const RegisterPage: NextPage = () => { ) const ready = useMemo(() => { - return ( - username.trim() !== '' && - displayName.trim() !== '' && - password.trim() !== '' && - password.length >= 8 && - password === passwordAgain - ) + return username.trim() !== '' && displayName.trim() !== '' && password.trim() !== '' && password === passwordAgain }, [username, password, displayName, passwordAgain]) const isWeakPassword = useMemo(() => { - return error?.apiErrorName === 'passwordTooWeak' + return error?.backendErrorName === 'PasswordTooWeakError' }, [error]) const onUsernameChange = useOnInputChange(setUsername) @@ -90,7 +84,7 @@ export const RegisterPage: NextPage = () => { -
+ @@ -106,9 +100,9 @@ export const RegisterPage: NextPage = () => { - - + +