From 563a341a99e2dfba93df40406c84adb2fbd99ae6 Mon Sep 17 00:00:00 2001 From: Thanakarn Klangkasame <77600906+Simulationable@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:18:44 +0700 Subject: [PATCH] Add Login Module --- .../Controllers/AuthenticationController.cs | 89 +- .../Authentications/IIssueTokenPairUseCase.cs | 8 + .../Authentications/IRefreshUseCase.cs | 8 + .../Repositories/IUserRepository.cs | 15 +- .../Security/IJwtFactory.cs | 8 + .../Authentications/IssueTokenPairUseCase.cs | 111 ++ .../UseCases/Authentications/LoginUseCase.cs | 3 +- .../Authentications/LogoutAllUseCase.cs | 7 +- .../UseCases/Authentications/LogoutUseCase.cs | 7 +- .../Authentications/RefreshUseCase.cs | 102 ++ .../Authentications/RegisterUseCase.cs | 3 +- .../IssueTokenPair/IssueTokenPairRequest.cs | 8 + .../IssueTokenPair/IssueTokenPairResponse.cs | 9 + .../Authentications/Login/LoginResponse.cs | 1 - .../Authentications/Refresh/RefreshRequest.cs | 9 + .../Refresh/RefreshResponse.cs | 9 + .../Authentications/AccessTokenDeny.cs | 9 + .../Entities/Authentications/Permission.cs | 1 - .../Entities/Authentications/RefreshToken.cs | 18 + .../Entities/Authentications/Role.cs | 1 - .../Authentications/RolePermission.cs | 1 - .../Entities/Authentications/User.cs | 2 - .../Authentications/UserExternalAccount.cs | 1 - .../Entities/Authentications/UserIdentity.cs | 1 - .../Entities/Authentications/UserMfaFactor.cs | 1 - .../Authentications/UserPasswordHistory.cs | 1 - .../Entities/Authentications/UserRole.cs | 1 - .../Entities/Authentications/UserSession.cs | 1 - .../Entities/Common/BaseEntity.cs | 2 +- .../Entities/HumanResources/Department.cs | 1 - .../HumanResources/EmergencyContact.cs | 1 - .../HumanResources/EmployeeAddress.cs | 1 - .../HumanResources/EmployeeBankAccount.cs | 1 - .../Entities/HumanResources/Employment.cs | 1 - .../Entities/HumanResources/Position.cs | 1 - .../Entities/HumanResources/UserProfile.cs | 1 - .../AMREZ.EOP.Infrastructures.csproj | 1 + .../Data/AppDbContext.cs | 150 +- .../ServiceCollectionExtensions.cs | 5 +- .../20250924034125_InitDatabase.Designer.cs | 1536 ----------------- .../20250930101327_Add_Tenant_Guid.cs | 32 - ...> 20251002040013_InitDatabase.Designer.cs} | 213 ++- ...base.cs => 20251002040013_InitDatabase.cs} | 258 +-- .../Migrations/AppDbContextModelSnapshot.cs | 209 ++- .../Options/JwtOptions.cs | 9 + .../Repositories/TenantRepository.cs | 21 +- .../Repositories/UserProfileRepository.cs | 42 +- .../Repositories/UserRepository.cs | 153 +- .../Security/JwtFactory.cs | 51 + .../Tenancy/DefaultTenantResolver.cs | 1 - .../Tenancy/TenantMapLoader.cs | 1 - .../UnitOfWork/EFUnitOfWork.cs | 37 +- 52 files changed, 1127 insertions(+), 2036 deletions(-) create mode 100644 AMREZ.EOP.Abstractions/Applications/UseCases/Authentications/IIssueTokenPairUseCase.cs create mode 100644 AMREZ.EOP.Abstractions/Applications/UseCases/Authentications/IRefreshUseCase.cs create mode 100644 AMREZ.EOP.Abstractions/Security/IJwtFactory.cs create mode 100644 AMREZ.EOP.Application/UseCases/Authentications/IssueTokenPairUseCase.cs create mode 100644 AMREZ.EOP.Application/UseCases/Authentications/RefreshUseCase.cs create mode 100644 AMREZ.EOP.Contracts/DTOs/Authentications/IssueTokenPair/IssueTokenPairRequest.cs create mode 100644 AMREZ.EOP.Contracts/DTOs/Authentications/IssueTokenPair/IssueTokenPairResponse.cs create mode 100644 AMREZ.EOP.Contracts/DTOs/Authentications/Refresh/RefreshRequest.cs create mode 100644 AMREZ.EOP.Contracts/DTOs/Authentications/Refresh/RefreshResponse.cs create mode 100644 AMREZ.EOP.Domain/Entities/Authentications/AccessTokenDeny.cs create mode 100644 AMREZ.EOP.Domain/Entities/Authentications/RefreshToken.cs delete mode 100644 AMREZ.EOP.Infrastructures/Migrations/20250924034125_InitDatabase.Designer.cs delete mode 100644 AMREZ.EOP.Infrastructures/Migrations/20250930101327_Add_Tenant_Guid.cs rename AMREZ.EOP.Infrastructures/Migrations/{20250930101327_Add_Tenant_Guid.Designer.cs => 20251002040013_InitDatabase.Designer.cs} (89%) rename AMREZ.EOP.Infrastructures/Migrations/{20250924034125_InitDatabase.cs => 20251002040013_InitDatabase.cs} (84%) create mode 100644 AMREZ.EOP.Infrastructures/Options/JwtOptions.cs create mode 100644 AMREZ.EOP.Infrastructures/Security/JwtFactory.cs diff --git a/AMREZ.EOP.API/Controllers/AuthenticationController.cs b/AMREZ.EOP.API/Controllers/AuthenticationController.cs index 6ea7461..9f7e5ce 100644 --- a/AMREZ.EOP.API/Controllers/AuthenticationController.cs +++ b/AMREZ.EOP.API/Controllers/AuthenticationController.cs @@ -4,9 +4,11 @@ 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.IssueTokenPair; 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.Refresh; using AMREZ.EOP.Contracts.DTOs.Authentications.Register; using AMREZ.EOP.Contracts.DTOs.Authentications.VerifyEmail; using AMREZ.EOP.Domain.Shared.Contracts; @@ -32,14 +34,33 @@ public class AuthenticationController : ControllerBase private readonly ILogoutUseCase _logout; private readonly ILogoutAllUseCase _logoutAll; + private readonly IIssueTokenPairUseCase _issueTokens; + private readonly IRefreshUseCase _refresh; + public AuthenticationController( ILoginUseCase login, IRegisterUseCase register, - IChangePasswordUseCase changePassword) + IChangePasswordUseCase changePassword, + IAddEmailIdentityUseCase addEmail, + IVerifyEmailUseCase verifyEmail, + IEnableTotpUseCase enableTotp, + IDisableMfaUseCase disableMfa, + ILogoutUseCase logout, + ILogoutAllUseCase logoutAll, + IIssueTokenPairUseCase issueTokens, + IRefreshUseCase refresh) { _login = login; _register = register; _changePassword = changePassword; + _addEmail = addEmail; + _verifyEmail = verifyEmail; + _enableTotp = enableTotp; + _disableMfa = disableMfa; + _logout = logout; + _logoutAll = logoutAll; + _issueTokens = issueTokens; + _refresh = refresh; } [HttpPost("login")] @@ -51,7 +72,7 @@ public class AuthenticationController : ControllerBase var claims = new List { new(ClaimTypes.NameIdentifier, res.UserId.ToString()), - new(ClaimTypes.Name, string.IsNullOrWhiteSpace(res.DisplayName) ? res.Email : res.DisplayName), + new(ClaimTypes.Name, res.Email), new(ClaimTypes.Email, res.Email), new("tenant", res.TenantId) }; @@ -59,9 +80,71 @@ public class AuthenticationController : ControllerBase var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthPolicies.Scheme)); await HttpContext.SignInAsync(AuthPolicies.Scheme, principal); - return Ok(res); + var tokenPair = await _issueTokens.ExecuteAsync(new IssueTokenPairRequest() + { + UserId = res.UserId, + Tenant = res.TenantId, + Email = res.Email + }, ct); + + if (!string.IsNullOrWhiteSpace(tokenPair.RefreshToken)) + { + Response.Cookies.Append( + "refresh_token", + tokenPair.RefreshToken!, + new CookieOptions + { + HttpOnly = true, + Secure = true, + SameSite = SameSiteMode.Strict, + Expires = tokenPair.RefreshExpiresAt?.UtcDateTime + }); + } + + return Ok(new + { + user = res, + access_token = tokenPair.AccessToken, + token_type = "Bearer", + expires_at = tokenPair.AccessExpiresAt + }); } + [HttpPost("refresh")] + public async Task Refresh([FromBody] RefreshRequest body, CancellationToken ct) + { + var raw = string.IsNullOrWhiteSpace(body.RefreshToken) + ? Request.Cookies["refresh_token"] + : body.RefreshToken; + + if (string.IsNullOrWhiteSpace(raw)) + return Unauthorized(new { message = "Missing refresh token" }); + + var res = await _refresh.ExecuteAsync(body, ct); + if (res is null) return Unauthorized(new { message = "Invalid/expired refresh token" }); + + if (!string.IsNullOrWhiteSpace(res.RefreshToken)) + { + Response.Cookies.Append( + "refresh_token", + res.RefreshToken!, + new CookieOptions + { + HttpOnly = true, + Secure = true, + SameSite = SameSiteMode.Strict, + Expires = res.RefreshExpiresAt?.UtcDateTime + }); + } + + return Ok(new + { + access_token = res.AccessToken, + token_type = "Bearer", + expires_at = res.AccessExpiresAt + }); + } + [HttpPost("register")] public async Task Register([FromBody] RegisterRequest body, CancellationToken ct) { diff --git a/AMREZ.EOP.Abstractions/Applications/UseCases/Authentications/IIssueTokenPairUseCase.cs b/AMREZ.EOP.Abstractions/Applications/UseCases/Authentications/IIssueTokenPairUseCase.cs new file mode 100644 index 0000000..b868121 --- /dev/null +++ b/AMREZ.EOP.Abstractions/Applications/UseCases/Authentications/IIssueTokenPairUseCase.cs @@ -0,0 +1,8 @@ +using AMREZ.EOP.Contracts.DTOs.Authentications.IssueTokenPair; + +namespace AMREZ.EOP.Abstractions.Applications.UseCases.Authentications; + +public interface IIssueTokenPairUseCase +{ + Task ExecuteAsync(IssueTokenPairRequest request, CancellationToken ct); +} \ No newline at end of file diff --git a/AMREZ.EOP.Abstractions/Applications/UseCases/Authentications/IRefreshUseCase.cs b/AMREZ.EOP.Abstractions/Applications/UseCases/Authentications/IRefreshUseCase.cs new file mode 100644 index 0000000..1903d6f --- /dev/null +++ b/AMREZ.EOP.Abstractions/Applications/UseCases/Authentications/IRefreshUseCase.cs @@ -0,0 +1,8 @@ +using AMREZ.EOP.Contracts.DTOs.Authentications.Refresh; + +namespace AMREZ.EOP.Abstractions.Applications.UseCases.Authentications; + +public interface IRefreshUseCase +{ + Task ExecuteAsync(RefreshRequest request, CancellationToken ct); +} \ No newline at end of file diff --git a/AMREZ.EOP.Abstractions/Infrastructures/Repositories/IUserRepository.cs b/AMREZ.EOP.Abstractions/Infrastructures/Repositories/IUserRepository.cs index 36b3836..35598c5 100644 --- a/AMREZ.EOP.Abstractions/Infrastructures/Repositories/IUserRepository.cs +++ b/AMREZ.EOP.Abstractions/Infrastructures/Repositories/IUserRepository.cs @@ -5,27 +5,34 @@ namespace AMREZ.EOP.Abstractions.Infrastructures.Repositories; public interface IUserRepository { + // ===== Users/Identities/Password/MFA (ของเดิม) ===== Task FindByIdAsync(Guid userId, CancellationToken ct = default); Task FindActiveByEmailAsync(string email, CancellationToken ct = default); Task EmailExistsAsync(string email, CancellationToken ct = default); Task AddAsync(User user, CancellationToken ct = default); - // Identities Task AddIdentityAsync(Guid userId, IdentityType type, string identifier, bool isPrimary, CancellationToken ct = default); Task VerifyIdentityAsync(Guid userId, IdentityType type, string identifier, DateTimeOffset verifiedAt, CancellationToken ct = default); Task GetPrimaryIdentityAsync(Guid userId, IdentityType type, CancellationToken ct = default); - // Password Task ChangePasswordAsync(Guid userId, string newPasswordHash, CancellationToken ct = default); Task AddPasswordHistoryAsync(Guid userId, string passwordHash, CancellationToken ct = default); - // MFA Task AddTotpFactorAsync(Guid userId, string label, string secret, CancellationToken ct = default); Task DisableMfaFactorAsync(Guid factorId, CancellationToken ct = default); Task HasAnyMfaAsync(Guid userId, CancellationToken ct = default); - // Sessions + // ===== Sessions (เก็บ refresh ไว้ใน session) ===== Task CreateSessionAsync(UserSession session, CancellationToken ct = default); + Task FindSessionByRefreshHashAsync(Guid tenantId, string refreshTokenHash, CancellationToken ct = default); + Task RotateSessionRefreshAsync(Guid tenantId, Guid sessionId, string newRefreshTokenHash, DateTimeOffset newIssuedAt, DateTimeOffset? newExpiresAt, CancellationToken ct = default); Task RevokeSessionAsync(Guid userId, Guid sessionId, CancellationToken ct = default); Task RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default); + Task IsSessionActiveAsync(Guid userId, Guid sessionId, CancellationToken ct = default); + + // ===== Kill switches / stamps (ใช้สำหรับ revoke-all ระดับ tenant/user) ===== + Task GetTenantTokenVersionAsync(Guid tenantId, CancellationToken ct = default); + Task BumpTenantTokenVersionAsync(Guid tenantId, CancellationToken ct = default); + Task GetUserSecurityStampAsync(Guid userId, CancellationToken ct = default); + Task BumpUserSecurityStampAsync(Guid userId, CancellationToken ct = default); } \ No newline at end of file diff --git a/AMREZ.EOP.Abstractions/Security/IJwtFactory.cs b/AMREZ.EOP.Abstractions/Security/IJwtFactory.cs new file mode 100644 index 0000000..5334f96 --- /dev/null +++ b/AMREZ.EOP.Abstractions/Security/IJwtFactory.cs @@ -0,0 +1,8 @@ +using System.Security.Claims; + +namespace AMREZ.EOP.Abstractions.Security; + +public interface IJwtFactory +{ + (string token, DateTimeOffset expiresAt) CreateAccessToken(IEnumerable claims); +} \ No newline at end of file diff --git a/AMREZ.EOP.Application/UseCases/Authentications/IssueTokenPairUseCase.cs b/AMREZ.EOP.Application/UseCases/Authentications/IssueTokenPairUseCase.cs new file mode 100644 index 0000000..3994246 --- /dev/null +++ b/AMREZ.EOP.Application/UseCases/Authentications/IssueTokenPairUseCase.cs @@ -0,0 +1,111 @@ +using System.Data; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using AMREZ.EOP.Abstractions.Applications.Tenancy; +using AMREZ.EOP.Abstractions.Applications.UseCases.Authentications; +using AMREZ.EOP.Abstractions.Infrastructures.Common; +using AMREZ.EOP.Abstractions.Infrastructures.Repositories; +using AMREZ.EOP.Abstractions.Security; +using AMREZ.EOP.Contracts.DTOs.Authentications.IssueTokenPair; +using AMREZ.EOP.Domain.Entities.Authentications; +using Microsoft.AspNetCore.Http; + +namespace AMREZ.EOP.Application.UseCases.Authentications; + +public sealed class IssueTokenPairUseCase : IIssueTokenPairUseCase +{ + private readonly ITenantResolver _tenantResolver; // ctx/key for UoW + private readonly ITenantRepository _tenants; // get TenantId (Guid) + private readonly IUserRepository _users; + private readonly IJwtFactory _jwt; + private readonly IHttpContextAccessor _http; + private readonly IUnitOfWork _uow; + + private const int RefreshDays = 14; + + public IssueTokenPairUseCase( + ITenantResolver resolver, + ITenantRepository tenants, + IUserRepository users, + IJwtFactory jwt, + IHttpContextAccessor http, + IUnitOfWork uow) + { + _tenantResolver = resolver; + _tenants = tenants; + _users = users; + _jwt = jwt; + _http = http; + _uow = uow; + } + + public async Task ExecuteAsync(IssueTokenPairRequest request, CancellationToken ct = default) + { + var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext"); + + var tenantCtx = _tenantResolver.Resolve(http, request); + if (tenantCtx is null) throw new InvalidOperationException("Cannot resolve tenant context"); + + await _uow.BeginAsync(tenantCtx, IsolationLevel.ReadCommitted, ct); + + try + { + var tn = await _tenants.GetAsync(tenantCtx.Id, ct); + var tenantId = tn.TenantId; + + var refreshRaw = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); + var refreshHash = Sha256(refreshRaw); + var now = DateTimeOffset.UtcNow; + + var session = await _users.CreateSessionAsync(new UserSession + { + Id = Guid.NewGuid(), + TenantId = tenantId, + UserId = request.UserId, + RefreshTokenHash = refreshHash, + IssuedAt = now, + ExpiresAt = now.AddDays(RefreshDays), + DeviceId = http.Request.Headers["X-Device-Id"].FirstOrDefault(), + UserAgent = http.Request.Headers["User-Agent"].FirstOrDefault(), + IpAddress = http.Connection.RemoteIpAddress?.ToString() + }, ct); + + // 6) tv / sstamp + var tv = await _users.GetTenantTokenVersionAsync(tenantId, ct); + var sstamp = await _users.GetUserSecurityStampAsync(request.UserId, ct) ?? string.Empty; + + var claims = new List + { + new("sub", request.UserId.ToString()), + new("tenant_id", tenantId.ToString()), + new("sid", session.Id.ToString()), + new("jti", Guid.NewGuid().ToString("N")), + new("tv", tv), + new("sstamp", sstamp) + }; + var (access, accessExp) = _jwt.CreateAccessToken(claims); + + await _uow.CommitAsync(ct); + + return new IssueTokenPairResponse + { + AccessToken = access, + AccessExpiresAt = accessExp, + RefreshToken = refreshRaw, // raw ออกให้ client + RefreshExpiresAt = session.ExpiresAt + }; + } + catch + { + await _uow.RollbackAsync(ct); + throw; + } + } + + private static string Sha256(string raw) + { + var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(raw)); + return Convert.ToHexString(bytes); + } +} \ No newline at end of file diff --git a/AMREZ.EOP.Application/UseCases/Authentications/LoginUseCase.cs b/AMREZ.EOP.Application/UseCases/Authentications/LoginUseCase.cs index dfa2df1..4452690 100644 --- a/AMREZ.EOP.Application/UseCases/Authentications/LoginUseCase.cs +++ b/AMREZ.EOP.Application/UseCases/Authentications/LoginUseCase.cs @@ -39,8 +39,7 @@ public sealed class LoginUseCase : ILoginUseCase await _uow.CommitAsync(ct); - // NOTE: ไม่ใช้ DisplayName ใน Entity แล้ว — ส่งกลับเป็นค่าว่าง/ไปดึงจาก HR ฝั่ง API - return new LoginResponse(user.Id, string.Empty, email, tenant.Id); + return new LoginResponse(user.Id , email, tenant.Id); } catch { diff --git a/AMREZ.EOP.Application/UseCases/Authentications/LogoutAllUseCase.cs b/AMREZ.EOP.Application/UseCases/Authentications/LogoutAllUseCase.cs index 3d9d255..21dd8e6 100644 --- a/AMREZ.EOP.Application/UseCases/Authentications/LogoutAllUseCase.cs +++ b/AMREZ.EOP.Application/UseCases/Authentications/LogoutAllUseCase.cs @@ -16,12 +16,7 @@ public sealed class LogoutAllUseCase : ILogoutAllUseCase private readonly IHttpContextAccessor _http; public LogoutAllUseCase(ITenantResolver r, IUnitOfWork uow, IUserRepository users, IHttpContextAccessor http) - { - _resolver = r; - _uow = uow; - _users = users; - _http = http; - } + { _resolver = r; _uow = uow; _users = users; _http = http; } public async Task ExecuteAsync(LogoutAllRequest request, CancellationToken ct = default) { diff --git a/AMREZ.EOP.Application/UseCases/Authentications/LogoutUseCase.cs b/AMREZ.EOP.Application/UseCases/Authentications/LogoutUseCase.cs index da25aa8..55f74be 100644 --- a/AMREZ.EOP.Application/UseCases/Authentications/LogoutUseCase.cs +++ b/AMREZ.EOP.Application/UseCases/Authentications/LogoutUseCase.cs @@ -4,6 +4,7 @@ using AMREZ.EOP.Abstractions.Applications.UseCases.Authentications; using AMREZ.EOP.Abstractions.Infrastructures.Common; using AMREZ.EOP.Abstractions.Infrastructures.Repositories; using AMREZ.EOP.Contracts.DTOs.Authentications.Logout; +using AMREZ.EOP.Domain.Entities.Authentications; using Microsoft.AspNetCore.Http; namespace AMREZ.EOP.Application.UseCases.Authentications; @@ -31,6 +32,10 @@ public sealed class LogoutUseCase : ILogoutUseCase await _uow.CommitAsync(ct); return n > 0; } - catch { await _uow.RollbackAsync(ct); throw; } + catch + { + await _uow.RollbackAsync(ct); + throw; + } } } \ No newline at end of file diff --git a/AMREZ.EOP.Application/UseCases/Authentications/RefreshUseCase.cs b/AMREZ.EOP.Application/UseCases/Authentications/RefreshUseCase.cs new file mode 100644 index 0000000..0b69f9b --- /dev/null +++ b/AMREZ.EOP.Application/UseCases/Authentications/RefreshUseCase.cs @@ -0,0 +1,102 @@ +using System.Data; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using AMREZ.EOP.Abstractions.Applications.Tenancy; +using AMREZ.EOP.Abstractions.Applications.UseCases.Authentications; +using AMREZ.EOP.Abstractions.Infrastructures.Common; +using AMREZ.EOP.Abstractions.Infrastructures.Repositories; +using AMREZ.EOP.Abstractions.Security; +using AMREZ.EOP.Contracts.DTOs.Authentications.Refresh; +using Microsoft.AspNetCore.Http; + +namespace AMREZ.EOP.Application.UseCases.Authentications; + +public sealed class RefreshUseCase : IRefreshUseCase +{ + private readonly ITenantResolver _resolver; + private readonly ITenantRepository _tenants; + private readonly IUserRepository _users; + private readonly IJwtFactory _jwt; + private readonly IHttpContextAccessor _http; + private readonly IUnitOfWork _uow; + + private const int RefreshDays = 14; + + public RefreshUseCase( + ITenantResolver resolver, + ITenantRepository tenants, + IUserRepository users, + IJwtFactory jwt, + IHttpContextAccessor http, + IUnitOfWork uow) + { _resolver = resolver; _tenants = tenants; _users = users; _jwt = jwt; _http = http; _uow = uow; } + + public async Task ExecuteAsync(RefreshRequest request, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(request.RefreshToken)) return null; + + var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext"); + var tenantCtx = _resolver.Resolve(http, request); + if (tenantCtx is null) return null; + + await _uow.BeginAsync(tenantCtx, IsolationLevel.ReadCommitted, ct); + try + { + var tn = await _tenants.GetAsync(tenantCtx.TenantKey, ct); + var tenantId = tn.TenantId; + + var hash = Sha256(request.RefreshToken); + var session = await _users.FindSessionByRefreshHashAsync(tenantId, hash, ct); + if (session is null || session.RevokedAt.HasValue || + (session.ExpiresAt.HasValue && session.ExpiresAt.Value <= DateTimeOffset.UtcNow)) + { await _uow.RollbackAsync(ct); return null; } + + if (!await _users.IsSessionActiveAsync(session.UserId, session.Id, ct)) + { await _uow.RollbackAsync(ct); return null; } + + var tv = await _users.GetTenantTokenVersionAsync(tenantId, ct); + var sstamp = await _users.GetUserSecurityStampAsync(session.UserId, ct) ?? string.Empty; + + var claims = new List + { + new("sub", session.UserId.ToString()), + new("tenant_id", tenantId.ToString()), + new("sid", session.Id.ToString()), + new("jti", Guid.NewGuid().ToString("N")), + new("tv", tv), + new("sstamp", sstamp) + }; + var (access, accessExp) = _jwt.CreateAccessToken(claims); + + var newRaw = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); + var newHash = Sha256(newRaw); + var now = DateTimeOffset.UtcNow; + var newExp = now.AddDays(RefreshDays); + + var ok = await _users.RotateSessionRefreshAsync(tenantId, session.Id, newHash, now, newExp, ct); + if (!ok) { await _uow.RollbackAsync(ct); return null; } + + await _uow.CommitAsync(ct); + + return new RefreshResponse + { + AccessToken = access, + AccessExpiresAt = accessExp, + RefreshToken = newRaw, + RefreshExpiresAt = newExp + }; + } + catch + { + await _uow.RollbackAsync(ct); + throw; + } + } + + private static string Sha256(string raw) + { + var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(raw)); + return Convert.ToHexString(bytes); + } +} \ No newline at end of file diff --git a/AMREZ.EOP.Application/UseCases/Authentications/RegisterUseCase.cs b/AMREZ.EOP.Application/UseCases/Authentications/RegisterUseCase.cs index 622c63e..3426fa1 100644 --- a/AMREZ.EOP.Application/UseCases/Authentications/RegisterUseCase.cs +++ b/AMREZ.EOP.Application/UseCases/Authentications/RegisterUseCase.cs @@ -64,9 +64,9 @@ public sealed class RegisterUseCase : IRegisterUseCase IsActive = true, }; - // แนบอัตลักษณ์แบบ Email (เก็บในตารางลูก) user.Identities.Add(new UserIdentity { + TenantId = tn.TenantId, Type = IdentityType.Email, Identifier = emailNorm, IsPrimary = true, @@ -76,7 +76,6 @@ public sealed class RegisterUseCase : IRegisterUseCase await _users.AddAsync(user, ct); await _uow.CommitAsync(ct); - // ไม่ส่ง DisplayName (ปล่อยให้ HR/Presentation สร้าง) return new RegisterResponse(user.Id, string.Empty, emailNorm, tenant.Id); } catch diff --git a/AMREZ.EOP.Contracts/DTOs/Authentications/IssueTokenPair/IssueTokenPairRequest.cs b/AMREZ.EOP.Contracts/DTOs/Authentications/IssueTokenPair/IssueTokenPairRequest.cs new file mode 100644 index 0000000..e6a9a81 --- /dev/null +++ b/AMREZ.EOP.Contracts/DTOs/Authentications/IssueTokenPair/IssueTokenPairRequest.cs @@ -0,0 +1,8 @@ +namespace AMREZ.EOP.Contracts.DTOs.Authentications.IssueTokenPair; + +public sealed class IssueTokenPairRequest +{ + public Guid UserId { get; init; } + public string Tenant { get; init; } = default!; + public string Email { get; init; } = default!; +} \ No newline at end of file diff --git a/AMREZ.EOP.Contracts/DTOs/Authentications/IssueTokenPair/IssueTokenPairResponse.cs b/AMREZ.EOP.Contracts/DTOs/Authentications/IssueTokenPair/IssueTokenPairResponse.cs new file mode 100644 index 0000000..19de1e9 --- /dev/null +++ b/AMREZ.EOP.Contracts/DTOs/Authentications/IssueTokenPair/IssueTokenPairResponse.cs @@ -0,0 +1,9 @@ +namespace AMREZ.EOP.Contracts.DTOs.Authentications.IssueTokenPair; + +public sealed class IssueTokenPairResponse +{ + public string AccessToken { get; init; } = default!; + public DateTimeOffset AccessExpiresAt { get; init; } + public string? RefreshToken { get; init; } + public DateTimeOffset? RefreshExpiresAt { get; init; } +} \ No newline at end of file diff --git a/AMREZ.EOP.Contracts/DTOs/Authentications/Login/LoginResponse.cs b/AMREZ.EOP.Contracts/DTOs/Authentications/Login/LoginResponse.cs index 882aff1..dd73c05 100644 --- a/AMREZ.EOP.Contracts/DTOs/Authentications/Login/LoginResponse.cs +++ b/AMREZ.EOP.Contracts/DTOs/Authentications/Login/LoginResponse.cs @@ -2,7 +2,6 @@ namespace AMREZ.EOP.Contracts.DTOs.Authentications.Login; public sealed record LoginResponse( Guid UserId, - string DisplayName, string Email, string TenantId ); \ No newline at end of file diff --git a/AMREZ.EOP.Contracts/DTOs/Authentications/Refresh/RefreshRequest.cs b/AMREZ.EOP.Contracts/DTOs/Authentications/Refresh/RefreshRequest.cs new file mode 100644 index 0000000..9f3b684 --- /dev/null +++ b/AMREZ.EOP.Contracts/DTOs/Authentications/Refresh/RefreshRequest.cs @@ -0,0 +1,9 @@ +namespace AMREZ.EOP.Contracts.DTOs.Authentications.Refresh; + +public sealed class RefreshRequest +{ + /// + /// ถ้าไม่ส่งมา จะไปอ่านจาก HttpOnly cookie ชื่อ "refresh_token" ใน Controller + /// + public string? RefreshToken { get; init; } +} \ No newline at end of file diff --git a/AMREZ.EOP.Contracts/DTOs/Authentications/Refresh/RefreshResponse.cs b/AMREZ.EOP.Contracts/DTOs/Authentications/Refresh/RefreshResponse.cs new file mode 100644 index 0000000..a4ac997 --- /dev/null +++ b/AMREZ.EOP.Contracts/DTOs/Authentications/Refresh/RefreshResponse.cs @@ -0,0 +1,9 @@ +namespace AMREZ.EOP.Contracts.DTOs.Authentications.Refresh; + +public sealed class RefreshResponse +{ + public string AccessToken { get; init; } = default!; + public DateTimeOffset AccessExpiresAt { get; init; } + public string? RefreshToken { get; init; } + public DateTimeOffset? RefreshExpiresAt { get; init; } +} \ No newline at end of file diff --git a/AMREZ.EOP.Domain/Entities/Authentications/AccessTokenDeny.cs b/AMREZ.EOP.Domain/Entities/Authentications/AccessTokenDeny.cs new file mode 100644 index 0000000..d4ba3d9 --- /dev/null +++ b/AMREZ.EOP.Domain/Entities/Authentications/AccessTokenDeny.cs @@ -0,0 +1,9 @@ +using AMREZ.EOP.Domain.Entities.Common; + +namespace AMREZ.EOP.Domain.Entities.Authentications; + +public sealed class AccessTokenDeny : BaseEntity +{ + public string Jti { get; set; } = default!; + public DateTimeOffset ExpiresAt { get; set; } +} \ No newline at end of file diff --git a/AMREZ.EOP.Domain/Entities/Authentications/Permission.cs b/AMREZ.EOP.Domain/Entities/Authentications/Permission.cs index 8e0b922..e09c2b6 100644 --- a/AMREZ.EOP.Domain/Entities/Authentications/Permission.cs +++ b/AMREZ.EOP.Domain/Entities/Authentications/Permission.cs @@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications; public sealed class Permission : BaseEntity { - public Guid TenantId { get; set; } public string Code { get; set; } = default!; // e.g. "auth:session:read" public string Name { get; set; } = default!; diff --git a/AMREZ.EOP.Domain/Entities/Authentications/RefreshToken.cs b/AMREZ.EOP.Domain/Entities/Authentications/RefreshToken.cs new file mode 100644 index 0000000..29007a6 --- /dev/null +++ b/AMREZ.EOP.Domain/Entities/Authentications/RefreshToken.cs @@ -0,0 +1,18 @@ +using AMREZ.EOP.Domain.Entities.Common; + +namespace AMREZ.EOP.Domain.Entities.Authentications; + +public sealed class RefreshToken : BaseEntity +{ + public string FamilyId { get; set; } = default!; + public string RefreshTokenHash { get; set; } = default!; + + public Guid UserId { get; set; } + public Guid SessionId { get; set; } + + public DateTimeOffset ExpiresAt { get; set; } + public DateTimeOffset? RevokedAt { get; set; } + + public User User { get; set; } = default!; + public UserSession Session { get; set; } = default!; +} \ No newline at end of file diff --git a/AMREZ.EOP.Domain/Entities/Authentications/Role.cs b/AMREZ.EOP.Domain/Entities/Authentications/Role.cs index bad792d..123534d 100644 --- a/AMREZ.EOP.Domain/Entities/Authentications/Role.cs +++ b/AMREZ.EOP.Domain/Entities/Authentications/Role.cs @@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications; public sealed class Role : BaseEntity { - public Guid TenantId { get; set; } public string Code { get; set; } = default!; // system code, unique per tenant public string Name { get; set; } = default!; diff --git a/AMREZ.EOP.Domain/Entities/Authentications/RolePermission.cs b/AMREZ.EOP.Domain/Entities/Authentications/RolePermission.cs index 3580fec..4b9f234 100644 --- a/AMREZ.EOP.Domain/Entities/Authentications/RolePermission.cs +++ b/AMREZ.EOP.Domain/Entities/Authentications/RolePermission.cs @@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications; public sealed class RolePermission : BaseEntity { - public Guid TenantId { get; set; } public Guid RoleId { get; set; } public Guid PermissionId { get; set; } diff --git a/AMREZ.EOP.Domain/Entities/Authentications/User.cs b/AMREZ.EOP.Domain/Entities/Authentications/User.cs index 9a77933..cf3e91e 100644 --- a/AMREZ.EOP.Domain/Entities/Authentications/User.cs +++ b/AMREZ.EOP.Domain/Entities/Authentications/User.cs @@ -4,8 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications; public sealed class User : BaseEntity { - public Guid TenantId { get; set; } - public string PasswordHash { get; set; } = default!; public bool IsActive { get; set; } = true; diff --git a/AMREZ.EOP.Domain/Entities/Authentications/UserExternalAccount.cs b/AMREZ.EOP.Domain/Entities/Authentications/UserExternalAccount.cs index fab642c..287be31 100644 --- a/AMREZ.EOP.Domain/Entities/Authentications/UserExternalAccount.cs +++ b/AMREZ.EOP.Domain/Entities/Authentications/UserExternalAccount.cs @@ -5,7 +5,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications; public sealed class UserExternalAccount : BaseEntity { - public Guid TenantId { get; set; } public Guid UserId { get; set; } public ExternalProvider Provider { get; set; } diff --git a/AMREZ.EOP.Domain/Entities/Authentications/UserIdentity.cs b/AMREZ.EOP.Domain/Entities/Authentications/UserIdentity.cs index 9e1e90a..e5cd795 100644 --- a/AMREZ.EOP.Domain/Entities/Authentications/UserIdentity.cs +++ b/AMREZ.EOP.Domain/Entities/Authentications/UserIdentity.cs @@ -5,7 +5,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications; public sealed class UserIdentity : BaseEntity { - public Guid TenantId { get; set; } public Guid UserId { get; set; } public IdentityType Type { get; set; } diff --git a/AMREZ.EOP.Domain/Entities/Authentications/UserMfaFactor.cs b/AMREZ.EOP.Domain/Entities/Authentications/UserMfaFactor.cs index 15de337..7cbbcad 100644 --- a/AMREZ.EOP.Domain/Entities/Authentications/UserMfaFactor.cs +++ b/AMREZ.EOP.Domain/Entities/Authentications/UserMfaFactor.cs @@ -5,7 +5,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications; public sealed class UserMfaFactor : BaseEntity { - public Guid TenantId { get; set; } public Guid UserId { get; set; } public MfaType Type { get; set; } diff --git a/AMREZ.EOP.Domain/Entities/Authentications/UserPasswordHistory.cs b/AMREZ.EOP.Domain/Entities/Authentications/UserPasswordHistory.cs index bba1b73..99b2847 100644 --- a/AMREZ.EOP.Domain/Entities/Authentications/UserPasswordHistory.cs +++ b/AMREZ.EOP.Domain/Entities/Authentications/UserPasswordHistory.cs @@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications; public sealed class UserPasswordHistory : BaseEntity { - public Guid TenantId { get; set; } public Guid UserId { get; set; } public string PasswordHash { get; set; } = default!; diff --git a/AMREZ.EOP.Domain/Entities/Authentications/UserRole.cs b/AMREZ.EOP.Domain/Entities/Authentications/UserRole.cs index 696ef27..9907f31 100644 --- a/AMREZ.EOP.Domain/Entities/Authentications/UserRole.cs +++ b/AMREZ.EOP.Domain/Entities/Authentications/UserRole.cs @@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications; public sealed class UserRole : BaseEntity { - public Guid TenantId { get; set; } public Guid UserId { get; set; } public Guid RoleId { get; set; } diff --git a/AMREZ.EOP.Domain/Entities/Authentications/UserSession.cs b/AMREZ.EOP.Domain/Entities/Authentications/UserSession.cs index 0c75e9d..fe0b773 100644 --- a/AMREZ.EOP.Domain/Entities/Authentications/UserSession.cs +++ b/AMREZ.EOP.Domain/Entities/Authentications/UserSession.cs @@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications; public sealed class UserSession : BaseEntity { - public Guid TenantId { get; set; } public Guid UserId { get; set; } public string RefreshTokenHash { get; set; } = default!; diff --git a/AMREZ.EOP.Domain/Entities/Common/BaseEntity.cs b/AMREZ.EOP.Domain/Entities/Common/BaseEntity.cs index e65932d..60919e6 100644 --- a/AMREZ.EOP.Domain/Entities/Common/BaseEntity.cs +++ b/AMREZ.EOP.Domain/Entities/Common/BaseEntity.cs @@ -3,7 +3,7 @@ namespace AMREZ.EOP.Domain.Entities.Common; public abstract class BaseEntity { public Guid Id { get; set; } - public string TenantId { get; set; } = default!; + public Guid TenantId { get; set; } = default!; public DateTimeOffset CreatedAt { get; set; } public string? CreatedBy { get; set; } diff --git a/AMREZ.EOP.Domain/Entities/HumanResources/Department.cs b/AMREZ.EOP.Domain/Entities/HumanResources/Department.cs index 3665516..9902951 100644 --- a/AMREZ.EOP.Domain/Entities/HumanResources/Department.cs +++ b/AMREZ.EOP.Domain/Entities/HumanResources/Department.cs @@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources; public sealed class Department : BaseEntity { - public Guid TenantId { get; set; } public string Code { get; set; } = default!; public string Name { get; set; } = default!; diff --git a/AMREZ.EOP.Domain/Entities/HumanResources/EmergencyContact.cs b/AMREZ.EOP.Domain/Entities/HumanResources/EmergencyContact.cs index 0dfcfae..d8cdda7 100644 --- a/AMREZ.EOP.Domain/Entities/HumanResources/EmergencyContact.cs +++ b/AMREZ.EOP.Domain/Entities/HumanResources/EmergencyContact.cs @@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources; public sealed class EmergencyContact : BaseEntity { - public Guid TenantId { get; set; } public Guid UserProfileId { get; set; } public string Name { get; set; } = default!; diff --git a/AMREZ.EOP.Domain/Entities/HumanResources/EmployeeAddress.cs b/AMREZ.EOP.Domain/Entities/HumanResources/EmployeeAddress.cs index 9e609d5..c23d86d 100644 --- a/AMREZ.EOP.Domain/Entities/HumanResources/EmployeeAddress.cs +++ b/AMREZ.EOP.Domain/Entities/HumanResources/EmployeeAddress.cs @@ -5,7 +5,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources; public sealed class EmployeeAddress : BaseEntity { - public Guid TenantId { get; set; } public Guid UserProfileId { get; set; } public AddressType Type { get; set; } = AddressType.Home; diff --git a/AMREZ.EOP.Domain/Entities/HumanResources/EmployeeBankAccount.cs b/AMREZ.EOP.Domain/Entities/HumanResources/EmployeeBankAccount.cs index 0718853..7c8590b 100644 --- a/AMREZ.EOP.Domain/Entities/HumanResources/EmployeeBankAccount.cs +++ b/AMREZ.EOP.Domain/Entities/HumanResources/EmployeeBankAccount.cs @@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources; public sealed class EmployeeBankAccount : BaseEntity { - public Guid TenantId { get; set; } public Guid UserProfileId { get; set; } public string BankName { get; set; } = default!; diff --git a/AMREZ.EOP.Domain/Entities/HumanResources/Employment.cs b/AMREZ.EOP.Domain/Entities/HumanResources/Employment.cs index 0013355..f2212b7 100644 --- a/AMREZ.EOP.Domain/Entities/HumanResources/Employment.cs +++ b/AMREZ.EOP.Domain/Entities/HumanResources/Employment.cs @@ -5,7 +5,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources; public sealed class Employment : BaseEntity { - public Guid TenantId { get; set; } public Guid UserProfileId { get; set; } public EmploymentType EmploymentType { get; set; } = EmploymentType.Permanent; diff --git a/AMREZ.EOP.Domain/Entities/HumanResources/Position.cs b/AMREZ.EOP.Domain/Entities/HumanResources/Position.cs index c363726..3d807da 100644 --- a/AMREZ.EOP.Domain/Entities/HumanResources/Position.cs +++ b/AMREZ.EOP.Domain/Entities/HumanResources/Position.cs @@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources; public sealed class Position : BaseEntity { - public Guid TenantId { get; set; } public string Code { get; set; } = default!; public string Title { get; set; } = default!; public int? Level { get; set; } diff --git a/AMREZ.EOP.Domain/Entities/HumanResources/UserProfile.cs b/AMREZ.EOP.Domain/Entities/HumanResources/UserProfile.cs index 0e03428..1dbd9fa 100644 --- a/AMREZ.EOP.Domain/Entities/HumanResources/UserProfile.cs +++ b/AMREZ.EOP.Domain/Entities/HumanResources/UserProfile.cs @@ -6,7 +6,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources; public sealed class UserProfile : BaseEntity { - public Guid TenantId { get; set; } public Guid UserId { get; set; } public string FirstName { get; set; } = default!; diff --git a/AMREZ.EOP.Infrastructures/AMREZ.EOP.Infrastructures.csproj b/AMREZ.EOP.Infrastructures/AMREZ.EOP.Infrastructures.csproj index 14593c7..00b400c 100644 --- a/AMREZ.EOP.Infrastructures/AMREZ.EOP.Infrastructures.csproj +++ b/AMREZ.EOP.Infrastructures/AMREZ.EOP.Infrastructures.csproj @@ -17,6 +17,7 @@ + diff --git a/AMREZ.EOP.Infrastructures/Data/AppDbContext.cs b/AMREZ.EOP.Infrastructures/Data/AppDbContext.cs index 85bb717..0ca8286 100644 --- a/AMREZ.EOP.Infrastructures/Data/AppDbContext.cs +++ b/AMREZ.EOP.Infrastructures/Data/AppDbContext.cs @@ -3,6 +3,7 @@ using AMREZ.EOP.Domain.Entities.Common; using AMREZ.EOP.Domain.Entities.HumanResources; using AMREZ.EOP.Domain.Entities.Tenancy; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; namespace AMREZ.EOP.Infrastructures.Data; @@ -37,11 +38,14 @@ public class AppDbContext : DbContext protected override void OnModelCreating(ModelBuilder model) { - // ====== Global Tenancy Config (meta schema) — ไม่สืบทอด BaseEntity ====== + // ====== Tenancy (meta) ====== model.Entity(b => { b.ToTable("tenants", schema: "meta"); - b.HasKey(x => x.TenantKey); + b.HasKey(x => x.TenantKey); // PK = key (slug) + b.HasAlternateKey(x => x.TenantId); // AK = GUID + b.HasIndex(x => x.TenantId).IsUnique(); + b.Property(x => x.TenantKey).HasMaxLength(128).IsRequired(); b.Property(x => x.Schema).HasMaxLength(128); b.Property(x => x.ConnectionString); @@ -58,7 +62,7 @@ public class AppDbContext : DbContext b.ToTable("tenant_domains", schema: "meta"); b.HasKey(x => x.Domain); b.Property(x => x.Domain).HasMaxLength(253).IsRequired(); - b.Property(x => x.TenantKey).HasMaxLength(128); + b.Property(x => x.TenantKey).HasMaxLength(128); // optional b.Property(x => x.IsPlatformBaseDomain).HasDefaultValue(false); b.Property(x => x.IsActive).HasDefaultValue(true); b.Property(x => x.UpdatedAtUtc) @@ -81,36 +85,13 @@ public class AppDbContext : DbContext b.ToTable("users"); b.HasKey(x => x.Id); + // principal key สำหรับ composite FK จากลูก ๆ + b.HasAlternateKey(u => new { u.TenantId, u.Id }); + b.Property(x => x.PasswordHash).IsRequired(); b.Property(x => x.IsActive).HasDefaultValue(true); - b.Property(x => x.AccessFailedCount).HasDefaultValue(0); b.Property(x => x.MfaEnabled).HasDefaultValue(false); - - b.HasMany(x => x.Identities) - .WithOne(i => i.User) - .HasForeignKey(i => i.UserId) - .OnDelete(DeleteBehavior.Cascade); - - b.HasMany(x => x.MfaFactors) - .WithOne(i => i.User) - .HasForeignKey(i => i.UserId) - .OnDelete(DeleteBehavior.Cascade); - - b.HasMany(x => x.Sessions) - .WithOne(s => s.User) - .HasForeignKey(s => s.UserId) - .OnDelete(DeleteBehavior.Cascade); - - b.HasMany(x => x.PasswordHistories) - .WithOne(ph => ph.User) - .HasForeignKey(ph => ph.UserId) - .OnDelete(DeleteBehavior.Cascade); - - b.HasMany(x => x.ExternalAccounts) - .WithOne(ea => ea.User) - .HasForeignKey(ea => ea.UserId) - .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => @@ -122,11 +103,16 @@ public class AppDbContext : DbContext b.Property(x => x.Identifier).IsRequired().HasMaxLength(256); b.Property(x => x.IsPrimary).HasDefaultValue(false); - b.HasIndex(x => new { x.TenantId, x.Type, x.Identifier }) - .IsUnique(); - + b.HasIndex(x => new { x.TenantId, x.Type, x.Identifier }).IsUnique(); b.HasIndex(x => new { x.TenantId, x.UserId, x.Type, x.IsPrimary }) .HasDatabaseName("ix_user_identity_primary_per_type"); + + // (TenantId, UserId) -> User.(TenantId, Id) + b.HasOne(i => i.User) + .WithMany(u => u.Identities) + .HasForeignKey(i => new { i.TenantId, i.UserId }) + .HasPrincipalKey(nameof(User.TenantId), nameof(User.Id)) + .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => @@ -136,8 +122,13 @@ public class AppDbContext : DbContext b.Property(x => x.Type).IsRequired(); b.Property(x => x.Enabled).HasDefaultValue(true); - b.HasIndex(x => new { x.TenantId, x.UserId }); + + b.HasOne(x => x.User) + .WithMany(u => u.MfaFactors) + .HasForeignKey(x => new { x.TenantId, x.UserId }) + .HasPrincipalKey(nameof(User.TenantId), nameof(User.Id)) + .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => @@ -148,6 +139,12 @@ public class AppDbContext : DbContext b.Property(x => x.RefreshTokenHash).IsRequired(); b.HasIndex(x => new { x.TenantId, x.UserId }); b.HasIndex(x => new { x.TenantId, x.DeviceId }); + + b.HasOne(x => x.User) + .WithMany(u => u.Sessions) + .HasForeignKey(x => new { x.TenantId, x.UserId }) + .HasPrincipalKey(nameof(User.TenantId), nameof(User.Id)) + .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => @@ -156,6 +153,12 @@ public class AppDbContext : DbContext b.HasKey(x => x.Id); b.Property(x => x.PasswordHash).IsRequired(); b.HasIndex(x => new { x.TenantId, x.UserId, x.ChangedAt }); + + b.HasOne(x => x.User) + .WithMany(u => u.PasswordHistories) + .HasForeignKey(x => new { x.TenantId, x.UserId }) + .HasPrincipalKey(nameof(User.TenantId), nameof(User.Id)) + .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => @@ -165,15 +168,20 @@ public class AppDbContext : DbContext b.Property(x => x.Provider).IsRequired(); b.Property(x => x.Subject).IsRequired(); + b.HasIndex(x => new { x.TenantId, x.Provider, x.Subject }).IsUnique(); - b.HasIndex(x => new { x.TenantId, x.Provider, x.Subject }) - .IsUnique(); + b.HasOne(x => x.User) + .WithMany(u => u.ExternalAccounts) + .HasForeignKey(x => new { x.TenantId, x.UserId }) + .HasPrincipalKey(nameof(User.TenantId), nameof(User.Id)) + .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => { b.ToTable("roles"); b.HasKey(x => x.Id); + b.HasAlternateKey(r => new { r.TenantId, r.Id }); b.Property(x => x.Code).IsRequired().HasMaxLength(128); b.Property(x => x.Name).IsRequired().HasMaxLength(256); @@ -185,6 +193,7 @@ public class AppDbContext : DbContext { b.ToTable("permissions"); b.HasKey(x => x.Id); + b.HasAlternateKey(p => new { p.TenantId, p.Id }); b.Property(x => x.Code).IsRequired().HasMaxLength(256); b.Property(x => x.Name).IsRequired().HasMaxLength(256); @@ -198,6 +207,18 @@ public class AppDbContext : DbContext b.HasKey(x => x.Id); b.HasIndex(x => new { x.TenantId, x.UserId, x.RoleId }).IsUnique(); + + b.HasOne() + .WithMany() + .HasForeignKey(x => new { x.TenantId, x.UserId }) + .HasPrincipalKey(nameof(User.TenantId), nameof(User.Id)) + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne() + .WithMany() + .HasForeignKey(x => new { x.TenantId, x.RoleId }) + .HasPrincipalKey(nameof(Role.TenantId), nameof(Role.Id)) + .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => @@ -206,6 +227,18 @@ public class AppDbContext : DbContext b.HasKey(x => x.Id); b.HasIndex(x => new { x.TenantId, x.RoleId, x.PermissionId }).IsUnique(); + + b.HasOne() + .WithMany() + .HasForeignKey(x => new { x.TenantId, x.RoleId }) + .HasPrincipalKey(nameof(Role.TenantId), nameof(Role.Id)) + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne() + .WithMany() + .HasForeignKey(x => new { x.TenantId, x.PermissionId }) + .HasPrincipalKey(nameof(Permission.TenantId), nameof(Permission.Id)) + .OnDelete(DeleteBehavior.Cascade); }); // ====== HR ====== @@ -213,22 +246,25 @@ public class AppDbContext : DbContext { b.ToTable("user_profiles"); b.HasKey(x => x.Id); + b.HasAlternateKey(p => new { p.TenantId, p.Id }); b.Property(x => x.FirstName).IsRequired().HasMaxLength(128); b.Property(x => x.LastName).IsRequired().HasMaxLength(128); - b.HasOne(x => x.User) - .WithOne() - .HasForeignKey(x => x.UserId) - .OnDelete(DeleteBehavior.Cascade); - b.HasIndex(x => new { x.TenantId, x.UserId }).IsUnique(); + + b.HasOne(x => x.User) + .WithOne() + .HasForeignKey(x => new { x.TenantId, x.UserId }) + .HasPrincipalKey(u => new { u.TenantId, u.Id }) // <-- เปลี่ยนตรงนี้ + .OnDelete(DeleteBehavior.Cascade); }); model.Entity(b => { b.ToTable("departments"); b.HasKey(x => x.Id); + b.HasAlternateKey(d => new { d.TenantId, d.Id }); b.Property(x => x.Code).IsRequired().HasMaxLength(64); b.Property(x => x.Name).IsRequired().HasMaxLength(256); @@ -237,7 +273,8 @@ public class AppDbContext : DbContext b.HasOne(x => x.Parent) .WithMany(x => x.Children) - .HasForeignKey(x => x.ParentDepartmentId) + .HasForeignKey(x => new { x.TenantId, x.ParentDepartmentId }) + .HasPrincipalKey(nameof(Department.TenantId), nameof(Department.Id)) .OnDelete(DeleteBehavior.Restrict); }); @@ -245,6 +282,7 @@ public class AppDbContext : DbContext { b.ToTable("positions"); b.HasKey(x => x.Id); + b.HasAlternateKey(p => new { p.TenantId, p.Id }); b.Property(x => x.Code).IsRequired().HasMaxLength(64); b.Property(x => x.Title).IsRequired().HasMaxLength(256); @@ -264,17 +302,20 @@ public class AppDbContext : DbContext b.HasOne(x => x.UserProfile) .WithMany(p => p.Employments) - .HasForeignKey(x => x.UserProfileId) + .HasForeignKey(x => new { x.TenantId, x.UserProfileId }) + .HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id)) .OnDelete(DeleteBehavior.Cascade); b.HasOne(x => x.Department) .WithMany() - .HasForeignKey(x => x.DepartmentId) + .HasForeignKey(x => new { x.TenantId, x.DepartmentId }) + .HasPrincipalKey(nameof(Department.TenantId), nameof(Department.Id)) .OnDelete(DeleteBehavior.Restrict); b.HasOne(x => x.Position) .WithMany() - .HasForeignKey(x => x.PositionId) + .HasForeignKey(x => new { x.TenantId, x.PositionId }) + .HasPrincipalKey(nameof(Position.TenantId), nameof(Position.Id)) .OnDelete(DeleteBehavior.Restrict); }); @@ -290,7 +331,8 @@ public class AppDbContext : DbContext b.HasOne(x => x.UserProfile) .WithMany(p => p.Addresses) - .HasForeignKey(x => x.UserProfileId) + .HasForeignKey(x => new { x.TenantId, x.UserProfileId }) + .HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id)) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary }); @@ -306,7 +348,8 @@ public class AppDbContext : DbContext b.HasOne(x => x.UserProfile) .WithMany(p => p.EmergencyContacts) - .HasForeignKey(x => x.UserProfileId) + .HasForeignKey(x => new { x.TenantId, x.UserProfileId }) + .HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id)) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary }); @@ -323,7 +366,8 @@ public class AppDbContext : DbContext b.HasOne(x => x.UserProfile) .WithMany(p => p.BankAccounts) - .HasForeignKey(x => x.UserProfileId) + .HasForeignKey(x => new { x.TenantId, x.UserProfileId }) + .HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id)) .OnDelete(DeleteBehavior.Cascade); b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary }); @@ -347,10 +391,18 @@ public class AppDbContext : DbContext .HasColumnName("tenant_id") .HasColumnType("uuid") .IsRequired() - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .ValueGeneratedNever(); b.HasIndex(nameof(BaseEntity.TenantId)); - b.HasCheckConstraint($"ck_{et.GetTableName()}_tenant_not_null", "tenant_id is not null"); + + // ชื่อ constraint สร้างแบบ concat แทน string interpolation กัน ambiguous handler + var tn = et.GetTableName(); + if (!string.IsNullOrEmpty(tn)) + { + b.HasCheckConstraint(string.Concat("ck_", tn, "_tenant_not_null"), "tenant_id is not null"); + b.HasCheckConstraint(string.Concat("ck_", tn, "_tenant_not_zero"), + "tenant_id <> '00000000-0000-0000-0000-000000000000'"); + } // Audit b.Property("CreatedAt") diff --git a/AMREZ.EOP.Infrastructures/DependencyInjections/ServiceCollectionExtensions.cs b/AMREZ.EOP.Infrastructures/DependencyInjections/ServiceCollectionExtensions.cs index 4bde9a2..623779e 100644 --- a/AMREZ.EOP.Infrastructures/DependencyInjections/ServiceCollectionExtensions.cs +++ b/AMREZ.EOP.Infrastructures/DependencyInjections/ServiceCollectionExtensions.cs @@ -29,6 +29,7 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddInfrastructure(this IServiceCollection services) { services.AddHttpContextAccessor(); + services.AddScoped(); // Options services.AddOptions() @@ -90,7 +91,9 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - + services.AddScoped(); + services.AddScoped(); + // UseCases — HR services.AddScoped(); services.AddScoped(); diff --git a/AMREZ.EOP.Infrastructures/Migrations/20250924034125_InitDatabase.Designer.cs b/AMREZ.EOP.Infrastructures/Migrations/20250924034125_InitDatabase.Designer.cs deleted file mode 100644 index 91b5bc2..0000000 --- a/AMREZ.EOP.Infrastructures/Migrations/20250924034125_InitDatabase.Designer.cs +++ /dev/null @@ -1,1536 +0,0 @@ -// -using System; -using AMREZ.EOP.Infrastructures.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace AMREZ.EOP.Infrastructures.Migrations -{ - [DbContext(typeof(AppDbContext))] - [Migration("20250924034125_InitDatabase")] - partial class InitDatabase - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.Permission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Code") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("TenantId", "Code") - .IsUnique(); - - b.ToTable("permissions", null, t => - { - t.HasCheckConstraint("ck_permissions_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.Role", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Code") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("TenantId", "Code") - .IsUnique(); - - b.ToTable("roles", null, t => - { - t.HasCheckConstraint("ck_roles_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.RolePermission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("PermissionId") - .HasColumnType("uuid"); - - b.Property("RoleId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.HasKey("Id"); - - b.HasIndex("PermissionId"); - - b.HasIndex("RoleId"); - - b.HasIndex("TenantId"); - - b.HasIndex("TenantId", "RoleId", "PermissionId") - .IsUnique(); - - b.ToTable("role_permissions", null, t => - { - t.HasCheckConstraint("ck_role_permissions_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AccessFailedCount") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("IsActive") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("LockoutEndUtc") - .HasColumnType("timestamp with time zone"); - - b.Property("MfaEnabled") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("SecurityStamp") - .HasColumnType("text"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.ToTable("users", null, t => - { - t.HasCheckConstraint("ck_users_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.UserExternalAccount", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("Email") - .HasColumnType("text"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("LinkedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Provider") - .HasColumnType("integer"); - - b.Property("Subject") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("UserId"); - - b.HasIndex("TenantId", "Provider", "Subject") - .IsUnique(); - - b.ToTable("user_external_accounts", null, t => - { - t.HasCheckConstraint("ck_user_external_accounts_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.UserIdentity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("Identifier") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("IsPrimary") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("Type") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("VerifiedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("UserId"); - - b.HasIndex("TenantId", "Type", "Identifier") - .IsUnique(); - - b.HasIndex("TenantId", "UserId", "Type", "IsPrimary") - .HasDatabaseName("ix_user_identity_primary_per_type"); - - b.ToTable("user_identities", null, t => - { - t.HasCheckConstraint("ck_user_identities_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.UserMfaFactor", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AddedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("CredentialId") - .HasColumnType("text"); - - b.Property("Email") - .HasColumnType("text"); - - b.Property("Enabled") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("Label") - .HasColumnType("text"); - - b.Property("LastUsedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PhoneE164") - .HasColumnType("text"); - - b.Property("PublicKey") - .HasColumnType("text"); - - b.Property("Secret") - .HasColumnType("text"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("Type") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("UserId"); - - b.HasIndex("TenantId", "UserId"); - - b.ToTable("user_mfa_factors", null, t => - { - t.HasCheckConstraint("ck_user_mfa_factors_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.UserPasswordHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChangedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("UserId"); - - b.HasIndex("TenantId", "UserId", "ChangedAt"); - - b.ToTable("user_password_histories", null, t => - { - t.HasCheckConstraint("ck_user_password_histories_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.UserRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("RoleId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.HasIndex("TenantId"); - - b.HasIndex("UserId"); - - b.HasIndex("TenantId", "UserId", "RoleId") - .IsUnique(); - - b.ToTable("user_roles", null, t => - { - t.HasCheckConstraint("ck_user_roles_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.UserSession", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("DeviceId") - .HasColumnType("text"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IpAddress") - .HasColumnType("text"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("IssuedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("RefreshTokenHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("RevokedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.Property("UserAgent") - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("UserId"); - - b.HasIndex("TenantId", "DeviceId"); - - b.HasIndex("TenantId", "UserId"); - - b.ToTable("user_sessions", null, t => - { - t.HasCheckConstraint("ck_user_sessions_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.Department", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Code") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("ParentDepartmentId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.HasKey("Id"); - - b.HasIndex("ParentDepartmentId"); - - b.HasIndex("TenantId"); - - b.HasIndex("TenantId", "Code") - .IsUnique(); - - b.ToTable("departments", null, t => - { - t.HasCheckConstraint("ck_departments_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.EmergencyContact", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("Email") - .HasColumnType("text"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("IsPrimary") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("Relationship") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.Property("UserProfileId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("UserProfileId"); - - b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); - - b.ToTable("emergency_contacts", null, t => - { - t.HasCheckConstraint("ck_emergency_contacts_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.EmployeeAddress", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("City") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Country") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("IsPrimary") - .HasColumnType("boolean"); - - b.Property("Line1") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("Line2") - .HasColumnType("text"); - - b.Property("PostalCode") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("State") - .HasColumnType("text"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("Type") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.Property("UserProfileId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("UserProfileId"); - - b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); - - b.ToTable("employee_addresses", null, t => - { - t.HasCheckConstraint("ck_employee_addresses_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.EmployeeBankAccount", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AccountHolder") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("AccountNumber") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("BankName") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Branch") - .HasColumnType("text"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("IsPrimary") - .HasColumnType("boolean"); - - b.Property("Note") - .HasColumnType("text"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.Property("UserProfileId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("UserProfileId"); - - b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); - - b.ToTable("employee_bank_accounts", null, t => - { - t.HasCheckConstraint("ck_employee_bank_accounts_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.Employment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("DepartmentId") - .HasColumnType("uuid"); - - b.Property("EmploymentType") - .HasColumnType("integer"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("ManagerUserId") - .HasColumnType("uuid"); - - b.Property("PositionId") - .HasColumnType("uuid"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.Property("UserProfileId") - .HasColumnType("uuid"); - - b.Property("WorkEmail") - .HasColumnType("text"); - - b.Property("WorkPhone") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("DepartmentId"); - - b.HasIndex("PositionId"); - - b.HasIndex("TenantId"); - - b.HasIndex("UserProfileId"); - - b.HasIndex("TenantId", "UserProfileId", "StartDate"); - - b.ToTable("employments", null, t => - { - t.HasCheckConstraint("ck_employments_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.Position", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Code") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("Level") - .HasColumnType("integer"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.HasKey("Id"); - - b.HasIndex("TenantId"); - - b.HasIndex("TenantId", "Code") - .IsUnique(); - - b.ToTable("positions", null, t => - { - t.HasCheckConstraint("ck_positions_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("DateOfBirth") - .HasColumnType("timestamp with time zone"); - - b.Property("DepartmentId") - .HasColumnType("uuid"); - - b.Property("FirstName") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("Gender") - .HasColumnType("integer"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_deleted"); - - b.Property("LastName") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("MiddleName") - .HasColumnType("text"); - - b.Property("Nickname") - .HasColumnType("text"); - - b.Property("PositionId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UpdatedBy") - .HasColumnType("text") - .HasColumnName("updated_by"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("DepartmentId"); - - b.HasIndex("PositionId"); - - b.HasIndex("TenantId"); - - b.HasIndex("UserId") - .IsUnique(); - - b.HasIndex("TenantId", "UserId") - .IsUnique(); - - b.ToTable("user_profiles", null, t => - { - t.HasCheckConstraint("ck_user_profiles_tenant_not_null", "tenant_id is not null"); - }); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Tenancy.TenantConfig", b => - { - b.Property("TenantKey") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("ConnectionString") - .HasColumnType("text"); - - b.Property("IsActive") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("Mode") - .HasColumnType("integer"); - - b.Property("Schema") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("UpdatedAtUtc") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at_utc") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.HasKey("TenantKey"); - - b.HasIndex("IsActive"); - - b.ToTable("tenants", "meta"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Tenancy.TenantDomain", b => - { - b.Property("Domain") - .HasMaxLength(253) - .HasColumnType("character varying(253)"); - - b.Property("IsActive") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(true); - - b.Property("IsPlatformBaseDomain") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - - b.Property("TenantKey") - .HasMaxLength(128) - .HasColumnType("character varying(128)"); - - b.Property("UpdatedAtUtc") - .ValueGeneratedOnAdd() - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at_utc") - .HasDefaultValueSql("now() at time zone 'utc'"); - - b.HasKey("Domain"); - - b.HasIndex("IsActive"); - - b.HasIndex("IsPlatformBaseDomain"); - - b.HasIndex("TenantKey"); - - b.ToTable("tenant_domains", "meta"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.RolePermission", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Permission", "Permission") - .WithMany("RolePermissions") - .HasForeignKey("PermissionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Role", "Role") - .WithMany("RolePermissions") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Permission"); - - b.Navigation("Role"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.UserExternalAccount", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") - .WithMany("ExternalAccounts") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.UserIdentity", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") - .WithMany("Identities") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.UserMfaFactor", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") - .WithMany("MfaFactors") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.UserPasswordHistory", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") - .WithMany("PasswordHistories") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.UserRole", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Role", "Role") - .WithMany("UserRoles") - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") - .WithMany("UserRoles") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Role"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.UserSession", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") - .WithMany("Sessions") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.Department", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Parent") - .WithMany("Children") - .HasForeignKey("ParentDepartmentId") - .OnDelete(DeleteBehavior.Restrict); - - b.Navigation("Parent"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.EmergencyContact", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") - .WithMany("EmergencyContacts") - .HasForeignKey("UserProfileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("UserProfile"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.EmployeeAddress", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") - .WithMany("Addresses") - .HasForeignKey("UserProfileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("UserProfile"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.EmployeeBankAccount", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") - .WithMany("BankAccounts") - .HasForeignKey("UserProfileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("UserProfile"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.Employment", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Department") - .WithMany() - .HasForeignKey("DepartmentId") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Position", "Position") - .WithMany() - .HasForeignKey("PositionId") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") - .WithMany("Employments") - .HasForeignKey("UserProfileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Department"); - - b.Navigation("Position"); - - b.Navigation("UserProfile"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", null) - .WithMany("Profiles") - .HasForeignKey("DepartmentId"); - - b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Position", null) - .WithMany("Profiles") - .HasForeignKey("PositionId"); - - b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") - .WithOne() - .HasForeignKey("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Tenancy.TenantDomain", b => - { - b.HasOne("AMREZ.EOP.Domain.Entities.Tenancy.TenantConfig", null) - .WithMany() - .HasForeignKey("TenantKey") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.Permission", b => - { - b.Navigation("RolePermissions"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.Role", b => - { - b.Navigation("RolePermissions"); - - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.Authentications.User", b => - { - b.Navigation("ExternalAccounts"); - - b.Navigation("Identities"); - - b.Navigation("MfaFactors"); - - b.Navigation("PasswordHistories"); - - b.Navigation("Sessions"); - - b.Navigation("UserRoles"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.Department", b => - { - b.Navigation("Children"); - - b.Navigation("Profiles"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.Position", b => - { - b.Navigation("Profiles"); - }); - - modelBuilder.Entity("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", b => - { - b.Navigation("Addresses"); - - b.Navigation("BankAccounts"); - - b.Navigation("EmergencyContacts"); - - b.Navigation("Employments"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/AMREZ.EOP.Infrastructures/Migrations/20250930101327_Add_Tenant_Guid.cs b/AMREZ.EOP.Infrastructures/Migrations/20250930101327_Add_Tenant_Guid.cs deleted file mode 100644 index 998c697..0000000 --- a/AMREZ.EOP.Infrastructures/Migrations/20250930101327_Add_Tenant_Guid.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace AMREZ.EOP.Infrastructures.Migrations -{ - /// - public partial class Add_Tenant_Guid : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "TenantId", - schema: "meta", - table: "tenants", - type: "uuid", - nullable: false, - defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "TenantId", - schema: "meta", - table: "tenants"); - } - } -} diff --git a/AMREZ.EOP.Infrastructures/Migrations/20250930101327_Add_Tenant_Guid.Designer.cs b/AMREZ.EOP.Infrastructures/Migrations/20251002040013_InitDatabase.Designer.cs similarity index 89% rename from AMREZ.EOP.Infrastructures/Migrations/20250930101327_Add_Tenant_Guid.Designer.cs rename to AMREZ.EOP.Infrastructures/Migrations/20251002040013_InitDatabase.Designer.cs index 430a85d..13df41e 100644 --- a/AMREZ.EOP.Infrastructures/Migrations/20250930101327_Add_Tenant_Guid.Designer.cs +++ b/AMREZ.EOP.Infrastructures/Migrations/20251002040013_InitDatabase.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace AMREZ.EOP.Infrastructures.Migrations { [DbContext(typeof(AppDbContext))] - [Migration("20250930101327_Add_Tenant_Guid")] - partial class Add_Tenant_Guid + [Migration("20251002040013_InitDatabase")] + partial class InitDatabase { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -58,10 +58,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("character varying(256)"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -81,6 +79,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("permissions", null, t => { t.HasCheckConstraint("ck_permissions_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -117,10 +117,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("character varying(256)"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -140,6 +138,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("roles", null, t => { t.HasCheckConstraint("ck_roles_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -172,10 +172,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("uuid"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -193,12 +191,16 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); + b.HasIndex("TenantId", "PermissionId"); + b.HasIndex("TenantId", "RoleId", "PermissionId") .IsUnique(); b.ToTable("role_permissions", null, t => { t.HasCheckConstraint("ck_role_permissions_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_role_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -250,10 +252,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("text"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -270,6 +270,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("users", null, t => { t.HasCheckConstraint("ck_users_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_users_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -309,10 +311,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("text"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -329,7 +329,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserId"); + b.HasIndex("TenantId", "UserId"); b.HasIndex("TenantId", "Provider", "Subject") .IsUnique(); @@ -337,6 +337,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("user_external_accounts", null, t => { t.HasCheckConstraint("ck_user_external_accounts_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_external_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -373,10 +375,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasDefaultValue(false); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("Type") .HasColumnType("integer"); @@ -399,8 +399,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserId"); - b.HasIndex("TenantId", "Type", "Identifier") .IsUnique(); @@ -410,6 +408,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("user_identities", null, t => { t.HasCheckConstraint("ck_user_identities_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_identities_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -465,10 +465,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("text"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("Type") .HasColumnType("integer"); @@ -488,13 +486,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserId"); - b.HasIndex("TenantId", "UserId"); b.ToTable("user_mfa_factors", null, t => { t.HasCheckConstraint("ck_user_mfa_factors_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_mfa_factors_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -528,10 +526,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("text"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -548,13 +544,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserId"); - b.HasIndex("TenantId", "UserId", "ChangedAt"); b.ToTable("user_password_histories", null, t => { t.HasCheckConstraint("ck_user_password_histories_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_password_histories_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -584,10 +580,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("uuid"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -608,12 +602,16 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("UserId"); + b.HasIndex("TenantId", "RoleId"); + b.HasIndex("TenantId", "UserId", "RoleId") .IsUnique(); b.ToTable("user_roles", null, t => { t.HasCheckConstraint("ck_user_roles_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -659,10 +657,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("timestamp with time zone"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -682,8 +678,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserId"); - b.HasIndex("TenantId", "DeviceId"); b.HasIndex("TenantId", "UserId"); @@ -691,6 +685,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("user_sessions", null, t => { t.HasCheckConstraint("ck_user_sessions_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_sessions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -730,10 +726,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("uuid"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -745,16 +739,18 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasKey("Id"); - b.HasIndex("ParentDepartmentId"); - b.HasIndex("TenantId"); b.HasIndex("TenantId", "Code") .IsUnique(); + b.HasIndex("TenantId", "ParentDepartmentId"); + b.ToTable("departments", null, t => { t.HasCheckConstraint("ck_departments_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_departments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -800,10 +796,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("character varying(64)"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -820,13 +814,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserProfileId"); - b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); b.ToTable("emergency_contacts", null, t => { t.HasCheckConstraint("ck_emergency_contacts_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_emergency_contacts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -882,10 +876,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("text"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("Type") .HasColumnType("integer"); @@ -905,13 +897,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserProfileId"); - b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); b.ToTable("employee_addresses", null, t => { t.HasCheckConstraint("ck_employee_addresses_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_employee_addresses_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -962,10 +954,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("text"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -982,13 +972,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserProfileId"); - b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); b.ToTable("employee_bank_accounts", null, t => { t.HasCheckConstraint("ck_employee_bank_accounts_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_employee_bank_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -1033,10 +1023,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("timestamp with time zone"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -1057,19 +1045,19 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasKey("Id"); - b.HasIndex("DepartmentId"); - - b.HasIndex("PositionId"); - b.HasIndex("TenantId"); - b.HasIndex("UserProfileId"); + b.HasIndex("TenantId", "DepartmentId"); + + b.HasIndex("TenantId", "PositionId"); b.HasIndex("TenantId", "UserProfileId", "StartDate"); b.ToTable("employments", null, t => { t.HasCheckConstraint("ck_employments_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_employments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -1104,10 +1092,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("integer"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("Title") .IsRequired() @@ -1132,6 +1118,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("positions", null, t => { t.HasCheckConstraint("ck_positions_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_positions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -1186,10 +1174,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("uuid"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -1210,15 +1196,14 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserId") - .IsUnique(); - b.HasIndex("TenantId", "UserId") .IsUnique(); b.ToTable("user_profiles", null, t => { t.HasCheckConstraint("ck_user_profiles_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_profiles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -1254,8 +1239,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasKey("TenantKey"); + b.HasAlternateKey("TenantId"); + b.HasIndex("IsActive"); + b.HasIndex("TenantId") + .IsUnique(); + b.ToTable("tenants", "meta"); }); @@ -1310,6 +1300,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Permission", null) + .WithMany() + .HasForeignKey("TenantId", "PermissionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Role", null) + .WithMany() + .HasForeignKey("TenantId", "RoleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.Navigation("Permission"); b.Navigation("Role"); @@ -1319,7 +1323,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") .WithMany("ExternalAccounts") - .HasForeignKey("UserId") + .HasForeignKey("TenantId", "UserId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1330,7 +1335,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") .WithMany("Identities") - .HasForeignKey("UserId") + .HasForeignKey("TenantId", "UserId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1341,7 +1347,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") .WithMany("MfaFactors") - .HasForeignKey("UserId") + .HasForeignKey("TenantId", "UserId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1352,7 +1359,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") .WithMany("PasswordHistories") - .HasForeignKey("UserId") + .HasForeignKey("TenantId", "UserId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1373,6 +1381,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Role", null) + .WithMany() + .HasForeignKey("TenantId", "RoleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", null) + .WithMany() + .HasForeignKey("TenantId", "UserId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.Navigation("Role"); b.Navigation("User"); @@ -1382,7 +1404,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") .WithMany("Sessions") - .HasForeignKey("UserId") + .HasForeignKey("TenantId", "UserId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1393,7 +1416,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Parent") .WithMany("Children") - .HasForeignKey("ParentDepartmentId") + .HasForeignKey("TenantId", "ParentDepartmentId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Restrict); b.Navigation("Parent"); @@ -1403,7 +1427,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") .WithMany("EmergencyContacts") - .HasForeignKey("UserProfileId") + .HasForeignKey("TenantId", "UserProfileId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1414,7 +1439,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") .WithMany("Addresses") - .HasForeignKey("UserProfileId") + .HasForeignKey("TenantId", "UserProfileId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1425,7 +1451,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") .WithMany("BankAccounts") - .HasForeignKey("UserProfileId") + .HasForeignKey("TenantId", "UserProfileId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1436,17 +1463,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Department") .WithMany() - .HasForeignKey("DepartmentId") + .HasForeignKey("TenantId", "DepartmentId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Restrict); b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Position", "Position") .WithMany() - .HasForeignKey("PositionId") + .HasForeignKey("TenantId", "PositionId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Restrict); b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") .WithMany("Employments") - .HasForeignKey("UserProfileId") + .HasForeignKey("TenantId", "UserProfileId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1469,7 +1499,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") .WithOne() - .HasForeignKey("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserId") + .HasForeignKey("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "TenantId", "UserId") + .HasPrincipalKey("AMREZ.EOP.Domain.Entities.Authentications.User", "TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/AMREZ.EOP.Infrastructures/Migrations/20250924034125_InitDatabase.cs b/AMREZ.EOP.Infrastructures/Migrations/20251002040013_InitDatabase.cs similarity index 84% rename from AMREZ.EOP.Infrastructures/Migrations/20250924034125_InitDatabase.cs rename to AMREZ.EOP.Infrastructures/Migrations/20251002040013_InitDatabase.cs index 55239d2..374f6dd 100644 --- a/AMREZ.EOP.Infrastructures/Migrations/20250924034125_InitDatabase.cs +++ b/AMREZ.EOP.Infrastructures/Migrations/20251002040013_InitDatabase.cs @@ -19,10 +19,10 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), Code = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), ParentDepartmentId = table.Column(type: "uuid", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -32,12 +32,14 @@ namespace AMREZ.EOP.Infrastructures.Migrations constraints: table => { table.PrimaryKey("PK_departments", x => x.Id); + table.UniqueConstraint("AK_departments_tenant_id_Id", x => new { x.tenant_id, x.Id }); table.CheckConstraint("ck_departments_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_departments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); table.ForeignKey( - name: "FK_departments_departments_ParentDepartmentId", - column: x => x.ParentDepartmentId, + name: "FK_departments_departments_tenant_id_ParentDepartmentId", + columns: x => new { x.tenant_id, x.ParentDepartmentId }, principalTable: "departments", - principalColumn: "Id", + principalColumns: new[] { "tenant_id", "Id" }, onDelete: ReferentialAction.Restrict); }); @@ -46,9 +48,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), Code = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -58,7 +60,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations constraints: table => { table.PrimaryKey("PK_permissions", x => x.Id); + table.UniqueConstraint("AK_permissions_tenant_id_Id", x => new { x.tenant_id, x.Id }); table.CheckConstraint("ck_permissions_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); migrationBuilder.CreateTable( @@ -66,10 +70,10 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), Code = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), Title = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), Level = table.Column(type: "integer", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -79,7 +83,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations constraints: table => { table.PrimaryKey("PK_positions", x => x.Id); + table.UniqueConstraint("AK_positions_tenant_id_Id", x => new { x.tenant_id, x.Id }); table.CheckConstraint("ck_positions_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_positions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); migrationBuilder.CreateTable( @@ -87,9 +93,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), Code = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -99,7 +105,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations constraints: table => { table.PrimaryKey("PK_roles", x => x.Id); + table.UniqueConstraint("AK_roles_tenant_id_Id", x => new { x.tenant_id, x.Id }); table.CheckConstraint("ck_roles_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); migrationBuilder.CreateTable( @@ -108,6 +116,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { TenantKey = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + TenantId = table.Column(type: "uuid", nullable: false), Schema = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), ConnectionString = table.Column(type: "text", nullable: true), Mode = table.Column(type: "integer", nullable: false), @@ -117,6 +126,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations constraints: table => { table.PrimaryKey("PK_tenants", x => x.TenantKey); + table.UniqueConstraint("AK_tenants_TenantId", x => x.TenantId); }); migrationBuilder.CreateTable( @@ -124,13 +134,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), PasswordHash = table.Column(type: "text", nullable: false), IsActive = table.Column(type: "boolean", nullable: false, defaultValue: true), AccessFailedCount = table.Column(type: "integer", nullable: false, defaultValue: 0), LockoutEndUtc = table.Column(type: "timestamp with time zone", nullable: true), MfaEnabled = table.Column(type: "boolean", nullable: false, defaultValue: false), SecurityStamp = table.Column(type: "text", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -140,7 +150,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations constraints: table => { table.PrimaryKey("PK_users", x => x.Id); + table.UniqueConstraint("AK_users_tenant_id_Id", x => new { x.tenant_id, x.Id }); table.CheckConstraint("ck_users_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_users_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); migrationBuilder.CreateTable( @@ -148,9 +160,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), RoleId = table.Column(type: "uuid", nullable: false), PermissionId = table.Column(type: "uuid", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -161,18 +173,31 @@ namespace AMREZ.EOP.Infrastructures.Migrations { table.PrimaryKey("PK_role_permissions", x => x.Id); table.CheckConstraint("ck_role_permissions_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_role_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); table.ForeignKey( name: "FK_role_permissions_permissions_PermissionId", column: x => x.PermissionId, principalTable: "permissions", principalColumn: "Id", onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_role_permissions_permissions_tenant_id_PermissionId", + columns: x => new { x.tenant_id, x.PermissionId }, + principalTable: "permissions", + principalColumns: new[] { "tenant_id", "Id" }, + onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_role_permissions_roles_RoleId", column: x => x.RoleId, principalTable: "roles", principalColumn: "Id", onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_role_permissions_roles_tenant_id_RoleId", + columns: x => new { x.tenant_id, x.RoleId }, + principalTable: "roles", + principalColumns: new[] { "tenant_id", "Id" }, + onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( @@ -203,12 +228,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), UserId = table.Column(type: "uuid", nullable: false), Provider = table.Column(type: "integer", nullable: false), Subject = table.Column(type: "text", nullable: false), Email = table.Column(type: "text", nullable: true), LinkedAt = table.Column(type: "timestamp with time zone", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -219,11 +244,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations { table.PrimaryKey("PK_user_external_accounts", x => x.Id); table.CheckConstraint("ck_user_external_accounts_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_user_external_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); table.ForeignKey( - name: "FK_user_external_accounts_users_UserId", - column: x => x.UserId, + name: "FK_user_external_accounts_users_tenant_id_UserId", + columns: x => new { x.tenant_id, x.UserId }, principalTable: "users", - principalColumn: "Id", + principalColumns: new[] { "tenant_id", "Id" }, onDelete: ReferentialAction.Cascade); }); @@ -232,12 +258,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), UserId = table.Column(type: "uuid", nullable: false), Type = table.Column(type: "integer", nullable: false), Identifier = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), IsPrimary = table.Column(type: "boolean", nullable: false, defaultValue: false), VerifiedAt = table.Column(type: "timestamp with time zone", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -248,11 +274,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations { table.PrimaryKey("PK_user_identities", x => x.Id); table.CheckConstraint("ck_user_identities_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_user_identities_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); table.ForeignKey( - name: "FK_user_identities_users_UserId", - column: x => x.UserId, + name: "FK_user_identities_users_tenant_id_UserId", + columns: x => new { x.tenant_id, x.UserId }, principalTable: "users", - principalColumn: "Id", + principalColumns: new[] { "tenant_id", "Id" }, onDelete: ReferentialAction.Cascade); }); @@ -261,7 +288,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), UserId = table.Column(type: "uuid", nullable: false), Type = table.Column(type: "integer", nullable: false), Label = table.Column(type: "text", nullable: true), @@ -273,6 +299,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true), AddedAt = table.Column(type: "timestamp with time zone", nullable: false), LastUsedAt = table.Column(type: "timestamp with time zone", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -283,11 +310,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations { table.PrimaryKey("PK_user_mfa_factors", x => x.Id); table.CheckConstraint("ck_user_mfa_factors_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_user_mfa_factors_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); table.ForeignKey( - name: "FK_user_mfa_factors_users_UserId", - column: x => x.UserId, + name: "FK_user_mfa_factors_users_tenant_id_UserId", + columns: x => new { x.tenant_id, x.UserId }, principalTable: "users", - principalColumn: "Id", + principalColumns: new[] { "tenant_id", "Id" }, onDelete: ReferentialAction.Cascade); }); @@ -296,10 +324,10 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), UserId = table.Column(type: "uuid", nullable: false), PasswordHash = table.Column(type: "text", nullable: false), ChangedAt = table.Column(type: "timestamp with time zone", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -310,11 +338,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations { table.PrimaryKey("PK_user_password_histories", x => x.Id); table.CheckConstraint("ck_user_password_histories_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_user_password_histories_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); table.ForeignKey( - name: "FK_user_password_histories_users_UserId", - column: x => x.UserId, + name: "FK_user_password_histories_users_tenant_id_UserId", + columns: x => new { x.tenant_id, x.UserId }, principalTable: "users", - principalColumn: "Id", + principalColumns: new[] { "tenant_id", "Id" }, onDelete: ReferentialAction.Cascade); }); @@ -323,7 +352,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), UserId = table.Column(type: "uuid", nullable: false), FirstName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), LastName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), @@ -333,6 +361,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations Gender = table.Column(type: "integer", nullable: true), DepartmentId = table.Column(type: "uuid", nullable: true), PositionId = table.Column(type: "uuid", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -342,7 +371,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations constraints: table => { table.PrimaryKey("PK_user_profiles", x => x.Id); + table.UniqueConstraint("AK_user_profiles_tenant_id_Id", x => new { x.tenant_id, x.Id }); table.CheckConstraint("ck_user_profiles_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_user_profiles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); table.ForeignKey( name: "FK_user_profiles_departments_DepartmentId", column: x => x.DepartmentId, @@ -354,10 +385,10 @@ namespace AMREZ.EOP.Infrastructures.Migrations principalTable: "positions", principalColumn: "Id"); table.ForeignKey( - name: "FK_user_profiles_users_UserId", - column: x => x.UserId, + name: "FK_user_profiles_users_tenant_id_UserId", + columns: x => new { x.tenant_id, x.UserId }, principalTable: "users", - principalColumn: "Id", + principalColumns: new[] { "tenant_id", "Id" }, onDelete: ReferentialAction.Cascade); }); @@ -366,9 +397,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), UserId = table.Column(type: "uuid", nullable: false), RoleId = table.Column(type: "uuid", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -379,18 +410,31 @@ namespace AMREZ.EOP.Infrastructures.Migrations { table.PrimaryKey("PK_user_roles", x => x.Id); table.CheckConstraint("ck_user_roles_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_user_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); table.ForeignKey( name: "FK_user_roles_roles_RoleId", column: x => x.RoleId, principalTable: "roles", principalColumn: "Id", onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_user_roles_roles_tenant_id_RoleId", + columns: x => new { x.tenant_id, x.RoleId }, + principalTable: "roles", + principalColumns: new[] { "tenant_id", "Id" }, + onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_user_roles_users_UserId", column: x => x.UserId, principalTable: "users", principalColumn: "Id", onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_user_roles_users_tenant_id_UserId", + columns: x => new { x.tenant_id, x.UserId }, + principalTable: "users", + principalColumns: new[] { "tenant_id", "Id" }, + onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( @@ -398,7 +442,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), UserId = table.Column(type: "uuid", nullable: false), RefreshTokenHash = table.Column(type: "text", nullable: false), IssuedAt = table.Column(type: "timestamp with time zone", nullable: false), @@ -407,6 +450,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations DeviceId = table.Column(type: "text", nullable: true), UserAgent = table.Column(type: "text", nullable: true), IpAddress = table.Column(type: "text", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -417,11 +461,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations { table.PrimaryKey("PK_user_sessions", x => x.Id); table.CheckConstraint("ck_user_sessions_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_user_sessions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); table.ForeignKey( - name: "FK_user_sessions_users_UserId", - column: x => x.UserId, + name: "FK_user_sessions_users_tenant_id_UserId", + columns: x => new { x.tenant_id, x.UserId }, principalTable: "users", - principalColumn: "Id", + principalColumns: new[] { "tenant_id", "Id" }, onDelete: ReferentialAction.Cascade); }); @@ -430,13 +475,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), UserProfileId = table.Column(type: "uuid", nullable: false), Name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), Relationship = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), Phone = table.Column(type: "text", nullable: true), Email = table.Column(type: "text", nullable: true), IsPrimary = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -447,11 +492,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations { table.PrimaryKey("PK_emergency_contacts", x => x.Id); table.CheckConstraint("ck_emergency_contacts_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_emergency_contacts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); table.ForeignKey( - name: "FK_emergency_contacts_user_profiles_UserProfileId", - column: x => x.UserProfileId, + name: "FK_emergency_contacts_user_profiles_tenant_id_UserProfileId", + columns: x => new { x.tenant_id, x.UserProfileId }, principalTable: "user_profiles", - principalColumn: "Id", + principalColumns: new[] { "tenant_id", "Id" }, onDelete: ReferentialAction.Cascade); }); @@ -460,7 +506,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), UserProfileId = table.Column(type: "uuid", nullable: false), Type = table.Column(type: "integer", nullable: false), Line1 = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), @@ -470,6 +515,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations PostalCode = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), Country = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), IsPrimary = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -480,11 +526,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations { table.PrimaryKey("PK_employee_addresses", x => x.Id); table.CheckConstraint("ck_employee_addresses_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_employee_addresses_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); table.ForeignKey( - name: "FK_employee_addresses_user_profiles_UserProfileId", - column: x => x.UserProfileId, + name: "FK_employee_addresses_user_profiles_tenant_id_UserProfileId", + columns: x => new { x.tenant_id, x.UserProfileId }, principalTable: "user_profiles", - principalColumn: "Id", + principalColumns: new[] { "tenant_id", "Id" }, onDelete: ReferentialAction.Cascade); }); @@ -493,7 +540,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), UserProfileId = table.Column(type: "uuid", nullable: false), BankName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), AccountNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), @@ -501,6 +547,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations Branch = table.Column(type: "text", nullable: true), Note = table.Column(type: "text", nullable: true), IsPrimary = table.Column(type: "boolean", nullable: false), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -511,11 +558,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations { table.PrimaryKey("PK_employee_bank_accounts", x => x.Id); table.CheckConstraint("ck_employee_bank_accounts_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_employee_bank_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); table.ForeignKey( - name: "FK_employee_bank_accounts_user_profiles_UserProfileId", - column: x => x.UserProfileId, + name: "FK_employee_bank_accounts_user_profiles_tenant_id_UserProfileId", + columns: x => new { x.tenant_id, x.UserProfileId }, principalTable: "user_profiles", - principalColumn: "Id", + principalColumns: new[] { "tenant_id", "Id" }, onDelete: ReferentialAction.Cascade); }); @@ -524,7 +572,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - tenant_id = table.Column(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"), UserProfileId = table.Column(type: "uuid", nullable: false), EmploymentType = table.Column(type: "integer", nullable: false), StartDate = table.Column(type: "timestamp with time zone", nullable: false), @@ -534,6 +581,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations ManagerUserId = table.Column(type: "uuid", nullable: true), WorkEmail = table.Column(type: "text", nullable: true), WorkPhone = table.Column(type: "text", nullable: true), + tenant_id = table.Column(type: "uuid", nullable: false), created_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_by = table.Column(type: "text", nullable: true), updated_at = table.Column(type: "timestamp with time zone", nullable: true), @@ -544,31 +592,27 @@ namespace AMREZ.EOP.Infrastructures.Migrations { table.PrimaryKey("PK_employments", x => x.Id); table.CheckConstraint("ck_employments_tenant_not_null", "tenant_id is not null"); + table.CheckConstraint("ck_employments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); table.ForeignKey( - name: "FK_employments_departments_DepartmentId", - column: x => x.DepartmentId, + name: "FK_employments_departments_tenant_id_DepartmentId", + columns: x => new { x.tenant_id, x.DepartmentId }, principalTable: "departments", - principalColumn: "Id", + principalColumns: new[] { "tenant_id", "Id" }, onDelete: ReferentialAction.Restrict); table.ForeignKey( - name: "FK_employments_positions_PositionId", - column: x => x.PositionId, + name: "FK_employments_positions_tenant_id_PositionId", + columns: x => new { x.tenant_id, x.PositionId }, principalTable: "positions", - principalColumn: "Id", + principalColumns: new[] { "tenant_id", "Id" }, onDelete: ReferentialAction.Restrict); table.ForeignKey( - name: "FK_employments_user_profiles_UserProfileId", - column: x => x.UserProfileId, + name: "FK_employments_user_profiles_tenant_id_UserProfileId", + columns: x => new { x.tenant_id, x.UserProfileId }, principalTable: "user_profiles", - principalColumn: "Id", + principalColumns: new[] { "tenant_id", "Id" }, onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateIndex( - name: "IX_departments_ParentDepartmentId", - table: "departments", - column: "ParentDepartmentId"); - migrationBuilder.CreateIndex( name: "IX_departments_tenant_id", table: "departments", @@ -580,6 +624,11 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: new[] { "tenant_id", "Code" }, unique: true); + migrationBuilder.CreateIndex( + name: "IX_departments_tenant_id_ParentDepartmentId", + table: "departments", + columns: new[] { "tenant_id", "ParentDepartmentId" }); + migrationBuilder.CreateIndex( name: "IX_emergency_contacts_tenant_id", table: "emergency_contacts", @@ -590,11 +639,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations table: "emergency_contacts", columns: new[] { "tenant_id", "UserProfileId", "IsPrimary" }); - migrationBuilder.CreateIndex( - name: "IX_emergency_contacts_UserProfileId", - table: "emergency_contacts", - column: "UserProfileId"); - migrationBuilder.CreateIndex( name: "IX_employee_addresses_tenant_id", table: "employee_addresses", @@ -605,11 +649,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations table: "employee_addresses", columns: new[] { "tenant_id", "UserProfileId", "IsPrimary" }); - migrationBuilder.CreateIndex( - name: "IX_employee_addresses_UserProfileId", - table: "employee_addresses", - column: "UserProfileId"); - migrationBuilder.CreateIndex( name: "IX_employee_bank_accounts_tenant_id", table: "employee_bank_accounts", @@ -620,36 +659,26 @@ namespace AMREZ.EOP.Infrastructures.Migrations table: "employee_bank_accounts", columns: new[] { "tenant_id", "UserProfileId", "IsPrimary" }); - migrationBuilder.CreateIndex( - name: "IX_employee_bank_accounts_UserProfileId", - table: "employee_bank_accounts", - column: "UserProfileId"); - - migrationBuilder.CreateIndex( - name: "IX_employments_DepartmentId", - table: "employments", - column: "DepartmentId"); - - migrationBuilder.CreateIndex( - name: "IX_employments_PositionId", - table: "employments", - column: "PositionId"); - migrationBuilder.CreateIndex( name: "IX_employments_tenant_id", table: "employments", column: "tenant_id"); + migrationBuilder.CreateIndex( + name: "IX_employments_tenant_id_DepartmentId", + table: "employments", + columns: new[] { "tenant_id", "DepartmentId" }); + + migrationBuilder.CreateIndex( + name: "IX_employments_tenant_id_PositionId", + table: "employments", + columns: new[] { "tenant_id", "PositionId" }); + migrationBuilder.CreateIndex( name: "IX_employments_tenant_id_UserProfileId_StartDate", table: "employments", columns: new[] { "tenant_id", "UserProfileId", "StartDate" }); - migrationBuilder.CreateIndex( - name: "IX_employments_UserProfileId", - table: "employments", - column: "UserProfileId"); - migrationBuilder.CreateIndex( name: "IX_permissions_tenant_id", table: "permissions", @@ -687,6 +716,11 @@ namespace AMREZ.EOP.Infrastructures.Migrations table: "role_permissions", column: "tenant_id"); + migrationBuilder.CreateIndex( + name: "IX_role_permissions_tenant_id_PermissionId", + table: "role_permissions", + columns: new[] { "tenant_id", "PermissionId" }); + migrationBuilder.CreateIndex( name: "IX_role_permissions_tenant_id_RoleId_PermissionId", table: "role_permissions", @@ -728,6 +762,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations table: "tenants", column: "IsActive"); + migrationBuilder.CreateIndex( + name: "IX_tenants_TenantId", + schema: "meta", + table: "tenants", + column: "TenantId", + unique: true); + migrationBuilder.CreateIndex( name: "IX_user_external_accounts_tenant_id", table: "user_external_accounts", @@ -740,9 +781,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations unique: true); migrationBuilder.CreateIndex( - name: "IX_user_external_accounts_UserId", + name: "IX_user_external_accounts_tenant_id_UserId", table: "user_external_accounts", - column: "UserId"); + columns: new[] { "tenant_id", "UserId" }); migrationBuilder.CreateIndex( name: "IX_user_identities_tenant_id", @@ -755,11 +796,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: new[] { "tenant_id", "Type", "Identifier" }, unique: true); - migrationBuilder.CreateIndex( - name: "IX_user_identities_UserId", - table: "user_identities", - column: "UserId"); - migrationBuilder.CreateIndex( name: "ix_user_identity_primary_per_type", table: "user_identities", @@ -775,11 +811,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations table: "user_mfa_factors", columns: new[] { "tenant_id", "UserId" }); - migrationBuilder.CreateIndex( - name: "IX_user_mfa_factors_UserId", - table: "user_mfa_factors", - column: "UserId"); - migrationBuilder.CreateIndex( name: "IX_user_password_histories_tenant_id", table: "user_password_histories", @@ -790,11 +821,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations table: "user_password_histories", columns: new[] { "tenant_id", "UserId", "ChangedAt" }); - migrationBuilder.CreateIndex( - name: "IX_user_password_histories_UserId", - table: "user_password_histories", - column: "UserId"); - migrationBuilder.CreateIndex( name: "IX_user_profiles_DepartmentId", table: "user_profiles", @@ -816,12 +842,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations columns: new[] { "tenant_id", "UserId" }, unique: true); - migrationBuilder.CreateIndex( - name: "IX_user_profiles_UserId", - table: "user_profiles", - column: "UserId", - unique: true); - migrationBuilder.CreateIndex( name: "IX_user_roles_RoleId", table: "user_roles", @@ -832,6 +852,11 @@ namespace AMREZ.EOP.Infrastructures.Migrations table: "user_roles", column: "tenant_id"); + migrationBuilder.CreateIndex( + name: "IX_user_roles_tenant_id_RoleId", + table: "user_roles", + columns: new[] { "tenant_id", "RoleId" }); + migrationBuilder.CreateIndex( name: "IX_user_roles_tenant_id_UserId_RoleId", table: "user_roles", @@ -858,11 +883,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations table: "user_sessions", columns: new[] { "tenant_id", "UserId" }); - migrationBuilder.CreateIndex( - name: "IX_user_sessions_UserId", - table: "user_sessions", - column: "UserId"); - migrationBuilder.CreateIndex( name: "IX_users_tenant_id", table: "users", diff --git a/AMREZ.EOP.Infrastructures/Migrations/AppDbContextModelSnapshot.cs b/AMREZ.EOP.Infrastructures/Migrations/AppDbContextModelSnapshot.cs index 6f4e0c3..8b6e62c 100644 --- a/AMREZ.EOP.Infrastructures/Migrations/AppDbContextModelSnapshot.cs +++ b/AMREZ.EOP.Infrastructures/Migrations/AppDbContextModelSnapshot.cs @@ -55,10 +55,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("character varying(256)"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -78,6 +76,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("permissions", null, t => { t.HasCheckConstraint("ck_permissions_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -114,10 +114,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("character varying(256)"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -137,6 +135,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("roles", null, t => { t.HasCheckConstraint("ck_roles_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -169,10 +169,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("uuid"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -190,12 +188,16 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); + b.HasIndex("TenantId", "PermissionId"); + b.HasIndex("TenantId", "RoleId", "PermissionId") .IsUnique(); b.ToTable("role_permissions", null, t => { t.HasCheckConstraint("ck_role_permissions_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_role_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -247,10 +249,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("text"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -267,6 +267,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("users", null, t => { t.HasCheckConstraint("ck_users_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_users_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -306,10 +308,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("text"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -326,7 +326,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserId"); + b.HasIndex("TenantId", "UserId"); b.HasIndex("TenantId", "Provider", "Subject") .IsUnique(); @@ -334,6 +334,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("user_external_accounts", null, t => { t.HasCheckConstraint("ck_user_external_accounts_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_external_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -370,10 +372,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasDefaultValue(false); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("Type") .HasColumnType("integer"); @@ -396,8 +396,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserId"); - b.HasIndex("TenantId", "Type", "Identifier") .IsUnique(); @@ -407,6 +405,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("user_identities", null, t => { t.HasCheckConstraint("ck_user_identities_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_identities_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -462,10 +462,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("text"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("Type") .HasColumnType("integer"); @@ -485,13 +483,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserId"); - b.HasIndex("TenantId", "UserId"); b.ToTable("user_mfa_factors", null, t => { t.HasCheckConstraint("ck_user_mfa_factors_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_mfa_factors_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -525,10 +523,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("text"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -545,13 +541,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserId"); - b.HasIndex("TenantId", "UserId", "ChangedAt"); b.ToTable("user_password_histories", null, t => { t.HasCheckConstraint("ck_user_password_histories_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_password_histories_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -581,10 +577,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("uuid"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -605,12 +599,16 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("UserId"); + b.HasIndex("TenantId", "RoleId"); + b.HasIndex("TenantId", "UserId", "RoleId") .IsUnique(); b.ToTable("user_roles", null, t => { t.HasCheckConstraint("ck_user_roles_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -656,10 +654,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("timestamp with time zone"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -679,8 +675,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserId"); - b.HasIndex("TenantId", "DeviceId"); b.HasIndex("TenantId", "UserId"); @@ -688,6 +682,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("user_sessions", null, t => { t.HasCheckConstraint("ck_user_sessions_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_sessions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -727,10 +723,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("uuid"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -742,16 +736,18 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasKey("Id"); - b.HasIndex("ParentDepartmentId"); - b.HasIndex("TenantId"); b.HasIndex("TenantId", "Code") .IsUnique(); + b.HasIndex("TenantId", "ParentDepartmentId"); + b.ToTable("departments", null, t => { t.HasCheckConstraint("ck_departments_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_departments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -797,10 +793,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("character varying(64)"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -817,13 +811,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserProfileId"); - b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); b.ToTable("emergency_contacts", null, t => { t.HasCheckConstraint("ck_emergency_contacts_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_emergency_contacts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -879,10 +873,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("text"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("Type") .HasColumnType("integer"); @@ -902,13 +894,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserProfileId"); - b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); b.ToTable("employee_addresses", null, t => { t.HasCheckConstraint("ck_employee_addresses_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_employee_addresses_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -959,10 +951,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("text"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -979,13 +969,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserProfileId"); - b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); b.ToTable("employee_bank_accounts", null, t => { t.HasCheckConstraint("ck_employee_bank_accounts_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_employee_bank_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -1030,10 +1020,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("timestamp with time zone"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -1054,19 +1042,19 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasKey("Id"); - b.HasIndex("DepartmentId"); - - b.HasIndex("PositionId"); - b.HasIndex("TenantId"); - b.HasIndex("UserProfileId"); + b.HasIndex("TenantId", "DepartmentId"); + + b.HasIndex("TenantId", "PositionId"); b.HasIndex("TenantId", "UserProfileId", "StartDate"); b.ToTable("employments", null, t => { t.HasCheckConstraint("ck_employments_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_employments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -1101,10 +1089,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("integer"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("Title") .IsRequired() @@ -1129,6 +1115,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.ToTable("positions", null, t => { t.HasCheckConstraint("ck_positions_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_positions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -1183,10 +1171,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations .HasColumnType("uuid"); b.Property("TenantId") - .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasColumnName("tenant_id") - .HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); + .HasColumnName("tenant_id"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -1207,15 +1193,14 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasIndex("TenantId"); - b.HasIndex("UserId") - .IsUnique(); - b.HasIndex("TenantId", "UserId") .IsUnique(); b.ToTable("user_profiles", null, t => { t.HasCheckConstraint("ck_user_profiles_tenant_not_null", "tenant_id is not null"); + + t.HasCheckConstraint("ck_user_profiles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'"); }); }); @@ -1251,8 +1236,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasKey("TenantKey"); + b.HasAlternateKey("TenantId"); + b.HasIndex("IsActive"); + b.HasIndex("TenantId") + .IsUnique(); + b.ToTable("tenants", "meta"); }); @@ -1307,6 +1297,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Permission", null) + .WithMany() + .HasForeignKey("TenantId", "PermissionId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Role", null) + .WithMany() + .HasForeignKey("TenantId", "RoleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.Navigation("Permission"); b.Navigation("Role"); @@ -1316,7 +1320,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") .WithMany("ExternalAccounts") - .HasForeignKey("UserId") + .HasForeignKey("TenantId", "UserId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1327,7 +1332,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") .WithMany("Identities") - .HasForeignKey("UserId") + .HasForeignKey("TenantId", "UserId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1338,7 +1344,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") .WithMany("MfaFactors") - .HasForeignKey("UserId") + .HasForeignKey("TenantId", "UserId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1349,7 +1356,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") .WithMany("PasswordHistories") - .HasForeignKey("UserId") + .HasForeignKey("TenantId", "UserId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1370,6 +1378,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Role", null) + .WithMany() + .HasForeignKey("TenantId", "RoleId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", null) + .WithMany() + .HasForeignKey("TenantId", "UserId") + .HasPrincipalKey("TenantId", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.Navigation("Role"); b.Navigation("User"); @@ -1379,7 +1401,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") .WithMany("Sessions") - .HasForeignKey("UserId") + .HasForeignKey("TenantId", "UserId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1390,7 +1413,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Parent") .WithMany("Children") - .HasForeignKey("ParentDepartmentId") + .HasForeignKey("TenantId", "ParentDepartmentId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Restrict); b.Navigation("Parent"); @@ -1400,7 +1424,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") .WithMany("EmergencyContacts") - .HasForeignKey("UserProfileId") + .HasForeignKey("TenantId", "UserProfileId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1411,7 +1436,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") .WithMany("Addresses") - .HasForeignKey("UserProfileId") + .HasForeignKey("TenantId", "UserProfileId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1422,7 +1448,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") .WithMany("BankAccounts") - .HasForeignKey("UserProfileId") + .HasForeignKey("TenantId", "UserProfileId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1433,17 +1460,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations { b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Department") .WithMany() - .HasForeignKey("DepartmentId") + .HasForeignKey("TenantId", "DepartmentId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Restrict); b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Position", "Position") .WithMany() - .HasForeignKey("PositionId") + .HasForeignKey("TenantId", "PositionId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Restrict); b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") .WithMany("Employments") - .HasForeignKey("UserProfileId") + .HasForeignKey("TenantId", "UserProfileId") + .HasPrincipalKey("TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1466,7 +1496,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") .WithOne() - .HasForeignKey("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserId") + .HasForeignKey("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "TenantId", "UserId") + .HasPrincipalKey("AMREZ.EOP.Domain.Entities.Authentications.User", "TenantId", "Id") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/AMREZ.EOP.Infrastructures/Options/JwtOptions.cs b/AMREZ.EOP.Infrastructures/Options/JwtOptions.cs new file mode 100644 index 0000000..8090331 --- /dev/null +++ b/AMREZ.EOP.Infrastructures/Options/JwtOptions.cs @@ -0,0 +1,9 @@ +namespace AMREZ.EOP.Infrastructures.Options; + +public sealed class JwtOptions +{ + public string Issuer { get; init; } = default!; + public string Audience { get; init; } = default!; + public string SigningKey { get; init; } = default!; + public int AccessMinutes { get; init; } = 10; +} \ No newline at end of file diff --git a/AMREZ.EOP.Infrastructures/Repositories/TenantRepository.cs b/AMREZ.EOP.Infrastructures/Repositories/TenantRepository.cs index e503eae..781b06a 100644 --- a/AMREZ.EOP.Infrastructures/Repositories/TenantRepository.cs +++ b/AMREZ.EOP.Infrastructures/Repositories/TenantRepository.cs @@ -1,8 +1,10 @@ +using AMREZ.EOP.Abstractions.Applications.Tenancy; using AMREZ.EOP.Abstractions.Infrastructures.Repositories; using AMREZ.EOP.Abstractions.Storage; using AMREZ.EOP.Domain.Entities.Tenancy; using AMREZ.EOP.Infrastructures.Data; using AMREZ.EOP.Infrastructures.Options; +using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -11,9 +13,19 @@ namespace AMREZ.EOP.Infrastructures.Repositories; public sealed class TenantRepository : ITenantRepository { private readonly IDbScope _scope; - public TenantRepository(IDbScope scope) => _scope = scope; + private readonly ITenantResolver _resolver; + private readonly IHttpContextAccessor _http; - private AppDbContext Db() => _scope.Get(); + public TenantRepository(IDbScope scope, ITenantResolver resolver, IHttpContextAccessor http) + { _scope = scope; _resolver = resolver; _http = http; } + + private AppDbContext Db() + { + var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext"); + var platform = _resolver.Resolve(http, "@platform") ?? throw new InvalidOperationException("No platform tenant"); + _scope.EnsureForTenant(platform); + return _scope.Get(); + } private static string Norm(string s) => (s ?? string.Empty).Trim().ToLowerInvariant(); @@ -51,7 +63,7 @@ public sealed class TenantRepository : ITenantRepository if (cur is null) return false; if (ifUnmodifiedSince.HasValue && cur.UpdatedAtUtc > ifUnmodifiedSince.Value) - return false; // 412 + return false; cur.Schema = row.Schema is null ? cur.Schema : (string.IsNullOrWhiteSpace(row.Schema) ? null : row.Schema.Trim()); cur.ConnectionString = row.ConnectionString is null ? cur.ConnectionString : (string.IsNullOrWhiteSpace(row.ConnectionString) ? null : row.ConnectionString.Trim()); @@ -70,7 +82,6 @@ public sealed class TenantRepository : ITenantRepository var t = await db.Set().FirstOrDefaultAsync(x => x.TenantKey == key, ct); if (t is null) return false; - // ❌ ไม่ลบนะ — ✅ deactivate if (t.IsActive) { t.IsActive = false; @@ -120,7 +131,7 @@ public sealed class TenantRepository : ITenantRepository if (ex is null) return false; ex.IsActive = false; - ex.TenantKey = null; + ex.TenantKey = null; ex.IsPlatformBaseDomain = false; ex.UpdatedAtUtc = DateTimeOffset.UtcNow; diff --git a/AMREZ.EOP.Infrastructures/Repositories/UserProfileRepository.cs b/AMREZ.EOP.Infrastructures/Repositories/UserProfileRepository.cs index a72d217..cd2c1ec 100644 --- a/AMREZ.EOP.Infrastructures/Repositories/UserProfileRepository.cs +++ b/AMREZ.EOP.Infrastructures/Repositories/UserProfileRepository.cs @@ -2,6 +2,7 @@ using AMREZ.EOP.Abstractions.Applications.Tenancy; using AMREZ.EOP.Abstractions.Infrastructures.Repositories; using AMREZ.EOP.Abstractions.Storage; using AMREZ.EOP.Domain.Entities.HumanResources; +using AMREZ.EOP.Domain.Entities.Tenancy; using AMREZ.EOP.Infrastructures.Data; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; @@ -19,22 +20,33 @@ public class UserProfileRepository : IUserProfileRepository private Guid TenantId() { - var http = _http.HttpContext; - var tc = http is not null ? _tenantResolver.Resolve(http) : null; - return Guid.TryParse(tc?.Id, out var g) ? g : Guid.Empty; + var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext"); + var tc = _tenantResolver.Resolve(http) ?? throw new InvalidOperationException("No tenant"); + _scope.EnsureForTenant(tc); + + if (!string.IsNullOrWhiteSpace(tc.Id) && Guid.TryParse(tc.Id, out var g) && g != Guid.Empty) + return g; + + var key = (http.Items.TryGetValue("TargetTenantKey", out var v) ? v as string : null) ?? tc.TenantKey; + if (string.IsNullOrWhiteSpace(key)) throw new InvalidOperationException("Tenant key missing"); + + var db = _scope.Get(); + var cfg = db.Set().AsNoTracking().FirstOrDefault(x => x.TenantKey == key); + if (cfg is null) throw new InvalidOperationException($"Tenant '{key}' not found"); + return cfg.TenantId; } public async Task GetByUserIdAsync(Guid userId, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); return await db.UserProfiles.FirstOrDefaultAsync(p => p.TenantId == tid && p.UserId == userId, ct); } public async Task UpsertAsync(UserProfile profile, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); var existing = await db.UserProfiles.FirstOrDefaultAsync(p => p.TenantId == tid && p.UserId == profile.UserId, ct); if (existing is null) @@ -56,8 +68,9 @@ public class UserProfileRepository : IUserProfileRepository public async Task AddEmploymentAsync(Employment e, CancellationToken ct = default) { + var tid = TenantId(); var db = _scope.Get(); - e.TenantId = TenantId(); + e.TenantId = tid; await db.Employments.AddAsync(e, ct); await db.SaveChangesAsync(ct); return e; @@ -65,8 +78,8 @@ public class UserProfileRepository : IUserProfileRepository public async Task EndEmploymentAsync(Guid employmentId, DateTime endDate, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); var e = await db.Employments.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == employmentId, ct); if (e is null) return; e.EndDate = endDate; @@ -75,8 +88,9 @@ public class UserProfileRepository : IUserProfileRepository public async Task AddAddressAsync(EmployeeAddress a, CancellationToken ct = default) { + var tid = TenantId(); var db = _scope.Get(); - a.TenantId = TenantId(); + a.TenantId = tid; await db.EmployeeAddresses.AddAsync(a, ct); await db.SaveChangesAsync(ct); return a; @@ -84,8 +98,8 @@ public class UserProfileRepository : IUserProfileRepository public async Task SetPrimaryAddressAsync(Guid userProfileId, Guid addressId, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); var all = await db.EmployeeAddresses.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct); foreach (var x in all) x.IsPrimary = false; @@ -98,8 +112,9 @@ public class UserProfileRepository : IUserProfileRepository public async Task AddEmergencyContactAsync(EmergencyContact c, CancellationToken ct = default) { + var tid = TenantId(); var db = _scope.Get(); - c.TenantId = TenantId(); + c.TenantId = tid; await db.EmergencyContacts.AddAsync(c, ct); await db.SaveChangesAsync(ct); return c; @@ -107,8 +122,8 @@ public class UserProfileRepository : IUserProfileRepository public async Task SetPrimaryEmergencyContactAsync(Guid userProfileId, Guid contactId, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); var all = await db.EmergencyContacts.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct); foreach (var x in all) x.IsPrimary = false; @@ -121,8 +136,9 @@ public class UserProfileRepository : IUserProfileRepository public async Task AddBankAccountAsync(EmployeeBankAccount b, CancellationToken ct = default) { + var tid = TenantId(); var db = _scope.Get(); - b.TenantId = TenantId(); + b.TenantId = tid; await db.EmployeeBankAccounts.AddAsync(b, ct); await db.SaveChangesAsync(ct); return b; @@ -130,8 +146,8 @@ public class UserProfileRepository : IUserProfileRepository public async Task SetPrimaryBankAccountAsync(Guid userProfileId, Guid bankAccountId, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); var all = await db.EmployeeBankAccounts.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct); foreach (var x in all) x.IsPrimary = false; diff --git a/AMREZ.EOP.Infrastructures/Repositories/UserRepository.cs b/AMREZ.EOP.Infrastructures/Repositories/UserRepository.cs index a5cfa9e..85f259e 100644 --- a/AMREZ.EOP.Infrastructures/Repositories/UserRepository.cs +++ b/AMREZ.EOP.Infrastructures/Repositories/UserRepository.cs @@ -3,6 +3,7 @@ using AMREZ.EOP.Abstractions.Applications.Tenancy; using AMREZ.EOP.Abstractions.Infrastructures.Repositories; using AMREZ.EOP.Abstractions.Storage; using AMREZ.EOP.Domain.Entities.Authentications; +using AMREZ.EOP.Domain.Entities.Tenancy; using AMREZ.EOP.Domain.Shared._Users; using AMREZ.EOP.Infrastructures.Data; using Microsoft.AspNetCore.Http; @@ -25,16 +26,27 @@ public class UserRepository : IUserRepository IHttpContextAccessor http) { _scope = scope; - _redis = redis; // null = ไม่มี/ต่อไม่ได้ → ข้าม cache + _redis = redis; _tenantResolver = tenantResolver; _http = http; } private Guid TenantId() { - var http = _http.HttpContext; - var tc = http is not null ? _tenantResolver.Resolve(http) : null; - return Guid.TryParse(tc?.Id, out var g) ? g : Guid.Empty; + var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext"); + var tc = _tenantResolver.Resolve(http) ?? throw new InvalidOperationException("No tenant"); + _scope.EnsureForTenant(tc); + + if (!string.IsNullOrWhiteSpace(tc.Id) && Guid.TryParse(tc.Id, out var g) && g != Guid.Empty) + return g; + + var key = (http.Items.TryGetValue("TargetTenantKey", out var v) ? v as string : null) ?? tc.Id; + if (string.IsNullOrWhiteSpace(key)) throw new InvalidOperationException("Tenant key missing"); + + var db = _scope.Get(); + var cfg = db.Set().AsNoTracking().FirstOrDefault(x => x.TenantKey == key); + if (cfg is null) throw new InvalidOperationException($"Tenant '{key}' not found"); + return cfg.TenantId; } private static string IdentityEmailKey(string tenantId, string email) @@ -45,8 +57,8 @@ public class UserRepository : IUserRepository public async Task FindByIdAsync(Guid userId, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); return await db.Users.AsNoTracking() .FirstOrDefaultAsync(u => u.TenantId == tid && u.Id == userId, ct); } @@ -57,7 +69,6 @@ public class UserRepository : IUserRepository var tidStr = tid.ToString(); var r = _redis?.GetDatabase(); - // 1) Redis if (r is not null) { try @@ -69,10 +80,9 @@ public class UserRepository : IUserRepository if (hit?.IsActive == true) return hit; } } - catch { /* ignore → query DB */ } + catch { } } - // 2) EF: join identities var db = _scope.Get(); var norm = email.Trim().ToLowerInvariant(); @@ -86,7 +96,6 @@ public class UserRepository : IUserRepository i.Identifier == norm)) .FirstOrDefaultAsync(ct); - // 3) cache if (user is not null && r is not null) { try @@ -96,7 +105,7 @@ public class UserRepository : IUserRepository await r.StringSetAsync(IdentityEmailKey(tidStr, norm), payload, ttl); await r.StringSetAsync(UserIdKey(tidStr, user.Id), payload, ttl); } - catch { /* ignore */ } + catch { } } return user; @@ -104,8 +113,8 @@ public class UserRepository : IUserRepository public async Task EmailExistsAsync(string email, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); var norm = email.Trim().ToLowerInvariant(); return await db.UserIdentities.AsNoTracking() @@ -114,40 +123,36 @@ public class UserRepository : IUserRepository public async Task AddAsync(User user, CancellationToken ct = default) { + var tid = TenantId(); var db = _scope.Get(); - user.TenantId = TenantId(); + user.TenantId = tid; await db.Users.AddAsync(user, ct); await db.SaveChangesAsync(ct); - // cache var r = _redis?.GetDatabase(); if (r is not null) { try { - var tid = user.TenantId.ToString(); var payload = JsonSerializer.Serialize(user); var ttl = TimeSpan.FromMinutes(5); - await r.StringSetAsync(UserIdKey(tid, user.Id), payload, ttl); + await r.StringSetAsync(UserIdKey(tid.ToString(), user.Id), payload, ttl); var email = user.Identities .FirstOrDefault(i => i.Type == IdentityType.Email && i.IsPrimary)?.Identifier; if (!string.IsNullOrWhiteSpace(email)) - await r.StringSetAsync(IdentityEmailKey(tid, email!), payload, ttl); + await r.StringSetAsync(IdentityEmailKey(tid.ToString(), email!), payload, ttl); } - catch { /* ignore */ } + catch { } } } - // ========= Identities ========= - - // (1) IMPLEMENTED: AddIdentityAsync public async Task AddIdentityAsync(Guid userId, IdentityType type, string identifier, bool isPrimary, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); var norm = identifier.Trim().ToLowerInvariant(); if (isPrimary) @@ -171,11 +176,10 @@ public class UserRepository : IUserRepository await db.SaveChangesAsync(ct); } - // (2) IMPLEMENTED: VerifyIdentityAsync public async Task VerifyIdentityAsync(Guid userId, IdentityType type, string identifier, DateTimeOffset verifiedAt, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); var norm = identifier.Trim().ToLowerInvariant(); var id = await db.UserIdentities @@ -190,11 +194,10 @@ public class UserRepository : IUserRepository await db.SaveChangesAsync(ct); } - // (3) IMPLEMENTED: GetPrimaryIdentityAsync public async Task GetPrimaryIdentityAsync(Guid userId, IdentityType type, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); return await db.UserIdentities.AsNoTracking() .FirstOrDefaultAsync(i => @@ -204,12 +207,10 @@ public class UserRepository : IUserRepository i.IsPrimary, ct); } - // ========= Password ========= - public async Task ChangePasswordAsync(Guid userId, string newPasswordHash, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); var user = await db.Users.FirstOrDefaultAsync(u => u.TenantId == tid && u.Id == userId, ct); if (user is null) return; @@ -221,8 +222,8 @@ public class UserRepository : IUserRepository public async Task AddPasswordHistoryAsync(Guid userId, string passwordHash, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); var h = new UserPasswordHistory { @@ -236,12 +237,10 @@ public class UserRepository : IUserRepository await db.SaveChangesAsync(ct); } - // ========= MFA ========= - public async Task AddTotpFactorAsync(Guid userId, string label, string secret, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); var f = new UserMfaFactor { @@ -265,8 +264,8 @@ public class UserRepository : IUserRepository public async Task DisableMfaFactorAsync(Guid factorId, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); var f = await db.UserMfaFactors.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == factorId, ct); if (f is null) return; @@ -285,26 +284,45 @@ public class UserRepository : IUserRepository public async Task HasAnyMfaAsync(Guid userId, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); return await db.UserMfaFactors.AnyAsync(x => x.TenantId == tid && x.UserId == userId && x.Enabled, ct); } - // ========= Sessions ========= - public async Task CreateSessionAsync(UserSession session, CancellationToken ct = default) { + var tid = TenantId(); var db = _scope.Get(); - session.TenantId = TenantId(); + session.TenantId = tid; await db.UserSessions.AddAsync(session, ct); await db.SaveChangesAsync(ct); return session; } - public async Task RevokeSessionAsync(Guid userId, Guid sessionId, CancellationToken ct = default) + public async Task FindSessionByRefreshHashAsync(Guid tenantId, string refreshTokenHash, CancellationToken ct = default) { var db = _scope.Get(); + return await db.UserSessions + .AsNoTracking() + .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.RefreshTokenHash == refreshTokenHash, ct); + } + + public async Task RotateSessionRefreshAsync(Guid tenantId, Guid sessionId, string newRefreshTokenHash, DateTimeOffset newIssuedAt, DateTimeOffset? newExpiresAt, CancellationToken ct = default) + { + var db = _scope.Get(); + var s = await db.UserSessions.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Id == sessionId, ct); + if (s is null || s.RevokedAt.HasValue) return false; + s.RefreshTokenHash = newRefreshTokenHash; + s.IssuedAt = newIssuedAt; + s.ExpiresAt = newExpiresAt; + await db.SaveChangesAsync(ct); + return true; + } + + public async Task RevokeSessionAsync(Guid userId, Guid sessionId, CancellationToken ct = default) + { var tid = TenantId(); + var db = _scope.Get(); var s = await db.UserSessions.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == sessionId && x.UserId == userId, ct); if (s is null) return 0; @@ -315,8 +333,8 @@ public class UserRepository : IUserRepository public async Task RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default) { - var db = _scope.Get(); var tid = TenantId(); + var db = _scope.Get(); var sessions = await db.UserSessions .Where(x => x.TenantId == tid && x.UserId == userId && x.RevokedAt == null) @@ -326,4 +344,61 @@ public class UserRepository : IUserRepository return await db.SaveChangesAsync(ct); } + + public async Task IsSessionActiveAsync(Guid userId, Guid sessionId, CancellationToken ct = default) + { + var tid = TenantId(); + var db = _scope.Get(); + var now = DateTimeOffset.UtcNow; + return await db.UserSessions + .AsNoTracking() + .AnyAsync(x => + x.TenantId == tid && + x.Id == sessionId && + x.UserId == userId && + x.RevokedAt == null && + (!x.ExpiresAt.HasValue || x.ExpiresAt > now), ct); + } + + private static string TenantTokenVersionKey(Guid tenantId) => $"eop:{tenantId}:token:version"; + + public async Task GetTenantTokenVersionAsync(Guid tenantId, CancellationToken ct = default) + { + var r = _redis?.GetDatabase(); + if (r is null) return "1"; + var key = TenantTokenVersionKey(tenantId); + var val = await r.StringGetAsync(key); + if (val.HasValue) return val.ToString(); + await r.StringSetAsync(key, "1"); + return "1"; + } + + public async Task BumpTenantTokenVersionAsync(Guid tenantId, CancellationToken ct = default) + { + var r = _redis?.GetDatabase(); + if (r is null) return; + var key = TenantTokenVersionKey(tenantId); + await r.StringIncrementAsync(key); + } + + public async Task GetUserSecurityStampAsync(Guid userId, CancellationToken ct = default) + { + var tid = TenantId(); + var db = _scope.Get(); + return await db.Users + .AsNoTracking() + .Where(x => x.TenantId == tid && x.Id == userId) + .Select(x => x.SecurityStamp) + .FirstOrDefaultAsync(ct); + } + + public async Task BumpUserSecurityStampAsync(Guid userId, CancellationToken ct = default) + { + var tid = TenantId(); + var db = _scope.Get(); + var u = await db.Users.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == userId, ct); + if (u is null) return; + u.SecurityStamp = Guid.NewGuid().ToString("N"); + await db.SaveChangesAsync(ct); + } } \ No newline at end of file diff --git a/AMREZ.EOP.Infrastructures/Security/JwtFactory.cs b/AMREZ.EOP.Infrastructures/Security/JwtFactory.cs new file mode 100644 index 0000000..9fc29ad --- /dev/null +++ b/AMREZ.EOP.Infrastructures/Security/JwtFactory.cs @@ -0,0 +1,51 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using AMREZ.EOP.Abstractions.Applications.Tenancy; +using AMREZ.EOP.Abstractions.Security; +using AMREZ.EOP.Infrastructures.Options; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace AMREZ.EOP.Infrastructures.Security; + +public sealed class JwtFactory : IJwtFactory +{ + private readonly ITenantResolver _resolver; + private readonly IHttpContextAccessor _http; + + private const string Issuer = "amrez.eop"; + private const string Audience = "amrez.eop.clients"; + private const int AccessMinutes = 10; + + public JwtFactory(ITenantResolver resolver, IHttpContextAccessor http) + { + _resolver = resolver; + _http = http; + } + + public (string token, DateTimeOffset expiresAt) CreateAccessToken(IEnumerable claims) + { + var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext"); + var tenant = _resolver.Resolve(http) ?? throw new InvalidOperationException("No tenant context"); + + var material = !string.IsNullOrWhiteSpace(tenant.Id) ? tenant.Id! : tenant.TenantKey!; + var keyBytes = SHA256.HashData(Encoding.UTF8.GetBytes(material)); + var cred = new SigningCredentials(new SymmetricSecurityKey(keyBytes), SecurityAlgorithms.HmacSha256); + + var now = DateTimeOffset.UtcNow; + var exp = now.AddMinutes(AccessMinutes); + + var jwt = new JwtSecurityToken( + issuer: Issuer, + audience: Audience, + claims: claims, + notBefore: now.UtcDateTime, + expires: exp.UtcDateTime, + signingCredentials: cred); + + return (new JwtSecurityTokenHandler().WriteToken(jwt), exp); + } +} \ No newline at end of file diff --git a/AMREZ.EOP.Infrastructures/Tenancy/DefaultTenantResolver.cs b/AMREZ.EOP.Infrastructures/Tenancy/DefaultTenantResolver.cs index d42e3ad..d651d9b 100644 --- a/AMREZ.EOP.Infrastructures/Tenancy/DefaultTenantResolver.cs +++ b/AMREZ.EOP.Infrastructures/Tenancy/DefaultTenantResolver.cs @@ -28,7 +28,6 @@ public sealed class DefaultTenantResolver : ITenantResolver var cid = http.TraceIdentifier; var path = http.Request.Path.Value ?? string.Empty; - // ✅ ทางลัด: ขอ platform ตรง ๆ ด้วย hint if (hint is string hs && string.Equals(hs, "@platform", StringComparison.OrdinalIgnoreCase)) { if (_map.TryGetBySlug(PlatformSlug, out var platform)) diff --git a/AMREZ.EOP.Infrastructures/Tenancy/TenantMapLoader.cs b/AMREZ.EOP.Infrastructures/Tenancy/TenantMapLoader.cs index d2027d7..daa2cd9 100644 --- a/AMREZ.EOP.Infrastructures/Tenancy/TenantMapLoader.cs +++ b/AMREZ.EOP.Infrastructures/Tenancy/TenantMapLoader.cs @@ -63,7 +63,6 @@ namespace AMREZ.EOP.Infrastructures.Tenancy; } } - // 3) Bootstrap 'public' ถ้ายังไม่มี (กัน chicken-and-egg) if (!map.TryGetBySlug("public", out _)) { map.UpsertTenant( diff --git a/AMREZ.EOP.Infrastructures/UnitOfWork/EFUnitOfWork.cs b/AMREZ.EOP.Infrastructures/UnitOfWork/EFUnitOfWork.cs index c3c8cdc..d42a586 100644 --- a/AMREZ.EOP.Infrastructures/UnitOfWork/EFUnitOfWork.cs +++ b/AMREZ.EOP.Infrastructures/UnitOfWork/EFUnitOfWork.cs @@ -18,7 +18,6 @@ public sealed class EFUnitOfWork : IUnitOfWork, IAsyncDisposable private readonly ITenantDbContextFactory _factory; private readonly IConnectionMultiplexer? _redis; - private AppDbContext? _db; private IDbContextTransaction? _tx; private ITenantContext? _tenant; @@ -31,40 +30,38 @@ public sealed class EFUnitOfWork : IUnitOfWork, IAsyncDisposable { _scope = scope; _factory = factory; - _redis = redis; // optional; null ได้ + _redis = redis; } public async Task BeginAsync(ITenantContext tenant, IsolationLevel isolation = IsolationLevel.ReadCommitted, CancellationToken ct = default) { - if (_db is not null) return; + if (_tx is not null) return; _tenant = tenant ?? throw new ArgumentNullException(nameof(tenant)); _scope.EnsureForTenant(tenant); - _db = _scope.Get(); - _tx = await _db.Database.BeginTransactionAsync(isolation, ct); + var db = _scope.Get(); + _tx = await db.Database.BeginTransactionAsync(isolation, ct); } public async Task CommitAsync(CancellationToken ct = default) { - if (_db is null) return; // ยังไม่ Begin + if (_tx is null) return; - // track entities ที่เปลี่ยน (เพื่อ invalidate cache แบบแม่นขึ้น) - var changedUsers = _db.ChangeTracker.Entries() + var db = _scope.Get(); + + var changedUsers = db.ChangeTracker.Entries() .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) .Select(e => e.Entity) .ToList(); - var changedIdentities = _db.ChangeTracker.Entries() + var changedIdentities = db.ChangeTracker.Entries() .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) .Select(e => e.Entity) .ToList(); - await _db.SaveChangesAsync(ct); + await db.SaveChangesAsync(ct); + await _tx.CommitAsync(ct); - if (_tx is not null) - await _tx.CommitAsync(ct); - - // optional: invalidate/refresh Redis (ล่มก็ไม่พัง UoW) if (_redis is not null && _tenant is not null && (changedUsers.Count > 0 || changedIdentities.Count > 0)) { var r = _redis.GetDatabase(); @@ -75,8 +72,6 @@ public sealed class EFUnitOfWork : IUnitOfWork, IAsyncDisposable { var keyId = $"eop:{tenantId}:user:id:{u.Id:N}"; tasks.Add(r.KeyDeleteAsync(keyId)); - - // refresh cache (ถ้าต้องการ) var payload = JsonSerializer.Serialize(u); var ttl = TimeSpan.FromMinutes(5); tasks.Add(r.StringSetAsync(keyId, payload, ttl)); @@ -88,7 +83,7 @@ public sealed class EFUnitOfWork : IUnitOfWork, IAsyncDisposable tasks.Add(r.KeyDeleteAsync(k)); } - try { await Task.WhenAll(tasks); } catch { /* swallow */ } + try { await Task.WhenAll(tasks); } catch { } } await DisposeAsync(); @@ -109,11 +104,9 @@ public sealed class EFUnitOfWork : IUnitOfWork, IAsyncDisposable public ValueTask DisposeAsync() { - try { _tx?.Dispose(); } catch { /* ignore */ } - try { _db?.Dispose(); } catch { /* ignore */ } - - _tx = null; _db = null; _tenant = null; - + try { _tx?.Dispose(); } catch { } + _tx = null; + _tenant = null; return ValueTask.CompletedTask; } } \ No newline at end of file