Cleanrer validation, List in frontend for classes
This commit is contained in:
parent
378f7fbf2a
commit
aff4e3d557
12 changed files with 95 additions and 63 deletions
|
|
@ -15,13 +15,8 @@ public class ParticipantController(IVbytesParticipantRelayService relayService)
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
public async Task<IActionResult> RegisterForLan([FromBody] ParticipantRegistrationRequest request, CancellationToken cancellationToken)
|
public async Task<IActionResult> RegisterForLan([FromBody] ParticipantRegistrationRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var normalizedGuardianPhone = InputNormalization.NormalizeSwedishMobile(request.GuardianPhoneNumber);
|
var normalizedGuardianPhone = request.GuardianPhoneNumber.NormalizePhone();
|
||||||
|
var normalizedParticipantPhone = request.PhoneNumber.NormalizeOptionalPhone();
|
||||||
string? normalizedParticipantPhone = null;
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.PhoneNumber))
|
|
||||||
{
|
|
||||||
normalizedParticipantPhone = InputNormalization.NormalizeSwedishMobile(request.PhoneNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
var participant = new Participant
|
var participant = new Participant
|
||||||
{
|
{
|
||||||
|
|
@ -42,7 +37,7 @@ public class ParticipantController(IVbytesParticipantRelayService relayService)
|
||||||
|
|
||||||
var result = await _relayService.RegisterParticipantAsync(participant, cancellationToken);
|
var result = await _relayService.RegisterParticipantAsync(participant, cancellationToken);
|
||||||
|
|
||||||
if (result.StatusCode == 200 && !result.Message.Contains("401"))
|
if (result.Success)
|
||||||
{
|
{
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ namespace Registration.API.Controllers
|
||||||
[HttpGet("register/{ssn}")]
|
[HttpGet("register/{ssn}")]
|
||||||
public async Task<IActionResult> ValidateSsn(string ssn, CancellationToken cancellationToken)
|
public async Task<IActionResult> 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);
|
var isMember = await _authService.IsMemberAsync(normalizedSsn, cancellationToken);
|
||||||
|
|
@ -25,11 +25,11 @@ namespace Registration.API.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("register/{ssn}")]
|
[HttpPost("register/{ssn}")]
|
||||||
public async Task<IActionResult> RegisterMember(string ssn)
|
public async Task<IActionResult> 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);
|
var added = await _memberRepository.AddRegistration(normalizedSsn);
|
||||||
|
|
@ -40,9 +40,9 @@ namespace Registration.API.Controllers
|
||||||
[HttpGet("registered/{ssn}")]
|
[HttpGet("registered/{ssn}")]
|
||||||
public async Task<IActionResult> IsMemberRegistered(string ssn)
|
public async Task<IActionResult> 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);
|
var isRegistered = await _memberRepository.GetIsRegistered(normalizedSsn);
|
||||||
|
|
@ -50,30 +50,12 @@ namespace Registration.API.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("clear")]
|
[HttpDelete("clear")]
|
||||||
public async Task<IActionResult> ClearRegistrations()
|
public async Task<IActionResult> ClearRegistrations(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _memberRepository.ClearRegistrations();
|
await _memberRepository.ClearRegistrations();
|
||||||
return Ok(new { Message = "All registrations cleared." });
|
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<string, string[]>
|
|
||||||
{
|
|
||||||
["ssn"] = ["SSN must contain 10 or 12 digits."]
|
|
||||||
})
|
|
||||||
{
|
|
||||||
Title = "One or more validation errors occurred.",
|
|
||||||
Status = StatusCodes.Status400BadRequest
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ public class VolunteerController(IVbytesVolunteerRelayService relayService) : Co
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
public async Task<IActionResult> RegisterVolunteer([FromBody] VolunteerRegistrationRequest request, CancellationToken cancellationToken)
|
public async Task<IActionResult> RegisterVolunteer([FromBody] VolunteerRegistrationRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var normalizedPhone = InputNormalization.NormalizeSwedishMobile(request.PhoneNumber);
|
var normalizedPhone = request.PhoneNumber.NormalizePhone();
|
||||||
|
|
||||||
var trimmedAreas = request.AreasOfInterest
|
var trimmedAreas = request.AreasOfInterest
|
||||||
.Select(area => area.Name.Trim())
|
.Select(area => area.Name.Trim())
|
||||||
|
|
@ -39,12 +39,12 @@ public class VolunteerController(IVbytesVolunteerRelayService relayService) : Co
|
||||||
PhoneNumber = normalizedPhone,
|
PhoneNumber = normalizedPhone,
|
||||||
Email = request.Email.Trim(),
|
Email = request.Email.Trim(),
|
||||||
HasApprovedGdpr = request.HasApprovedGdpr!.Value,
|
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);
|
var result = await _relayService.RegisterVolunteerAsync(volunteer, cancellationToken);
|
||||||
|
|
||||||
if (result.StatusCode == 200 && !result.Message.Contains("401"))
|
if (result.Success)
|
||||||
{
|
{
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,33 +16,34 @@ var relayOptions = builder.Configuration.GetSection("VbytesRelay").Get<VbytesRel
|
||||||
var certificate = X509CertificateLoader.LoadPkcs12FromFile(
|
var certificate = X509CertificateLoader.LoadPkcs12FromFile(
|
||||||
relayOptions.ClientCertificatePfxPath, password: null);
|
relayOptions.ClientCertificatePfxPath, password: null);
|
||||||
|
|
||||||
builder.Services.AddSingleton(sp =>
|
var authApiOptions = builder.Configuration.GetSection("AuthApi").Get<AuthApiOptions>()
|
||||||
|
?? throw new InvalidOperationException("AuthApi configuration section is missing.");
|
||||||
|
|
||||||
|
builder.Services.Configure<VbytesRelayOptions>(builder.Configuration.GetSection("VbytesRelay"));
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient<IVbytesParticipantRelayService, VbytesParticipantRelayService>(o =>
|
||||||
|
{
|
||||||
|
o.BaseAddress = new Uri(relayOptions.BaseUrl);
|
||||||
|
o.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
})
|
||||||
|
.ConfigurePrimaryHttpMessageHandler(() =>
|
||||||
{
|
{
|
||||||
var handler = new HttpClientHandler();
|
var handler = new HttpClientHandler();
|
||||||
handler.ClientCertificates.Add(certificate);
|
handler.ClientCertificates.Add(certificate);
|
||||||
return handler;
|
return handler;
|
||||||
});
|
});
|
||||||
|
|
||||||
var authApiOptions = builder.Configuration.GetSection("AuthApi").Get<AuthApiOptions>()
|
|
||||||
?? throw new InvalidOperationException("AuthApi configuration section is missing.");
|
|
||||||
|
|
||||||
builder.Services.Configure<VbytesRelayOptions>(builder.Configuration.GetSection("VbytesRelay"));
|
|
||||||
|
|
||||||
|
|
||||||
builder.Services.AddHttpClient<IVbytesParticipantRelayService, VbytesParticipantRelayService>(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<HttpClientHandler>());
|
|
||||||
|
|
||||||
builder.Services.AddHttpClient<IVbytesVolunteerRelayService, VbytesVolunteerRelayService>(o =>
|
builder.Services.AddHttpClient<IVbytesVolunteerRelayService, VbytesVolunteerRelayService>(o =>
|
||||||
{
|
{
|
||||||
o.BaseAddress = new Uri(relayOptions.BaseUrl);
|
o.BaseAddress = new Uri(relayOptions.BaseUrl);
|
||||||
o.Timeout = TimeSpan.FromSeconds(30);
|
o.Timeout = TimeSpan.FromSeconds(30);
|
||||||
})
|
})
|
||||||
.ConfigurePrimaryHttpMessageHandler(sp => sp.GetRequiredService<HttpClientHandler>());
|
.ConfigurePrimaryHttpMessageHandler(() =>
|
||||||
|
{
|
||||||
|
var handler = new HttpClientHandler();
|
||||||
|
handler.ClientCertificates.Add(certificate);
|
||||||
|
return handler;
|
||||||
|
});
|
||||||
|
|
||||||
builder.Services.Configure<AuthApiOptions>(builder.Configuration.GetSection("AuthApi"));
|
builder.Services.Configure<AuthApiOptions>(builder.Configuration.GetSection("AuthApi"));
|
||||||
builder.Services.AddHttpClient<IAuthService, AuthService>(o =>
|
builder.Services.AddHttpClient<IAuthService, AuthService>(o =>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ public class ParticipantRegistrationRequest
|
||||||
public string SurName { get; set; } = string.Empty;
|
public string SurName { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Required]
|
[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 Grade { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string? PhoneNumber { get; set; }
|
public string? PhoneNumber { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ namespace Registration.API.Services;
|
||||||
|
|
||||||
public class AuthService(HttpClient httpClient, IOptions<AuthApiOptions> options) : IAuthService
|
public class AuthService(HttpClient httpClient, IOptions<AuthApiOptions> options) : IAuthService
|
||||||
{
|
{
|
||||||
public const string HttpClientName = "AuthApi";
|
|
||||||
private readonly HttpClient _httpClient = httpClient;
|
private readonly HttpClient _httpClient = httpClient;
|
||||||
private readonly AuthApiOptions _options = options.Value;
|
private readonly AuthApiOptions _options = options.Value;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ public class VbytesParticipantRelayService(
|
||||||
ILogger<VbytesParticipantRelayService> logger)
|
ILogger<VbytesParticipantRelayService> logger)
|
||||||
: VbytesRelayServiceBase(httpClient, options, logger), IVbytesParticipantRelayService
|
: VbytesRelayServiceBase(httpClient, options, logger), IVbytesParticipantRelayService
|
||||||
{
|
{
|
||||||
public const string HttpClientName = "VbytesRelay";
|
|
||||||
|
|
||||||
private readonly ILogger<VbytesParticipantRelayService> _logger = logger;
|
private readonly ILogger<VbytesParticipantRelayService> _logger = logger;
|
||||||
|
|
||||||
public async Task<VbytesRelayResult> RegisterParticipantAsync(Participant participant, CancellationToken cancellationToken = default)
|
public async Task<VbytesRelayResult> RegisterParticipantAsync(Participant participant, CancellationToken cancellationToken = default)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,11 @@ public class VbytesVolunteerRelayService(
|
||||||
public async Task<VbytesRelayResult> RegisterVolunteerAsync(Volunteer volunteer, CancellationToken cancellationToken = default)
|
public async Task<VbytesRelayResult> RegisterVolunteerAsync(Volunteer volunteer, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var configError = ValidateConfiguration();
|
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
|
try
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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<string, string[]>
|
||||||
|
{
|
||||||
|
["ssn"] = ["SSN must contain 10 or 12 digits."]
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Title = "One or more validation errors occurred.",
|
||||||
|
Status = StatusCodes.Status400BadRequest
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
namespace Registration.Domain.Models;
|
namespace Registration.Domain.Models;
|
||||||
|
|
||||||
public class AreasOfInterest
|
public class AreaOfInterest
|
||||||
{
|
{
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,5 @@ public class Volunteer
|
||||||
public required string PhoneNumber { get; set; }
|
public required string PhoneNumber { get; set; }
|
||||||
public required string Email { get; set; }
|
public required string Email { get; set; }
|
||||||
public required bool HasApprovedGdpr { get; set; }
|
public required bool HasApprovedGdpr { get; set; }
|
||||||
public required List<AreasOfInterest> AreasOfInterest { get; set; }
|
public required List<AreaOfInterest> AreasOfInterest { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import router from "next/router";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
interface EventContent {
|
interface EventContent {
|
||||||
registrationEnabled: boolean;
|
registrationEnabled: boolean;
|
||||||
|
|
@ -29,6 +29,7 @@ const isValidMobileNumber = (value: string) =>
|
||||||
/^07\d{8}$/.test(normalizeMobileNumber(value));
|
/^07\d{8}$/.test(normalizeMobileNumber(value));
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
|
const router = useRouter();
|
||||||
const [content, setContent] = useState<EventContent | null>(null);
|
const [content, setContent] = useState<EventContent | null>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
firstName: "",
|
firstName: "",
|
||||||
|
|
@ -355,15 +356,30 @@ export default function RegisterPage() {
|
||||||
>
|
>
|
||||||
Årskurs *
|
Årskurs *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<select
|
||||||
type="text"
|
|
||||||
name="grade"
|
name="grade"
|
||||||
id="grade"
|
id="grade"
|
||||||
required
|
required
|
||||||
className={`mt-1 block w-full rounded-md shadow-sm sm:text-sm p-2 border text-gray-900 ${fieldErrors.grade ? "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.grade ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"}`}
|
||||||
value={formData.grade}
|
value={formData.grade}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
>
|
||||||
|
<option value="">Välj årskurs</option>
|
||||||
|
<optgroup label="Grundskola">
|
||||||
|
{["1", "2", "3", "4", "5", "6", "7", "8", "9"].map((g) => (
|
||||||
|
<option key={g} value={g}>
|
||||||
|
Årskurs {g}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Gymnasium">
|
||||||
|
{["1", "2", "3"].map((g) => (
|
||||||
|
<option key={`gym${g}`} value={`Gymnasium ${g}`}>
|
||||||
|
Gymnasium {g}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
{fieldErrors.grade && (
|
{fieldErrors.grade && (
|
||||||
<p className="mt-1 text-xs text-red-600">{fieldErrors.grade}</p>
|
<p className="mt-1 text-xs text-red-600">{fieldErrors.grade}</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue