Files
frontend/src/features/auth/model/auth.schema.ts
T
codex-bot ebe92684a7
Frontend verify / commits (push) Skipped
Frontend verify / commits (pull_request) Failing after 40s
Frontend verify / security (pull_request) Skipped
Frontend verify / verify (pull_request) Skipped
Frontend verify / security (push) Successful in 11s
Frontend verify / verify (push) Failing after 7m35s
fix: align password validation with backend ASCII policy (WealthLedger/backend#60) (#89)
2026-10-16 10:00:54 +03:00

86 lines
2.2 KiB
TypeScript

import { z } from "zod";
import { validationMessage } from "@/shared/i18n";
const EMAIL_PART_COUNT = 2;
const ASCII_SPACE = 32;
const ASCII_DELETE = 127;
const MAX_EMAIL_LENGTH = 254;
const MIN_PASSWORD_LENGTH = 8;
const MAX_PASSWORD_BYTES = 72;
// Printable ASCII keeps characters == bytes, within the bcrypt 72-byte limit.
const ASCII_TILDE = 126;
function isEmailCharacterInvalid(character: string): boolean {
const codePoint = character.codePointAt(0);
return (
/\s/.test(character)
|| (codePoint !== undefined && codePoint < ASCII_SPACE)
|| codePoint === ASCII_DELETE
);
}
function isPrintableAscii(value: string): boolean {
return value.split("").every((character) => {
const code = character.codePointAt(0) ?? 0;
return code >= ASCII_SPACE && code <= ASCII_TILDE;
});
}
const email = z
.string()
.trim()
.toLowerCase()
.max(MAX_EMAIL_LENGTH)
.refine(
value =>
value.split("@").length === EMAIL_PART_COUNT
&& value.split("@").every(Boolean)
&& value.split("").every(element => !isEmailCharacterInvalid(element)),
{ error: validationMessage("auth.validation.invalidEmail") },
);
// Login stays lenient: accounts created under an older policy must still sign in.
const loginPassword = z
.string()
.min(1)
.refine(
value => new TextEncoder().encode(value).length <= MAX_PASSWORD_BYTES,
{ error: validationMessage("auth.validation.passwordTooLong") },
);
const registerPassword = z
.string()
.min(MIN_PASSWORD_LENGTH, {
error: validationMessage("auth.validation.invalidPasswordLength"),
})
.max(MAX_PASSWORD_BYTES, {
error: validationMessage("auth.validation.invalidPasswordLength"),
})
.refine(isPrintableAscii, {
error: validationMessage("auth.validation.invalidPasswordLength"),
});
const loginSchema = z.object({
email,
password: loginPassword,
passwordConfirmation: z.string().optional(),
});
const registerSchema = loginSchema
.extend({ password: registerPassword })
.refine(
value => value.password === value.passwordConfirmation,
{
error: validationMessage("auth.validation.passwordsDoNotMatch"),
path: ["passwordConfirmation"],
},
);
type AuthFormValues = z.input<typeof loginSchema>;
export { type AuthFormValues, loginSchema, registerSchema };