Fix for upcomming lan
This commit is contained in:
parent
204176e40a
commit
c73a54101c
11 changed files with 147 additions and 42 deletions
|
|
@ -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=${SVEROK_API_URL}
|
||||
- EnvironmentVariables__ApiKey=${SVEROK_API_KEY}
|
||||
- EnvironmentVariables__AssociationNumber=${SVEROK_ASSOCIATION_NUMBER}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue