This commit is contained in:
Thanakarn Klangkasame
2025-09-30 11:01:02 +07:00
commit 92e614674c
182 changed files with 9596 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
using System.Security.Claims;
using AMREZ.EOP.Abstractions.Applications.UseCases.Authentications;
using AMREZ.EOP.Contracts.DTOs.Authentications.AddEmailIdentity;
using AMREZ.EOP.Contracts.DTOs.Authentications.ChangePassword;
using AMREZ.EOP.Contracts.DTOs.Authentications.DisableMfa;
using AMREZ.EOP.Contracts.DTOs.Authentications.EnableTotp;
using AMREZ.EOP.Contracts.DTOs.Authentications.Login;
using AMREZ.EOP.Contracts.DTOs.Authentications.Logout;
using AMREZ.EOP.Contracts.DTOs.Authentications.LogoutAll;
using AMREZ.EOP.Contracts.DTOs.Authentications.Register;
using AMREZ.EOP.Contracts.DTOs.Authentications.VerifyEmail;
using AMREZ.EOP.Domain.Shared.Contracts;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
namespace AMREZ.EOP.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthenticationController : ControllerBase
{
private readonly ILoginUseCase _login;
private readonly IRegisterUseCase _register;
private readonly IChangePasswordUseCase _changePassword;
private readonly IAddEmailIdentityUseCase _addEmail;
private readonly IVerifyEmailUseCase _verifyEmail;
private readonly IEnableTotpUseCase _enableTotp;
private readonly IDisableMfaUseCase _disableMfa;
private readonly ILogoutUseCase _logout;
private readonly ILogoutAllUseCase _logoutAll;
public AuthenticationController(
ILoginUseCase login,
IRegisterUseCase register,
IChangePasswordUseCase changePassword)
{
_login = login;
_register = register;
_changePassword = changePassword;
}
[HttpPost("login")]
public async Task<IActionResult> PostLogin([FromBody] LoginRequest body, CancellationToken ct)
{
var res = await _login.ExecuteAsync(body, ct);
if (res is null) return Unauthorized(new { message = "Invalid credentials" });
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, res.UserId.ToString()),
new(ClaimTypes.Name, string.IsNullOrWhiteSpace(res.DisplayName) ? res.Email : res.DisplayName),
new(ClaimTypes.Email, res.Email),
new("tenant", res.TenantId)
};
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthPolicies.Scheme));
await HttpContext.SignInAsync(AuthPolicies.Scheme, principal);
return Ok(res);
}
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterRequest body, CancellationToken ct)
{
var res = await _register.ExecuteAsync(body, ct);
if (res is null)
return Conflict(new { message = "Email already exists or tenant not found" });
return Created($"/api/authentication/users/{res.UserId}", res);
}
[HttpPost("change-password")]
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest body, CancellationToken ct)
{
var ok = await _changePassword.ExecuteAsync(body, ct);
if (!ok) return BadRequest(new { message = "Change password failed" });
return NoContent();
}
[HttpPost("logout")]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(AuthPolicies.Scheme);
return NoContent();
}
[HttpPost("email")]
public async Task<IActionResult> AddEmail([FromBody] AddEmailIdentityRequest body, CancellationToken ct)
{
var ok = await _addEmail.ExecuteAsync(body, ct);
if (!ok) return BadRequest(new { message = "Cannot add email identity" });
return NoContent();
}
[HttpPost("email/verify")]
public async Task<IActionResult> VerifyEmail([FromBody] VerifyEmailRequest body, CancellationToken ct)
{
var ok = await _verifyEmail.ExecuteAsync(body, ct);
if (!ok) return BadRequest(new { message = "Verify email failed" });
return NoContent();
}
[HttpPost("totp/enable")]
public async Task<IActionResult> EnableTotp([FromBody] EnableTotpRequest body, CancellationToken ct)
{
var res = await _enableTotp.ExecuteAsync(body, ct);
if (res is null) return BadRequest(new { message = "Enable TOTP failed" });
return Ok(res);
}
[HttpPost("disable")]
public async Task<IActionResult> DisableMfa([FromBody] DisableMfaRequest body, CancellationToken ct)
{
var ok = await _disableMfa.ExecuteAsync(body, ct);
if (!ok) return BadRequest(new { message = "Disable MFA failed" });
return NoContent();
}
[HttpPost("revoke")]
public async Task<IActionResult> Revoke([FromBody] LogoutRequest body, CancellationToken ct)
{
var ok = await _logout.ExecuteAsync(body, ct);
if (!ok) return NotFound(new { message = "Session not found" });
return NoContent();
}
[HttpPost("revoke-all")]
public async Task<IActionResult> RevokeAll([FromBody] LogoutAllRequest body, CancellationToken ct)
{
var n = await _logoutAll.ExecuteAsync(body, ct);
return Ok(new { revoked = n });
}
}

View File

@@ -0,0 +1,128 @@
using AMREZ.EOP.Abstractions.Applications.UseCases.HumanResources;
using AMREZ.EOP.Contracts.DTOs.HumanResources.EmergencyContactAdd;
using AMREZ.EOP.Contracts.DTOs.HumanResources.EmployeeAddressAdd;
using AMREZ.EOP.Contracts.DTOs.HumanResources.EmployeeBankAccountAdd;
using AMREZ.EOP.Contracts.DTOs.HumanResources.EmploymentAdd;
using AMREZ.EOP.Contracts.DTOs.HumanResources.EmploymentEnd;
using AMREZ.EOP.Contracts.DTOs.HumanResources.SetPrimaryAddress;
using AMREZ.EOP.Contracts.DTOs.HumanResources.SetPrimaryBankAccount;
using AMREZ.EOP.Contracts.DTOs.HumanResources.SetPrimaryEmergencyContact;
using AMREZ.EOP.Contracts.DTOs.HumanResources.UserProfileUpsert;
using Microsoft.AspNetCore.Mvc;
namespace AMREZ.EOP.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class HumanResourcesController : ControllerBase
{
private readonly IUpsertUserProfileUseCase _upsertProfile;
private readonly IAddEmploymentUseCase _addEmployment;
private readonly IEndEmploymentUseCase _endEmployment;
private readonly IAddEmployeeAddressUseCase _addAddress;
private readonly ISetPrimaryAddressUseCase _setPrimaryAddress;
private readonly IAddEmergencyContactUseCase _addEmergencyContact;
private readonly ISetPrimaryEmergencyContactUseCase _setPrimaryEmergencyContact;
private readonly IAddEmployeeBankAccountUseCase _addBankAccount;
private readonly ISetPrimaryBankAccountUseCase _setPrimaryBankAccount;
public HumanResourcesController(
IUpsertUserProfileUseCase upsertProfile,
IAddEmploymentUseCase addEmployment,
IEndEmploymentUseCase endEmployment,
IAddEmployeeAddressUseCase addAddress,
ISetPrimaryAddressUseCase setPrimaryAddress,
IAddEmergencyContactUseCase addEmergencyContact,
ISetPrimaryEmergencyContactUseCase setPrimaryEmergencyContact,
IAddEmployeeBankAccountUseCase addBankAccount,
ISetPrimaryBankAccountUseCase setPrimaryBankAccount)
{
_upsertProfile = upsertProfile;
_addEmployment = addEmployment;
_endEmployment = endEmployment;
_addAddress = addAddress;
_setPrimaryAddress = setPrimaryAddress;
_addEmergencyContact = addEmergencyContact;
_setPrimaryEmergencyContact = setPrimaryEmergencyContact;
_addBankAccount = addBankAccount;
_setPrimaryBankAccount = setPrimaryBankAccount;
}
// ===== User Profile =====
[HttpPost("profiles/upsert")]
public async Task<IActionResult> UpsertProfile([FromBody] UserProfileUpsertRequest body, CancellationToken ct)
{
var res = await _upsertProfile.ExecuteAsync(body, ct);
if (res is null) return BadRequest(new { message = "Upsert failed" });
return Ok(res);
}
// ===== Employment =====
[HttpPost("employments")]
public async Task<IActionResult> AddEmployment([FromBody] EmploymentAddRequest body, CancellationToken ct)
{
var res = await _addEmployment.ExecuteAsync(body, ct);
if (res is null) return BadRequest(new { message = "Add employment failed" });
return Created($"/api/humanresources/employments/{res.Id}", res);
}
[HttpPost("employments/{id:guid}/end")]
public async Task<IActionResult> EndEmployment([FromRoute] Guid id, [FromBody] EmploymentEndRequest body, CancellationToken ct)
{
if (id != body.EmploymentId) return BadRequest(new { message = "Mismatched id" });
var ok = await _endEmployment.ExecuteAsync(body, ct);
if (!ok) return NotFound(new { message = "Employment not found" });
return NoContent();
}
// ===== Addresses =====
[HttpPost("addresses")]
public async Task<IActionResult> AddAddress([FromBody] EmployeeAddressAddRequest body, CancellationToken ct)
{
var res = await _addAddress.ExecuteAsync(body, ct);
if (res is null) return BadRequest(new { message = "Add address failed" });
return Created($"/api/humanresources/addresses/{res.Id}", res);
}
[HttpPost("addresses/set-primary")]
public async Task<IActionResult> SetPrimaryAddress([FromBody] SetPrimaryAddressRequest body, CancellationToken ct)
{
var ok = await _setPrimaryAddress.ExecuteAsync(body, ct);
if (!ok) return NotFound(new { message = "Address not found" });
return NoContent();
}
// ===== Emergency Contacts =====
[HttpPost("emergency-contacts")]
public async Task<IActionResult> AddEmergencyContact([FromBody] EmergencyContactAddRequest body, CancellationToken ct)
{
var res = await _addEmergencyContact.ExecuteAsync(body, ct);
if (res is null) return BadRequest(new { message = "Add emergency contact failed" });
return Created($"/api/humanresources/emergency-contacts/{res.Id}", res);
}
[HttpPost("emergency-contacts/set-primary")]
public async Task<IActionResult> SetPrimaryEmergencyContact([FromBody] SetPrimaryEmergencyContactRequest body, CancellationToken ct)
{
var ok = await _setPrimaryEmergencyContact.ExecuteAsync(body, ct);
if (!ok) return NotFound(new { message = "Emergency contact not found" });
return NoContent();
}
// ===== Bank Accounts =====
[HttpPost("bank-accounts")]
public async Task<IActionResult> AddBankAccount([FromBody] EmployeeBankAccountAddRequest body, CancellationToken ct)
{
var res = await _addBankAccount.ExecuteAsync(body, ct);
if (res is null) return BadRequest(new { message = "Add bank account failed" });
return Created($"/api/humanresources/bank-accounts/{res.Id}", res);
}
[HttpPost("bank-accounts/set-primary")]
public async Task<IActionResult> SetPrimaryBankAccount([FromBody] SetPrimaryBankAccountRequest body, CancellationToken ct)
{
var ok = await _setPrimaryBankAccount.ExecuteAsync(body, ct);
if (!ok) return NotFound(new { message = "Bank account not found" });
return NoContent();
}
}

