add validation and remove debug and add errorlogs
This commit is contained in:
parent
0f673c6a6e
commit
3c036a4891
13 changed files with 224 additions and 30 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Registration.API.RequestModels;
|
||||
|
||||
public class AreaOfInterestRequest
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; } = [];
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
Loading…
Reference in a new issue