add validation and remove debug and add errorlogs

This commit is contained in:
Kruille 2026-02-23 21:21:44 +01:00
parent 0f673c6a6e
commit 3c036a4891
13 changed files with 224 additions and 30 deletions

View file

@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
using AuthAPI;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.SetMinimumLevel(LogLevel.Error);
builder.Services.AddHttpClient();
builder.Services.Configure<AuthSettings>(
builder.Configuration.GetSection("EnvironmentVariables"));
@ -18,7 +19,8 @@ app.MapPost("/validate", async (
[FromBody] Request validationRequest,
HttpClient httpClient,
MemberValidationService memberService,
IOptions<AuthSettings> settings) =>
IOptions<AuthSettings> settings,
ILogger<Program> logger) =>
{
var request = memberService.GetRequestWithApiKey(validationRequest);
if (request.IsFailure)
@ -29,10 +31,12 @@ app.MapPost("/validate", async (
request.Success);
if (!response.IsSuccessStatusCode)
{
logger.LogError("Sverok validation returned non-success status code: {StatusCode}", (int)response.StatusCode);
return Results.StatusCode((int)response.StatusCode);
}
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"[DEBUG] Sverok response: {content}");
return content.Contains("\"member_found\":true") ? Results.Ok() : Results.NotFound();
})
.WithName("ValidateMember");

View file

@ -7,5 +7,5 @@ public class VbytesRelayOptions
public string ApiKeyHeaderName { get; set; } = "X-Api-Key";
public string ApiKey { get; set; } = string.Empty;
public string ClientCertificatePfxPath { get; set; } = string.Empty;
public string VolunteerRegisterPath { get; set; } = "/api/volunteer/register";
public string VolunteerRegisterPath { get; set; } = string.Empty;
}

View file

@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Registration.API.RequestModels;
using Registration.API.Services;
using Registration.API.Validation;
using Registration.Domain.Models;
namespace Registration.API.Controllers;
@ -11,8 +13,33 @@ public class ParticipantController(IVbytesParticipantRelayService relayService)
private readonly IVbytesParticipantRelayService _relayService = relayService;
[HttpPost("register")]
public async Task<IActionResult> RegisterForLan([FromBody] Participant participant, CancellationToken cancellationToken)
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 participant = new Participant
{
IsMember = request.IsMember!.Value,
FirstName = request.FirstName.Trim(),
SurName = request.SurName.Trim(),
Grade = request.Grade.Trim(),
PhoneNumber = normalizedParticipantPhone,
Email = request.Email?.Trim(),
GuardianName = request.GuardianName.Trim(),
GuardianPhoneNumber = normalizedGuardianPhone,
GuardianEmail = request.GuardianEmail.Trim(),
IsVisitor = request.IsVisitor!.Value,
HasApprovedGdpr = request.HasApprovedGdpr!.Value,
Friends = request.Friends?.Trim(),
SpecialDiet = request.SpecialDiet?.Trim(),
};
var result = await _relayService.RegisterParticipantAsync(participant, cancellationToken);
if (result.StatusCode == 200 && !result.Message.Contains("401"))

View file

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Registration.API.Services;
using Registration.API.Validation;
using Registration.Infra.Repositories;
namespace Registration.API.Controllers
@ -14,14 +15,24 @@ namespace Registration.API.Controllers
[HttpGet("register/{ssn}")]
public async Task<IActionResult> ValidateSsn(string ssn, CancellationToken cancellationToken)
{
var isMember = await _authService.IsMemberAsync(ssn, cancellationToken);
if (!TryNormalizeSsn(ssn, out var normalizedSsn, out var errorResult))
{
return errorResult;
}
var isMember = await _authService.IsMemberAsync(normalizedSsn, cancellationToken);
return isMember ? Ok() : NotFound();
}
[HttpPost("register/{ssn}")]
public async Task<IActionResult> RegisterMember(string ssn)
{
var added = await _memberRepository.AddRegistration(ssn);
if (!TryNormalizeSsn(ssn, out var normalizedSsn, out var errorResult))
{
return errorResult;
}
var added = await _memberRepository.AddRegistration(normalizedSsn);
return added ? Ok() : Conflict();
}
@ -29,7 +40,12 @@ namespace Registration.API.Controllers
[HttpGet("registered/{ssn}")]
public async Task<IActionResult> IsMemberRegistered(string ssn)
{
var isRegistered = await _memberRepository.GetIsRegistered(ssn);
if (!TryNormalizeSsn(ssn, out var normalizedSsn, out var errorResult))
{
return errorResult;
}
var isRegistered = await _memberRepository.GetIsRegistered(normalizedSsn);
return isRegistered ? Conflict() : Ok();
}
@ -39,5 +55,25 @@ namespace Registration.API.Controllers
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;
}
}
}

View file

@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Registration.API.RequestModels;
using Registration.API.Services;
using Registration.API.Validation;
using Registration.Domain.Models;
namespace Registration.API.Controllers;
@ -11,8 +13,35 @@ public class VolunteerController(IVbytesVolunteerRelayService relayService) : Co
private readonly IVbytesVolunteerRelayService _relayService = relayService;
[HttpPost("register")]
public async Task<IActionResult> RegisterVolunteer([FromBody] Volunteer volunteer, CancellationToken cancellationToken)
public async Task<IActionResult> RegisterVolunteer([FromBody] VolunteerRegistrationRequest request, CancellationToken cancellationToken)
{
var normalizedPhone = InputNormalization.NormalizeSwedishMobile(request.PhoneNumber);
var trimmedAreas = request.AreasOfInterest
.Select(area => area.Name.Trim())
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToList();
if (trimmedAreas.Count == 0)
{
ModelState.AddModelError(nameof(request.AreasOfInterest), "At least one valid area of interest is required.");
}
if (!ModelState.IsValid)
{
return ValidationProblem(ModelState);
}
var volunteer = new Volunteer
{
FirstName = request.FirstName.Trim(),
SurName = request.SurName.Trim(),
PhoneNumber = normalizedPhone,
Email = request.Email.Trim(),
HasApprovedGdpr = request.HasApprovedGdpr!.Value,
AreasOfInterest = trimmedAreas.Select(name => new AreasOfInterest { Name = name }).ToList()
};
var result = await _relayService.RegisterVolunteerAsync(volunteer, cancellationToken);
if (result.StatusCode == 200 && !result.Message.Contains("401"))

View file

@ -4,6 +4,7 @@ using Registration.API.Configuration;
using Registration.API.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.SetMinimumLevel(LogLevel.Error);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

View file

@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace Registration.API.RequestModels;
public class AreaOfInterestRequest
{
[Required]
public string Name { get; set; } = string.Empty;
}

View file

@ -0,0 +1,43 @@
using System.ComponentModel.DataAnnotations;
namespace Registration.API.RequestModels;
public class ParticipantRegistrationRequest
{
[Required]
public bool? IsMember { get; set; }
[Required]
public string FirstName { get; set; } = string.Empty;
[Required]
public string SurName { get; set; } = string.Empty;
[Required]
public string Grade { get; set; } = string.Empty;
public string? PhoneNumber { get; set; }
[EmailAddress]
public string? Email { get; set; }
[Required]
public string GuardianName { get; set; } = string.Empty;
[Required]
public string GuardianPhoneNumber { get; set; } = string.Empty;
[Required]
[EmailAddress]
public string GuardianEmail { get; set; } = string.Empty;
[Required]
public bool? IsVisitor { get; set; }
[Required]
public bool? HasApprovedGdpr { get; set; }
public string? Friends { get; set; }
public string? SpecialDiet { get; set; }
}

View file

@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
namespace Registration.API.RequestModels;
public class VolunteerRegistrationRequest
{
[Required]
public string FirstName { get; set; } = string.Empty;
[Required]
public string SurName { get; set; } = string.Empty;
[Required]
public string PhoneNumber { get; set; } = string.Empty;
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
public bool? HasApprovedGdpr { get; set; }
[Required]
[MinLength(1)]
public List<AreaOfInterestRequest> AreasOfInterest { get; set; } = [];
}

View file

@ -9,7 +9,7 @@ public class VbytesParticipantRelayService(
HttpClient httpClient,
IOptions<VbytesRelayOptions> options,
ILogger<VbytesParticipantRelayService> logger)
: VbytesRelayServiceBase(httpClient, options), IVbytesParticipantRelayService
: VbytesRelayServiceBase(httpClient, options, logger), IVbytesParticipantRelayService
{
public const string HttpClientName = "VbytesRelay";
@ -20,24 +20,22 @@ public class VbytesParticipantRelayService(
var configError = ValidateConfiguration();
if (configError is not null)
{
_logger.LogWarning("Relay configuration invalid: {Message}", configError.Message);
_logger.LogError("Relay configuration invalid: {Message}", configError.Message);
return configError;
}
var payload = Map(participant);
_logger.LogInformation("Relaying participant to {Path}. Payload: {@Payload}", Options.ParticipantRegisterPath, payload);
try
{
var result = await SendAsync(Options.ParticipantRegisterPath, payload, cancellationToken);
_logger.LogInformation("Relay response: {StatusCode} - {Message}", result.StatusCode, result.Message);
return result;
}
catch (TaskCanceledException)
{
_logger.LogWarning("Relay timed out calling {Path}", Options.ParticipantRegisterPath);
_logger.LogError("Relay timed out calling {Path}", Options.ParticipantRegisterPath);
return new VbytesRelayResult(false, StatusCodes.Status504GatewayTimeout, "Upstream timeout.");
}

View file

@ -1,4 +1,3 @@
using System.Text.Json;
using Microsoft.Extensions.Options;
using Registration.API.Configuration;
@ -6,9 +5,11 @@ namespace Registration.API.Services;
public abstract class VbytesRelayServiceBase(
HttpClient httpClient,
IOptions<VbytesRelayOptions> options)
IOptions<VbytesRelayOptions> options,
ILogger logger)
{
private readonly HttpClient _httpClient = httpClient;
private readonly ILogger _logger = logger;
protected readonly VbytesRelayOptions Options = options.Value;
protected async Task<VbytesRelayResult> SendAsync(string path, object payload, CancellationToken cancellationToken)
@ -22,16 +23,16 @@ public abstract class VbytesRelayServiceBase(
using var response = await _httpClient.SendAsync(request, cancellationToken);
var body = await response.Content.ReadAsStringAsync(cancellationToken);
var safeBody = body.Length > 600 ? body[..600] : body;
if (response.IsSuccessStatusCode)
{
Console.WriteLine($"[DEBUG] Relay success body: {body}");
return new VbytesRelayResult(true, StatusCodes.Status200OK, body);
}
var statusCode = (int)response.StatusCode;
Console.WriteLine($"[DEBUG] Relay error body: {body}");
var message = body.Length > 600 ? body[..600] : body;
_logger.LogError("Outbound relay response error: {StatusCode}. Body: {Body}", statusCode, safeBody);
var message = safeBody;
return new VbytesRelayResult(false, statusCode, string.IsNullOrWhiteSpace(message) ? "Upstream request failed." : message);
}
@ -60,17 +61,6 @@ public abstract class VbytesRelayServiceBase(
return path.StartsWith('/') ? path : $"/{path}";
}
private static async Task<string> ReadSafeErrorMessage(HttpResponseMessage response, CancellationToken cancellationToken)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(body))
{
return "Upstream request failed.";
}
return body.Length > 600 ? body[..600] : body;
}
protected static bool IsConfigured(string value)
{
return !string.IsNullOrWhiteSpace(value) && !value.StartsWith("__SET_", StringComparison.Ordinal);

View file

@ -9,7 +9,7 @@ public class VbytesVolunteerRelayService(
HttpClient httpClient,
IOptions<VbytesRelayOptions> options,
ILogger<VbytesVolunteerRelayService> logger)
: VbytesRelayServiceBase(httpClient, options), IVbytesVolunteerRelayService
: VbytesRelayServiceBase(httpClient, options, logger), IVbytesVolunteerRelayService
{
private readonly ILogger<VbytesVolunteerRelayService> _logger = logger;

View file

@ -0,0 +1,31 @@
using System.Text.RegularExpressions;
namespace Registration.API.Validation;
public static partial class InputNormalization
{
public static string NormalizeSsn(string value)
{
var raw = value ?? string.Empty;
return NonDigitsRegex().Replace(raw, string.Empty);
}
public static bool IsValidSsn(string value)
{
return value.Length is 10 or 12;
}
public static string NormalizeSwedishMobile(string value)
{
var digitsOnly = NonDigitsRegex().Replace(value ?? string.Empty, string.Empty);
if (digitsOnly.StartsWith("0046")) return $"0{digitsOnly[4..]}";
if (digitsOnly.StartsWith("46")) return $"0{digitsOnly[2..]}";
if (digitsOnly.StartsWith('7')) return $"0{digitsOnly}";
return digitsOnly;
}
[GeneratedRegex("\\D")]
private static partial Regex NonDigitsRegex();
}