View File

@@ -0,0 +1,192 @@
using AMREZ.EOP.Abstractions.Applications.Tenancy;
using AMREZ.EOP.Abstractions.Applications.UseCases.Tenancy;
using AMREZ.EOP.Contracts.DTOs.Tenancy.AddBaseDomain;
using AMREZ.EOP.Contracts.DTOs.Tenancy.CreateTenant;
using AMREZ.EOP.Contracts.DTOs.Tenancy.ListDomains;
using AMREZ.EOP.Contracts.DTOs.Tenancy.ListTenants;
using AMREZ.EOP.Contracts.DTOs.Tenancy.MapDomain;
using AMREZ.EOP.Contracts.DTOs.Tenancy.RemoveBaseDomain;
using AMREZ.EOP.Contracts.DTOs.Tenancy.UnmapDomain;
using AMREZ.EOP.Contracts.DTOs.Tenancy.UpdateTenant;
using Microsoft.AspNetCore.Mvc;
namespace AMREZ.EOP.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public sealed class TenancyController : ControllerBase
{
private readonly ICreateTenantUseCase _createTenant;
private readonly IUpdateTenantUseCase _updateTenant;
private readonly IDeleteTenantUseCase _deleteTenant;
private readonly IMapDomainUseCase _mapDomain;
private readonly IUnmapDomainUseCase _unmapDomain;
private readonly IAddBaseDomainUseCase _addBaseDomain;
private readonly IRemoveBaseDomainUseCase _removeBaseDomain;
private readonly IListTenantsUseCase _listTenants;
private readonly IListDomainsUseCase _listDomains;
private readonly ITenantResolver _resolver;
public TenancyController(
ICreateTenantUseCase createTenant,
IUpdateTenantUseCase updateTenant,
IDeleteTenantUseCase deleteTenant,
IMapDomainUseCase mapDomain,
IUnmapDomainUseCase unmapDomain,
IAddBaseDomainUseCase addBaseDomain,
IRemoveBaseDomainUseCase removeBaseDomain,
IListTenantsUseCase listTenants,
IListDomainsUseCase listDomains,
ITenantResolver resolver)
{
_createTenant = createTenant;
_updateTenant = updateTenant;
_deleteTenant = deleteTenant;
_mapDomain = mapDomain;
_unmapDomain = unmapDomain;
_addBaseDomain = addBaseDomain;
_removeBaseDomain = removeBaseDomain;
_listTenants = listTenants;
_listDomains = listDomains;
_resolver = resolver;
}
private static object ErrTenantResolve => new
{
code = "TENANT_CONTEXT_NOT_RESOLVED",
message = "Check X-Tenant (e.g., public) or permission scope."
};
// Tenants
// Create: อย่า feed body เข้า resolver (tenant ยังไม่เกิด) ให้ใช้ context ปัจจุบันเท่านั้น
[HttpPost("tenants")]
public async Task<IActionResult> CreateTenant([FromBody] CreateTenantRequest body, CancellationToken ct)
{
var resolved = _resolver.Resolve(HttpContext, hint: null);
if (resolved is null)
return NotFound(ErrTenantResolve);
var res = await _createTenant.ExecuteAsync(body, ct);
if (res is null)
return Conflict(new { code = "TENANT_EXISTS", message = "Use PUT to update/reactivate." });
return Created($"/api/Tenancy/tenants/{res.TenantKey}", res);
}
[HttpPut("tenants/{tenantKey}")]
public async Task<IActionResult> UpdateTenant([FromRoute] string tenantKey, [FromBody] UpdateTenantRequest body, CancellationToken ct)
{
if (!string.Equals(tenantKey?.Trim(), body.TenantKey?.Trim(), StringComparison.OrdinalIgnoreCase))
return BadRequest(new { code = "TENANT_KEY_MISMATCH", message = "Route tenantKey must match body.TenantKey" });
// การแก้ไข tenant ที่มีอยู่: ให้ resolver ใช้ tenantKey เป็น hint เพื่อบังคับ context ให้ถูกต้อง
var resolved = _resolver.Resolve(HttpContext, hint: null);
if (resolved is null)
return NotFound(ErrTenantResolve);
var res = await _updateTenant.ExecuteAsync(body, ct);
if (res is null)
return NotFound(new { code = "TENANT_UPDATE_FAILED", message = "Update failed (not found, precondition, or tenant not resolved)" });
return Ok(res);
}
[HttpDelete("tenants/{tenantKey}")]
public async Task<IActionResult> DeleteTenant([FromRoute] string tenantKey, CancellationToken ct)
{
var resolved = _resolver.Resolve(HttpContext, hint: null);
if (resolved is null)
return NotFound(ErrTenantResolve);
var ok = await _deleteTenant.ExecuteAsync(tenantKey, ct);
if (!ok)
return NotFound(new { code = "TENANT_DELETE_FAILED", message = "Delete failed (not found or tenant not resolved)" });
return NoContent();
}
[HttpGet("tenants")]
public async Task<IActionResult> ListTenants(CancellationToken ct)
{
var resolved = _resolver.Resolve(HttpContext, hint: null);
if (resolved is null)
return NotFound(ErrTenantResolve);
var res = await _listTenants.ExecuteAsync(new ListTenantsRequest(), ct);
if (res is null)
return NotFound(new { code = "TENANT_LIST_FAILED", message = "Tenant resolve failed" });
return Ok(res);
}
// Domains
[HttpGet("domains")]
public async Task<IActionResult> ListDomains([FromQuery] string? tenantKey, CancellationToken ct)
{
var resolved = _resolver.Resolve(HttpContext, hint: null);
if (resolved is null)
return NotFound(ErrTenantResolve);
var res = await _listDomains.ExecuteAsync(new ListDomainsRequest { TenantKey = tenantKey }, ct);
if (res is null)
return NotFound(new { code = "DOMAIN_LIST_FAILED", message = "Tenant resolve failed" });
return Ok(res);
}
[HttpPost("domains/map")]
public async Task<IActionResult> MapDomain([FromBody] MapDomainRequest body, CancellationToken ct)
{
var resolved = _resolver.Resolve(HttpContext, hint: body?.TenantKey);
if (resolved is null)
return NotFound(ErrTenantResolve);
var res = await _mapDomain.ExecuteAsync(body, ct);
if (res is null)
return Conflict(new { code = "DOMAIN_MAP_FAILED", message = "Map failed (tenant/domain invalid or tenant not resolved)" });
return Ok(res);
}
[HttpDelete("domains/{domain}")]
public async Task<IActionResult> UnmapDomain([FromRoute] string domain, [FromQuery] string? tenantKey, CancellationToken ct)
{
var resolved = _resolver.Resolve(HttpContext, hint: null);
if (resolved is null)
return NotFound(ErrTenantResolve);
var res = await _unmapDomain.ExecuteAsync(new UnmapDomainRequest { Domain = domain, TenantKey = tenantKey }, ct);
if (res is null)
return NotFound(new { code = "DOMAIN_UNMAP_FAILED", message = "Unmap failed (not found or tenant not resolved)" });
return NoContent();
}
[HttpPost("domains/base")]
public async Task<IActionResult> AddBaseDomain([FromBody] AddBaseDomainRequest body, CancellationToken ct)
{
var resolved = _resolver.Resolve(HttpContext, hint: null);
if (resolved is null)
return NotFound(ErrTenantResolve);
var res = await _addBaseDomain.ExecuteAsync(body, ct);
if (res is null)
return Conflict(new { code = "BASE_DOMAIN_ADD_FAILED", message = "Add base domain failed (tenant not resolved or duplicate)" });
return Ok(res);
}
[HttpDelete("domains/base/{baseDomain}")]
public async Task<IActionResult> RemoveBaseDomain([FromRoute] string baseDomain, [FromQuery] string? tenantKey, CancellationToken ct)
{
var resolved = _resolver.Resolve(HttpContext, hint: null);
if (resolved is null)
return NotFound(ErrTenantResolve);
var res = await _removeBaseDomain.ExecuteAsync(new RemoveBaseDomainRequest { BaseDomain = baseDomain, TenantKey = tenantKey }, ct);
if (res is null)
return NotFound(new { code = "BASE_DOMAIN_REMOVE_FAILED", message = "Remove base domain failed (not found or tenant not resolved)" });
return NoContent();
}
}