Add regex to names and move frontendvaliadtion to separate file

This commit is contained in:
Kruille 2026-03-01 17:25:45 +01:00
parent 5535ef672c
commit 95bacea624
5 changed files with 107 additions and 40 deletions

View file

@ -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 19 or Gymnasium 13.")]
ErrorMessage = "Årkurs måste vara 19 eller Gymnasium 13.")]
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; }

View file

@ -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]

View file

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

View file

@ -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<EventContent | null>(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}

View file

@ -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<EventContent | null>(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}