mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 02:39:03 +00:00
Refactor data layer, add business layer
This commit is contained in:
@@ -4,4 +4,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../DataAccessLayer/DataAccessLayer.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
15
BusinessLayer/Services/IService.cs
Normal file
15
BusinessLayer/Services/IService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BusinessLayer.Services
|
||||
{
|
||||
public interface IService<T>
|
||||
where T : class
|
||||
{
|
||||
IEnumerable<T> GetAll(int? limit, int? offset);
|
||||
T? GetById(Guid id);
|
||||
void Add(T entity);
|
||||
void Update(T entity);
|
||||
void Delete(Guid id);
|
||||
}
|
||||
}
|
||||
10
BusinessLayer/Services/IUserService.cs
Normal file
10
BusinessLayer/Services/IUserService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using DataAccessLayer.Entities;
|
||||
|
||||
namespace BusinessLayer.Services
|
||||
{
|
||||
public interface IUserService : IService<UserAccount>
|
||||
{
|
||||
UserAccount? GetByUsername(string username);
|
||||
UserAccount? GetByEmail(string email);
|
||||
}
|
||||
}
|
||||
50
BusinessLayer/Services/UserService.cs
Normal file
50
BusinessLayer/Services/UserService.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using DataAccessLayer;
|
||||
using DataAccessLayer.Entities;
|
||||
|
||||
namespace BusinessLayer.Services
|
||||
{
|
||||
public class UserService : IUserService
|
||||
{
|
||||
private readonly IUserAccountRepository _userAccountRepository;
|
||||
|
||||
public UserService(IUserAccountRepository userAccountRepository)
|
||||
{
|
||||
_userAccountRepository = userAccountRepository;
|
||||
}
|
||||
|
||||
public IEnumerable<UserAccount> GetAll(int? limit, int? offset)
|
||||
{
|
||||
return _userAccountRepository.GetAll(limit, offset);
|
||||
}
|
||||
|
||||
public UserAccount? GetById(Guid id)
|
||||
{
|
||||
return _userAccountRepository.GetById(id);
|
||||
}
|
||||
|
||||
public UserAccount? GetByUsername(string username)
|
||||
{
|
||||
return _userAccountRepository.GetByUsername(username);
|
||||
}
|
||||
|
||||
public UserAccount? GetByEmail(string email)
|
||||
{
|
||||
return _userAccountRepository.GetByEmail(email);
|
||||
}
|
||||
|
||||
public void Add(UserAccount userAccount)
|
||||
{
|
||||
_userAccountRepository.Add(userAccount);
|
||||
}
|
||||
|
||||
public void Update(UserAccount userAccount)
|
||||
{
|
||||
_userAccountRepository.Update(userAccount);
|
||||
}
|
||||
|
||||
public void Delete(Guid id)
|
||||
{
|
||||
_userAccountRepository.Delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,11 +144,72 @@ namespace DALTests
|
||||
_repository.Add(user2);
|
||||
|
||||
// Act
|
||||
var allUsers = _repository.GetAll();
|
||||
var allUsers = _repository.GetAll(null, null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(allUsers);
|
||||
Assert.True(allUsers.Count() >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAll_WithPagination_ShouldRespectLimit()
|
||||
{
|
||||
// Arrange
|
||||
var users = new List<UserAccount>
|
||||
{
|
||||
new UserAccount
|
||||
{
|
||||
UserAccountID = Guid.NewGuid(),
|
||||
Username = $"pageuser_{Guid.NewGuid():N}",
|
||||
FirstName = "Page",
|
||||
LastName = "User",
|
||||
Email = $"pageuser_{Guid.NewGuid():N}@example.com",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
DateOfBirth = new DateTime(1991, 4, 4),
|
||||
},
|
||||
new UserAccount
|
||||
{
|
||||
UserAccountID = Guid.NewGuid(),
|
||||
Username = $"pageuser_{Guid.NewGuid():N}",
|
||||
FirstName = "Page",
|
||||
LastName = "User",
|
||||
Email = $"pageuser_{Guid.NewGuid():N}@example.com",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
DateOfBirth = new DateTime(1992, 5, 5),
|
||||
},
|
||||
new UserAccount
|
||||
{
|
||||
UserAccountID = Guid.NewGuid(),
|
||||
Username = $"pageuser_{Guid.NewGuid():N}",
|
||||
FirstName = "Page",
|
||||
LastName = "User",
|
||||
Email = $"pageuser_{Guid.NewGuid():N}@example.com",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
DateOfBirth = new DateTime(1993, 6, 6),
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var user in users)
|
||||
{
|
||||
_repository.Add(user);
|
||||
}
|
||||
|
||||
// Act
|
||||
var page = _repository.GetAll(2, 0).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, page.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAll_WithPagination_ShouldValidateArguments()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => _repository.GetAll(0, 0).ToList()
|
||||
);
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => _repository.GetAll(1, -1).ToList()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ namespace DataAccessLayer
|
||||
public interface IRepository<T>
|
||||
where T : class
|
||||
{
|
||||
|
||||
void Add(T entity);
|
||||
|
||||
IEnumerable<T> GetAll(int? limit, int? offset);
|
||||
|
||||
T? GetById(Guid id);
|
||||
void Update(T entity);
|
||||
void Delete(Guid id);
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace DataAccessLayer
|
||||
{
|
||||
public interface IUserAccountRepository : IRepository<UserAccount>
|
||||
{
|
||||
IEnumerable<UserAccount> GetAll();
|
||||
UserAccount? GetByUsername(string username);
|
||||
UserAccount? GetByEmail(string email);
|
||||
}
|
||||
|
||||
@@ -66,14 +66,46 @@ namespace DataAccessLayer
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public IEnumerable<UserAccount> GetAll()
|
||||
public IEnumerable<UserAccount> GetAll(int? limit, int? offset)
|
||||
{
|
||||
if (limit.HasValue && limit <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(limit),
|
||||
"Limit must be greater than zero."
|
||||
);
|
||||
}
|
||||
|
||||
if (offset < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(offset),
|
||||
"Offset cannot be negative."
|
||||
);
|
||||
}
|
||||
|
||||
if (offset.HasValue && !limit.HasValue)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(offset),
|
||||
"Offset cannot be provided without a limit."
|
||||
);
|
||||
}
|
||||
|
||||
using SqlConnection connection = new(_connectionString);
|
||||
using SqlCommand command = new(
|
||||
"usp_GetAllUserAccounts",
|
||||
connection
|
||||
);
|
||||
command.CommandType = System.Data.CommandType.StoredProcedure;
|
||||
if (limit.HasValue)
|
||||
{
|
||||
command.Parameters.AddWithValue("@Limit", limit.Value);
|
||||
}
|
||||
if (offset.HasValue)
|
||||
{
|
||||
command.Parameters.AddWithValue("@Offset", offset.Value);
|
||||
}
|
||||
connection.Open();
|
||||
|
||||
using SqlDataReader reader = command.ExecuteReader();
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
USE Biergarten;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_CreateUserAccount
|
||||
(
|
||||
@UserAccountId UNIQUEIDENTIFIER = NULL,
|
||||
@Username VARCHAR(64),
|
||||
@FirstName NVARCHAR(128),
|
||||
@LastName NVARCHAR(128),
|
||||
@DateOfBirth DATETIME,
|
||||
@Email VARCHAR(128)
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
SET XACT_ABORT ON
|
||||
BEGIN TRANSACTION
|
||||
|
||||
INSERT INTO UserAccount
|
||||
(
|
||||
UserAccountID,
|
||||
Username,
|
||||
FirstName,
|
||||
LastName,
|
||||
DateOfBirth,
|
||||
Email
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
COALESCE(@UserAccountId, NEWID()),
|
||||
@Username,
|
||||
@FirstName,
|
||||
@LastName,
|
||||
@DateOfBirth,
|
||||
@Email
|
||||
);
|
||||
|
||||
|
||||
|
||||
COMMIT TRANSACTION
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_DeleteUserAccount
|
||||
(
|
||||
@UserAccountId UNIQUEIDENTIFIER
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
SET XACT_ABORT ON
|
||||
BEGIN TRANSACTION
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM UserAccount WHERE UserAccountId = @UserAccountId)
|
||||
BEGIN
|
||||
RAISERROR('UserAccount with the specified ID does not exist.', 16,
|
||||
1);
|
||||
ROLLBACK TRANSACTION
|
||||
RETURN
|
||||
END
|
||||
|
||||
DELETE FROM UserAccount
|
||||
WHERE UserAccountId = @UserAccountId;
|
||||
COMMIT TRANSACTION
|
||||
END;
|
||||
GO
|
||||
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_UpdateUserAccount
|
||||
(
|
||||
@Username VARCHAR(64),
|
||||
@FirstName NVARCHAR(128),
|
||||
@LastName NVARCHAR(128),
|
||||
@DateOfBirth DATETIME,
|
||||
@Email VARCHAR(128),
|
||||
@UserAccountId UNIQUEIDENTIFIER
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
SET XACT_ABORT ON
|
||||
BEGIN TRANSACTION
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM UserAccount WHERE UserAccountId = @UserAccountId)
|
||||
BEGIN
|
||||
RAISERROR('UserAccount with the specified ID does not exist.', 16,
|
||||
1);
|
||||
ROLLBACK TRANSACTION
|
||||
RETURN
|
||||
END
|
||||
|
||||
UPDATE UserAccount
|
||||
SET
|
||||
Username = @Username,
|
||||
FirstName = @FirstName,
|
||||
LastName = @LastName,
|
||||
DateOfBirth = @DateOfBirth,
|
||||
Email = @Email
|
||||
WHERE UserAccountId = @UserAccountId;
|
||||
|
||||
COMMIT TRANSACTION
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_GetUserAccountById
|
||||
(
|
||||
@UserAccountId UNIQUEIDENTIFIER
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT UserAccountID,
|
||||
Username,
|
||||
FirstName,
|
||||
LastName,
|
||||
Email,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
DateOfBirth,
|
||||
Timer
|
||||
FROM dbo.UserAccount
|
||||
WHERE UserAccountID = @UserAccountId;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_GetAllUserAccounts
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT UserAccountID,
|
||||
Username,
|
||||
FirstName,
|
||||
LastName,
|
||||
Email,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
DateOfBirth,
|
||||
Timer
|
||||
FROM dbo.UserAccount;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_GetUserAccountByUsername
|
||||
(
|
||||
@Username VARCHAR(64)
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT UserAccountID,
|
||||
Username,
|
||||
FirstName,
|
||||
LastName,
|
||||
Email,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
DateOfBirth,
|
||||
Timer
|
||||
FROM dbo.UserAccount
|
||||
WHERE Username = @Username;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_GetUserAccountByEmail
|
||||
(
|
||||
@Email VARCHAR(128)
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT UserAccountID,
|
||||
Username,
|
||||
FirstName,
|
||||
LastName,
|
||||
Email,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
DateOfBirth,
|
||||
Timer
|
||||
FROM dbo.UserAccount
|
||||
WHERE Email = @Email;
|
||||
END;
|
||||
GO
|
||||
@@ -5,11 +5,14 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference
|
||||
Include="Konscious.Security.Cryptography.Argon2"
|
||||
Version="1.3.1"
|
||||
/>
|
||||
<PackageReference Include="dbup" Version="5.0.41" />
|
||||
<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.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="scripts/**/*.sql" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
125
DataLayer/Program.cs
Normal file
125
DataLayer/Program.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System.Data;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using idunno.Password;
|
||||
using Konscious.Security.Cryptography;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
/// <summary>
|
||||
/// Executes USP_AddUserCredentials to add missing user credentials using a table-valued parameter
|
||||
/// consisting of the user account id and a generated Argon2 hash.
|
||||
/// </summary>
|
||||
/// <param name="connection">An open SQL connection.</param>
|
||||
/// <param name="credentialTable">A table-valued parameter payload containing user IDs and hashes.</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a DataTable of user account IDs and generated Argon2 password hashes for users that do not yet
|
||||
/// have credentials.
|
||||
/// </summary>
|
||||
/// <param name="connection">An open SQL connection.</param>
|
||||
/// <returns>A DataTable matching dbo.TblUserHashes with user IDs and hashes.</returns>
|
||||
static async Task<DataTable> 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
|
||||
);
|
||||
""";
|
||||
|
||||
await using var command = new SqlCommand(sql, connection);
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
|
||||
// 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 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an Argon2id hash for the given password.
|
||||
/// </summary>
|
||||
/// <param name="pwd">The plaintext password.</param>
|
||||
/// <returns>A string in the format "base64(salt):base64(hash)".</returns>
|
||||
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)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the seed process to add test users and generate missing credentials.
|
||||
/// </summary>
|
||||
/// <param name="connection">An open SQL connection.</param>
|
||||
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.");
|
||||
}
|
||||
36
DataLayer/scripts/crud/USP_CreateUserAccount.sql
Normal file
36
DataLayer/scripts/crud/USP_CreateUserAccount.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_CreateUserAccount
|
||||
(
|
||||
@UserAccountId UNIQUEIDENTIFIER = NULL,
|
||||
@Username VARCHAR(64),
|
||||
@FirstName NVARCHAR(128),
|
||||
@LastName NVARCHAR(128),
|
||||
@DateOfBirth DATETIME,
|
||||
@Email VARCHAR(128)
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
SET XACT_ABORT ON
|
||||
BEGIN TRANSACTION
|
||||
|
||||
INSERT INTO UserAccount
|
||||
(
|
||||
UserAccountID,
|
||||
Username,
|
||||
FirstName,
|
||||
LastName,
|
||||
DateOfBirth,
|
||||
Email
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
COALESCE(@UserAccountId, NEWID()),
|
||||
@Username,
|
||||
@FirstName,
|
||||
@LastName,
|
||||
@DateOfBirth,
|
||||
@Email
|
||||
);
|
||||
COMMIT TRANSACTION
|
||||
END;
|
||||
23
DataLayer/scripts/crud/USP_DeleteUserAccount.sql
Normal file
23
DataLayer/scripts/crud/USP_DeleteUserAccount.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_DeleteUserAccount
|
||||
(
|
||||
@UserAccountId UNIQUEIDENTIFIER
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
SET XACT_ABORT ON
|
||||
BEGIN TRANSACTION
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM UserAccount WHERE UserAccountId = @UserAccountId)
|
||||
BEGIN
|
||||
RAISERROR('UserAccount with the specified ID does not exist.', 16,
|
||||
1);
|
||||
ROLLBACK TRANSACTION
|
||||
RETURN
|
||||
END
|
||||
|
||||
DELETE FROM UserAccount
|
||||
WHERE UserAccountId = @UserAccountId;
|
||||
COMMIT TRANSACTION
|
||||
END;
|
||||
17
DataLayer/scripts/crud/USP_GetAllUserAccounts.sql
Normal file
17
DataLayer/scripts/crud/USP_GetAllUserAccounts.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_GetAllUserAccounts
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT UserAccountID,
|
||||
Username,
|
||||
FirstName,
|
||||
LastName,
|
||||
Email,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
DateOfBirth,
|
||||
Timer
|
||||
FROM dbo.UserAccount;
|
||||
END;
|
||||
21
DataLayer/scripts/crud/USP_GetUserAccountByEmail.sql
Normal file
21
DataLayer/scripts/crud/USP_GetUserAccountByEmail.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_GetUserAccountByEmail
|
||||
(
|
||||
@Email VARCHAR(128)
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT UserAccountID,
|
||||
Username,
|
||||
FirstName,
|
||||
LastName,
|
||||
Email,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
DateOfBirth,
|
||||
Timer
|
||||
FROM dbo.UserAccount
|
||||
WHERE Email = @Email;
|
||||
END;
|
||||
21
DataLayer/scripts/crud/USP_GetUserAccountById.sql
Normal file
21
DataLayer/scripts/crud/USP_GetUserAccountById.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_GetUserAccountById
|
||||
(
|
||||
@UserAccountId UNIQUEIDENTIFIER
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT UserAccountID,
|
||||
Username,
|
||||
FirstName,
|
||||
LastName,
|
||||
Email,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
DateOfBirth,
|
||||
Timer
|
||||
FROM dbo.UserAccount
|
||||
WHERE UserAccountID = @UserAccountId;
|
||||
END;
|
||||
21
DataLayer/scripts/crud/USP_GetUserAccountByUsername.sql
Normal file
21
DataLayer/scripts/crud/USP_GetUserAccountByUsername.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_GetUserAccountByUsername
|
||||
(
|
||||
@Username VARCHAR(64)
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT UserAccountID,
|
||||
Username,
|
||||
FirstName,
|
||||
LastName,
|
||||
Email,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
DateOfBirth,
|
||||
Timer
|
||||
FROM dbo.UserAccount
|
||||
WHERE Username = @Username;
|
||||
END;
|
||||
35
DataLayer/scripts/crud/USP_UpdateUserAccount.sql
Normal file
35
DataLayer/scripts/crud/USP_UpdateUserAccount.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
CREATE OR ALTER PROCEDURE usp_UpdateUserAccount
|
||||
(
|
||||
@Username VARCHAR(64),
|
||||
@FirstName NVARCHAR(128),
|
||||
@LastName NVARCHAR(128),
|
||||
@DateOfBirth DATETIME,
|
||||
@Email VARCHAR(128),
|
||||
@UserAccountId UNIQUEIDENTIFIER
|
||||
)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
SET XACT_ABORT ON
|
||||
BEGIN TRANSACTION
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM UserAccount WHERE UserAccountId = @UserAccountId)
|
||||
BEGIN
|
||||
RAISERROR('UserAccount with the specified ID does not exist.', 16,
|
||||
1);
|
||||
ROLLBACK TRANSACTION
|
||||
RETURN
|
||||
END
|
||||
|
||||
UPDATE UserAccount
|
||||
SET
|
||||
Username = @Username,
|
||||
FirstName = @FirstName,
|
||||
LastName = @LastName,
|
||||
DateOfBirth = @DateOfBirth,
|
||||
Email = @Email
|
||||
WHERE UserAccountId = @UserAccountId;
|
||||
|
||||
COMMIT TRANSACTION
|
||||
END;
|
||||
@@ -1,26 +1,22 @@
|
||||
----------------------------------------------------------------------------
|
||||
----------------------------------------------------------------------------
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
USE master;
|
||||
-- USE master;
|
||||
|
||||
IF EXISTS (SELECT name
|
||||
FROM sys.databases
|
||||
WHERE name = N'Biergarten')
|
||||
BEGIN
|
||||
ALTER DATABASE Biergarten SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
||||
END
|
||||
GO
|
||||
-- IF EXISTS (SELECT name
|
||||
-- FROM sys.databases
|
||||
-- WHERE name = N'Biergarten')
|
||||
-- BEGIN
|
||||
-- ALTER DATABASE Biergarten SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
||||
-- END
|
||||
|
||||
DROP DATABASE IF EXISTS Biergarten;
|
||||
GO
|
||||
-- DROP DATABASE IF EXISTS Biergarten;
|
||||
|
||||
CREATE DATABASE Biergarten;
|
||||
GO
|
||||
-- CREATE DATABASE Biergarten;
|
||||
|
||||
USE Biergarten;
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
----------------------------------------------------------------------------
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE dbo.UserAccount
|
||||
(
|
||||
@@ -553,6 +549,3 @@ CREATE TABLE BeerPostComment
|
||||
CREATE NONCLUSTERED INDEX IX_BeerPostComment_BeerPost
|
||||
ON BeerPostComment(BeerPostID)
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
----------------------------------------------------------------------------
|
||||
-- EOF
|
||||
@@ -1,6 +1,3 @@
|
||||
USE Biergarten;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER FUNCTION dbo.UDF_GetCountryIdByCode
|
||||
(
|
||||
@CountryCode NVARCHAR(2)
|
||||
@@ -16,4 +13,3 @@ BEGIN
|
||||
|
||||
RETURN @CountryId;
|
||||
END;
|
||||
GO
|
||||
@@ -1,6 +1,3 @@
|
||||
USE Biergarten;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER FUNCTION dbo.UDF_GetStateProvinceIdByCode
|
||||
(
|
||||
@StateProvinceCode NVARCHAR(6)
|
||||
@@ -14,4 +11,3 @@ BEGIN
|
||||
WHERE ISO3616_2 = @StateProvinceCode;
|
||||
RETURN @StateProvinceId;
|
||||
END;
|
||||
GO
|
||||
@@ -1,6 +1,3 @@
|
||||
USE Biergarten;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_AddLocations
|
||||
AS
|
||||
BEGIN
|
||||
@@ -501,4 +498,3 @@ BEGIN
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
END;
|
||||
GO
|
||||
@@ -1,6 +1,3 @@
|
||||
USE Biergarten;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_AddTestUsers
|
||||
AS
|
||||
BEGIN
|
||||
@@ -133,4 +130,3 @@ BEGIN
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
END;
|
||||
GO
|
||||
@@ -1,15 +1,3 @@
|
||||
USE Biergarten;
|
||||
GO
|
||||
|
||||
IF TYPE_ID(N'dbo.TblUserHashes') IS NULL
|
||||
EXEC('CREATE TYPE dbo.TblUserHashes AS TABLE
|
||||
(
|
||||
UserAccountId UNIQUEIDENTIFIER NOT NULL,
|
||||
Hash NVARCHAR(MAX) NOT NULL
|
||||
);');
|
||||
GO
|
||||
|
||||
-- Stored procedure to insert Argon2 hashes
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_AddUserCredentials
|
||||
(
|
||||
@Hash dbo.TblUserHashes READONLY
|
||||
@@ -30,4 +18,3 @@ BEGIN
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
END;
|
||||
GO
|
||||
@@ -1,6 +1,3 @@
|
||||
USE Biergarten;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.USP_CreateUserVerification
|
||||
AS
|
||||
BEGIN
|
||||
@@ -32,4 +29,3 @@ BEGIN
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
END
|
||||
GO
|
||||
@@ -1,285 +0,0 @@
|
||||
using System.Data;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Konscious.Security.Cryptography;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
|
||||
string ConnectionString = Environment.GetEnvironmentVariable(
|
||||
"DB_CONNECTION_STRING"
|
||||
)!;
|
||||
|
||||
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 AddStoredProcsAndFunctions(SqlConnection connection)
|
||||
{
|
||||
string projectRoot = Path.GetFullPath(
|
||||
Path.Combine(AppContext.BaseDirectory, "..", "..", "..")
|
||||
);
|
||||
|
||||
string functionsDir = Path.Combine(projectRoot, "seed", "functions");
|
||||
string proceduresDir = Path.Combine(projectRoot, "seed", "procedures");
|
||||
string crudDir = Path.GetFullPath(
|
||||
Path.Combine(projectRoot, "..", "DataAccessLayer", "Sql", "crud")
|
||||
);
|
||||
|
||||
if (Directory.Exists(functionsDir))
|
||||
{
|
||||
foreach (
|
||||
string file in Directory
|
||||
.EnumerateFiles(
|
||||
functionsDir,
|
||||
"*.sql",
|
||||
SearchOption.TopDirectoryOnly
|
||||
)
|
||||
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
string sql = await File.ReadAllTextAsync(file);
|
||||
await ExecuteScriptAsync(connection, sql);
|
||||
Console.WriteLine(
|
||||
$"Executed function script: {Path.GetFileName(file)}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Directory.Exists(proceduresDir))
|
||||
{
|
||||
foreach (
|
||||
string file in Directory
|
||||
.EnumerateFiles(
|
||||
proceduresDir,
|
||||
"*.sql",
|
||||
SearchOption.TopDirectoryOnly
|
||||
)
|
||||
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
string sql = await File.ReadAllTextAsync(file);
|
||||
await ExecuteScriptAsync(connection, sql);
|
||||
Console.WriteLine(
|
||||
$"Executed procedure script: {Path.GetFileName(file)}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Directory.Exists(crudDir))
|
||||
{
|
||||
foreach (
|
||||
string file in Directory
|
||||
.EnumerateFiles(
|
||||
crudDir,
|
||||
"*.sql",
|
||||
SearchOption.TopDirectoryOnly
|
||||
)
|
||||
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
string sql = await File.ReadAllTextAsync(file);
|
||||
await ExecuteScriptAsync(connection, sql);
|
||||
Console.WriteLine(
|
||||
$"Executed CRUD script: {Path.GetFileName(file)}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
"Functions and stored procedures added or updated successfully."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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<DataTable> 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<string> 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 AddStoredProcsAndFunctions(connection);
|
||||
await RunSeedAsync(connection);
|
||||
Console.WriteLine("Seeding complete.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Seeding failed: {ex.Message}");
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
@@ -32,14 +32,14 @@ The repositories are currently responsible for:
|
||||
## Database schema and seed
|
||||
|
||||
- `DataLayer/schema.sql` contains the database schema definitions.
|
||||
- `DataLayer/seed/SeedDB.cs` provides seeding and stored procedure/function loading.
|
||||
- Stored procedure scripts are organized under `DataAccessLayer/Sql/crud/` (UserAccount and related).
|
||||
- `DataLayer/database/` holds application functions and CRUD procedures.
|
||||
- `DataLayer/seed/` holds seed-only procedures and the `SeedDB.cs` entry point.
|
||||
- `SeedDB.cs` honors `SEED_MODE` (`database`, `seed`, or `all`) to control which scripts run.
|
||||
|
||||
## Key conventions
|
||||
|
||||
- **Environment variables**: `DB_CONNECTION_STRING` is required for DAL and seed tooling.
|
||||
- **Stored procedures**: CRUD operations use `usp_*` procedures.
|
||||
- **Rowversion** columns are represented as `byte[]` in entities (e.g., `Timer`).
|
||||
|
||||
## Suggested dependency direction
|
||||
|
||||
|
||||
16
WebAPI/Controllers/NotFoundController.cs
Normal file
16
WebAPI/Controllers/NotFoundController.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace WebAPI.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[Route("error")] // ← required
|
||||
public class NotFoundController : ControllerBase
|
||||
{
|
||||
[HttpGet("404")] // ← required
|
||||
public IActionResult Handle404()
|
||||
{
|
||||
return NotFound(new { message = "Route not found." });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using DataAccessLayer;
|
||||
using DataAccessLayer.Entities;
|
||||
using BusinessLayer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace WebAPI.Controllers
|
||||
@@ -8,40 +8,57 @@ namespace WebAPI.Controllers
|
||||
[Route("api/users")]
|
||||
public class UsersController : ControllerBase
|
||||
{
|
||||
private readonly IUserAccountRepository _userAccountRepository;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public UsersController()
|
||||
public UsersController(IUserService userService)
|
||||
{
|
||||
_userAccountRepository = new UserAccountRepository();
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
// all users
|
||||
[HttpGet]
|
||||
[HttpGet("users")]
|
||||
public IActionResult GetAllUsers()
|
||||
public IActionResult GetAllUsers(
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] int? offset
|
||||
)
|
||||
{
|
||||
var users = _userAccountRepository.GetAll();
|
||||
if (offset.HasValue && !limit.HasValue)
|
||||
{
|
||||
return BadRequest("Limit is required when offset is provided.");
|
||||
}
|
||||
|
||||
if (limit.HasValue && limit <= 0)
|
||||
{
|
||||
return BadRequest("Limit must be greater than zero.");
|
||||
}
|
||||
|
||||
if (offset.HasValue && offset < 0)
|
||||
{
|
||||
return BadRequest("Offset cannot be negative.");
|
||||
}
|
||||
|
||||
var users = _userService.GetAll(limit, offset);
|
||||
return Ok(users);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public IActionResult GetUserById(Guid id)
|
||||
{
|
||||
var user = _userAccountRepository.GetById(id);
|
||||
var user = _userService.GetById(id);
|
||||
return user is null ? NotFound() : Ok(user);
|
||||
}
|
||||
|
||||
[HttpGet("by-username/{username}")]
|
||||
[HttpGet("username/{username}")]
|
||||
public IActionResult GetUserByUsername(string username)
|
||||
{
|
||||
var user = _userAccountRepository.GetByUsername(username);
|
||||
var user = _userService.GetByUsername(username);
|
||||
return user is null ? NotFound() : Ok(user);
|
||||
}
|
||||
|
||||
[HttpGet("by-email/{email}")]
|
||||
[HttpGet("email/{email}")]
|
||||
public IActionResult GetUserByEmail(string email)
|
||||
{
|
||||
var user = _userAccountRepository.GetByEmail(email);
|
||||
var user = _userService.GetByEmail(email);
|
||||
return user is null ? NotFound() : Ok(user);
|
||||
}
|
||||
|
||||
@@ -53,7 +70,7 @@ namespace WebAPI.Controllers
|
||||
userAccount.UserAccountID = Guid.NewGuid();
|
||||
}
|
||||
|
||||
_userAccountRepository.Add(userAccount);
|
||||
_userService.Add(userAccount);
|
||||
return CreatedAtAction(
|
||||
nameof(GetUserById),
|
||||
new { id = userAccount.UserAccountID },
|
||||
@@ -70,14 +87,14 @@ namespace WebAPI.Controllers
|
||||
}
|
||||
|
||||
userAccount.UserAccountID = id;
|
||||
_userAccountRepository.Update(userAccount);
|
||||
_userService.Update(userAccount);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
public IActionResult DeleteUser(Guid id)
|
||||
{
|
||||
_userAccountRepository.Delete(id);
|
||||
_userService.Delete(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
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<IUserAccountRepository, UserAccountRepository>();
|
||||
builder.Services.AddScoped<IUserService, UserService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@@ -16,5 +21,6 @@ if (app.Environment.IsDevelopment())
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.MapControllers();
|
||||
app.MapFallbackToController("Handle404", "NotFound");
|
||||
app.Run();
|
||||
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../DataAccessLayer/DataAccessLayer.csproj" />
|
||||
<ProjectReference Include="../BusinessLayer/BusinessLayer.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// See https://aka.ms/new-console-template for more information
|
||||
Console.WriteLine("Hello, World!");
|
||||
@@ -1,10 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -13,8 +13,6 @@ 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}") = "WebCrawler", "WebCrawler\WebCrawler.csproj", "{5B37FCDB-1BD0-439A-A840-61322353EAAE}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
||||
Reference in New Issue
Block a user