feat: add Logs
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.IdentityModel.JsonWebTokens;
|
||||||
|
using mws.backend.dotnet.application.Common;
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.api.Authentication;
|
||||||
|
|
||||||
|
public class HttpCurrentUser(IHttpContextAccessor accessor) : ICurrentUser
|
||||||
|
{
|
||||||
|
public Guid? UserId
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var value = accessor.HttpContext?.User.FindFirstValue(JwtRegisteredClaimNames.Sub)
|
||||||
|
?? accessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||||
|
?? accessor.HttpContext?.User.FindFirstValue("sub");
|
||||||
|
return value is not null && Guid.TryParse(value, out var id) ? id : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Username =>
|
||||||
|
accessor.HttpContext?.User.FindFirstValue(ClaimTypes.Name)
|
||||||
|
?? accessor.HttpContext?.User.FindFirstValue("name");
|
||||||
|
|
||||||
|
public string? IpAddress
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var context = accessor.HttpContext;
|
||||||
|
if (context is null) return null;
|
||||||
|
var forwarded = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
|
||||||
|
if (!string.IsNullOrWhiteSpace(forwarded))
|
||||||
|
{
|
||||||
|
var first = forwarded.Split(',')[0].Trim();
|
||||||
|
if (first.Length > 0) return Truncate(first, 45);
|
||||||
|
}
|
||||||
|
return Truncate(context.Connection.RemoteIpAddress?.ToString(), 45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? UserAgent =>
|
||||||
|
Truncate(accessor.HttpContext?.Request.Headers.UserAgent.ToString(), 512);
|
||||||
|
|
||||||
|
private static string? Truncate(string? value, int max) =>
|
||||||
|
string.IsNullOrEmpty(value) ? value : (value.Length <= max ? value : value[..max]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
|
using mws.backend.dotnet.application.Common;
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/audit")]
|
||||||
|
[Authorize]
|
||||||
|
public class AuditController(ISender sender) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("actions")]
|
||||||
|
public async Task<ActionResult<PagedResult<ActionLogDto>>> GetActions(
|
||||||
|
[FromQuery] DateTimeOffset? from,
|
||||||
|
[FromQuery] DateTimeOffset? to,
|
||||||
|
[FromQuery] string? actor,
|
||||||
|
[FromQuery] string? action,
|
||||||
|
[FromQuery] string? entityType,
|
||||||
|
[FromQuery] Guid? entityId,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 20,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var filter = new ActionLogFilter(from, to, actor, action, entityType, entityId);
|
||||||
|
return Ok(await sender.Send(new GetActionLogsQuery(User.GetUserId(), filter, page, pageSize), ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("logins")]
|
||||||
|
public async Task<ActionResult<PagedResult<LoginLogDto>>> GetLogins(
|
||||||
|
[FromQuery] DateTimeOffset? from,
|
||||||
|
[FromQuery] DateTimeOffset? to,
|
||||||
|
[FromQuery] string? username,
|
||||||
|
[FromQuery] bool? success,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 20,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var filter = new LoginLogFilter(from, to, username, success);
|
||||||
|
return Ok(await sender.Send(new GetLoginLogsQuery(User.GetUserId(), filter, page, pageSize), ct));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Permissions;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
using mws.backend.dotnet.application.Users;
|
using mws.backend.dotnet.application.Users;
|
||||||
using mws.backend.dotnet.domain.Users;
|
|
||||||
using mws.backend.dotnet.infrastructure.Persistence;
|
using mws.backend.dotnet.infrastructure.Persistence;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.api.Controllers;
|
namespace mws.backend.dotnet.api.Controllers;
|
||||||
@@ -95,32 +94,14 @@ public class UsersController(ISender sender, AppDbContext db, IPermissionService
|
|||||||
[HttpPost("{id:guid}/roles")]
|
[HttpPost("{id:guid}/roles")]
|
||||||
public async Task<IActionResult> AssignRole(Guid id, [FromBody] AssignRoleRequest req, CancellationToken ct)
|
public async Task<IActionResult> AssignRole(Guid id, [FromBody] AssignRoleRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.Edit, ct);
|
await sender.Send(new AssignUserRoleCommand(User.GetUserId(), id, req.RoleId), ct);
|
||||||
|
|
||||||
var user = await db.Users.Include(u => u.UserRoles).FirstOrDefaultAsync(u => u.Id == id, ct);
|
|
||||||
if (user is null) return NotFound("User not found");
|
|
||||||
|
|
||||||
if (!user.UserRoles.Any(ur => ur.RoleId == req.RoleId))
|
|
||||||
{
|
|
||||||
user.UserRoles.Add(new UserRole { UserId = id, RoleId = req.RoleId });
|
|
||||||
await db.SaveChangesAsync(ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}/roles/{roleId:guid}")]
|
[HttpDelete("{id:guid}/roles/{roleId:guid}")]
|
||||||
public async Task<IActionResult> UnassignRole(Guid id, Guid roleId, CancellationToken ct)
|
public async Task<IActionResult> UnassignRole(Guid id, Guid roleId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.Edit, ct);
|
await sender.Send(new UnassignUserRoleCommand(User.GetUserId(), id, roleId), ct);
|
||||||
|
|
||||||
var ur = await db.UserRoles.FirstOrDefaultAsync(x => x.UserId == id && x.RoleId == roleId, ct);
|
|
||||||
if (ur != null)
|
|
||||||
{
|
|
||||||
db.UserRoles.Remove(ur);
|
|
||||||
await db.SaveChangesAsync(ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Microsoft.OpenApi;
|
using Microsoft.OpenApi;
|
||||||
|
using mws.backend.dotnet.api.Authentication;
|
||||||
using mws.backend.dotnet.api.Middleware;
|
using mws.backend.dotnet.api.Middleware;
|
||||||
using mws.backend.dotnet.application.Auth;
|
using mws.backend.dotnet.application.Auth;
|
||||||
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.infrastructure;
|
using mws.backend.dotnet.infrastructure;
|
||||||
using mws.backend.dotnet.infrastructure.Persistence;
|
using mws.backend.dotnet.infrastructure.Persistence;
|
||||||
|
|
||||||
@@ -46,6 +48,8 @@ builder.Services.AddSwaggerGen(options =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddInfrastructure(builder.Configuration);
|
builder.Services.AddInfrastructure(builder.Configuration);
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
builder.Services.AddScoped<ICurrentUser, HttpCurrentUser>();
|
||||||
|
|
||||||
var jwtSection = builder.Configuration.GetSection("Jwt");
|
var jwtSection = builder.Configuration.GetSection("Jwt");
|
||||||
var jwtSecret = jwtSection["Secret"];
|
var jwtSecret = jwtSection["Secret"];
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace mws.backend.dotnet.application.Audit;
|
||||||
|
|
||||||
|
public class AuditContext : IAuditContext
|
||||||
|
{
|
||||||
|
public Guid? EntityId { get; set; }
|
||||||
|
public string? EntityName { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Common;
|
||||||
|
using mws.backend.dotnet.domain.Audit;
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.application.Audit;
|
||||||
|
|
||||||
|
public class AuditLogBehavior<TRequest, TResponse>(
|
||||||
|
IUnitOfWork uow,
|
||||||
|
ICurrentUser currentUser,
|
||||||
|
IAuditContext auditContext)
|
||||||
|
: IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
|
||||||
|
{
|
||||||
|
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (request is not IAuditableRequest auditable)
|
||||||
|
{
|
||||||
|
return await next();
|
||||||
|
}
|
||||||
|
|
||||||
|
auditContext.EntityId = null;
|
||||||
|
auditContext.EntityName = null;
|
||||||
|
|
||||||
|
var response = await next();
|
||||||
|
|
||||||
|
uow.ActionLogs.Add(new ActionLog
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
ActorUserId = currentUser.UserId ?? Guid.Empty,
|
||||||
|
ActorUsername = TruncateRequired(currentUser.Username ?? "unknown", 100),
|
||||||
|
Action = auditable.Action,
|
||||||
|
EntityType = auditable.EntityType,
|
||||||
|
EntityId = auditable.EntityId ?? auditContext.EntityId ?? (response as IHasEntityId)?.EntityId,
|
||||||
|
EntityName = Truncate(auditable.EntityName ?? auditContext.EntityName, 255),
|
||||||
|
IpAddress = currentUser.IpAddress,
|
||||||
|
UserAgent = currentUser.UserAgent,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
});
|
||||||
|
await uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TruncateRequired(string value, int max) =>
|
||||||
|
value.Length <= max ? value : value[..max];
|
||||||
|
|
||||||
|
private static string? Truncate(string? value, int max) =>
|
||||||
|
value is null || value.Length <= max ? value : value[..max];
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
namespace mws.backend.dotnet.application.Audit;
|
||||||
|
|
||||||
|
public record ActionLogFilter(
|
||||||
|
DateTimeOffset? From,
|
||||||
|
DateTimeOffset? To,
|
||||||
|
string? Actor,
|
||||||
|
string? Action,
|
||||||
|
string? EntityType,
|
||||||
|
Guid? EntityId);
|
||||||
|
|
||||||
|
public record LoginLogFilter(
|
||||||
|
DateTimeOffset? From,
|
||||||
|
DateTimeOffset? To,
|
||||||
|
string? Username,
|
||||||
|
bool? Success);
|
||||||
|
|
||||||
|
public class ActionLogDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid ActorUserId { get; set; }
|
||||||
|
public string ActorUsername { get; set; } = string.Empty;
|
||||||
|
public string Action { get; set; } = string.Empty;
|
||||||
|
public string EntityType { get; set; } = string.Empty;
|
||||||
|
public Guid? EntityId { get; set; }
|
||||||
|
public string? EntityName { get; set; }
|
||||||
|
public string? IpAddress { get; set; }
|
||||||
|
public string? UserAgent { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LoginLogDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public Guid? UserId { get; set; }
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? FailureReason { get; set; }
|
||||||
|
public string? IpAddress { get; set; }
|
||||||
|
public string? UserAgent { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace mws.backend.dotnet.application.Audit;
|
||||||
|
|
||||||
|
public interface IAuditContext
|
||||||
|
{
|
||||||
|
Guid? EntityId { get; set; }
|
||||||
|
string? EntityName { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace mws.backend.dotnet.application.Audit;
|
||||||
|
|
||||||
|
public interface IAuditableRequest
|
||||||
|
{
|
||||||
|
string Action { get; }
|
||||||
|
string EntityType { get; }
|
||||||
|
Guid? EntityId => null;
|
||||||
|
string? EntityName => null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace mws.backend.dotnet.application.Audit;
|
||||||
|
|
||||||
|
public interface IHasEntityId
|
||||||
|
{
|
||||||
|
Guid EntityId { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Common;
|
||||||
|
using mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.application.Audit;
|
||||||
|
|
||||||
|
public record GetActionLogsQuery(Guid ActorUserId, ActionLogFilter Filter, int Page, int PageSize)
|
||||||
|
: IRequest<PagedResult<ActionLogDto>>;
|
||||||
|
|
||||||
|
public class GetActionLogsHandler(IUnitOfWork uow, IPermissionService permissions)
|
||||||
|
: IRequestHandler<GetActionLogsQuery, PagedResult<ActionLogDto>>
|
||||||
|
{
|
||||||
|
public async Task<PagedResult<ActionLogDto>> Handle(GetActionLogsQuery query, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await permissions.EnsureAsync(query.ActorUserId, "actionlogs", PermissionAction.View, ct);
|
||||||
|
|
||||||
|
var page = await uow.ActionLogs.SearchAsync(query.Filter, query.Page, query.PageSize, ct);
|
||||||
|
return new PagedResult<ActionLogDto>
|
||||||
|
{
|
||||||
|
Items = page.Items.Select(x => new ActionLogDto
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
ActorUserId = x.ActorUserId,
|
||||||
|
ActorUsername = x.ActorUsername,
|
||||||
|
Action = x.Action,
|
||||||
|
EntityType = x.EntityType,
|
||||||
|
EntityId = x.EntityId,
|
||||||
|
EntityName = x.EntityName,
|
||||||
|
IpAddress = x.IpAddress,
|
||||||
|
UserAgent = x.UserAgent,
|
||||||
|
CreatedAt = x.CreatedAt,
|
||||||
|
}).ToList(),
|
||||||
|
TotalCount = page.TotalCount,
|
||||||
|
Page = page.Page,
|
||||||
|
PageSize = page.PageSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Common;
|
||||||
|
using mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.application.Audit;
|
||||||
|
|
||||||
|
public record GetLoginLogsQuery(Guid ActorUserId, LoginLogFilter Filter, int Page, int PageSize)
|
||||||
|
: IRequest<PagedResult<LoginLogDto>>;
|
||||||
|
|
||||||
|
public class GetLoginLogsHandler(IUnitOfWork uow, IPermissionService permissions)
|
||||||
|
: IRequestHandler<GetLoginLogsQuery, PagedResult<LoginLogDto>>
|
||||||
|
{
|
||||||
|
public async Task<PagedResult<LoginLogDto>> Handle(GetLoginLogsQuery query, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await permissions.EnsureAsync(query.ActorUserId, "loginlogs", PermissionAction.View, ct);
|
||||||
|
|
||||||
|
var page = await uow.LoginLogs.SearchAsync(query.Filter, query.Page, query.PageSize, ct);
|
||||||
|
return new PagedResult<LoginLogDto>
|
||||||
|
{
|
||||||
|
Items = page.Items.Select(x => new LoginLogDto
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
Username = x.Username,
|
||||||
|
UserId = x.UserId,
|
||||||
|
Success = x.Success,
|
||||||
|
FailureReason = x.FailureReason,
|
||||||
|
IpAddress = x.IpAddress,
|
||||||
|
UserAgent = x.UserAgent,
|
||||||
|
CreatedAt = x.CreatedAt,
|
||||||
|
}).ToList(),
|
||||||
|
TotalCount = page.TotalCount,
|
||||||
|
Page = page.Page,
|
||||||
|
PageSize = page.PageSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,13 @@ using AutoMapper;
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Users;
|
using mws.backend.dotnet.application.Users;
|
||||||
|
using mws.backend.dotnet.domain.Audit;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Auth;
|
namespace mws.backend.dotnet.application.Auth;
|
||||||
|
|
||||||
public record LoginCommand(LoginRequest Request) : IRequest<LoginResponse>;
|
public record LoginCommand(LoginRequest Request) : IRequest<LoginResponse>;
|
||||||
|
|
||||||
public class LoginHandler(IUnitOfWork uow, IPasswordHasher passwordHasher, ITokenService tokenService, IMapper mapper)
|
public class LoginHandler(IUnitOfWork uow, IPasswordHasher passwordHasher, ITokenService tokenService, IMapper mapper, ICurrentUser currentUser)
|
||||||
: IRequestHandler<LoginCommand, LoginResponse>
|
: IRequestHandler<LoginCommand, LoginResponse>
|
||||||
{
|
{
|
||||||
public async Task<LoginResponse> Handle(LoginCommand command, CancellationToken ct)
|
public async Task<LoginResponse> Handle(LoginCommand command, CancellationToken ct)
|
||||||
@@ -15,20 +16,46 @@ public class LoginHandler(IUnitOfWork uow, IPasswordHasher passwordHasher, IToke
|
|||||||
var username = command.Request.Username.Trim();
|
var username = command.Request.Username.Trim();
|
||||||
var user = await uow.Users.GetByUsernameWithRoleAsync(username, ct);
|
var user = await uow.Users.GetByUsernameWithRoleAsync(username, ct);
|
||||||
|
|
||||||
if (user is null || !passwordHasher.Verify(command.Request.Password, user.PasswordHash))
|
if (user is null)
|
||||||
{
|
{
|
||||||
|
await WriteLoginLogAsync(username, null, false, "Unknown username", ct);
|
||||||
|
throw new UnauthorizedException("Invalid username or password");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!passwordHasher.Verify(command.Request.Password, user.PasswordHash))
|
||||||
|
{
|
||||||
|
await WriteLoginLogAsync(username, user.Id, false, "Invalid password", ct);
|
||||||
throw new UnauthorizedException("Invalid username or password");
|
throw new UnauthorizedException("Invalid username or password");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.IsActive)
|
if (!user.IsActive)
|
||||||
{
|
{
|
||||||
|
await WriteLoginLogAsync(username, user.Id, false, "Account disabled", ct);
|
||||||
throw new ForbiddenException("Account disabled");
|
throw new ForbiddenException("Account disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await WriteLoginLogAsync(username, user.Id, true, null, ct);
|
||||||
|
|
||||||
return new LoginResponse
|
return new LoginResponse
|
||||||
{
|
{
|
||||||
Token = tokenService.CreateToken(user.Id, user.Username),
|
Token = tokenService.CreateToken(user.Id, user.Username),
|
||||||
User = mapper.Map<UserDto>(user),
|
User = mapper.Map<UserDto>(user),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task WriteLoginLogAsync(string username, Guid? userId, bool success, string? failureReason, CancellationToken ct)
|
||||||
|
{
|
||||||
|
uow.LoginLogs.Add(new LoginLog
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Username = username.Length <= 100 ? username : username[..100],
|
||||||
|
UserId = userId,
|
||||||
|
Success = success,
|
||||||
|
FailureReason = failureReason,
|
||||||
|
IpAddress = currentUser.IpAddress,
|
||||||
|
UserAgent = currentUser.UserAgent,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
});
|
||||||
|
await uow.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace mws.backend.dotnet.application.Common;
|
||||||
|
|
||||||
|
public interface ICurrentUser
|
||||||
|
{
|
||||||
|
Guid? UserId { get; }
|
||||||
|
string? Username { get; }
|
||||||
|
string? IpAddress { get; }
|
||||||
|
string? UserAgent { get; }
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ public interface IUnitOfWork
|
|||||||
IDocumentRepository Documents { get; }
|
IDocumentRepository Documents { get; }
|
||||||
IRoleRepository Roles { get; }
|
IRoleRepository Roles { get; }
|
||||||
IMasterDataRepository MasterData { get; }
|
IMasterDataRepository MasterData { get; }
|
||||||
|
IActionLogRepository ActionLogs { get; }
|
||||||
|
ILoginLogRepository LoginLogs { get; }
|
||||||
|
|
||||||
Task<int> SaveChangesAsync(CancellationToken ct = default);
|
Task<int> SaveChangesAsync(CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
|
using mws.backend.dotnet.application.Common;
|
||||||
|
using mws.backend.dotnet.domain.Audit;
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.application.Common.Repositories;
|
||||||
|
|
||||||
|
public interface IActionLogRepository : IRepository<ActionLog>
|
||||||
|
{
|
||||||
|
Task<PagedResult<ActionLog>> SearchAsync(ActionLogFilter filter, int page, int pageSize, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
|
using mws.backend.dotnet.application.Common;
|
||||||
|
using mws.backend.dotnet.domain.Audit;
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.application.Common.Repositories;
|
||||||
|
|
||||||
|
public interface ILoginLogRepository : IRepository<LoginLog>
|
||||||
|
{
|
||||||
|
Task<PagedResult<LoginLog>> SearchAsync(LoginLogFilter filter, int page, int pageSize, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Permissions;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
using mws.backend.dotnet.domain.Documents;
|
using mws.backend.dotnet.domain.Documents;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Documents;
|
namespace mws.backend.dotnet.application.Documents;
|
||||||
|
|
||||||
public record CreateDocumentCommand(Guid UserId, Guid ProjectId, CreateDocumentRequest Request) : IRequest<DocumentDto>;
|
public record CreateDocumentCommand(Guid UserId, Guid ProjectId, CreateDocumentRequest Request) : IRequest<DocumentDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "Document.Create";
|
||||||
|
public string EntityType => "Document";
|
||||||
|
public string? EntityName => Request.Title;
|
||||||
|
}
|
||||||
|
|
||||||
public class CreateDocumentHandler(IUnitOfWork uow, IMapper mapper, IDocumentContentStore contentStore)
|
public class CreateDocumentHandler(IUnitOfWork uow, IMapper mapper, IDocumentContentStore contentStore)
|
||||||
: IRequestHandler<CreateDocumentCommand, DocumentDto>
|
: IRequestHandler<CreateDocumentCommand, DocumentDto>
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Permissions;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Documents;
|
namespace mws.backend.dotnet.application.Documents;
|
||||||
|
|
||||||
public record DeleteDocumentCommand(Guid UserId, Guid DocumentId) : IRequest;
|
public record DeleteDocumentCommand(Guid UserId, Guid DocumentId) : IRequest, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "Document.Delete";
|
||||||
|
public string EntityType => "Document";
|
||||||
|
public Guid? EntityId => DocumentId;
|
||||||
|
}
|
||||||
|
|
||||||
public class DeleteDocumentHandler(IUnitOfWork uow, IDocumentContentStore contentStore)
|
public class DeleteDocumentHandler(IUnitOfWork uow, IDocumentContentStore contentStore, IAuditContext auditContext)
|
||||||
: IRequestHandler<DeleteDocumentCommand>
|
: IRequestHandler<DeleteDocumentCommand>
|
||||||
{
|
{
|
||||||
public async Task Handle(DeleteDocumentCommand command, CancellationToken ct)
|
public async Task Handle(DeleteDocumentCommand command, CancellationToken ct)
|
||||||
@@ -25,6 +31,8 @@ public class DeleteDocumentHandler(IUnitOfWork uow, IDocumentContentStore conten
|
|||||||
uow.Documents.Remove(doc);
|
uow.Documents.Remove(doc);
|
||||||
await uow.SaveChangesAsync(ct);
|
await uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
auditContext.EntityName = doc.Title;
|
||||||
|
|
||||||
foreach (var key in keys)
|
foreach (var key in keys)
|
||||||
{
|
{
|
||||||
await contentStore.DeleteAsync(key, ct);
|
await contentStore.DeleteAsync(key, ct);
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Permissions;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
using mws.backend.dotnet.domain.Documents;
|
using mws.backend.dotnet.domain.Documents;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Documents;
|
namespace mws.backend.dotnet.application.Documents;
|
||||||
|
|
||||||
public record MoveDocumentCommand(Guid UserId, Guid DocumentId, Guid? NewParentId) : IRequest;
|
public record MoveDocumentCommand(Guid UserId, Guid DocumentId, Guid? NewParentId) : IRequest, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "Document.Move";
|
||||||
|
public string EntityType => "Document";
|
||||||
|
public Guid? EntityId => DocumentId;
|
||||||
|
}
|
||||||
|
|
||||||
public class MoveDocumentHandler(IUnitOfWork uow) : IRequestHandler<MoveDocumentCommand>
|
public class MoveDocumentHandler(IUnitOfWork uow, IAuditContext auditContext) : IRequestHandler<MoveDocumentCommand>
|
||||||
{
|
{
|
||||||
public async Task Handle(MoveDocumentCommand command, CancellationToken ct)
|
public async Task Handle(MoveDocumentCommand command, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -47,5 +53,7 @@ public class MoveDocumentHandler(IUnitOfWork uow) : IRequestHandler<MoveDocument
|
|||||||
doc.UpdatedAt = DateTime.UtcNow;
|
doc.UpdatedAt = DateTime.UtcNow;
|
||||||
doc.UpdatedBy = command.UserId;
|
doc.UpdatedBy = command.UserId;
|
||||||
await uow.SaveChangesAsync(ct);
|
await uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
auditContext.EntityName = doc.Title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Permissions;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
using mws.backend.dotnet.domain.Documents;
|
using mws.backend.dotnet.domain.Documents;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Documents;
|
namespace mws.backend.dotnet.application.Documents;
|
||||||
|
|
||||||
public record UpdateDocumentCommand(Guid UserId, Guid DocumentId, UpdateDocumentRequest Request) : IRequest<DocumentDto>;
|
public record UpdateDocumentCommand(Guid UserId, Guid DocumentId, UpdateDocumentRequest Request) : IRequest<DocumentDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "Document.Update";
|
||||||
|
public string EntityType => "Document";
|
||||||
|
public Guid? EntityId => DocumentId;
|
||||||
|
public string? EntityName => Request.Title;
|
||||||
|
}
|
||||||
|
|
||||||
public class UpdateDocumentHandler(IUnitOfWork uow, IMapper mapper, IDocumentContentStore contentStore)
|
public class UpdateDocumentHandler(IUnitOfWork uow, IMapper mapper, IDocumentContentStore contentStore)
|
||||||
: IRequestHandler<UpdateDocumentCommand, DocumentDto>
|
: IRequestHandler<UpdateDocumentCommand, DocumentDto>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.domain.Documents;
|
using mws.backend.dotnet.domain.Documents;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Documents;
|
namespace mws.backend.dotnet.application.Documents;
|
||||||
@@ -21,9 +22,10 @@ public class MoveDocumentRequest
|
|||||||
public Guid? NewParentId { get; set; }
|
public Guid? NewParentId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DocumentDto
|
public class DocumentDto : IHasEntityId
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
public Guid EntityId => Id;
|
||||||
public Guid ProjectId { get; set; }
|
public Guid ProjectId { get; set; }
|
||||||
public Guid? ParentId { get; set; }
|
public Guid? ParentId { get; set; }
|
||||||
public string Title { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.MasterData;
|
namespace mws.backend.dotnet.application.MasterData;
|
||||||
|
|
||||||
public record CreateMasterDataCommand(SaveMasterDataRequest Request) : IRequest<MasterDataDto>;
|
public record CreateMasterDataCommand(SaveMasterDataRequest Request) : IRequest<MasterDataDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "MasterData.Create";
|
||||||
|
public string EntityType => "MasterData";
|
||||||
|
public string? EntityName => Request.Label;
|
||||||
|
}
|
||||||
|
|
||||||
public class CreateMasterDataHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<CreateMasterDataCommand, MasterDataDto>
|
public class CreateMasterDataHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<CreateMasterDataCommand, MasterDataDto>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.MasterData;
|
namespace mws.backend.dotnet.application.MasterData;
|
||||||
|
|
||||||
public record DeleteMasterDataCommand(Guid Id) : IRequest;
|
public record DeleteMasterDataCommand(Guid Id) : IRequest, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "MasterData.Delete";
|
||||||
|
public string EntityType => "MasterData";
|
||||||
|
public Guid? EntityId => Id;
|
||||||
|
}
|
||||||
|
|
||||||
public class DeleteMasterDataHandler(IUnitOfWork uow) : IRequestHandler<DeleteMasterDataCommand>
|
public class DeleteMasterDataHandler(IUnitOfWork uow, IAuditContext auditContext) : IRequestHandler<DeleteMasterDataCommand>
|
||||||
{
|
{
|
||||||
public async Task Handle(DeleteMasterDataCommand command, CancellationToken ct)
|
public async Task Handle(DeleteMasterDataCommand command, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -14,5 +20,7 @@ public class DeleteMasterDataHandler(IUnitOfWork uow) : IRequestHandler<DeleteMa
|
|||||||
|
|
||||||
uow.MasterData.Remove(entry);
|
uow.MasterData.Remove(entry);
|
||||||
await uow.SaveChangesAsync(ct);
|
await uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
auditContext.EntityName = entry.Label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.MasterData;
|
namespace mws.backend.dotnet.application.MasterData;
|
||||||
|
|
||||||
public record UpdateMasterDataCommand(Guid Id, SaveMasterDataRequest Request) : IRequest<MasterDataDto>;
|
public record UpdateMasterDataCommand(Guid Id, SaveMasterDataRequest Request) : IRequest<MasterDataDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "MasterData.Update";
|
||||||
|
public string EntityType => "MasterData";
|
||||||
|
public Guid? EntityId => Id;
|
||||||
|
public string? EntityName => Request.Label;
|
||||||
|
}
|
||||||
|
|
||||||
public class UpdateMasterDataHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<UpdateMasterDataCommand, MasterDataDto>
|
public class UpdateMasterDataHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<UpdateMasterDataCommand, MasterDataDto>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.MasterData;
|
namespace mws.backend.dotnet.application.MasterData;
|
||||||
|
|
||||||
public class MasterDataDto
|
public class MasterDataDto : IHasEntityId
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
public Guid EntityId => Id;
|
||||||
public string Group { get; set; } = string.Empty;
|
public string Group { get; set; } = string.Empty;
|
||||||
public string Label { get; set; } = string.Empty;
|
public string Label { get; set; } = string.Empty;
|
||||||
public string Value { get; set; } = string.Empty;
|
public string Value { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.domain.Roles;
|
using mws.backend.dotnet.domain.Roles;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Permissions;
|
namespace mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
public record CreateRoleCommand(SaveRoleRequest Request) : IRequest<RoleDto>;
|
public record CreateRoleCommand(SaveRoleRequest Request) : IRequest<RoleDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "Role.Create";
|
||||||
|
public string EntityType => "Role";
|
||||||
|
public string? EntityName => Request.Name;
|
||||||
|
}
|
||||||
|
|
||||||
public class CreateRoleHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<CreateRoleCommand, RoleDto>
|
public class CreateRoleHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<CreateRoleCommand, RoleDto>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Permissions;
|
namespace mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
public record DeleteRoleCommand(Guid Id) : IRequest;
|
public record DeleteRoleCommand(Guid Id) : IRequest, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "Role.Delete";
|
||||||
|
public string EntityType => "Role";
|
||||||
|
public Guid? EntityId => Id;
|
||||||
|
}
|
||||||
|
|
||||||
public class DeleteRoleHandler(IUnitOfWork uow) : IRequestHandler<DeleteRoleCommand>
|
public class DeleteRoleHandler(IUnitOfWork uow, IAuditContext auditContext) : IRequestHandler<DeleteRoleCommand>
|
||||||
{
|
{
|
||||||
public async Task Handle(DeleteRoleCommand command, CancellationToken ct)
|
public async Task Handle(DeleteRoleCommand command, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -24,5 +30,7 @@ public class DeleteRoleHandler(IUnitOfWork uow) : IRequestHandler<DeleteRoleComm
|
|||||||
|
|
||||||
uow.Roles.Remove(role);
|
uow.Roles.Remove(role);
|
||||||
await uow.SaveChangesAsync(ct);
|
await uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
auditContext.EntityName = role.Name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Permissions;
|
namespace mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
public record UpdateRoleCommand(Guid Id, SaveRoleRequest Request) : IRequest<RoleDto>;
|
public record UpdateRoleCommand(Guid Id, SaveRoleRequest Request) : IRequest<RoleDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "Role.Update";
|
||||||
|
public string EntityType => "Role";
|
||||||
|
public Guid? EntityId => Id;
|
||||||
|
public string? EntityName => Request.Name;
|
||||||
|
}
|
||||||
|
|
||||||
public class UpdateRoleHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<UpdateRoleCommand, RoleDto>
|
public class UpdateRoleHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<UpdateRoleCommand, RoleDto>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Permissions;
|
namespace mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
public enum PermissionAction
|
public enum PermissionAction
|
||||||
@@ -28,9 +30,10 @@ public class PermissionEntryDto
|
|||||||
public bool CanDelete { get; set; }
|
public bool CanDelete { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RoleDto
|
public class RoleDto : IHasEntityId
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
public Guid EntityId => Id;
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public bool IsSystem { get; set; }
|
public bool IsSystem { get; set; }
|
||||||
public List<PermissionEntryDto> Permissions { get; set; } = [];
|
public List<PermissionEntryDto> Permissions { get; set; } = [];
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ public static class ScreenCatalog
|
|||||||
new("users", "Users", "/users"),
|
new("users", "Users", "/users"),
|
||||||
new("permissions", "Permissions", "/settings/permission"),
|
new("permissions", "Permissions", "/settings/permission"),
|
||||||
new("masterdata", "Master Data", "/settings/masterdata"),
|
new("masterdata", "Master Data", "/settings/masterdata"),
|
||||||
|
new("actionlogs", "Action Logs", "/settings/actionlogs"),
|
||||||
|
new("loginlogs", "Login Logs", "/settings/loginlogs"),
|
||||||
];
|
];
|
||||||
|
|
||||||
public static readonly IReadOnlySet<string> Keys = Screens.Select(s => s.Key).ToHashSet();
|
public static readonly IReadOnlySet<string> Keys = Screens.Select(s => s.Key).ToHashSet();
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.domain.Projects;
|
using mws.backend.dotnet.domain.Projects;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Projects;
|
namespace mws.backend.dotnet.application.Projects;
|
||||||
|
|
||||||
public record AddProjectMemberCommand(Guid UserId, Guid ProjectId, AddMemberRequest Request) : IRequest<ProjectMemberDto>;
|
public record AddProjectMemberCommand(Guid UserId, Guid ProjectId, AddMemberRequest Request) : IRequest<ProjectMemberDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "ProjectMember.Add";
|
||||||
|
public string EntityType => "ProjectMember";
|
||||||
|
public Guid? EntityId => Request.UserId;
|
||||||
|
}
|
||||||
|
|
||||||
public class AddProjectMemberHandler(IUnitOfWork uow) : IRequestHandler<AddProjectMemberCommand, ProjectMemberDto>
|
public class AddProjectMemberHandler(IUnitOfWork uow, IAuditContext auditContext) : IRequestHandler<AddProjectMemberCommand, ProjectMemberDto>
|
||||||
{
|
{
|
||||||
public async Task<ProjectMemberDto> Handle(AddProjectMemberCommand command, CancellationToken ct)
|
public async Task<ProjectMemberDto> Handle(AddProjectMemberCommand command, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -46,6 +52,8 @@ public class AddProjectMemberHandler(IUnitOfWork uow) : IRequestHandler<AddProje
|
|||||||
});
|
});
|
||||||
await uow.SaveChangesAsync(ct);
|
await uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
auditContext.EntityName = user.Username;
|
||||||
|
|
||||||
return new ProjectMemberDto
|
return new ProjectMemberDto
|
||||||
{
|
{
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Permissions;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
using mws.backend.dotnet.domain.Projects;
|
using mws.backend.dotnet.domain.Projects;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Projects;
|
namespace mws.backend.dotnet.application.Projects;
|
||||||
|
|
||||||
public record ArchiveProjectCommand(Guid UserId, Guid ProjectId) : IRequest;
|
public record ArchiveProjectCommand(Guid UserId, Guid ProjectId) : IRequest, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "Project.Archive";
|
||||||
|
public string EntityType => "Project";
|
||||||
|
public Guid? EntityId => ProjectId;
|
||||||
|
}
|
||||||
|
|
||||||
public class ArchiveProjectHandler(IUnitOfWork uow, IPermissionService permissions) : IRequestHandler<ArchiveProjectCommand>
|
public class ArchiveProjectHandler(IUnitOfWork uow, IPermissionService permissions, IAuditContext auditContext) : IRequestHandler<ArchiveProjectCommand>
|
||||||
{
|
{
|
||||||
public async Task Handle(ArchiveProjectCommand command, CancellationToken ct)
|
public async Task Handle(ArchiveProjectCommand command, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -18,5 +24,7 @@ public class ArchiveProjectHandler(IUnitOfWork uow, IPermissionService permissio
|
|||||||
project.Status = ProjectStatus.Archived;
|
project.Status = ProjectStatus.Archived;
|
||||||
project.UpdatedAt = DateTime.UtcNow;
|
project.UpdatedAt = DateTime.UtcNow;
|
||||||
await uow.SaveChangesAsync(ct);
|
await uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
auditContext.EntityName = project.Name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Permissions;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
using mws.backend.dotnet.domain.Projects;
|
using mws.backend.dotnet.domain.Projects;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Projects;
|
namespace mws.backend.dotnet.application.Projects;
|
||||||
|
|
||||||
public record CreateProjectCommand(Guid UserId, CreateProjectRequest Request) : IRequest<ProjectDto>;
|
public record CreateProjectCommand(Guid UserId, CreateProjectRequest Request) : IRequest<ProjectDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "Project.Create";
|
||||||
|
public string EntityType => "Project";
|
||||||
|
public string? EntityName => Request.Name;
|
||||||
|
}
|
||||||
|
|
||||||
public class CreateProjectHandler(IUnitOfWork uow, IMapper mapper, IPermissionService permissions) : IRequestHandler<CreateProjectCommand, ProjectDto>
|
public class CreateProjectHandler(IUnitOfWork uow, IMapper mapper, IPermissionService permissions) : IRequestHandler<CreateProjectCommand, ProjectDto>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.domain.Projects;
|
using mws.backend.dotnet.domain.Projects;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Projects;
|
namespace mws.backend.dotnet.application.Projects;
|
||||||
|
|
||||||
public record RemoveProjectMemberCommand(Guid UserId, Guid ProjectId, Guid MemberUserId) : IRequest;
|
public record RemoveProjectMemberCommand(Guid UserId, Guid ProjectId, Guid MemberUserId) : IRequest, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "ProjectMember.Remove";
|
||||||
|
public string EntityType => "ProjectMember";
|
||||||
|
public Guid? EntityId => MemberUserId;
|
||||||
|
}
|
||||||
|
|
||||||
public class RemoveProjectMemberHandler(IUnitOfWork uow) : IRequestHandler<RemoveProjectMemberCommand>
|
public class RemoveProjectMemberHandler(IUnitOfWork uow, IAuditContext auditContext) : IRequestHandler<RemoveProjectMemberCommand>
|
||||||
{
|
{
|
||||||
public async Task Handle(RemoveProjectMemberCommand command, CancellationToken ct)
|
public async Task Handle(RemoveProjectMemberCommand command, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -15,7 +21,7 @@ public class RemoveProjectMemberHandler(IUnitOfWork uow) : IRequestHandler<Remov
|
|||||||
throw new ForbiddenException("Only the project owner can remove members");
|
throw new ForbiddenException("Only the project owner can remove members");
|
||||||
}
|
}
|
||||||
|
|
||||||
var member = await uow.Projects.GetMemberAsync(command.ProjectId, command.MemberUserId, ct)
|
var member = await uow.Projects.GetMemberWithUserAsync(command.ProjectId, command.MemberUserId, ct)
|
||||||
?? throw new NotFoundException("Member not found in project");
|
?? throw new NotFoundException("Member not found in project");
|
||||||
|
|
||||||
var owners = await uow.Projects.CountOwnersAsync(command.ProjectId, ct);
|
var owners = await uow.Projects.CountOwnersAsync(command.ProjectId, ct);
|
||||||
@@ -27,5 +33,7 @@ public class RemoveProjectMemberHandler(IUnitOfWork uow) : IRequestHandler<Remov
|
|||||||
|
|
||||||
uow.Projects.RemoveMember(member);
|
uow.Projects.RemoveMember(member);
|
||||||
await uow.SaveChangesAsync(ct);
|
await uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
auditContext.EntityName = member.User.Username;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.domain.Projects;
|
using mws.backend.dotnet.domain.Projects;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Projects;
|
namespace mws.backend.dotnet.application.Projects;
|
||||||
|
|
||||||
public record UpdateMemberDocumentPermissionsCommand(
|
public record UpdateMemberDocumentPermissionsCommand(
|
||||||
Guid UserId, Guid ProjectId, Guid MemberUserId, UpdateMemberDocumentPermissionsRequest Request) : IRequest<ProjectMemberDto>;
|
Guid UserId, Guid ProjectId, Guid MemberUserId, UpdateMemberDocumentPermissionsRequest Request) : IRequest<ProjectMemberDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "ProjectMember.UpdatePermissions";
|
||||||
|
public string EntityType => "ProjectMember";
|
||||||
|
public Guid? EntityId => MemberUserId;
|
||||||
|
}
|
||||||
|
|
||||||
public class UpdateMemberDocumentPermissionsHandler(IUnitOfWork uow)
|
public class UpdateMemberDocumentPermissionsHandler(IUnitOfWork uow, IAuditContext auditContext)
|
||||||
: IRequestHandler<UpdateMemberDocumentPermissionsCommand, ProjectMemberDto>
|
: IRequestHandler<UpdateMemberDocumentPermissionsCommand, ProjectMemberDto>
|
||||||
{
|
{
|
||||||
public async Task<ProjectMemberDto> Handle(UpdateMemberDocumentPermissionsCommand command, CancellationToken ct)
|
public async Task<ProjectMemberDto> Handle(UpdateMemberDocumentPermissionsCommand command, CancellationToken ct)
|
||||||
@@ -39,6 +45,8 @@ public class UpdateMemberDocumentPermissionsHandler(IUnitOfWork uow)
|
|||||||
permission.CanDelete = request.CanDeleteDocuments;
|
permission.CanDelete = request.CanDeleteDocuments;
|
||||||
await uow.SaveChangesAsync(ct);
|
await uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
auditContext.EntityName = member.User.Username;
|
||||||
|
|
||||||
return new ProjectMemberDto
|
return new ProjectMemberDto
|
||||||
{
|
{
|
||||||
UserId = member.UserId,
|
UserId = member.UserId,
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Permissions;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Projects;
|
namespace mws.backend.dotnet.application.Projects;
|
||||||
|
|
||||||
public record UpdateProjectCommand(Guid UserId, Guid ProjectId, UpdateProjectRequest Request) : IRequest<ProjectDto>;
|
public record UpdateProjectCommand(Guid UserId, Guid ProjectId, UpdateProjectRequest Request) : IRequest<ProjectDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "Project.Update";
|
||||||
|
public string EntityType => "Project";
|
||||||
|
public Guid? EntityId => ProjectId;
|
||||||
|
public string? EntityName => Request.Name;
|
||||||
|
}
|
||||||
|
|
||||||
public class UpdateProjectHandler(IUnitOfWork uow, IMapper mapper, IPermissionService permissions) : IRequestHandler<UpdateProjectCommand, ProjectDto>
|
public class UpdateProjectHandler(IUnitOfWork uow, IMapper mapper, IPermissionService permissions) : IRequestHandler<UpdateProjectCommand, ProjectDto>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.domain.Projects;
|
using mws.backend.dotnet.domain.Projects;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Projects;
|
namespace mws.backend.dotnet.application.Projects;
|
||||||
@@ -41,9 +42,10 @@ public class UpdateMemberDocumentPermissionsRequest
|
|||||||
public bool CanDeleteDocuments { get; set; }
|
public bool CanDeleteDocuments { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ProjectDto
|
public class ProjectDto : IHasEntityId
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
public Guid EntityId => Id;
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public ProjectStatus Status { get; set; }
|
public ProjectStatus Status { get; set; }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.domain.Tasks;
|
using mws.backend.dotnet.domain.Tasks;
|
||||||
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
|
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
|
||||||
@@ -7,7 +8,12 @@ using TaskStatus = mws.backend.dotnet.domain.Tasks.TaskStatus;
|
|||||||
|
|
||||||
namespace mws.backend.dotnet.application.Tasks;
|
namespace mws.backend.dotnet.application.Tasks;
|
||||||
|
|
||||||
public record CreateTaskCommand(Guid UserId, Guid ProjectId, CreateTaskRequest Request) : IRequest<TaskDto>;
|
public record CreateTaskCommand(Guid UserId, Guid ProjectId, CreateTaskRequest Request) : IRequest<TaskDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "Task.Create";
|
||||||
|
public string EntityType => "Task";
|
||||||
|
public string? EntityName => Request.Title;
|
||||||
|
}
|
||||||
|
|
||||||
public class CreateTaskHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<CreateTaskCommand, TaskDto>
|
public class CreateTaskHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<CreateTaskCommand, TaskDto>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Tasks;
|
namespace mws.backend.dotnet.application.Tasks;
|
||||||
|
|
||||||
public record DeleteTaskCommand(Guid UserId, Guid TaskId) : IRequest;
|
public record DeleteTaskCommand(Guid UserId, Guid TaskId) : IRequest, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "Task.Delete";
|
||||||
|
public string EntityType => "Task";
|
||||||
|
public Guid? EntityId => TaskId;
|
||||||
|
}
|
||||||
|
|
||||||
public class DeleteTaskHandler(IUnitOfWork uow) : IRequestHandler<DeleteTaskCommand>
|
public class DeleteTaskHandler(IUnitOfWork uow, IAuditContext auditContext) : IRequestHandler<DeleteTaskCommand>
|
||||||
{
|
{
|
||||||
public async Task Handle(DeleteTaskCommand command, CancellationToken ct)
|
public async Task Handle(DeleteTaskCommand command, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var task = await TaskAccess.GetTaskForUserAsync(uow, command.UserId, command.TaskId, ct);
|
var task = await TaskAccess.GetTaskForUserAsync(uow, command.UserId, command.TaskId, ct);
|
||||||
uow.Tasks.Remove(task);
|
uow.Tasks.Remove(task);
|
||||||
await uow.SaveChangesAsync(ct);
|
await uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
auditContext.EntityName = task.Title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Tasks;
|
namespace mws.backend.dotnet.application.Tasks;
|
||||||
|
|
||||||
public record UpdateTaskCommand(Guid UserId, Guid TaskId, UpdateTaskRequest Request) : IRequest<TaskDto>;
|
public record UpdateTaskCommand(Guid UserId, Guid TaskId, UpdateTaskRequest Request) : IRequest<TaskDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "Task.Update";
|
||||||
|
public string EntityType => "Task";
|
||||||
|
public Guid? EntityId => TaskId;
|
||||||
|
public string? EntityName => Request.Title;
|
||||||
|
}
|
||||||
|
|
||||||
public class UpdateTaskHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<UpdateTaskCommand, TaskDto>
|
public class UpdateTaskHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<UpdateTaskCommand, TaskDto>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.domain.Tasks;
|
using mws.backend.dotnet.domain.Tasks;
|
||||||
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
|
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
|
||||||
using TaskStatus = mws.backend.dotnet.domain.Tasks.TaskStatus;
|
using TaskStatus = mws.backend.dotnet.domain.Tasks.TaskStatus;
|
||||||
@@ -24,9 +25,10 @@ public class UpdateTaskRequest
|
|||||||
public DateTime? DueDate { get; set; }
|
public DateTime? DueDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TaskDto
|
public class TaskDto : IHasEntityId
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
public Guid EntityId => Id;
|
||||||
public Guid ProjectId { get; set; }
|
public Guid ProjectId { get; set; }
|
||||||
public string Title { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
|
using mws.backend.dotnet.application.Common;
|
||||||
|
using mws.backend.dotnet.application.Permissions;
|
||||||
|
using mws.backend.dotnet.domain.Users;
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.application.Users;
|
||||||
|
|
||||||
|
public record AssignUserRoleCommand(Guid ActorUserId, Guid TargetUserId, Guid RoleId) : IRequest, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "User.AssignRole";
|
||||||
|
public string EntityType => "User";
|
||||||
|
public Guid? EntityId => TargetUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AssignUserRoleHandler(IUnitOfWork uow, IPermissionService permissions, IAuditContext auditContext)
|
||||||
|
: IRequestHandler<AssignUserRoleCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(AssignUserRoleCommand command, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await permissions.EnsureAsync(command.ActorUserId, "permissions", PermissionAction.Edit, ct);
|
||||||
|
|
||||||
|
var user = await uow.Users.GetByIdWithRoleAsync(command.TargetUserId, ct)
|
||||||
|
?? throw new NotFoundException("User not found");
|
||||||
|
|
||||||
|
if (user.UserRoles.All(ur => ur.RoleId != command.RoleId))
|
||||||
|
{
|
||||||
|
user.UserRoles.Add(new UserRole { UserId = command.TargetUserId, RoleId = command.RoleId });
|
||||||
|
await uow.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
auditContext.EntityName = user.Username;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Auth;
|
using mws.backend.dotnet.application.Auth;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Permissions;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
@@ -8,7 +9,12 @@ using mws.backend.dotnet.domain.Users;
|
|||||||
|
|
||||||
namespace mws.backend.dotnet.application.Users;
|
namespace mws.backend.dotnet.application.Users;
|
||||||
|
|
||||||
public record CreateUserCommand(Guid ActorUserId, CreateUserRequest Request) : IRequest<UserDto>;
|
public record CreateUserCommand(Guid ActorUserId, CreateUserRequest Request) : IRequest<UserDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "User.Create";
|
||||||
|
public string EntityType => "User";
|
||||||
|
public string? EntityName => Request.Username;
|
||||||
|
}
|
||||||
|
|
||||||
public class CreateUserHandler(IUnitOfWork uow, IPasswordHasher passwordHasher, IPermissionService permissions, IMapper mapper)
|
public class CreateUserHandler(IUnitOfWork uow, IPasswordHasher passwordHasher, IPermissionService permissions, IMapper mapper)
|
||||||
: IRequestHandler<CreateUserCommand, UserDto>
|
: IRequestHandler<CreateUserCommand, UserDto>
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Permissions;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Users;
|
namespace mws.backend.dotnet.application.Users;
|
||||||
|
|
||||||
public record DeleteUserCommand(Guid ActorUserId, Guid Id) : IRequest;
|
public record DeleteUserCommand(Guid ActorUserId, Guid Id) : IRequest, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "User.Delete";
|
||||||
|
public string EntityType => "User";
|
||||||
|
public Guid? EntityId => Id;
|
||||||
|
}
|
||||||
|
|
||||||
public class DeleteUserHandler(IUnitOfWork uow, IPermissionService permissions) : IRequestHandler<DeleteUserCommand>
|
public class DeleteUserHandler(IUnitOfWork uow, IPermissionService permissions, IAuditContext auditContext) : IRequestHandler<DeleteUserCommand>
|
||||||
{
|
{
|
||||||
private const string Screen = "users";
|
private const string Screen = "users";
|
||||||
|
|
||||||
@@ -30,5 +36,7 @@ public class DeleteUserHandler(IUnitOfWork uow, IPermissionService permissions)
|
|||||||
|
|
||||||
uow.Users.Remove(user);
|
uow.Users.Remove(user);
|
||||||
await uow.SaveChangesAsync(ct);
|
await uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
auditContext.EntityName = user.Username;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Auth;
|
using mws.backend.dotnet.application.Auth;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Permissions;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Users;
|
namespace mws.backend.dotnet.application.Users;
|
||||||
|
|
||||||
public record ResetPasswordCommand(Guid ActorUserId, Guid Id, ResetPasswordRequest Request) : IRequest;
|
public record ResetPasswordCommand(Guid ActorUserId, Guid Id, ResetPasswordRequest Request) : IRequest, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "User.ResetPassword";
|
||||||
|
public string EntityType => "User";
|
||||||
|
public Guid? EntityId => Id;
|
||||||
|
}
|
||||||
|
|
||||||
public class ResetPasswordHandler(IUnitOfWork uow, IPasswordHasher passwordHasher, IPermissionService permissions)
|
public class ResetPasswordHandler(IUnitOfWork uow, IPasswordHasher passwordHasher, IPermissionService permissions, IAuditContext auditContext)
|
||||||
: IRequestHandler<ResetPasswordCommand>
|
: IRequestHandler<ResetPasswordCommand>
|
||||||
{
|
{
|
||||||
private const string Screen = "users";
|
private const string Screen = "users";
|
||||||
@@ -27,5 +33,7 @@ public class ResetPasswordHandler(IUnitOfWork uow, IPasswordHasher passwordHashe
|
|||||||
user.PasswordHash = passwordHasher.Hash(command.Request.NewPassword);
|
user.PasswordHash = passwordHasher.Hash(command.Request.NewPassword);
|
||||||
user.UpdatedAt = DateTime.UtcNow;
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
await uow.SaveChangesAsync(ct);
|
await uow.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
auditContext.EntityName = user.Username;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
|
using mws.backend.dotnet.application.Common;
|
||||||
|
using mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.application.Users;
|
||||||
|
|
||||||
|
public record UnassignUserRoleCommand(Guid ActorUserId, Guid TargetUserId, Guid RoleId) : IRequest, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "User.UnassignRole";
|
||||||
|
public string EntityType => "User";
|
||||||
|
public Guid? EntityId => TargetUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UnassignUserRoleHandler(IUnitOfWork uow, IPermissionService permissions, IAuditContext auditContext)
|
||||||
|
: IRequestHandler<UnassignUserRoleCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(UnassignUserRoleCommand command, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await permissions.EnsureAsync(command.ActorUserId, "permissions", PermissionAction.Edit, ct);
|
||||||
|
|
||||||
|
var user = await uow.Users.GetByIdWithRoleAsync(command.TargetUserId, ct)
|
||||||
|
?? throw new NotFoundException("User not found");
|
||||||
|
|
||||||
|
var userRole = user.UserRoles.FirstOrDefault(ur => ur.RoleId == command.RoleId);
|
||||||
|
if (userRole is not null)
|
||||||
|
{
|
||||||
|
user.UserRoles.Remove(userRole);
|
||||||
|
await uow.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
auditContext.EntityName = user.Username;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Permissions;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Users;
|
namespace mws.backend.dotnet.application.Users;
|
||||||
|
|
||||||
public record UpdateUserCommand(Guid ActorUserId, Guid Id, UpdateUserRequest Request) : IRequest<UserDto>;
|
public record UpdateUserCommand(Guid ActorUserId, Guid Id, UpdateUserRequest Request) : IRequest<UserDto>, IAuditableRequest
|
||||||
|
{
|
||||||
|
public string Action => "User.Update";
|
||||||
|
public string EntityType => "User";
|
||||||
|
public Guid? EntityId => Id;
|
||||||
|
public string? EntityName => Request.DisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
public class UpdateUserHandler(IUnitOfWork uow, IPermissionService permissions, IMapper mapper)
|
public class UpdateUserHandler(IUnitOfWork uow, IPermissionService permissions, IMapper mapper)
|
||||||
: IRequestHandler<UpdateUserCommand, UserDto>
|
: IRequestHandler<UpdateUserCommand, UserDto>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Permissions;
|
using mws.backend.dotnet.application.Permissions;
|
||||||
|
|
||||||
namespace mws.backend.dotnet.application.Users;
|
namespace mws.backend.dotnet.application.Users;
|
||||||
@@ -22,9 +23,10 @@ public class ResetPasswordRequest
|
|||||||
public string NewPassword { get; set; } = string.Empty;
|
public string NewPassword { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserDto
|
public class UserDto : IHasEntityId
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
public Guid EntityId => Id;
|
||||||
public string Username { get; set; } = string.Empty;
|
public string Username { get; set; } = string.Empty;
|
||||||
public string DisplayName { get; set; } = string.Empty;
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
public Guid RoleId { get; set; }
|
public Guid RoleId { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace mws.backend.dotnet.domain.Audit;
|
||||||
|
|
||||||
|
public class ActionLog
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid ActorUserId { get; set; }
|
||||||
|
public string ActorUsername { get; set; } = string.Empty;
|
||||||
|
public string Action { get; set; } = string.Empty;
|
||||||
|
public string EntityType { get; set; } = string.Empty;
|
||||||
|
public Guid? EntityId { get; set; }
|
||||||
|
public string? EntityName { get; set; }
|
||||||
|
public string? IpAddress { get; set; }
|
||||||
|
public string? UserAgent { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace mws.backend.dotnet.domain.Audit;
|
||||||
|
|
||||||
|
public class LoginLog
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public Guid? UserId { get; set; }
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? FailureReason { get; set; }
|
||||||
|
public string? IpAddress { get; set; }
|
||||||
|
public string? UserAgent { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using MediatR;
|
||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
using mws.backend.dotnet.application.Auth;
|
using mws.backend.dotnet.application.Auth;
|
||||||
using mws.backend.dotnet.application.Common;
|
using mws.backend.dotnet.application.Common;
|
||||||
using mws.backend.dotnet.application.Documents;
|
using mws.backend.dotnet.application.Documents;
|
||||||
@@ -25,7 +27,12 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||||
|
|
||||||
services.AddAutoMapper(cfg => { }, typeof(MappingProfile).Assembly);
|
services.AddAutoMapper(cfg => { }, typeof(MappingProfile).Assembly);
|
||||||
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(IUnitOfWork).Assembly));
|
services.AddMediatR(cfg =>
|
||||||
|
{
|
||||||
|
cfg.RegisterServicesFromAssembly(typeof(IUnitOfWork).Assembly);
|
||||||
|
cfg.AddOpenBehavior(typeof(AuditLogBehavior<,>));
|
||||||
|
});
|
||||||
|
services.AddScoped<IAuditContext, AuditContext>();
|
||||||
|
|
||||||
services.AddScoped<IPasswordHasher, BcryptPasswordHasher>();
|
services.AddScoped<IPasswordHasher, BcryptPasswordHasher>();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,594 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using mws.backend.dotnet.infrastructure.Persistence;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260920085021_AddAuditLogs")]
|
||||||
|
partial class AddAuditLogs
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.11")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Audit.ActionLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Action")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<Guid>("ActorUserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ActorUsername")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("EntityId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("EntityName")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<string>("EntityType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(45)
|
||||||
|
.HasColumnType("character varying(45)");
|
||||||
|
|
||||||
|
b.Property<string>("UserAgent")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Action");
|
||||||
|
|
||||||
|
b.HasIndex("ActorUserId");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("EntityType");
|
||||||
|
|
||||||
|
b.ToTable("action_logs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Audit.LoginLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("FailureReason")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(45)
|
||||||
|
.HasColumnType("character varying(45)");
|
||||||
|
|
||||||
|
b.Property<bool>("Success")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("UserAgent")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("Success");
|
||||||
|
|
||||||
|
b.HasIndex("Username");
|
||||||
|
|
||||||
|
b.ToTable("login_logs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Documents.Document", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ContentHash")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<long?>("ContentSize")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ParentId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ProjectId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("StorageKey")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("character varying(300)");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UpdatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedContentAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.HasIndex("ProjectId", "ParentId");
|
||||||
|
|
||||||
|
b.ToTable("documents", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.MasterData.MasterDataEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Group")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Label")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Group", "Value")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("master_data", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.Project", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UpdatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedBy");
|
||||||
|
|
||||||
|
b.HasIndex("UpdatedBy");
|
||||||
|
|
||||||
|
b.ToTable("projects", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.ProjectMember", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("ProjectId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Role")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.HasKey("ProjectId", "UserId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("project_members", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.ProjectMemberPermission", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("ProjectId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Screen")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<bool>("CanCreate")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("CanDelete")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("CanEdit")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("CanView")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.HasKey("ProjectId", "UserId", "Screen");
|
||||||
|
|
||||||
|
b.ToTable("project_member_permissions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Roles.Role", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSystem")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("roles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Roles.RolePermission", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Screen")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<bool>("CanCreate")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("CanDelete")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("CanEdit")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("CanView")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.HasKey("RoleId", "Screen");
|
||||||
|
|
||||||
|
b.ToTable("role_permissions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Tasks.TaskItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AssigneeId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DueDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Priority")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<Guid>("ProjectId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("character varying(300)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssigneeId");
|
||||||
|
|
||||||
|
b.HasIndex("ProjectId");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.ToTable("tasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Users.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Users.UserRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("user_roles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Documents.Document", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("mws.backend.dotnet.domain.Documents.Document", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.Project", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("mws.backend.dotnet.domain.Users.User", "CreatedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CreatedBy")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("mws.backend.dotnet.domain.Users.User", "UpdatedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UpdatedBy")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("CreatedByUser");
|
||||||
|
|
||||||
|
b.Navigation("UpdatedByUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.ProjectMember", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("mws.backend.dotnet.domain.Projects.Project", "Project")
|
||||||
|
.WithMany("Members")
|
||||||
|
.HasForeignKey("ProjectId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("mws.backend.dotnet.domain.Users.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Project");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.ProjectMemberPermission", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("mws.backend.dotnet.domain.Projects.ProjectMember", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProjectId", "UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Roles.RolePermission", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("mws.backend.dotnet.domain.Roles.Role", "Role")
|
||||||
|
.WithMany("Permissions")
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Role");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Tasks.TaskItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("mws.backend.dotnet.domain.Users.User", "Assignee")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssigneeId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Assignee");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Users.UserRole", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("mws.backend.dotnet.domain.Roles.Role", "Role")
|
||||||
|
.WithMany("UserRoles")
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("mws.backend.dotnet.domain.Users.User", "User")
|
||||||
|
.WithMany("UserRoles")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Role");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Documents.Document", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Projects.Project", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Members");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Roles.Role", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Permissions");
|
||||||
|
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Users.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserRoles");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAuditLogs : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "action_logs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
ActorUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
ActorUsername = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
Action = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
EntityType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
|
EntityId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
EntityName = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
||||||
|
IpAddress = table.Column<string>(type: "character varying(45)", maxLength: 45, nullable: true),
|
||||||
|
UserAgent = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_action_logs", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "login_logs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Username = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
UserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
Success = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
FailureReason = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||||
|
IpAddress = table.Column<string>(type: "character varying(45)", maxLength: 45, nullable: true),
|
||||||
|
UserAgent = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_login_logs", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_action_logs_Action",
|
||||||
|
table: "action_logs",
|
||||||
|
column: "Action");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_action_logs_ActorUserId",
|
||||||
|
table: "action_logs",
|
||||||
|
column: "ActorUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_action_logs_CreatedAt",
|
||||||
|
table: "action_logs",
|
||||||
|
column: "CreatedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_action_logs_EntityType",
|
||||||
|
table: "action_logs",
|
||||||
|
column: "EntityType");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_login_logs_CreatedAt",
|
||||||
|
table: "login_logs",
|
||||||
|
column: "CreatedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_login_logs_Success",
|
||||||
|
table: "login_logs",
|
||||||
|
column: "Success");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_login_logs_Username",
|
||||||
|
table: "login_logs",
|
||||||
|
column: "Username");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "action_logs");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "login_logs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,104 @@ namespace mws.backend.dotnet.infrastructure.Migrations
|
|||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Audit.ActionLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Action")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<Guid>("ActorUserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ActorUsername")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("EntityId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("EntityName")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<string>("EntityType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(45)
|
||||||
|
.HasColumnType("character varying(45)");
|
||||||
|
|
||||||
|
b.Property<string>("UserAgent")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Action");
|
||||||
|
|
||||||
|
b.HasIndex("ActorUserId");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("EntityType");
|
||||||
|
|
||||||
|
b.ToTable("action_logs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("mws.backend.dotnet.domain.Audit.LoginLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("FailureReason")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(45)
|
||||||
|
.HasColumnType("character varying(45)");
|
||||||
|
|
||||||
|
b.Property<bool>("Success")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("UserAgent")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("Success");
|
||||||
|
|
||||||
|
b.HasIndex("Username");
|
||||||
|
|
||||||
|
b.ToTable("login_logs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("mws.backend.dotnet.domain.Documents.Document", b =>
|
modelBuilder.Entity("mws.backend.dotnet.domain.Documents.Document", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using mws.backend.dotnet.domain.Audit;
|
||||||
using mws.backend.dotnet.domain.Documents;
|
using mws.backend.dotnet.domain.Documents;
|
||||||
using mws.backend.dotnet.domain.MasterData;
|
using mws.backend.dotnet.domain.MasterData;
|
||||||
using mws.backend.dotnet.domain.Projects;
|
using mws.backend.dotnet.domain.Projects;
|
||||||
@@ -20,6 +21,8 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
|||||||
public DbSet<Role> Roles => Set<Role>();
|
public DbSet<Role> Roles => Set<Role>();
|
||||||
public DbSet<RolePermission> RolePermissions => Set<RolePermission>();
|
public DbSet<RolePermission> RolePermissions => Set<RolePermission>();
|
||||||
public DbSet<MasterDataEntry> MasterData => Set<MasterDataEntry>();
|
public DbSet<MasterDataEntry> MasterData => Set<MasterDataEntry>();
|
||||||
|
public DbSet<ActionLog> ActionLogs => Set<ActionLog>();
|
||||||
|
public DbSet<LoginLog> LoginLogs => Set<LoginLog>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using mws.backend.dotnet.domain.Audit;
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
|
||||||
|
|
||||||
|
public class ActionLogConfiguration : IEntityTypeConfiguration<ActionLog>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<ActionLog> e)
|
||||||
|
{
|
||||||
|
e.ToTable("action_logs");
|
||||||
|
e.HasKey(x => x.Id);
|
||||||
|
e.Property(x => x.ActorUsername).HasMaxLength(100).IsRequired();
|
||||||
|
e.Property(x => x.Action).HasMaxLength(100).IsRequired();
|
||||||
|
e.Property(x => x.EntityType).HasMaxLength(50).IsRequired();
|
||||||
|
e.Property(x => x.EntityName).HasMaxLength(255);
|
||||||
|
e.Property(x => x.IpAddress).HasMaxLength(45);
|
||||||
|
e.Property(x => x.UserAgent).HasMaxLength(512);
|
||||||
|
e.HasIndex(x => x.CreatedAt);
|
||||||
|
e.HasIndex(x => x.ActorUserId);
|
||||||
|
e.HasIndex(x => x.Action);
|
||||||
|
e.HasIndex(x => x.EntityType);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using mws.backend.dotnet.domain.Audit;
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
|
||||||
|
|
||||||
|
public class LoginLogConfiguration : IEntityTypeConfiguration<LoginLog>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<LoginLog> e)
|
||||||
|
{
|
||||||
|
e.ToTable("login_logs");
|
||||||
|
e.HasKey(x => x.Id);
|
||||||
|
e.Property(x => x.Username).HasMaxLength(100).IsRequired();
|
||||||
|
e.Property(x => x.FailureReason).HasMaxLength(100);
|
||||||
|
e.Property(x => x.IpAddress).HasMaxLength(45);
|
||||||
|
e.Property(x => x.UserAgent).HasMaxLength(512);
|
||||||
|
e.HasIndex(x => x.CreatedAt);
|
||||||
|
e.HasIndex(x => x.Username);
|
||||||
|
e.HasIndex(x => x.Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -141,7 +141,7 @@ public static class DbSeeder
|
|||||||
"Member",
|
"Member",
|
||||||
screen => screen.Key switch
|
screen => screen.Key switch
|
||||||
{
|
{
|
||||||
"users" or "permissions" or "masterdata" => (View: false, Create: false, Edit: false, Delete: false),
|
"users" or "permissions" or "masterdata" or "actionlogs" or "loginlogs" => (View: false, Create: false, Edit: false, Delete: false),
|
||||||
"projects" => (View: true, Create: false, Edit: false, Delete: false),
|
"projects" => (View: true, Create: false, Edit: false, Delete: false),
|
||||||
_ => (View: true, Create: true, Edit: true, Delete: false),
|
_ => (View: true, Create: true, Edit: true, Delete: false),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
|
using mws.backend.dotnet.application.Common;
|
||||||
|
using mws.backend.dotnet.application.Common.Repositories;
|
||||||
|
using mws.backend.dotnet.domain.Audit;
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
|
public class ActionLogRepository(AppDbContext db) : RepositoryBase<ActionLog>(db), IActionLogRepository
|
||||||
|
{
|
||||||
|
public Task<PagedResult<ActionLog>> SearchAsync(ActionLogFilter filter, int page, int pageSize, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var query = Set.AsQueryable();
|
||||||
|
|
||||||
|
if (filter.From is { } from) query = query.Where(x => x.CreatedAt >= from);
|
||||||
|
if (filter.To is { } to) query = query.Where(x => x.CreatedAt <= to);
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.Actor))
|
||||||
|
{
|
||||||
|
var lower = filter.Actor.Trim().ToLower();
|
||||||
|
query = query.Where(x => x.ActorUsername.ToLower().Contains(lower));
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.Action))
|
||||||
|
{
|
||||||
|
var lower = filter.Action.Trim().ToLower();
|
||||||
|
query = query.Where(x => x.Action.ToLower().Contains(lower));
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.EntityType))
|
||||||
|
{
|
||||||
|
var lower = filter.EntityType.Trim().ToLower();
|
||||||
|
query = query.Where(x => x.EntityType.ToLower().Contains(lower));
|
||||||
|
}
|
||||||
|
if (filter.EntityId is { } entityId) query = query.Where(x => x.EntityId == entityId);
|
||||||
|
|
||||||
|
return query.OrderByDescending(x => x.CreatedAt).ToPagedResultAsync(page, pageSize, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using mws.backend.dotnet.application.Audit;
|
||||||
|
using mws.backend.dotnet.application.Common;
|
||||||
|
using mws.backend.dotnet.application.Common.Repositories;
|
||||||
|
using mws.backend.dotnet.domain.Audit;
|
||||||
|
|
||||||
|
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
|
public class LoginLogRepository(AppDbContext db) : RepositoryBase<LoginLog>(db), ILoginLogRepository
|
||||||
|
{
|
||||||
|
public Task<PagedResult<LoginLog>> SearchAsync(LoginLogFilter filter, int page, int pageSize, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var query = Set.AsQueryable();
|
||||||
|
|
||||||
|
if (filter.From is { } from) query = query.Where(x => x.CreatedAt >= from);
|
||||||
|
if (filter.To is { } to) query = query.Where(x => x.CreatedAt <= to);
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.Username))
|
||||||
|
{
|
||||||
|
var lower = filter.Username.Trim().ToLower();
|
||||||
|
query = query.Where(x => x.Username.ToLower().Contains(lower));
|
||||||
|
}
|
||||||
|
if (filter.Success is { } success) query = query.Where(x => x.Success == success);
|
||||||
|
|
||||||
|
return query.OrderByDescending(x => x.CreatedAt).ToPagedResultAsync(page, pageSize, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ public class UnitOfWork : IUnitOfWork
|
|||||||
Documents = new DocumentRepository(db);
|
Documents = new DocumentRepository(db);
|
||||||
Roles = new RoleRepository(db);
|
Roles = new RoleRepository(db);
|
||||||
MasterData = new MasterDataRepository(db);
|
MasterData = new MasterDataRepository(db);
|
||||||
|
ActionLogs = new ActionLogRepository(db);
|
||||||
|
LoginLogs = new LoginLogRepository(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IUserRepository Users { get; }
|
public IUserRepository Users { get; }
|
||||||
@@ -25,6 +27,8 @@ public class UnitOfWork : IUnitOfWork
|
|||||||
public IDocumentRepository Documents { get; }
|
public IDocumentRepository Documents { get; }
|
||||||
public IRoleRepository Roles { get; }
|
public IRoleRepository Roles { get; }
|
||||||
public IMasterDataRepository MasterData { get; }
|
public IMasterDataRepository MasterData { get; }
|
||||||
|
public IActionLogRepository ActionLogs { get; }
|
||||||
|
public ILoginLogRepository LoginLogs { get; }
|
||||||
|
|
||||||
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _db.SaveChangesAsync(ct);
|
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _db.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user