From 9d9c9b3f387dda08d0e092022fe66bf9228493b9 Mon Sep 17 00:00:00 2001 From: Kruille Date: Thu, 19 Feb 2026 10:54:32 +0100 Subject: [PATCH] Relay all api calls --- .vscode/tasks.json | 107 ++++++++++++++++++ README.md | 89 +++++++++++++++ src/Auth/AuthAPI/AuthAPI.csproj | 3 +- src/Auth/AuthAPI/AuthAPI.http | 16 +-- src/Auth/AuthAPI/Program.cs | 7 +- src/Auth/AuthAPI/appsettings.Development.json | 4 +- src/Auth/AuthAPI/appsettings.example.json | 14 +++ src/Auth/AuthAPI/appsettings.json | 4 +- .../Configuration/AuthApiOptions.cs | 7 ++ .../Configuration/VbytesRelayOptions.cs | 11 ++ .../Controllers/ParticipantController.cs | 16 ++- .../Controllers/RegistrationController.cs | 17 +-- .../Controllers/VolunteerController.cs | 16 ++- .../DtoModels/ParticipantRelayDto.cs | 19 ++++ .../DtoModels/VolunteerRelayDto.cs | 12 ++ src/Registration/Registration.API/Program.cs | 45 +++++++- .../Properties/launchSettings.json | 10 +- .../Registration.API/Registration.API.csproj | 3 +- .../Registration.API/RegistrationAPI.http | 44 +++++++ .../Registration.API/Services/AuthService.cs | 22 ++++ .../Registration.API/Services/IAuthService.cs | 6 + .../IVbytesParticipantRelayService.cs | 10 ++ .../Services/IVbytesVolunteerRelayService.cs | 8 ++ .../Services/VbytesParticipantRelayService.cs | 63 +++++++++++ .../Services/VbytesRelayServiceBase.cs | 80 +++++++++++++ .../Services/VbytesVolunteerRelayService.cs | 44 +++++++ .../appsettings.Development.json | 16 ++- .../Registration.API/appsettings.example.json | 30 +++++ .../Registration.API/appsettings.json | 16 ++- .../Registration.Domain/Models/Volunteer.cs | 2 +- 30 files changed, 699 insertions(+), 42 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 src/Auth/AuthAPI/appsettings.example.json create mode 100644 src/Registration/Registration.API/Configuration/AuthApiOptions.cs create mode 100644 src/Registration/Registration.API/Configuration/VbytesRelayOptions.cs create mode 100644 src/Registration/Registration.API/DtoModels/ParticipantRelayDto.cs create mode 100644 src/Registration/Registration.API/DtoModels/VolunteerRelayDto.cs create mode 100644 src/Registration/Registration.API/Services/AuthService.cs create mode 100644 src/Registration/Registration.API/Services/IAuthService.cs create mode 100644 src/Registration/Registration.API/Services/IVbytesParticipantRelayService.cs create mode 100644 src/Registration/Registration.API/Services/IVbytesVolunteerRelayService.cs create mode 100644 src/Registration/Registration.API/Services/VbytesParticipantRelayService.cs create mode 100644 src/Registration/Registration.API/Services/VbytesRelayServiceBase.cs create mode 100644 src/Registration/Registration.API/Services/VbytesVolunteerRelayService.cs create mode 100644 src/Registration/Registration.API/appsettings.example.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..893fc6d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,107 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start: Postgres", + "type": "shell", + "command": "docker compose up -d", + "options": { + "cwd": "${workspaceFolder}" + }, + "group": "build", + "presentation": { + "reveal": "always", + "panel": "dedicated", + "group": "dev" + }, + "problemMatcher": [] + }, + { + "label": "Start: Registration.API", + "type": "shell", + "command": "dotnet run --project src/Registration/Registration.API --launch-profile https", + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": true, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "group": "dev" + }, + "problemMatcher": { + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": "Building\\.\\.\\.", + "endsPattern": "Now listening on:" + } + } + }, + { + "label": "Start: AuthAPI", + "type": "shell", + "command": "dotnet run --project src/Auth/AuthAPI", + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": true, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "group": "dev" + }, + "problemMatcher": { + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": "Building\\.\\.\\.", + "endsPattern": "Now listening on:" + } + } + }, + { + "label": "Start: Frontend", + "type": "shell", + "command": "npm run dev", + "options": { + "cwd": "${workspaceFolder}/src/Web/lan-frontend" + }, + "isBackground": true, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "group": "dev" + }, + "problemMatcher": { + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Local:.*localhost" + } + } + }, + { + "label": "Start: All", + "dependsOn": [ + "Start: Postgres", + "Start: Registration.API", + "Start: AuthAPI", + "Start: Frontend" + ], + "dependsOrder": "sequence", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md index 78e40b2..9c9f2df 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,92 @@ Micro Service For Registering For An Event ### Database - Postgres + +## Configuration + +Sensitive values are never stored in source control. Configure them via [user secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) (development) or environment variables (production). + +Key-only examples are available here: + +- `src/Registration/Registration.API/appsettings.example.json` +- `src/Auth/AuthAPI/appsettings.example.json` +- `src/Web/lan-frontend/.env.example` + +### Frontend (Next.js) + +Run from `src/Web/lan-frontend`: + +```bash +cp .env.example .env.local +``` + +Set the backend API base URL in `.env.local`: + +```env +REGISTRATION_API_URL=http://localhost:5063 +``` + +### Registration.API + +Run from `src/Registration/Registration.API`: + +```bash +dotnet user-secrets set "VbytesRelay:ApiKey" "" +dotnet user-secrets set "VbytesRelay:ClientCertificatePfxPath" ".pfx" +dotnet user-secrets set "VbytesRelay:ParticipantRegisterPath" "/api/participant" +dotnet user-secrets set "Security:SsnPepper" "" +``` + +| Key | Description | +| -------------------------------------- | ------------------------------------------------------------------------------------- | +| `VbytesRelay:ApiKey` | API key for the Vbytes relay service | +| `VbytesRelay:ClientCertificatePfxPath` | Absolute path to the client certificate `.pfx` file | +| `VbytesRelay:ParticipantRegisterPath` | Path for the participant registration endpoint on the relay (e.g. `/api/participant`) | +| `Security:SsnPepper` | Pepper value used when hashing SSNs | + +For non-development environments, ensure these environment variables are set: + +```bash +ConnectionStrings__DefaultConnection= +Security__SsnPepper= +VbytesRelay__BaseUrl=https://api.lan.vbytes.se +VbytesRelay__ParticipantRegisterPath=/api/participant +VbytesRelay__VolunteerRegisterPath=/api/volunteer +VbytesRelay__ApiKeyHeaderName=X-Api-Key +VbytesRelay__ApiKey= +VbytesRelay__ClientCertificatePfxPath=.pfx +AuthApi__BaseUrl= +AuthApi__ValidatePath=/validate +``` + +> **Note:** ASP.NET Core's configuration priority is `appsettings.json` → `appsettings.Development.json` → **user secrets** → **environment variables**. Environment variables always win, so any value set in `launchSettings.json` will override user secrets. Secret values (`ApiKey`, cert paths, relay paths) are intentionally absent from `launchSettings.json` to ensure user secrets are respected. + +### AuthAPI + +Run from `src/Auth/AuthAPI`: + +```bash +dotnet user-secrets set "EnvironmentVariables:ApiUrl" "" +dotnet user-secrets set "EnvironmentVariables:ApiKey" "" +dotnet user-secrets set "EnvironmentVariables:AssociationNumber" "" +``` + +| Key | Description | +| ---------------------------------------- | ------------------------------------------ | +| `EnvironmentVariables:ApiUrl` | Base URL of the member validation API | +| `EnvironmentVariables:ApiKey` | API key for the member validation API | +| `EnvironmentVariables:AssociationNumber` | Association number used for member lookups | + +For non-development environments, ensure these environment variables are set: + +```bash +EnvironmentVariables__ApiUrl= +EnvironmentVariables__ApiKey= +EnvironmentVariables__AssociationNumber= +``` + +For frontend production configuration, set: + +```bash +REGISTRATION_API_URL= +``` diff --git a/src/Auth/AuthAPI/AuthAPI.csproj b/src/Auth/AuthAPI/AuthAPI.csproj index d78f8bc..db53521 100644 --- a/src/Auth/AuthAPI/AuthAPI.csproj +++ b/src/Auth/AuthAPI/AuthAPI.csproj @@ -1,9 +1,10 @@ - + net9.0 enable enable + e53e6e14-fe65-43c6-82fd-0f1fb394679d diff --git a/src/Auth/AuthAPI/AuthAPI.http b/src/Auth/AuthAPI/AuthAPI.http index 233a19d..9ba1c40 100644 --- a/src/Auth/AuthAPI/AuthAPI.http +++ b/src/Auth/AuthAPI/AuthAPI.http @@ -1,20 +1,22 @@ @AuthAPI_HostAddress = http://localhost:5127 -GET {{AuthAPI_HostAddress}}/validate/ -Accept: application/json +# Validate by SSN +POST {{AuthAPI_HostAddress}}/validate Content-Type: application/json + { - "Email": "someValue", - "FirstName": "name" + "ssn": "8612125643" } ### -GET {{AuthAPI_HostAddress}}/validate/ -Accept: application/json +# Validate by email + first name +POST {{AuthAPI_HostAddress}}/validate Content-Type: application/json + { - "Ssn": "10 or 8 number length" + "email": "testwith@validemail.se", + "firstName": "AndCorrectName" } ### \ No newline at end of file diff --git a/src/Auth/AuthAPI/Program.cs b/src/Auth/AuthAPI/Program.cs index 992dfcf..f46afe5 100644 --- a/src/Auth/AuthAPI/Program.cs +++ b/src/Auth/AuthAPI/Program.cs @@ -5,7 +5,7 @@ using AuthAPI; var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient(); builder.Services.Configure( - builder.Configuration.GetSection("EnviromentVariables")); + builder.Configuration.GetSection("EnvironmentVariables")); builder.Services.AddScoped(); builder.Services.ConfigureHttpJsonOptions(options => { @@ -14,7 +14,7 @@ builder.Services.ConfigureHttpJsonOptions(options => var app = builder.Build(); -app.MapGet("/validate", async ( +app.MapPost("/validate", async ( [FromBody] Request validationRequest, HttpClient httpClient, MemberValidationService memberService, @@ -32,7 +32,8 @@ app.MapGet("/validate", async ( return Results.StatusCode((int)response.StatusCode); var content = await response.Content.ReadAsStringAsync(); - return content.Contains("\"member_found\":true,") ? Results.Ok() : Results.NotFound(); + Console.WriteLine($"[DEBUG] Sverok response: {content}"); + return content.Contains("\"member_found\":true") ? Results.Ok() : Results.NotFound(); }) .WithName("ValidateMember"); diff --git a/src/Auth/AuthAPI/appsettings.Development.json b/src/Auth/AuthAPI/appsettings.Development.json index 8982012..d1e3ae3 100644 --- a/src/Auth/AuthAPI/appsettings.Development.json +++ b/src/Auth/AuthAPI/appsettings.Development.json @@ -5,9 +5,9 @@ "Microsoft.AspNetCore": "Warning" } }, - "EnviromentVariables": { + "EnvironmentVariables": { "ApiUrl": "", "ApiKey": "", "AssociationNumber": "" } -} \ No newline at end of file +} diff --git a/src/Auth/AuthAPI/appsettings.example.json b/src/Auth/AuthAPI/appsettings.example.json new file mode 100644 index 0000000..480fd43 --- /dev/null +++ b/src/Auth/AuthAPI/appsettings.example.json @@ -0,0 +1,14 @@ +{ + "EnvironmentVariables": { + "ApiUrl": "", + "ApiKey": "", + "AssociationNumber": "" + }, + "Logging": { + "LogLevel": { + "Default": "", + "Microsoft.AspNetCore": "" + } + }, + "AllowedHosts": "" +} diff --git a/src/Auth/AuthAPI/appsettings.json b/src/Auth/AuthAPI/appsettings.json index c43a6f9..73e5cee 100644 --- a/src/Auth/AuthAPI/appsettings.json +++ b/src/Auth/AuthAPI/appsettings.json @@ -5,10 +5,10 @@ "Microsoft.AspNetCore": "Warning" } }, - "EnviromentVariables": { + "EnvironmentVariables": { "ApiUrl": "", "ApiKey": "", "AssociationNumber": "" }, "AllowedHosts": "*" -} \ No newline at end of file +} diff --git a/src/Registration/Registration.API/Configuration/AuthApiOptions.cs b/src/Registration/Registration.API/Configuration/AuthApiOptions.cs new file mode 100644 index 0000000..d47889e --- /dev/null +++ b/src/Registration/Registration.API/Configuration/AuthApiOptions.cs @@ -0,0 +1,7 @@ +namespace Registration.API.Configuration; + +public class AuthApiOptions +{ + public string BaseUrl { get; set; } = string.Empty; + public string ValidatePath { get; set; } = "/validate"; +} diff --git a/src/Registration/Registration.API/Configuration/VbytesRelayOptions.cs b/src/Registration/Registration.API/Configuration/VbytesRelayOptions.cs new file mode 100644 index 0000000..28e457f --- /dev/null +++ b/src/Registration/Registration.API/Configuration/VbytesRelayOptions.cs @@ -0,0 +1,11 @@ +namespace Registration.API.Configuration; + +public class VbytesRelayOptions +{ + public string BaseUrl { get; set; } = string.Empty; + public string ParticipantRegisterPath { get; set; } = string.Empty; + 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"; +} diff --git a/src/Registration/Registration.API/Controllers/ParticipantController.cs b/src/Registration/Registration.API/Controllers/ParticipantController.cs index 3bc980c..d666148 100644 --- a/src/Registration/Registration.API/Controllers/ParticipantController.cs +++ b/src/Registration/Registration.API/Controllers/ParticipantController.cs @@ -1,16 +1,26 @@ using Microsoft.AspNetCore.Mvc; +using Registration.API.Services; using Registration.Domain.Models; namespace Registration.API.Controllers { [Route("api/[controller]")] [ApiController] - public class ParticipantController : ControllerBase + public class ParticipantController(IVbytesParticipantRelayService relayService) : ControllerBase { + private readonly IVbytesParticipantRelayService _relayService = relayService; + [HttpPost("register")] - public IActionResult RegisterForLan([FromBody] Participant participant) + public async Task RegisterForLan([FromBody] Participant participant, CancellationToken cancellationToken) { - return Ok(); + var result = await _relayService.RegisterParticipantAsync(participant, cancellationToken); + + if (result.Success) + { + return Ok(); + } + + return StatusCode(result.StatusCode, new { message = result.Message }); } } } diff --git a/src/Registration/Registration.API/Controllers/RegistrationController.cs b/src/Registration/Registration.API/Controllers/RegistrationController.cs index 1068826..8ef425f 100644 --- a/src/Registration/Registration.API/Controllers/RegistrationController.cs +++ b/src/Registration/Registration.API/Controllers/RegistrationController.cs @@ -1,26 +1,21 @@ using Microsoft.AspNetCore.Mvc; +using Registration.API.Services; using Registration.Infra.Repositories; namespace Registration.API.Controllers { [Route("api/[controller]")] [ApiController] - public class RegistrationController(IMemberRepository memberRepository) : ControllerBase + public class RegistrationController(IMemberRepository memberRepository, IAuthService authService) : ControllerBase { private readonly IMemberRepository _memberRepository = memberRepository; + private readonly IAuthService _authService = authService; [HttpGet("register/{ssn}")] - public IActionResult ValidateSsn(string ssn) + public async Task ValidateSsn(string ssn, CancellationToken cancellationToken) { - // Should talk to the auth api to validate the ssn properly. - if (ssn.Length == 10 && long.TryParse(ssn, out _)) - { - return Ok(); - } - else - { - return NotFound(); - } + var isMember = await _authService.IsMemberAsync(ssn, cancellationToken); + return isMember ? Ok() : NotFound(); } [HttpPost("register/{ssn}")] diff --git a/src/Registration/Registration.API/Controllers/VolunteerController.cs b/src/Registration/Registration.API/Controllers/VolunteerController.cs index 7d58849..42a0956 100644 --- a/src/Registration/Registration.API/Controllers/VolunteerController.cs +++ b/src/Registration/Registration.API/Controllers/VolunteerController.cs @@ -1,16 +1,26 @@ using Microsoft.AspNetCore.Mvc; +using Registration.API.Services; using Registration.Domain.Models; namespace Registration.API.Controllers { [Route("api/[controller]")] [ApiController] - public class VolunteerController : ControllerBase + public class VolunteerController(IVbytesVolunteerRelayService relayService) : ControllerBase { + private readonly IVbytesVolunteerRelayService _relayService = relayService; + [HttpPost("register")] - public IActionResult RegisterVolunteer([FromBody] Volunteer volunteer) + public async Task RegisterVolunteer([FromBody] Volunteer volunteer, CancellationToken cancellationToken) { - return Ok(); + var result = await _relayService.RegisterVolunteerAsync(volunteer, cancellationToken); + + if (result.Success) + { + return Ok(); + } + + return StatusCode(result.StatusCode, new { message = result.Message }); } } } diff --git a/src/Registration/Registration.API/DtoModels/ParticipantRelayDto.cs b/src/Registration/Registration.API/DtoModels/ParticipantRelayDto.cs new file mode 100644 index 0000000..adb59be --- /dev/null +++ b/src/Registration/Registration.API/DtoModels/ParticipantRelayDto.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace Registration.API.DtoModels; + +public record ParticipantRelayDto( + [property: JsonPropertyName("member")] bool Member, + [property: JsonPropertyName("first_name")] string FirstName, + [property: JsonPropertyName("surname")] string Surname, + [property: JsonPropertyName("grade")] string Grade, + [property: JsonPropertyName("phone")] string? Phone, + [property: JsonPropertyName("email")] string? Email, + [property: JsonPropertyName("guardian_name")] string GuardianName, + [property: JsonPropertyName("guardian_phone")] string GuardianPhone, + [property: JsonPropertyName("guardian_email")] string GuardianEmail, + [property: JsonPropertyName("is_visiting")] bool IsVisiting, + [property: JsonPropertyName("gdpr")] bool Gdpr, + [property: JsonPropertyName("friends")] string? Friends, + [property: JsonPropertyName("special_diet")] string? SpecialDiet +); diff --git a/src/Registration/Registration.API/DtoModels/VolunteerRelayDto.cs b/src/Registration/Registration.API/DtoModels/VolunteerRelayDto.cs new file mode 100644 index 0000000..714c085 --- /dev/null +++ b/src/Registration/Registration.API/DtoModels/VolunteerRelayDto.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Registration.API.DtoModels; + +public record VolunteerRelayDto( + [property: JsonPropertyName("first_name")] string FirstName, + [property: JsonPropertyName("surname")] string Surname, + [property: JsonPropertyName("phone")] string Phone, + [property: JsonPropertyName("email")] string Email, + [property: JsonPropertyName("gdpr")] bool Gdpr, + [property: JsonPropertyName("areas")] string[] Areas +); diff --git a/src/Registration/Registration.API/Program.cs b/src/Registration/Registration.API/Program.cs index 54018f5..8420c9b 100644 --- a/src/Registration/Registration.API/Program.cs +++ b/src/Registration/Registration.API/Program.cs @@ -1,10 +1,49 @@ +using System.Security.Cryptography.X509Certificates; using Registration.Infra.Repositories; +using Registration.API.Configuration; +using Registration.API.Services; var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddScoped(); + +var relayOptions = builder.Configuration.GetSection("VbytesRelay").Get() + ?? throw new InvalidOperationException("VbytesRelay configuration section is missing."); + +builder.Services.Configure(builder.Configuration.GetSection("VbytesRelay")); + + +var certificate = X509CertificateLoader.LoadPkcs12FromFile( + relayOptions.ClientCertificatePfxPath, password: null); + +builder.Services.AddHttpClient(VbytesParticipantRelayService.HttpClientName, client => +{ + client.BaseAddress = new Uri(relayOptions.BaseUrl); + client.Timeout = TimeSpan.FromSeconds(30); +}) +.ConfigurePrimaryHttpMessageHandler(() => +{ + var handler = new HttpClientHandler(); + handler.ClientCertificates.Add(certificate); + return handler; +}); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var authApiOptions = builder.Configuration.GetSection("AuthApi").Get() + ?? throw new InvalidOperationException("AuthApi configuration section is missing."); + +builder.Services.Configure(builder.Configuration.GetSection("AuthApi")); +builder.Services.AddHttpClient(AuthService.HttpClientName, client => +{ + client.BaseAddress = new Uri(authApiOptions.BaseUrl); + client.Timeout = TimeSpan.FromSeconds(10); +}); +builder.Services.AddScoped(); + builder.Services.AddControllers(); var app = builder.Build(); @@ -22,7 +61,11 @@ if (app.Environment.IsDevelopment()) app.UseSwaggerUI(); } -app.UseHttpsRedirection(); +if (!app.Environment.IsDevelopment()) +{ + app.UseHttpsRedirection(); +} + app.UseAuthorization(); app.MapControllers(); diff --git a/src/Registration/Registration.API/Properties/launchSettings.json b/src/Registration/Registration.API/Properties/launchSettings.json index 5253021..cc75bca 100644 --- a/src/Registration/Registration.API/Properties/launchSettings.json +++ b/src/Registration/Registration.API/Properties/launchSettings.json @@ -7,7 +7,9 @@ "launchBrowser": false, "applicationUrl": "http://localhost:5063", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "VbytesRelay__BaseUrl": "https://api.lan.vbytes.se", + "VbytesRelay__ApiKeyHeaderName": "X-Api-Key" } }, "https": { @@ -16,8 +18,10 @@ "launchBrowser": false, "applicationUrl": "https://localhost:7209;http://localhost:5063", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "VbytesRelay__BaseUrl": "https://api.lan.vbytes.se", + "VbytesRelay__ApiKeyHeaderName": "X-Api-Key" } } } -} \ No newline at end of file +} diff --git a/src/Registration/Registration.API/Registration.API.csproj b/src/Registration/Registration.API/Registration.API.csproj index b64035c..548bd3d 100644 --- a/src/Registration/Registration.API/Registration.API.csproj +++ b/src/Registration/Registration.API/Registration.API.csproj @@ -1,9 +1,10 @@ - + net9.0 enable enable + 92929106-8d00-401d-8f20-f372f475ea39 diff --git a/src/Registration/Registration.API/RegistrationAPI.http b/src/Registration/Registration.API/RegistrationAPI.http index 8c9f95e..4101ca6 100644 --- a/src/Registration/Registration.API/RegistrationAPI.http +++ b/src/Registration/Registration.API/RegistrationAPI.http @@ -1,3 +1,47 @@ @RegistrationAPI_HostAddress = http://localhost:5063 +### Clear all registrations +DELETE {{RegistrationAPI_HostAddress}}/api/Registration/clear + +### + +POST {{RegistrationAPI_HostAddress}}/api/participant/register +Content-Type: application/json + +{ + "isMember": true, + "firstName": "Test", + "surName": "User", + "grade": "9", + "phoneNumber": "0700000000", + "email": "test.user@example.com", + "guardianName": "Guardian User", + "guardianPhoneNumber": "0700000001", + "guardianEmail": "guardian.user@example.com", + "isVisitor": false, + "hasApprovedGdpr": true, + "friends": "Friend One", + "specialDiet": "None" +} + +### Register volunteer +POST {{RegistrationAPI_HostAddress}}/api/volunteer/register +Content-Type: application/json + +{ + "firstName": "Jane", + "surName": "Doe", + "phoneNumber": "0700123456", + "email": "email@email.com", + "hasApprovedGdpr": true, + "areasOfInterest": [ + { + "name": "Städ" + }, + { + "name": "Kiosk" + } + ] +} + diff --git a/src/Registration/Registration.API/Services/AuthService.cs b/src/Registration/Registration.API/Services/AuthService.cs new file mode 100644 index 0000000..6da411f --- /dev/null +++ b/src/Registration/Registration.API/Services/AuthService.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Options; +using Registration.API.Configuration; + +namespace Registration.API.Services; + +public class AuthService(IHttpClientFactory httpClientFactory, IOptions options) : IAuthService +{ + public const string HttpClientName = "AuthApi"; + + private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; + private readonly AuthApiOptions _options = options.Value; + + public async Task IsMemberAsync(string ssn, CancellationToken cancellationToken = default) + { + var client = _httpClientFactory.CreateClient(HttpClientName); + var response = await client.PostAsJsonAsync( + _options.ValidatePath, + new { ssn }, + cancellationToken); + return response.IsSuccessStatusCode; + } +} diff --git a/src/Registration/Registration.API/Services/IAuthService.cs b/src/Registration/Registration.API/Services/IAuthService.cs new file mode 100644 index 0000000..0394365 --- /dev/null +++ b/src/Registration/Registration.API/Services/IAuthService.cs @@ -0,0 +1,6 @@ +namespace Registration.API.Services; + +public interface IAuthService +{ + Task IsMemberAsync(string ssn, CancellationToken cancellationToken = default); +} diff --git a/src/Registration/Registration.API/Services/IVbytesParticipantRelayService.cs b/src/Registration/Registration.API/Services/IVbytesParticipantRelayService.cs new file mode 100644 index 0000000..c534a37 --- /dev/null +++ b/src/Registration/Registration.API/Services/IVbytesParticipantRelayService.cs @@ -0,0 +1,10 @@ +using Registration.Domain.Models; + +namespace Registration.API.Services; + +public interface IVbytesParticipantRelayService +{ + Task RegisterParticipantAsync(Participant participant, CancellationToken cancellationToken = default); +} + +public sealed record VbytesRelayResult(bool Success, int StatusCode, string Message); diff --git a/src/Registration/Registration.API/Services/IVbytesVolunteerRelayService.cs b/src/Registration/Registration.API/Services/IVbytesVolunteerRelayService.cs new file mode 100644 index 0000000..55bba22 --- /dev/null +++ b/src/Registration/Registration.API/Services/IVbytesVolunteerRelayService.cs @@ -0,0 +1,8 @@ +using Registration.Domain.Models; + +namespace Registration.API.Services; + +public interface IVbytesVolunteerRelayService +{ + Task RegisterVolunteerAsync(Volunteer volunteer, CancellationToken cancellationToken = default); +} diff --git a/src/Registration/Registration.API/Services/VbytesParticipantRelayService.cs b/src/Registration/Registration.API/Services/VbytesParticipantRelayService.cs new file mode 100644 index 0000000..f8128bb --- /dev/null +++ b/src/Registration/Registration.API/Services/VbytesParticipantRelayService.cs @@ -0,0 +1,63 @@ +using Microsoft.Extensions.Options; +using Registration.API.Configuration; +using Registration.API.DtoModels; +using Registration.Domain.Models; + +namespace Registration.API.Services; + +public class VbytesParticipantRelayService( + IHttpClientFactory httpClientFactory, + IOptions options, + ILogger logger) + : VbytesRelayServiceBase(httpClientFactory, options), IVbytesParticipantRelayService +{ + public const string HttpClientName = "VbytesRelay"; + + private readonly ILogger _logger = logger; + + public async Task RegisterParticipantAsync(Participant participant, CancellationToken cancellationToken = default) + { + var configError = ValidateConfiguration(); + if (configError is not null) + { + _logger.LogWarning("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); + return new VbytesRelayResult(false, StatusCodes.Status504GatewayTimeout, "Upstream timeout."); + } + catch (Exception ex) + { + _logger.LogError(ex, "VBytes participant relay exception."); + return new VbytesRelayResult(false, StatusCodes.Status502BadGateway, "Upstream relay failed."); + } + } + + private static ParticipantRelayDto Map(Participant participant) => new( + Member: participant.IsMember, + FirstName: participant.FirstName, + Surname: participant.SurName, + Grade: participant.Grade, + Phone: participant.PhoneNumber, + Email: participant.Email, + GuardianName: participant.GuardianName, + GuardianPhone: participant.GuardianPhoneNumber, + GuardianEmail: participant.GuardianEmail, + IsVisiting: participant.IsVisitor, + Gdpr: participant.HasApprovedGdpr, + Friends: participant.Friends, + SpecialDiet: participant.SpecialDiet + ); +} diff --git a/src/Registration/Registration.API/Services/VbytesRelayServiceBase.cs b/src/Registration/Registration.API/Services/VbytesRelayServiceBase.cs new file mode 100644 index 0000000..aca99fe --- /dev/null +++ b/src/Registration/Registration.API/Services/VbytesRelayServiceBase.cs @@ -0,0 +1,80 @@ +using System.Net.Http.Json; +using Microsoft.Extensions.Options; +using Registration.API.Configuration; + +namespace Registration.API.Services; + +public abstract class VbytesRelayServiceBase( + IHttpClientFactory httpClientFactory, + IOptions options) +{ + private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; + protected readonly VbytesRelayOptions Options = options.Value; + + protected async Task SendAsync(string path, object payload, CancellationToken cancellationToken) + { + var client = _httpClientFactory.CreateClient(VbytesParticipantRelayService.HttpClientName); + + using var request = new HttpRequestMessage(HttpMethod.Post, BuildPath(path)) + { + Content = JsonContent.Create(payload) + }; + + request.Headers.TryAddWithoutValidation(Options.ApiKeyHeaderName, Options.ApiKey); + + using var response = await client.SendAsync(request, cancellationToken); + var body = await response.Content.ReadAsStringAsync(cancellationToken); + + if (response.IsSuccessStatusCode) + { + Console.WriteLine($"[DEBUG] Relay success body: {body}"); + return new VbytesRelayResult(true, StatusCodes.Status200OK, "Request relayed successfully."); + } + + var statusCode = (int)response.StatusCode; + Console.WriteLine($"[DEBUG] Relay error body: {body}"); + var message = body.Length > 600 ? body[..600] : body; + return new VbytesRelayResult(false, statusCode, string.IsNullOrWhiteSpace(message) ? "Upstream request failed." : message); + } + + protected VbytesRelayResult? ValidateConfiguration() + { + if (string.IsNullOrWhiteSpace(Options.BaseUrl)) + { + return new VbytesRelayResult(false, StatusCodes.Status500InternalServerError, "Relay base URL is not configured."); + } + + if (!IsConfigured(Options.ApiKey)) + { + return new VbytesRelayResult(false, StatusCodes.Status500InternalServerError, "Relay API key is not configured."); + } + + return null; + } + + private static string BuildPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return "/"; + } + + return path.StartsWith('/') ? path : $"/{path}"; + } + + private static async Task 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); + } +} diff --git a/src/Registration/Registration.API/Services/VbytesVolunteerRelayService.cs b/src/Registration/Registration.API/Services/VbytesVolunteerRelayService.cs new file mode 100644 index 0000000..bf99067 --- /dev/null +++ b/src/Registration/Registration.API/Services/VbytesVolunteerRelayService.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Options; +using Registration.API.Configuration; +using Registration.API.DtoModels; +using Registration.Domain.Models; + +namespace Registration.API.Services; + +public class VbytesVolunteerRelayService( + IHttpClientFactory httpClientFactory, + IOptions options, + ILogger logger) + : VbytesRelayServiceBase(httpClientFactory, options), IVbytesVolunteerRelayService +{ + private readonly ILogger _logger = logger; + + public async Task RegisterVolunteerAsync(Volunteer volunteer, CancellationToken cancellationToken = default) + { + var configError = ValidateConfiguration(); + if (configError is not null) return configError; + + try + { + return await SendAsync(Options.VolunteerRegisterPath, Map(volunteer), cancellationToken); + } + catch (TaskCanceledException) + { + return new VbytesRelayResult(false, StatusCodes.Status504GatewayTimeout, "Upstream timeout."); + } + catch (Exception ex) + { + _logger.LogError(ex, "VBytes volunteer relay exception."); + return new VbytesRelayResult(false, StatusCodes.Status502BadGateway, "Upstream relay failed."); + } + } + + private static VolunteerRelayDto Map(Volunteer volunteer) => new( + FirstName: volunteer.FirstName, + Surname: volunteer.SurName, + Phone: volunteer.PhoneNumber, + Email: volunteer.Email, + Gdpr: volunteer.HasApprovedGdpr, + Areas: volunteer.AreasOfInterest.Select(area => area.Name).ToArray() + ); +} diff --git a/src/Registration/Registration.API/appsettings.Development.json b/src/Registration/Registration.API/appsettings.Development.json index db8e3f3..43ad566 100644 --- a/src/Registration/Registration.API/appsettings.Development.json +++ b/src/Registration/Registration.API/appsettings.Development.json @@ -9,6 +9,18 @@ "DefaultConnection": "Host=localhost;Username=postgres;Password=postgres;Database=postgres;Port=5432" }, "Security": { - "SsnPepper": "VBYTES_LAN_2026_SECRET_PEPPER" + "SsnPepper": "__SET_IN_USER_SECRETS__" + }, + "VbytesRelay": { + "BaseUrl": "https://api.lan.vbytes.se", + "ParticipantRegisterPath": "/api/participant", + "VolunteerRegisterPath": "/api/volunteer/register", + "ApiKeyHeaderName": "X-Api-Key", + "ApiKey": "", + "ClientCertificatePfxPath": "" + }, + "AuthApi": { + "BaseUrl": "http://localhost:5127", + "ValidatePath": "/validate" } -} \ No newline at end of file +} diff --git a/src/Registration/Registration.API/appsettings.example.json b/src/Registration/Registration.API/appsettings.example.json new file mode 100644 index 0000000..2afd3c1 --- /dev/null +++ b/src/Registration/Registration.API/appsettings.example.json @@ -0,0 +1,30 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "" + }, + "Security": { + "SsnPepper": "" + }, + "Admin": { + "Password": "" + }, + "VbytesRelay": { + "BaseUrl": "", + "ParticipantRegisterPath": "/api/participant", + "VolunteerRegisterPath": "", + "ApiKeyHeaderName": "", + "ApiKey": "", + "ClientCertificatePfxPath": "" + }, + "AuthApi": { + "BaseUrl": "", + "ValidatePath": "/validate" + }, + "Logging": { + "LogLevel": { + "Default": "", + "Microsoft.AspNetCore": "" + } + }, + "AllowedHosts": "" +} diff --git a/src/Registration/Registration.API/appsettings.json b/src/Registration/Registration.API/appsettings.json index 1a4b525..d3ae39b 100644 --- a/src/Registration/Registration.API/appsettings.json +++ b/src/Registration/Registration.API/appsettings.json @@ -3,11 +3,23 @@ "DefaultConnection": "Host=localhost;Username=postgres;Password=postgres;Database=postgres;Port=5432" }, "Security": { - "SsnPepper": "VBYTES_LAN_2026_SECRET_PEPPER" + "SsnPepper": "__SET_IN_USER_SECRETS__" }, "Admin": { "Password": "admin" }, + "VbytesRelay": { + "BaseUrl": "https://api.lan.vbytes.se", + "ParticipantRegisterPath": "/api/participant", + "VolunteerRegisterPath": "/api/volunteer/register", + "ApiKeyHeaderName": "X-Api-Key", + "ApiKey": "__SET_IN_USER_SECRETS__", + "ClientCertificatePfxPath": "__SET_IN_USER_SECRETS__" + }, + "AuthApi": { + "BaseUrl": "", + "ValidatePath": "/validate" + }, "Logging": { "LogLevel": { "Default": "Information", @@ -15,4 +27,4 @@ } }, "AllowedHosts": "*" -} \ No newline at end of file +} diff --git a/src/Registration/Registration.Domain/Models/Volunteer.cs b/src/Registration/Registration.Domain/Models/Volunteer.cs index 3ceb6da..5b405dc 100644 --- a/src/Registration/Registration.Domain/Models/Volunteer.cs +++ b/src/Registration/Registration.Domain/Models/Volunteer.cs @@ -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 AreasOfInterest AreasOfInterest { get; set; } + public required List AreasOfInterest { get; set; } }