From 0588a30c886169daf2d48fa8c22e011c4e705d6c Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 30 Oct 2025 19:27:29 +0200 Subject: [PATCH] fix: auth pages errors handler --- .../commands/AuthSendResetPassword.service.ts | 13 ++++---- .../Auth/commands/AuthSignin.service.ts | 18 ++++------- .../Auth/commands/AuthSignup.service.ts | 3 +- .../InvalidEmailPassword.exception.ts | 15 +++++++++ .../Auth/exceptions/UserNotFound.exception.ts | 13 ++++++++ .../src/modules/Roles/Authorization.guard.ts | 6 ++-- .../src/containers/Authentication/Login.tsx | 10 +++--- .../Authentication/SendResetPassword.tsx | 29 +++++++---------- .../containers/Authentication/_components.tsx | 7 +++- .../src/containers/Authentication/utils.tsx | 22 +++---------- .../components/CompanyLogoUpload.module.scss | 2 +- .../components/CompanyLogoUpload.tsx | 1 - .../webapp/src/hooks/query/authentication.tsx | 15 ++++----- packages/webapp/src/hooks/useRequest.tsx | 32 +++++++++++++++++++ 14 files changed, 111 insertions(+), 75 deletions(-) create mode 100644 packages/server/src/modules/Auth/exceptions/InvalidEmailPassword.exception.ts create mode 100644 packages/server/src/modules/Auth/exceptions/UserNotFound.exception.ts diff --git a/packages/server/src/modules/Auth/commands/AuthSendResetPassword.service.ts b/packages/server/src/modules/Auth/commands/AuthSendResetPassword.service.ts index 90a6b3191..0fe2725e2 100644 --- a/packages/server/src/modules/Auth/commands/AuthSendResetPassword.service.ts +++ b/packages/server/src/modules/Auth/commands/AuthSendResetPassword.service.ts @@ -24,7 +24,7 @@ export class AuthSendResetPasswordService { @Inject(SystemUser.name) private readonly systemUserModel: typeof SystemUser, - ) {} + ) { } /** * Sends the given email reset password email. @@ -33,8 +33,9 @@ export class AuthSendResetPasswordService { async sendResetPassword(email: string): Promise { const user = await this.systemUserModel .query() - .findOne({ email }) - .throwIfNotFound(); + .findOne({ email }); + + if (!user) return; const token: string = uniqid(); @@ -48,10 +49,8 @@ export class AuthSendResetPasswordService { this.deletePasswordResetToken(email); // Creates a new password reset row with unique token. - const passwordReset = await this.resetPasswordModel.query().insert({ - email, - token, - }); + await this.resetPasswordModel.query().insert({ email, token }); + // Triggers sent reset password event. await this.eventPublisher.emitAsync(events.auth.sendResetPassword, { user, diff --git a/packages/server/src/modules/Auth/commands/AuthSignin.service.ts b/packages/server/src/modules/Auth/commands/AuthSignin.service.ts index 0ca89fe6f..48694d3ea 100644 --- a/packages/server/src/modules/Auth/commands/AuthSignin.service.ts +++ b/packages/server/src/modules/Auth/commands/AuthSignin.service.ts @@ -1,9 +1,11 @@ import { ClsService } from 'nestjs-cls'; -import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { SystemUser } from '@/modules/System/models/SystemUser'; import { ModelObject } from 'objection'; import { JwtPayload } from '../Auth.interfaces'; +import { InvalidEmailPasswordException } from '../exceptions/InvalidEmailPassword.exception'; +import { UserNotFoundException } from '../exceptions/UserNotFound.exception'; @Injectable() export class AuthSigninService { @@ -12,7 +14,7 @@ export class AuthSigninService { private readonly systemUserModel: typeof SystemUser, private readonly jwtService: JwtService, private readonly clsService: ClsService, - ) {} + ) { } /** * Validates the given email and password. @@ -32,14 +34,10 @@ export class AuthSigninService { .findOne({ email }) .throwIfNotFound(); } catch (err) { - throw new UnauthorizedException( - `There isn't any user with email: ${email}`, - ); + throw new InvalidEmailPasswordException(email); } if (!(await user.checkPassword(password))) { - throw new UnauthorizedException( - `Wrong password for user with email: ${email}`, - ); + throw new InvalidEmailPasswordException(email); } return user; } @@ -61,9 +59,7 @@ export class AuthSigninService { this.clsService.set('tenantId', user.tenantId); this.clsService.set('userId', user.id); } catch (error) { - throw new UnauthorizedException( - `There isn't any user with email: ${payload.sub}`, - ); + throw new UserNotFoundException(String(payload.sub)); } return payload; } diff --git a/packages/server/src/modules/Auth/commands/AuthSignup.service.ts b/packages/server/src/modules/Auth/commands/AuthSignup.service.ts index 50d9020b3..9eb595e92 100644 --- a/packages/server/src/modules/Auth/commands/AuthSignup.service.ts +++ b/packages/server/src/modules/Auth/commands/AuthSignup.service.ts @@ -32,7 +32,7 @@ export class AuthSignupService { @Inject(SystemUser.name) private readonly systemUserModel: typeof SystemUser, - ) {} + ) { } /** * Registers a new tenant with user from user input. @@ -121,7 +121,6 @@ export class AuthSignupService { const isAllowedDomain = signupRestrictions.allowedDomains.some( (domain) => emailDomain === domain, ); - if (!isAllowedEmail && !isAllowedDomain) { throw new ServiceError( ERRORS.SIGNUP_RESTRICTED_NOT_ALLOWED, diff --git a/packages/server/src/modules/Auth/exceptions/InvalidEmailPassword.exception.ts b/packages/server/src/modules/Auth/exceptions/InvalidEmailPassword.exception.ts new file mode 100644 index 000000000..15626b1b4 --- /dev/null +++ b/packages/server/src/modules/Auth/exceptions/InvalidEmailPassword.exception.ts @@ -0,0 +1,15 @@ +import { UnauthorizedException } from '@nestjs/common'; +import { ERRORS } from '../Auth.constants'; + +export class InvalidEmailPasswordException extends UnauthorizedException { + constructor(email: string) { + super({ + statusCode: 401, + error: 'Unauthorized', + message: `Invalid email or password for ${email}`, + code: ERRORS.INVALID_DETAILS, + }); + } +} + + diff --git a/packages/server/src/modules/Auth/exceptions/UserNotFound.exception.ts b/packages/server/src/modules/Auth/exceptions/UserNotFound.exception.ts new file mode 100644 index 000000000..6e9981fa0 --- /dev/null +++ b/packages/server/src/modules/Auth/exceptions/UserNotFound.exception.ts @@ -0,0 +1,13 @@ +import { UnauthorizedException } from '@nestjs/common'; +import { ERRORS } from '../Auth.constants'; + +export class UserNotFoundException extends UnauthorizedException { + constructor(identifier: string) { + super({ + statusCode: 401, + error: 'Unauthorized', + message: `User not found: ${identifier}`, + code: ERRORS.USER_NOT_FOUND, + }); + } +} diff --git a/packages/server/src/modules/Roles/Authorization.guard.ts b/packages/server/src/modules/Roles/Authorization.guard.ts index b9995371d..c6bf3bdba 100644 --- a/packages/server/src/modules/Roles/Authorization.guard.ts +++ b/packages/server/src/modules/Roles/Authorization.guard.ts @@ -17,12 +17,11 @@ import { TenantUser } from '../Tenancy/TenancyModels/models/TenantUser.model'; @Injectable() export class AuthorizationGuard implements CanActivate { constructor( - private reflector: Reflector, private readonly clsService: ClsService, @Inject(TenantUser.name) private readonly tenantUserModel: TenantModelProxy, - ) {} + ) { } /** * Checks if the user has the required abilities to access the route @@ -31,7 +30,7 @@ export class AuthorizationGuard implements CanActivate { */ async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); - const { tenantId, user } = request as any; + const { user } = request as any; if (ABILITIES_CACHE.has(user.id)) { (request as any).ability = ABILITIES_CACHE.get(user.id); @@ -40,7 +39,6 @@ export class AuthorizationGuard implements CanActivate { (request as any).ability = ability; ABILITIES_CACHE.set(user.id, ability); } - return true; } diff --git a/packages/webapp/src/containers/Authentication/Login.tsx b/packages/webapp/src/containers/Authentication/Login.tsx index d904d05c2..7a2ac4a92 100644 --- a/packages/webapp/src/containers/Authentication/Login.tsx +++ b/packages/webapp/src/containers/Authentication/Login.tsx @@ -32,13 +32,11 @@ export default function Login() { email: values.crediential, password: values.password, }).catch(({ response }) => { - const { - data: { errors }, - } = response; - const toastBuilders = transformLoginErrorsToToasts(errors); + const { data: error } = response; + const toastMessages = transformLoginErrorsToToasts(error); - toastBuilders.forEach((builder) => { - Toaster.show(builder); + toastMessages.forEach((toastMessage) => { + Toaster.show(toastMessage); }); setSubmitting(false); }); diff --git a/packages/webapp/src/containers/Authentication/SendResetPassword.tsx b/packages/webapp/src/containers/Authentication/SendResetPassword.tsx index b8f24831c..fb8578f85 100644 --- a/packages/webapp/src/containers/Authentication/SendResetPassword.tsx +++ b/packages/webapp/src/containers/Authentication/SendResetPassword.tsx @@ -35,7 +35,7 @@ export default function SendResetPassword() { // Handle form submitting. const handleSubmit = (values, { setSubmitting }) => { sendResetPasswordMutate({ email: values.crediential }) - .then((response) => { + .then(() => { AppToaster.show({ message: intl.get('check_your_email_for_a_link_to_reset'), intent: Intent.SUCCESS, @@ -43,20 +43,9 @@ export default function SendResetPassword() { history.push('/auth/login'); setSubmitting(false); }) - .catch( - ({ - response: { - data: { errors }, - }, - }) => { - const toastBuilders = transformSendResetPassErrorsToToasts(errors); - - toastBuilders.forEach((builder) => { - AppToaster.show(builder); - }); - setSubmitting(false); - }, - ); + .catch(() => { + setSubmitting(false); + }); }; return ( @@ -82,11 +71,17 @@ function SendResetPasswordFooterLinks() { {!signupDisabled && ( - + {' '} + + + )} - + {' '} + + + ); diff --git a/packages/webapp/src/containers/Authentication/_components.tsx b/packages/webapp/src/containers/Authentication/_components.tsx index 3f62794aa..c73d50e28 100644 --- a/packages/webapp/src/containers/Authentication/_components.tsx +++ b/packages/webapp/src/containers/Authentication/_components.tsx @@ -13,12 +13,17 @@ export function AuthenticationLoadingOverlay() { } const AuthOverlayRoot = styled.div` + --x-color-background: rgba(252, 253, 255, 0.5); + + .bp4-dark & { + --x-color-background: rgba(37, 42, 49, 0.60); + } position: absolute; top: 0; left: 0; bottom: 0; right: 0; - background: rgba(252, 253, 255, 0.5); + background: var(--x-color-background); display: flex; justify-content: center; `; diff --git a/packages/webapp/src/containers/Authentication/utils.tsx b/packages/webapp/src/containers/Authentication/utils.tsx index c5ad00449..9c2602784 100644 --- a/packages/webapp/src/containers/Authentication/utils.tsx +++ b/packages/webapp/src/containers/Authentication/utils.tsx @@ -45,10 +45,10 @@ export const InviteAcceptSchema = Yup.object().shape({ password: Yup.string().min(4).required().label(intl.get('password')), }); -export const transformSendResetPassErrorsToToasts = (errors) => { +export const transformSendResetPassErrorsToToasts = (error) => { const toastBuilders = []; - if (errors.find((e) => e.type === 'EMAIL.NOT.REGISTERED')) { + if (error.code === ERRORS.EMAIL_NOT_REGISTERED) { toastBuilders.push({ message: intl.get('we_couldn_t_find_your_account_with_that_email'), intent: Intent.DANGER, @@ -57,38 +57,26 @@ export const transformSendResetPassErrorsToToasts = (errors) => { return toastBuilders; }; -export const transformLoginErrorsToToasts = (errors) => { +export const transformLoginErrorsToToasts = (error) => { const toastBuilders = []; - if (errors.find((e) => e.type === LOGIN_ERRORS.INVALID_DETAILS)) { + if (error.code === LOGIN_ERRORS.INVALID_DETAILS) { toastBuilders.push({ message: intl.get('email_and_password_entered_did_not_match'), intent: Intent.DANGER, }); - } - if (errors.find((e) => e.type === LOGIN_ERRORS.USER_INACTIVE)) { + } else if (error.code === LOGIN_ERRORS.USER_INACTIVE) { toastBuilders.push({ message: intl.get('the_user_has_been_suspended_from_admin'), intent: Intent.DANGER, }); } - if (errors.find((e) => e.type === LOGIN_ERRORS.LOGIN_TO_MANY_ATTEMPTS)) { - toastBuilders.push({ - message: intl.get('your_account_has_been_locked'), - intent: Intent.DANGER, - }); - } return toastBuilders; }; export const transformRegisterErrorsToForm = (errors) => { const formErrors = {}; - if (errors.some((e) => e.type === REGISTER_ERRORS.PHONE_NUMBER_EXISTS)) { - formErrors.phone_number = intl.get( - 'the_phone_number_already_used_in_another_account', - ); - } if (errors.some((e) => e.type === REGISTER_ERRORS.EMAIL_EXISTS)) { formErrors.email = intl.get('the_email_already_used_in_another_account'); } diff --git a/packages/webapp/src/containers/ElementCustomize/components/CompanyLogoUpload.module.scss b/packages/webapp/src/containers/ElementCustomize/components/CompanyLogoUpload.module.scss index bb1331672..47f87aa0a 100644 --- a/packages/webapp/src/containers/ElementCustomize/components/CompanyLogoUpload.module.scss +++ b/packages/webapp/src/containers/ElementCustomize/components/CompanyLogoUpload.module.scss @@ -2,7 +2,7 @@ --x-border-color: #E1E1E1; --x-color-placeholder-text: #738091; - .bp4-dark & { + :global(.bp4-dark) & { --x-border-color: rgba(225, 225, 225, 0.15); --x-color-placeholder-text: rgba(225, 225, 225, 0.65); } diff --git a/packages/webapp/src/containers/ElementCustomize/components/CompanyLogoUpload.tsx b/packages/webapp/src/containers/ElementCustomize/components/CompanyLogoUpload.tsx index a5b3ec195..54fcfc9c7 100644 --- a/packages/webapp/src/containers/ElementCustomize/components/CompanyLogoUpload.tsx +++ b/packages/webapp/src/containers/ElementCustomize/components/CompanyLogoUpload.tsx @@ -53,7 +53,6 @@ export function CompanyLogoUpload({ const [initialLocalPreview, setInitialLocalPreview] = useState( initialPreview || null, ); - const openRef = useRef<() => void>(null); const handleRemove = () => { diff --git a/packages/webapp/src/hooks/query/authentication.tsx b/packages/webapp/src/hooks/query/authentication.tsx index 9235cfe81..657729d31 100644 --- a/packages/webapp/src/hooks/query/authentication.tsx +++ b/packages/webapp/src/hooks/query/authentication.tsx @@ -1,7 +1,7 @@ // @ts-nocheck import { useMutation } from 'react-query'; import { batch } from 'react-redux'; -import useApiRequest from '../useRequest'; +import useApiRequest, { useAuthApiRequest } from '../useRequest'; import { setCookie } from '../../utils'; import { useRequestQuery } from '../useQueryRequest'; import t from './types'; @@ -40,7 +40,7 @@ export function setAuthLoginCookies(data) { * Authentication login. */ export const useAuthLogin = (props) => { - const apiRequest = useApiRequest(); + const apiRequest = useAuthApiRequest(); const setAuthToken = useSetAuthToken(); const setOrganizationId = useSetOrganizationId(); @@ -49,7 +49,6 @@ export const useAuthLogin = (props) => { const setLocale = useSetLocale(); return useMutation((values) => apiRequest.post(AuthRoute.Signin, values), { - select: (res) => res.data, onSuccess: (res) => { // Set authentication cookies. setAuthLoginCookies(res.data); @@ -75,7 +74,7 @@ export const useAuthLogin = (props) => { * Authentication register. */ export const useAuthRegister = (props) => { - const apiRequest = useApiRequest(); + const apiRequest = useAuthApiRequest(); return useMutation( (values) => apiRequest.post(AuthRoute.Signup, values), @@ -87,7 +86,7 @@ export const useAuthRegister = (props) => { * Authentication send reset password. */ export const useAuthSendResetPassword = (props) => { - const apiRequest = useApiRequest(); + const apiRequest = useAuthApiRequest(); return useMutation( (values) => apiRequest.post(AuthRoute.SendResetPassword, values), @@ -99,7 +98,7 @@ export const useAuthSendResetPassword = (props) => { * Authentication reset password. */ export const useAuthResetPassword = (props) => { - const apiRequest = useApiRequest(); + const apiRequest = useAuthApiRequest(); return useMutation( ([token, values]) => apiRequest.post(`auth/reset/${token}`, values), @@ -129,7 +128,7 @@ export const useAuthMetadata = (props = {}) => { * Resend the mail of signup verification. */ export const useAuthSignUpVerifyResendMail = (props) => { - const apiRequest = useApiRequest(); + const apiRequest = useAuthApiRequest(); return useMutation( () => apiRequest.post(AuthRoute.SignupVerifyResend), @@ -146,7 +145,7 @@ interface AuthSignUpVerifyValues { * Signup verification. */ export const useAuthSignUpVerify = (props) => { - const apiRequest = useApiRequest(); + const apiRequest = useAuthApiRequest(); return useMutation( (values: AuthSignUpVerifyValues) => diff --git a/packages/webapp/src/hooks/useRequest.tsx b/packages/webapp/src/hooks/useRequest.tsx index b3d249674..68cf065e1 100644 --- a/packages/webapp/src/hooks/useRequest.tsx +++ b/packages/webapp/src/hooks/useRequest.tsx @@ -120,3 +120,35 @@ export default function useApiRequest() { [http], ); } + +export function useAuthApiRequest() { + const http = React.useMemo(() => { + // Axios instance. + return axios.create(); + }, []); + + return React.useMemo( + () => ({ + http, + get(resource, params) { + return http.get(`/api/${resource}`, params); + }, + post(resource, params, config) { + return http.post(`/api/${resource}`, params, config); + }, + update(resource, slug, params) { + return http.put(`/api/${resource}/${slug}`, params); + }, + put(resource, params) { + return http.put(`/api/${resource}`, params); + }, + patch(resource, params, config) { + return http.patch(`/api/${resource}`, params, config); + }, + delete(resource, params) { + return http.delete(`/api/${resource}`, params); + }, + }), + [http], + ); +} \ No newline at end of file