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; } public bool? IsMember { get; set; }
[Required] [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; public string FirstName { get; set; } = string.Empty;
[Required] [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; public string SurName { get; set; } = string.Empty;
[Required] [Required]
[RegularExpression(@"^([1-9]|Gymnasium [1-3])$", [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 Grade { get; set; } = string.Empty;
public string? PhoneNumber { get; set; } public string? PhoneNumber { get; set; }
@ -23,6 +29,9 @@ public class ParticipantRegistrationRequest
public string? Email { get; set; } public string? Email { get; set; }
[Required] [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; public string GuardianName { get; set; } = string.Empty;
[Required] [Required]
@ -36,6 +45,8 @@ public class ParticipantRegistrationRequest
public bool? IsVisitor { get; set; } public bool? IsVisitor { get; set; }
[Required] [Required]
[Range(typeof(bool), "true", "true",
ErrorMessage = "GDPR-godkännande krävs.")]
public bool? HasApprovedGdpr { get; set; } public bool? HasApprovedGdpr { get; set; }
public string? Friends { get; set; } public string? Friends { get; set; }

View file

@ -5,9 +5,15 @@ namespace Registration.API.RequestModels;
public class VolunteerRegistrationRequest public class VolunteerRegistrationRequest
{ {
[Required] [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; public string FirstName { get; set; } = string.Empty;
[Required] [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; public string SurName { get; set; } = string.Empty;
[Required] [Required]
@ -18,6 +24,8 @@ public class VolunteerRegistrationRequest
public string Email { get; set; } = string.Empty; public string Email { get; set; } = string.Empty;
[Required] [Required]
[Range(typeof(bool), "true", "true",
ErrorMessage = "GDPR-godkännande krävs.")]
public bool? HasApprovedGdpr { get; set; } public bool? HasApprovedGdpr { get; set; }
[Required] [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 Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import {
normalizeSsn,
isValidSsnFormat,
getAgeFromSsn,
isValidName,
isValidEmail,
normalizeMobileNumber,
isValidMobileNumber,
} from "@/app/lib/validation";
interface EventContent { interface EventContent {
registrationEnabled: boolean; registrationEnabled: boolean;
@ -12,33 +21,6 @@ interface EventContent {
volunteerAreas: string; 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() { export default function RegisterPage() {
const router = useRouter(); const router = useRouter();
const [content, setContent] = useState<EventContent | null>(null); const [content, setContent] = useState<EventContent | null>(null);
@ -154,15 +136,37 @@ export default function RegisterPage() {
if (!trimmedFirstName) if (!trimmedFirstName)
nextFieldErrors.firstName = "Förnamn är obligatoriskt."; 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) if (!trimmedSurName)
nextFieldErrors.surName = "Efternamn är obligatoriskt."; 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 (!trimmedGrade) nextFieldErrors.grade = "Årskurs är obligatorisk.";
if (!trimmedGuardianFirstName) if (!trimmedGuardianFirstName)
nextFieldErrors.guardianFirstName = nextFieldErrors.guardianFirstName =
"Vårdnadshavares förnamn är obligatoriskt."; "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) if (!trimmedGuardianLastName)
nextFieldErrors.guardianLastName = nextFieldErrors.guardianLastName =
"Vårdnadshavares efternamn är obligatoriskt."; "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) if (!trimmedGuardianPhone)
nextFieldErrors.guardianPhoneNumber = nextFieldErrors.guardianPhoneNumber =
"Vårdnadshavares Mobilnummer är obligatoriskt."; "Vårdnadshavares Mobilnummer är obligatoriskt.";
@ -388,6 +392,7 @@ export default function RegisterPage() {
name="firstName" name="firstName"
id="firstName" id="firstName"
required 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"}`} 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} value={formData.firstName}
onChange={handleInputChange} onChange={handleInputChange}
@ -411,6 +416,7 @@ export default function RegisterPage() {
name="surName" name="surName"
id="surName" id="surName"
required 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"}`} 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} value={formData.surName}
onChange={handleInputChange} onChange={handleInputChange}
@ -520,6 +526,7 @@ export default function RegisterPage() {
name="guardianFirstName" name="guardianFirstName"
id="guardianFirstName" id="guardianFirstName"
required 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"}`} 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} value={formData.guardianFirstName}
onChange={handleInputChange} onChange={handleInputChange}
@ -543,6 +550,7 @@ export default function RegisterPage() {
name="guardianLastName" name="guardianLastName"
id="guardianLastName" id="guardianLastName"
required 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"}`} 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} value={formData.guardianLastName}
onChange={handleInputChange} onChange={handleInputChange}

View file

@ -3,24 +3,18 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import {
isValidName,
isValidEmail,
normalizeMobileNumber,
isValidMobileNumber,
} from "@/app/lib/validation";
interface EventContent { interface EventContent {
volunteerAreas: string; volunteerAreas: string;
volunteerRegistrationEnabled?: boolean; 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() { export default function VolunteerPage() {
const router = useRouter(); const router = useRouter();
const [content, setContent] = useState<EventContent | null>(null); const [content, setContent] = useState<EventContent | null>(null);
@ -104,8 +98,16 @@ export default function VolunteerPage() {
if (!trimmedFirstName) if (!trimmedFirstName)
nextFieldErrors.firstName = "Förnamn är obligatoriskt."; 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) if (!trimmedSurName)
nextFieldErrors.surName = "Efternamn är obligatoriskt."; 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) if (!trimmedPhoneNumber)
nextFieldErrors.phoneNumber = "Mobilnummer är obligatoriskt."; nextFieldErrors.phoneNumber = "Mobilnummer är obligatoriskt.";
if (!trimmedEmail) nextFieldErrors.email = "E-post är obligatorisk."; if (!trimmedEmail) nextFieldErrors.email = "E-post är obligatorisk.";
@ -239,6 +241,7 @@ export default function VolunteerPage() {
name="firstName" name="firstName"
id="firstName" id="firstName"
required 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"}`} 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} value={formData.firstName}
onChange={handleInputChange} onChange={handleInputChange}
@ -262,6 +265,7 @@ export default function VolunteerPage() {
name="surName" name="surName"
id="surName" id="surName"
required 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"}`} 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} value={formData.surName}
onChange={handleInputChange} onChange={handleInputChange}