diff --git a/web/backend/Database/Database.Seed/Database.Seed.csproj b/web/backend/Database/Database.Seed/Database.Seed.csproj
index 9d11ce1..0d9b350 100644
--- a/web/backend/Database/Database.Seed/Database.Seed.csproj
+++ b/web/backend/Database/Database.Seed/Database.Seed.csproj
@@ -10,8 +10,15 @@
-
+
+
+
+
+
diff --git a/web/backend/Database/Database.Seed/DatabaseHelpers.cs b/web/backend/Database/Database.Seed/DatabaseHelpers.cs
new file mode 100644
index 0000000..574296b
--- /dev/null
+++ b/web/backend/Database/Database.Seed/DatabaseHelpers.cs
@@ -0,0 +1,54 @@
+using Microsoft.Data.SqlClient;
+
+namespace Database.Seed;
+
+public class ConnectionStrings
+{
+ ///
+ /// Builds a SQL Server connection string from the DB_SERVER, DB_NAME,
+ /// DB_USER, DB_PASSWORD, and DB_TRUST_SERVER_CERTIFICATE
+ /// environment variables.
+ ///
+ /// A fully built SQL Server connection string.
+ ///
+ /// Thrown when DB_SERVER, DB_NAME, DB_USER, or DB_PASSWORD
+ /// is not set.
+ ///
+ private static string GetSqlServerConnectionString()
+ {
+ string server =
+ Environment.GetEnvironmentVariable("DB_SERVER")
+ ?? throw new InvalidOperationException("DB_SERVER environment variable is not set");
+
+ string dbName =
+ Environment.GetEnvironmentVariable("DB_NAME")
+ ?? throw new InvalidOperationException("DB_NAME environment variable is not set");
+
+ string user =
+ Environment.GetEnvironmentVariable("DB_USER")
+ ?? throw new InvalidOperationException("DB_USER environment variable is not set");
+
+ string password =
+ Environment.GetEnvironmentVariable("DB_PASSWORD")
+ ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set");
+
+ string trustServerCertificate =
+ Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") ?? "True";
+
+ SqlConnectionStringBuilder builder = new()
+ {
+ DataSource = server,
+ InitialCatalog = dbName,
+ UserID = user,
+ Password = password,
+ TrustServerCertificate = bool.Parse(trustServerCertificate),
+ Encrypt = true,
+ };
+
+ return builder.ConnectionString;
+ }
+
+
+ public static string SqlServerConnectionString => GetSqlServerConnectionString();
+ public static string SqliteConnectionString => "Data Source=seed.sqlite";
+}
diff --git a/web/backend/Database/Database.Seed/ISeeder.cs b/web/backend/Database/Database.Seed/ISeeder.cs
deleted file mode 100644
index 7c08938..0000000
--- a/web/backend/Database/Database.Seed/ISeeder.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Microsoft.Data.SqlClient;
-
-namespace Database.Seed;
-
-///
-/// Defines a unit of seed data that can be applied to the database. Implementations
-/// should be safe to run against an already-seeded database (e.g. by checking for
-/// existing data before inserting) and may depend on data created by seeders that run
-/// before them.
-///
-internal interface ISeeder
-{
- ///
- /// Inserts this seeder's data into the database using the supplied open connection.
- ///
- /// An open connection to the target database.
- /// A task that completes when seeding is finished.
- Task SeedAsync(SqlConnection connection);
-}
diff --git a/web/backend/Database/Database.Seed/LocationSeeder.cs b/web/backend/Database/Database.Seed/LocationSeeder.cs
deleted file mode 100644
index e3ba5c2..0000000
--- a/web/backend/Database/Database.Seed/LocationSeeder.cs
+++ /dev/null
@@ -1,341 +0,0 @@
-using System.Data;
-using Microsoft.Data.SqlClient;
-
-namespace Database.Seed;
-
-///
-/// Seeds the location hierarchy (countries, states/provinces, and cities) used to
-/// associate other entities (e.g. breweries, users) with a geographic location.
-/// Countries must be seeded before states/provinces, which must be seeded before
-/// cities, since each level references its parent by code. The underlying stored
-/// procedures (USP_CreateCountry, USP_CreateStateProvince,
-/// USP_CreateCity) are expected to be idempotent, so re-running this seeder
-/// against an already-seeded database is safe.
-///
-internal class LocationSeeder : ISeeder
-{
- /// The set of countries to seed, identified by name and ISO 3166-1 code.
- private static readonly IReadOnlyList<(string CountryName, string CountryCode)> Countries =
- [
- ("Canada", "CA"),
- ("Mexico", "MX"),
- ("United States", "US"),
- ];
-
- ///
- /// The set of states/provinces to seed, each identified by name, ISO 3166-2 code,
- /// and the ISO 3166-1 code of its parent country.
- ///
- private static IReadOnlyList<(
- string StateProvinceName,
- string StateProvinceCode,
- string CountryCode
- )> States { get; } =
- [
- ("Alabama", "US-AL", "US"),
- ("Alaska", "US-AK", "US"),
- ("Arizona", "US-AZ", "US"),
- ("Arkansas", "US-AR", "US"),
- ("California", "US-CA", "US"),
- ("Colorado", "US-CO", "US"),
- ("Connecticut", "US-CT", "US"),
- ("Delaware", "US-DE", "US"),
- ("Florida", "US-FL", "US"),
- ("Georgia", "US-GA", "US"),
- ("Hawaii", "US-HI", "US"),
- ("Idaho", "US-ID", "US"),
- ("Illinois", "US-IL", "US"),
- ("Indiana", "US-IN", "US"),
- ("Iowa", "US-IA", "US"),
- ("Kansas", "US-KS", "US"),
- ("Kentucky", "US-KY", "US"),
- ("Louisiana", "US-LA", "US"),
- ("Maine", "US-ME", "US"),
- ("Maryland", "US-MD", "US"),
- ("Massachusetts", "US-MA", "US"),
- ("Michigan", "US-MI", "US"),
- ("Minnesota", "US-MN", "US"),
- ("Mississippi", "US-MS", "US"),
- ("Missouri", "US-MO", "US"),
- ("Montana", "US-MT", "US"),
- ("Nebraska", "US-NE", "US"),
- ("Nevada", "US-NV", "US"),
- ("New Hampshire", "US-NH", "US"),
- ("New Jersey", "US-NJ", "US"),
- ("New Mexico", "US-NM", "US"),
- ("New York", "US-NY", "US"),
- ("North Carolina", "US-NC", "US"),
- ("North Dakota", "US-ND", "US"),
- ("Ohio", "US-OH", "US"),
- ("Oklahoma", "US-OK", "US"),
- ("Oregon", "US-OR", "US"),
- ("Pennsylvania", "US-PA", "US"),
- ("Rhode Island", "US-RI", "US"),
- ("South Carolina", "US-SC", "US"),
- ("South Dakota", "US-SD", "US"),
- ("Tennessee", "US-TN", "US"),
- ("Texas", "US-TX", "US"),
- ("Utah", "US-UT", "US"),
- ("Vermont", "US-VT", "US"),
- ("Virginia", "US-VA", "US"),
- ("Washington", "US-WA", "US"),
- ("West Virginia", "US-WV", "US"),
- ("Wisconsin", "US-WI", "US"),
- ("Wyoming", "US-WY", "US"),
- ("District of Columbia", "US-DC", "US"),
- ("Puerto Rico", "US-PR", "US"),
- ("U.S. Virgin Islands", "US-VI", "US"),
- ("Guam", "US-GU", "US"),
- ("Northern Mariana Islands", "US-MP", "US"),
- ("American Samoa", "US-AS", "US"),
- ("Ontario", "CA-ON", "CA"),
- ("Québec", "CA-QC", "CA"),
- ("Nova Scotia", "CA-NS", "CA"),
- ("New Brunswick", "CA-NB", "CA"),
- ("Manitoba", "CA-MB", "CA"),
- ("British Columbia", "CA-BC", "CA"),
- ("Prince Edward Island", "CA-PE", "CA"),
- ("Saskatchewan", "CA-SK", "CA"),
- ("Alberta", "CA-AB", "CA"),
- ("Newfoundland and Labrador", "CA-NL", "CA"),
- ("Northwest Territories", "CA-NT", "CA"),
- ("Yukon", "CA-YT", "CA"),
- ("Nunavut", "CA-NU", "CA"),
- ("Aguascalientes", "MX-AGU", "MX"),
- ("Baja California", "MX-BCN", "MX"),
- ("Baja California Sur", "MX-BCS", "MX"),
- ("Campeche", "MX-CAM", "MX"),
- ("Chiapas", "MX-CHP", "MX"),
- ("Chihuahua", "MX-CHH", "MX"),
- ("Coahuila de Zaragoza", "MX-COA", "MX"),
- ("Colima", "MX-COL", "MX"),
- ("Durango", "MX-DUR", "MX"),
- ("Guanajuato", "MX-GUA", "MX"),
- ("Guerrero", "MX-GRO", "MX"),
- ("Hidalgo", "MX-HID", "MX"),
- ("Jalisco", "MX-JAL", "MX"),
- ("México State", "MX-MEX", "MX"),
- ("Michoacán de Ocampo", "MX-MIC", "MX"),
- ("Morelos", "MX-MOR", "MX"),
- ("Nayarit", "MX-NAY", "MX"),
- ("Nuevo León", "MX-NLE", "MX"),
- ("Oaxaca", "MX-OAX", "MX"),
- ("Puebla", "MX-PUE", "MX"),
- ("Querétaro", "MX-QUE", "MX"),
- ("Quintana Roo", "MX-ROO", "MX"),
- ("San Luis Potosí", "MX-SLP", "MX"),
- ("Sinaloa", "MX-SIN", "MX"),
- ("Sonora", "MX-SON", "MX"),
- ("Tabasco", "MX-TAB", "MX"),
- ("Tamaulipas", "MX-TAM", "MX"),
- ("Tlaxcala", "MX-TLA", "MX"),
- ("Veracruz de Ignacio de la Llave", "MX-VER", "MX"),
- ("Yucatán", "MX-YUC", "MX"),
- ("Zacatecas", "MX-ZAC", "MX"),
- ("Ciudad de México", "MX-CMX", "MX"),
- ];
-
- ///
- /// The set of cities to seed, each identified by name and the ISO 3166-2 code of its
- /// parent state/province.
- ///
- private static IReadOnlyList<(string StateProvinceCode, string CityName)> Cities { get; } =
- [
- ("US-CA", "Los Angeles"),
- ("US-CA", "San Diego"),
- ("US-CA", "San Francisco"),
- ("US-CA", "Sacramento"),
- ("US-TX", "Houston"),
- ("US-TX", "Dallas"),
- ("US-TX", "Austin"),
- ("US-TX", "San Antonio"),
- ("US-FL", "Miami"),
- ("US-FL", "Orlando"),
- ("US-FL", "Tampa"),
- ("US-NY", "New York"),
- ("US-NY", "Buffalo"),
- ("US-NY", "Rochester"),
- ("US-IL", "Chicago"),
- ("US-IL", "Springfield"),
- ("US-PA", "Philadelphia"),
- ("US-PA", "Pittsburgh"),
- ("US-AZ", "Phoenix"),
- ("US-AZ", "Tucson"),
- ("US-CO", "Denver"),
- ("US-CO", "Colorado Springs"),
- ("US-MA", "Boston"),
- ("US-MA", "Worcester"),
- ("US-WA", "Seattle"),
- ("US-WA", "Spokane"),
- ("US-GA", "Atlanta"),
- ("US-GA", "Savannah"),
- ("US-NV", "Las Vegas"),
- ("US-NV", "Reno"),
- ("US-MI", "Detroit"),
- ("US-MI", "Grand Rapids"),
- ("US-MN", "Minneapolis"),
- ("US-MN", "Saint Paul"),
- ("US-OH", "Columbus"),
- ("US-OH", "Cleveland"),
- ("US-OR", "Portland"),
- ("US-OR", "Salem"),
- ("US-TN", "Nashville"),
- ("US-TN", "Memphis"),
- ("US-VA", "Richmond"),
- ("US-VA", "Virginia Beach"),
- ("US-MD", "Baltimore"),
- ("US-MD", "Frederick"),
- ("US-DC", "Washington"),
- ("US-UT", "Salt Lake City"),
- ("US-UT", "Provo"),
- ("US-LA", "New Orleans"),
- ("US-LA", "Baton Rouge"),
- ("US-KY", "Louisville"),
- ("US-KY", "Lexington"),
- ("US-IA", "Des Moines"),
- ("US-IA", "Cedar Rapids"),
- ("US-OK", "Oklahoma City"),
- ("US-OK", "Tulsa"),
- ("US-NE", "Omaha"),
- ("US-NE", "Lincoln"),
- ("US-MO", "Kansas City"),
- ("US-MO", "St. Louis"),
- ("US-NC", "Charlotte"),
- ("US-NC", "Raleigh"),
- ("US-SC", "Columbia"),
- ("US-SC", "Charleston"),
- ("US-WI", "Milwaukee"),
- ("US-WI", "Madison"),
- ("US-MN", "Duluth"),
- ("US-AK", "Anchorage"),
- ("US-HI", "Honolulu"),
- ("CA-ON", "Toronto"),
- ("CA-ON", "Ottawa"),
- ("CA-QC", "Montréal"),
- ("CA-QC", "Québec City"),
- ("CA-BC", "Vancouver"),
- ("CA-BC", "Victoria"),
- ("CA-AB", "Calgary"),
- ("CA-AB", "Edmonton"),
- ("CA-MB", "Winnipeg"),
- ("CA-NS", "Halifax"),
- ("CA-SK", "Saskatoon"),
- ("CA-SK", "Regina"),
- ("CA-NB", "Moncton"),
- ("CA-NB", "Saint John"),
- ("CA-PE", "Charlottetown"),
- ("CA-NL", "St. John's"),
- ("CA-ON", "Hamilton"),
- ("CA-ON", "London"),
- ("CA-QC", "Gatineau"),
- ("CA-QC", "Laval"),
- ("CA-BC", "Kelowna"),
- ("CA-AB", "Red Deer"),
- ("CA-MB", "Brandon"),
- ("MX-CMX", "Ciudad de México"),
- ("MX-JAL", "Guadalajara"),
- ("MX-NLE", "Monterrey"),
- ("MX-PUE", "Puebla"),
- ("MX-ROO", "Cancún"),
- ("MX-GUA", "Guanajuato"),
- ("MX-MIC", "Morelia"),
- ("MX-BCN", "Tijuana"),
- ("MX-JAL", "Zapopan"),
- ("MX-NLE", "San Nicolás"),
- ("MX-CAM", "Campeche"),
- ("MX-TAB", "Villahermosa"),
- ("MX-VER", "Veracruz"),
- ("MX-OAX", "Oaxaca"),
- ("MX-SLP", "San Luis Potosí"),
- ("MX-CHH", "Chihuahua"),
- ("MX-AGU", "Aguascalientes"),
- ("MX-MEX", "Toluca"),
- ("MX-COA", "Saltillo"),
- ("MX-BCS", "La Paz"),
- ("MX-NAY", "Tepic"),
- ("MX-ZAC", "Zacatecas"),
- ];
-
- ///
- /// Seeds all countries, then states/provinces, then cities, in that order, so that
- /// each level's parent reference already exists by the time it is created.
- ///
- /// An open connection to the target database.
- /// A task that completes when all locations have been seeded.
- public async Task SeedAsync(SqlConnection connection)
- {
- foreach ((string countryName, string countryCode) in Countries)
- await CreateCountryAsync(connection, countryName, countryCode);
-
- foreach ((string stateProvinceName, string stateProvinceCode, string countryCode) in States)
- await CreateStateProvinceAsync(
- connection,
- stateProvinceName,
- stateProvinceCode,
- countryCode
- );
-
- foreach ((string stateProvinceCode, string cityName) in Cities)
- await CreateCityAsync(connection, cityName, stateProvinceCode);
- }
-
- /// Creates a single country by invoking the dbo.USP_CreateCountry stored procedure.
- /// An open connection to the target database.
- /// The display name of the country.
- /// The ISO 3166-1 code of the country.
- /// A task that completes when the country has been created.
- private static async Task CreateCountryAsync(
- SqlConnection connection,
- string countryName,
- string countryCode
- )
- {
- await using SqlCommand command = new("dbo.USP_CreateCountry", connection);
- command.CommandType = CommandType.StoredProcedure;
- command.Parameters.AddWithValue("@CountryName", countryName);
- command.Parameters.AddWithValue("@ISO3166_1", countryCode);
-
- await command.ExecuteNonQueryAsync();
- }
-
- /// Creates a single state/province by invoking the dbo.USP_CreateStateProvince stored procedure.
- /// An open connection to the target database.
- /// The display name of the state/province.
- /// The ISO 3166-2 code of the state/province.
- /// The ISO 3166-1 code of the parent country, which must already exist.
- /// A task that completes when the state/province has been created.
- private static async Task CreateStateProvinceAsync(
- SqlConnection connection,
- string stateProvinceName,
- string stateProvinceCode,
- string countryCode
- )
- {
- await using SqlCommand command = new("dbo.USP_CreateStateProvince", connection);
- command.CommandType = CommandType.StoredProcedure;
- command.Parameters.AddWithValue("@StateProvinceName", stateProvinceName);
- command.Parameters.AddWithValue("@ISO3166_2", stateProvinceCode);
- command.Parameters.AddWithValue("@CountryCode", countryCode);
-
- await command.ExecuteNonQueryAsync();
- }
-
- /// Creates a single city by invoking the dbo.USP_CreateCity stored procedure.
- /// An open connection to the target database.
- /// The display name of the city.
- /// The ISO 3166-2 code of the parent state/province, which must already exist.
- /// A task that completes when the city has been created.
- private static async Task CreateCityAsync(
- SqlConnection connection,
- string cityName,
- string stateProvinceCode
- )
- {
- await using SqlCommand command = new("dbo.USP_CreateCity", connection);
- command.CommandType = CommandType.StoredProcedure;
- command.Parameters.AddWithValue("@CityName", cityName);
- command.Parameters.AddWithValue("@StateProvinceCode", stateProvinceCode);
-
- await command.ExecuteNonQueryAsync();
- }
-}
diff --git a/web/backend/Database/Database.Seed/Program.cs b/web/backend/Database/Database.Seed/Program.cs
deleted file mode 100644
index fa048de..0000000
--- a/web/backend/Database/Database.Seed/Program.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using Database.Seed;
-using Microsoft.Data.SqlClient;
-
-// Entry point for the database seeding utility. Connects to the target database
-// (retrying on transient failures), then runs each registered ISeeder in order to
-// populate location and user data.
-
-///
-/// Builds a SQL Server connection string from the DB_SERVER, DB_NAME,
-/// DB_USER, DB_PASSWORD, and DB_TRUST_SERVER_CERTIFICATE
-/// environment variables.
-///
-/// A fully built SQL Server connection string.
-///
-/// Thrown when DB_SERVER, DB_NAME, DB_USER, or DB_PASSWORD
-/// is not set.
-///
-string BuildConnectionString()
-{
- string server =
- Environment.GetEnvironmentVariable("DB_SERVER")
- ?? throw new InvalidOperationException("DB_SERVER environment variable is not set");
-
- string dbName =
- Environment.GetEnvironmentVariable("DB_NAME")
- ?? throw new InvalidOperationException("DB_NAME environment variable is not set");
-
- string user =
- Environment.GetEnvironmentVariable("DB_USER")
- ?? throw new InvalidOperationException("DB_USER environment variable is not set");
-
- string password =
- Environment.GetEnvironmentVariable("DB_PASSWORD")
- ?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set");
-
- string trustServerCertificate =
- Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE") ?? "True";
-
- SqlConnectionStringBuilder builder = new()
- {
- DataSource = server,
- InitialCatalog = dbName,
- UserID = user,
- Password = password,
- TrustServerCertificate = bool.Parse(trustServerCertificate),
- Encrypt = true,
- };
-
- return builder.ConnectionString;
-}
-
-try
-{
- string connectionString = BuildConnectionString();
-
- Console.WriteLine("Attempting to connect to database...");
-
- // Retry logic for database connection
- SqlConnection? connection = null;
- int maxRetries = 10;
- int retryDelayMs = 2000;
-
- for (int attempt = 1; attempt <= maxRetries; attempt++)
- try
- {
- connection = new SqlConnection(connectionString);
- await connection.OpenAsync();
- Console.WriteLine($"Connected to database successfully on attempt {attempt}.");
- break;
- }
- catch (SqlException ex) when (attempt < maxRetries)
- {
- Console.WriteLine($"Connection attempt {attempt}/{maxRetries} failed: {ex.Message}");
- Console.WriteLine($"Retrying in {retryDelayMs}ms...");
- await Task.Delay(retryDelayMs);
- connection?.Dispose();
- connection = null;
- }
-
- if (connection == null)
- throw new Exception($"Failed to connect to database after {maxRetries} attempts.");
-
- Console.WriteLine("Starting seeding...");
-
- using (connection)
- {
- ISeeder[] seeders = [new LocationSeeder(), new UserSeeder()];
-
- foreach (ISeeder seeder in seeders)
- {
- Console.WriteLine($"Seeding {seeder.GetType().Name}...");
- await seeder.SeedAsync(connection);
- Console.WriteLine($"{seeder.GetType().Name} seeded.");
- }
-
- Console.WriteLine("Seed completed successfully.");
- }
-
- return 0;
-}
-catch (Exception ex)
-{
- Console.Error.WriteLine("Seed failed:");
- Console.Error.WriteLine(ex);
- return 1;
-}
diff --git a/web/backend/Database/Database.Seed/UserSeeder.cs b/web/backend/Database/Database.Seed/UserSeeder.cs
deleted file mode 100644
index 5a3a312..0000000
--- a/web/backend/Database/Database.Seed/UserSeeder.cs
+++ /dev/null
@@ -1,303 +0,0 @@
-using System.Data;
-using System.Security.Cryptography;
-using System.Text;
-using idunno.Password;
-using Konscious.Security.Cryptography;
-using Microsoft.Data.SqlClient;
-
-namespace Database.Seed;
-
-///
-/// Seeds user accounts, credentials, and verification records. Creates one fixed
-/// "Test User" account (test.user@thebiergarten.app / password "password")
-/// for testing, followed by a randomly-generated account for each name in
-/// , each with a random password and date of birth. Skips
-/// adding a verification record for a user that already has one, so this seeder is
-/// safe to re-run, though re-running will still attempt to register (and may fail on)
-/// users that already exist.
-///
-internal class UserSeeder : ISeeder
-{
- /// The first/last name pairs used to generate seed user accounts.
- private static readonly IReadOnlyList<(string FirstName, string LastName)> SeedNames =
- [
- ("Aarya", "Mathews"),
- ("Aiden", "Wells"),
- ("Aleena", "Gonzalez"),
- ("Alessandra", "Nelson"),
- ("Amari", "Tucker"),
- ("Ameer", "Huff"),
- ("Amirah", "Hicks"),
- ("Analia", "Dominguez"),
- ("Anne", "Jenkins"),
- ("Apollo", "Davis"),
- ("Arianna", "White"),
- ("Aubree", "Moore"),
- ("Aubrielle", "Raymond"),
- ("Aydin", "Odom"),
- ("Bowen", "Casey"),
- ("Brock", "Huber"),
- ("Caiden", "Strong"),
- ("Cecilia", "Rosales"),
- ("Celeste", "Barber"),
- ("Chance", "Small"),
- ("Clara", "Roberts"),
- ("Collins", "Brandt"),
- ("Damir", "Wallace"),
- ("Declan", "Crawford"),
- ("Dennis", "Decker"),
- ("Dylan", "Lang"),
- ("Eliza", "Kane"),
- ("Elle", "Poole"),
- ("Elliott", "Miles"),
- ("Emelia", "Lucas"),
- ("Emilia", "Simpson"),
- ("Emmett", "Lugo"),
- ("Ethan", "Stephens"),
- ("Etta", "Woods"),
- ("Gael", "Moran"),
- ("Grant", "Benson"),
- ("Gwen", "James"),
- ("Huxley", "Chen"),
- ("Isabella", "Fisher"),
- ("Ivan", "Mathis"),
- ("Jamir", "McMillan"),
- ("Jaxson", "Shields"),
- ("Jimmy", "Richmond"),
- ("Josiah", "Flores"),
- ("Kaden", "Enriquez"),
- ("Kai", "Lawson"),
- ("Karsyn", "Adkins"),
- ("Karsyn", "Proctor"),
- ("Kayden", "Henson"),
- ("Kaylie", "Spears"),
- ("Kinslee", "Jones"),
- ("Kora", "Guerra"),
- ("Lane", "Skinner"),
- ("Laylani", "Christian"),
- ("Ledger", "Carroll"),
- ("Leilany", "Small"),
- ("Leland", "McCall"),
- ("Leonard", "Calhoun"),
- ("Levi", "Ochoa"),
- ("Lillie", "Vang"),
- ("Lola", "Sheppard"),
- ("Luciana", "Poole"),
- ("Maddox", "Hughes"),
- ("Mara", "Blackwell"),
- ("Marcellus", "Bartlett"),
- ("Margo", "Koch"),
- ("Maurice", "Gibson"),
- ("Maxton", "Dodson"),
- ("Mia", "Parrish"),
- ("Millie", "Fuentes"),
- ("Nellie", "Villanueva"),
- ("Nicolas", "Mata"),
- ("Nicolas", "Miller"),
- ("Oakleigh", "Foster"),
- ("Octavia", "Pierce"),
- ("Paisley", "Allison"),
- ("Quincy", "Andersen"),
- ("Quincy", "Frazier"),
- ("Raiden", "Roberts"),
- ("Raquel", "Lara"),
- ("Rudy", "McIntosh"),
- ("Salvador", "Stein"),
- ("Samantha", "Dickson"),
- ("Solomon", "Richards"),
- ("Sylvia", "Hanna"),
- ("Talia", "Trujillo"),
- ("Thalia", "Farrell"),
- ("Trent", "Mayo"),
- ("Trinity", "Cummings"),
- ("Ty", "Perry"),
- ("Tyler", "Romero"),
- ("Valeria", "Pierce"),
- ("Vance", "Neal"),
- ("Whitney", "Bell"),
- ("Wilder", "Graves"),
- ("William", "Logan"),
- ("Zara", "Wilkinson"),
- ("Zaria", "Gibson"),
- ("Zion", "Watkins"),
- ("Zoie", "Armstrong"),
- ];
-
- ///
- /// Registers a fixed test user account followed by one randomly-generated account
- /// per entry in , adding a user verification record for each
- /// newly created account that does not already have one. Progress counts are
- /// written to the console on completion.
- ///
- /// An open connection to the target database.
- /// A task that completes when all users have been seeded.
- public async Task SeedAsync(SqlConnection connection)
- {
- PasswordGenerator generator = new();
- Random rng = new();
- int createdUsers = 0;
- int createdCredentials = 0;
- int createdVerifications = 0;
-
- // create a known user for testing purposes
- {
- const string firstName = "Test";
- const string lastName = "User";
- const string email = "test.user@thebiergarten.app";
- DateTime dob = new(1985, 03, 01);
- string hash = GeneratePasswordHash("password");
-
- await RegisterUserAsync(
- connection,
- $"{firstName}.{lastName}",
- firstName,
- lastName,
- dob,
- email,
- hash
- );
- }
- foreach ((string firstName, string lastName) in SeedNames)
- {
- // prepare user fields
- string username = $"{firstName[0]}.{lastName}";
- string email = $"{firstName}.{lastName}@thebiergarten.app";
- DateTime dob = GenerateDateOfBirth(rng);
-
- // generate a password and hash it
- string pwd = generator.Generate(64, 10, 10);
- string hash = GeneratePasswordHash(pwd);
-
- // register the user (creates account + credential)
- Guid id = await RegisterUserAsync(
- connection,
- username,
- firstName,
- lastName,
- dob,
- email,
- hash
- );
- createdUsers++;
- createdCredentials++;
-
- // add user verification
- if (await HasUserVerificationAsync(connection, id))
- continue;
-
- await AddUserVerificationAsync(connection, id);
- createdVerifications++;
- }
-
- Console.WriteLine($"Created {createdUsers} user accounts.");
- Console.WriteLine($"Added {createdCredentials} user credentials.");
- Console.WriteLine($"Added {createdVerifications} user verifications.");
- }
-
- ///
- /// Registers a new user account and its credential by invoking the
- /// dbo.USP_RegisterUser stored procedure.
- ///
- /// An open connection to the target database.
- /// The unique username for the account.
- /// The user's first name.
- /// The user's last name.
- /// The user's date of birth.
- /// The user's email address.
- /// The salted password hash, as produced by .
- /// The newly created user account's identifier.
- private static async Task RegisterUserAsync(
- SqlConnection connection,
- string username,
- string firstName,
- string lastName,
- DateTime dateOfBirth,
- string email,
- string hash
- )
- {
- await using SqlCommand command = new("dbo.USP_RegisterUser", connection);
- command.CommandType = CommandType.StoredProcedure;
-
- command.Parameters.Add("@Username", SqlDbType.VarChar, 64).Value = username;
- command.Parameters.Add("@FirstName", SqlDbType.NVarChar, 128).Value = firstName;
- command.Parameters.Add("@LastName", SqlDbType.NVarChar, 128).Value = lastName;
- command.Parameters.Add("@DateOfBirth", SqlDbType.DateTime).Value = dateOfBirth;
- command.Parameters.Add("@Email", SqlDbType.VarChar, 128).Value = email;
- command.Parameters.Add("@Hash", SqlDbType.NVarChar, -1).Value = hash;
-
- object? result = await command.ExecuteScalarAsync();
-
- return (Guid)result!;
- }
-
- ///
- /// Hashes a plaintext password using Argon2id with a randomly generated 16-byte
- /// salt, a degree of parallelism matching the available processor count, 64 MB of
- /// memory, and 4 iterations.
- ///
- /// The plaintext password to hash.
- /// A string containing the base64-encoded salt and hash, separated by a colon.
- private static string GeneratePasswordHash(string pwd)
- {
- byte[] salt = RandomNumberGenerator.GetBytes(16);
-
- Argon2id argon2 = new(Encoding.UTF8.GetBytes(pwd))
- {
- Salt = salt,
- DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1),
- MemorySize = 65536,
- Iterations = 4,
- };
-
- byte[] hash = argon2.GetBytes(32);
- return $"{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}";
- }
-
- /// Checks whether a user verification record already exists for the given user account.
- /// An open connection to the target database.
- /// The user account identifier to check.
- /// true if a verification record already exists; otherwise false.
- private static async Task HasUserVerificationAsync(
- SqlConnection connection,
- Guid userAccountId
- )
- {
- const string sql = """
- SELECT 1
- FROM dbo.UserVerification
- WHERE UserAccountId = @UserAccountId;
- """;
- await using SqlCommand command = new(sql, connection);
- command.Parameters.AddWithValue("@UserAccountId", userAccountId);
- object? result = await command.ExecuteScalarAsync();
- return result is not null;
- }
-
- /// Creates a user verification record by invoking the dbo.USP_CreateUserVerification stored procedure.
- /// An open connection to the target database.
- /// The identifier of the user account to verify.
- /// A task that completes when the verification record has been created.
- private static async Task AddUserVerificationAsync(SqlConnection connection, Guid userAccountId)
- {
- await using SqlCommand command = new("dbo.USP_CreateUserVerification", connection);
- command.CommandType = CommandType.StoredProcedure;
- command.Parameters.AddWithValue("@UserAccountID_", userAccountId);
-
- await command.ExecuteNonQueryAsync();
- }
-
- ///
- /// Generates a random date of birth corresponding to an age between 19 and 48
- /// years (inclusive), with a random day offset within that birth year.
- ///
- /// The random number source to use.
- /// A randomly generated date of birth.
- private static DateTime GenerateDateOfBirth(Random random)
- {
- int age = 19 + random.Next(0, 30);
- DateTime baseDate = DateTime.UtcNow.Date.AddYears(-age);
- int offsetDays = random.Next(0, 365);
- return baseDate.AddDays(-offsetDays);
- }
-}
diff --git a/web/backend/Database/Program.cs b/web/backend/Database/Program.cs
new file mode 100644
index 0000000..f486b06
--- /dev/null
+++ b/web/backend/Database/Program.cs
@@ -0,0 +1,18 @@
+namespace Database.Seed;
+
+
+public interface IExportDatabase
+{
+ void SeedLocations(IEnumerable locations);
+ void SeedUsers(IEnumerable users);
+
+ void SeedBreweries(IEnumerable breweries);
+}
+
+public interface IImportDatabase
+{
+ IEnumerable GetLocations();
+ IEnumerable GetUsers();
+
+ IEnumerable GetBreweries();
+}
diff --git a/web/backend/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs b/web/backend/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs
index 72837bb..f8707b3 100644
--- a/web/backend/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs
+++ b/web/backend/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs
@@ -88,7 +88,7 @@ public class JwtInfrastructure : ITokenInfrastructure
return new ClaimsPrincipal(result.ClaimsIdentity);
}
- catch (Exception e)
+ catch (Exception)
{
throw new UnauthorizedException("Invalid token");
}
diff --git a/web/backend/Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj b/web/backend/Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj
index 7ff048e..dcc69bb 100644
--- a/web/backend/Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj
+++ b/web/backend/Infrastructure/Infrastructure.Sql/Infrastructure.Sql.csproj
@@ -6,7 +6,7 @@
Infrastructure.Sql
-
+