From 95bacea624ec4a421f963e9c519fb3f2b3e1d6fd Mon Sep 17 00:00:00 2001 From: Kruille Date: Sun, 1 Mar 2026 17:25:45 +0100 Subject: [PATCH] Add regex to names and move frontendvaliadtion to separate file --- .../ParticipantRegistrationRequest.cs | 13 +++- .../VolunteerRegistrationRequest.cs | 8 +++ src/Web/lan-frontend/app/lib/validation.ts | 36 +++++++++++ src/Web/lan-frontend/app/register/page.tsx | 62 +++++++++++-------- src/Web/lan-frontend/app/volunteer/page.tsx | 28 +++++---- 5 files changed, 107 insertions(+), 40 deletions(-) create mode 100644 src/Web/lan-frontend/app/lib/validation.ts diff --git a/src/Registration/Registration.API/RequestModels/ParticipantRegistrationRequest.cs b/src/Registration/Registration.API/RequestModels/ParticipantRegistrationRequest.cs index d7b36b0..ee48fc1 100644 --- a/src/Registration/Registration.API/RequestModels/ParticipantRegistrationRequest.cs +++ b/src/Registration/Registration.API/RequestModels/ParticipantRegistrationRequest.cs @@ -8,14 +8,20 @@ public class ParticipantRegistrationRequest public bool? IsMember { get; set; } [Required] + [MaxLength(30)] + [RegularExpression(@"^[\p{L}]+([-\s][\p{L}]+)?$", + ErrorMessage = "Förnamn får endast innehålla bokstäver, ett bindestreck eller ett mellanslag.")] public string FirstName { get; set; } = string.Empty; [Required] + [MaxLength(30)] + [RegularExpression(@"^[\p{L}]+([-\s][\p{L}]+)?$", + ErrorMessage = "Efternamn får endast innehålla bokstäver, ett bindestreck eller ett mellanslag.")] public string SurName { get; set; } = string.Empty; [Required] [RegularExpression(@"^([1-9]|Gymnasium [1-3])$", - ErrorMessage = "Grade must be 1–9 or Gymnasium 1–3.")] + ErrorMessage = "Årkurs måste vara 1–9 eller Gymnasium 1–3.")] public string Grade { get; set; } = string.Empty; public string? PhoneNumber { get; set; } @@ -23,6 +29,9 @@ public class ParticipantRegistrationRequest public string? Email { get; set; } [Required] + [MaxLength(61)] + [RegularExpression(@"^[\p{L}]+([-\s][\p{L}]+)*$", + ErrorMessage = "Vårdnadshavares namn får endast innehålla bokstäver, bindestreck och mellanslag.")] public string GuardianName { get; set; } = string.Empty; [Required] @@ -36,6 +45,8 @@ public class ParticipantRegistrationRequest public bool? IsVisitor { get; set; } [Required] + [Range(typeof(bool), "true", "true", + ErrorMessage = "GDPR-godkännande krävs.")] public bool? HasApprovedGdpr { get; set; } public string? Friends { get; set; } diff --git a/src/Registration/Registration.API/RequestModels/VolunteerRegistrationRequest.cs b/src/Registration/Registration.API/RequestModels/VolunteerRegistrationRequest.cs index f0bbd25..b41be1d 100644 --- a/src/Registration/Registration.API/RequestModels/VolunteerRegistrationRequest.cs +++ b/src/Registration/Registration.API/RequestModels/VolunteerRegistrationRequest.cs @@ -5,9 +5,15 @@ namespace Registration.API.RequestModels; public class VolunteerRegistrationRequest { [Required] + [MaxLength(30)] + [RegularExpression(@"^[\p{L}]+([-\s][\p{L}]+)?$", + ErrorMessage = "Förnamn får endast innehålla bokstäver, ett bindestreck eller ett mellanslag.")] public string FirstName { get; set; } = string.Empty; [Required] + [MaxLength(30)] + [RegularExpression(@"^[\p{L}]+([-\s][\p{L}]+)?$", + ErrorMessage = "Efternamn får endast innehålla bokstäver, ett bindestreck eller ett mellanslag.")] public string SurName { get; set; } = string.Empty; [Required] @@ -18,6 +24,8 @@ public class VolunteerRegistrationRequest public string Email { get; set; } = string.Empty; [Required] + [Range(typeof(bool), "true", "true", + ErrorMessage = "GDPR-godkännande krävs.")] public bool? HasApprovedGdpr { get; set; } [Required] diff --git a/src/Web/lan-frontend/app/lib/validation.ts b/src/Web/lan-frontend/app/lib/validation.ts new file mode 100644 index 0000000..70ba73a --- /dev/null +++ b/src/Web/lan-frontend/app/lib/validation.ts @@ -0,0 +1,36 @@ +export const normalizeSsn = (value: string) => + value.replace(/[\s\-+]/g, "").replace(/\D/g, ""); + +export const isValidSsnFormat = (value: string) => { + if (!/^\d{12}$/.test(value)) return false; + const month = parseInt(value.substring(4, 6)); + const day = parseInt(value.substring(6, 8)); + return month >= 1 && month <= 12 && day >= 1 && day <= 31; +}; + +export const getAgeFromSsn = (value: string) => { + const birthYear = parseInt(value.substring(0, 4)); + const currentYear = new Date().getFullYear(); + return currentYear - birthYear; +}; + +export const isValidName = (value: string) => { + if (!/^[\p{L}\-\s]+$/u.test(value)) return false; + if ((value.match(/-/g) ?? []).length > 1) return false; + if ((value.match(/\s/g) ?? []).length > 1) return false; + return true; +}; + +export const isValidEmail = (value: string) => + /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); + +export const normalizeMobileNumber = (value: string) => { + const digitsOnly = value.replace(/\D/g, ""); + if (digitsOnly.startsWith("0046")) return `0${digitsOnly.slice(4)}`; + if (digitsOnly.startsWith("46")) return `0${digitsOnly.slice(2)}`; + if (digitsOnly.startsWith("7")) return `0${digitsOnly}`; + return digitsOnly; +}; + +export const isValidMobileNumber = (value: string) => + /^07\d{8}$/.test(normalizeMobileNumber(value)); diff --git a/src/Web/lan-frontend/app/register/page.tsx b/src/Web/lan-frontend/app/register/page.tsx index 612bfe3..43e6cf4 100644 --- a/src/Web/lan-frontend/app/register/page.tsx +++ b/src/Web/lan-frontend/app/register/page.tsx @@ -5,6 +5,15 @@ import { useEffect, useRef, useState } from "react"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import { + normalizeSsn, + isValidSsnFormat, + getAgeFromSsn, + isValidName, + isValidEmail, + normalizeMobileNumber, + isValidMobileNumber, +} from "@/app/lib/validation"; interface EventContent { registrationEnabled: boolean; @@ -12,33 +21,6 @@ interface EventContent { volunteerAreas: string; } -const normalizeSsn = (value: string) => - value.replace(/[\s\-+]/g, "").replace(/\D/g, ""); - -const isValidSsnFormat = (value: string) => { - if (!/^\d{12}$/.test(value)) return false; - const month = parseInt(value.substring(4, 6)); - const day = parseInt(value.substring(6, 8)); - return month >= 1 && month <= 12 && day >= 1 && day <= 31; -}; - -const getAgeFromSsn = (value: string) => { - const birthYear = parseInt(value.substring(0, 4)); - const currentYear = new Date().getFullYear(); - return currentYear - birthYear; -}; -const isValidEmail = (value: string) => - /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); -const normalizeMobileNumber = (value: string) => { - const digitsOnly = value.replace(/\D/g, ""); - if (digitsOnly.startsWith("0046")) return `0${digitsOnly.slice(4)}`; - if (digitsOnly.startsWith("46")) return `0${digitsOnly.slice(2)}`; - if (digitsOnly.startsWith("7")) return `0${digitsOnly}`; - return digitsOnly; -}; -const isValidMobileNumber = (value: string) => - /^07\d{8}$/.test(normalizeMobileNumber(value)); - export default function RegisterPage() { const router = useRouter(); const [content, setContent] = useState(null); @@ -154,15 +136,37 @@ export default function RegisterPage() { if (!trimmedFirstName) nextFieldErrors.firstName = "Förnamn är obligatoriskt."; + else if (trimmedFirstName.length > 30) + nextFieldErrors.firstName = "Förnamn får vara max 30 tecken."; + else if (!isValidName(trimmedFirstName)) + nextFieldErrors.firstName = + "Förnamn får endast innehålla bokstäver och bindestreck."; if (!trimmedSurName) nextFieldErrors.surName = "Efternamn är obligatoriskt."; + else if (trimmedSurName.length > 30) + nextFieldErrors.surName = "Efternamn får vara max 30 tecken."; + else if (!isValidName(trimmedSurName)) + nextFieldErrors.surName = + "Efternamn får endast innehålla bokstäver och bindestreck."; if (!trimmedGrade) nextFieldErrors.grade = "Årskurs är obligatorisk."; if (!trimmedGuardianFirstName) nextFieldErrors.guardianFirstName = "Vårdnadshavares förnamn är obligatoriskt."; + else if (trimmedGuardianFirstName.length > 30) + nextFieldErrors.guardianFirstName = + "Vårdnadshavares förnamn får vara max 30 tecken."; + else if (!isValidName(trimmedGuardianFirstName)) + nextFieldErrors.guardianFirstName = + "Vårdnadshavares förnamn får endast innehålla bokstäver och bindestreck."; if (!trimmedGuardianLastName) nextFieldErrors.guardianLastName = "Vårdnadshavares efternamn är obligatoriskt."; + else if (trimmedGuardianLastName.length > 30) + nextFieldErrors.guardianLastName = + "Vårdnadshavares efternamn får vara max 30 tecken."; + else if (!isValidName(trimmedGuardianLastName)) + nextFieldErrors.guardianLastName = + "Vårdnadshavares efternamn får endast innehålla bokstäver och bindestreck."; if (!trimmedGuardianPhone) nextFieldErrors.guardianPhoneNumber = "Vårdnadshavares Mobilnummer är obligatoriskt."; @@ -388,6 +392,7 @@ export default function RegisterPage() { name="firstName" id="firstName" required + maxLength={30} className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.firstName ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`} value={formData.firstName} onChange={handleInputChange} @@ -411,6 +416,7 @@ export default function RegisterPage() { name="surName" id="surName" required + maxLength={30} className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.surName ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`} value={formData.surName} onChange={handleInputChange} @@ -520,6 +526,7 @@ export default function RegisterPage() { name="guardianFirstName" id="guardianFirstName" required + maxLength={30} className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.guardianFirstName ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`} value={formData.guardianFirstName} onChange={handleInputChange} @@ -543,6 +550,7 @@ export default function RegisterPage() { name="guardianLastName" id="guardianLastName" required + maxLength={30} className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.guardianLastName ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`} value={formData.guardianLastName} onChange={handleInputChange} diff --git a/src/Web/lan-frontend/app/volunteer/page.tsx b/src/Web/lan-frontend/app/volunteer/page.tsx index 23db879..901a4fc 100644 --- a/src/Web/lan-frontend/app/volunteer/page.tsx +++ b/src/Web/lan-frontend/app/volunteer/page.tsx @@ -3,24 +3,18 @@ import { useEffect, useState } from "react"; import Image from "next/image"; import { useRouter } from "next/navigation"; +import { + isValidName, + isValidEmail, + normalizeMobileNumber, + isValidMobileNumber, +} from "@/app/lib/validation"; interface EventContent { volunteerAreas: string; volunteerRegistrationEnabled?: boolean; } -const isValidEmail = (value: string) => - /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); -const normalizeMobileNumber = (value: string) => { - const digitsOnly = value.replace(/\D/g, ""); - if (digitsOnly.startsWith("0046")) return `0${digitsOnly.slice(4)}`; - if (digitsOnly.startsWith("46")) return `0${digitsOnly.slice(2)}`; - if (digitsOnly.startsWith("7")) return `0${digitsOnly}`; - return digitsOnly; -}; -const isValidMobileNumber = (value: string) => - /^07\d{8}$/.test(normalizeMobileNumber(value)); - export default function VolunteerPage() { const router = useRouter(); const [content, setContent] = useState(null); @@ -104,8 +98,16 @@ export default function VolunteerPage() { if (!trimmedFirstName) nextFieldErrors.firstName = "Förnamn är obligatoriskt."; + else if (trimmedFirstName.length > 30) + nextFieldErrors.firstName = "Förnamn får vara max 30 tecken."; + else if (!isValidName(trimmedFirstName)) + nextFieldErrors.firstName = "Förnamn får endast innehålla bokstäver och bindestreck."; if (!trimmedSurName) nextFieldErrors.surName = "Efternamn är obligatoriskt."; + else if (trimmedSurName.length > 30) + nextFieldErrors.surName = "Efternamn får vara max 30 tecken."; + else if (!isValidName(trimmedSurName)) + nextFieldErrors.surName = "Efternamn får endast innehålla bokstäver och bindestreck."; if (!trimmedPhoneNumber) nextFieldErrors.phoneNumber = "Mobilnummer är obligatoriskt."; if (!trimmedEmail) nextFieldErrors.email = "E-post är obligatorisk."; @@ -239,6 +241,7 @@ export default function VolunteerPage() { name="firstName" id="firstName" required + maxLength={30} className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.firstName ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`} value={formData.firstName} onChange={handleInputChange} @@ -262,6 +265,7 @@ export default function VolunteerPage() { name="surName" id="surName" required + maxLength={30} className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.surName ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`} value={formData.surName} onChange={handleInputChange}