Commit MWS backend source tree

The ASP.NET Core solution (mws.api/mws.application/mws.domain/
mws.infrastructure) existed only as untracked working files. Adding
it to version control, plus a .gitignore for build output and local
tooling directories, so the CQRS/mediator refactor plan has a real
git history to branch and diff against.
This commit is contained in:
2026-08-13 23:10:22 +07:00
parent 96944ad2c3
commit f72aaa2329
94 changed files with 7758 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
using AutoMapper;
using Mws.Application.Auth;
using Mws.Application.Common;
using Mws.Application.Permissions;
using Mws.Domain.Users;
namespace Mws.Application.Accounts;
public class AccountService(IUnitOfWork uow, IPasswordHasher passwordHasher, IPermissionService permissions, IMapper mapper) : IAccountService
{
private const string Screen = "accounts";
public async Task<List<AccountDto>> GetAccountsAsync(Guid actorUserId, string? term, CancellationToken ct = default)
{
await permissions.EnsureAsync(actorUserId, Screen, PermissionAction.View, ct);
var users = await uow.Users.SearchWithRoleAsync(term, null, ct);
return mapper.Map<List<AccountDto>>(users);
}
public async Task<AccountDto> CreateAccountAsync(Guid actorUserId, CreateAccountRequest request, CancellationToken ct = default)
{
await permissions.EnsureAsync(actorUserId, Screen, PermissionAction.Create, ct);
var username = request.Username.Trim();
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(request.Password))
{
throw new BadRequestException("Username and password are required");
}
if (await uow.Users.ExistsByUsernameAsync(username, ct))
{
throw new BadRequestException("Username already exists");
}
var role = await uow.Roles.GetByIdAsync(request.RoleId, ct)
?? throw new BadRequestException("Role not found");
var now = DateTime.UtcNow;
var user = new User
{
Id = Guid.NewGuid(),
Username = username,
PasswordHash = passwordHasher.Hash(request.Password),
DisplayName = request.DisplayName.Trim(),
RoleId = role.Id,
IsActive = true,
CreatedAt = now,
UpdatedAt = now,
};
uow.Users.Add(user);
await uow.SaveChangesAsync(ct);
user.Role = role;
return mapper.Map<AccountDto>(user);
}
public async Task<AccountDto> UpdateAccountAsync(Guid actorUserId, Guid id, UpdateAccountRequest request, CancellationToken ct = default)
{
await permissions.EnsureAsync(actorUserId, Screen, PermissionAction.Edit, ct);
var user = await uow.Users.GetByIdWithRoleAsync(id, ct)
?? throw new NotFoundException("Account not found");
var role = await uow.Roles.GetByIdAsync(request.RoleId, ct)
?? throw new BadRequestException("Role not found");
user.DisplayName = request.DisplayName.Trim();
user.RoleId = role.Id;
user.Role = role;
user.IsActive = request.IsActive;
user.UpdatedAt = DateTime.UtcNow;
await uow.SaveChangesAsync(ct);
return mapper.Map<AccountDto>(user);
}
public async Task DeleteAccountAsync(Guid actorUserId, Guid id, CancellationToken ct = default)
{
await permissions.EnsureAsync(actorUserId, Screen, PermissionAction.Delete, ct);
var user = await uow.Users.GetByIdAsync(id, ct)
?? throw new NotFoundException("Account not found");
var soleOwnerProjectIds = await uow.Projects.GetOwnedProjectIdsAsync(id, ct);
foreach (var projectId in soleOwnerProjectIds)
{
var ownerCount = await uow.Projects.CountOwnersAsync(projectId, ct);
if (ownerCount <= 1)
{
throw new BadRequestException("Cannot delete an account that is the sole owner of a project");
}
}
uow.Users.Remove(user);
await uow.SaveChangesAsync(ct);
}
public async Task ResetPasswordAsync(Guid actorUserId, Guid id, ResetPasswordRequest request, CancellationToken ct = default)
{
await permissions.EnsureAsync(actorUserId, Screen, PermissionAction.Edit, ct);
if (string.IsNullOrWhiteSpace(request.NewPassword))
{
throw new BadRequestException("New password is required");
}
var user = await uow.Users.GetByIdAsync(id, ct)
?? throw new NotFoundException("Account not found");
user.PasswordHash = passwordHasher.Hash(request.NewPassword);
user.UpdatedAt = DateTime.UtcNow;
await uow.SaveChangesAsync(ct);
}
}
+32
View File
@@ -0,0 +1,32 @@
namespace Mws.Application.Accounts;
public class CreateAccountRequest
{
public string Username { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public Guid RoleId { get; set; }
}
public class UpdateAccountRequest
{
public string DisplayName { get; set; } = string.Empty;
public Guid RoleId { get; set; }
public bool IsActive { get; set; } = true;
}
public class ResetPasswordRequest
{
public string NewPassword { get; set; } = string.Empty;
}
public class AccountDto
{
public Guid Id { get; set; }
public string Username { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public Guid RoleId { get; set; }
public string RoleName { get; set; } = string.Empty;
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
}
@@ -0,0 +1,10 @@
namespace Mws.Application.Accounts;
public interface IAccountService
{
Task<List<AccountDto>> GetAccountsAsync(Guid actorUserId, string? term, CancellationToken ct = default);
Task<AccountDto> CreateAccountAsync(Guid actorUserId, CreateAccountRequest request, CancellationToken ct = default);
Task<AccountDto> UpdateAccountAsync(Guid actorUserId, Guid id, UpdateAccountRequest request, CancellationToken ct = default);
Task DeleteAccountAsync(Guid actorUserId, Guid id, CancellationToken ct = default);
Task ResetPasswordAsync(Guid actorUserId, Guid id, ResetPasswordRequest request, CancellationToken ct = default);
}