fix: auth pages errors handler

This commit is contained in:
Ahmed Bouhuolia
2025-10-30 19:27:29 +02:00
parent 4a0091d3f8
commit 0588a30c88
14 changed files with 111 additions and 75 deletions

View File

@@ -24,7 +24,7 @@ export class AuthSendResetPasswordService {
@Inject(SystemUser.name) @Inject(SystemUser.name)
private readonly systemUserModel: typeof SystemUser, private readonly systemUserModel: typeof SystemUser,
) {} ) { }
/** /**
* Sends the given email reset password email. * Sends the given email reset password email.
@@ -33,8 +33,9 @@ export class AuthSendResetPasswordService {
async sendResetPassword(email: string): Promise<void> { async sendResetPassword(email: string): Promise<void> {
const user = await this.systemUserModel const user = await this.systemUserModel
.query() .query()
.findOne({ email }) .findOne({ email });
.throwIfNotFound();
if (!user) return;
const token: string = uniqid(); const token: string = uniqid();
@@ -48,10 +49,8 @@ export class AuthSendResetPasswordService {
this.deletePasswordResetToken(email); this.deletePasswordResetToken(email);
// Creates a new password reset row with unique token. // Creates a new password reset row with unique token.
const passwordReset = await this.resetPasswordModel.query().insert({ await this.resetPasswordModel.query().insert({ email, token });
email,
token,
});
// Triggers sent reset password event. // Triggers sent reset password event.
await this.eventPublisher.emitAsync(events.auth.sendResetPassword, { await this.eventPublisher.emitAsync(events.auth.sendResetPassword, {
user, user,

View File

@@ -1,9 +1,11 @@
import { ClsService } from 'nestjs-cls'; import { ClsService } from 'nestjs-cls';
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { SystemUser } from '@/modules/System/models/SystemUser'; import { SystemUser } from '@/modules/System/models/SystemUser';
import { ModelObject } from 'objection'; import { ModelObject } from 'objection';
import { JwtPayload } from '../Auth.interfaces'; import { JwtPayload } from '../Auth.interfaces';
import { InvalidEmailPasswordException } from '../exceptions/InvalidEmailPassword.exception';
import { UserNotFoundException } from '../exceptions/UserNotFound.exception';
@Injectable() @Injectable()
export class AuthSigninService { export class AuthSigninService {
@@ -12,7 +14,7 @@ export class AuthSigninService {
private readonly systemUserModel: typeof SystemUser, private readonly systemUserModel: typeof SystemUser,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly clsService: ClsService, private readonly clsService: ClsService,
) {} ) { }
/** /**
* Validates the given email and password. * Validates the given email and password.
@@ -32,14 +34,10 @@ export class AuthSigninService {
.findOne({ email }) .findOne({ email })
.throwIfNotFound(); .throwIfNotFound();
} catch (err) { } catch (err) {
throw new UnauthorizedException( throw new InvalidEmailPasswordException(email);
`There isn't any user with email: ${email}`,
);
} }
if (!(await user.checkPassword(password))) { if (!(await user.checkPassword(password))) {
throw new UnauthorizedException( throw new InvalidEmailPasswordException(email);
`Wrong password for user with email: ${email}`,
);
} }
return user; return user;
} }
@@ -61,9 +59,7 @@ export class AuthSigninService {
this.clsService.set('tenantId', user.tenantId); this.clsService.set('tenantId', user.tenantId);
this.clsService.set('userId', user.id); this.clsService.set('userId', user.id);
} catch (error) { } catch (error) {
throw new UnauthorizedException( throw new UserNotFoundException(String(payload.sub));
`There isn't any user with email: ${payload.sub}`,
);
} }
return payload; return payload;
} }

View File

@@ -32,7 +32,7 @@ export class AuthSignupService {
@Inject(SystemUser.name) @Inject(SystemUser.name)
private readonly systemUserModel: typeof SystemUser, private readonly systemUserModel: typeof SystemUser,
) {} ) { }
/** /**
* Registers a new tenant with user from user input. * Registers a new tenant with user from user input.
@@ -121,7 +121,6 @@ export class AuthSignupService {
const isAllowedDomain = signupRestrictions.allowedDomains.some( const isAllowedDomain = signupRestrictions.allowedDomains.some(
(domain) => emailDomain === domain, (domain) => emailDomain === domain,
); );
if (!isAllowedEmail && !isAllowedDomain) { if (!isAllowedEmail && !isAllowedDomain) {
throw new ServiceError( throw new ServiceError(
ERRORS.SIGNUP_RESTRICTED_NOT_ALLOWED, ERRORS.SIGNUP_RESTRICTED_NOT_ALLOWED,

View File

@@ -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,
});
}
}

View File

@@ -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,
});
}
}

View File

@@ -17,12 +17,11 @@ import { TenantUser } from '../Tenancy/TenancyModels/models/TenantUser.model';
@Injectable() @Injectable()
export class AuthorizationGuard implements CanActivate { export class AuthorizationGuard implements CanActivate {
constructor( constructor(
private reflector: Reflector,
private readonly clsService: ClsService, private readonly clsService: ClsService,
@Inject(TenantUser.name) @Inject(TenantUser.name)
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>, private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
) {} ) { }
/** /**
* Checks if the user has the required abilities to access the route * 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<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>(); const request = context.switchToHttp().getRequest<Request>();
const { tenantId, user } = request as any; const { user } = request as any;
if (ABILITIES_CACHE.has(user.id)) { if (ABILITIES_CACHE.has(user.id)) {
(request as any).ability = ABILITIES_CACHE.get(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; (request as any).ability = ability;
ABILITIES_CACHE.set(user.id, ability); ABILITIES_CACHE.set(user.id, ability);
} }
return true; return true;
} }

View File

@@ -32,13 +32,11 @@ export default function Login() {
email: values.crediential, email: values.crediential,
password: values.password, password: values.password,
}).catch(({ response }) => { }).catch(({ response }) => {
const { const { data: error } = response;
data: { errors }, const toastMessages = transformLoginErrorsToToasts(error);
} = response;
const toastBuilders = transformLoginErrorsToToasts(errors);
toastBuilders.forEach((builder) => { toastMessages.forEach((toastMessage) => {
Toaster.show(builder); Toaster.show(toastMessage);
}); });
setSubmitting(false); setSubmitting(false);
}); });

View File

@@ -35,7 +35,7 @@ export default function SendResetPassword() {
// Handle form submitting. // Handle form submitting.
const handleSubmit = (values, { setSubmitting }) => { const handleSubmit = (values, { setSubmitting }) => {
sendResetPasswordMutate({ email: values.crediential }) sendResetPasswordMutate({ email: values.crediential })
.then((response) => { .then(() => {
AppToaster.show({ AppToaster.show({
message: intl.get('check_your_email_for_a_link_to_reset'), message: intl.get('check_your_email_for_a_link_to_reset'),
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
@@ -43,20 +43,9 @@ export default function SendResetPassword() {
history.push('/auth/login'); history.push('/auth/login');
setSubmitting(false); setSubmitting(false);
}) })
.catch( .catch(() => {
({ setSubmitting(false);
response: { });
data: { errors },
},
}) => {
const toastBuilders = transformSendResetPassErrorsToToasts(errors);
toastBuilders.forEach((builder) => {
AppToaster.show(builder);
});
setSubmitting(false);
},
);
}; };
return ( return (
@@ -82,11 +71,17 @@ function SendResetPasswordFooterLinks() {
<AuthFooterLinks> <AuthFooterLinks>
{!signupDisabled && ( {!signupDisabled && (
<AuthFooterLink> <AuthFooterLink>
<T id={'dont_have_an_account'} /> <Link to={'/auth/register'}><T id={'sign_up'} /></Link> <T id={'dont_have_an_account'} />{' '}
<Link to={'/auth/register'}>
<T id={'sign_up'} />
</Link>
</AuthFooterLink> </AuthFooterLink>
)} )}
<AuthFooterLink> <AuthFooterLink>
<T id={'return_to'} /> <Link to={'/auth/login'}><T id={'sign_in'} /></Link> <T id={'return_to'} />{' '}
<Link to={'/auth/login'}>
<T id={'sign_in'} />
</Link>
</AuthFooterLink> </AuthFooterLink>
</AuthFooterLinks> </AuthFooterLinks>
); );

View File

@@ -13,12 +13,17 @@ export function AuthenticationLoadingOverlay() {
} }
const AuthOverlayRoot = styled.div` 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; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
bottom: 0; bottom: 0;
right: 0; right: 0;
background: rgba(252, 253, 255, 0.5); background: var(--x-color-background);
display: flex; display: flex;
justify-content: center; justify-content: center;
`; `;

View File

@@ -45,10 +45,10 @@ export const InviteAcceptSchema = Yup.object().shape({
password: Yup.string().min(4).required().label(intl.get('password')), password: Yup.string().min(4).required().label(intl.get('password')),
}); });
export const transformSendResetPassErrorsToToasts = (errors) => { export const transformSendResetPassErrorsToToasts = (error) => {
const toastBuilders = []; const toastBuilders = [];
if (errors.find((e) => e.type === 'EMAIL.NOT.REGISTERED')) { if (error.code === ERRORS.EMAIL_NOT_REGISTERED) {
toastBuilders.push({ toastBuilders.push({
message: intl.get('we_couldn_t_find_your_account_with_that_email'), message: intl.get('we_couldn_t_find_your_account_with_that_email'),
intent: Intent.DANGER, intent: Intent.DANGER,
@@ -57,38 +57,26 @@ export const transformSendResetPassErrorsToToasts = (errors) => {
return toastBuilders; return toastBuilders;
}; };
export const transformLoginErrorsToToasts = (errors) => { export const transformLoginErrorsToToasts = (error) => {
const toastBuilders = []; const toastBuilders = [];
if (errors.find((e) => e.type === LOGIN_ERRORS.INVALID_DETAILS)) { if (error.code === LOGIN_ERRORS.INVALID_DETAILS) {
toastBuilders.push({ toastBuilders.push({
message: intl.get('email_and_password_entered_did_not_match'), message: intl.get('email_and_password_entered_did_not_match'),
intent: Intent.DANGER, intent: Intent.DANGER,
}); });
} } else if (error.code === LOGIN_ERRORS.USER_INACTIVE) {
if (errors.find((e) => e.type === LOGIN_ERRORS.USER_INACTIVE)) {
toastBuilders.push({ toastBuilders.push({
message: intl.get('the_user_has_been_suspended_from_admin'), message: intl.get('the_user_has_been_suspended_from_admin'),
intent: Intent.DANGER, 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; return toastBuilders;
}; };
export const transformRegisterErrorsToForm = (errors) => { export const transformRegisterErrorsToForm = (errors) => {
const formErrors = {}; 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)) { if (errors.some((e) => e.type === REGISTER_ERRORS.EMAIL_EXISTS)) {
formErrors.email = intl.get('the_email_already_used_in_another_account'); formErrors.email = intl.get('the_email_already_used_in_another_account');
} }

View File

@@ -2,7 +2,7 @@
--x-border-color: #E1E1E1; --x-border-color: #E1E1E1;
--x-color-placeholder-text: #738091; --x-color-placeholder-text: #738091;
.bp4-dark & { :global(.bp4-dark) & {
--x-border-color: rgba(225, 225, 225, 0.15); --x-border-color: rgba(225, 225, 225, 0.15);
--x-color-placeholder-text: rgba(225, 225, 225, 0.65); --x-color-placeholder-text: rgba(225, 225, 225, 0.65);
} }

View File

@@ -53,7 +53,6 @@ export function CompanyLogoUpload({
const [initialLocalPreview, setInitialLocalPreview] = useState<string | null>( const [initialLocalPreview, setInitialLocalPreview] = useState<string | null>(
initialPreview || null, initialPreview || null,
); );
const openRef = useRef<() => void>(null); const openRef = useRef<() => void>(null);
const handleRemove = () => { const handleRemove = () => {

View File

@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { batch } from 'react-redux'; import { batch } from 'react-redux';
import useApiRequest from '../useRequest'; import useApiRequest, { useAuthApiRequest } from '../useRequest';
import { setCookie } from '../../utils'; import { setCookie } from '../../utils';
import { useRequestQuery } from '../useQueryRequest'; import { useRequestQuery } from '../useQueryRequest';
import t from './types'; import t from './types';
@@ -40,7 +40,7 @@ export function setAuthLoginCookies(data) {
* Authentication login. * Authentication login.
*/ */
export const useAuthLogin = (props) => { export const useAuthLogin = (props) => {
const apiRequest = useApiRequest(); const apiRequest = useAuthApiRequest();
const setAuthToken = useSetAuthToken(); const setAuthToken = useSetAuthToken();
const setOrganizationId = useSetOrganizationId(); const setOrganizationId = useSetOrganizationId();
@@ -49,7 +49,6 @@ export const useAuthLogin = (props) => {
const setLocale = useSetLocale(); const setLocale = useSetLocale();
return useMutation((values) => apiRequest.post(AuthRoute.Signin, values), { return useMutation((values) => apiRequest.post(AuthRoute.Signin, values), {
select: (res) => res.data,
onSuccess: (res) => { onSuccess: (res) => {
// Set authentication cookies. // Set authentication cookies.
setAuthLoginCookies(res.data); setAuthLoginCookies(res.data);
@@ -75,7 +74,7 @@ export const useAuthLogin = (props) => {
* Authentication register. * Authentication register.
*/ */
export const useAuthRegister = (props) => { export const useAuthRegister = (props) => {
const apiRequest = useApiRequest(); const apiRequest = useAuthApiRequest();
return useMutation( return useMutation(
(values) => apiRequest.post(AuthRoute.Signup, values), (values) => apiRequest.post(AuthRoute.Signup, values),
@@ -87,7 +86,7 @@ export const useAuthRegister = (props) => {
* Authentication send reset password. * Authentication send reset password.
*/ */
export const useAuthSendResetPassword = (props) => { export const useAuthSendResetPassword = (props) => {
const apiRequest = useApiRequest(); const apiRequest = useAuthApiRequest();
return useMutation( return useMutation(
(values) => apiRequest.post(AuthRoute.SendResetPassword, values), (values) => apiRequest.post(AuthRoute.SendResetPassword, values),
@@ -99,7 +98,7 @@ export const useAuthSendResetPassword = (props) => {
* Authentication reset password. * Authentication reset password.
*/ */
export const useAuthResetPassword = (props) => { export const useAuthResetPassword = (props) => {
const apiRequest = useApiRequest(); const apiRequest = useAuthApiRequest();
return useMutation( return useMutation(
([token, values]) => apiRequest.post(`auth/reset/${token}`, values), ([token, values]) => apiRequest.post(`auth/reset/${token}`, values),
@@ -129,7 +128,7 @@ export const useAuthMetadata = (props = {}) => {
* Resend the mail of signup verification. * Resend the mail of signup verification.
*/ */
export const useAuthSignUpVerifyResendMail = (props) => { export const useAuthSignUpVerifyResendMail = (props) => {
const apiRequest = useApiRequest(); const apiRequest = useAuthApiRequest();
return useMutation( return useMutation(
() => apiRequest.post(AuthRoute.SignupVerifyResend), () => apiRequest.post(AuthRoute.SignupVerifyResend),
@@ -146,7 +145,7 @@ interface AuthSignUpVerifyValues {
* Signup verification. * Signup verification.
*/ */
export const useAuthSignUpVerify = (props) => { export const useAuthSignUpVerify = (props) => {
const apiRequest = useApiRequest(); const apiRequest = useAuthApiRequest();
return useMutation( return useMutation(
(values: AuthSignUpVerifyValues) => (values: AuthSignUpVerifyValues) =>

View File

@@ -120,3 +120,35 @@ export default function useApiRequest() {
[http], [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],
);
}