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")]
|
||||
public async Task<IActionResult> 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ namespace Registration.API.Controllers
|
|||
[HttpGet("register/{ssn}")]
|
||||
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);
|
||||
|
|
@ -25,11 +25,11 @@ namespace Registration.API.Controllers
|
|||
}
|
||||
|
||||
[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);
|
||||
|
|
@ -40,9 +40,9 @@ namespace Registration.API.Controllers
|
|||
[HttpGet("registered/{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);
|
||||
|
|
@ -50,30 +50,12 @@ namespace Registration.API.Controllers
|
|||
}
|
||||
|
||||
[HttpDelete("clear")]
|
||||
public async Task<IActionResult> ClearRegistrations()
|
||||
public async Task<IActionResult> 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<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")]
|
||||
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
|
||||
.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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,33 +16,34 @@ var relayOptions = builder.Configuration.GetSection("VbytesRelay").Get<VbytesRel
|
|||
var certificate = X509CertificateLoader.LoadPkcs12FromFile(
|
||||
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();
|
||||
handler.ClientCertificates.Add(certificate);
|
||||
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 =>
|
||||
{
|
||||
o.BaseAddress = new Uri(relayOptions.BaseUrl);
|
||||
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.AddHttpClient<IAuthService, AuthService>(o =>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ namespace Registration.API.Services;
|
|||
|
||||
public class AuthService(HttpClient httpClient, IOptions<AuthApiOptions> options) : IAuthService
|
||||
{
|
||||
public const string HttpClientName = "AuthApi";
|
||||
private readonly HttpClient _httpClient = httpClient;
|
||||
private readonly AuthApiOptions _options = options.Value;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ public class VbytesParticipantRelayService(
|
|||
ILogger<VbytesParticipantRelayService> logger)
|
||||
: VbytesRelayServiceBase(httpClient, options, logger), IVbytesParticipantRelayService
|
||||
{
|
||||
public const string HttpClientName = "VbytesRelay";
|
||||
|
||||
private readonly ILogger<VbytesParticipantRelayService> _logger = logger;
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
public class AreasOfInterest
|
||||
public class AreaOfInterest
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> 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 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<EventContent | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: "",
|
||||
|
|
@ -355,15 +356,30 @@ export default function RegisterPage() {
|
|||
>
|
||||
Årskurs *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
<select
|
||||
name="grade"
|
||||
id="grade"
|
||||
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"}`}
|
||||
value={formData.grade}
|
||||
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 && (
|
||||
<p className="mt-1 text-xs text-red-600">{fieldErrors.grade}</p>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue