From b7f22fcc6628d5d212288fa6afd5ff9fb01f6587 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Wed, 12 Nov 2025 00:41:27 -0500 Subject: [PATCH] Add seed db c# project --- .config/dotnet-tools.json | 13 -- .csharpierrc.json | 10 + .vscode/settings.json | 3 + SeedDB/Program.cs | 218 ++++++++++++++++++++ SeedDB/SeedDB.csproj | 15 ++ test-data.sql => SeedDB/SeedStoredProcs.sql | 75 +++---- schema.sql => SeedDB/schema.sql | 16 +- biergarten.sln | 34 +-- data/users.csv | 31 --- 9 files changed, 303 insertions(+), 112 deletions(-) delete mode 100644 .config/dotnet-tools.json create mode 100644 .csharpierrc.json create mode 100644 .vscode/settings.json create mode 100644 SeedDB/Program.cs create mode 100644 SeedDB/SeedDB.csproj rename test-data.sql => SeedDB/SeedStoredProcs.sql (82%) rename schema.sql => SeedDB/schema.sql (96%) delete mode 100644 data/users.csv diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json deleted file mode 100644 index 321b5d6..0000000 --- a/.config/dotnet-tools.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "csharpier": { - "version": "1.1.2", - "commands": [ - "csharpier" - ], - "rollForward": false - } - } -} \ No newline at end of file diff --git a/.csharpierrc.json b/.csharpierrc.json new file mode 100644 index 0000000..1073d01 --- /dev/null +++ b/.csharpierrc.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/belav/csharpier/main/src/CSharpier.Cli/schema.json", + "printWidth": 80, + "useTabs": false, + "tabWidth": 4, + "endOfLine": "auto", + "indentStyle": "space", + "lineEndings": "auto", + "wrapLineLength": 80 +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6a8e057 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "SeedDB.sln" +} diff --git a/SeedDB/Program.cs b/SeedDB/Program.cs new file mode 100644 index 0000000..d13b16c --- /dev/null +++ b/SeedDB/Program.cs @@ -0,0 +1,218 @@ +using System.Data; +using System.Security.Cryptography; +using System.Text; +using Konscious.Security.Cryptography; +using Microsoft.Data.SqlClient; + +// @todo store this securely using environment variables or a secret manager +const string connectionString = + @"Data Source=AARONPC\INFO5052;Integrated Security=True; + Persist Security Info=False;Pooling=False; + MultipleActiveResultSets=False;Encrypt=True; + TrustServerCertificate=True;Connection Timeout=30;"; + +static async Task BuildSchema(SqlConnection connection) +{ + string sql = await File.ReadAllTextAsync(GetScriptPath("schema.sql")); + await ExecuteScriptAsync(connection, sql); + Console.WriteLine("Database schema created or updated successfully."); +} + +static async Task AddStoredProcs(SqlConnection connection) +{ + string sql = await File.ReadAllTextAsync( + GetScriptPath("SeedStoredProcs.sql") + ); + await ExecuteScriptAsync(connection, sql); + Console.WriteLine("Stored procedures added or updated successfully."); +} + +static async Task RunSeedAsync(SqlConnection connection) +{ + await ExecuteStoredProcedureAsync(connection, "dbo.USP_AddTestUsers"); + Console.WriteLine("Inserted or refreshed test users."); + + DataTable credentialRows = await BuildCredentialTableAsync(connection); + if (credentialRows.Rows.Count > 0) + { + await ExecuteCredentialProcedureAsync(connection, credentialRows); + Console.WriteLine( + $"Generated {credentialRows.Rows.Count} credential hashes." + ); + } + else + { + Console.WriteLine("No new credentials required."); + } + + await ExecuteStoredProcedureAsync( + connection, + "dbo.USP_CreateUserVerification" + ); + Console.WriteLine("Ensured verification rows exist for all users."); +} + +static async Task ExecuteStoredProcedureAsync( + SqlConnection connection, + string storedProcedureName +) +{ + await using SqlCommand command = new SqlCommand( + storedProcedureName, + connection + ); + command.CommandType = CommandType.StoredProcedure; + await command.ExecuteNonQueryAsync(); +} + +static async Task ExecuteCredentialProcedureAsync( + SqlConnection connection, + DataTable credentialTable +) +{ + await using SqlCommand command = new SqlCommand( + "dbo.USP_AddUserCredentials", + connection + ); + command.CommandType = CommandType.StoredProcedure; + + SqlParameter tvpParameter = command.Parameters.Add( + "@Hash", + SqlDbType.Structured + ); + tvpParameter.TypeName = "dbo.TblUserHashes"; + tvpParameter.Value = credentialTable; + + await command.ExecuteNonQueryAsync(); +} + +static async Task BuildCredentialTableAsync(SqlConnection connection) +{ + const string sql = """ +SELECT ua.UserAccountID, + ua.Username +FROM dbo.UserAccount AS ua +WHERE NOT EXISTS ( + SELECT 1 + FROM dbo.UserCredential AS uc + WHERE uc.UserAccountID = ua.UserAccountID); +"""; + + await using SqlCommand command = new(sql, connection); + await using SqlDataReader reader = await command.ExecuteReaderAsync(); + + DataTable table = new(); + table.Columns.Add("UserAccountId", typeof(Guid)); + table.Columns.Add("Hash", typeof(string)); + + while (await reader.ReadAsync()) + { + Guid userId = reader.GetGuid(0); + string username = reader.GetString(1); + + string password = CreatePlainTextPassword(username); + string hash = GeneratePasswordHash(password); + + DataRow row = table.NewRow(); + row["UserAccountId"] = userId; + row["Hash"] = hash; + table.Rows.Add(row); + } + + return table; +} + +static string CreatePlainTextPassword(string username) => $"{username}#2025!"; + +static string GeneratePasswordHash(string password) +{ + byte[] salt = RandomNumberGenerator.GetBytes(16); + + var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password)) + { + Salt = salt, + DegreeOfParallelism = Math.Max(Environment.ProcessorCount, 1), + MemorySize = 65536, + Iterations = 4, + }; + + byte[] hash = argon2.GetBytes(32); + string saltBase64 = Convert.ToBase64String(salt); + string hashBase64 = Convert.ToBase64String(hash); + + // Store salt and hash together so verification can rebuild the key material. + return $"{saltBase64}:{hashBase64}"; +} + +static async Task ExecuteScriptAsync(SqlConnection connection, string sql) +{ + foreach (string batch in SplitSqlBatches(sql)) + { + if (string.IsNullOrWhiteSpace(batch)) + { + continue; + } + + await using SqlCommand command = new(batch, connection); + await command.ExecuteNonQueryAsync(); + } +} + +static IEnumerable SplitSqlBatches(string sql) +{ + using StringReader reader = new(sql); + StringBuilder buffer = new(); + + string? line; + while ((line = reader.ReadLine()) is not null) + { + if (line.Trim().Equals("GO", StringComparison.OrdinalIgnoreCase)) + { + yield return buffer.ToString(); + buffer.Clear(); + continue; + } + + buffer.AppendLine(line); + } + + if (buffer.Length > 0) + { + yield return buffer.ToString(); + } +} + +static string GetScriptPath(string fileName) +{ + string projectRoot = Path.GetFullPath( + Path.Combine(AppContext.BaseDirectory, "..", "..", "..") + ); + string candidate = Path.Combine(projectRoot, fileName); + + if (File.Exists(candidate)) + { + return candidate; + } + + throw new FileNotFoundException( + $"SQL script '{fileName}' was not found.", + candidate + ); +} + +try +{ + await using SqlConnection connection = new(connectionString); + await connection.OpenAsync(); + Console.WriteLine("Connection to database established successfully."); + + await BuildSchema(connection); + await AddStoredProcs(connection); + await RunSeedAsync(connection); + Console.WriteLine("Seeding complete."); +} +catch (Exception ex) +{ + Console.Error.WriteLine($"Seeding failed: {ex.Message}"); + Environment.ExitCode = 1; +} diff --git a/SeedDB/SeedDB.csproj b/SeedDB/SeedDB.csproj new file mode 100644 index 0000000..f32938c --- /dev/null +++ b/SeedDB/SeedDB.csproj @@ -0,0 +1,15 @@ + + + Exe + net9.0 + enable + enable + + + + + + diff --git a/test-data.sql b/SeedDB/SeedStoredProcs.sql similarity index 82% rename from test-data.sql rename to SeedDB/SeedStoredProcs.sql index 3e05a2f..795ff73 100644 --- a/test-data.sql +++ b/SeedDB/SeedStoredProcs.sql @@ -136,31 +136,38 @@ BEGIN END; GO --- -- 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; +CREATE TYPE TblUserHashes AS TABLE +( + UserAccountId UNIQUEIDENTIFIER NOT NULL, + Hash NVARCHAR(MAX) NOT NULL +); +GO --- BEGIN TRANSACTION; +-- 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; --- INSERT INTO dbo.UserCredential --- (UserAccountId, Hash) --- SELECT --- uah.UserAccountId, --- uah.Hash --- FROM @Hash AS uah; + BEGIN TRANSACTION; --- COMMIT TRANSACTION; --- END; --- GO + INSERT INTO dbo.UserCredential + (UserAccountId, Hash) + SELECT + uah.UserAccountId, + uah.Hash + FROM @Hash AS uah; + + COMMIT TRANSACTION; +END; +GO CREATE OR ALTER PROCEDURE dbo.USP_CreateUserVerification -AS +AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; @@ -174,11 +181,13 @@ BEGIN FROM dbo.UserAccount AS ua WHERE NOT EXISTS (SELECT 1 - FROM dbo.UserVerification AS uv - WHERE uv.UserAccountId = ua.UserAccountID); + FROM dbo.UserVerification AS uv + WHERE uv.UserAccountId = ua.UserAccountID); - IF (SELECT COUNT(*) FROM dbo.UserVerification) != (SELECT COUNT(*) FROM dbo.UserAccount) + 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; @@ -188,23 +197,3 @@ BEGIN COMMIT TRANSACTION; END GO - -BEGIN TRY - EXEC dbo.USP_AddTestUsers; - PRINT 'AddTestUsers completed.'; - - EXEC dbo.USP_CreateUserVerification; - PRINT 'CreateUserVerification completed.'; -END TRY -BEGIN CATCH - PRINT ERROR_MESSAGE(); -END CATCH -GO - - -SELECT * -FROM dbo.UserAccount; - -SELECT * -FROM dbo.UserVerification; -GO \ No newline at end of file diff --git a/schema.sql b/SeedDB/schema.sql similarity index 96% rename from schema.sql rename to SeedDB/schema.sql index fad6629..cbf9406 100644 --- a/schema.sql +++ b/SeedDB/schema.sql @@ -164,7 +164,7 @@ CREATE TABLE UserCredential -- delete credentials when user account is deleted Expiry DATETIME CONSTRAINT DF_UserCredential_Expiry DEFAULT DATEADD(DAY, 90, GETDATE()) NOT NULL, - Hash NVARCHAR(100) NOT NULL, + Hash NVARCHAR(MAX) NOT NULL, -- uses argon2 Timer ROWVERSION, @@ -503,10 +503,10 @@ CREATE NONCLUSTERED INDEX IX_BeerPostComment_BeerPost ----------------------------------------------------------------------------- ----------------------------------------------------------------------------- -/* - -dotnet add package Microsoft.EntityFrameworkCore.SqlServer -dotnet add package Microsoft.EntityFrameworkCore.Tools - -Scaffold-DbContext "Data Source=AARONPC\INFO5052;Integrated Security=True;Persist Security Info=False;Pooling=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=True;Database=Biergarten" Microsoft.EntityFrameworkCore.SqlServer -Context BiergartenContext -ContextDir "." -OutputDir "Entities" -UseDatabaseNames -Force -*/ +USE Biergarten; +SELECT * +FROM UserAccount; +SELECT * +FROM UserCredential; +SELECT * +FROM UserVerification; \ No newline at end of file diff --git a/biergarten.sln b/biergarten.sln index 6cb0770..1038e2e 100644 --- a/biergarten.sln +++ b/biergarten.sln @@ -3,30 +3,30 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.14.36603.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataAccessLayer", "DataAccessLayer\DataAccessLayer.csproj", "{A9FCCEB3-DD88-F8C0-89A3-FD31A7C54F23}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataAccessLayer-Tests", "DataAccessLayer-Tests\DataAccessLayer-Tests.csproj", "{3638DC4E-D4C8-4DBE-CF3B-EF2C805509CE}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" - ProjectSection(SolutionItems) = preProject - schema.sql = schema.sql - test-data.sql = test-data.sql - EndProjectSection +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeedDB", "SeedDB\SeedDB.csproj", "{45F7F75E-FD8D-4862-9BDB-6E59F6941DFB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A9FCCEB3-DD88-F8C0-89A3-FD31A7C54F23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A9FCCEB3-DD88-F8C0-89A3-FD31A7C54F23}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A9FCCEB3-DD88-F8C0-89A3-FD31A7C54F23}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A9FCCEB3-DD88-F8C0-89A3-FD31A7C54F23}.Release|Any CPU.Build.0 = Release|Any CPU - {3638DC4E-D4C8-4DBE-CF3B-EF2C805509CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3638DC4E-D4C8-4DBE-CF3B-EF2C805509CE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3638DC4E-D4C8-4DBE-CF3B-EF2C805509CE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3638DC4E-D4C8-4DBE-CF3B-EF2C805509CE}.Release|Any CPU.Build.0 = Release|Any CPU + {45F7F75E-FD8D-4862-9BDB-6E59F6941DFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45F7F75E-FD8D-4862-9BDB-6E59F6941DFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45F7F75E-FD8D-4862-9BDB-6E59F6941DFB}.Debug|x64.ActiveCfg = Debug|Any CPU + {45F7F75E-FD8D-4862-9BDB-6E59F6941DFB}.Debug|x64.Build.0 = Debug|Any CPU + {45F7F75E-FD8D-4862-9BDB-6E59F6941DFB}.Debug|x86.ActiveCfg = Debug|Any CPU + {45F7F75E-FD8D-4862-9BDB-6E59F6941DFB}.Debug|x86.Build.0 = Debug|Any CPU + {45F7F75E-FD8D-4862-9BDB-6E59F6941DFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45F7F75E-FD8D-4862-9BDB-6E59F6941DFB}.Release|Any CPU.Build.0 = Release|Any CPU + {45F7F75E-FD8D-4862-9BDB-6E59F6941DFB}.Release|x64.ActiveCfg = Release|Any CPU + {45F7F75E-FD8D-4862-9BDB-6E59F6941DFB}.Release|x64.Build.0 = Release|Any CPU + {45F7F75E-FD8D-4862-9BDB-6E59F6941DFB}.Release|x86.ActiveCfg = Release|Any CPU + {45F7F75E-FD8D-4862-9BDB-6E59F6941DFB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/data/users.csv b/data/users.csv deleted file mode 100644 index 9e3eaa2..0000000 --- a/data/users.csv +++ /dev/null @@ -1,31 +0,0 @@ -UserAccountID,Username,FirstName,LastName,Email,CreatedAt,UpdatedAt,DateOfBirth -b8624cea-1a90-4b2d-a12e-a71a86eb1f10,lambrosch0,Lissy,Ambrosch,lambrosch0@upenn.edu,2022-09-18,2018-12-14,1987-02-19 -3954ed02-4f51-4682-99d4-703dfcfed61a,keslinger1,Knox,Eslinger,keslinger1@smugmug.com,2025-05-21,2021-04-22,1958-01-18 -95c1d06f-dc6d-4dfa-a4e3-4e29b412b043,mtrobey2,Manuel,Trobey,mtrobey2@wp.com,2016-09-25,2011-01-05,1993-01-06 -7ceb8774-9cbe-4da3-b697-98ea14b3a86d,kgadsden3,Kerianne,Gadsden,kgadsden3@tinypic.com,2018-12-16,2011-12-11,1950-05-01 -04957847-0564-4b8b-8b4a-cdce1c387914,mvane4,Mord,Vane,mvane4@booking.com,2014-08-31,2025-05-07,1928-08-28 -1b53e41e-60e4-4c81-b162-473d036fd40e,iklimowski5,Ileana,Klimowski,iklimowski5@mtv.com,2024-11-28,2014-05-17,1933-07-20 -cba032ba-ba35-415d-aff0-14936d73b23a,mvickars6,Mischa,Vickars,mvickars6@census.gov,2012-08-06,2014-04-03,1991-03-06 -5a05b977-f6d8-4e9f-b700-ef74e0d4cd79,cbarrus7,Charin,Barrus,cbarrus7@digg.com,2019-08-01,2017-02-26,1984-10-19 -c36f23b6-1af0-4525-9078-03f7285f11fd,gbroady8,Gerri,Broady,gbroady8@cafepress.com,2015-03-23,2021-12-15,1986-09-22 -9eee230d-8e88-4ab7-ad3a-08909f0f179f,jwoolf9,Justis,Woolf,jwoolf9@oakley.com,2020-10-29,2018-07-03,1984-04-13 -4db300d3-8dae-4efb-8be6-2ee31e10489b,agrigsa,Annabell,Grigs,agrigsa@behance.net,2014-09-08,2020-07-17,1932-06-23 -0bdce4af-77e9-495c-b96e-447d6020e851,lmilkinb,Lucia,Milkin,lmilkinb@yahoo.com,2019-07-02,2016-06-04,1975-09-07 -7a815e7c-baa8-4eef-aab8-0e2c394191b0,ahumbatchc,Alvira,Humbatch,ahumbatchc@hc360.com,2015-06-18,2012-04-04,2004-05-13 -50454152-ece0-489f-a1c1-1db8d03b3ce6,ttackd,Teodoro,Tack,ttackd@ucsd.edu,2012-04-25,2022-05-20,1975-05-13 -cf775ced-ecd9-4291-9bc4-ab0cd3ea2ccf,rscholarde,Rip,Scholard,rscholarde@paginegialle.it,2011-09-15,2014-01-13,1975-11-18 -8157d559-2e7c-448f-a845-c649302b8f36,pwarlandf,Phaedra,Warland,pwarlandf@gravatar.com,2024-03-21,2016-09-07,1929-05-29 -c3b257e1-9d79-4494-982e-2fb44a28a7f9,sshallog,Selia,Shallo,sshallog@google.ca,2014-07-19,2021-07-23,1935-06-20 -8dc3f85f-a3bb-46b2-83d5-475c0d33f533,carensonh,Consolata,Arenson,carensonh@jalbum.net,2023-08-26,2024-06-06,1940-03-13 -f39db8d5-d2a7-4726-bcee-aeb1c88a6340,fsushamsi,Falito,Sushams,fsushamsi@indiatimes.com,2022-11-07,2024-09-05,1989-02-06 -97460048-226d-4124-b545-730d0a45fcbf,cschutzej,Creighton,Schutze,cschutzej@seattletimes.com,2010-05-28,2019-05-01,1945-01-12 -183d0825-b6b4-4184-8128-6b1b3ee9c34e,pivanuschkak,Park,Ivanuschka,pivanuschkak@unblog.fr,2017-04-04,2018-02-02,1961-08-10 -75b72f28-7f12-435e-9347-e7026724255d,sgodardl,Stanfield,Godard,sgodardl@statcounter.com,2014-08-02,2020-06-01,1978-03-03 -82b57935-4d01-4eee-a95c-5f7660bc1308,sdeversm,Shaylah,Devers,sdeversm@sbwire.com,2017-07-21,2015-08-20,1963-06-21 -5a76558c-77d2-4698-9aa2-91adf09c3e40,nakessn,Noellyn,Akess,nakessn@sakura.ne.jp,2012-07-28,2023-01-08,1947-02-26 -b2dc17ab-31cc-40c5-9a7c-d95e790fecea,jdumbello,Jobie,Dumbell,jdumbello@google.de,2015-02-01,2013-08-27,1941-04-22 -0c35296a-4833-499f-9c27-2469ba2d41e6,dbiggsp,Dacia,Biggs,dbiggsp@paginegialle.it,2020-07-06,2025-05-25,1921-10-26 -9a0c0852-2db8-4824-8395-97485edf3fc3,ctraiteq,Chan,Traite,ctraiteq@ibm.com,2021-12-02,2018-05-21,1952-03-05 -3d7005b0-7ad0-45a8-a958-184686430af2,lcowlinr,Lorant,Cowlin,lcowlinr@soup.io,2012-01-10,2015-05-22,1947-04-25 -9ab507dd-a0af-4ccd-99b9-ab9077aabf96,csmewings,Corissa,Smewing,csmewings@friendfeed.com,2016-11-15,2020-05-13,1989-12-20 -f0a6c3da-465e-4ff3-9069-4383dcb0e0f6,renderbyt,Rancell,Enderby,renderbyt@quantcast.com,2013-06-07,2023-07-26,1970-05-13