Refactor password validation and enhance localization (#35)

* Refactor password validation and enhance localization

A new PasswordSchema is introduced to handle the password validation in a centralized way and is used across all authentication schemas. The password requirements are also altered with additional special character, number, and uppercase letter checks. Error messages now utilize localization to provide dynamic error notifications.

* Sign out before impersonating a user

This update adds a call to sign out before impersonating a user. This is an additional measure to ensure the security of the system, accentuating the isolation of user sessions.

* Refactor password validation and refine password schemas

The password validation process has been restructured. The 'PasswordSchema' is now split into two separate schemas - 'PasswordSchema' and 'RefinedPasswordSchema'. The logic for validating repeating passwords has been moved into a separate function named 'refineRepeatPassword'. This streamlines the password validation process and ensures consistency across password checks.
This commit is contained in:
Giancarlo Buomprisco
2024-06-17 14:37:18 +08:00
committed by GitHub
parent 298e70b738
commit fbe7ca4c9e
7 changed files with 105 additions and 21 deletions

View File

@@ -26,7 +26,6 @@
"passwordHint": "Ensure it's at least 8 characters",
"repeatPasswordHint": "Type your password again",
"repeatPassword": "Repeat password",
"passwordsDoNotMatch": "The passwords do not match",
"passwordForgottenQuestion": "Password forgotten?",
"passwordResetLabel": "Reset Password",
"passwordResetSubheading": "Enter your email address below. You will receive a link to reset your password.",
@@ -69,6 +68,11 @@
"default": "We have encountered an error. Please ensure you have a working internet connection and try again",
"generic": "Sorry, we weren't able to authenticate you. Please try again.",
"link": "Sorry, we encountered an error while sending your link. Please try again.",
"codeVerifierMismatch": "It looks like you're trying to sign in using a different browser than the one you used to request the sign in link. Please try again using the same browser."
"codeVerifierMismatch": "It looks like you're trying to sign in using a different browser than the one you used to request the sign in link. Please try again using the same browser.",
"minPasswordLength": "Password must be at least 8 characters long",
"passwordsDoNotMatch": "The passwords do not match",
"minPasswordNumbers": "Password must contain at least one number",
"minPasswordSpecialChars": "Password must contain at least one special character",
"uppercasePassword": "Password must contain at least one uppercase letter"
}
}

View File

@@ -142,6 +142,8 @@ function useSetSession(tokens: { accessToken: string; refreshToken: string }) {
queryKey: ['impersonate-user', tokens.accessToken, tokens.refreshToken],
gcTime: 0,
queryFn: async () => {
await supabase.auth.signOut();
await supabase.auth.setSession({
refresh_token: tokens.refreshToken,
access_token: tokens.accessToken,

View File

@@ -1,11 +1,10 @@
import { z } from 'zod';
import { RefinedPasswordSchema, refineRepeatPassword } from './password.schema';
export const PasswordResetSchema = z
.object({
password: z.string().min(8).max(99),
repeatPassword: z.string().min(8).max(99),
password: RefinedPasswordSchema,
repeatPassword: RefinedPasswordSchema,
})
.refine((data) => data.password === data.repeatPassword, {
message: 'Passwords do not match',
path: ['repeatPassword'],
});
.superRefine(refineRepeatPassword);

View File

@@ -1,6 +1,8 @@
import { z } from 'zod';
import { PasswordSchema } from './password.schema';
export const PasswordSignInSchema = z.object({
email: z.string().email(),
password: z.string().min(8).max(99),
password: PasswordSchema,
});

View File

@@ -1,17 +1,11 @@
import { z } from 'zod';
import { RefinedPasswordSchema, refineRepeatPassword } from './password.schema';
export const PasswordSignUpSchema = z
.object({
email: z.string().email(),
password: z.string().min(8).max(99),
repeatPassword: z.string().min(8).max(99),
password: RefinedPasswordSchema,
repeatPassword: RefinedPasswordSchema,
})
.refine(
(schema) => {
return schema.password === schema.repeatPassword;
},
{
message: 'Passwords do not match',
path: ['repeatPassword'],
},
);
.superRefine(refineRepeatPassword);

View File

@@ -0,0 +1,82 @@
import { z } from 'zod';
/**
* Password requirements
* These are the requirements for the password when signing up or changing the password
*/
const requirements = {
minLength: 8,
maxLength: 99,
specialChars:
process.env.NEXT_PUBLIC_PASSWORD_REQUIRE_SPECIAL_CHARS === 'true',
numbers: process.env.NEXT_PUBLIC_PASSWORD_REQUIRE_NUMBERS === 'true',
uppercase: process.env.NEXT_PUBLIC_PASSWORD_REQUIRE_UPPERCASE === 'true',
};
/**
* Password schema
* This is used to validate the password on sign in (for existing users when requirements are not enforced)
*/
export const PasswordSchema = z
.string()
.min(requirements.minLength)
.max(requirements.maxLength);
/**
* Refined password schema with additional requirements
* This is required to validate the password requirements on sign up and password change
*/
export const RefinedPasswordSchema = PasswordSchema.superRefine((val, ctx) =>
validatePassword(val, ctx),
);
export function refineRepeatPassword(
data: { password: string; repeatPassword: string },
ctx: z.RefinementCtx,
) {
if (data.password !== data.repeatPassword) {
ctx.addIssue({
message: 'auth:errors.passwordsDoNotMatch',
path: ['repeatPassword'],
code: 'custom',
});
}
return true;
}
function validatePassword(password: string, ctx: z.RefinementCtx) {
if (requirements.specialChars) {
const specialCharsCount =
password.match(/[!@#$%^&*(),.?":{}|<>]/g)?.length ?? 0;
if (specialCharsCount < 1) {
ctx.addIssue({
message: 'auth:errors.minPasswordSpecialChars',
code: 'custom',
});
}
}
if (requirements.numbers) {
const numbersCount = password.match(/\d/g)?.length ?? 0;
if (numbersCount < 1) {
ctx.addIssue({
message: 'auth:errors.minPasswordNumbers',
code: 'custom',
});
}
}
if (requirements.uppercase) {
if (!/[A-Z]/.test(password)) {
ctx.addIssue({
message: 'auth:errors.uppercasePassword',
code: 'custom',
});
}
}
return true;
}

View File

@@ -9,6 +9,7 @@ import { Controller, FormProvider, useFormContext } from 'react-hook-form';
import { cn } from '../utils';
import { Label } from './label';
import {Trans} from "../makerkit/trans";
const Form = FormProvider;
@@ -156,7 +157,7 @@ const FormMessage = React.forwardRef<
className={cn('text-[0.8rem] font-medium text-destructive', className)}
{...props}
>
{body}
{typeof body === 'string' ? <Trans i18nKey={body} defaults={body} /> : body}
</p>
);
});