diff --git a/Directory.Build.props b/Directory.Build.props
index 8b8f2ac..b6340c5 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,9 +3,16 @@
true
none.ignore
default
+ net9.0
+ enable
+ enable
+
+
+ CS8618
+
\ No newline at end of file
diff --git a/PasswordKeeper.BusinessLogic/PasswordKeeper.BusinessLogic.csproj b/PasswordKeeper.BusinessLogic/PasswordKeeper.BusinessLogic.csproj
index fc9640d..0b3a384 100644
--- a/PasswordKeeper.BusinessLogic/PasswordKeeper.BusinessLogic.csproj
+++ b/PasswordKeeper.BusinessLogic/PasswordKeeper.BusinessLogic.csproj
@@ -1,11 +1,5 @@
-
-
- net9.0
- enable
- enable
-
-
+
diff --git a/PasswordKeeper.BusinessLogic/Users.cs b/PasswordKeeper.BusinessLogic/Users.cs
index b4b6399..80dc979 100644
--- a/PasswordKeeper.BusinessLogic/Users.cs
+++ b/PasswordKeeper.BusinessLogic/Users.cs
@@ -34,6 +34,18 @@ public async Task UsersExist(bool? admin = null)
{
return await users.UsersExist(admin);
}
+
+ ///
+ public async Task> GetAllUsers()
+ {
+ return await users.GetAllUsers();
+ }
+
+ ///
+ public async Task DeleteUser(long id)
+ {
+ await users.DeleteUser(id);
+ }
private const int IterationCount = 1000000;
@@ -117,6 +129,7 @@ public async Task Login(string username, string password, byte[] jw
PasswordHash = HashPassword(password, ref salt),
PasswordSalt = Convert.ToBase64String(salt!),
IsAdmin = true,
+ UserFullName = "Administrator",
};
userDto = await users.UpsertUser(userDto);
@@ -126,9 +139,13 @@ public async Task Login(string username, string password, byte[] jw
return new LoginResult(false, Passwords.CreateMessageString(LoginRejectReason.FailedToCreateAdminUser), false, LoginRejectReason.FailedToCreateAdminUser);
}
- var token = Passwords.GenerateJwtToken(username, userDto.Id, jwtKey, pseudoDomain);
+ var token = Passwords.GenerateJwtToken(username, userDto.Id, jwtKey, pseudoDomain, userDto.IsAdmin);
return new LoginResult(true, token, false, LoginRejectReason.None);
}
+ else
+ {
+ userDto = await users.GetUserByName(username);
+ }
// An existing user, verify the password
if (userDto is not null)
@@ -136,7 +153,7 @@ public async Task Login(string username, string password, byte[] jw
if (Users.VerifyPassword(password, userDto.PasswordHash,
Convert.FromBase64String(userDto.PasswordSalt)))
{
- var token = Passwords.GenerateJwtToken(username, userDto.Id, jwtKey, pseudoDomain);
+ var token = Passwords.GenerateJwtToken(username, userDto.Id, jwtKey, pseudoDomain, userDto.IsAdmin);
return new LoginResult(true, token, false, LoginRejectReason.None);
}
}
diff --git a/PasswordKeeper.Classes/DatabaseUtilities.cs b/PasswordKeeper.Classes/DatabaseUtilities.cs
new file mode 100644
index 0000000..762f23d
--- /dev/null
+++ b/PasswordKeeper.Classes/DatabaseUtilities.cs
@@ -0,0 +1,18 @@
+namespace PasswordKeeper.Classes;
+
+///
+/// Database utilities.
+///
+public static class DatabaseUtilities
+{
+ ///
+ /// Gets the connection string for a SQLite database.
+ ///
+ /// The name of the database.
+ /// The connection string.
+ // ReSharper disable once InconsistentNaming (this is how SQLite is written)
+ public static string GetSQLiteConnectionString(string databaseName)
+ {
+ return $"Data Source=./{databaseName}.db;Pooling=False;";
+ }
+}
\ No newline at end of file
diff --git a/PasswordKeeper.Classes/PasswordKeeper.Classes.csproj b/PasswordKeeper.Classes/PasswordKeeper.Classes.csproj
index 6e17900..cb81b7a 100644
--- a/PasswordKeeper.Classes/PasswordKeeper.Classes.csproj
+++ b/PasswordKeeper.Classes/PasswordKeeper.Classes.csproj
@@ -1,11 +1,5 @@
-
- net9.0
- enable
- enable
-
-
diff --git a/PasswordKeeper.Classes/Passwords.cs b/PasswordKeeper.Classes/Passwords.cs
index d1992cb..f27cbf9 100644
--- a/PasswordKeeper.Classes/Passwords.cs
+++ b/PasswordKeeper.Classes/Passwords.cs
@@ -18,14 +18,16 @@ public static class Passwords
/// The user ID to generate the JWT token for.
/// The JWT key to use for signing the token.
/// The pseudo domain to use for the token issuer and audience.
+ /// Whether the user is an admin or not.
/// The JWT token.
- public static string GenerateJwtToken(string username, long userId, byte[] jwtKey, string pseudoDomain)
+ public static string GenerateJwtToken(string username, long userId, byte[] jwtKey, string pseudoDomain, bool isAdmin)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.NameId, userId.ToString()),
+ new Claim(ClaimTypes.Role, isAdmin ? "Admin" : "User"),
};
var key = new SymmetricSecurityKey(jwtKey);
@@ -35,7 +37,11 @@ public static string GenerateJwtToken(string username, long userId, byte[] jwtKe
issuer: pseudoDomain,
audience: pseudoDomain,
claims: claims,
+ #if DEBUG
+ expires: DateTime.Now.AddYears(1),
+ #else
expires: DateTime.Now.AddMinutes(30),
+ #endif
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
diff --git a/PasswordKeeper.DAO/PasswordKeeper.DAO.csproj b/PasswordKeeper.DAO/PasswordKeeper.DAO.csproj
index c90f988..125f219 100644
--- a/PasswordKeeper.DAO/PasswordKeeper.DAO.csproj
+++ b/PasswordKeeper.DAO/PasswordKeeper.DAO.csproj
@@ -1,11 +1,5 @@
-
-
- net9.0
- enable
- enable
-
-
+
diff --git a/PasswordKeeper.DAO/User.cs b/PasswordKeeper.DAO/User.cs
index c736bfe..3ee5790 100644
--- a/PasswordKeeper.DAO/User.cs
+++ b/PasswordKeeper.DAO/User.cs
@@ -17,6 +17,10 @@ public class User : IUser
[MaxLength(255)]
public string Username { get; set; } = string.Empty;
+ ///
+ [MaxLength(512)]
+ public string UserFullName { get; set; } = string.Empty;
+
///
[MaxLength(1000)]
public string PasswordHash { get; set; } = string.Empty;
diff --git a/PasswordKeeper.DTO/PasswordKeeper.DTO.csproj b/PasswordKeeper.DTO/PasswordKeeper.DTO.csproj
index b229e05..9fa87b2 100644
--- a/PasswordKeeper.DTO/PasswordKeeper.DTO.csproj
+++ b/PasswordKeeper.DTO/PasswordKeeper.DTO.csproj
@@ -1,11 +1,5 @@
-
- net9.0
- enable
- enable
-
-
diff --git a/PasswordKeeper.DTO/UserDto.cs b/PasswordKeeper.DTO/UserDto.cs
index 2610ea6..f96bf94 100644
--- a/PasswordKeeper.DTO/UserDto.cs
+++ b/PasswordKeeper.DTO/UserDto.cs
@@ -14,6 +14,9 @@ public class UserDto : IUser
///
public string Username { get; set; } = string.Empty;
+ ///
+ public string UserFullName { get; set; } = string.Empty;
+
///
[JsonIgnore]
public string PasswordHash { get; set; } = string.Empty;
diff --git a/PasswordKeeper.DataAccess/PasswordKeeper.DataAccess.csproj b/PasswordKeeper.DataAccess/PasswordKeeper.DataAccess.csproj
index 1eb2392..3969f7f 100644
--- a/PasswordKeeper.DataAccess/PasswordKeeper.DataAccess.csproj
+++ b/PasswordKeeper.DataAccess/PasswordKeeper.DataAccess.csproj
@@ -1,11 +1,5 @@
-
- net9.0
- enable
- enable
-
-
diff --git a/PasswordKeeper.DataAccess/Users.cs b/PasswordKeeper.DataAccess/Users.cs
index 070b324..5e10861 100644
--- a/PasswordKeeper.DataAccess/Users.cs
+++ b/PasswordKeeper.DataAccess/Users.cs
@@ -83,4 +83,35 @@ public async Task UsersExist(bool? admin = null)
return mapper.Map(user);
}
+
+ ///
+ /// Deletes the user with the given ID.
+ ///
+ /// The ID of the user to delete.
+ /// A task representing the asynchronous operation.
+ public async Task DeleteUser(long id)
+ {
+ await using var context = await dbContextFactory.CreateDbContextAsync();
+
+ var user = await context.Users.FirstOrDefaultAsync(user => user.Id == id);
+
+ if (user != null)
+ {
+ context.Users.Remove(user);
+ await context.SaveChangesAsync();
+ }
+
+ // TODO:Delete user data
+ }
+
+ ///
+ /// Gets all users.
+ ///
+ /// A collection of all users.
+ public async Task> GetAllUsers()
+ {
+ await using var context = await dbContextFactory.CreateDbContextAsync();
+
+ return mapper.Map>(await context.Users.ToListAsync());
+ }
}
\ No newline at end of file
diff --git a/PasswordKeeper.DatabaseMigrations/Migrations/001_InitialMigration.cs b/PasswordKeeper.DatabaseMigrations/Migrations/001_InitialMigration.cs
index f578f47..3bdcf92 100644
--- a/PasswordKeeper.DatabaseMigrations/Migrations/001_InitialMigration.cs
+++ b/PasswordKeeper.DatabaseMigrations/Migrations/001_InitialMigration.cs
@@ -24,6 +24,7 @@ public override void Up()
this.Create.Table(nameof(User)).InSchemaIf(Program.DatabaseName, !isSqlite)
.WithColumn(nameof(User.Id)).AsInt64().NotNullable().PrimaryKey().Identity()
.WithColumn(nameof(User.Username)).AsString(255).NotNullable().Unique()
+ .WithColumn(nameof(User.UserFullName)).AsString(512).NotNullable()
.WithColumn(nameof(User.PasswordHash)).AsString(1000).NotNullable()
.WithColumn(nameof(User.PasswordSalt)).AsString(1000).NotNullable()
.WithColumn(nameof(User.IsAdmin)).AsBoolean().NotNullable().WithDefaultValue(false);
diff --git a/PasswordKeeper.DatabaseMigrations/PasswordKeeper.DatabaseMigrations.csproj b/PasswordKeeper.DatabaseMigrations/PasswordKeeper.DatabaseMigrations.csproj
index 7607de5..422a2e4 100644
--- a/PasswordKeeper.DatabaseMigrations/PasswordKeeper.DatabaseMigrations.csproj
+++ b/PasswordKeeper.DatabaseMigrations/PasswordKeeper.DatabaseMigrations.csproj
@@ -2,9 +2,6 @@
Exe
- net9.0
- enable
- enable
@@ -18,6 +15,7 @@
+
diff --git a/PasswordKeeper.DatabaseMigrations/Program.cs b/PasswordKeeper.DatabaseMigrations/Program.cs
index 5e8d164..6e9d1e6 100644
--- a/PasswordKeeper.DatabaseMigrations/Program.cs
+++ b/PasswordKeeper.DatabaseMigrations/Program.cs
@@ -2,6 +2,8 @@
using FluentMigrator.Runner;
using Microsoft.Extensions.DependencyInjection;
using McMaster.Extensions.CommandLineUtils;
+using PasswordKeeper.Classes;
+
// ReSharper disable MemberCanBePrivate.Global
namespace PasswordKeeper.DatabaseMigrations;
@@ -51,7 +53,7 @@ public void OnExecute()
if (TestDbName != null)
{
// NOTE: Pooling=False is required for SQLite for the database file to be released after migrations!
- connectionString = $"Data Source=./{TestDbName}.db;Pooling=False;";
+ connectionString = DatabaseUtilities.GetSQLiteConnectionString(TestDbName);
IsTestDb = true;
}
else
diff --git a/PasswordKeeper.Interfaces/IUser.cs b/PasswordKeeper.Interfaces/IUser.cs
index f5e7df7..d703a8e 100644
--- a/PasswordKeeper.Interfaces/IUser.cs
+++ b/PasswordKeeper.Interfaces/IUser.cs
@@ -10,6 +10,11 @@ public interface IUser : IHasId
///
string Username { get; set; }
+ ///
+ /// The full name of the user.
+ ///
+ string UserFullName { get; set; }
+
///
/// The password of the user.
///
diff --git a/PasswordKeeper.Interfaces/PasswordKeeper.Interfaces.csproj b/PasswordKeeper.Interfaces/PasswordKeeper.Interfaces.csproj
index 17b910f..c632161 100644
--- a/PasswordKeeper.Interfaces/PasswordKeeper.Interfaces.csproj
+++ b/PasswordKeeper.Interfaces/PasswordKeeper.Interfaces.csproj
@@ -1,9 +1,3 @@
-
- net9.0
- enable
- enable
-
-
diff --git a/PasswordKeeper.Server/Controllers/AliveController.cs b/PasswordKeeper.Server/Controllers/AliveController.cs
new file mode 100644
index 0000000..a0a9c11
--- /dev/null
+++ b/PasswordKeeper.Server/Controllers/AliveController.cs
@@ -0,0 +1,22 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace PasswordKeeper.Server.Controllers;
+
+///
+/// The alive controller.
+///
+public class AliveController : ControllerBase
+{
+ ///
+ /// Gets the current date and time.S
+ ///
+ /// The current date and time.
+ [Route("")]
+ [AllowAnonymous]
+ [HttpGet]
+ public DateTimeOffset Get()
+ {
+ return DateTimeOffset.UtcNow;
+ }
+}
\ No newline at end of file
diff --git a/PasswordKeeper.Server/Controllers/AuthenticationController.cs b/PasswordKeeper.Server/Controllers/AuthenticationController.cs
index d80c528..f387fd4 100644
--- a/PasswordKeeper.Server/Controllers/AuthenticationController.cs
+++ b/PasswordKeeper.Server/Controllers/AuthenticationController.cs
@@ -1,9 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PasswordKeeper.BusinessLogic;
-using PasswordKeeper.Classes;
-using PasswordKeeper.DTO;
-using PasswordKeeper.Server.Controllers.Extensions;
namespace PasswordKeeper.Server.Controllers;
@@ -20,14 +17,6 @@ public class AuthenticationController(Users users) : ControllerBase
/// The username for the login.
/// The password for the login.
public record UserLogin(string Username, string Password);
-
- ///
- /// User change data.
- ///
- /// The ID of the user to change.
- /// The new username for the user.
- /// The new password for the user.
- public record UserChangeRequest(int UserId, string Username, string Password);
///
/// Logs the user in and returns a JWT token.
@@ -52,150 +41,24 @@ public async Task Login([FromBody] UserLogin user)
return Unauthorized();
}
-
-
- ///
- /// Checks a password against the password complexity requirements.
- ///
- /// The password to check.
- /// Ok if the password meets the complexity requirements, otherwise BadRequest with an error message.
- [Route(nameof(PasswordOk))]
- [HttpPost]
- public IActionResult PasswordOk([FromBody] string password)
- {
- var result = Passwords.IsPasswordOk(password, out var message, out _);
- return result ? Ok() : BadRequest(message);
- }
-
- ///
- /// Updates the user's password if the requester is authorized and the password meets the complexity requirements.
- ///
- /// The user change request containing the user ID and new password.
- ///
- /// Unauthorized if the requester is not the user or admin, BadRequest if the password is invalid or the upsert operation fails,
- /// NotFound if the user does not exist, otherwise Ok with the updated user data.
- ///
- [Route(nameof(UpdateUserPassword))]
- [HttpPost]
- public async Task UpdateUserPassword([FromBody] UserChangeRequest user)
- {
- var userDto = await users.GetUserById(user.UserId);
-
- if (userDto is null)
- {
- return NotFound();
- }
-
- if (this.GetLoggedUserId() != user.UserId || !userDto.IsAdmin)
- {
- return Unauthorized();
- }
-
- if (!Passwords.IsPasswordOk(user.Password, out var message, out _))
- {
- return BadRequest(message);
- }
-
- var salt = Convert.FromBase64String(userDto.PasswordSalt);
- userDto.PasswordHash = Users.HashPassword(user.Password, ref salt);
- userDto.PasswordSalt = Convert.ToBase64String(salt!);
- var result = await users.UpsertUser(userDto);
-
- return result is null ? BadRequest() : Ok(result);
- }
///
- /// Updates the user's username if the requester is authorized.
- ///
- /// The user change request containing the user ID and new username.
- ///
- /// Unauthorized if the requester is not the user or admin, NotFound if the user does not exist,
- /// otherwise BadRequest if the upsert operation fails, or Ok with the updated user data.
- ///
- public async Task UpdateUserName([FromBody] UserChangeRequest user)
- {
- var userDto = await users.GetUserById(user.UserId);
-
- if (userDto is null)
- {
- return NotFound();
- }
-
- if (this.GetLoggedUserId() != user.UserId || !userDto.IsAdmin)
- {
- return Unauthorized();
- }
-
- if (user.Username.Length < 4)
- {
- return BadRequest(Passwords.UsernameMustBeAtLeast4CharactersLong);
- }
-
- userDto.Username = user.Username;
- var result = await users.UpsertUser(userDto);
-
- return result is null ? BadRequest() : Ok(result);
- }
-
- ///
- /// Creates a new user if the requester is admin.
+ /// An endpoint for testing unauthorized access.
///
- /// The user data to create.
- ///
- /// Unauthorized if the requester is not admin, BadRequest if the upsert operation fails,
- /// otherwise Ok with the created user data.
- ///
- public async Task CreateUser([FromBody] UserChangeRequest user)
+ /// An Ok result if the request is authorized.
+ [Route(nameof(TestUnauthorized))]
+ public IActionResult TestUnauthorized()
{
- var loggedUser = await users.GetUserById(this.GetLoggedUserId());
-
- if (await users.GetUserByName(user.Username) is not null)
- {
- return BadRequest(Passwords.UserAlreadyExists);
- }
-
- if (loggedUser == null)
- {
- return Unauthorized();
- }
-
- if (loggedUser.IsAdmin)
- {
- if (!Passwords.IsPasswordOk(user.Password, out var message, out _))
- {
- return BadRequest(message);
- }
-
- if (user.Username.Length < 4)
- {
- return BadRequest(Passwords.UsernameMustBeAtLeast4CharactersLong);
- }
-
- var userDto = new UserDto
- {
- Username = user.Username,
- PasswordHash = string.Empty,
- PasswordSalt = string.Empty,
- IsAdmin = false,
- };
-
- var salt = Convert.FromBase64String(userDto.PasswordSalt);
- userDto.PasswordHash = Users.HashPassword(user.Password, ref salt);
- userDto.PasswordSalt = Convert.ToBase64String(salt!);
- var result = await users.UpsertUser(userDto);
-
- return result is null ? BadRequest() : Ok(result);
- }
-
- return Unauthorized();
+ return Ok();
}
///
- /// An endpoint for testing unauthorized access.
+ /// An endpoint for testing admin access.
///
/// An Ok result if the request is authorized.
- [Route(nameof(TestUnauthorized))]
- public IActionResult TestUnauthorized()
+ [Route(nameof(TestAdminAuthorized))]
+ [Authorize(Roles = "Admin")]
+ public IActionResult TestAdminAuthorized()
{
return Ok();
}
diff --git a/PasswordKeeper.Server/Controllers/UsersController.cs b/PasswordKeeper.Server/Controllers/UsersController.cs
new file mode 100644
index 0000000..40604d3
--- /dev/null
+++ b/PasswordKeeper.Server/Controllers/UsersController.cs
@@ -0,0 +1,209 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using PasswordKeeper.BusinessLogic;
+using PasswordKeeper.Classes;
+using PasswordKeeper.DTO;
+using PasswordKeeper.Server.Controllers.Extensions;
+
+namespace PasswordKeeper.Server.Controllers;
+
+///
+/// The users controller.
+///
+[ApiController]
+[Route("api/[controller]")]
+public class UsersController(Users users) : ControllerBase
+{
+ ///
+ /// User change data.
+ ///
+ /// The ID of the user to change.
+ /// The new username for the user.
+ /// The new password for the user.
+ /// The new full name for the user.
+ public record UserChangeRequest(int UserId, string Username, string Password, string UserFullName);
+
+ ///
+ /// Creates a new user if the requester is admin.
+ ///
+ /// The user data to create.
+ ///
+ /// Unauthorized if the requester is not admin, BadRequest if the upsert operation fails,
+ /// otherwise Ok with the created user data.
+ ///
+ [HttpPost]
+ [Route(nameof(CreateUser))]
+ [Authorize(Roles = "Admin")]
+ public async Task CreateUser([FromBody] UserChangeRequest user)
+ {
+ var loggedUser = await users.GetUserById(this.GetLoggedUserId());
+
+ if (await users.GetUserByName(user.Username) is not null)
+ {
+ return BadRequest(Passwords.UserAlreadyExists);
+ }
+
+ if (loggedUser == null)
+ {
+ return Unauthorized();
+ }
+
+ if (loggedUser.IsAdmin)
+ {
+ if (!Passwords.IsPasswordOk(user.Password, out var message, out _))
+ {
+ return BadRequest(message);
+ }
+
+ if (user.Username.Length < 4)
+ {
+ return BadRequest(Passwords.UsernameMustBeAtLeast4CharactersLong);
+ }
+
+ var userDto = new UserDto
+ {
+ Username = user.Username,
+ PasswordHash = string.Empty,
+ PasswordSalt = string.Empty,
+ IsAdmin = false,
+ UserFullName = user.UserFullName,
+ };
+
+ var salt = string.IsNullOrEmpty(userDto.PasswordSalt) ? null : Convert.FromBase64String(userDto.PasswordSalt);
+ userDto.PasswordHash = Users.HashPassword(user.Password, ref salt);
+ userDto.PasswordSalt = Convert.ToBase64String(salt!);
+ var result = await users.UpsertUser(userDto);
+
+ return result is null ? BadRequest() : Ok(result);
+ }
+
+ return Unauthorized();
+ }
+
+ ///
+ /// Updates the user's password if the requester is authorized and the password meets the complexity requirements.
+ ///
+ /// The user change request containing the user ID and new password.
+ ///
+ /// Unauthorized if the requester is not the user or admin, BadRequest if the password is invalid or the upsert operation fails,
+ /// NotFound if the user does not exist, otherwise Ok with the updated user data.
+ ///
+ [Route(nameof(UpdateUserPassword))]
+ [HttpPost]
+ public async Task UpdateUserPassword([FromBody] UserChangeRequest user)
+ {
+ var userDto = await users.GetUserById(user.UserId);
+
+ if (userDto is null)
+ {
+ return NotFound();
+ }
+
+ if (this.GetLoggedUserId() != user.UserId || !userDto.IsAdmin)
+ {
+ return Unauthorized();
+ }
+
+ if (!Passwords.IsPasswordOk(user.Password, out var message, out _))
+ {
+ return BadRequest(message);
+ }
+
+ var salt = Convert.FromBase64String(userDto.PasswordSalt);
+ userDto.PasswordHash = Users.HashPassword(user.Password, ref salt);
+ userDto.PasswordSalt = Convert.ToBase64String(salt!);
+ var result = await users.UpsertUser(userDto);
+
+ return result is null ? BadRequest() : Ok(result);
+ }
+
+ ///
+ /// Updates the user's username and full name if the requester is authorized.
+ ///
+ /// The user change request containing the user ID, new username and new full name.
+ ///
+ /// Unauthorized if the requester is not the user or admin, NotFound if the user does not exist,
+ /// otherwise BadRequest if the upsert operation fails, or Ok with the updated user data.
+ ///
+ public async Task UpdateUserName([FromBody] UserChangeRequest user)
+ {
+ var userDto = await users.GetUserById(user.UserId);
+
+ if (userDto is null)
+ {
+ return NotFound();
+ }
+
+ if (this.GetLoggedUserId() != user.UserId || !userDto.IsAdmin)
+ {
+ return Unauthorized();
+ }
+
+ if (user.Username.Length < 4)
+ {
+ return BadRequest(Passwords.UsernameMustBeAtLeast4CharactersLong);
+ }
+
+ userDto.Username = user.Username;
+ userDto.UserFullName = user.UserFullName;
+ var result = await users.UpsertUser(userDto);
+
+ return result is null ? BadRequest() : Ok(result);
+ }
+
+ ///
+ /// Deletes the user with the given ID if the requester is admin.
+ ///
+ /// The ID of the user to delete.
+ /// Unauthorized if the requester is not admin, NotFound if the user does not exist, otherwise Ok.
+ [Route(nameof(DeleteUser))]
+ [HttpDelete]
+ public async Task DeleteUser(int userId)
+ {
+ var userDto = await users.GetUserById(userId);
+
+ if (userDto is null)
+ {
+ return NotFound();
+ }
+
+ var loggedUserId = this.GetLoggedUserId();
+ var isLoggedUserAdmin = await users.GetUserById(loggedUserId) is { IsAdmin: true };
+
+ // Only admins can delete users and admins cannot delete themselves
+ if (!isLoggedUserAdmin || loggedUserId == userId)
+ {
+ return Unauthorized();
+ }
+
+ await users.DeleteUser(userId);
+
+ return Ok();
+ }
+
+ ///
+ /// Gets all users.
+ ///
+ /// A collection of all users.
+ [Route(nameof(GetAllUsers))]
+ [HttpGet]
+ [Authorize(Roles = "Admin")]
+ public async Task> GetAllUsers()
+ {
+
+ return await users.GetAllUsers();
+ }
+
+ ///
+ /// Checks a password against the password complexity requirements.
+ ///
+ /// The password to check.
+ /// Ok if the password meets the complexity requirements, otherwise BadRequest with an error message.
+ [Route(nameof(PasswordOk))]
+ [HttpPost]
+ public IActionResult PasswordOk([FromBody] string password)
+ {
+ var result = Passwords.IsPasswordOk(password, out var message, out _);
+ return result ? Ok() : BadRequest(message);
+ }
+}
\ No newline at end of file
diff --git a/PasswordKeeper.Server/PasswordKeeper.Server.csproj b/PasswordKeeper.Server/PasswordKeeper.Server.csproj
index 88c945b..9009082 100644
--- a/PasswordKeeper.Server/PasswordKeeper.Server.csproj
+++ b/PasswordKeeper.Server/PasswordKeeper.Server.csproj
@@ -1,9 +1,6 @@
- net9.0
- enable
- enable
Linux
diff --git a/PasswordKeeper.Server/Program.cs b/PasswordKeeper.Server/Program.cs
index e90ea18..6ac0750 100644
--- a/PasswordKeeper.Server/Program.cs
+++ b/PasswordKeeper.Server/Program.cs
@@ -1,4 +1,6 @@
+using System.Security.Claims;
using System.Security.Cryptography;
+using System.Text.Json;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
@@ -42,6 +44,7 @@ public static void Main(string[] args)
ValidIssuer = Program.PseudoDomain,
ValidAudience = Program.PseudoDomain,
IssuerSigningKey = new SymmetricSecurityKey(GetJwtKey()),
+ RoleClaimType = ClaimTypes.Role,
};
});
diff --git a/PasswordKeeper.Tests/ControllerTests.cs b/PasswordKeeper.Tests/ControllerTests.cs
index 87eee0c..4dc3d86 100644
--- a/PasswordKeeper.Tests/ControllerTests.cs
+++ b/PasswordKeeper.Tests/ControllerTests.cs
@@ -60,13 +60,14 @@ public async Task ControllerCreateUserTest()
var dbContextFactory = Helpers.GetMockDbContextFactory(nameof(ControllerTests));
var dataAccess = new PasswordKeeper.DataAccess.Users(dbContextFactory, Helpers.CreateMapper());
var businessLogic = new PasswordKeeper.BusinessLogic.Users(dataAccess);
- var controller = new AuthenticationController(businessLogic);
+ var authenticationController = new AuthenticationController(businessLogic);
+ var usersController = new UsersController(businessLogic);
var loginData = new AuthenticationController.UserLogin("firsUserIsAdmin", "Pa1sword%");
- await controller.Login(loginData);
+ await authenticationController.Login(loginData);
- var user = new AuthenticationController.UserChangeRequest(0, "normalUser", "pAssw0rd_");
+ var user = new UsersController.UserChangeRequest(0, "normalUser", "pAssw0rd_", "Normal User");
- var createdUser = await controller.CreateUser(user);
+ var createdUser = await usersController.CreateUser(user);
Assert.That(createdUser, Is.TypeOf());
var userDto = ((OkObjectResult)createdUser).Value as UserDto;
diff --git a/PasswordKeeper.Tests/Helpers.cs b/PasswordKeeper.Tests/Helpers.cs
index e9e6c85..0827126 100644
--- a/PasswordKeeper.Tests/Helpers.cs
+++ b/PasswordKeeper.Tests/Helpers.cs
@@ -1,6 +1,5 @@
using System.Security.Cryptography;
using AutoMapper;
-using Microsoft.EntityFrameworkCore;
using PasswordKeeper.DAO;
using PasswordKeeper.DataAccess;
@@ -11,20 +10,6 @@ namespace PasswordKeeper.Tests;
///
public static class Helpers
{
- ///
- /// Creates a new SQLite database context.
- ///
- /// The name of the test class.
- /// The database context.
- public static Entities GetMemoryContext(string testClassName)
- {
- var options = new DbContextOptionsBuilder()
- .UseSqlite($"Data Source=./{testClassName}.db;Pooling=False;")
- .Options;
-
- return new Entities(options);
- }
-
///
/// Creates a new SQLite database context factory.
///
diff --git a/PasswordKeeper.Tests/MockDbContextFactory.cs b/PasswordKeeper.Tests/MockDbContextFactory.cs
index 6729fe4..a17e828 100644
--- a/PasswordKeeper.Tests/MockDbContextFactory.cs
+++ b/PasswordKeeper.Tests/MockDbContextFactory.cs
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
+using PasswordKeeper.Classes;
using PasswordKeeper.DAO;
namespace PasswordKeeper.Tests;
@@ -19,7 +20,7 @@ public class MockDbContextFactory(string testClassName) : IDisposableContextFact
public Entities CreateDbContext()
{
var options = new DbContextOptionsBuilder()
- .UseSqlite($"Data Source=./{testClassName}.db;Pooling=False;")
+ .UseSqlite(DatabaseUtilities.GetSQLiteConnectionString(testClassName))
.Options;
context = new Entities(options);
diff --git a/PasswordKeeper.Tests/PasswordKeeper.Tests.csproj b/PasswordKeeper.Tests/PasswordKeeper.Tests.csproj
index 80dc29f..1093705 100644
--- a/PasswordKeeper.Tests/PasswordKeeper.Tests.csproj
+++ b/PasswordKeeper.Tests/PasswordKeeper.Tests.csproj
@@ -1,10 +1,6 @@
- net9.0
- enable
- enable
-
false
true