Compare commits
No commits in common. "9d9c9b3f387dda08d0e092022fe66bf9228493b9" and "7c0030b08875012fd68ca8cfe17c7bc94a5ad1a3" have entirely different histories.
9d9c9b3f38
...
7c0030b088
31 changed files with 57 additions and 698 deletions
107
.vscode/tasks.json
vendored
107
.vscode/tasks.json
vendored
|
|
@ -1,107 +0,0 @@
|
|||
{
|
||||
"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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
89
README.md
89
README.md
|
|
@ -23,92 +23,3 @@ 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" "<your-api-key>"
|
||||
dotnet user-secrets set "VbytesRelay:ClientCertificatePfxPath" "<absolute-path-to>.pfx"
|
||||
dotnet user-secrets set "VbytesRelay:ParticipantRegisterPath" "/api/participant"
|
||||
dotnet user-secrets set "Security:SsnPepper" "<your-pepper-value>"
|
||||
```
|
||||
|
||||
| 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=<postgres-connection-string>
|
||||
Security__SsnPepper=<pepper-value>
|
||||
VbytesRelay__BaseUrl=https://api.lan.vbytes.se
|
||||
VbytesRelay__ParticipantRegisterPath=/api/participant
|
||||
VbytesRelay__VolunteerRegisterPath=/api/volunteer
|
||||
VbytesRelay__ApiKeyHeaderName=X-Api-Key
|
||||
VbytesRelay__ApiKey=<relay-api-key>
|
||||
VbytesRelay__ClientCertificatePfxPath=<absolute-path-to>.pfx
|
||||
AuthApi__BaseUrl=<auth-api-base-url>
|
||||
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" "<member-api-url>"
|
||||
dotnet user-secrets set "EnvironmentVariables:ApiKey" "<member-api-key>"
|
||||
dotnet user-secrets set "EnvironmentVariables:AssociationNumber" "<association-number>"
|
||||
```
|
||||
|
||||
| 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=<member-api-url>
|
||||
EnvironmentVariables__ApiKey=<member-api-key>
|
||||
EnvironmentVariables__AssociationNumber=<association-number>
|
||||
```
|
||||
|
||||
For frontend production configuration, set:
|
||||
|
||||
```bash
|
||||
REGISTRATION_API_URL=<registration-api-base-url>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>e53e6e14-fe65-43c6-82fd-0f1fb394679d</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,20 @@
|
|||
@AuthAPI_HostAddress = http://localhost:5127
|
||||
|
||||
# Validate by SSN
|
||||
POST {{AuthAPI_HostAddress}}/validate
|
||||
GET {{AuthAPI_HostAddress}}/validate/
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ssn": "8612125643"
|
||||
"Email": "someValue",
|
||||
"FirstName": "name"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# Validate by email + first name
|
||||
POST {{AuthAPI_HostAddress}}/validate
|
||||
GET {{AuthAPI_HostAddress}}/validate/
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "testwith@validemail.se",
|
||||
"firstName": "AndCorrectName"
|
||||
"Ssn": "10 or 8 number length"
|
||||
}
|
||||
|
||||
###
|
||||
|
|
@ -5,7 +5,7 @@ using AuthAPI;
|
|||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.Configure<AuthSettings>(
|
||||
builder.Configuration.GetSection("EnvironmentVariables"));
|
||||
builder.Configuration.GetSection("EnviromentVariables"));
|
||||
builder.Services.AddScoped<MemberValidationService>();
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
{
|
||||
|
|
@ -14,7 +14,7 @@ builder.Services.ConfigureHttpJsonOptions(options =>
|
|||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapPost("/validate", async (
|
||||
app.MapGet("/validate", async (
|
||||
[FromBody] Request validationRequest,
|
||||
HttpClient httpClient,
|
||||
MemberValidationService memberService,
|
||||
|
|
@ -32,8 +32,7 @@ app.MapPost("/validate", async (
|
|||
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();
|
||||
return content.Contains("\"member_found\":true,") ? Results.Ok() : Results.NotFound();
|
||||
})
|
||||
.WithName("ValidateMember");
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"EnvironmentVariables": {
|
||||
"EnviromentVariables": {
|
||||
"ApiUrl": "",
|
||||
"ApiKey": "",
|
||||
"AssociationNumber": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"EnvironmentVariables": {
|
||||
"ApiUrl": "",
|
||||
"ApiKey": "",
|
||||
"AssociationNumber": ""
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "",
|
||||
"Microsoft.AspNetCore": ""
|
||||
}
|
||||
},
|
||||
"AllowedHosts": ""
|
||||
}
|
||||
|
|
@ -5,10 +5,10 @@
|
|||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"EnvironmentVariables": {
|
||||
"EnviromentVariables": {
|
||||
"ApiUrl": "",
|
||||
"ApiKey": "",
|
||||
"AssociationNumber": ""
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
namespace Registration.API.Configuration;
|
||||
|
||||
public class AuthApiOptions
|
||||
{
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
public string ValidatePath { get; set; } = "/validate";
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
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";
|
||||
}
|
||||
|
|
@ -1,26 +1,16 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Registration.API.Services;
|
||||
using Registration.Domain.Models;
|
||||
|
||||
namespace Registration.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class ParticipantController(IVbytesParticipantRelayService relayService) : ControllerBase
|
||||
public class ParticipantController : ControllerBase
|
||||
{
|
||||
private readonly IVbytesParticipantRelayService _relayService = relayService;
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> RegisterForLan([FromBody] Participant participant, CancellationToken cancellationToken)
|
||||
public IActionResult RegisterForLan([FromBody] Participant participant)
|
||||
{
|
||||
var result = await _relayService.RegisterParticipantAsync(participant, cancellationToken);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return StatusCode(result.StatusCode, new { message = result.Message });
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,26 @@
|
|||
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, IAuthService authService) : ControllerBase
|
||||
public class RegistrationController(IMemberRepository memberRepository) : ControllerBase
|
||||
{
|
||||
private readonly IMemberRepository _memberRepository = memberRepository;
|
||||
private readonly IAuthService _authService = authService;
|
||||
|
||||
[HttpGet("register/{ssn}")]
|
||||
public async Task<IActionResult> ValidateSsn(string ssn, CancellationToken cancellationToken)
|
||||
public IActionResult ValidateSsn(string ssn)
|
||||
{
|
||||
var isMember = await _authService.IsMemberAsync(ssn, cancellationToken);
|
||||
return isMember ? Ok() : NotFound();
|
||||
// Should talk to the auth api to validate the ssn properly.
|
||||
if (ssn.Length == 10 && long.TryParse(ssn, out _))
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("register/{ssn}")]
|
||||
|
|
|
|||
|
|
@ -1,26 +1,16 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Registration.API.Services;
|
||||
using Registration.Domain.Models;
|
||||
|
||||
namespace Registration.API.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class VolunteerController(IVbytesVolunteerRelayService relayService) : ControllerBase
|
||||
public class VolunteerController : ControllerBase
|
||||
{
|
||||
private readonly IVbytesVolunteerRelayService _relayService = relayService;
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> RegisterVolunteer([FromBody] Volunteer volunteer, CancellationToken cancellationToken)
|
||||
public IActionResult RegisterVolunteer([FromBody] Volunteer volunteer)
|
||||
{
|
||||
var result = await _relayService.RegisterVolunteerAsync(volunteer, cancellationToken);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return StatusCode(result.StatusCode, new { message = result.Message });
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
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
|
||||
);
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
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
|
||||
);
|
||||
|
|
@ -1,49 +1,10 @@
|
|||
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<IMemberRepository, MemberRepository>();
|
||||
|
||||
var relayOptions = builder.Configuration.GetSection("VbytesRelay").Get<VbytesRelayOptions>()
|
||||
?? throw new InvalidOperationException("VbytesRelay configuration section is missing.");
|
||||
|
||||
builder.Services.Configure<VbytesRelayOptions>(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<IVbytesParticipantRelayService, VbytesParticipantRelayService>();
|
||||
builder.Services.AddScoped<IVbytesVolunteerRelayService, VbytesVolunteerRelayService>();
|
||||
|
||||
var authApiOptions = builder.Configuration.GetSection("AuthApi").Get<AuthApiOptions>()
|
||||
?? throw new InvalidOperationException("AuthApi configuration section is missing.");
|
||||
|
||||
builder.Services.Configure<AuthApiOptions>(builder.Configuration.GetSection("AuthApi"));
|
||||
builder.Services.AddHttpClient(AuthService.HttpClientName, client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(authApiOptions.BaseUrl);
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
});
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
var app = builder.Build();
|
||||
|
|
@ -61,11 +22,7 @@ if (app.Environment.IsDevelopment())
|
|||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@
|
|||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5063",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"VbytesRelay__BaseUrl": "https://api.lan.vbytes.se",
|
||||
"VbytesRelay__ApiKeyHeaderName": "X-Api-Key"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
|
|
@ -18,10 +16,8 @@
|
|||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7209;http://localhost:5063",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"VbytesRelay__BaseUrl": "https://api.lan.vbytes.se",
|
||||
"VbytesRelay__ApiKeyHeaderName": "X-Api-Key"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>92929106-8d00-401d-8f20-f372f475ea39</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,47 +1,6 @@
|
|||
@RegistrationAPI_HostAddress = http://localhost:5063
|
||||
|
||||
### Clear all registrations
|
||||
DELETE {{RegistrationAPI_HostAddress}}/api/Registration/clear
|
||||
GET {{RegistrationAPI_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
using Microsoft.Extensions.Options;
|
||||
using Registration.API.Configuration;
|
||||
|
||||
namespace Registration.API.Services;
|
||||
|
||||
public class AuthService(IHttpClientFactory httpClientFactory, IOptions<AuthApiOptions> options) : IAuthService
|
||||
{
|
||||
public const string HttpClientName = "AuthApi";
|
||||
|
||||
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
|
||||
private readonly AuthApiOptions _options = options.Value;
|
||||
|
||||
public async Task<bool> IsMemberAsync(string ssn, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient(HttpClientName);
|
||||
var response = await client.PostAsJsonAsync(
|
||||
_options.ValidatePath,
|
||||
new { ssn },
|
||||
cancellationToken);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
namespace Registration.API.Services;
|
||||
|
||||
public interface IAuthService
|
||||
{
|
||||
Task<bool> IsMemberAsync(string ssn, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
using Registration.Domain.Models;
|
||||
|
||||
namespace Registration.API.Services;
|
||||
|
||||
public interface IVbytesParticipantRelayService
|
||||
{
|
||||
Task<VbytesRelayResult> RegisterParticipantAsync(Participant participant, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed record VbytesRelayResult(bool Success, int StatusCode, string Message);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
using Registration.Domain.Models;
|
||||
|
||||
namespace Registration.API.Services;
|
||||
|
||||
public interface IVbytesVolunteerRelayService
|
||||
{
|
||||
Task<VbytesRelayResult> RegisterVolunteerAsync(Volunteer volunteer, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
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<VbytesRelayOptions> options,
|
||||
ILogger<VbytesParticipantRelayService> logger)
|
||||
: VbytesRelayServiceBase(httpClientFactory, options), IVbytesParticipantRelayService
|
||||
{
|
||||
public const string HttpClientName = "VbytesRelay";
|
||||
|
||||
private readonly ILogger<VbytesParticipantRelayService> _logger = logger;
|
||||
|
||||
public async Task<VbytesRelayResult> 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
|
||||
);
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Registration.API.Configuration;
|
||||
|
||||
namespace Registration.API.Services;
|
||||
|
||||
public abstract class VbytesRelayServiceBase(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IOptions<VbytesRelayOptions> options)
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
|
||||
protected readonly VbytesRelayOptions Options = options.Value;
|
||||
|
||||
protected async Task<VbytesRelayResult> 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<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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
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<VbytesRelayOptions> options,
|
||||
ILogger<VbytesVolunteerRelayService> logger)
|
||||
: VbytesRelayServiceBase(httpClientFactory, options), IVbytesVolunteerRelayService
|
||||
{
|
||||
private readonly ILogger<VbytesVolunteerRelayService> _logger = logger;
|
||||
|
||||
public async Task<VbytesRelayResult> 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()
|
||||
);
|
||||
}
|
||||
13
src/Registration/Registration.API/WeatherForecast.cs
Normal file
13
src/Registration/Registration.API/WeatherForecast.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
namespace RegistrationAPI;
|
||||
|
||||
public class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
|
||||
public int TemperatureC { get; set; }
|
||||
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
|
||||
public string? Summary { get; set; }
|
||||
}
|
||||
|
||||
|
|
@ -9,18 +9,6 @@
|
|||
"DefaultConnection": "Host=localhost;Username=postgres;Password=postgres;Database=postgres;Port=5432"
|
||||
},
|
||||
"Security": {
|
||||
"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"
|
||||
"SsnPepper": "VBYTES_LAN_2026_SECRET_PEPPER"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"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": ""
|
||||
}
|
||||
|
|
@ -3,23 +3,11 @@
|
|||
"DefaultConnection": "Host=localhost;Username=postgres;Password=postgres;Database=postgres;Port=5432"
|
||||
},
|
||||
"Security": {
|
||||
"SsnPepper": "__SET_IN_USER_SECRETS__"
|
||||
"SsnPepper": "VBYTES_LAN_2026_SECRET_PEPPER"
|
||||
},
|
||||
"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",
|
||||
|
|
@ -27,4 +15,4 @@
|
|||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 AreasOfInterest AreasOfInterest { get; set; }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue