diff --git a/.idea/.idea.biergarten/.idea/.gitignore b/.idea/.idea.biergarten/.idea/.gitignore new file mode 100644 index 0000000..611d03c --- /dev/null +++ b/.idea/.idea.biergarten/.idea/.gitignore @@ -0,0 +1,15 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/.idea.biergarten.iml +/contentModel.xml +/modules.xml +/projectSettingsUpdater.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/.idea.biergarten/.idea/.name b/.idea/.idea.biergarten/.idea/.name new file mode 100644 index 0000000..a1865e9 --- /dev/null +++ b/.idea/.idea.biergarten/.idea/.name @@ -0,0 +1 @@ +biergarten \ No newline at end of file diff --git a/.idea/.idea.biergarten/.idea/dataSources.xml b/.idea/.idea.biergarten/.idea/dataSources.xml new file mode 100644 index 0000000..7b8055e --- /dev/null +++ b/.idea/.idea.biergarten/.idea/dataSources.xml @@ -0,0 +1,24 @@ + + + + + sqlserver.jb + true + com.jetbrains.jdbc.sqlserver.SqlServerDriver + Server=localhost;Database=Biergarten;TrustServerCertificate=True; + + + + + + $ProjectFileDir$ + + + diff --git a/.idea/.idea.biergarten/.idea/data_source_mapping.xml b/.idea/.idea.biergarten/.idea/data_source_mapping.xml new file mode 100644 index 0000000..3edede8 --- /dev/null +++ b/.idea/.idea.biergarten/.idea/data_source_mapping.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/.idea/.idea.biergarten/.idea/indexLayout.xml b/.idea/.idea.biergarten/.idea/indexLayout.xml new file mode 100644 index 0000000..67b8dc9 --- /dev/null +++ b/.idea/.idea.biergarten/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.idea/.idea.biergarten/.idea/sqldialects.xml b/.idea/.idea.biergarten/.idea/sqldialects.xml new file mode 100644 index 0000000..fb7c0dd --- /dev/null +++ b/.idea/.idea.biergarten/.idea/sqldialects.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.biergarten/.idea/vcs.xml b/.idea/.idea.biergarten/.idea/vcs.xml new file mode 100644 index 0000000..dcb6b8c --- /dev/null +++ b/.idea/.idea.biergarten/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/DALTests/UserAccountRepositoryTests.cs b/DALTests/UserAccountRepositoryTests.cs index 825cea8..d02d34f 100644 --- a/DALTests/UserAccountRepositoryTests.cs +++ b/DALTests/UserAccountRepositoryTests.cs @@ -19,7 +19,6 @@ namespace DALTests [Fact] public void Add_ShouldInsertUserAccount() { - // Arrange var userAccount = new UserAccount { @@ -186,7 +185,7 @@ namespace DALTests Email = $"pageuser_{Guid.NewGuid():N}@example.com", CreatedAt = DateTime.UtcNow, DateOfBirth = new DateTime(1993, 6, 6), - } + }, }; foreach (var user in users) @@ -204,11 +203,11 @@ namespace DALTests [Fact] public void GetAll_WithPagination_ShouldValidateArguments() { - Assert.Throws( - () => _repository.GetAll(0, 0).ToList() + Assert.Throws(() => + _repository.GetAll(0, 0).ToList() ); - Assert.Throws( - () => _repository.GetAll(1, -1).ToList() + Assert.Throws(() => + _repository.GetAll(1, -1).ToList() ); } } diff --git a/DBSeed/DBSeed.csproj b/DBSeed/DBSeed.csproj new file mode 100644 index 0000000..958ec61 --- /dev/null +++ b/DBSeed/DBSeed.csproj @@ -0,0 +1,17 @@ + + + Exe + net10.0 + enable + enable + + + + + + + + diff --git a/DBSeed/LocationSeeder.cs b/DBSeed/LocationSeeder.cs new file mode 100644 index 0000000..8c825ac --- /dev/null +++ b/DBSeed/LocationSeeder.cs @@ -0,0 +1,330 @@ +using System.Data; +using Microsoft.Data.SqlClient; + +namespace DBSeed; + +internal static class LocationSeeder +{ + private static readonly IReadOnlyList<( + string CountryName, + string CountryCode + )> Countries = + [ + ("Canada", "CA"), + ("Mexico", "MX"), + ("United States", "US"), + ]; + + private static readonly IReadOnlyList<( + string StateProvinceName, + string StateProvinceCode, + string CountryCode + )> States = + [ + ("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"), + ]; + + private static readonly IReadOnlyList<( + string StateProvinceCode, + string CityName + )> Cities = + [ + ("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"), + ]; + + internal static async Task SeedAsync(SqlConnection connection) + { + foreach (var (countryName, countryCode) in Countries) + { + await CreateCountryAsync(connection, countryName, countryCode); + } + + foreach ( + var (stateProvinceName, stateProvinceCode, countryCode) in States + ) + { + await CreateStateProvinceAsync( + connection, + stateProvinceName, + stateProvinceCode, + countryCode + ); + } + + foreach (var (stateProvinceCode, cityName) in Cities) + { + await CreateCityAsync(connection, cityName, stateProvinceCode); + } + } + + private static async Task CreateCountryAsync( + SqlConnection connection, + string countryName, + string countryCode + ) + { + await using var command = new SqlCommand( + "dbo.USP_CreateCountry", + connection + ); + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue("@CountryName", countryName); + command.Parameters.AddWithValue("@ISO3616_1", countryCode); + + await command.ExecuteNonQueryAsync(); + } + + private static async Task CreateStateProvinceAsync( + SqlConnection connection, + string stateProvinceName, + string stateProvinceCode, + string countryCode + ) + { + await using var command = new SqlCommand( + "dbo.USP_CreateStateProvince", + connection + ); + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue( + "@StateProvinceName", + stateProvinceName + ); + command.Parameters.AddWithValue("@ISO3616_2", stateProvinceCode); + command.Parameters.AddWithValue("@CountryCode", countryCode); + + await command.ExecuteNonQueryAsync(); + } + + private static async Task CreateCityAsync( + SqlConnection connection, + string cityName, + string stateProvinceCode + ) + { + await using var command = new SqlCommand( + "dbo.USP_CreateCity", + connection + ); + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue("@CityName", cityName); + command.Parameters.AddWithValue( + "@StateProvinceCode", + stateProvinceCode + ); + + await command.ExecuteNonQueryAsync(); + } +} diff --git a/DBSeed/Program.cs b/DBSeed/Program.cs new file mode 100644 index 0000000..59e3931 --- /dev/null +++ b/DBSeed/Program.cs @@ -0,0 +1,33 @@ +using DBSeed; +using Microsoft.Data.SqlClient; + +try +{ + var connectionString = Environment.GetEnvironmentVariable( + "DB_CONNECTION_STRING" + ); + if (string.IsNullOrWhiteSpace(connectionString)) + throw new InvalidOperationException( + "Environment variable DB_CONNECTION_STRING is not set or is empty." + ); + + await using var connection = new SqlConnection(connectionString); + await connection.OpenAsync(); + + Console.WriteLine("Connected to database."); + + await LocationSeeder.SeedAsync(connection); + Console.WriteLine("Seeded locations."); + + await UserSeeder.SeedAsync(connection); + Console.WriteLine("Seeded users."); + + Console.WriteLine("Seed completed successfully."); + return 0; +} +catch (Exception ex) +{ + Console.Error.WriteLine("Seed failed:"); + Console.Error.WriteLine(ex); + return 1; +} diff --git a/DBSeed/UserSeeder.cs b/DBSeed/UserSeeder.cs new file mode 100644 index 0000000..26ca3fd --- /dev/null +++ b/DBSeed/UserSeeder.cs @@ -0,0 +1,334 @@ +using System.Data; +using System.Security.Cryptography; +using System.Text; +using idunno.Password; +using Konscious.Security.Cryptography; +using Microsoft.Data.SqlClient; + +namespace DBSeed; + +internal static class UserSeeder +{ + 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"), + ]; + + public static async Task SeedAsync(SqlConnection connection) + { + var generator = new PasswordGenerator(); + var random = new Random(); + int createdUsers = 0; + int createdCredentials = 0; + int createdVerifications = 0; + + foreach (var (firstName, lastName) in SeedNames) + { + string username = BuildUsername(firstName, lastName); + string email = BuildEmail(firstName, lastName); + Guid? existingId = + await GetUserAccountIdByUsernameAsync(connection, username) + ?? await GetUserAccountIdByEmailAsync(connection, email); + + Guid userAccountId; + if (existingId.HasValue) + { + userAccountId = existingId.Value; + } + else + { + userAccountId = Guid.NewGuid(); + DateTime dateOfBirth = GenerateDateOfBirth(random); + await CreateUserAccountAsync( + connection, + userAccountId, + username, + firstName, + lastName, + email, + dateOfBirth + ); + createdUsers++; + } + + if (!await HasUserCredentialAsync(connection, userAccountId)) + { + string pwd = generator.Generate( + length: 64, + numberOfDigits: 10, + numberOfSymbols: 10 + ); + string hash = GeneratePasswordHash(pwd); + await AddUserCredentialAsync(connection, userAccountId, hash); + createdCredentials++; + } + + if (!await HasUserVerificationAsync(connection, userAccountId)) + { + await AddUserVerificationAsync(connection, userAccountId); + createdVerifications++; + } + } + + Console.WriteLine($"Created {createdUsers} user accounts."); + Console.WriteLine($"Added {createdCredentials} user credentials."); + Console.WriteLine($"Added {createdVerifications} user verifications."); + } + + private static string GeneratePasswordHash(string pwd) + { + byte[] salt = RandomNumberGenerator.GetBytes(16); + + var argon2 = new Argon2id(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)}"; + } + + private static async Task GetUserAccountIdByUsernameAsync( + SqlConnection connection, + string username + ) + { + await using var command = new SqlCommand( + "usp_GetUserAccountByUsername", + connection + ); + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue("@Username", username); + + await using var reader = await command.ExecuteReaderAsync(); + return await reader.ReadAsync() ? reader.GetGuid(0) : null; + } + + private static async Task GetUserAccountIdByEmailAsync( + SqlConnection connection, + string email + ) + { + await using var command = new SqlCommand( + "usp_GetUserAccountByEmail", + connection + ); + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue("@Email", email); + + await using var reader = await command.ExecuteReaderAsync(); + return await reader.ReadAsync() ? reader.GetGuid(0) : null; + } + + private static async Task CreateUserAccountAsync( + SqlConnection connection, + Guid userAccountId, + string username, + string firstName, + string lastName, + string email, + DateTime dateOfBirth + ) + { + await using var command = new SqlCommand( + "usp_CreateUserAccount", + connection + ); + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue("@UserAccountId", userAccountId); + command.Parameters.AddWithValue("@Username", username); + command.Parameters.AddWithValue("@FirstName", firstName); + command.Parameters.AddWithValue("@LastName", lastName); + command.Parameters.AddWithValue("@DateOfBirth", dateOfBirth); + command.Parameters.AddWithValue("@Email", email); + + await command.ExecuteNonQueryAsync(); + } + + private static async Task HasUserCredentialAsync( + SqlConnection connection, + Guid userAccountId + ) + { + const string sql = """ +SELECT 1 +FROM dbo.UserCredential +WHERE UserAccountId = @UserAccountId; +"""; + await using var command = new SqlCommand(sql, connection); + command.Parameters.AddWithValue("@UserAccountId", userAccountId); + object? result = await command.ExecuteScalarAsync(); + return result is not null; + } + + private static async Task AddUserCredentialAsync( + SqlConnection connection, + Guid userAccountId, + string hash + ) + { + await using var command = new SqlCommand( + "dbo.USP_AddUserCredential", + connection + ); + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue("@UserAccountId", userAccountId); + command.Parameters.AddWithValue("@Hash", hash); + + await command.ExecuteNonQueryAsync(); + } + + private static async Task HasUserVerificationAsync( + SqlConnection connection, + Guid userAccountId + ) + { + const string sql = """ +SELECT 1 +FROM dbo.UserVerification +WHERE UserAccountId = @UserAccountId; +"""; + await using var command = new SqlCommand(sql, connection); + command.Parameters.AddWithValue("@UserAccountId", userAccountId); + object? result = await command.ExecuteScalarAsync(); + return result is not null; + } + + private static async Task AddUserVerificationAsync( + SqlConnection connection, + Guid userAccountId + ) + { + await using var command = new SqlCommand( + "dbo.USP_CreateUserVerification", + connection + ); + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue("@UserAccountID", userAccountId); + + await command.ExecuteNonQueryAsync(); + } + + private static string BuildUsername(string firstName, string lastName) + { + string username = $"{firstName}.{lastName}".ToLowerInvariant(); + return username.Length <= 64 ? username : username[..64]; + } + + private static string BuildEmail(string firstName, string lastName) + { + string email = $"{firstName}.{lastName}@example.com".ToLowerInvariant(); + return email.Length <= 128 ? email : email[..128]; + } + + 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/DataAccessLayer/IRepository.cs b/DataAccessLayer/IRepository.cs index a72f5f7..5aee600 100644 --- a/DataAccessLayer/IRepository.cs +++ b/DataAccessLayer/IRepository.cs @@ -6,11 +6,10 @@ namespace DataAccessLayer public interface IRepository where T : class { - void Add(T entity); IEnumerable GetAll(int? limit, int? offset); - + T? GetById(Guid id); void Update(T entity); void Delete(Guid id); diff --git a/DataAccessLayer/Repositories/UserAccountRepository.cs b/DataAccessLayer/Repositories/UserAccountRepository.cs index 923d4ee..4790f3e 100644 --- a/DataAccessLayer/Repositories/UserAccountRepository.cs +++ b/DataAccessLayer/Repositories/UserAccountRepository.cs @@ -8,6 +8,7 @@ namespace DataAccessLayer public class UserAccountRepository : IUserAccountRepository { private readonly string _connectionString; + public UserAccountRepository() { // Retrieve the connection string from environment variables @@ -56,10 +57,7 @@ namespace DataAccessLayer public void Delete(Guid id) { using SqlConnection connection = new(_connectionString); - using SqlCommand command = new( - "usp_DeleteUserAccount", - connection - ); + using SqlCommand command = new("usp_DeleteUserAccount", connection); command.CommandType = System.Data.CommandType.StoredProcedure; command.Parameters.AddWithValue("@UserAccountId", id); connection.Open(); @@ -158,10 +156,16 @@ namespace DataAccessLayer userAccount.UserAccountID ); command.Parameters.AddWithValue("@Username", userAccount.Username); - command.Parameters.AddWithValue("@FirstName", userAccount.FirstName); + command.Parameters.AddWithValue( + "@FirstName", + userAccount.FirstName + ); command.Parameters.AddWithValue("@LastName", userAccount.LastName); command.Parameters.AddWithValue("@Email", userAccount.Email); - command.Parameters.AddWithValue("@DateOfBirth", userAccount.DateOfBirth); + command.Parameters.AddWithValue( + "@DateOfBirth", + userAccount.DateOfBirth + ); } private static void AddUserAccountUpdateParameters( diff --git a/DataLayer/DataLayer.csproj b/DataLayer/DataLayer.csproj index e9ded85..b24604f 100644 --- a/DataLayer/DataLayer.csproj +++ b/DataLayer/DataLayer.csproj @@ -5,14 +5,17 @@ enable enable - + - + - \ No newline at end of file + diff --git a/DataLayer/Program.cs b/DataLayer/Program.cs index 652200e..d3405e9 100644 --- a/DataLayer/Program.cs +++ b/DataLayer/Program.cs @@ -1,157 +1,32 @@ -using System.Data; +// Get connection string from environment variable + using System.Reflection; -using System.Security.Cryptography; -using System.Text; using DbUp; -using idunno.Password; -using Konscious.Security.Cryptography; -using Microsoft.Data.SqlClient; -/// -/// Executes USP_AddUserCredentials to add missing user credentials using a table-valued parameter -/// consisting of the user account id and a generated Argon2 hash. -/// -/// An open SQL connection. -/// A table-valued parameter payload containing user IDs and hashes. -static async Task ExecuteCredentialProcedureAsync(SqlConnection connection, DataTable credentialTable) -{ - await using var command = new SqlCommand("dbo.USP_AddUserCredentials", connection) - { - CommandType = CommandType.StoredProcedure - }; - - // Must match your stored proc parameter name: - var tvpParameter = command.Parameters.Add("@Hash", SqlDbType.Structured); - tvpParameter.TypeName = "dbo.TblUserHashes"; - tvpParameter.Value = credentialTable; - - await command.ExecuteNonQueryAsync(); -} - -/// -/// Builds a DataTable of user account IDs and generated Argon2 password hashes for users that do not yet -/// have credentials. -/// -/// An open SQL connection. -/// A DataTable matching dbo.TblUserHashes with user IDs and hashes. -static async Task BuildCredentialTableAsync(SqlConnection connection) -{ - const string sql = """ -SELECT ua.UserAccountID -FROM dbo.UserAccount AS ua -WHERE NOT EXISTS ( - SELECT 1 - FROM dbo.UserCredential AS uc - WHERE uc.UserAccountID = ua.UserAccountID +var connectionString = Environment.GetEnvironmentVariable( + "DB_CONNECTION_STRING" ); -"""; - await using var command = new SqlCommand(sql, connection); - await using var reader = await command.ExecuteReaderAsync(); +var upgrader = DeployChanges + .To.SqlDatabase(connectionString) + .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly()) + .LogToConsole() + .Build(); - // IMPORTANT: column names/types/order should match dbo.TblUserHashes - var table = new DataTable(); - table.Columns.Add("UserAccountID", typeof(Guid)); - table.Columns.Add("Hash", typeof(string)); +var result = upgrader.PerformUpgrade(); - var generator = new PasswordGenerator(); - - while (await reader.ReadAsync()) - { - Guid userId = reader.GetGuid(0); - - // idunno.Password PasswordGenerator signature: - // Generate(length, numberOfDigits, numberOfSymbols, noUpper, allowRepeat) - string pwd = generator.Generate( - length: 64, - numberOfDigits: 10, - numberOfSymbols: 10 - ); - - string hash = GeneratePasswordHash(pwd); - - var row = table.NewRow(); - row["UserAccountID"] = userId; - row["Hash"] = hash; - table.Rows.Add(row); - } - - return table; -} - -/// -/// Generates an Argon2id hash for the given password. -/// -/// The plaintext password. -/// A string in the format "base64(salt):base64(hash)". -static string GeneratePasswordHash(string pwd) +if (!result.Successful) { - byte[] salt = RandomNumberGenerator.GetBytes(16); - - var argon2 = new Argon2id(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)}"; -} - -/// -/// Runs the seed process to add test users and generate missing credentials. -/// -/// An open SQL connection. -static async Task RunSeedAsync(SqlConnection connection) -{ - //run add test users - await using var insertCommand = new SqlCommand("dbo.USP_SeedTestUsers", connection) - { - CommandType = CommandType.StoredProcedure - }; - await insertCommand.ExecuteNonQueryAsync(); - - Console.WriteLine("Inserted or refreshed test users."); - - DataTable credentialRows = await BuildCredentialTableAsync(connection); - if (credentialRows.Rows.Count == 0) - { - Console.WriteLine("No new credentials required."); - return; - } - - await ExecuteCredentialProcedureAsync(connection, credentialRows); - Console.WriteLine($"Generated {credentialRows.Rows.Count} credential hashes."); -} - - - -// Get connection string from environment variable - var connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); - - var upgrader = - DeployChanges.To - .SqlDatabase(connectionString) - .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly()) - .LogToConsole() - .Build(); - - var result = upgrader.PerformUpgrade(); - - if (!result.Successful) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(result.Error); - Console.ResetColor(); -#if DEBUG - Console.ReadLine(); -#endif - return -1; - } - - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("Success!"); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(result.Error); Console.ResetColor(); - return 0; +#if DEBUG + Console.ReadLine(); +#endif + return -1; +} + +Console.ForegroundColor = ConsoleColor.Green; +Console.WriteLine("Success!"); +Console.ResetColor(); +return 0; diff --git a/DataLayer/scripts/01-schema/schema.sql b/DataLayer/scripts/01-schema/schema.sql index e04a681..637832e 100644 --- a/DataLayer/scripts/01-schema/schema.sql +++ b/DataLayer/scripts/01-schema/schema.sql @@ -14,6 +14,7 @@ DROP DATABASE IF EXISTS Biergarten; CREATE DATABASE Biergarten; +USE Biergarten; */ -- ---------------------------------------------------------------------------- -- ---------------------------------------------------------------------------- diff --git a/DataLayer/scripts/seed/functions/UDF_GetCountryIdByCode.sql b/DataLayer/scripts/02-functions/UDF_GetCountryIdByCode.sql similarity index 100% rename from DataLayer/scripts/seed/functions/UDF_GetCountryIdByCode.sql rename to DataLayer/scripts/02-functions/UDF_GetCountryIdByCode.sql diff --git a/DataLayer/scripts/seed/functions/UDF_GetStateProvinceIdByCode.sql b/DataLayer/scripts/02-functions/UDF_GetStateProvinceIdByCode.sql similarity index 100% rename from DataLayer/scripts/seed/functions/UDF_GetStateProvinceIdByCode.sql rename to DataLayer/scripts/02-functions/UDF_GetStateProvinceIdByCode.sql diff --git a/DataLayer/scripts/crud/USP_CreateUserAccount.sql b/DataLayer/scripts/03-crud/01-UserAccount/USP_CreateUserAccount.sql similarity index 100% rename from DataLayer/scripts/crud/USP_CreateUserAccount.sql rename to DataLayer/scripts/03-crud/01-UserAccount/USP_CreateUserAccount.sql diff --git a/DataLayer/scripts/crud/USP_DeleteUserAccount.sql b/DataLayer/scripts/03-crud/01-UserAccount/USP_DeleteUserAccount.sql similarity index 100% rename from DataLayer/scripts/crud/USP_DeleteUserAccount.sql rename to DataLayer/scripts/03-crud/01-UserAccount/USP_DeleteUserAccount.sql diff --git a/DataLayer/scripts/crud/USP_GetAllUserAccounts.sql b/DataLayer/scripts/03-crud/01-UserAccount/USP_GetAllUserAccounts.sql similarity index 100% rename from DataLayer/scripts/crud/USP_GetAllUserAccounts.sql rename to DataLayer/scripts/03-crud/01-UserAccount/USP_GetAllUserAccounts.sql diff --git a/DataLayer/scripts/crud/USP_GetUserAccountByEmail.sql b/DataLayer/scripts/03-crud/01-UserAccount/USP_GetUserAccountByEmail.sql similarity index 100% rename from DataLayer/scripts/crud/USP_GetUserAccountByEmail.sql rename to DataLayer/scripts/03-crud/01-UserAccount/USP_GetUserAccountByEmail.sql diff --git a/DataLayer/scripts/crud/USP_GetUserAccountById.sql b/DataLayer/scripts/03-crud/01-UserAccount/USP_GetUserAccountById.sql similarity index 100% rename from DataLayer/scripts/crud/USP_GetUserAccountById.sql rename to DataLayer/scripts/03-crud/01-UserAccount/USP_GetUserAccountById.sql diff --git a/DataLayer/scripts/crud/USP_GetUserAccountByUsername.sql b/DataLayer/scripts/03-crud/01-UserAccount/USP_GetUserAccountByUsername.sql similarity index 100% rename from DataLayer/scripts/crud/USP_GetUserAccountByUsername.sql rename to DataLayer/scripts/03-crud/01-UserAccount/USP_GetUserAccountByUsername.sql diff --git a/DataLayer/scripts/crud/USP_UpdateUserAccount.sql b/DataLayer/scripts/03-crud/01-UserAccount/USP_UpdateUserAccount.sql similarity index 100% rename from DataLayer/scripts/crud/USP_UpdateUserAccount.sql rename to DataLayer/scripts/03-crud/01-UserAccount/USP_UpdateUserAccount.sql diff --git a/DataLayer/scripts/03-crud/02-UserCredential/USP_AddUserCredential.sql b/DataLayer/scripts/03-crud/02-UserCredential/USP_AddUserCredential.sql new file mode 100644 index 0000000..99077b8 --- /dev/null +++ b/DataLayer/scripts/03-crud/02-UserCredential/USP_AddUserCredential.sql @@ -0,0 +1,18 @@ +CREATE OR ALTER PROCEDURE dbo.USP_AddUserCredential( + @UserAccountId uniqueidentifier, + @Hash nvarchar(max) +) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + BEGIN TRANSACTION; + + INSERT INTO dbo.UserCredential + (UserAccountId, Hash) + VALUES + (@UserAccountId, @Hash); + + COMMIT TRANSACTION; +END; \ No newline at end of file diff --git a/DataLayer/scripts/03-crud/03-UserVerification/USP_AddUserVerification.sql b/DataLayer/scripts/03-crud/03-UserVerification/USP_AddUserVerification.sql new file mode 100644 index 0000000..d556fa2 --- /dev/null +++ b/DataLayer/scripts/03-crud/03-UserVerification/USP_AddUserVerification.sql @@ -0,0 +1,20 @@ +CREATE OR ALTER PROCEDURE dbo.USP_CreateUserVerification + @UserAccountID uniqueidentifier, + @VerificationDateTime datetime = NULL +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + IF @VerificationDateTime IS NULL + SET @VerificationDateTime = GETDATE(); + + BEGIN TRANSACTION; + + INSERT INTO dbo.UserVerification + (UserAccountId, VerificationDateTime) + VALUES + (@UserAccountID, @VerificationDateTime); + + COMMIT TRANSACTION; +END diff --git a/DataLayer/scripts/03-crud/04-Location/USP_CreateCity.sql b/DataLayer/scripts/03-crud/04-Location/USP_CreateCity.sql new file mode 100644 index 0000000..bae29a1 --- /dev/null +++ b/DataLayer/scripts/03-crud/04-Location/USP_CreateCity.sql @@ -0,0 +1,30 @@ +CREATE OR ALTER PROCEDURE dbo.USP_CreateCity +( + @CityName NVARCHAR(100), + @StateProvinceCode NVARCHAR(6) +) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + DECLARE @StateProvinceId UNIQUEIDENTIFIER = dbo.UDF_GetStateProvinceIdByCode(@StateProvinceCode); + IF @StateProvinceId IS NULL + BEGIN + RAISERROR('State/province not found for code.', 16, 1); + RETURN; + END + + IF EXISTS ( + SELECT 1 + FROM dbo.City + WHERE CityName = @CityName + AND StateProvinceID = @StateProvinceId + ) + RETURN; + + INSERT INTO dbo.City + (StateProvinceID, CityName) + VALUES + (@StateProvinceId, @CityName); +END; diff --git a/DataLayer/scripts/03-crud/04-Location/USP_CreateCountry.sql b/DataLayer/scripts/03-crud/04-Location/USP_CreateCountry.sql new file mode 100644 index 0000000..e6e79e1 --- /dev/null +++ b/DataLayer/scripts/03-crud/04-Location/USP_CreateCountry.sql @@ -0,0 +1,22 @@ +CREATE OR ALTER PROCEDURE dbo.USP_CreateCountry +( + @CountryName NVARCHAR(100), + @ISO3616_1 NVARCHAR(2) +) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + IF EXISTS ( + SELECT 1 + FROM dbo.Country + WHERE ISO3616_1 = @ISO3616_1 + ) + RETURN; + + INSERT INTO dbo.Country + (CountryName, ISO3616_1) + VALUES + (@CountryName, @ISO3616_1); +END; diff --git a/DataLayer/scripts/03-crud/04-Location/USP_CreateStateProvince.sql b/DataLayer/scripts/03-crud/04-Location/USP_CreateStateProvince.sql new file mode 100644 index 0000000..7caca90 --- /dev/null +++ b/DataLayer/scripts/03-crud/04-Location/USP_CreateStateProvince.sql @@ -0,0 +1,30 @@ +CREATE OR ALTER PROCEDURE dbo.USP_CreateStateProvince +( + @StateProvinceName NVARCHAR(100), + @ISO3616_2 NVARCHAR(6), + @CountryCode NVARCHAR(2) +) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + IF EXISTS ( + SELECT 1 + FROM dbo.StateProvince + WHERE ISO3616_2 = @ISO3616_2 + ) + RETURN; + + DECLARE @CountryId UNIQUEIDENTIFIER = dbo.UDF_GetCountryIdByCode(@CountryCode); + IF @CountryId IS NULL + BEGIN + RAISERROR('Country not found for code.', 16, 1); + RETURN; + END + + INSERT INTO dbo.StateProvince + (StateProvinceName, ISO3616_2, CountryID) + VALUES + (@StateProvinceName, @ISO3616_2, @CountryId); +END; diff --git a/DataLayer/scripts/seed/01-types/TblUserHashes.sql b/DataLayer/scripts/seed/01-types/TblUserHashes.sql deleted file mode 100644 index c26b6b1..0000000 --- a/DataLayer/scripts/seed/01-types/TblUserHashes.sql +++ /dev/null @@ -1,6 +0,0 @@ - -CREATE TYPE dbo.TblUserHashes AS TABLE - ( - UserAccountId UNIQUEIDENTIFIER NOT NULL, - Hash NVARCHAR(MAX) NOT NULL - ); \ No newline at end of file diff --git a/DataLayer/scripts/seed/procedures/USP_AddLocations.sql b/DataLayer/scripts/seed/procedures/USP_AddLocations.sql deleted file mode 100644 index abc068b..0000000 --- a/DataLayer/scripts/seed/procedures/USP_AddLocations.sql +++ /dev/null @@ -1,500 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_AddLocations -AS -BEGIN - SET NOCOUNT ON; - SET XACT_ABORT ON; - - BEGIN TRANSACTION; - -- Countries (alpha-2) - WITH - Countries(CountryName, Alpha2) - AS - ( - SELECT 'Canada', 'CA' - UNION ALL - SELECT 'Mexico', 'MX' - UNION ALL - SELECT 'United States', 'US' - ) - INSERT INTO dbo.Country - (CountryName, ISO3616_1) - SELECT c.CountryName, c.Alpha2 - FROM Countries AS c - WHERE NOT EXISTS (SELECT 1 - FROM dbo.Country AS x - WHERE x.ISO3616_1 = c.Alpha2 - ); - - WITH - Regions(StateProvinceName, ISO2, CountryAlpha2) - AS - ( - -- United States (50 + DC + territories) - SELECT 'Alabama', 'US-AL', 'US' - UNION ALL - SELECT 'Alaska', 'US-AK', 'US' - UNION ALL - SELECT 'Arizona', 'US-AZ', 'US' - UNION ALL - SELECT 'Arkansas', 'US-AR', 'US' - UNION ALL - SELECT 'California', 'US-CA', 'US' - UNION ALL - SELECT 'Colorado', 'US-CO', 'US' - UNION ALL - SELECT 'Connecticut', 'US-CT', 'US' - UNION ALL - SELECT 'Delaware', 'US-DE', 'US' - UNION ALL - SELECT 'Florida', 'US-FL', 'US' - UNION ALL - SELECT 'Georgia', 'US-GA', 'US' - UNION ALL - SELECT 'Hawaii', 'US-HI', 'US' - UNION ALL - SELECT 'Idaho', 'US-ID', 'US' - UNION ALL - SELECT 'Illinois', 'US-IL', 'US' - UNION ALL - SELECT 'Indiana', 'US-IN', 'US' - UNION ALL - SELECT 'Iowa', 'US-IA', 'US' - UNION ALL - SELECT 'Kansas', 'US-KS', 'US' - UNION ALL - SELECT 'Kentucky', 'US-KY', 'US' - UNION ALL - SELECT 'Louisiana', 'US-LA', 'US' - UNION ALL - SELECT 'Maine', 'US-ME', 'US' - UNION ALL - SELECT 'Maryland', 'US-MD', 'US' - UNION ALL - SELECT 'Massachusetts', 'US-MA', 'US' - UNION ALL - SELECT 'Michigan', 'US-MI', 'US' - UNION ALL - SELECT 'Minnesota', 'US-MN', 'US' - UNION ALL - SELECT 'Mississippi', 'US-MS', 'US' - UNION ALL - SELECT 'Missouri', 'US-MO', 'US' - UNION ALL - SELECT 'Montana', 'US-MT', 'US' - UNION ALL - SELECT 'Nebraska', 'US-NE', 'US' - UNION ALL - SELECT 'Nevada', 'US-NV', 'US' - UNION ALL - SELECT 'New Hampshire', 'US-NH', 'US' - UNION ALL - SELECT 'New Jersey', 'US-NJ', 'US' - UNION ALL - SELECT 'New Mexico', 'US-NM', 'US' - UNION ALL - SELECT 'New York', 'US-NY', 'US' - UNION ALL - SELECT 'North Carolina', 'US-NC', 'US' - UNION ALL - SELECT 'North Dakota', 'US-ND', 'US' - UNION ALL - SELECT 'Ohio', 'US-OH', 'US' - UNION ALL - SELECT 'Oklahoma', 'US-OK', 'US' - UNION ALL - SELECT 'Oregon', 'US-OR', 'US' - UNION ALL - SELECT 'Pennsylvania', 'US-PA', 'US' - UNION ALL - SELECT 'Rhode Island', 'US-RI', 'US' - UNION ALL - SELECT 'South Carolina', 'US-SC', 'US' - UNION ALL - SELECT 'South Dakota', 'US-SD', 'US' - UNION ALL - SELECT 'Tennessee', 'US-TN', 'US' - UNION ALL - SELECT 'Texas', 'US-TX', 'US' - UNION ALL - SELECT 'Utah', 'US-UT', 'US' - UNION ALL - SELECT 'Vermont', 'US-VT', 'US' - UNION ALL - SELECT 'Virginia', 'US-VA', 'US' - UNION ALL - SELECT 'Washington', 'US-WA', 'US' - UNION ALL - SELECT 'West Virginia', 'US-WV', 'US' - UNION ALL - SELECT 'Wisconsin', 'US-WI', 'US' - UNION ALL - SELECT 'Wyoming', 'US-WY', 'US' - UNION ALL - SELECT 'District of Columbia', 'US-DC', 'US' - UNION ALL - SELECT 'Puerto Rico', 'US-PR', 'US' - UNION ALL - SELECT 'U.S. Virgin Islands', 'US-VI', 'US' - UNION ALL - SELECT 'Guam', 'US-GU', 'US' - UNION ALL - SELECT 'Northern Mariana Islands', 'US-MP', 'US' - UNION ALL - SELECT 'American Samoa', 'US-AS', 'US' - - -- Canada (10 provinces + 3 territories) - UNION ALL - SELECT 'Ontario', 'CA-ON', 'CA' - UNION ALL - SELECT N'Québec', 'CA-QC', 'CA' - UNION ALL - SELECT 'Nova Scotia', 'CA-NS', 'CA' - UNION ALL - SELECT 'New Brunswick', 'CA-NB', 'CA' - UNION ALL - SELECT 'Manitoba', 'CA-MB', 'CA' - UNION ALL - SELECT 'British Columbia', 'CA-BC', 'CA' - UNION ALL - SELECT 'Prince Edward Island', 'CA-PE', 'CA' - UNION ALL - SELECT 'Saskatchewan', 'CA-SK', 'CA' - UNION ALL - SELECT 'Alberta', 'CA-AB', 'CA' - UNION ALL - SELECT 'Newfoundland and Labrador', 'CA-NL', 'CA' - UNION ALL - SELECT 'Northwest Territories', 'CA-NT', 'CA' - UNION ALL - SELECT 'Yukon', 'CA-YT', 'CA' - UNION ALL - SELECT 'Nunavut', 'CA-NU', 'CA' - - -- Mexico (32 states incl. CDMX) - UNION ALL - SELECT 'Aguascalientes', 'MX-AGU', 'MX' - UNION ALL - SELECT 'Baja California', 'MX-BCN', 'MX' - UNION ALL - SELECT 'Baja California Sur', 'MX-BCS', 'MX' - UNION ALL - SELECT 'Campeche', 'MX-CAM', 'MX' - UNION ALL - SELECT 'Chiapas', 'MX-CHP', 'MX' - UNION ALL - SELECT 'Chihuahua', 'MX-CHH', 'MX' - UNION ALL - SELECT 'Coahuila de Zaragoza', 'MX-COA', 'MX' - UNION ALL - SELECT 'Colima', 'MX-COL', 'MX' - UNION ALL - SELECT 'Durango', 'MX-DUR', 'MX' - UNION ALL - SELECT 'Guanajuato', 'MX-GUA', 'MX' - UNION ALL - SELECT 'Guerrero', 'MX-GRO', 'MX' - UNION ALL - SELECT 'Hidalgo', 'MX-HID', 'MX' - UNION ALL - SELECT 'Jalisco', 'MX-JAL', 'MX' - UNION ALL - SELECT N'México State', 'MX-MEX', 'MX' - UNION ALL - SELECT N'Michoacán de Ocampo', 'MX-MIC', 'MX' - UNION ALL - SELECT 'Morelos', 'MX-MOR', 'MX' - UNION ALL - SELECT 'Nayarit', 'MX-NAY', 'MX' - UNION ALL - SELECT N'Nuevo León', 'MX-NLE', 'MX' - UNION ALL - SELECT 'Oaxaca', 'MX-OAX', 'MX' - UNION ALL - SELECT 'Puebla', 'MX-PUE', 'MX' - UNION ALL - SELECT N'Querétaro', 'MX-QUE', 'MX' - UNION ALL - SELECT 'Quintana Roo', 'MX-ROO', 'MX' - UNION ALL - SELECT N'San Luis Potosí', 'MX-SLP', 'MX' - UNION ALL - SELECT 'Sinaloa', 'MX-SIN', 'MX' - UNION ALL - SELECT 'Sonora', 'MX-SON', 'MX' - UNION ALL - SELECT 'Tabasco', 'MX-TAB', 'MX' - UNION ALL - SELECT 'Tamaulipas', 'MX-TAM', 'MX' - UNION ALL - SELECT 'Tlaxcala', 'MX-TLA', 'MX' - UNION ALL - SELECT 'Veracruz de Ignacio de la Llave', 'MX-VER', 'MX' - UNION ALL - SELECT N'Yucatán', 'MX-YUC', 'MX' - UNION ALL - SELECT 'Zacatecas', 'MX-ZAC', 'MX' - UNION ALL - SELECT N'Ciudad de México', 'MX-CMX', 'MX' - ) - INSERT INTO dbo.StateProvince - (StateProvinceName, ISO3616_2, CountryID) - SELECT - r.StateProvinceName, - r.ISO2, - dbo.UDF_GetCountryIdByCode(r.CountryAlpha2) - FROM Regions AS r - WHERE NOT EXISTS ( - SELECT 1 - FROM dbo.StateProvince AS sp - WHERE sp.ISO3616_2 = r.ISO2 - ); - - WITH - Cities(StateProvinceISO2, CityName) - AS - ( - -- USA - SELECT 'US-CA', 'Los Angeles' - UNION ALL - SELECT 'US-CA', 'San Diego' - UNION ALL - SELECT 'US-CA', 'San Francisco' - UNION ALL - SELECT 'US-CA', 'Sacramento' - UNION ALL - SELECT 'US-TX', 'Houston' - UNION ALL - SELECT 'US-TX', 'Dallas' - UNION ALL - SELECT 'US-TX', 'Austin' - UNION ALL - SELECT 'US-TX', 'San Antonio' - UNION ALL - SELECT 'US-FL', 'Miami' - UNION ALL - SELECT 'US-FL', 'Orlando' - UNION ALL - SELECT 'US-FL', 'Tampa' - UNION ALL - SELECT 'US-NY', 'New York' - UNION ALL - SELECT 'US-NY', 'Buffalo' - UNION ALL - SELECT 'US-NY', 'Rochester' - UNION ALL - SELECT 'US-IL', 'Chicago' - UNION ALL - SELECT 'US-IL', 'Springfield' - UNION ALL - SELECT 'US-PA', 'Philadelphia' - UNION ALL - SELECT 'US-PA', 'Pittsburgh' - UNION ALL - SELECT 'US-AZ', 'Phoenix' - UNION ALL - SELECT 'US-AZ', 'Tucson' - UNION ALL - SELECT 'US-CO', 'Denver' - UNION ALL - SELECT 'US-CO', 'Colorado Springs' - UNION ALL - SELECT 'US-MA', 'Boston' - UNION ALL - SELECT 'US-MA', 'Worcester' - UNION ALL - SELECT 'US-WA', 'Seattle' - UNION ALL - SELECT 'US-WA', 'Spokane' - UNION ALL - SELECT 'US-GA', 'Atlanta' - UNION ALL - SELECT 'US-GA', 'Savannah' - UNION ALL - SELECT 'US-NV', 'Las Vegas' - UNION ALL - SELECT 'US-NV', 'Reno' - UNION ALL - SELECT 'US-MI', 'Detroit' - UNION ALL - SELECT 'US-MI', 'Grand Rapids' - UNION ALL - SELECT 'US-MN', 'Minneapolis' - UNION ALL - SELECT 'US-MN', 'Saint Paul' - UNION ALL - SELECT 'US-OH', 'Columbus' - UNION ALL - SELECT 'US-OH', 'Cleveland' - UNION ALL - SELECT 'US-OR', 'Portland' - UNION ALL - SELECT 'US-OR', 'Salem' - UNION ALL - SELECT 'US-TN', 'Nashville' - UNION ALL - SELECT 'US-TN', 'Memphis' - UNION ALL - SELECT 'US-VA', 'Richmond' - UNION ALL - SELECT 'US-VA', 'Virginia Beach' - UNION ALL - SELECT 'US-MD', 'Baltimore' - UNION ALL - SELECT 'US-MD', 'Frederick' - UNION ALL - SELECT 'US-DC', 'Washington' - UNION ALL - SELECT 'US-UT', 'Salt Lake City' - UNION ALL - SELECT 'US-UT', 'Provo' - UNION ALL - SELECT 'US-LA', 'New Orleans' - UNION ALL - SELECT 'US-LA', 'Baton Rouge' - UNION ALL - SELECT 'US-KY', 'Louisville' - UNION ALL - SELECT 'US-KY', 'Lexington' - UNION ALL - SELECT 'US-IA', 'Des Moines' - UNION ALL - SELECT 'US-IA', 'Cedar Rapids' - UNION ALL - SELECT 'US-OK', 'Oklahoma City' - UNION ALL - SELECT 'US-OK', 'Tulsa' - UNION ALL - SELECT 'US-NE', 'Omaha' - UNION ALL - SELECT 'US-NE', 'Lincoln' - UNION ALL - SELECT 'US-MO', 'Kansas City' - UNION ALL - SELECT 'US-MO', 'St. Louis' - UNION ALL - SELECT 'US-NC', 'Charlotte' - UNION ALL - SELECT 'US-NC', 'Raleigh' - UNION ALL - SELECT 'US-SC', 'Columbia' - UNION ALL - SELECT 'US-SC', 'Charleston' - UNION ALL - SELECT 'US-WI', 'Milwaukee' - UNION ALL - SELECT 'US-WI', 'Madison' - UNION ALL - SELECT 'US-MN', 'Duluth' - UNION ALL - SELECT 'US-AK', 'Anchorage' - UNION ALL - SELECT 'US-HI', 'Honolulu' - -- Canada - UNION ALL - SELECT 'CA-ON', 'Toronto' - UNION ALL - SELECT 'CA-ON', 'Ottawa' - UNION ALL - SELECT 'CA-QC', N'Montréal' - UNION ALL - SELECT 'CA-QC', N'Québec City' - UNION ALL - SELECT 'CA-BC', 'Vancouver' - UNION ALL - SELECT 'CA-BC', 'Victoria' - UNION ALL - SELECT 'CA-AB', 'Calgary' - UNION ALL - SELECT 'CA-AB', 'Edmonton' - UNION ALL - SELECT 'CA-MB', 'Winnipeg' - UNION ALL - SELECT 'CA-NS', 'Halifax' - UNION ALL - SELECT 'CA-SK', 'Saskatoon' - UNION ALL - SELECT 'CA-SK', 'Regina' - UNION ALL - SELECT 'CA-NB', 'Moncton' - UNION ALL - SELECT 'CA-NB', 'Saint John' - UNION ALL - SELECT 'CA-PE', 'Charlottetown' - UNION ALL - SELECT 'CA-NL', N'St. John''s' - UNION ALL - SELECT 'CA-ON', 'Hamilton' - UNION ALL - SELECT 'CA-ON', 'London' - UNION ALL - SELECT 'CA-QC', 'Gatineau' - UNION ALL - SELECT 'CA-QC', 'Laval' - UNION ALL - SELECT 'CA-BC', 'Kelowna' - UNION ALL - SELECT 'CA-AB', 'Red Deer' - UNION ALL - SELECT 'CA-MB', 'Brandon' - -- MEXICO - UNION ALL - SELECT 'MX-CMX', N'Ciudad de México' - UNION ALL - SELECT 'MX-JAL', 'Guadalajara' - UNION ALL - SELECT 'MX-NLE', 'Monterrey' - UNION ALL - SELECT 'MX-PUE', 'Puebla' - UNION ALL - SELECT 'MX-ROO', N'Cancún' - UNION ALL - SELECT 'MX-GUA', 'Guanajuato' - UNION ALL - SELECT 'MX-MIC', 'Morelia' - UNION ALL - SELECT 'MX-BCN', 'Tijuana' - UNION ALL - SELECT 'MX-JAL', 'Zapopan' - UNION ALL - SELECT 'MX-NLE', N'San Nicolás' - UNION ALL - SELECT 'MX-CAM', 'Campeche' - UNION ALL - SELECT 'MX-TAB', 'Villahermosa' - UNION ALL - SELECT 'MX-VER', 'Veracruz' - UNION ALL - SELECT 'MX-OAX', 'Oaxaca' - UNION ALL - SELECT 'MX-SLP', N'San Luis Potosí' - UNION ALL - SELECT 'MX-CHH', 'Chihuahua' - UNION ALL - SELECT 'MX-AGU', 'Aguascalientes' - UNION ALL - SELECT 'MX-MEX', 'Toluca' - UNION ALL - SELECT 'MX-COA', 'Saltillo' - UNION ALL - SELECT 'MX-BCS', 'La Paz' - UNION ALL - SELECT 'MX-NAY', 'Tepic' - UNION ALL - SELECT 'MX-ZAC', 'Zacatecas' - ) - INSERT INTO dbo.City - (StateProvinceID, CityName) - SELECT - dbo.UDF_GetStateProvinceIdByCode(c.StateProvinceISO2), - c.CityName - FROM Cities AS c - WHERE NOT EXISTS ( - SELECT 1 - FROM dbo.City AS ci - WHERE ci.CityName = c.CityName - AND ci.StateProvinceID = dbo.UDF_GetStateProvinceIdByCode(c.StateProvinceISO2) - ); - - - COMMIT TRANSACTION; -END; diff --git a/DataLayer/scripts/seed/procedures/USP_AddTestUsers.sql b/DataLayer/scripts/seed/procedures/USP_AddTestUsers.sql deleted file mode 100644 index ac93a18..0000000 --- a/DataLayer/scripts/seed/procedures/USP_AddTestUsers.sql +++ /dev/null @@ -1,132 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_AddTestUsers -AS -BEGIN - SET NOCOUNT ON; - SET XACT_ABORT ON; - - BEGIN TRANSACTION; - - DECLARE @FullNames TABLE - (FirstName NVARCHAR(128), - LastName NVARCHAR(128)); - - INSERT INTO @FullNames - (FirstName, LastName) - VALUES - ('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'); - - - INSERT INTO dbo.UserAccount - (Username, FirstName, LastName, Email, DateOfBirth) - SELECT - LEFT(LOWER(CONCAT(fn.FirstName, '.', fn.LastName)), 64) AS Username, - fn.FirstName, - fn.LastName, - LEFT(LOWER(CONCAT(fn.FirstName, '.', fn.LastName, '@example.com')), 128) AS Email, - - -- date of birth: pick age between 18 and 47 (18 + 0..29) - DATEADD(YEAR, -(19 + ABS(CHECKSUM(NEWID())) % 30), CAST(GETDATE() AS DATE)) - FROM @FullNames AS fn; - - - COMMIT TRANSACTION; -END; diff --git a/DataLayer/scripts/seed/procedures/USP_AddUserCredentials.sql b/DataLayer/scripts/seed/procedures/USP_AddUserCredentials.sql deleted file mode 100644 index c5a6043..0000000 --- a/DataLayer/scripts/seed/procedures/USP_AddUserCredentials.sql +++ /dev/null @@ -1,22 +0,0 @@ --- Stored procedure to insert Argon2 hashes -CREATE OR ALTER PROCEDURE dbo.USP_AddUserCredentials -( - @Hash dbo.TblUserHashes READONLY -) -AS -BEGIN - SET NOCOUNT ON; - SET XACT_ABORT ON; - - BEGIN TRANSACTION; - - INSERT INTO dbo.UserCredential - (UserAccountId, Hash) - SELECT - uah.UserAccountId, - uah.Hash - FROM @Hash AS uah; - - COMMIT TRANSACTION; -END; -GO diff --git a/DataLayer/scripts/seed/procedures/USP_CreateUserVerification.sql b/DataLayer/scripts/seed/procedures/USP_CreateUserVerification.sql deleted file mode 100644 index 38ed1ba..0000000 --- a/DataLayer/scripts/seed/procedures/USP_CreateUserVerification.sql +++ /dev/null @@ -1,31 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_CreateUserVerification -AS -BEGIN - SET NOCOUNT ON; - SET XACT_ABORT ON; - - BEGIN TRANSACTION; - - INSERT INTO dbo.UserVerification - (UserAccountId) - SELECT - ua.UserAccountID - FROM dbo.UserAccount AS ua - WHERE NOT EXISTS - (SELECT 1 - FROM dbo.UserVerification AS uv - WHERE uv.UserAccountId = ua.UserAccountID); - - - IF (SELECT COUNT(*) - FROM dbo.UserVerification) - != (SELECT COUNT(*) - FROM dbo.UserAccount) - BEGIN - RAISERROR('UserVerification count does not match UserAccount count after insertion.', 16, 1); - ROLLBACK TRANSACTION; - RETURN; - END - - COMMIT TRANSACTION; -END diff --git a/WebAPI/Controllers/NotFoundController.cs b/WebAPI/Controllers/NotFoundController.cs index 44af9e0..857694d 100644 --- a/WebAPI/Controllers/NotFoundController.cs +++ b/WebAPI/Controllers/NotFoundController.cs @@ -4,10 +4,10 @@ namespace WebAPI.Controllers { [ApiController] [ApiExplorerSettings(IgnoreApi = true)] - [Route("error")] // ← required + [Route("error")] // ← required public class NotFoundController : ControllerBase { - [HttpGet("404")] // ← required + [HttpGet("404")] // ← required public IActionResult Handle404() { return NotFound(new { message = "Route not found." }); diff --git a/WebAPI/Controllers/UsersController.cs b/WebAPI/Controllers/UsersController.cs index c91a2db..18c08b3 100644 --- a/WebAPI/Controllers/UsersController.cs +++ b/WebAPI/Controllers/UsersController.cs @@ -1,5 +1,5 @@ -using DataAccessLayer.Entities; using BusinessLayer.Services; +using DataAccessLayer.Entities; using Microsoft.AspNetCore.Mvc; namespace WebAPI.Controllers @@ -79,9 +79,15 @@ namespace WebAPI.Controllers } [HttpPut("{id:guid}")] - public IActionResult UpdateUser(Guid id, [FromBody] UserAccount userAccount) + public IActionResult UpdateUser( + Guid id, + [FromBody] UserAccount userAccount + ) { - if (userAccount.UserAccountID != Guid.Empty && userAccount.UserAccountID != id) + if ( + userAccount.UserAccountID != Guid.Empty + && userAccount.UserAccountID != id + ) { return BadRequest("UserAccountID does not match route id."); } diff --git a/WebAPI/Program.cs b/WebAPI/Program.cs index 4abd51f..24618ed 100644 --- a/WebAPI/Program.cs +++ b/WebAPI/Program.cs @@ -2,25 +2,24 @@ using BusinessLayer.Services; using DataAccessLayer; var builder = WebApplication.CreateBuilder(args); - + builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddOpenApi(); builder.Services.AddScoped(); builder.Services.AddScoped(); - -var app = builder.Build(); - -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); - app.MapOpenApi(); -} - + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); + app.MapOpenApi(); +} + app.UseHttpsRedirection(); app.MapControllers(); app.MapFallbackToController("Handle404", "NotFound"); app.Run(); - diff --git a/biergarten.sln b/biergarten.sln index d3bfd84..76307d6 100644 --- a/biergarten.sln +++ b/biergarten.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BusinessLayer", "BusinessLa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DALTests", "DALTests\DALTests.csproj", "{99A04D79-A1A9-4BF5-9B70-58FC2B5D2E8A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBSeed", "DBSeed\DBSeed.csproj", "{82C1A7F9-695C-4243-83AB-8B8A54810763}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,6 +97,18 @@ Global {5B37FCDB-1BD0-439A-A840-61322353EAAE}.Release|x64.Build.0 = Release|Any CPU {5B37FCDB-1BD0-439A-A840-61322353EAAE}.Release|x86.ActiveCfg = Release|Any CPU {5B37FCDB-1BD0-439A-A840-61322353EAAE}.Release|x86.Build.0 = Release|Any CPU + {82C1A7F9-695C-4243-83AB-8B8A54810763}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82C1A7F9-695C-4243-83AB-8B8A54810763}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82C1A7F9-695C-4243-83AB-8B8A54810763}.Debug|x64.ActiveCfg = Debug|Any CPU + {82C1A7F9-695C-4243-83AB-8B8A54810763}.Debug|x64.Build.0 = Debug|Any CPU + {82C1A7F9-695C-4243-83AB-8B8A54810763}.Debug|x86.ActiveCfg = Debug|Any CPU + {82C1A7F9-695C-4243-83AB-8B8A54810763}.Debug|x86.Build.0 = Debug|Any CPU + {82C1A7F9-695C-4243-83AB-8B8A54810763}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82C1A7F9-695C-4243-83AB-8B8A54810763}.Release|Any CPU.Build.0 = Release|Any CPU + {82C1A7F9-695C-4243-83AB-8B8A54810763}.Release|x64.ActiveCfg = Release|Any CPU + {82C1A7F9-695C-4243-83AB-8B8A54810763}.Release|x64.Build.0 = Release|Any CPU + {82C1A7F9-695C-4243-83AB-8B8A54810763}.Release|x86.ActiveCfg = Release|Any CPU + {82C1A7F9-695C-4243-83AB-8B8A54810763}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE