diff --git a/src/Registration/Registration.API/Controllers/ParticipantController.cs b/src/Registration/Registration.API/Controllers/ParticipantController.cs index 5aaf78c..cc27b87 100644 --- a/src/Registration/Registration.API/Controllers/ParticipantController.cs +++ b/src/Registration/Registration.API/Controllers/ParticipantController.cs @@ -15,13 +15,8 @@ public class ParticipantController(IVbytesParticipantRelayService relayService) [HttpPost("register")] public async Task RegisterForLan([FromBody] ParticipantRegistrationRequest request, CancellationToken cancellationToken) { - var normalizedGuardianPhone = InputNormalization.NormalizeSwedishMobile(request.GuardianPhoneNumber); - - string? normalizedParticipantPhone = null; - if (!string.IsNullOrWhiteSpace(request.PhoneNumber)) - { - normalizedParticipantPhone = InputNormalization.NormalizeSwedishMobile(request.PhoneNumber); - } + var normalizedGuardianPhone = request.GuardianPhoneNumber.NormalizePhone(); + var normalizedParticipantPhone = request.PhoneNumber.NormalizeOptionalPhone(); var participant = new Participant { @@ -42,7 +37,7 @@ public class ParticipantController(IVbytesParticipantRelayService relayService) var result = await _relayService.RegisterParticipantAsync(participant, cancellationToken); - if (result.StatusCode == 200 && !result.Message.Contains("401")) + if (result.Success) { return Ok(); } diff --git a/src/Registration/Registration.API/Controllers/RegistrationController.cs b/src/Registration/Registration.API/Controllers/RegistrationController.cs index 7d6a9c7..6752a0b 100644 --- a/src/Registration/Registration.API/Controllers/RegistrationController.cs +++ b/src/Registration/Registration.API/Controllers/RegistrationController.cs @@ -15,9 +15,9 @@ namespace Registration.API.Controllers [HttpGet("register/{ssn}")] public async Task ValidateSsn(string ssn, CancellationToken cancellationToken) { - if (!TryNormalizeSsn(ssn, out var normalizedSsn, out var errorResult)) + if (!ssn.TryNormalizeSwedishSsn(out var normalizedSsn, out var errorResult)) { - return errorResult; + return errorResult!; } var isMember = await _authService.IsMemberAsync(normalizedSsn, cancellationToken); @@ -25,11 +25,11 @@ namespace Registration.API.Controllers } [HttpPost("register/{ssn}")] - public async Task RegisterMember(string ssn) + public async Task RegisterMember(string ssn, CancellationToken cancellationToken) { - if (!TryNormalizeSsn(ssn, out var normalizedSsn, out var errorResult)) + if (!ssn.TryNormalizeSwedishSsn(out var normalizedSsn, out var errorResult)) { - return errorResult; + return errorResult!; } var added = await _memberRepository.AddRegistration(normalizedSsn); @@ -40,9 +40,9 @@ namespace Registration.API.Controllers [HttpGet("registered/{ssn}")] public async Task IsMemberRegistered(string ssn) { - if (!TryNormalizeSsn(ssn, out var normalizedSsn, out var errorResult)) + if (!ssn.TryNormalizeSwedishSsn(out var normalizedSsn, out var errorResult)) { - return errorResult; + return errorResult!; } var isRegistered = await _memberRepository.GetIsRegistered(normalizedSsn); @@ -50,30 +50,12 @@ namespace Registration.API.Controllers } [HttpDelete("clear")] - public async Task ClearRegistrations() + public async Task ClearRegistrations(CancellationToken cancellationToken) { await _memberRepository.ClearRegistrations(); return Ok(new { Message = "All registrations cleared." }); } - private bool TryNormalizeSsn(string ssn, out string normalizedSsn, out IActionResult errorResult) - { - normalizedSsn = InputNormalization.NormalizeSsn(ssn); - if (InputNormalization.IsValidSsn(normalizedSsn)) - { - errorResult = Ok(); - return true; - } - errorResult = BadRequest(new ValidationProblemDetails(new Dictionary - { - ["ssn"] = ["SSN must contain 10 or 12 digits."] - }) - { - Title = "One or more validation errors occurred.", - Status = StatusCodes.Status400BadRequest - }); - return false; - } } } diff --git a/src/Registration/Registration.API/Controllers/VolunteerController.cs b/src/Registration/Registration.API/Controllers/VolunteerController.cs index 727d0ac..5d719c8 100644 --- a/src/Registration/Registration.API/Controllers/VolunteerController.cs +++ b/src/Registration/Registration.API/Controllers/VolunteerController.cs @@ -15,7 +15,7 @@ public class VolunteerController(IVbytesVolunteerRelayService relayService) : Co [HttpPost("register")] public async Task RegisterVolunteer([FromBody] VolunteerRegistrationRequest request, CancellationToken cancellationToken) { - var normalizedPhone = InputNormalization.NormalizeSwedishMobile(request.PhoneNumber); + var normalizedPhone = request.PhoneNumber.NormalizePhone(); var trimmedAreas = request.AreasOfInterest .Select(area => area.Name.Trim()) @@ -39,12 +39,12 @@ public class VolunteerController(IVbytesVolunteerRelayService relayService) : Co PhoneNumber = normalizedPhone, Email = request.Email.Trim(), HasApprovedGdpr = request.HasApprovedGdpr!.Value, - AreasOfInterest = trimmedAreas.Select(name => new AreasOfInterest { Name = name }).ToList() + AreasOfInterest = trimmedAreas.Select(name => new AreaOfInterest { Name = name }).ToList() }; var result = await _relayService.RegisterVolunteerAsync(volunteer, cancellationToken); - if (result.StatusCode == 200 && !result.Message.Contains("401")) + if (result.Success) { return Ok(); } diff --git a/src/Registration/Registration.API/Program.cs b/src/Registration/Registration.API/Program.cs index f1c2992..f9ae27f 100644 --- a/src/Registration/Registration.API/Program.cs +++ b/src/Registration/Registration.API/Program.cs @@ -16,33 +16,34 @@ var relayOptions = builder.Configuration.GetSection("VbytesRelay").Get +var authApiOptions = builder.Configuration.GetSection("AuthApi").Get() + ?? throw new InvalidOperationException("AuthApi configuration section is missing."); + +builder.Services.Configure(builder.Configuration.GetSection("VbytesRelay")); + +builder.Services.AddHttpClient(o => +{ + o.BaseAddress = new Uri(relayOptions.BaseUrl); + o.Timeout = TimeSpan.FromSeconds(30); +}) +.ConfigurePrimaryHttpMessageHandler(() => { var handler = new HttpClientHandler(); handler.ClientCertificates.Add(certificate); return handler; }); -var authApiOptions = builder.Configuration.GetSection("AuthApi").Get() - ?? throw new InvalidOperationException("AuthApi configuration section is missing."); - -builder.Services.Configure(builder.Configuration.GetSection("VbytesRelay")); - - -builder.Services.AddHttpClient(o => -{ - o.BaseAddress = new Uri(relayOptions.BaseUrl) ?? - throw new InvalidOperationException("BaseUrl is missing in VbytesRelay configuration."); - o.Timeout = TimeSpan.FromSeconds(30); -}) -.ConfigurePrimaryHttpMessageHandler(sp => sp.GetRequiredService()); - builder.Services.AddHttpClient(o => { o.BaseAddress = new Uri(relayOptions.BaseUrl); o.Timeout = TimeSpan.FromSeconds(30); }) -.ConfigurePrimaryHttpMessageHandler(sp => sp.GetRequiredService()); +.ConfigurePrimaryHttpMessageHandler(() => +{ + var handler = new HttpClientHandler(); + handler.ClientCertificates.Add(certificate); + return handler; +}); builder.Services.Configure(builder.Configuration.GetSection("AuthApi")); builder.Services.AddHttpClient(o => diff --git a/src/Registration/Registration.API/RequestModels/ParticipantRegistrationRequest.cs b/src/Registration/Registration.API/RequestModels/ParticipantRegistrationRequest.cs index 627ee35..61f75c3 100644 --- a/src/Registration/Registration.API/RequestModels/ParticipantRegistrationRequest.cs +++ b/src/Registration/Registration.API/RequestModels/ParticipantRegistrationRequest.cs @@ -14,6 +14,8 @@ public class ParticipantRegistrationRequest public string SurName { get; set; } = string.Empty; [Required] + [RegularExpression(@"^([1-9]|Gymnasium [1-3])$", + ErrorMessage = "Grade must be 1–9 or Gymnasium 1–3.")] public string Grade { get; set; } = string.Empty; public string? PhoneNumber { get; set; } diff --git a/src/Registration/Registration.API/Services/AuthService.cs b/src/Registration/Registration.API/Services/AuthService.cs index 17e7530..cf069c7 100644 --- a/src/Registration/Registration.API/Services/AuthService.cs +++ b/src/Registration/Registration.API/Services/AuthService.cs @@ -5,7 +5,6 @@ namespace Registration.API.Services; public class AuthService(HttpClient httpClient, IOptions options) : IAuthService { - public const string HttpClientName = "AuthApi"; private readonly HttpClient _httpClient = httpClient; private readonly AuthApiOptions _options = options.Value; diff --git a/src/Registration/Registration.API/Services/VbytesParticipantRelayService.cs b/src/Registration/Registration.API/Services/VbytesParticipantRelayService.cs index de6a269..0eb51a9 100644 --- a/src/Registration/Registration.API/Services/VbytesParticipantRelayService.cs +++ b/src/Registration/Registration.API/Services/VbytesParticipantRelayService.cs @@ -11,8 +11,6 @@ public class VbytesParticipantRelayService( ILogger logger) : VbytesRelayServiceBase(httpClient, options, logger), IVbytesParticipantRelayService { - public const string HttpClientName = "VbytesRelay"; - private readonly ILogger _logger = logger; public async Task RegisterParticipantAsync(Participant participant, CancellationToken cancellationToken = default) diff --git a/src/Registration/Registration.API/Services/VbytesVolunteerRelayService.cs b/src/Registration/Registration.API/Services/VbytesVolunteerRelayService.cs index 8a0017b..95d729d 100644 --- a/src/Registration/Registration.API/Services/VbytesVolunteerRelayService.cs +++ b/src/Registration/Registration.API/Services/VbytesVolunteerRelayService.cs @@ -16,7 +16,11 @@ public class VbytesVolunteerRelayService( public async Task RegisterVolunteerAsync(Volunteer volunteer, CancellationToken cancellationToken = default) { var configError = ValidateConfiguration(); - if (configError is not null) return configError; + if (configError is not null) + { + _logger.LogError("Relay configuration invalid: {Message}", configError.Message); + return configError; + } try { diff --git a/src/Registration/Registration.API/Validation/InputNormalizationExtensions.cs b/src/Registration/Registration.API/Validation/InputNormalizationExtensions.cs new file mode 100644 index 0000000..51fa4df --- /dev/null +++ b/src/Registration/Registration.API/Validation/InputNormalizationExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Registration.API.Validation; + +public static class InputNormalizationExtensions +{ + public static string NormalizePhone(this string phone) => + InputNormalization.NormalizeSwedishMobile(phone); + + public static string? NormalizeOptionalPhone(this string? phone) => + string.IsNullOrWhiteSpace(phone) ? null : InputNormalization.NormalizeSwedishMobile(phone); + + public static bool TryNormalizeSwedishSsn(this string ssn, out string normalized, out IActionResult? errorResult) + { + normalized = InputNormalization.NormalizeSsn(ssn); + + if (InputNormalization.IsValidSsn(normalized)) + { + errorResult = null; + return true; + } + + errorResult = new BadRequestObjectResult(new ValidationProblemDetails( + new Dictionary + { + ["ssn"] = ["SSN must contain 10 or 12 digits."] + }) + { + Title = "One or more validation errors occurred.", + Status = StatusCodes.Status400BadRequest + }); + + return false; + } +} diff --git a/src/Registration/Registration.Domain/Models/AreaOfInterest.cs b/src/Registration/Registration.Domain/Models/AreaOfInterest.cs index 073fb85..7da6cfe 100644 --- a/src/Registration/Registration.Domain/Models/AreaOfInterest.cs +++ b/src/Registration/Registration.Domain/Models/AreaOfInterest.cs @@ -1,6 +1,6 @@ namespace Registration.Domain.Models; -public class AreasOfInterest +public class AreaOfInterest { public required string Name { get; set; } } diff --git a/src/Registration/Registration.Domain/Models/Volunteer.cs b/src/Registration/Registration.Domain/Models/Volunteer.cs index 5b405dc..c62dd3f 100644 --- a/src/Registration/Registration.Domain/Models/Volunteer.cs +++ b/src/Registration/Registration.Domain/Models/Volunteer.cs @@ -7,5 +7,5 @@ public class Volunteer public required string PhoneNumber { get; set; } public required string Email { get; set; } public required bool HasApprovedGdpr { get; set; } - public required List AreasOfInterest { get; set; } + public required List AreasOfInterest { get; set; } } diff --git a/src/Web/lan-frontend/app/register/page.tsx b/src/Web/lan-frontend/app/register/page.tsx index 69879c1..5ebb383 100644 --- a/src/Web/lan-frontend/app/register/page.tsx +++ b/src/Web/lan-frontend/app/register/page.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "react"; import Image from "next/image"; import Link from "next/link"; -import router from "next/router"; +import { useRouter } from "next/navigation"; interface EventContent { registrationEnabled: boolean; @@ -29,6 +29,7 @@ const isValidMobileNumber = (value: string) => /^07\d{8}$/.test(normalizeMobileNumber(value)); export default function RegisterPage() { + const router = useRouter(); const [content, setContent] = useState(null); const [formData, setFormData] = useState({ firstName: "", @@ -355,15 +356,30 @@ export default function RegisterPage() { > Årskurs * - + > + + + {["1", "2", "3", "4", "5", "6", "7", "8", "9"].map((g) => ( + + ))} + + + {["1", "2", "3"].map((g) => ( + + ))} + + {fieldErrors.grade && (

{fieldErrors.grade}

)}