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 - +