mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-17 09:47:23 +00:00
begin work
This commit is contained in:
@@ -10,8 +10,15 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="idunno.Password.Generator" Version="1.0.1" />
|
||||
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.9" />
|
||||
<PackageReference Include="dbup" Version="5.0.41" />
|
||||
|
||||
<!--
|
||||
remove this override once microsoft ships official fix to vulnerability
|
||||
https://github.com/advisories/GHSA-2m69-gcr7-jv3q
|
||||
-->
|
||||
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
54
web/backend/Database/Database.Seed/DatabaseHelpers.cs
Normal file
54
web/backend/Database/Database.Seed/DatabaseHelpers.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Database.Seed;
|
||||
|
||||
public class ConnectionStrings
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a SQL Server connection string from the <c>DB_SERVER</c>, <c>DB_NAME</c>,
|
||||
/// <c>DB_USER</c>, <c>DB_PASSWORD</c>, and <c>DB_TRUST_SERVER_CERTIFICATE</c>
|
||||
/// environment variables.
|
||||
/// </summary>
|
||||
/// <returns>A fully built SQL Server connection string.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when <c>DB_SERVER</c>, <c>DB_NAME</c>, <c>DB_USER</c>, or <c>DB_PASSWORD</c>
|
||||
/// is not set.
|
||||
/// </exception>
|
||||
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";
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Database.Seed;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal interface ISeeder
|
||||
{
|
||||
/// <summary>
|
||||
/// Inserts this seeder's data into the database using the supplied open connection.
|
||||
/// </summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <returns>A task that completes when seeding is finished.</returns>
|
||||
Task SeedAsync(SqlConnection connection);
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
using System.Data;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Database.Seed;
|
||||
|
||||
/// <summary>
|
||||
/// 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 (<c>USP_CreateCountry</c>, <c>USP_CreateStateProvince</c>,
|
||||
/// <c>USP_CreateCity</c>) are expected to be idempotent, so re-running this seeder
|
||||
/// against an already-seeded database is safe.
|
||||
/// </summary>
|
||||
internal class LocationSeeder : ISeeder
|
||||
{
|
||||
/// <summary>The set of countries to seed, identified by name and ISO 3166-1 code.</summary>
|
||||
private static readonly IReadOnlyList<(string CountryName, string CountryCode)> Countries =
|
||||
[
|
||||
("Canada", "CA"),
|
||||
("Mexico", "MX"),
|
||||
("United States", "US"),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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"),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// The set of cities to seed, each identified by name and the ISO 3166-2 code of its
|
||||
/// parent state/province.
|
||||
/// </summary>
|
||||
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"),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <returns>A task that completes when all locations have been seeded.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Creates a single country by invoking the <c>dbo.USP_CreateCountry</c> stored procedure.</summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <param name="countryName">The display name of the country.</param>
|
||||
/// <param name="countryCode">The ISO 3166-1 code of the country.</param>
|
||||
/// <returns>A task that completes when the country has been created.</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>Creates a single state/province by invoking the <c>dbo.USP_CreateStateProvince</c> stored procedure.</summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <param name="stateProvinceName">The display name of the state/province.</param>
|
||||
/// <param name="stateProvinceCode">The ISO 3166-2 code of the state/province.</param>
|
||||
/// <param name="countryCode">The ISO 3166-1 code of the parent country, which must already exist.</param>
|
||||
/// <returns>A task that completes when the state/province has been created.</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>Creates a single city by invoking the <c>dbo.USP_CreateCity</c> stored procedure.</summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <param name="cityName">The display name of the city.</param>
|
||||
/// <param name="stateProvinceCode">The ISO 3166-2 code of the parent state/province, which must already exist.</param>
|
||||
/// <returns>A task that completes when the city has been created.</returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
/// <summary>
|
||||
/// Builds a SQL Server connection string from the <c>DB_SERVER</c>, <c>DB_NAME</c>,
|
||||
/// <c>DB_USER</c>, <c>DB_PASSWORD</c>, and <c>DB_TRUST_SERVER_CERTIFICATE</c>
|
||||
/// environment variables.
|
||||
/// </summary>
|
||||
/// <returns>A fully built SQL Server connection string.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when <c>DB_SERVER</c>, <c>DB_NAME</c>, <c>DB_USER</c>, or <c>DB_PASSWORD</c>
|
||||
/// is not set.
|
||||
/// </exception>
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Seeds user accounts, credentials, and verification records. Creates one fixed
|
||||
/// "Test User" account (<c>test.user@thebiergarten.app</c> / password <c>"password"</c>)
|
||||
/// for testing, followed by a randomly-generated account for each name in
|
||||
/// <see cref="SeedNames" />, 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.
|
||||
/// </summary>
|
||||
internal class UserSeeder : ISeeder
|
||||
{
|
||||
/// <summary>The first/last name pairs used to generate seed user accounts.</summary>
|
||||
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"),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Registers a fixed test user account followed by one randomly-generated account
|
||||
/// per entry in <see cref="SeedNames" />, 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.
|
||||
/// </summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <returns>A task that completes when all users have been seeded.</returns>
|
||||
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.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new user account and its credential by invoking the
|
||||
/// <c>dbo.USP_RegisterUser</c> stored procedure.
|
||||
/// </summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <param name="username">The unique username for the account.</param>
|
||||
/// <param name="firstName">The user's first name.</param>
|
||||
/// <param name="lastName">The user's last name.</param>
|
||||
/// <param name="dateOfBirth">The user's date of birth.</param>
|
||||
/// <param name="email">The user's email address.</param>
|
||||
/// <param name="hash">The salted password hash, as produced by <see cref="GeneratePasswordHash" />.</param>
|
||||
/// <returns>The newly created user account's <see cref="Guid" /> identifier.</returns>
|
||||
private static async Task<Guid> 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!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="pwd">The plaintext password to hash.</param>
|
||||
/// <returns>A string containing the base64-encoded salt and hash, separated by a colon.</returns>
|
||||
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)}";
|
||||
}
|
||||
|
||||
/// <summary>Checks whether a user verification record already exists for the given user account.</summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <param name="userAccountId">The user account identifier to check.</param>
|
||||
/// <returns><c>true</c> if a verification record already exists; otherwise <c>false</c>.</returns>
|
||||
private static async Task<bool> 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;
|
||||
}
|
||||
|
||||
/// <summary>Creates a user verification record by invoking the <c>dbo.USP_CreateUserVerification</c> stored procedure.</summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <param name="userAccountId">The identifier of the user account to verify.</param>
|
||||
/// <returns>A task that completes when the verification record has been created.</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="random">The random number source to use.</param>
|
||||
/// <returns>A randomly generated date of birth.</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
18
web/backend/Database/Program.cs
Normal file
18
web/backend/Database/Program.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Database.Seed;
|
||||
|
||||
|
||||
public interface IExportDatabase
|
||||
{
|
||||
void SeedLocations(IEnumerable<Location> locations);
|
||||
void SeedUsers(IEnumerable<User> users);
|
||||
|
||||
void SeedBreweries(IEnumerable<Brewery> breweries);
|
||||
}
|
||||
|
||||
public interface IImportDatabase
|
||||
{
|
||||
IEnumerable<Location> GetLocations();
|
||||
IEnumerable<User> GetUsers();
|
||||
|
||||
IEnumerable<Brewery> GetBreweries();
|
||||
}
|
||||
Reference in New Issue
Block a user