initial commit

This commit is contained in:
2026-07-21 14:08:10 +07:00
commit f7cf118326
533 changed files with 41762 additions and 0 deletions
@@ -0,0 +1,64 @@
using Indotalent.Data.Entities;
using MediatR;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.WebUtilities;
using System.Text;
namespace Indotalent.Features.Setting.SystemUser.Cqrs;
public class AdminForgotPasswordResponse
{
public bool IsSuccess { get; set; }
public string Message { get; set; } = string.Empty;
}
public record AdminForgotPasswordCommand(string UserId) : IRequest<AdminForgotPasswordResponse>;
public class AdminForgotPasswordHandler : IRequestHandler<AdminForgotPasswordCommand, AdminForgotPasswordResponse>
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IEmailSender<ApplicationUser> _emailSender;
private readonly IHttpContextAccessor _httpContextAccessor;
public AdminForgotPasswordHandler(
UserManager<ApplicationUser> userManager,
IEmailSender<ApplicationUser> emailSender,
IHttpContextAccessor httpContextAccessor)
{
_userManager = userManager;
_emailSender = emailSender;
_httpContextAccessor = httpContextAccessor;
}
public async Task<AdminForgotPasswordResponse> Handle(AdminForgotPasswordCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByIdAsync(request.UserId);
if (user == null) return new AdminForgotPasswordResponse { IsSuccess = false, Message = "User not found" };
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var requestHttp = _httpContextAccessor.HttpContext?.Request;
var scheme = requestHttp?.Scheme ?? "https";
var host = requestHttp?.Host.Value ?? "localhost:8080";
var callbackUrl = $"{scheme}://{host}/account/reset-password?userId={user.Id}&code={code}";
var message = $@"
<div style=""font-family: Arial, sans-serif;"">
<h3>System Administrator Password Reset</h3>
<p>Hello, {user.FullName}!</p>
<p>An administrator has initiated a password reset for your account. Please click the link below to set your new password:</p>
<p><a href=""{callbackUrl}"" style=""color: #2196F3; font-weight: bold;"">Reset My Password</a></p>
<br/>
<p style=""font-size: 0.8em; color: #666;"">If you believe this is an error, please contact your IT support.</p>
</div>";
await _emailSender.SendPasswordResetLinkAsync(user, user.Email!, message);
return new AdminForgotPasswordResponse
{
IsSuccess = true,
Message = "Reset link sent successfully"
};
}
}
@@ -0,0 +1,63 @@
using Indotalent.Data.Entities;
using Indotalent.Infrastructure.File;
using MediatR;
using Microsoft.AspNetCore.Identity;
namespace Indotalent.Features.Setting.SystemUser.Cqrs;
public class ChangeAvatarResponse
{
public bool IsSuccess { get; set; }
public string Message { get; set; } = string.Empty;
}
public class ChangeAvatarRequest
{
public string UserId { get; set; } = string.Empty;
public byte[] FileData { get; set; } = Array.Empty<byte>();
public string FileName { get; set; } = string.Empty;
}
public record ChangeAvatarCommand(ChangeAvatarRequest Data) : IRequest<ChangeAvatarResponse>;
public class ChangeAvatarHandler : IRequestHandler<ChangeAvatarCommand, ChangeAvatarResponse>
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly FileStorageService _fileStorage;
public ChangeAvatarHandler(UserManager<ApplicationUser> userManager, FileStorageService fileStorage)
{
_userManager = userManager;
_fileStorage = fileStorage;
}
public async Task<ChangeAvatarResponse> Handle(ChangeAvatarCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByIdAsync(request.Data.UserId);
if (user == null) return new ChangeAvatarResponse { IsSuccess = false, Message = "User not found" };
try
{
var extension = Path.GetExtension(request.Data.FileName);
_fileStorage.DeleteOldAvatar(user.AvatarFile);
var newFileName = await _fileStorage.SaveAvatarAsync(user.Id, request.Data.FileData, extension);
user.AvatarFile = newFileName;
user.UpdatedAt = DateTime.Now;
var result = await _userManager.UpdateAsync(user);
return new ChangeAvatarResponse
{
IsSuccess = result.Succeeded,
Message = result.Succeeded ? "Avatar updated successfully" : "Failed to update database"
};
}
catch (Exception ex)
{
return new ChangeAvatarResponse { IsSuccess = false, Message = ex.Message };
}
}
}
@@ -0,0 +1,52 @@
using Indotalent.Data.Entities;
using MediatR;
using Microsoft.AspNetCore.Identity;
namespace Indotalent.Features.Setting.SystemUser.Cqrs;
public class ChangePasswordRequest
{
public string UserId { get; set; } = string.Empty;
public string NewPassword { get; set; } = string.Empty;
public string ConfirmPassword { get; set; } = string.Empty;
}
public class ChangePasswordResponse
{
public bool IsSuccess { get; set; }
public string Message { get; set; } = string.Empty;
}
public record ChangePasswordCommand(ChangePasswordRequest Data) : IRequest<ChangePasswordResponse>;
public class ChangePasswordHandler : IRequestHandler<ChangePasswordCommand, ChangePasswordResponse>
{
private readonly UserManager<ApplicationUser> _userManager;
public ChangePasswordHandler(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public async Task<ChangePasswordResponse> Handle(ChangePasswordCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByIdAsync(request.Data.UserId);
if (user == null) return new ChangePasswordResponse { IsSuccess = false, Message = "User not found" };
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var result = await _userManager.ResetPasswordAsync(user, token, request.Data.NewPassword);
if (result.Succeeded)
{
return new ChangePasswordResponse { IsSuccess = true, Message = "Password has been changed successfully" };
}
return new ChangePasswordResponse
{
IsSuccess = false,
Message = string.Join(", ", result.Errors.Select(e => e.Description))
};
}
}
@@ -0,0 +1,16 @@
using FluentValidation;
namespace Indotalent.Features.Setting.SystemUser.Cqrs;
public class ChangePasswordValidator : AbstractValidator<ChangePasswordRequest>
{
public ChangePasswordValidator()
{
RuleFor(x => x.UserId).NotEmpty();
RuleFor(x => x.NewPassword)
.NotEmpty().WithMessage("New password is required")
.MinimumLength(6).WithMessage("Password must be at least 6 characters");
RuleFor(x => x.ConfirmPassword)
.Equal(x => x.NewPassword).WithMessage("Passwords do not match");
}
}
@@ -0,0 +1,47 @@
using Indotalent.Data.Entities;
using MediatR;
using Microsoft.AspNetCore.Identity;
namespace Indotalent.Features.Setting.SystemUser.Cqrs;
public class ChangeRoleResponse
{
public bool IsSuccess { get; set; }
public string Message { get; set; } = string.Empty;
}
public class ChangeRoleRequest
{
public string UserId { get; set; } = string.Empty;
public List<string> Roles { get; set; } = new();
}
public record ChangeRoleCommand(ChangeRoleRequest Data) : IRequest<ChangeRoleResponse>;
public class ChangeRoleHandler : IRequestHandler<ChangeRoleCommand, ChangeRoleResponse>
{
private readonly UserManager<ApplicationUser> _userManager;
public ChangeRoleHandler(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public async Task<ChangeRoleResponse> Handle(ChangeRoleCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByIdAsync(request.Data.UserId);
if (user == null) return new ChangeRoleResponse { IsSuccess = false, Message = "User not found" };
var currentRoles = await _userManager.GetRolesAsync(user);
var removeResult = await _userManager.RemoveFromRolesAsync(user, currentRoles);
if (!removeResult.Succeeded) return new ChangeRoleResponse { IsSuccess = false, Message = "Remove role fail" };
var addResult = await _userManager.AddToRolesAsync(user, request.Data.Roles);
return new ChangeRoleResponse
{
IsSuccess = true,
Message = "Roles updated successfully"
};
}
}
@@ -0,0 +1,86 @@
using Indotalent.Data.Entities;
using Indotalent.Infrastructure.Authorization.Identity;
using MediatR;
using Microsoft.AspNetCore.Identity;
namespace Indotalent.Features.Setting.SystemUser.Cqrs;
public class CreateSystemUserRequest
{
public string? FullName { get; set; }
public string? Email { get; set; }
public string? Password { get; set; }
public string? PhoneNumber { get; set; }
public string? UserName { get; set; }
public bool EmailConfirmed { get; set; }
public bool PhoneNumberConfirmed { get; set; }
public bool TwoFactorEnabled { get; set; }
public bool IsActive { get; set; }
public bool LockoutEnabled { get; set; }
public DateTimeOffset? LockoutEnd { get; set; }
public string? SsoIdFirebase { get; set; }
public string? SsoIdKeycloak { get; set; }
public string? SsoIdAzure { get; set; }
public string? SsoIdAws { get; set; }
public string? SsoIdOther1 { get; set; }
public string? SsoIdOther2 { get; set; }
public string? SsoIdOther3 { get; set; }
}
public class CreateSystemUserResponse
{
public string? Id { get; set; }
public string? Email { get; set; }
}
public record CreateSystemUserCommand(CreateSystemUserRequest Data) : IRequest<CreateSystemUserResponse>;
public class CreateSystemUserHandler : IRequestHandler<CreateSystemUserCommand, CreateSystemUserResponse>
{
private readonly UserManager<ApplicationUser> _userManager;
public CreateSystemUserHandler(UserManager<ApplicationUser> userManager) => _userManager = userManager;
public async Task<CreateSystemUserResponse> Handle(CreateSystemUserCommand request, CancellationToken cancellationToken)
{
var user = new ApplicationUser
{
FullName = request.Data.FullName,
Email = request.Data.Email,
UserName = request.Data.Email,
PhoneNumber = request.Data.PhoneNumber,
EmailConfirmed = request.Data.EmailConfirmed,
PhoneNumberConfirmed = request.Data.PhoneNumberConfirmed,
TwoFactorEnabled = request.Data.TwoFactorEnabled,
IsActive = request.Data.IsActive,
LockoutEnabled = request.Data.LockoutEnabled,
LockoutEnd = request.Data.LockoutEnd,
SsoIdFirebase = request.Data.SsoIdFirebase,
SsoIdKeycloak = request.Data.SsoIdKeycloak,
SsoIdAzure = request.Data.SsoIdAzure,
SsoIdAws = request.Data.SsoIdAws,
SsoIdOther1 = request.Data.SsoIdOther1,
SsoIdOther2 = request.Data.SsoIdOther2,
SsoIdOther3 = request.Data.SsoIdOther3,
CreatedAt = DateTime.Now
};
var result = await _userManager.CreateAsync(user, request.Data.Password ?? "123456");
if (!result.Succeeded)
{
var errors = string.Join(", ", result.Errors.Select(e => e.Description));
throw new Exception(errors);
}
var rolesToAdd = ApplicationRoles.AllRoles
.Where(role => role != ApplicationRoles.Admin)
.ToList();
if (rolesToAdd.Any())
{
await _userManager.AddToRolesAsync(user, rolesToAdd);
}
return new CreateSystemUserResponse { Id = user.Id, Email = user.Email };
}
}
@@ -0,0 +1,30 @@
using FluentValidation;
using Indotalent.Shared.Consts;
namespace Indotalent.Features.Setting.SystemUser.Cqrs;
public class CreateSystemUserValidator : AbstractValidator<CreateSystemUserRequest>
{
public CreateSystemUserValidator()
{
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("Password is required")
.MinimumLength(4).WithMessage("Password must be at least 4 characters");
RuleFor(x => x.FullName)
.NotEmpty().WithMessage("Full Name is required")
.MaximumLength(GlobalConsts.StringLengthShort);
RuleFor(x => x.SsoIdFirebase).MaximumLength(GlobalConsts.StringLengthMedium);
RuleFor(x => x.SsoIdKeycloak).MaximumLength(GlobalConsts.StringLengthMedium);
RuleFor(x => x.SsoIdAzure).MaximumLength(GlobalConsts.StringLengthMedium);
RuleFor(x => x.SsoIdAws).MaximumLength(GlobalConsts.StringLengthMedium);
RuleFor(x => x.SsoIdOther1).MaximumLength(GlobalConsts.StringLengthMedium);
RuleFor(x => x.SsoIdOther2).MaximumLength(GlobalConsts.StringLengthMedium);
RuleFor(x => x.SsoIdOther3).MaximumLength(GlobalConsts.StringLengthMedium);
}
}
@@ -0,0 +1,26 @@
using Indotalent.Data.Entities;
using MediatR;
using Microsoft.AspNetCore.Identity;
namespace Indotalent.Features.Setting.SystemUser.Cqrs;
public record DeleteSystemUserByIdRequest(string Id);
public record DeleteSystemUserByIdCommand(DeleteSystemUserByIdRequest Data) : IRequest<bool>;
public class DeleteSystemUserByIdHandler : IRequestHandler<DeleteSystemUserByIdCommand, bool>
{
private readonly UserManager<ApplicationUser> _userManager;
public DeleteSystemUserByIdHandler(UserManager<ApplicationUser> userManager) => _userManager = userManager;
public async Task<bool> Handle(DeleteSystemUserByIdCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByIdAsync(request.Data.Id);
if (user == null) return false;
var result = await _userManager.DeleteAsync(user);
return result.Succeeded;
}
}
@@ -0,0 +1,86 @@
using Indotalent.Infrastructure.Database;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace Indotalent.Features.Setting.SystemUser.Cqrs;
public class GetSystemUserByIdResponse
{
public string? Id { get; set; }
public string? FullName { get; set; }
public string? Email { get; set; }
public string? PhoneNumber { get; set; }
public string? UserName { get; set; }
public bool EmailConfirmed { get; set; }
public bool PhoneNumberConfirmed { get; set; }
public bool TwoFactorEnabled { get; set; }
public bool IsActive { get; set; }
public bool LockoutEnabled { get; set; }
public DateTimeOffset? LockoutEnd { get; set; }
public int AccessFailedCount { get; set; }
public string? SsoIdFirebase { get; set; }
public string? SsoIdKeycloak { get; set; }
public string? SsoIdAzure { get; set; }
public string? SsoIdAws { get; set; }
public string? SsoIdOther1 { get; set; }
public string? SsoIdOther2 { get; set; }
public string? SsoIdOther3 { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastLoginAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public List<string> UserRoles { get; set; } = new();
public string? AvatarFile { get; set; }
}
public record GetSystemUserByIdQuery(string Id) : IRequest<GetSystemUserByIdResponse?>;
public class GetSystemUserByIdHandler : IRequestHandler<GetSystemUserByIdQuery, GetSystemUserByIdResponse?>
{
private readonly AppDbContext _context;
public GetSystemUserByIdHandler(AppDbContext context) => _context = context;
public async Task<GetSystemUserByIdResponse?> Handle(GetSystemUserByIdQuery request, CancellationToken cancellationToken)
{
var user = await _context.Users
.AsNoTracking()
.Where(x => x.Id == request.Id)
.Select(x => new GetSystemUserByIdResponse
{
Id = x.Id,
FullName = x.FullName,
Email = x.Email,
PhoneNumber = x.PhoneNumber,
UserName = x.UserName,
EmailConfirmed = x.EmailConfirmed,
PhoneNumberConfirmed = x.PhoneNumberConfirmed,
TwoFactorEnabled = x.TwoFactorEnabled,
IsActive = x.IsActive,
LockoutEnabled = x.LockoutEnabled,
LockoutEnd = x.LockoutEnd,
AccessFailedCount = x.AccessFailedCount,
SsoIdFirebase = x.SsoIdFirebase,
SsoIdKeycloak = x.SsoIdKeycloak,
SsoIdAzure = x.SsoIdAzure,
SsoIdAws = x.SsoIdAws,
SsoIdOther1 = x.SsoIdOther1,
SsoIdOther2 = x.SsoIdOther2,
SsoIdOther3 = x.SsoIdOther3,
CreatedAt = x.CreatedAt,
LastLoginAt = x.LastLoginAt,
UpdatedAt = x.UpdatedAt,
AvatarFile = x.AvatarFile,
})
.FirstOrDefaultAsync(cancellationToken);
if (user == null) return null;
user.UserRoles = await (from ur in _context.UserRoles
join r in _context.Roles on ur.RoleId equals r.Id
where ur.UserId == user.Id
select r.Name!)
.ToListAsync(cancellationToken);
return user;
}
}
@@ -0,0 +1,62 @@
using Indotalent.Infrastructure.Database;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace Indotalent.Features.Setting.SystemUser.Cqrs;
public class GetSystemUserListResponse
{
public string? Id { get; set; }
public string? FullName { get; set; }
public string? AvatarFile { get; set; }
public string? Email { get; set; }
public string? SsoIdFirebase { get; set; }
public string? SsoIdKeycloak { get; set; }
public bool IsActive { get; set; }
public List<string> UserRoles { get; set; } = new();
}
public record GetSystemUserListQuery() : IRequest<List<GetSystemUserListResponse>>;
public class GetSystemUserListHandler : IRequestHandler<GetSystemUserListQuery, List<GetSystemUserListResponse>>
{
private readonly AppDbContext _context;
public GetSystemUserListHandler(AppDbContext context) => _context = context;
public async Task<List<GetSystemUserListResponse>> Handle(GetSystemUserListQuery request, CancellationToken cancellationToken)
{
var users = await _context.Users
.AsNoTracking()
.OrderByDescending(x => x.CreatedAt)
.Select(x => new GetSystemUserListResponse
{
Id = x.Id,
FullName = x.FullName,
Email = x.Email,
SsoIdFirebase = x.SsoIdFirebase,
SsoIdKeycloak = x.SsoIdKeycloak,
IsActive = x.IsActive,
AvatarFile = x.AvatarFile,
})
.ToListAsync(cancellationToken);
var userIds = users.Select(u => u.Id).ToList();
var userRolesMap = await (from ur in _context.UserRoles
join r in _context.Roles on ur.RoleId equals r.Id
where userIds.Contains(ur.UserId)
select new { ur.UserId, r.Name })
.ToListAsync(cancellationToken);
foreach (var user in users)
{
user.UserRoles = userRolesMap
.Where(x => x.UserId == user.Id)
.Select(x => x.Name!)
.ToList();
}
return users;
}
}
@@ -0,0 +1,87 @@
using Indotalent.Data.Entities;
using MediatR;
using Microsoft.AspNetCore.Identity;
namespace Indotalent.Features.Setting.SystemUser.Cqrs;
public class UpdateSystemUserRequest
{
public string? Id { get; set; }
public string? FullName { get; set; }
public string? Email { get; set; }
public string? PhoneNumber { get; set; }
public string? UserName { get; set; }
public bool EmailConfirmed { get; set; }
public bool PhoneNumberConfirmed { get; set; }
public bool TwoFactorEnabled { get; set; }
public bool IsActive { get; set; }
public bool LockoutEnabled { get; set; }
public DateTimeOffset? LockoutEnd { get; set; }
public int AccessFailedCount { get; set; }
public string? SsoIdFirebase { get; set; }
public string? SsoIdKeycloak { get; set; }
public string? SsoIdAzure { get; set; }
public string? SsoIdAws { get; set; }
public string? SsoIdOther1 { get; set; }
public string? SsoIdOther2 { get; set; }
public string? SsoIdOther3 { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastLoginAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public List<string> UserRoles { get; set; } = new();
public string? AvatarFile { get; set; }
}
public class UpdateSystemUserResponse
{
public string? Id { get; set; }
public bool Success { get; set; }
}
public record UpdateSystemUserCommand(UpdateSystemUserRequest Data) : IRequest<UpdateSystemUserResponse>;
public class UpdateSystemUserHandler : IRequestHandler<UpdateSystemUserCommand, UpdateSystemUserResponse>
{
private readonly UserManager<ApplicationUser> _userManager;
public UpdateSystemUserHandler(UserManager<ApplicationUser> userManager) => _userManager = userManager;
public async Task<UpdateSystemUserResponse> Handle(UpdateSystemUserCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByIdAsync(request.Data.Id ?? string.Empty);
if (user == null)
{
return new UpdateSystemUserResponse { Id = request.Data.Id, Success = false };
}
user.FullName = request.Data.FullName;
user.PhoneNumber = request.Data.PhoneNumber;
user.IsActive = request.Data.IsActive;
user.EmailConfirmed = request.Data.EmailConfirmed;
user.PhoneNumberConfirmed = request.Data.PhoneNumberConfirmed;
user.TwoFactorEnabled = request.Data.TwoFactorEnabled;
user.LockoutEnabled = request.Data.LockoutEnabled;
user.LockoutEnd = request.Data.LockoutEnd;
user.SsoIdFirebase = request.Data.SsoIdFirebase;
user.SsoIdKeycloak = request.Data.SsoIdKeycloak;
user.SsoIdAzure = request.Data.SsoIdAzure;
user.SsoIdAws = request.Data.SsoIdAws;
user.SsoIdOther1 = request.Data.SsoIdOther1;
user.SsoIdOther2 = request.Data.SsoIdOther2;
user.SsoIdOther3 = request.Data.SsoIdOther3;
user.UpdatedAt = DateTime.Now;
user.AvatarFile = request.Data.AvatarFile;
var result = await _userManager.UpdateAsync(user);
return new UpdateSystemUserResponse
{
Id = user.Id,
Success = result.Succeeded
};
}
}
@@ -0,0 +1,27 @@
using FluentValidation;
using Indotalent.Shared.Consts;
namespace Indotalent.Features.Setting.SystemUser.Cqrs;
public class UpdateSystemUserValidator : AbstractValidator<UpdateSystemUserRequest>
{
public UpdateSystemUserValidator()
{
RuleFor(x => x.Id).NotEmpty().WithMessage("User ID is required for update");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format");
RuleFor(x => x.FullName)
.NotEmpty().WithMessage("Full Name is required")
.MaximumLength(GlobalConsts.StringLengthShort);
RuleFor(x => x.SsoIdFirebase).MaximumLength(GlobalConsts.StringLengthMedium);
RuleFor(x => x.SsoIdKeycloak).MaximumLength(GlobalConsts.StringLengthMedium);
RuleFor(x => x.SsoIdAzure).MaximumLength(GlobalConsts.StringLengthMedium);
RuleFor(x => x.SsoIdAws).MaximumLength(GlobalConsts.StringLengthMedium);
RuleFor(x => x.SsoIdOther1).MaximumLength(GlobalConsts.StringLengthMedium);
RuleFor(x => x.SsoIdOther2).MaximumLength(GlobalConsts.StringLengthMedium);
RuleFor(x => x.SsoIdOther3).MaximumLength(GlobalConsts.StringLengthMedium);
}
}