diff --git a/web/backend/Database/Database.Seed/Database.Seed.csproj b/web/backend/Database/Database.Seed/Database.Seed.csproj
index 4dc3954b..7764cd68 100644
--- a/web/backend/Database/Database.Seed/Database.Seed.csproj
+++ b/web/backend/Database/Database.Seed/Database.Seed.csproj
@@ -8,29 +8,29 @@
-
-
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
-
+
diff --git a/web/backend/Database/Database.Seed/DatabaseHelpers/ConnectionStrings.cs b/web/backend/Database/Database.Seed/DatabaseHelpers/ConnectionStrings.cs
index fa07f2cc..9cd535d8 100644
--- a/web/backend/Database/Database.Seed/DatabaseHelpers/ConnectionStrings.cs
+++ b/web/backend/Database/Database.Seed/DatabaseHelpers/ConnectionStrings.cs
@@ -9,7 +9,7 @@ namespace Database.Seed.DatabaseHelpers;
public class ConnectionStrings
{
public static string SqlServerConnectionString => GetSqlServerConnectionString();
- public static string SqliteConnectionString => "Data Source=SeedData/seed.sqlite";
+ public static string SqliteConnectionString => "Data Source=SeedData/biergarten_seed_2026-07-20T05-46-02.993511Z.sqlite";
private static string GetSqlServerConnectionString()
{
@@ -44,4 +44,4 @@ public class ConnectionStrings
return builder.ConnectionString;
}
-}
\ No newline at end of file
+}
diff --git a/web/backend/Database/Database.Seed/Program.cs b/web/backend/Database/Database.Seed/Program.cs
index 9442365d..8d06ae88 100644
--- a/web/backend/Database/Database.Seed/Program.cs
+++ b/web/backend/Database/Database.Seed/Program.cs
@@ -4,6 +4,7 @@ using Database.Seed.DatabaseHelpers;
using Database.Seed.PipelineData;
using Database.Seed.Sqlite;
using Features.Auth.DependencyInjection;
+using Features.Auth.Dtos;
using Features.Auth.Repository;
using Features.Breweries.DependencyInjection;
using Features.Breweries.Repository;
@@ -11,68 +12,109 @@ using Infrastructure.Sql;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using idunno.Password;
+using Infrastructure.PasswordHashing;
+using Domain.Entities;
-IReadOnlyList breweries;
-IReadOnlyList users;
-try
+async static Task main()
{
- PipelineSeedDataReader reader = new(ConnectionStrings.SqliteConnectionString);
- breweries = reader.ReadBreweryRecords();
- users = reader.ReadUserRecords();
-}
-catch (SqliteException ex)
-{
- Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine($"Error opening database connection: {ex.Message}");
- Console.ResetColor();
- return 1;
-}
+ IReadOnlyList breweries;
+ IReadOnlyList users;
+ try
+ {
+ PipelineSeedDataReader reader = new(ConnectionStrings.SqliteConnectionString);
+ breweries = reader.ReadBreweryRecords();
+ users = reader.ReadUserRecords();
+ }
+ catch (SqliteException ex)
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine($"Error opening database connection: {ex.Message}");
+ Console.ResetColor();
+ return 1;
+ }
-ServiceCollection services = [];
-services.AddSingleton(new ConfigurationBuilder().Build());
-services.AddSingleton();
-services.AddFeaturesBreweries();
-services.AddFeaturesAuth();
+ ServiceCollection services = [];
+ services.AddSingleton(new ConfigurationBuilder().Build());
+ services.AddSingleton();
+ services.AddFeaturesBreweries();
+ services.AddFeaturesAuth();
-using ServiceProvider provider = services.BuildServiceProvider();
+ using ServiceProvider provider = services.BuildServiceProvider();
-IBreweryRepository breweryRepository;
-IAuthRepository authRepository;
-try
-{
- breweryRepository = provider.GetRequiredService();
- authRepository = provider.GetRequiredService();
-}
-catch (InvalidOperationException ex)
-{
- Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine($"Error configuring database connection: {ex.Message}");
- Console.ResetColor();
- return 1;
-}
+ IBreweryRepository breweryRepository;
+ IAuthRepository authRepository;
+ IPasswordInfrastructure passwordInfrastructure;
+ try
+ {
+ breweryRepository = provider.GetRequiredService();
+ authRepository = provider.GetRequiredService();
+ passwordInfrastructure = provider.GetRequiredService();
+ }
+ catch (InvalidOperationException ex)
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine($"Error configuring database connection: {ex.Message}");
+ Console.ResetColor();
+ return 1;
+ }
-Console.ForegroundColor = ConsoleColor.Green;
-Console.WriteLine(
- $"Loaded {breweries.Count} breweries."
-);
-Console.ResetColor();
-
-for (int i = 0; i < breweries.Count; i++)
-{
- BreweryRecord brewery = breweries[i];
+ Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(
- $"{i + 1}:\t{brewery.Brewery.NameEn}\t({brewery.Address.City.CityName}, {brewery.Address.City.Country})");
+ $"Loaded {breweries.Count} breweries."
+ );
+ Console.ResetColor();
+
+ for (int i = 0; i < breweries.Count; i++)
+ {
+ BreweryRecord brewery = breweries[i];
+ Console.WriteLine(
+ $"{i + 1}:\t{brewery.Brewery.NameEn}\t({brewery.Address.City.CityName}, {brewery.Address.City.Country})");
+ }
+
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine($"Loaded {users.Count} users.");
+ Console.ResetColor();
+
+ foreach (UserRecord user in users)
+ {
+ string username = user.User.Username;
+ string firstName = user.User.FirstName;
+ string lastName = user.User.LastName;
+ string email = user.Email;
+ DateTime dateOfBirth = DateTime.Parse(user.DateOfBirth);
+
+ // Generate a password that is 64 characters long with 10 digits, 10 symbols,
+ // allowing upper and lower case letters, disallowing repeat characters.
+ string generatedPassword = PasswordGenerator.Generate(length: 64, numberOfDigits: 10, numberOfSymbols: 10);
+ string hashedPassword = passwordInfrastructure.Hash(generatedPassword);
+
+ UserRegistrationDto registrationDto = new(
+ username,
+ firstName,
+ lastName,
+ email,
+ dateOfBirth,
+ hashedPassword
+ );
+
+ try
+ {
+ UserAccount createdUser = await authRepository.RegisterUserAsync(registrationDto);
+ Console.WriteLine($"Created user: {createdUser.Username} ({createdUser.UserAccountId})");
+ }
+ catch (Exception ex)
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine($"Error creating user {username}: {ex.Message}");
+ Console.ResetColor();
+ }
+
+ }
+
+ return 0;
}
-Console.ForegroundColor = ConsoleColor.Green;
-Console.WriteLine($"Loaded {users.Count} users.");
-Console.ResetColor();
-for (int i = 0; i < users.Count; i++)
-{
- UserRecord user = users[i];
- Console.WriteLine($"{i + 1}:\t{user.User.FirstName} {user.User.LastName}\t ({user.Email})");
-}
-
-return 0;
\ No newline at end of file
+return await main();
diff --git a/web/backend/Database/Database.Seed/Sqlite/PipelineSeedDataReader.cs b/web/backend/Database/Database.Seed/Sqlite/PipelineSeedDataReader.cs
index 9f0ca70f..d7b16939 100644
--- a/web/backend/Database/Database.Seed/Sqlite/PipelineSeedDataReader.cs
+++ b/web/backend/Database/Database.Seed/Sqlite/PipelineSeedDataReader.cs
@@ -105,9 +105,16 @@ public sealed class PipelineSeedDataReader
private static IReadOnlyDictionary ReadCities(SqliteConnection connection)
{
const string sql = """
- SELECT id, city, state_province, iso3166_2, country, iso3166_1,
- local_languages_json, postal_code_country_format_regex,
- postal_code_city_regex_json
+ SELECT
+ id,
+ city,
+ state_province,
+ iso3166_2,
+ country,
+ iso3166_1,
+ local_languages_json,
+ postal_code_country_format_regex,
+ postal_code_city_regex_json
FROM cities;
""";
@@ -115,27 +122,38 @@ public sealed class PipelineSeedDataReader
using SqliteDataReader reader = command.ExecuteReader();
Dictionary cities = [];
+
+ const int cityNameBindIndex = 1;
+ const int stateProvinceBindIndex = 2;
+ const int iso31662BindIndex = 3;
+ const int countryBindIndex = 4;
+ const int iso31661BindIndex = 5;
+ const int localLanguagesBindIndex = 6;
+ const int countryFormatRegexBindIndex = 7;
+ const int cityRegexesBindIndex = 8;
+
while (reader.Read())
cities[reader.GetInt64(0)] = new City
{
- CityName = reader.GetString(1),
- StateProvince = reader.GetString(2),
- Iso31662 = reader.GetString(3),
- Country = reader.GetString(4),
- Iso31661 = reader.GetString(5),
- LocalLanguages = DeserializeStringArray(reader.GetString(6)),
+ CityName = reader.GetString(cityNameBindIndex),
+ StateProvince = reader.GetString(stateProvinceBindIndex),
+ Iso31662 = reader.GetString(iso31662BindIndex),
+ Country = reader.GetString(countryBindIndex),
+ Iso31661 = reader.GetString(iso31661BindIndex),
+ LocalLanguages = DeserializeStringArray(reader.GetString(localLanguagesBindIndex)),
PostalCode = new PostalCodeSpec
{
- CountryFormatRegex = reader.GetString(7),
- CityRegexes = DeserializeStringArray(reader.GetString(8))
+ CountryFormatRegex = reader.GetString(countryFormatRegexBindIndex),
+ CityRegexes = DeserializeStringArray(reader.GetString(cityRegexesBindIndex))
}
};
return cities;
}
+
private static IReadOnlyList DeserializeStringArray(string json)
{
return JsonSerializer.Deserialize>(json) ?? [];
}
-}
\ No newline at end of file
+}
diff --git a/web/backend/Features/Features.Auth.Tests/Commands/RegisterUserHandlerTests.cs b/web/backend/Features/Features.Auth.Tests/Commands/RegisterUserHandlerTests.cs
index 999c3c7e..954c5374 100644
--- a/web/backend/Features/Features.Auth.Tests/Commands/RegisterUserHandlerTests.cs
+++ b/web/backend/Features/Features.Auth.Tests/Commands/RegisterUserHandlerTests.cs
@@ -68,12 +68,14 @@ public class RegisterUserHandlerTests
_authRepoMock
.Setup(x =>
x.RegisterUserAsync(
- command.Username,
- command.FirstName,
- command.LastName,
- command.Email,
- command.DateOfBirth,
- hashedPassword
+ new UserRegistrationDto(
+ command.Username,
+ command.FirstName,
+ command.LastName,
+ command.Email,
+ command.DateOfBirth,
+ hashedPassword
+ )
)
)
.ReturnsAsync(
@@ -139,15 +141,7 @@ public class RegisterUserHandlerTests
.WithMessage("Username or email already exists");
_authRepoMock.Verify(
- x =>
- x.RegisterUserAsync(
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny()
- ),
+ x => x.RegisterUserAsync(It.IsAny()),
Times.Never
);
}
@@ -192,12 +186,7 @@ public class RegisterUserHandlerTests
_authRepoMock
.Setup(x =>
x.RegisterUserAsync(
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- hashedPassword
+ It.Is(dto => dto.PasswordHash == hashedPassword)
)
)
.ReturnsAsync(new UserAccount { UserAccountId = Guid.NewGuid() });
@@ -215,12 +204,14 @@ public class RegisterUserHandlerTests
_authRepoMock.Verify(
x =>
x.RegisterUserAsync(
- command.Username,
- command.FirstName,
- command.LastName,
- command.Email,
- command.DateOfBirth,
- hashedPassword
+ new UserRegistrationDto(
+ command.Username,
+ command.FirstName,
+ command.LastName,
+ command.Email,
+ command.DateOfBirth,
+ hashedPassword
+ )
),
Times.Once
);
@@ -239,16 +230,7 @@ public class RegisterUserHandlerTests
.ReturnsAsync((UserAccount?)null);
_passwordInfraMock.Setup(x => x.Hash(It.IsAny())).Returns("hashed");
_authRepoMock
- .Setup(x =>
- x.RegisterUserAsync(
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny()
- )
- )
+ .Setup(x => x.RegisterUserAsync(It.IsAny()))
.ReturnsAsync(
new UserAccount
{
diff --git a/web/backend/Features/Features.Auth.Tests/Repository/AuthRepositoryTests.cs b/web/backend/Features/Features.Auth.Tests/Repository/AuthRepositoryTests.cs
index 1bc7e702..297241c7 100644
--- a/web/backend/Features/Features.Auth.Tests/Repository/AuthRepositoryTests.cs
+++ b/web/backend/Features/Features.Auth.Tests/Repository/AuthRepositoryTests.cs
@@ -1,5 +1,6 @@
using Apps72.Dev.Data.DbMocker;
using Domain.Entities;
+using Features.Auth.Dtos;
using Features.Auth.Repository;
using FluentAssertions;
@@ -50,12 +51,14 @@ public class AuthRepositoryTests
AuthRepository repo = CreateRepo(conn);
UserAccount result = await repo.RegisterUserAsync(
- "testuser",
- "Test",
- "User",
- "test@example.com",
- new DateTime(1990, 1, 1),
- "hashedpassword123"
+ new UserRegistrationDto(
+ "testuser",
+ "Test",
+ "User",
+ "test@example.com",
+ new DateTime(1990, 1, 1),
+ "hashedpassword123"
+ )
);
result.Should().NotBeNull();
diff --git a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserHandler.cs b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserHandler.cs
index 0adb2990..d056894c 100644
--- a/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserHandler.cs
+++ b/web/backend/Features/Features.Auth/Commands/RegisterUser/RegisterUserHandler.cs
@@ -38,12 +38,12 @@ public class RegisterUserHandler(
string hashed = passwordInfrastructure.Hash(request.Password);
UserAccount createdUser = await authRepo.RegisterUserAsync(
- request.Username,
+ new UserRegistrationDto(request.Username,
request.FirstName,
request.LastName,
request.Email,
request.DateOfBirth,
- hashed
+ hashed)
);
string accessToken = tokenService.GenerateAccessToken(createdUser);
diff --git a/web/backend/Features/Features.Auth/Dtos/UserRegistrationDto.cs b/web/backend/Features/Features.Auth/Dtos/UserRegistrationDto.cs
new file mode 100644
index 00000000..5a637567
--- /dev/null
+++ b/web/backend/Features/Features.Auth/Dtos/UserRegistrationDto.cs
@@ -0,0 +1,10 @@
+namespace Features.Auth.Dtos;
+
+public record UserRegistrationDto(
+ string Username,
+ string FirstName,
+ string LastName,
+ string Email,
+ DateTime DateOfBirth,
+ string PasswordHash
+);
diff --git a/web/backend/Features/Features.Auth/Repository/AuthRepository.cs b/web/backend/Features/Features.Auth/Repository/AuthRepository.cs
index 03fe1380..46f8aca7 100644
--- a/web/backend/Features/Features.Auth/Repository/AuthRepository.cs
+++ b/web/backend/Features/Features.Auth/Repository/AuthRepository.cs
@@ -1,6 +1,7 @@
using System.Data;
using System.Data.Common;
using Domain.Entities;
+using Features.Auth.Dtos;
using Infrastructure.Sql;
using Microsoft.Data.SqlClient;
@@ -35,12 +36,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
/// Thrown when the newly registered user cannot be retrieved after registration.
/// Thrown when the database command fails.
public async Task RegisterUserAsync(
- string username,
- string firstName,
- string lastName,
- string email,
- DateTime dateOfBirth,
- string passwordHash
+ UserRegistrationDto userRegistrationDto
)
{
await using DbConnection connection = await CreateConnection();
@@ -49,6 +45,9 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
command.CommandText = "USP_RegisterUser";
command.CommandType = CommandType.StoredProcedure;
+ var (username, firstName, lastName, email, dateOfBirth, passwordHash) =
+ userRegistrationDto;
+
AddParameter(command, "@Username", username);
AddParameter(command, "@FirstName", firstName);
AddParameter(command, "@LastName", lastName);
diff --git a/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs b/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs
index d405891b..6a2570fc 100644
--- a/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs
+++ b/web/backend/Features/Features.Auth/Repository/IAuthRepository.cs
@@ -1,7 +1,7 @@
using Domain.Entities;
+using Features.Auth.Dtos;
namespace Features.Auth.Repository;
-
///
/// Repository for authentication-related database operations including user registration and credential management.
///
@@ -11,20 +11,8 @@ public interface IAuthRepository
/// Registers a new user with account details and initial credential.
/// Uses stored procedure: USP_RegisterUser
///
- /// Unique username for the user
- /// User's first name
- /// User's last name
- /// User's email address
- /// User's date of birth
- /// Hashed password
- /// The newly created UserAccount with generated ID
Task RegisterUserAsync(
- string username,
- string firstName,
- string lastName,
- string email,
- DateTime dateOfBirth,
- string passwordHash
+ UserRegistrationDto userRegistrationDto
);
///