Fix for upcomming lan

This commit is contained in:
pmp 2026-09-13 13:47:34 +02:00
parent 204176e40a
commit d05a64e9a4
11 changed files with 147 additions and 42 deletions

View file

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

View file

@ -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<IActionResult> 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(),
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<IVbytesParticipantRelayService> _relay = new();
private readonly Mock<IRegistrationRepository> _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<StatusCodeResult>(result);
Assert.Equal(403, ((StatusCodeResult)result).StatusCode);
_relay.Verify(r => r.RegisterParticipantAsync(It.IsAny<Participant>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task RegisterForLan_clears_friends_for_visitors()
{
Participant? captured = null;
_relay.Setup(r => r.RegisterParticipantAsync(It.IsAny<Participant>(), It.IsAny<CancellationToken>()))
.Callback<Participant, CancellationToken>((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()
{

View file

@ -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<IRegistrationRepository> _repo = new();
private readonly Mock<IAuthService> _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<ConflictResult>(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<StatusCodeResult>(result);
Assert.Equal(403, ((StatusCodeResult)result).StatusCode);
_repo.Verify(r => r.AddRegistration(It.IsAny<string>()), Times.Never);
}
[Fact]
public async Task IsMemberRegistered_returns_400_for_invalid_ssn()
{

View file

@ -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<IVbytesVolunteerRelayService> _relay = new();
private readonly Mock<IRegistrationRepository> _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<Volunteer>(), It.IsAny<CancellationToken>()), 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<StatusCodeResult>(result);
Assert.Equal(403, ((StatusCodeResult)result).StatusCode);
_relay.Verify(r => r.RegisterVolunteerAsync(It.IsAny<Volunteer>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task RegisterVolunteer_returns_Unauthorized_when_relay_fails()
{

View file

@ -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() {
))}
</optgroup>
<optgroup label="Gymnasium">
{["1", "2", "3"].map((g) => (
<option key={`gym${g}`} value={`Gymnasium ${g}`}>
Gymnasium {g}
</option>
))}
{["11", "12", "13"].map((g) => (
<option key={`gym${g}`} value={g}>
Gymnasiet {g}
</option>
))}
</optgroup>
</select>
{fieldErrors.grade && (
@ -509,9 +518,13 @@ export default function RegisterPage() {
</div>
<div className="sm:col-span-2 pt-4 border-t border-gray-100 mt-4">
<h3 className="text-lg font-medium text-gray-900 mb-4">
<h3 className="text-lg font-medium text-gray-900 mb-1">
Vårdnadshavares Information
</h3>
<p className="text-xs text-gray-500 mb-4">
Bekräftelse via e-post och SMS skickas endast hit till
vårdnadshavaren – inte till den anmäldes uppgifter ovan.
</p>
</div>
<div>
@ -616,22 +629,24 @@ export default function RegisterPage() {
</h3>
</div>
<div className="sm:col-span-2">
<label
htmlFor="friends"
className="block text-sm font-medium text-gray-700"
>
"Önska" Vänner (att sitta jämte)
</label>
<input
type="text"
name="friends"
id="friends"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
value={formData.friends}
onChange={handleInputChange}
/>
</div>
{!formData.isVisitor && (
<div className="sm:col-span-2">
<label
htmlFor="friends"
className="block text-sm font-medium text-gray-700"
>
"Önska" Vänner (att sitta jämte)
</label>
<input
type="text"
name="friends"
id="friends"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
value={formData.friends}
onChange={handleInputChange}
/>
</div>
)}
<div className="sm:col-span-2">
<label