diff --git a/docker-compose.yml b/docker-compose.yml index 73564a7..176775b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,9 +24,9 @@ services: environment: - ASPNETCORE_ENVIRONMENT=Production # These should be replaced with real values or set in an .env file - - EnvironmentVariables__ApiUrl="" - - EnvironmentVariables__ApiKey="" - - EnvironmentVariables__AssociationNumber="" + - EnvironmentVariables__ApiUrl="https://ebas.sverok.se/apis/confirm_membership.json" + - EnvironmentVariables__ApiKey="stHvwoptoHxobOaXKQOyyNbqrv0EJQLx" + - EnvironmentVariables__AssociationNumber="F200833-2" depends_on: - postgres @@ -43,14 +43,14 @@ services: - ConnectionStrings__DefaultConnection=Host=postgres;Username=postgres;Password=postgres;Database=postgres;Port=5432 - AuthApi__BaseUrl=http://auth-api:8080 - VbytesRelay__BaseUrl=https://api.lan.vbytes.se - - VbytesRelay__ApiKey="" - - VbytesRelay__ClientCertificatePfxPath="" - - Security__SsnPepper="" + - VbytesRelay__ApiKey=${VBYTES_RELAY_API_KEY} + - VbytesRelay__ClientCertificatePfxPath=/app/certs/server.pfx + - Security__SsnPepper=${SSN_PEPPER} - VbytesRelay__VolunteerRegisterPath=/api/volunteer - VbytesRelay__ParticipantRegisterPath=/api/participant - Admin__Password=admin volumes: - - ./server.pfx:/app/certs/server.pfx:ro + - ./server.pfx:/app/certs/server.pfx:ro,z depends_on: - postgres - auth-api diff --git a/src/Registration/Registration.API/Controllers/ParticipantController.cs b/src/Registration/Registration.API/Controllers/ParticipantController.cs index a2ce064..85b5262 100644 --- a/src/Registration/Registration.API/Controllers/ParticipantController.cs +++ b/src/Registration/Registration.API/Controllers/ParticipantController.cs @@ -3,18 +3,26 @@ using Registration.API.RequestModels; using Registration.API.Services; using Registration.API.Validation; using Registration.Domain.Models; +using Registration.Infra.Repositories; namespace Registration.API.Controllers; [Route("api/[controller]")] [ApiController] -public class ParticipantController(IVbytesParticipantRelayService relayService) : ControllerBase +public class ParticipantController(IVbytesParticipantRelayService relayService, IRegistrationRepository registrationRepository) : ControllerBase { private readonly IVbytesParticipantRelayService _relayService = relayService; + private readonly IRegistrationRepository _registrationRepository = registrationRepository; [HttpPost("register")] public async Task RegisterForLan([FromBody] ParticipantRegistrationRequest request, CancellationToken cancellationToken) { + var content = await _registrationRepository.GetEventContent(); + if (!content.RegistrationEnabled) + { + return StatusCode(StatusCodes.Status403Forbidden); + } + var normalizedGuardianPhone = request.GuardianPhoneNumber.NormalizePhone(); var normalizedParticipantPhone = request.PhoneNumber.NormalizeOptionalPhone(); @@ -31,7 +39,7 @@ public class ParticipantController(IVbytesParticipantRelayService relayService) GuardianEmail = request.GuardianEmail.Trim(), IsVisitor = request.IsVisitor!.Value, HasApprovedGdpr = request.HasApprovedGdpr!.Value, - Friends = request.Friends?.Trim(), + Friends = request.IsVisitor == true ? null : request.Friends?.Trim(), SpecialDiet = request.SpecialDiet?.Trim(), }; diff --git a/src/Registration/Registration.API/Controllers/RegistrationController.cs b/src/Registration/Registration.API/Controllers/RegistrationController.cs index 8316df8..c1f0a29 100644 --- a/src/Registration/Registration.API/Controllers/RegistrationController.cs +++ b/src/Registration/Registration.API/Controllers/RegistrationController.cs @@ -32,6 +32,12 @@ public class RegistrationController(IRegistrationRepository registrationReposito return errorResult!; } + var content = await _registrationRepository.GetEventContent(); + if (!content.RegistrationEnabled) + { + return StatusCode(StatusCodes.Status403Forbidden); + } + var added = await _registrationRepository.AddRegistration(normalizedSsn); return added ? Ok() : Conflict(); diff --git a/src/Registration/Registration.API/Controllers/VolunteerController.cs b/src/Registration/Registration.API/Controllers/VolunteerController.cs index c19d848..b3f4770 100644 --- a/src/Registration/Registration.API/Controllers/VolunteerController.cs +++ b/src/Registration/Registration.API/Controllers/VolunteerController.cs @@ -3,18 +3,26 @@ using Registration.API.RequestModels; using Registration.API.Services; using Registration.API.Validation; using Registration.Domain.Models; +using Registration.Infra.Repositories; namespace Registration.API.Controllers; [Route("api/[controller]")] [ApiController] -public class VolunteerController(IVbytesVolunteerRelayService relayService) : ControllerBase +public class VolunteerController(IVbytesVolunteerRelayService relayService, IRegistrationRepository registrationRepository) : ControllerBase { private readonly IVbytesVolunteerRelayService _relayService = relayService; + private readonly IRegistrationRepository _registrationRepository = registrationRepository; [HttpPost("register")] public async Task RegisterVolunteer([FromBody] VolunteerRegistrationRequest request, CancellationToken cancellationToken) { + var content = await _registrationRepository.GetEventContent(); + if (!content.VolunteerRegistrationEnabled) + { + return StatusCode(StatusCodes.Status403Forbidden); + } + var normalizedPhone = request.PhoneNumber.NormalizePhone(); var trimmedAreas = request.AreasOfInterest diff --git a/src/Registration/Registration.API/DtoModels/ParticipantRelayDto.cs b/src/Registration/Registration.API/DtoModels/ParticipantRelayDto.cs index adb59be..308072f 100644 --- a/src/Registration/Registration.API/DtoModels/ParticipantRelayDto.cs +++ b/src/Registration/Registration.API/DtoModels/ParticipantRelayDto.cs @@ -6,6 +6,7 @@ public record ParticipantRelayDto( [property: JsonPropertyName("member")] bool Member, [property: JsonPropertyName("first_name")] string FirstName, [property: JsonPropertyName("surname")] string Surname, + [property: JsonPropertyName("ssn")] string? Ssn, [property: JsonPropertyName("grade")] string Grade, [property: JsonPropertyName("phone")] string? Phone, [property: JsonPropertyName("email")] string? Email, diff --git a/src/Registration/Registration.API/RequestModels/ParticipantRegistrationRequest.cs b/src/Registration/Registration.API/RequestModels/ParticipantRegistrationRequest.cs index ee48fc1..f8a6fdb 100644 --- a/src/Registration/Registration.API/RequestModels/ParticipantRegistrationRequest.cs +++ b/src/Registration/Registration.API/RequestModels/ParticipantRegistrationRequest.cs @@ -20,8 +20,8 @@ public class ParticipantRegistrationRequest public string SurName { get; set; } = string.Empty; [Required] - [RegularExpression(@"^([1-9]|Gymnasium [1-3])$", - ErrorMessage = "Årkurs måste vara 1–9 eller Gymnasium 1–3.")] + [RegularExpression(@"^([1-9]|1[1-3])$", + ErrorMessage = "Årkurs måste vara 1–9 eller 11–13.")] public string Grade { get; set; } = string.Empty; public string? PhoneNumber { get; set; } diff --git a/src/Registration/Registration.API/Services/VbytesParticipantRelayService.cs b/src/Registration/Registration.API/Services/VbytesParticipantRelayService.cs index 0eb51a9..cd93989 100644 --- a/src/Registration/Registration.API/Services/VbytesParticipantRelayService.cs +++ b/src/Registration/Registration.API/Services/VbytesParticipantRelayService.cs @@ -49,6 +49,7 @@ public class VbytesParticipantRelayService( Member: participant.IsMember, FirstName: participant.FirstName, Surname: participant.SurName, + Ssn: null, Grade: participant.Grade, Phone: participant.PhoneNumber, Email: participant.Email, diff --git a/src/Registration/Registration.Tests/Controllers/ParticipantControllerTests.cs b/src/Registration/Registration.Tests/Controllers/ParticipantControllerTests.cs index febf4cc..229f5da 100644 --- a/src/Registration/Registration.Tests/Controllers/ParticipantControllerTests.cs +++ b/src/Registration/Registration.Tests/Controllers/ParticipantControllerTests.cs @@ -4,14 +4,20 @@ using Registration.API.Controllers; using Registration.API.RequestModels; using Registration.API.Services; using Registration.Domain.Models; +using Registration.Infra.Repositories; namespace Registration.Tests.Controllers; public class ParticipantControllerTests { private readonly Mock _relay = new(); + private readonly Mock _repo = new(); - private ParticipantController Sut() => new(_relay.Object); + private ParticipantController Sut(EventContent? content = null) + { + _repo.Setup(r => r.GetEventContent()).ReturnsAsync(content ?? new EventContent()); + return new(_relay.Object, _repo.Object); + } private static ParticipantRegistrationRequest ValidRequest() => new() { @@ -73,6 +79,33 @@ public class ParticipantControllerTests Assert.Null(captured!.PhoneNumber); } + [Fact] + public async Task RegisterForLan_returns_Forbidden_when_registration_disabled() + { + var result = await Sut(new EventContent { RegistrationEnabled = false }) + .RegisterForLan(ValidRequest(), CancellationToken.None); + + Assert.IsType(result); + Assert.Equal(403, ((StatusCodeResult)result).StatusCode); + _relay.Verify(r => r.RegisterParticipantAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task RegisterForLan_clears_friends_for_visitors() + { + Participant? captured = null; + _relay.Setup(r => r.RegisterParticipantAsync(It.IsAny(), It.IsAny())) + .Callback((p, _) => captured = p) + .ReturnsAsync(new VbytesRelayResult(true, 200, "ok")); + + var req = ValidRequest(); + req.IsVisitor = true; + + await Sut().RegisterForLan(req, CancellationToken.None); + + Assert.Null(captured!.Friends); + } + [Fact] public async Task RegisterForLan_returns_Unauthorized_when_relay_fails() { diff --git a/src/Registration/Registration.Tests/Controllers/RegistrationControllerTests.cs b/src/Registration/Registration.Tests/Controllers/RegistrationControllerTests.cs index ad61e09..356d7cb 100644 --- a/src/Registration/Registration.Tests/Controllers/RegistrationControllerTests.cs +++ b/src/Registration/Registration.Tests/Controllers/RegistrationControllerTests.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Moq; using Registration.API.Controllers; using Registration.API.Services; +using Registration.Domain.Models; using Registration.Infra.Repositories; namespace Registration.Tests.Controllers; @@ -11,7 +12,11 @@ public class RegistrationControllerTests private readonly Mock _repo = new(); private readonly Mock _auth = new(); - private RegistrationController Sut() => new(_repo.Object, _auth.Object); + private RegistrationController Sut(EventContent? content = null) + { + _repo.Setup(r => r.GetEventContent()).ReturnsAsync(content ?? new EventContent()); + return new(_repo.Object, _auth.Object); + } [Fact] public async Task ValidateSsn_returns_400_for_invalid_ssn() @@ -74,6 +79,17 @@ public class RegistrationControllerTests Assert.IsType(result); } + [Fact] + public async Task RegisterMember_returns_Forbidden_when_registration_disabled() + { + var result = await Sut(new EventContent { RegistrationEnabled = false }) + .RegisterMember("19900101-1234", CancellationToken.None); + + Assert.IsType(result); + Assert.Equal(403, ((StatusCodeResult)result).StatusCode); + _repo.Verify(r => r.AddRegistration(It.IsAny()), Times.Never); + } + [Fact] public async Task IsMemberRegistered_returns_400_for_invalid_ssn() { diff --git a/src/Registration/Registration.Tests/Controllers/VolunteerControllerTests.cs b/src/Registration/Registration.Tests/Controllers/VolunteerControllerTests.cs index 9a98b89..5c33cb4 100644 --- a/src/Registration/Registration.Tests/Controllers/VolunteerControllerTests.cs +++ b/src/Registration/Registration.Tests/Controllers/VolunteerControllerTests.cs @@ -4,14 +4,20 @@ using Registration.API.Controllers; using Registration.API.RequestModels; using Registration.API.Services; using Registration.Domain.Models; +using Registration.Infra.Repositories; namespace Registration.Tests.Controllers; public class VolunteerControllerTests { private readonly Mock _relay = new(); + private readonly Mock _repo = new(); - private VolunteerController Sut() => new(_relay.Object); + private VolunteerController Sut(EventContent? content = null) + { + _repo.Setup(r => r.GetEventContent()).ReturnsAsync(content ?? new EventContent()); + return new(_relay.Object, _repo.Object); + } private static VolunteerRegistrationRequest ValidRequest() => new() { @@ -72,6 +78,17 @@ public class VolunteerControllerTests _relay.Verify(r => r.RegisterVolunteerAsync(It.IsAny(), It.IsAny()), Times.Never); } + [Fact] + public async Task RegisterVolunteer_returns_Forbidden_when_volunteer_registration_disabled() + { + var result = await Sut(new EventContent { VolunteerRegistrationEnabled = false }) + .RegisterVolunteer(ValidRequest(), CancellationToken.None); + + Assert.IsType(result); + Assert.Equal(403, ((StatusCodeResult)result).StatusCode); + _relay.Verify(r => r.RegisterVolunteerAsync(It.IsAny(), It.IsAny()), Times.Never); + } + [Fact] public async Task RegisterVolunteer_returns_Unauthorized_when_relay_fails() { diff --git a/src/Web/lan-frontend/app/register/page.tsx b/src/Web/lan-frontend/app/register/page.tsx index ee461c0..21675d8 100644 --- a/src/Web/lan-frontend/app/register/page.tsx +++ b/src/Web/lan-frontend/app/register/page.tsx @@ -241,7 +241,7 @@ export default function RegisterPage() { `${trimmedGuardianFirstName} ${trimmedGuardianLastName}`.trim(), guardianPhoneNumber: normalizedGuardianPhone, guardianEmail: trimmedGuardianEmail, - friends: formData.friends.trim(), + friends: formData.isVisitor ? "" : formData.friends.trim(), specialDiet: formData.specialDiet.trim(), isMember: isMember, }; @@ -258,19 +258,28 @@ export default function RegisterPage() { setMessage({ type: "success", text: isMember - ? "Registreringen är klar! Du är nu anmäld till LAN:et." - : "Din anmälan är mottagen, men det återstår ett viktigt steg för att du ska få delta.", + ? "Registreringen är klar! Du är nu anmäld till LAN:et. En bekräftelse skickas via e-post/SMS till vårdnadshavaren." + : "Din anmälan är mottagen, men det återstår ett viktigt steg för att du ska få delta. En bekräftelse skickas via e-post/SMS till vårdnadshavaren när anmälan är godkänd.", }); setShowBecomeMemberCta(!isMember); await fetch(`/api/Registration/register/${normalizedSsn}`, { method: "POST", }); + } else if (response.status === 403) { + setContent((prev) => (prev ? { ...prev, registrationEnabled: false } : prev)); + setMessage({ type: "", text: "" }); } else { - const errorData = await response.json(); + let text = "Registreringen misslyckades."; + try { + const errorData = await response.json(); + text = errorData.message || text; + } catch { + // response without JSON body + } setMessage({ type: "error", - text: errorData.message || "Registreringen misslyckades.", + text, }); } } catch (error) { @@ -452,11 +461,11 @@ export default function RegisterPage() { ))} - {["1", "2", "3"].map((g) => ( - - ))} + {["11", "12", "13"].map((g) => ( + + ))} {fieldErrors.grade && ( @@ -509,9 +518,13 @@ export default function RegisterPage() {
-

+

Vårdnadshavares Information

+

+ Bekräftelse via e-post och SMS skickas endast hit till + vårdnadshavaren – inte till den anmäldes uppgifter ovan. +

@@ -616,22 +629,24 @@ export default function RegisterPage() {
-
- - -
+ {!formData.isVisitor && ( +
+ + +
+ )}