Add Login Module

This commit is contained in:
Thanakarn Klangkasame
2025-10-02 11:18:44 +07:00
parent f505e31cfd
commit 563a341a99
52 changed files with 1127 additions and 2036 deletions

View File

@@ -4,9 +4,11 @@ using AMREZ.EOP.Contracts.DTOs.Authentications.AddEmailIdentity;
using AMREZ.EOP.Contracts.DTOs.Authentications.ChangePassword; using AMREZ.EOP.Contracts.DTOs.Authentications.ChangePassword;
using AMREZ.EOP.Contracts.DTOs.Authentications.DisableMfa; using AMREZ.EOP.Contracts.DTOs.Authentications.DisableMfa;
using AMREZ.EOP.Contracts.DTOs.Authentications.EnableTotp; 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.Login;
using AMREZ.EOP.Contracts.DTOs.Authentications.Logout; using AMREZ.EOP.Contracts.DTOs.Authentications.Logout;
using AMREZ.EOP.Contracts.DTOs.Authentications.LogoutAll; 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.Register;
using AMREZ.EOP.Contracts.DTOs.Authentications.VerifyEmail; using AMREZ.EOP.Contracts.DTOs.Authentications.VerifyEmail;
using AMREZ.EOP.Domain.Shared.Contracts; using AMREZ.EOP.Domain.Shared.Contracts;
@@ -32,14 +34,33 @@ public class AuthenticationController : ControllerBase
private readonly ILogoutUseCase _logout; private readonly ILogoutUseCase _logout;
private readonly ILogoutAllUseCase _logoutAll; private readonly ILogoutAllUseCase _logoutAll;
private readonly IIssueTokenPairUseCase _issueTokens;
private readonly IRefreshUseCase _refresh;
public AuthenticationController( public AuthenticationController(
ILoginUseCase login, ILoginUseCase login,
IRegisterUseCase register, IRegisterUseCase register,
IChangePasswordUseCase changePassword) IChangePasswordUseCase changePassword,
IAddEmailIdentityUseCase addEmail,
IVerifyEmailUseCase verifyEmail,
IEnableTotpUseCase enableTotp,
IDisableMfaUseCase disableMfa,
ILogoutUseCase logout,
ILogoutAllUseCase logoutAll,
IIssueTokenPairUseCase issueTokens,
IRefreshUseCase refresh)
{ {
_login = login; _login = login;
_register = register; _register = register;
_changePassword = changePassword; _changePassword = changePassword;
_addEmail = addEmail;
_verifyEmail = verifyEmail;
_enableTotp = enableTotp;
_disableMfa = disableMfa;
_logout = logout;
_logoutAll = logoutAll;
_issueTokens = issueTokens;
_refresh = refresh;
} }
[HttpPost("login")] [HttpPost("login")]
@@ -51,7 +72,7 @@ public class AuthenticationController : ControllerBase
var claims = new List<Claim> var claims = new List<Claim>
{ {
new(ClaimTypes.NameIdentifier, res.UserId.ToString()), 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(ClaimTypes.Email, res.Email),
new("tenant", res.TenantId) new("tenant", res.TenantId)
}; };
@@ -59,7 +80,69 @@ public class AuthenticationController : ControllerBase
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthPolicies.Scheme)); var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthPolicies.Scheme));
await HttpContext.SignInAsync(AuthPolicies.Scheme, principal); 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<IActionResult> 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")] [HttpPost("register")]

View File

@@ -0,0 +1,8 @@
using AMREZ.EOP.Contracts.DTOs.Authentications.IssueTokenPair;
namespace AMREZ.EOP.Abstractions.Applications.UseCases.Authentications;
public interface IIssueTokenPairUseCase
{
Task<IssueTokenPairResponse> ExecuteAsync(IssueTokenPairRequest request, CancellationToken ct);
}

View File

@@ -0,0 +1,8 @@
using AMREZ.EOP.Contracts.DTOs.Authentications.Refresh;
namespace AMREZ.EOP.Abstractions.Applications.UseCases.Authentications;
public interface IRefreshUseCase
{
Task<RefreshResponse?> ExecuteAsync(RefreshRequest request, CancellationToken ct);
}

View File

@@ -5,27 +5,34 @@ namespace AMREZ.EOP.Abstractions.Infrastructures.Repositories;
public interface IUserRepository public interface IUserRepository
{ {
// ===== Users/Identities/Password/MFA (ของเดิม) =====
Task<User?> FindByIdAsync(Guid userId, CancellationToken ct = default); Task<User?> FindByIdAsync(Guid userId, CancellationToken ct = default);
Task<User?> FindActiveByEmailAsync(string email, CancellationToken ct = default); Task<User?> FindActiveByEmailAsync(string email, CancellationToken ct = default);
Task<bool> EmailExistsAsync(string email, CancellationToken ct = default); Task<bool> EmailExistsAsync(string email, CancellationToken ct = default);
Task AddAsync(User user, 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 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 VerifyIdentityAsync(Guid userId, IdentityType type, string identifier, DateTimeOffset verifiedAt, CancellationToken ct = default);
Task<UserIdentity?> GetPrimaryIdentityAsync(Guid userId, IdentityType type, CancellationToken ct = default); Task<UserIdentity?> GetPrimaryIdentityAsync(Guid userId, IdentityType type, CancellationToken ct = default);
// Password
Task ChangePasswordAsync(Guid userId, string newPasswordHash, CancellationToken ct = default); Task ChangePasswordAsync(Guid userId, string newPasswordHash, CancellationToken ct = default);
Task AddPasswordHistoryAsync(Guid userId, string passwordHash, CancellationToken ct = default); Task AddPasswordHistoryAsync(Guid userId, string passwordHash, CancellationToken ct = default);
// MFA
Task<UserMfaFactor> AddTotpFactorAsync(Guid userId, string label, string secret, CancellationToken ct = default); Task<UserMfaFactor> AddTotpFactorAsync(Guid userId, string label, string secret, CancellationToken ct = default);
Task DisableMfaFactorAsync(Guid factorId, CancellationToken ct = default); Task DisableMfaFactorAsync(Guid factorId, CancellationToken ct = default);
Task<bool> HasAnyMfaAsync(Guid userId, CancellationToken ct = default); Task<bool> HasAnyMfaAsync(Guid userId, CancellationToken ct = default);
// Sessions // ===== Sessions (เก็บ refresh ไว้ใน session) =====
Task<UserSession> CreateSessionAsync(UserSession session, CancellationToken ct = default); Task<UserSession> CreateSessionAsync(UserSession session, CancellationToken ct = default);
Task<UserSession?> FindSessionByRefreshHashAsync(Guid tenantId, string refreshTokenHash, CancellationToken ct = default);
Task<bool> RotateSessionRefreshAsync(Guid tenantId, Guid sessionId, string newRefreshTokenHash, DateTimeOffset newIssuedAt, DateTimeOffset? newExpiresAt, CancellationToken ct = default);
Task<int> RevokeSessionAsync(Guid userId, Guid sessionId, CancellationToken ct = default); Task<int> RevokeSessionAsync(Guid userId, Guid sessionId, CancellationToken ct = default);
Task<int> RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default); Task<int> RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default);
Task<bool> IsSessionActiveAsync(Guid userId, Guid sessionId, CancellationToken ct = default);
// ===== Kill switches / stamps (ใช้สำหรับ revoke-all ระดับ tenant/user) =====
Task<string> GetTenantTokenVersionAsync(Guid tenantId, CancellationToken ct = default);
Task BumpTenantTokenVersionAsync(Guid tenantId, CancellationToken ct = default);
Task<string?> GetUserSecurityStampAsync(Guid userId, CancellationToken ct = default);
Task BumpUserSecurityStampAsync(Guid userId, CancellationToken ct = default);
} }

View File

@@ -0,0 +1,8 @@
using System.Security.Claims;
namespace AMREZ.EOP.Abstractions.Security;
public interface IJwtFactory
{
(string token, DateTimeOffset expiresAt) CreateAccessToken(IEnumerable<Claim> claims);
}

View File

@@ -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<IssueTokenPairResponse> 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<Claim>
{
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);
}
}

View File

@@ -39,8 +39,7 @@ public sealed class LoginUseCase : ILoginUseCase
await _uow.CommitAsync(ct); await _uow.CommitAsync(ct);
// NOTE: ไม่ใช้ DisplayName ใน Entity แล้ว — ส่งกลับเป็นค่าว่าง/ไปดึงจาก HR ฝั่ง API return new LoginResponse(user.Id , email, tenant.Id);
return new LoginResponse(user.Id, string.Empty, email, tenant.Id);
} }
catch catch
{ {

View File

@@ -16,12 +16,7 @@ public sealed class LogoutAllUseCase : ILogoutAllUseCase
private readonly IHttpContextAccessor _http; private readonly IHttpContextAccessor _http;
public LogoutAllUseCase(ITenantResolver r, IUnitOfWork uow, IUserRepository users, 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<int> ExecuteAsync(LogoutAllRequest request, CancellationToken ct = default) public async Task<int> ExecuteAsync(LogoutAllRequest request, CancellationToken ct = default)
{ {

View File

@@ -4,6 +4,7 @@ using AMREZ.EOP.Abstractions.Applications.UseCases.Authentications;
using AMREZ.EOP.Abstractions.Infrastructures.Common; using AMREZ.EOP.Abstractions.Infrastructures.Common;
using AMREZ.EOP.Abstractions.Infrastructures.Repositories; using AMREZ.EOP.Abstractions.Infrastructures.Repositories;
using AMREZ.EOP.Contracts.DTOs.Authentications.Logout; using AMREZ.EOP.Contracts.DTOs.Authentications.Logout;
using AMREZ.EOP.Domain.Entities.Authentications;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace AMREZ.EOP.Application.UseCases.Authentications; namespace AMREZ.EOP.Application.UseCases.Authentications;
@@ -31,6 +32,10 @@ public sealed class LogoutUseCase : ILogoutUseCase
await _uow.CommitAsync(ct); await _uow.CommitAsync(ct);
return n > 0; return n > 0;
} }
catch { await _uow.RollbackAsync(ct); throw; } catch
{
await _uow.RollbackAsync(ct);
throw;
}
} }
} }

View File

@@ -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<RefreshResponse?> 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<Claim>
{
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);
}
}

View File

@@ -64,9 +64,9 @@ public sealed class RegisterUseCase : IRegisterUseCase
IsActive = true, IsActive = true,
}; };
// แนบอัตลักษณ์แบบ Email (เก็บในตารางลูก)
user.Identities.Add(new UserIdentity user.Identities.Add(new UserIdentity
{ {
TenantId = tn.TenantId,
Type = IdentityType.Email, Type = IdentityType.Email,
Identifier = emailNorm, Identifier = emailNorm,
IsPrimary = true, IsPrimary = true,
@@ -76,7 +76,6 @@ public sealed class RegisterUseCase : IRegisterUseCase
await _users.AddAsync(user, ct); await _users.AddAsync(user, ct);
await _uow.CommitAsync(ct); await _uow.CommitAsync(ct);
// ไม่ส่ง DisplayName (ปล่อยให้ HR/Presentation สร้าง)
return new RegisterResponse(user.Id, string.Empty, emailNorm, tenant.Id); return new RegisterResponse(user.Id, string.Empty, emailNorm, tenant.Id);
} }
catch catch

View File

@@ -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!;
}

View File

@@ -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; }
}

View File

@@ -2,7 +2,6 @@ namespace AMREZ.EOP.Contracts.DTOs.Authentications.Login;
public sealed record LoginResponse( public sealed record LoginResponse(
Guid UserId, Guid UserId,
string DisplayName,
string Email, string Email,
string TenantId string TenantId
); );

View File

@@ -0,0 +1,9 @@
namespace AMREZ.EOP.Contracts.DTOs.Authentications.Refresh;
public sealed class RefreshRequest
{
/// <summary>
/// ถ้าไม่ส่งมา จะไปอ่านจาก HttpOnly cookie ชื่อ "refresh_token" ใน Controller
/// </summary>
public string? RefreshToken { get; init; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications;
public sealed class Permission : BaseEntity public sealed class Permission : BaseEntity
{ {
public Guid TenantId { get; set; }
public string Code { get; set; } = default!; // e.g. "auth:session:read" public string Code { get; set; } = default!; // e.g. "auth:session:read"
public string Name { get; set; } = default!; public string Name { get; set; } = default!;

View File

@@ -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!;
}

View File

@@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications;
public sealed class Role : BaseEntity public sealed class Role : BaseEntity
{ {
public Guid TenantId { get; set; }
public string Code { get; set; } = default!; // system code, unique per tenant public string Code { get; set; } = default!; // system code, unique per tenant
public string Name { get; set; } = default!; public string Name { get; set; } = default!;

View File

@@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications;
public sealed class RolePermission : BaseEntity public sealed class RolePermission : BaseEntity
{ {
public Guid TenantId { get; set; }
public Guid RoleId { get; set; } public Guid RoleId { get; set; }
public Guid PermissionId { get; set; } public Guid PermissionId { get; set; }

View File

@@ -4,8 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications;
public sealed class User : BaseEntity public sealed class User : BaseEntity
{ {
public Guid TenantId { get; set; }
public string PasswordHash { get; set; } = default!; public string PasswordHash { get; set; } = default!;
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;

View File

@@ -5,7 +5,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications;
public sealed class UserExternalAccount : BaseEntity public sealed class UserExternalAccount : BaseEntity
{ {
public Guid TenantId { get; set; }
public Guid UserId { get; set; } public Guid UserId { get; set; }
public ExternalProvider Provider { get; set; } public ExternalProvider Provider { get; set; }

View File

@@ -5,7 +5,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications;
public sealed class UserIdentity : BaseEntity public sealed class UserIdentity : BaseEntity
{ {
public Guid TenantId { get; set; }
public Guid UserId { get; set; } public Guid UserId { get; set; }
public IdentityType Type { get; set; } public IdentityType Type { get; set; }

View File

@@ -5,7 +5,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications;
public sealed class UserMfaFactor : BaseEntity public sealed class UserMfaFactor : BaseEntity
{ {
public Guid TenantId { get; set; }
public Guid UserId { get; set; } public Guid UserId { get; set; }
public MfaType Type { get; set; } public MfaType Type { get; set; }

View File

@@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications;
public sealed class UserPasswordHistory : BaseEntity public sealed class UserPasswordHistory : BaseEntity
{ {
public Guid TenantId { get; set; }
public Guid UserId { get; set; } public Guid UserId { get; set; }
public string PasswordHash { get; set; } = default!; public string PasswordHash { get; set; } = default!;

View File

@@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications;
public sealed class UserRole : BaseEntity public sealed class UserRole : BaseEntity
{ {
public Guid TenantId { get; set; }
public Guid UserId { get; set; } public Guid UserId { get; set; }
public Guid RoleId { get; set; } public Guid RoleId { get; set; }

View File

@@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.Authentications;
public sealed class UserSession : BaseEntity public sealed class UserSession : BaseEntity
{ {
public Guid TenantId { get; set; }
public Guid UserId { get; set; } public Guid UserId { get; set; }
public string RefreshTokenHash { get; set; } = default!; public string RefreshTokenHash { get; set; } = default!;

View File

@@ -3,7 +3,7 @@ namespace AMREZ.EOP.Domain.Entities.Common;
public abstract class BaseEntity public abstract class BaseEntity
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public string TenantId { get; set; } = default!; public Guid TenantId { get; set; } = default!;
public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset CreatedAt { get; set; }
public string? CreatedBy { get; set; } public string? CreatedBy { get; set; }

View File

@@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources;
public sealed class Department : BaseEntity public sealed class Department : BaseEntity
{ {
public Guid TenantId { get; set; }
public string Code { get; set; } = default!; public string Code { get; set; } = default!;
public string Name { get; set; } = default!; public string Name { get; set; } = default!;

View File

@@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources;
public sealed class EmergencyContact : BaseEntity public sealed class EmergencyContact : BaseEntity
{ {
public Guid TenantId { get; set; }
public Guid UserProfileId { get; set; } public Guid UserProfileId { get; set; }
public string Name { get; set; } = default!; public string Name { get; set; } = default!;

View File

@@ -5,7 +5,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources;
public sealed class EmployeeAddress : BaseEntity public sealed class EmployeeAddress : BaseEntity
{ {
public Guid TenantId { get; set; }
public Guid UserProfileId { get; set; } public Guid UserProfileId { get; set; }
public AddressType Type { get; set; } = AddressType.Home; public AddressType Type { get; set; } = AddressType.Home;

View File

@@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources;
public sealed class EmployeeBankAccount : BaseEntity public sealed class EmployeeBankAccount : BaseEntity
{ {
public Guid TenantId { get; set; }
public Guid UserProfileId { get; set; } public Guid UserProfileId { get; set; }
public string BankName { get; set; } = default!; public string BankName { get; set; } = default!;

View File

@@ -5,7 +5,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources;
public sealed class Employment : BaseEntity public sealed class Employment : BaseEntity
{ {
public Guid TenantId { get; set; }
public Guid UserProfileId { get; set; } public Guid UserProfileId { get; set; }
public EmploymentType EmploymentType { get; set; } = EmploymentType.Permanent; public EmploymentType EmploymentType { get; set; } = EmploymentType.Permanent;

View File

@@ -4,7 +4,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources;
public sealed class Position : BaseEntity public sealed class Position : BaseEntity
{ {
public Guid TenantId { get; set; }
public string Code { get; set; } = default!; public string Code { get; set; } = default!;
public string Title { get; set; } = default!; public string Title { get; set; } = default!;
public int? Level { get; set; } public int? Level { get; set; }

View File

@@ -6,7 +6,6 @@ namespace AMREZ.EOP.Domain.Entities.HumanResources;
public sealed class UserProfile : BaseEntity public sealed class UserProfile : BaseEntity
{ {
public Guid TenantId { get; set; }
public Guid UserId { get; set; } public Guid UserId { get; set; }
public string FirstName { get; set; } = default!; public string FirstName { get; set; } = default!;

View File

@@ -17,6 +17,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="StackExchange.Redis" Version="2.9.17" /> <PackageReference Include="StackExchange.Redis" Version="2.9.17" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -3,6 +3,7 @@ using AMREZ.EOP.Domain.Entities.Common;
using AMREZ.EOP.Domain.Entities.HumanResources; using AMREZ.EOP.Domain.Entities.HumanResources;
using AMREZ.EOP.Domain.Entities.Tenancy; using AMREZ.EOP.Domain.Entities.Tenancy;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace AMREZ.EOP.Infrastructures.Data; namespace AMREZ.EOP.Infrastructures.Data;
@@ -37,11 +38,14 @@ public class AppDbContext : DbContext
protected override void OnModelCreating(ModelBuilder model) protected override void OnModelCreating(ModelBuilder model)
{ {
// ====== Global Tenancy Config (meta schema) — ไม่สืบทอด BaseEntity ====== // ====== Tenancy (meta) ======
model.Entity<TenantConfig>(b => model.Entity<TenantConfig>(b =>
{ {
b.ToTable("tenants", schema: "meta"); 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.TenantKey).HasMaxLength(128).IsRequired();
b.Property(x => x.Schema).HasMaxLength(128); b.Property(x => x.Schema).HasMaxLength(128);
b.Property(x => x.ConnectionString); b.Property(x => x.ConnectionString);
@@ -58,7 +62,7 @@ public class AppDbContext : DbContext
b.ToTable("tenant_domains", schema: "meta"); b.ToTable("tenant_domains", schema: "meta");
b.HasKey(x => x.Domain); b.HasKey(x => x.Domain);
b.Property(x => x.Domain).HasMaxLength(253).IsRequired(); 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.IsPlatformBaseDomain).HasDefaultValue(false);
b.Property(x => x.IsActive).HasDefaultValue(true); b.Property(x => x.IsActive).HasDefaultValue(true);
b.Property(x => x.UpdatedAtUtc) b.Property(x => x.UpdatedAtUtc)
@@ -81,36 +85,13 @@ public class AppDbContext : DbContext
b.ToTable("users"); b.ToTable("users");
b.HasKey(x => x.Id); 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.PasswordHash).IsRequired();
b.Property(x => x.IsActive).HasDefaultValue(true); b.Property(x => x.IsActive).HasDefaultValue(true);
b.Property(x => x.AccessFailedCount).HasDefaultValue(0); b.Property(x => x.AccessFailedCount).HasDefaultValue(0);
b.Property(x => x.MfaEnabled).HasDefaultValue(false); 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<UserIdentity>(b => model.Entity<UserIdentity>(b =>
@@ -122,11 +103,16 @@ public class AppDbContext : DbContext
b.Property(x => x.Identifier).IsRequired().HasMaxLength(256); b.Property(x => x.Identifier).IsRequired().HasMaxLength(256);
b.Property(x => x.IsPrimary).HasDefaultValue(false); b.Property(x => x.IsPrimary).HasDefaultValue(false);
b.HasIndex(x => new { x.TenantId, x.Type, x.Identifier }) b.HasIndex(x => new { x.TenantId, x.Type, x.Identifier }).IsUnique();
.IsUnique();
b.HasIndex(x => new { x.TenantId, x.UserId, x.Type, x.IsPrimary }) b.HasIndex(x => new { x.TenantId, x.UserId, x.Type, x.IsPrimary })
.HasDatabaseName("ix_user_identity_primary_per_type"); .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<UserMfaFactor>(b => model.Entity<UserMfaFactor>(b =>
@@ -136,8 +122,13 @@ public class AppDbContext : DbContext
b.Property(x => x.Type).IsRequired(); b.Property(x => x.Type).IsRequired();
b.Property(x => x.Enabled).HasDefaultValue(true); b.Property(x => x.Enabled).HasDefaultValue(true);
b.HasIndex(x => new { x.TenantId, x.UserId }); 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<UserSession>(b => model.Entity<UserSession>(b =>
@@ -148,6 +139,12 @@ public class AppDbContext : DbContext
b.Property(x => x.RefreshTokenHash).IsRequired(); b.Property(x => x.RefreshTokenHash).IsRequired();
b.HasIndex(x => new { x.TenantId, x.UserId }); b.HasIndex(x => new { x.TenantId, x.UserId });
b.HasIndex(x => new { x.TenantId, x.DeviceId }); 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<UserPasswordHistory>(b => model.Entity<UserPasswordHistory>(b =>
@@ -156,6 +153,12 @@ public class AppDbContext : DbContext
b.HasKey(x => x.Id); b.HasKey(x => x.Id);
b.Property(x => x.PasswordHash).IsRequired(); b.Property(x => x.PasswordHash).IsRequired();
b.HasIndex(x => new { x.TenantId, x.UserId, x.ChangedAt }); 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<UserExternalAccount>(b => model.Entity<UserExternalAccount>(b =>
@@ -165,15 +168,20 @@ public class AppDbContext : DbContext
b.Property(x => x.Provider).IsRequired(); b.Property(x => x.Provider).IsRequired();
b.Property(x => x.Subject).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 }) b.HasOne(x => x.User)
.IsUnique(); .WithMany(u => u.ExternalAccounts)
.HasForeignKey(x => new { x.TenantId, x.UserId })
.HasPrincipalKey(nameof(User.TenantId), nameof(User.Id))
.OnDelete(DeleteBehavior.Cascade);
}); });
model.Entity<Role>(b => model.Entity<Role>(b =>
{ {
b.ToTable("roles"); b.ToTable("roles");
b.HasKey(x => x.Id); 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.Code).IsRequired().HasMaxLength(128);
b.Property(x => x.Name).IsRequired().HasMaxLength(256); b.Property(x => x.Name).IsRequired().HasMaxLength(256);
@@ -185,6 +193,7 @@ public class AppDbContext : DbContext
{ {
b.ToTable("permissions"); b.ToTable("permissions");
b.HasKey(x => x.Id); 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.Code).IsRequired().HasMaxLength(256);
b.Property(x => x.Name).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.HasKey(x => x.Id);
b.HasIndex(x => new { x.TenantId, x.UserId, x.RoleId }).IsUnique(); b.HasIndex(x => new { x.TenantId, x.UserId, x.RoleId }).IsUnique();
b.HasOne<User>()
.WithMany()
.HasForeignKey(x => new { x.TenantId, x.UserId })
.HasPrincipalKey(nameof(User.TenantId), nameof(User.Id))
.OnDelete(DeleteBehavior.Cascade);
b.HasOne<Role>()
.WithMany()
.HasForeignKey(x => new { x.TenantId, x.RoleId })
.HasPrincipalKey(nameof(Role.TenantId), nameof(Role.Id))
.OnDelete(DeleteBehavior.Cascade);
}); });
model.Entity<RolePermission>(b => model.Entity<RolePermission>(b =>
@@ -206,6 +227,18 @@ public class AppDbContext : DbContext
b.HasKey(x => x.Id); b.HasKey(x => x.Id);
b.HasIndex(x => new { x.TenantId, x.RoleId, x.PermissionId }).IsUnique(); b.HasIndex(x => new { x.TenantId, x.RoleId, x.PermissionId }).IsUnique();
b.HasOne<Role>()
.WithMany()
.HasForeignKey(x => new { x.TenantId, x.RoleId })
.HasPrincipalKey(nameof(Role.TenantId), nameof(Role.Id))
.OnDelete(DeleteBehavior.Cascade);
b.HasOne<Permission>()
.WithMany()
.HasForeignKey(x => new { x.TenantId, x.PermissionId })
.HasPrincipalKey(nameof(Permission.TenantId), nameof(Permission.Id))
.OnDelete(DeleteBehavior.Cascade);
}); });
// ====== HR ====== // ====== HR ======
@@ -213,22 +246,25 @@ public class AppDbContext : DbContext
{ {
b.ToTable("user_profiles"); b.ToTable("user_profiles");
b.HasKey(x => x.Id); 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.FirstName).IsRequired().HasMaxLength(128);
b.Property(x => x.LastName).IsRequired().HasMaxLength(128); b.Property(x => x.LastName).IsRequired().HasMaxLength(128);
b.HasOne(x => x.User)
.WithOne()
.HasForeignKey<UserProfile>(x => x.UserId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.TenantId, x.UserId }).IsUnique(); b.HasIndex(x => new { x.TenantId, x.UserId }).IsUnique();
b.HasOne(x => x.User)
.WithOne()
.HasForeignKey<UserProfile>(x => new { x.TenantId, x.UserId })
.HasPrincipalKey<User>(u => new { u.TenantId, u.Id }) // <-- เปลี่ยนตรงนี้
.OnDelete(DeleteBehavior.Cascade);
}); });
model.Entity<Department>(b => model.Entity<Department>(b =>
{ {
b.ToTable("departments"); b.ToTable("departments");
b.HasKey(x => x.Id); 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.Code).IsRequired().HasMaxLength(64);
b.Property(x => x.Name).IsRequired().HasMaxLength(256); b.Property(x => x.Name).IsRequired().HasMaxLength(256);
@@ -237,7 +273,8 @@ public class AppDbContext : DbContext
b.HasOne(x => x.Parent) b.HasOne(x => x.Parent)
.WithMany(x => x.Children) .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); .OnDelete(DeleteBehavior.Restrict);
}); });
@@ -245,6 +282,7 @@ public class AppDbContext : DbContext
{ {
b.ToTable("positions"); b.ToTable("positions");
b.HasKey(x => x.Id); 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.Code).IsRequired().HasMaxLength(64);
b.Property(x => x.Title).IsRequired().HasMaxLength(256); b.Property(x => x.Title).IsRequired().HasMaxLength(256);
@@ -264,17 +302,20 @@ public class AppDbContext : DbContext
b.HasOne(x => x.UserProfile) b.HasOne(x => x.UserProfile)
.WithMany(p => p.Employments) .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); .OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.Department) b.HasOne(x => x.Department)
.WithMany() .WithMany()
.HasForeignKey(x => x.DepartmentId) .HasForeignKey(x => new { x.TenantId, x.DepartmentId })
.HasPrincipalKey(nameof(Department.TenantId), nameof(Department.Id))
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
b.HasOne(x => x.Position) b.HasOne(x => x.Position)
.WithMany() .WithMany()
.HasForeignKey(x => x.PositionId) .HasForeignKey(x => new { x.TenantId, x.PositionId })
.HasPrincipalKey(nameof(Position.TenantId), nameof(Position.Id))
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
}); });
@@ -290,7 +331,8 @@ public class AppDbContext : DbContext
b.HasOne(x => x.UserProfile) b.HasOne(x => x.UserProfile)
.WithMany(p => p.Addresses) .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); .OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary }); b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary });
@@ -306,7 +348,8 @@ public class AppDbContext : DbContext
b.HasOne(x => x.UserProfile) b.HasOne(x => x.UserProfile)
.WithMany(p => p.EmergencyContacts) .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); .OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary }); b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary });
@@ -323,7 +366,8 @@ public class AppDbContext : DbContext
b.HasOne(x => x.UserProfile) b.HasOne(x => x.UserProfile)
.WithMany(p => p.BankAccounts) .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); .OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary }); b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary });
@@ -347,10 +391,18 @@ public class AppDbContext : DbContext
.HasColumnName("tenant_id") .HasColumnName("tenant_id")
.HasColumnType("uuid") .HasColumnType("uuid")
.IsRequired() .IsRequired()
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid"); .ValueGeneratedNever();
b.HasIndex(nameof(BaseEntity.TenantId)); 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 // Audit
b.Property<DateTimeOffset>("CreatedAt") b.Property<DateTimeOffset>("CreatedAt")

View File

@@ -29,6 +29,7 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddInfrastructure(this IServiceCollection services) public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{ {
services.AddHttpContextAccessor(); services.AddHttpContextAccessor();
services.AddScoped<IJwtFactory, JwtFactory>();
// Options // Options
services.AddOptions<AuthOptions>() services.AddOptions<AuthOptions>()
@@ -90,6 +91,8 @@ public static class ServiceCollectionExtensions
services.AddScoped<IDisableMfaUseCase, DisableMfaUseCase>(); services.AddScoped<IDisableMfaUseCase, DisableMfaUseCase>();
services.AddScoped<ILogoutUseCase, LogoutUseCase>(); services.AddScoped<ILogoutUseCase, LogoutUseCase>();
services.AddScoped<ILogoutAllUseCase, LogoutAllUseCase>(); services.AddScoped<ILogoutAllUseCase, LogoutAllUseCase>();
services.AddScoped<IIssueTokenPairUseCase, IssueTokenPairUseCase>();
services.AddScoped<IRefreshUseCase, RefreshUseCase>();
// UseCases — HR // UseCases — HR
services.AddScoped<IUpsertUserProfileUseCase, UpsertUserProfileUseCase>(); services.AddScoped<IUpsertUserProfileUseCase, UpsertUserProfileUseCase>();

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AMREZ.EOP.Infrastructures.Migrations
{
/// <inheritdoc />
public partial class Add_Tenant_Guid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "TenantId",
schema: "meta",
table: "tenants",
type: "uuid",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TenantId",
schema: "meta",
table: "tenants");
}
}
}

View File

@@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace AMREZ.EOP.Infrastructures.Migrations namespace AMREZ.EOP.Infrastructures.Migrations
{ {
[DbContext(typeof(AppDbContext))] [DbContext(typeof(AppDbContext))]
[Migration("20250930101327_Add_Tenant_Guid")] [Migration("20251002040013_InitDatabase")]
partial class Add_Tenant_Guid partial class InitDatabase
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -58,10 +58,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("character varying(256)"); .HasColumnType("character varying(256)");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -81,6 +79,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("permissions", null, t => b.ToTable("permissions", null, t =>
{ {
t.HasCheckConstraint("ck_permissions_tenant_not_null", "tenant_id is not null"); 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)"); .HasColumnType("character varying(256)");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -140,6 +138,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("roles", null, t => b.ToTable("roles", null, t =>
{ {
t.HasCheckConstraint("ck_roles_tenant_not_null", "tenant_id is not null"); 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"); .HasColumnType("uuid");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -193,12 +191,16 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("TenantId", "PermissionId");
b.HasIndex("TenantId", "RoleId", "PermissionId") b.HasIndex("TenantId", "RoleId", "PermissionId")
.IsUnique(); .IsUnique();
b.ToTable("role_permissions", null, t => 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_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"); .HasColumnType("text");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -270,6 +270,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("users", null, t => b.ToTable("users", null, t =>
{ {
t.HasCheckConstraint("ck_users_tenant_not_null", "tenant_id is not null"); 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"); .HasColumnType("text");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -329,7 +329,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserId"); b.HasIndex("TenantId", "UserId");
b.HasIndex("TenantId", "Provider", "Subject") b.HasIndex("TenantId", "Provider", "Subject")
.IsUnique(); .IsUnique();
@@ -337,6 +337,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("user_external_accounts", null, t => 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_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); .HasDefaultValue(false);
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -399,8 +399,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "Type", "Identifier") b.HasIndex("TenantId", "Type", "Identifier")
.IsUnique(); .IsUnique();
@@ -410,6 +408,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("user_identities", null, t => 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_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"); .HasColumnType("text");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -488,13 +486,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "UserId"); b.HasIndex("TenantId", "UserId");
b.ToTable("user_mfa_factors", null, t => 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_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"); .HasColumnType("text");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -548,13 +544,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "UserId", "ChangedAt"); b.HasIndex("TenantId", "UserId", "ChangedAt");
b.ToTable("user_password_histories", null, t => 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_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"); .HasColumnType("uuid");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -608,12 +602,16 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("UserId"); b.HasIndex("UserId");
b.HasIndex("TenantId", "RoleId");
b.HasIndex("TenantId", "UserId", "RoleId") b.HasIndex("TenantId", "UserId", "RoleId")
.IsUnique(); .IsUnique();
b.ToTable("user_roles", null, t => 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_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"); .HasColumnType("timestamp with time zone");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -682,8 +678,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "DeviceId"); b.HasIndex("TenantId", "DeviceId");
b.HasIndex("TenantId", "UserId"); b.HasIndex("TenantId", "UserId");
@@ -691,6 +685,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("user_sessions", null, t => 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_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"); .HasColumnType("uuid");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -745,16 +739,18 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ParentDepartmentId");
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("TenantId", "Code") b.HasIndex("TenantId", "Code")
.IsUnique(); .IsUnique();
b.HasIndex("TenantId", "ParentDepartmentId");
b.ToTable("departments", null, t => b.ToTable("departments", null, t =>
{ {
t.HasCheckConstraint("ck_departments_tenant_not_null", "tenant_id is not null"); 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)"); .HasColumnType("character varying(64)");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -820,13 +814,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); b.HasIndex("TenantId", "UserProfileId", "IsPrimary");
b.ToTable("emergency_contacts", null, t => 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_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"); .HasColumnType("text");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -905,13 +897,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); b.HasIndex("TenantId", "UserProfileId", "IsPrimary");
b.ToTable("employee_addresses", null, t => 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_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"); .HasColumnType("text");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -982,13 +972,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); b.HasIndex("TenantId", "UserProfileId", "IsPrimary");
b.ToTable("employee_bank_accounts", null, t => 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_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"); .HasColumnType("timestamp with time zone");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -1057,19 +1045,19 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("DepartmentId");
b.HasIndex("PositionId");
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserProfileId"); b.HasIndex("TenantId", "DepartmentId");
b.HasIndex("TenantId", "PositionId");
b.HasIndex("TenantId", "UserProfileId", "StartDate"); b.HasIndex("TenantId", "UserProfileId", "StartDate");
b.ToTable("employments", null, t => b.ToTable("employments", null, t =>
{ {
t.HasCheckConstraint("ck_employments_tenant_not_null", "tenant_id is not null"); 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"); .HasColumnType("integer");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<string>("Title") b.Property<string>("Title")
.IsRequired() .IsRequired()
@@ -1132,6 +1118,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("positions", null, t => b.ToTable("positions", null, t =>
{ {
t.HasCheckConstraint("ck_positions_tenant_not_null", "tenant_id is not null"); 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"); .HasColumnType("uuid");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -1210,15 +1196,14 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserId")
.IsUnique();
b.HasIndex("TenantId", "UserId") b.HasIndex("TenantId", "UserId")
.IsUnique(); .IsUnique();
b.ToTable("user_profiles", null, t => 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_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.HasKey("TenantKey");
b.HasAlternateKey("TenantId");
b.HasIndex("IsActive"); b.HasIndex("IsActive");
b.HasIndex("TenantId")
.IsUnique();
b.ToTable("tenants", "meta"); b.ToTable("tenants", "meta");
}); });
@@ -1310,6 +1300,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .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("Permission");
b.Navigation("Role"); b.Navigation("Role");
@@ -1319,7 +1323,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("ExternalAccounts") .WithMany("ExternalAccounts")
.HasForeignKey("UserId") .HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1330,7 +1335,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("Identities") .WithMany("Identities")
.HasForeignKey("UserId") .HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1341,7 +1347,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("MfaFactors") .WithMany("MfaFactors")
.HasForeignKey("UserId") .HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1352,7 +1359,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("PasswordHistories") .WithMany("PasswordHistories")
.HasForeignKey("UserId") .HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1373,6 +1381,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .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("Role");
b.Navigation("User"); b.Navigation("User");
@@ -1382,7 +1404,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("Sessions") .WithMany("Sessions")
.HasForeignKey("UserId") .HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1393,7 +1416,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Parent") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Parent")
.WithMany("Children") .WithMany("Children")
.HasForeignKey("ParentDepartmentId") .HasForeignKey("TenantId", "ParentDepartmentId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
b.Navigation("Parent"); b.Navigation("Parent");
@@ -1403,7 +1427,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("EmergencyContacts") .WithMany("EmergencyContacts")
.HasForeignKey("UserProfileId") .HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1414,7 +1439,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("Addresses") .WithMany("Addresses")
.HasForeignKey("UserProfileId") .HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1425,7 +1451,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("BankAccounts") .WithMany("BankAccounts")
.HasForeignKey("UserProfileId") .HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1436,17 +1463,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Department") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Department")
.WithMany() .WithMany()
.HasForeignKey("DepartmentId") .HasForeignKey("TenantId", "DepartmentId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Position", "Position") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Position", "Position")
.WithMany() .WithMany()
.HasForeignKey("PositionId") .HasForeignKey("TenantId", "PositionId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("Employments") .WithMany("Employments")
.HasForeignKey("UserProfileId") .HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1469,7 +1499,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithOne() .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) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();

View File

@@ -19,10 +19,10 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
Code = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), Code = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
ParentDepartmentId = table.Column<Guid>(type: "uuid", nullable: true), ParentDepartmentId = table.Column<Guid>(type: "uuid", nullable: true),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
@@ -32,12 +32,14 @@ namespace AMREZ.EOP.Infrastructures.Migrations
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_departments", x => x.Id); 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_null", "tenant_id is not null");
table.CheckConstraint("ck_departments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey( table.ForeignKey(
name: "FK_departments_departments_ParentDepartmentId", name: "FK_departments_departments_tenant_id_ParentDepartmentId",
column: x => x.ParentDepartmentId, columns: x => new { x.tenant_id, x.ParentDepartmentId },
principalTable: "departments", principalTable: "departments",
principalColumn: "Id", principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Restrict); onDelete: ReferentialAction.Restrict);
}); });
@@ -46,9 +48,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
Code = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Code = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
@@ -58,7 +60,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_permissions", x => x.Id); 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_null", "tenant_id is not null");
table.CheckConstraint("ck_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@@ -66,10 +70,10 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
Code = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), Code = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Level = table.Column<int>(type: "integer", nullable: true), Level = table.Column<int>(type: "integer", nullable: true),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
@@ -79,7 +83,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_positions", x => x.Id); 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_null", "tenant_id is not null");
table.CheckConstraint("ck_positions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@@ -87,9 +93,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
Code = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false), Code = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
@@ -99,7 +105,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_roles", x => x.Id); 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_null", "tenant_id is not null");
table.CheckConstraint("ck_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@@ -108,6 +116,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
TenantKey = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false), TenantKey = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
TenantId = table.Column<Guid>(type: "uuid", nullable: false),
Schema = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true), Schema = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
ConnectionString = table.Column<string>(type: "text", nullable: true), ConnectionString = table.Column<string>(type: "text", nullable: true),
Mode = table.Column<int>(type: "integer", nullable: false), Mode = table.Column<int>(type: "integer", nullable: false),
@@ -117,6 +126,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_tenants", x => x.TenantKey); table.PrimaryKey("PK_tenants", x => x.TenantKey);
table.UniqueConstraint("AK_tenants_TenantId", x => x.TenantId);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@@ -124,13 +134,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
PasswordHash = table.Column<string>(type: "text", nullable: false), PasswordHash = table.Column<string>(type: "text", nullable: false),
IsActive = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true), IsActive = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
AccessFailedCount = table.Column<int>(type: "integer", nullable: false, defaultValue: 0), AccessFailedCount = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
LockoutEndUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), LockoutEndUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
MfaEnabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false), MfaEnabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
SecurityStamp = table.Column<string>(type: "text", nullable: true), SecurityStamp = table.Column<string>(type: "text", nullable: true),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
@@ -140,7 +150,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_users", x => x.Id); 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_null", "tenant_id is not null");
table.CheckConstraint("ck_users_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@@ -148,9 +160,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
RoleId = table.Column<Guid>(type: "uuid", nullable: false), RoleId = table.Column<Guid>(type: "uuid", nullable: false),
PermissionId = table.Column<Guid>(type: "uuid", nullable: false), PermissionId = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(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.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_null", "tenant_id is not null");
table.CheckConstraint("ck_role_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey( table.ForeignKey(
name: "FK_role_permissions_permissions_PermissionId", name: "FK_role_permissions_permissions_PermissionId",
column: x => x.PermissionId, column: x => x.PermissionId,
principalTable: "permissions", principalTable: "permissions",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); 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( table.ForeignKey(
name: "FK_role_permissions_roles_RoleId", name: "FK_role_permissions_roles_RoleId",
column: x => x.RoleId, column: x => x.RoleId,
principalTable: "roles", principalTable: "roles",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); 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( migrationBuilder.CreateTable(
@@ -203,12 +228,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
UserId = table.Column<Guid>(type: "uuid", nullable: false), UserId = table.Column<Guid>(type: "uuid", nullable: false),
Provider = table.Column<int>(type: "integer", nullable: false), Provider = table.Column<int>(type: "integer", nullable: false),
Subject = table.Column<string>(type: "text", nullable: false), Subject = table.Column<string>(type: "text", nullable: false),
Email = table.Column<string>(type: "text", nullable: true), Email = table.Column<string>(type: "text", nullable: true),
LinkedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), LinkedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(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.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_null", "tenant_id is not null");
table.CheckConstraint("ck_user_external_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey( table.ForeignKey(
name: "FK_user_external_accounts_users_UserId", name: "FK_user_external_accounts_users_tenant_id_UserId",
column: x => x.UserId, columns: x => new { x.tenant_id, x.UserId },
principalTable: "users", principalTable: "users",
principalColumn: "Id", principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@@ -232,12 +258,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
UserId = table.Column<Guid>(type: "uuid", nullable: false), UserId = table.Column<Guid>(type: "uuid", nullable: false),
Type = table.Column<int>(type: "integer", nullable: false), Type = table.Column<int>(type: "integer", nullable: false),
Identifier = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Identifier = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
IsPrimary = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false), IsPrimary = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
VerifiedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), VerifiedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(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.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_null", "tenant_id is not null");
table.CheckConstraint("ck_user_identities_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey( table.ForeignKey(
name: "FK_user_identities_users_UserId", name: "FK_user_identities_users_tenant_id_UserId",
column: x => x.UserId, columns: x => new { x.tenant_id, x.UserId },
principalTable: "users", principalTable: "users",
principalColumn: "Id", principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@@ -261,7 +288,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
UserId = table.Column<Guid>(type: "uuid", nullable: false), UserId = table.Column<Guid>(type: "uuid", nullable: false),
Type = table.Column<int>(type: "integer", nullable: false), Type = table.Column<int>(type: "integer", nullable: false),
Label = table.Column<string>(type: "text", nullable: true), Label = table.Column<string>(type: "text", nullable: true),
@@ -273,6 +299,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true), Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
AddedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), AddedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
LastUsedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), LastUsedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(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.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_null", "tenant_id is not null");
table.CheckConstraint("ck_user_mfa_factors_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey( table.ForeignKey(
name: "FK_user_mfa_factors_users_UserId", name: "FK_user_mfa_factors_users_tenant_id_UserId",
column: x => x.UserId, columns: x => new { x.tenant_id, x.UserId },
principalTable: "users", principalTable: "users",
principalColumn: "Id", principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@@ -296,10 +324,10 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
UserId = table.Column<Guid>(type: "uuid", nullable: false), UserId = table.Column<Guid>(type: "uuid", nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: false), PasswordHash = table.Column<string>(type: "text", nullable: false),
ChangedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), ChangedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(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.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_null", "tenant_id is not null");
table.CheckConstraint("ck_user_password_histories_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey( table.ForeignKey(
name: "FK_user_password_histories_users_UserId", name: "FK_user_password_histories_users_tenant_id_UserId",
column: x => x.UserId, columns: x => new { x.tenant_id, x.UserId },
principalTable: "users", principalTable: "users",
principalColumn: "Id", principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@@ -323,7 +352,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
UserId = table.Column<Guid>(type: "uuid", nullable: false), UserId = table.Column<Guid>(type: "uuid", nullable: false),
FirstName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false), FirstName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
LastName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false), LastName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
@@ -333,6 +361,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
Gender = table.Column<int>(type: "integer", nullable: true), Gender = table.Column<int>(type: "integer", nullable: true),
DepartmentId = table.Column<Guid>(type: "uuid", nullable: true), DepartmentId = table.Column<Guid>(type: "uuid", nullable: true),
PositionId = table.Column<Guid>(type: "uuid", nullable: true), PositionId = table.Column<Guid>(type: "uuid", nullable: true),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
@@ -342,7 +371,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_user_profiles", x => x.Id); 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_null", "tenant_id is not null");
table.CheckConstraint("ck_user_profiles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey( table.ForeignKey(
name: "FK_user_profiles_departments_DepartmentId", name: "FK_user_profiles_departments_DepartmentId",
column: x => x.DepartmentId, column: x => x.DepartmentId,
@@ -354,10 +385,10 @@ namespace AMREZ.EOP.Infrastructures.Migrations
principalTable: "positions", principalTable: "positions",
principalColumn: "Id"); principalColumn: "Id");
table.ForeignKey( table.ForeignKey(
name: "FK_user_profiles_users_UserId", name: "FK_user_profiles_users_tenant_id_UserId",
column: x => x.UserId, columns: x => new { x.tenant_id, x.UserId },
principalTable: "users", principalTable: "users",
principalColumn: "Id", principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@@ -366,9 +397,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
UserId = table.Column<Guid>(type: "uuid", nullable: false), UserId = table.Column<Guid>(type: "uuid", nullable: false),
RoleId = table.Column<Guid>(type: "uuid", nullable: false), RoleId = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(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.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_null", "tenant_id is not null");
table.CheckConstraint("ck_user_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey( table.ForeignKey(
name: "FK_user_roles_roles_RoleId", name: "FK_user_roles_roles_RoleId",
column: x => x.RoleId, column: x => x.RoleId,
principalTable: "roles", principalTable: "roles",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); 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( table.ForeignKey(
name: "FK_user_roles_users_UserId", name: "FK_user_roles_users_UserId",
column: x => x.UserId, column: x => x.UserId,
principalTable: "users", principalTable: "users",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); 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( migrationBuilder.CreateTable(
@@ -398,7 +442,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
UserId = table.Column<Guid>(type: "uuid", nullable: false), UserId = table.Column<Guid>(type: "uuid", nullable: false),
RefreshTokenHash = table.Column<string>(type: "text", nullable: false), RefreshTokenHash = table.Column<string>(type: "text", nullable: false),
IssuedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), IssuedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
@@ -407,6 +450,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
DeviceId = table.Column<string>(type: "text", nullable: true), DeviceId = table.Column<string>(type: "text", nullable: true),
UserAgent = table.Column<string>(type: "text", nullable: true), UserAgent = table.Column<string>(type: "text", nullable: true),
IpAddress = table.Column<string>(type: "text", nullable: true), IpAddress = table.Column<string>(type: "text", nullable: true),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(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.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_null", "tenant_id is not null");
table.CheckConstraint("ck_user_sessions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey( table.ForeignKey(
name: "FK_user_sessions_users_UserId", name: "FK_user_sessions_users_tenant_id_UserId",
column: x => x.UserId, columns: x => new { x.tenant_id, x.UserId },
principalTable: "users", principalTable: "users",
principalColumn: "Id", principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@@ -430,13 +475,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
UserProfileId = table.Column<Guid>(type: "uuid", nullable: false), UserProfileId = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false), Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Relationship = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), Relationship = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Phone = table.Column<string>(type: "text", nullable: true), Phone = table.Column<string>(type: "text", nullable: true),
Email = table.Column<string>(type: "text", nullable: true), Email = table.Column<string>(type: "text", nullable: true),
IsPrimary = table.Column<bool>(type: "boolean", nullable: false), IsPrimary = table.Column<bool>(type: "boolean", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(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.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_null", "tenant_id is not null");
table.CheckConstraint("ck_emergency_contacts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey( table.ForeignKey(
name: "FK_emergency_contacts_user_profiles_UserProfileId", name: "FK_emergency_contacts_user_profiles_tenant_id_UserProfileId",
column: x => x.UserProfileId, columns: x => new { x.tenant_id, x.UserProfileId },
principalTable: "user_profiles", principalTable: "user_profiles",
principalColumn: "Id", principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@@ -460,7 +506,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
UserProfileId = table.Column<Guid>(type: "uuid", nullable: false), UserProfileId = table.Column<Guid>(type: "uuid", nullable: false),
Type = table.Column<int>(type: "integer", nullable: false), Type = table.Column<int>(type: "integer", nullable: false),
Line1 = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), Line1 = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
@@ -470,6 +515,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
PostalCode = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false), PostalCode = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
Country = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), Country = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
IsPrimary = table.Column<bool>(type: "boolean", nullable: false), IsPrimary = table.Column<bool>(type: "boolean", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(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.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_null", "tenant_id is not null");
table.CheckConstraint("ck_employee_addresses_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey( table.ForeignKey(
name: "FK_employee_addresses_user_profiles_UserProfileId", name: "FK_employee_addresses_user_profiles_tenant_id_UserProfileId",
column: x => x.UserProfileId, columns: x => new { x.tenant_id, x.UserProfileId },
principalTable: "user_profiles", principalTable: "user_profiles",
principalColumn: "Id", principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@@ -493,7 +540,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
UserProfileId = table.Column<Guid>(type: "uuid", nullable: false), UserProfileId = table.Column<Guid>(type: "uuid", nullable: false),
BankName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false), BankName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
AccountNumber = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false), AccountNumber = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
@@ -501,6 +547,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
Branch = table.Column<string>(type: "text", nullable: true), Branch = table.Column<string>(type: "text", nullable: true),
Note = table.Column<string>(type: "text", nullable: true), Note = table.Column<string>(type: "text", nullable: true),
IsPrimary = table.Column<bool>(type: "boolean", nullable: false), IsPrimary = table.Column<bool>(type: "boolean", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(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.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_null", "tenant_id is not null");
table.CheckConstraint("ck_employee_bank_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey( table.ForeignKey(
name: "FK_employee_bank_accounts_user_profiles_UserProfileId", name: "FK_employee_bank_accounts_user_profiles_tenant_id_UserProfileId",
column: x => x.UserProfileId, columns: x => new { x.tenant_id, x.UserProfileId },
principalTable: "user_profiles", principalTable: "user_profiles",
principalColumn: "Id", principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
@@ -524,7 +572,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false), Id = table.Column<Guid>(type: "uuid", nullable: false),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "nullif(current_setting('app.tenant_id', true),'')::uuid"),
UserProfileId = table.Column<Guid>(type: "uuid", nullable: false), UserProfileId = table.Column<Guid>(type: "uuid", nullable: false),
EmploymentType = table.Column<int>(type: "integer", nullable: false), EmploymentType = table.Column<int>(type: "integer", nullable: false),
StartDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), StartDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
@@ -534,6 +581,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
ManagerUserId = table.Column<Guid>(type: "uuid", nullable: true), ManagerUserId = table.Column<Guid>(type: "uuid", nullable: true),
WorkEmail = table.Column<string>(type: "text", nullable: true), WorkEmail = table.Column<string>(type: "text", nullable: true),
WorkPhone = table.Column<string>(type: "text", nullable: true), WorkPhone = table.Column<string>(type: "text", nullable: true),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"), created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now() at time zone 'utc'"),
created_by = table.Column<string>(type: "text", nullable: true), created_by = table.Column<string>(type: "text", nullable: true),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
@@ -544,31 +592,27 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
table.PrimaryKey("PK_employments", x => x.Id); 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_null", "tenant_id is not null");
table.CheckConstraint("ck_employments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey( table.ForeignKey(
name: "FK_employments_departments_DepartmentId", name: "FK_employments_departments_tenant_id_DepartmentId",
column: x => x.DepartmentId, columns: x => new { x.tenant_id, x.DepartmentId },
principalTable: "departments", principalTable: "departments",
principalColumn: "Id", principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Restrict); onDelete: ReferentialAction.Restrict);
table.ForeignKey( table.ForeignKey(
name: "FK_employments_positions_PositionId", name: "FK_employments_positions_tenant_id_PositionId",
column: x => x.PositionId, columns: x => new { x.tenant_id, x.PositionId },
principalTable: "positions", principalTable: "positions",
principalColumn: "Id", principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Restrict); onDelete: ReferentialAction.Restrict);
table.ForeignKey( table.ForeignKey(
name: "FK_employments_user_profiles_UserProfileId", name: "FK_employments_user_profiles_tenant_id_UserProfileId",
column: x => x.UserProfileId, columns: x => new { x.tenant_id, x.UserProfileId },
principalTable: "user_profiles", principalTable: "user_profiles",
principalColumn: "Id", principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateIndex(
name: "IX_departments_ParentDepartmentId",
table: "departments",
column: "ParentDepartmentId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_departments_tenant_id", name: "IX_departments_tenant_id",
table: "departments", table: "departments",
@@ -580,6 +624,11 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: new[] { "tenant_id", "Code" }, columns: new[] { "tenant_id", "Code" },
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_departments_tenant_id_ParentDepartmentId",
table: "departments",
columns: new[] { "tenant_id", "ParentDepartmentId" });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_emergency_contacts_tenant_id", name: "IX_emergency_contacts_tenant_id",
table: "emergency_contacts", table: "emergency_contacts",
@@ -590,11 +639,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "emergency_contacts", table: "emergency_contacts",
columns: new[] { "tenant_id", "UserProfileId", "IsPrimary" }); columns: new[] { "tenant_id", "UserProfileId", "IsPrimary" });
migrationBuilder.CreateIndex(
name: "IX_emergency_contacts_UserProfileId",
table: "emergency_contacts",
column: "UserProfileId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_employee_addresses_tenant_id", name: "IX_employee_addresses_tenant_id",
table: "employee_addresses", table: "employee_addresses",
@@ -605,11 +649,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "employee_addresses", table: "employee_addresses",
columns: new[] { "tenant_id", "UserProfileId", "IsPrimary" }); columns: new[] { "tenant_id", "UserProfileId", "IsPrimary" });
migrationBuilder.CreateIndex(
name: "IX_employee_addresses_UserProfileId",
table: "employee_addresses",
column: "UserProfileId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_employee_bank_accounts_tenant_id", name: "IX_employee_bank_accounts_tenant_id",
table: "employee_bank_accounts", table: "employee_bank_accounts",
@@ -620,36 +659,26 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "employee_bank_accounts", table: "employee_bank_accounts",
columns: new[] { "tenant_id", "UserProfileId", "IsPrimary" }); 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( migrationBuilder.CreateIndex(
name: "IX_employments_tenant_id", name: "IX_employments_tenant_id",
table: "employments", table: "employments",
column: "tenant_id"); 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( migrationBuilder.CreateIndex(
name: "IX_employments_tenant_id_UserProfileId_StartDate", name: "IX_employments_tenant_id_UserProfileId_StartDate",
table: "employments", table: "employments",
columns: new[] { "tenant_id", "UserProfileId", "StartDate" }); columns: new[] { "tenant_id", "UserProfileId", "StartDate" });
migrationBuilder.CreateIndex(
name: "IX_employments_UserProfileId",
table: "employments",
column: "UserProfileId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_permissions_tenant_id", name: "IX_permissions_tenant_id",
table: "permissions", table: "permissions",
@@ -687,6 +716,11 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "role_permissions", table: "role_permissions",
column: "tenant_id"); column: "tenant_id");
migrationBuilder.CreateIndex(
name: "IX_role_permissions_tenant_id_PermissionId",
table: "role_permissions",
columns: new[] { "tenant_id", "PermissionId" });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_role_permissions_tenant_id_RoleId_PermissionId", name: "IX_role_permissions_tenant_id_RoleId_PermissionId",
table: "role_permissions", table: "role_permissions",
@@ -728,6 +762,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "tenants", table: "tenants",
column: "IsActive"); column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_tenants_TenantId",
schema: "meta",
table: "tenants",
column: "TenantId",
unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_user_external_accounts_tenant_id", name: "IX_user_external_accounts_tenant_id",
table: "user_external_accounts", table: "user_external_accounts",
@@ -740,9 +781,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
unique: true); unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_user_external_accounts_UserId", name: "IX_user_external_accounts_tenant_id_UserId",
table: "user_external_accounts", table: "user_external_accounts",
column: "UserId"); columns: new[] { "tenant_id", "UserId" });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_user_identities_tenant_id", name: "IX_user_identities_tenant_id",
@@ -755,11 +796,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: new[] { "tenant_id", "Type", "Identifier" }, columns: new[] { "tenant_id", "Type", "Identifier" },
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_user_identities_UserId",
table: "user_identities",
column: "UserId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_user_identity_primary_per_type", name: "ix_user_identity_primary_per_type",
table: "user_identities", table: "user_identities",
@@ -775,11 +811,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "user_mfa_factors", table: "user_mfa_factors",
columns: new[] { "tenant_id", "UserId" }); columns: new[] { "tenant_id", "UserId" });
migrationBuilder.CreateIndex(
name: "IX_user_mfa_factors_UserId",
table: "user_mfa_factors",
column: "UserId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_user_password_histories_tenant_id", name: "IX_user_password_histories_tenant_id",
table: "user_password_histories", table: "user_password_histories",
@@ -790,11 +821,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "user_password_histories", table: "user_password_histories",
columns: new[] { "tenant_id", "UserId", "ChangedAt" }); columns: new[] { "tenant_id", "UserId", "ChangedAt" });
migrationBuilder.CreateIndex(
name: "IX_user_password_histories_UserId",
table: "user_password_histories",
column: "UserId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_user_profiles_DepartmentId", name: "IX_user_profiles_DepartmentId",
table: "user_profiles", table: "user_profiles",
@@ -816,12 +842,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: new[] { "tenant_id", "UserId" }, columns: new[] { "tenant_id", "UserId" },
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_user_profiles_UserId",
table: "user_profiles",
column: "UserId",
unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_user_roles_RoleId", name: "IX_user_roles_RoleId",
table: "user_roles", table: "user_roles",
@@ -832,6 +852,11 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "user_roles", table: "user_roles",
column: "tenant_id"); column: "tenant_id");
migrationBuilder.CreateIndex(
name: "IX_user_roles_tenant_id_RoleId",
table: "user_roles",
columns: new[] { "tenant_id", "RoleId" });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_user_roles_tenant_id_UserId_RoleId", name: "IX_user_roles_tenant_id_UserId_RoleId",
table: "user_roles", table: "user_roles",
@@ -858,11 +883,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "user_sessions", table: "user_sessions",
columns: new[] { "tenant_id", "UserId" }); columns: new[] { "tenant_id", "UserId" });
migrationBuilder.CreateIndex(
name: "IX_user_sessions_UserId",
table: "user_sessions",
column: "UserId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_users_tenant_id", name: "IX_users_tenant_id",
table: "users", table: "users",

View File

@@ -55,10 +55,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("character varying(256)"); .HasColumnType("character varying(256)");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -78,6 +76,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("permissions", null, t => b.ToTable("permissions", null, t =>
{ {
t.HasCheckConstraint("ck_permissions_tenant_not_null", "tenant_id is not null"); 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)"); .HasColumnType("character varying(256)");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -137,6 +135,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("roles", null, t => b.ToTable("roles", null, t =>
{ {
t.HasCheckConstraint("ck_roles_tenant_not_null", "tenant_id is not null"); 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"); .HasColumnType("uuid");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -190,12 +188,16 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("TenantId", "PermissionId");
b.HasIndex("TenantId", "RoleId", "PermissionId") b.HasIndex("TenantId", "RoleId", "PermissionId")
.IsUnique(); .IsUnique();
b.ToTable("role_permissions", null, t => 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_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"); .HasColumnType("text");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -267,6 +267,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("users", null, t => b.ToTable("users", null, t =>
{ {
t.HasCheckConstraint("ck_users_tenant_not_null", "tenant_id is not null"); 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"); .HasColumnType("text");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -326,7 +326,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserId"); b.HasIndex("TenantId", "UserId");
b.HasIndex("TenantId", "Provider", "Subject") b.HasIndex("TenantId", "Provider", "Subject")
.IsUnique(); .IsUnique();
@@ -334,6 +334,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("user_external_accounts", null, t => 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_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); .HasDefaultValue(false);
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -396,8 +396,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "Type", "Identifier") b.HasIndex("TenantId", "Type", "Identifier")
.IsUnique(); .IsUnique();
@@ -407,6 +405,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("user_identities", null, t => 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_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"); .HasColumnType("text");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -485,13 +483,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "UserId"); b.HasIndex("TenantId", "UserId");
b.ToTable("user_mfa_factors", null, t => 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_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"); .HasColumnType("text");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -545,13 +541,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "UserId", "ChangedAt"); b.HasIndex("TenantId", "UserId", "ChangedAt");
b.ToTable("user_password_histories", null, t => 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_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"); .HasColumnType("uuid");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -605,12 +599,16 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("UserId"); b.HasIndex("UserId");
b.HasIndex("TenantId", "RoleId");
b.HasIndex("TenantId", "UserId", "RoleId") b.HasIndex("TenantId", "UserId", "RoleId")
.IsUnique(); .IsUnique();
b.ToTable("user_roles", null, t => 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_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"); .HasColumnType("timestamp with time zone");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -679,8 +675,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "DeviceId"); b.HasIndex("TenantId", "DeviceId");
b.HasIndex("TenantId", "UserId"); b.HasIndex("TenantId", "UserId");
@@ -688,6 +682,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("user_sessions", null, t => 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_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"); .HasColumnType("uuid");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -742,16 +736,18 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ParentDepartmentId");
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("TenantId", "Code") b.HasIndex("TenantId", "Code")
.IsUnique(); .IsUnique();
b.HasIndex("TenantId", "ParentDepartmentId");
b.ToTable("departments", null, t => b.ToTable("departments", null, t =>
{ {
t.HasCheckConstraint("ck_departments_tenant_not_null", "tenant_id is not null"); 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)"); .HasColumnType("character varying(64)");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -817,13 +811,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); b.HasIndex("TenantId", "UserProfileId", "IsPrimary");
b.ToTable("emergency_contacts", null, t => 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_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"); .HasColumnType("text");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -902,13 +894,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); b.HasIndex("TenantId", "UserProfileId", "IsPrimary");
b.ToTable("employee_addresses", null, t => 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_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"); .HasColumnType("text");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -979,13 +969,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "UserProfileId", "IsPrimary"); b.HasIndex("TenantId", "UserProfileId", "IsPrimary");
b.ToTable("employee_bank_accounts", null, t => 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_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"); .HasColumnType("timestamp with time zone");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -1054,19 +1042,19 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("DepartmentId");
b.HasIndex("PositionId");
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserProfileId"); b.HasIndex("TenantId", "DepartmentId");
b.HasIndex("TenantId", "PositionId");
b.HasIndex("TenantId", "UserProfileId", "StartDate"); b.HasIndex("TenantId", "UserProfileId", "StartDate");
b.ToTable("employments", null, t => b.ToTable("employments", null, t =>
{ {
t.HasCheckConstraint("ck_employments_tenant_not_null", "tenant_id is not null"); 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"); .HasColumnType("integer");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<string>("Title") b.Property<string>("Title")
.IsRequired() .IsRequired()
@@ -1129,6 +1115,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("positions", null, t => b.ToTable("positions", null, t =>
{ {
t.HasCheckConstraint("ck_positions_tenant_not_null", "tenant_id is not null"); 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"); .HasColumnType("uuid");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("tenant_id") .HasColumnName("tenant_id");
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
b.Property<DateTimeOffset?>("UpdatedAt") b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -1207,15 +1193,14 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UserId")
.IsUnique();
b.HasIndex("TenantId", "UserId") b.HasIndex("TenantId", "UserId")
.IsUnique(); .IsUnique();
b.ToTable("user_profiles", null, t => 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_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.HasKey("TenantKey");
b.HasAlternateKey("TenantId");
b.HasIndex("IsActive"); b.HasIndex("IsActive");
b.HasIndex("TenantId")
.IsUnique();
b.ToTable("tenants", "meta"); b.ToTable("tenants", "meta");
}); });
@@ -1307,6 +1297,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .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("Permission");
b.Navigation("Role"); b.Navigation("Role");
@@ -1316,7 +1320,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("ExternalAccounts") .WithMany("ExternalAccounts")
.HasForeignKey("UserId") .HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1327,7 +1332,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("Identities") .WithMany("Identities")
.HasForeignKey("UserId") .HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1338,7 +1344,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("MfaFactors") .WithMany("MfaFactors")
.HasForeignKey("UserId") .HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1349,7 +1356,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("PasswordHistories") .WithMany("PasswordHistories")
.HasForeignKey("UserId") .HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1370,6 +1378,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .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("Role");
b.Navigation("User"); b.Navigation("User");
@@ -1379,7 +1401,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("Sessions") .WithMany("Sessions")
.HasForeignKey("UserId") .HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1390,7 +1413,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Parent") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Parent")
.WithMany("Children") .WithMany("Children")
.HasForeignKey("ParentDepartmentId") .HasForeignKey("TenantId", "ParentDepartmentId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
b.Navigation("Parent"); b.Navigation("Parent");
@@ -1400,7 +1424,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("EmergencyContacts") .WithMany("EmergencyContacts")
.HasForeignKey("UserProfileId") .HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1411,7 +1436,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("Addresses") .WithMany("Addresses")
.HasForeignKey("UserProfileId") .HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1422,7 +1448,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("BankAccounts") .WithMany("BankAccounts")
.HasForeignKey("UserProfileId") .HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1433,17 +1460,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{ {
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Department") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Department")
.WithMany() .WithMany()
.HasForeignKey("DepartmentId") .HasForeignKey("TenantId", "DepartmentId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Position", "Position") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Position", "Position")
.WithMany() .WithMany()
.HasForeignKey("PositionId") .HasForeignKey("TenantId", "PositionId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile") b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("Employments") .WithMany("Employments")
.HasForeignKey("UserProfileId") .HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
@@ -1466,7 +1496,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User") b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithOne() .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) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();

View File

@@ -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;
}

View File

@@ -1,8 +1,10 @@
using AMREZ.EOP.Abstractions.Applications.Tenancy;
using AMREZ.EOP.Abstractions.Infrastructures.Repositories; using AMREZ.EOP.Abstractions.Infrastructures.Repositories;
using AMREZ.EOP.Abstractions.Storage; using AMREZ.EOP.Abstractions.Storage;
using AMREZ.EOP.Domain.Entities.Tenancy; using AMREZ.EOP.Domain.Entities.Tenancy;
using AMREZ.EOP.Infrastructures.Data; using AMREZ.EOP.Infrastructures.Data;
using AMREZ.EOP.Infrastructures.Options; using AMREZ.EOP.Infrastructures.Options;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -11,9 +13,19 @@ namespace AMREZ.EOP.Infrastructures.Repositories;
public sealed class TenantRepository : ITenantRepository public sealed class TenantRepository : ITenantRepository
{ {
private readonly IDbScope _scope; private readonly IDbScope _scope;
public TenantRepository(IDbScope scope) => _scope = scope; private readonly ITenantResolver _resolver;
private readonly IHttpContextAccessor _http;
private AppDbContext Db() => _scope.Get<AppDbContext>(); 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<AppDbContext>();
}
private static string Norm(string s) => (s ?? string.Empty).Trim().ToLowerInvariant(); 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 (cur is null) return false;
if (ifUnmodifiedSince.HasValue && cur.UpdatedAtUtc > ifUnmodifiedSince.Value) 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.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()); 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<TenantConfig>().FirstOrDefaultAsync(x => x.TenantKey == key, ct); var t = await db.Set<TenantConfig>().FirstOrDefaultAsync(x => x.TenantKey == key, ct);
if (t is null) return false; if (t is null) return false;
// ❌ ไม่ลบนะ — ✅ deactivate
if (t.IsActive) if (t.IsActive)
{ {
t.IsActive = false; t.IsActive = false;

View File

@@ -2,6 +2,7 @@ using AMREZ.EOP.Abstractions.Applications.Tenancy;
using AMREZ.EOP.Abstractions.Infrastructures.Repositories; using AMREZ.EOP.Abstractions.Infrastructures.Repositories;
using AMREZ.EOP.Abstractions.Storage; using AMREZ.EOP.Abstractions.Storage;
using AMREZ.EOP.Domain.Entities.HumanResources; using AMREZ.EOP.Domain.Entities.HumanResources;
using AMREZ.EOP.Domain.Entities.Tenancy;
using AMREZ.EOP.Infrastructures.Data; using AMREZ.EOP.Infrastructures.Data;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -19,22 +20,33 @@ public class UserProfileRepository : IUserProfileRepository
private Guid TenantId() private Guid TenantId()
{ {
var http = _http.HttpContext; var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext");
var tc = http is not null ? _tenantResolver.Resolve(http) : null; var tc = _tenantResolver.Resolve(http) ?? throw new InvalidOperationException("No tenant");
return Guid.TryParse(tc?.Id, out var g) ? g : Guid.Empty; _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<AppDbContext>();
var cfg = db.Set<TenantConfig>().AsNoTracking().FirstOrDefault(x => x.TenantKey == key);
if (cfg is null) throw new InvalidOperationException($"Tenant '{key}' not found");
return cfg.TenantId;
} }
public async Task<UserProfile?> GetByUserIdAsync(Guid userId, CancellationToken ct = default) public async Task<UserProfile?> GetByUserIdAsync(Guid userId, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
return await db.UserProfiles.FirstOrDefaultAsync(p => p.TenantId == tid && p.UserId == userId, ct); return await db.UserProfiles.FirstOrDefaultAsync(p => p.TenantId == tid && p.UserId == userId, ct);
} }
public async Task UpsertAsync(UserProfile profile, CancellationToken ct = default) public async Task UpsertAsync(UserProfile profile, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var existing = await db.UserProfiles.FirstOrDefaultAsync(p => p.TenantId == tid && p.UserId == profile.UserId, ct); var existing = await db.UserProfiles.FirstOrDefaultAsync(p => p.TenantId == tid && p.UserId == profile.UserId, ct);
if (existing is null) if (existing is null)
@@ -56,8 +68,9 @@ public class UserProfileRepository : IUserProfileRepository
public async Task<Employment> AddEmploymentAsync(Employment e, CancellationToken ct = default) public async Task<Employment> AddEmploymentAsync(Employment e, CancellationToken ct = default)
{ {
var tid = TenantId();
var db = _scope.Get<AppDbContext>(); var db = _scope.Get<AppDbContext>();
e.TenantId = TenantId(); e.TenantId = tid;
await db.Employments.AddAsync(e, ct); await db.Employments.AddAsync(e, ct);
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
return e; return e;
@@ -65,8 +78,8 @@ public class UserProfileRepository : IUserProfileRepository
public async Task EndEmploymentAsync(Guid employmentId, DateTime endDate, CancellationToken ct = default) public async Task EndEmploymentAsync(Guid employmentId, DateTime endDate, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var e = await db.Employments.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == employmentId, ct); var e = await db.Employments.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == employmentId, ct);
if (e is null) return; if (e is null) return;
e.EndDate = endDate; e.EndDate = endDate;
@@ -75,8 +88,9 @@ public class UserProfileRepository : IUserProfileRepository
public async Task<EmployeeAddress> AddAddressAsync(EmployeeAddress a, CancellationToken ct = default) public async Task<EmployeeAddress> AddAddressAsync(EmployeeAddress a, CancellationToken ct = default)
{ {
var tid = TenantId();
var db = _scope.Get<AppDbContext>(); var db = _scope.Get<AppDbContext>();
a.TenantId = TenantId(); a.TenantId = tid;
await db.EmployeeAddresses.AddAsync(a, ct); await db.EmployeeAddresses.AddAsync(a, ct);
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
return a; return a;
@@ -84,8 +98,8 @@ public class UserProfileRepository : IUserProfileRepository
public async Task SetPrimaryAddressAsync(Guid userProfileId, Guid addressId, CancellationToken ct = default) public async Task SetPrimaryAddressAsync(Guid userProfileId, Guid addressId, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var all = await db.EmployeeAddresses.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct); var all = await db.EmployeeAddresses.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct);
foreach (var x in all) x.IsPrimary = false; foreach (var x in all) x.IsPrimary = false;
@@ -98,8 +112,9 @@ public class UserProfileRepository : IUserProfileRepository
public async Task<EmergencyContact> AddEmergencyContactAsync(EmergencyContact c, CancellationToken ct = default) public async Task<EmergencyContact> AddEmergencyContactAsync(EmergencyContact c, CancellationToken ct = default)
{ {
var tid = TenantId();
var db = _scope.Get<AppDbContext>(); var db = _scope.Get<AppDbContext>();
c.TenantId = TenantId(); c.TenantId = tid;
await db.EmergencyContacts.AddAsync(c, ct); await db.EmergencyContacts.AddAsync(c, ct);
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
return c; return c;
@@ -107,8 +122,8 @@ public class UserProfileRepository : IUserProfileRepository
public async Task SetPrimaryEmergencyContactAsync(Guid userProfileId, Guid contactId, CancellationToken ct = default) public async Task SetPrimaryEmergencyContactAsync(Guid userProfileId, Guid contactId, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var all = await db.EmergencyContacts.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct); var all = await db.EmergencyContacts.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct);
foreach (var x in all) x.IsPrimary = false; foreach (var x in all) x.IsPrimary = false;
@@ -121,8 +136,9 @@ public class UserProfileRepository : IUserProfileRepository
public async Task<EmployeeBankAccount> AddBankAccountAsync(EmployeeBankAccount b, CancellationToken ct = default) public async Task<EmployeeBankAccount> AddBankAccountAsync(EmployeeBankAccount b, CancellationToken ct = default)
{ {
var tid = TenantId();
var db = _scope.Get<AppDbContext>(); var db = _scope.Get<AppDbContext>();
b.TenantId = TenantId(); b.TenantId = tid;
await db.EmployeeBankAccounts.AddAsync(b, ct); await db.EmployeeBankAccounts.AddAsync(b, ct);
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
return b; return b;
@@ -130,8 +146,8 @@ public class UserProfileRepository : IUserProfileRepository
public async Task SetPrimaryBankAccountAsync(Guid userProfileId, Guid bankAccountId, CancellationToken ct = default) public async Task SetPrimaryBankAccountAsync(Guid userProfileId, Guid bankAccountId, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var all = await db.EmployeeBankAccounts.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct); var all = await db.EmployeeBankAccounts.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct);
foreach (var x in all) x.IsPrimary = false; foreach (var x in all) x.IsPrimary = false;

View File

@@ -3,6 +3,7 @@ using AMREZ.EOP.Abstractions.Applications.Tenancy;
using AMREZ.EOP.Abstractions.Infrastructures.Repositories; using AMREZ.EOP.Abstractions.Infrastructures.Repositories;
using AMREZ.EOP.Abstractions.Storage; using AMREZ.EOP.Abstractions.Storage;
using AMREZ.EOP.Domain.Entities.Authentications; using AMREZ.EOP.Domain.Entities.Authentications;
using AMREZ.EOP.Domain.Entities.Tenancy;
using AMREZ.EOP.Domain.Shared._Users; using AMREZ.EOP.Domain.Shared._Users;
using AMREZ.EOP.Infrastructures.Data; using AMREZ.EOP.Infrastructures.Data;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@@ -25,16 +26,27 @@ public class UserRepository : IUserRepository
IHttpContextAccessor http) IHttpContextAccessor http)
{ {
_scope = scope; _scope = scope;
_redis = redis; // null = ไม่มี/ต่อไม่ได้ → ข้าม cache _redis = redis;
_tenantResolver = tenantResolver; _tenantResolver = tenantResolver;
_http = http; _http = http;
} }
private Guid TenantId() private Guid TenantId()
{ {
var http = _http.HttpContext; var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext");
var tc = http is not null ? _tenantResolver.Resolve(http) : null; var tc = _tenantResolver.Resolve(http) ?? throw new InvalidOperationException("No tenant");
return Guid.TryParse(tc?.Id, out var g) ? g : Guid.Empty; _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<AppDbContext>();
var cfg = db.Set<TenantConfig>().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) private static string IdentityEmailKey(string tenantId, string email)
@@ -45,8 +57,8 @@ public class UserRepository : IUserRepository
public async Task<User?> FindByIdAsync(Guid userId, CancellationToken ct = default) public async Task<User?> FindByIdAsync(Guid userId, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
return await db.Users.AsNoTracking() return await db.Users.AsNoTracking()
.FirstOrDefaultAsync(u => u.TenantId == tid && u.Id == userId, ct); .FirstOrDefaultAsync(u => u.TenantId == tid && u.Id == userId, ct);
} }
@@ -57,7 +69,6 @@ public class UserRepository : IUserRepository
var tidStr = tid.ToString(); var tidStr = tid.ToString();
var r = _redis?.GetDatabase(); var r = _redis?.GetDatabase();
// 1) Redis
if (r is not null) if (r is not null)
{ {
try try
@@ -69,10 +80,9 @@ public class UserRepository : IUserRepository
if (hit?.IsActive == true) return hit; if (hit?.IsActive == true) return hit;
} }
} }
catch { /* ignore → query DB */ } catch { }
} }
// 2) EF: join identities
var db = _scope.Get<AppDbContext>(); var db = _scope.Get<AppDbContext>();
var norm = email.Trim().ToLowerInvariant(); var norm = email.Trim().ToLowerInvariant();
@@ -86,7 +96,6 @@ public class UserRepository : IUserRepository
i.Identifier == norm)) i.Identifier == norm))
.FirstOrDefaultAsync(ct); .FirstOrDefaultAsync(ct);
// 3) cache
if (user is not null && r is not null) if (user is not null && r is not null)
{ {
try try
@@ -96,7 +105,7 @@ public class UserRepository : IUserRepository
await r.StringSetAsync(IdentityEmailKey(tidStr, norm), payload, ttl); await r.StringSetAsync(IdentityEmailKey(tidStr, norm), payload, ttl);
await r.StringSetAsync(UserIdKey(tidStr, user.Id), payload, ttl); await r.StringSetAsync(UserIdKey(tidStr, user.Id), payload, ttl);
} }
catch { /* ignore */ } catch { }
} }
return user; return user;
@@ -104,8 +113,8 @@ public class UserRepository : IUserRepository
public async Task<bool> EmailExistsAsync(string email, CancellationToken ct = default) public async Task<bool> EmailExistsAsync(string email, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var norm = email.Trim().ToLowerInvariant(); var norm = email.Trim().ToLowerInvariant();
return await db.UserIdentities.AsNoTracking() return await db.UserIdentities.AsNoTracking()
@@ -114,40 +123,36 @@ public class UserRepository : IUserRepository
public async Task AddAsync(User user, CancellationToken ct = default) public async Task AddAsync(User user, CancellationToken ct = default)
{ {
var tid = TenantId();
var db = _scope.Get<AppDbContext>(); var db = _scope.Get<AppDbContext>();
user.TenantId = TenantId(); user.TenantId = tid;
await db.Users.AddAsync(user, ct); await db.Users.AddAsync(user, ct);
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
// cache
var r = _redis?.GetDatabase(); var r = _redis?.GetDatabase();
if (r is not null) if (r is not null)
{ {
try try
{ {
var tid = user.TenantId.ToString();
var payload = JsonSerializer.Serialize(user); var payload = JsonSerializer.Serialize(user);
var ttl = TimeSpan.FromMinutes(5); 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 var email = user.Identities
.FirstOrDefault(i => i.Type == IdentityType.Email && i.IsPrimary)?.Identifier; .FirstOrDefault(i => i.Type == IdentityType.Email && i.IsPrimary)?.Identifier;
if (!string.IsNullOrWhiteSpace(email)) 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) public async Task AddIdentityAsync(Guid userId, IdentityType type, string identifier, bool isPrimary, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var norm = identifier.Trim().ToLowerInvariant(); var norm = identifier.Trim().ToLowerInvariant();
if (isPrimary) if (isPrimary)
@@ -171,11 +176,10 @@ public class UserRepository : IUserRepository
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
} }
// (2) IMPLEMENTED: VerifyIdentityAsync
public async Task VerifyIdentityAsync(Guid userId, IdentityType type, string identifier, DateTimeOffset verifiedAt, CancellationToken ct = default) public async Task VerifyIdentityAsync(Guid userId, IdentityType type, string identifier, DateTimeOffset verifiedAt, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var norm = identifier.Trim().ToLowerInvariant(); var norm = identifier.Trim().ToLowerInvariant();
var id = await db.UserIdentities var id = await db.UserIdentities
@@ -190,11 +194,10 @@ public class UserRepository : IUserRepository
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
} }
// (3) IMPLEMENTED: GetPrimaryIdentityAsync
public async Task<UserIdentity?> GetPrimaryIdentityAsync(Guid userId, IdentityType type, CancellationToken ct = default) public async Task<UserIdentity?> GetPrimaryIdentityAsync(Guid userId, IdentityType type, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
return await db.UserIdentities.AsNoTracking() return await db.UserIdentities.AsNoTracking()
.FirstOrDefaultAsync(i => .FirstOrDefaultAsync(i =>
@@ -204,12 +207,10 @@ public class UserRepository : IUserRepository
i.IsPrimary, ct); i.IsPrimary, ct);
} }
// ========= Password =========
public async Task ChangePasswordAsync(Guid userId, string newPasswordHash, CancellationToken ct = default) public async Task ChangePasswordAsync(Guid userId, string newPasswordHash, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var user = await db.Users.FirstOrDefaultAsync(u => u.TenantId == tid && u.Id == userId, ct); var user = await db.Users.FirstOrDefaultAsync(u => u.TenantId == tid && u.Id == userId, ct);
if (user is null) return; if (user is null) return;
@@ -221,8 +222,8 @@ public class UserRepository : IUserRepository
public async Task AddPasswordHistoryAsync(Guid userId, string passwordHash, CancellationToken ct = default) public async Task AddPasswordHistoryAsync(Guid userId, string passwordHash, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var h = new UserPasswordHistory var h = new UserPasswordHistory
{ {
@@ -236,12 +237,10 @@ public class UserRepository : IUserRepository
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
} }
// ========= MFA =========
public async Task<UserMfaFactor> AddTotpFactorAsync(Guid userId, string label, string secret, CancellationToken ct = default) public async Task<UserMfaFactor> AddTotpFactorAsync(Guid userId, string label, string secret, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var f = new UserMfaFactor var f = new UserMfaFactor
{ {
@@ -265,8 +264,8 @@ public class UserRepository : IUserRepository
public async Task DisableMfaFactorAsync(Guid factorId, CancellationToken ct = default) public async Task DisableMfaFactorAsync(Guid factorId, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var f = await db.UserMfaFactors.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == factorId, ct); var f = await db.UserMfaFactors.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == factorId, ct);
if (f is null) return; if (f is null) return;
@@ -285,26 +284,45 @@ public class UserRepository : IUserRepository
public async Task<bool> HasAnyMfaAsync(Guid userId, CancellationToken ct = default) public async Task<bool> HasAnyMfaAsync(Guid userId, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
return await db.UserMfaFactors.AnyAsync(x => x.TenantId == tid && x.UserId == userId && x.Enabled, ct); return await db.UserMfaFactors.AnyAsync(x => x.TenantId == tid && x.UserId == userId && x.Enabled, ct);
} }
// ========= Sessions =========
public async Task<UserSession> CreateSessionAsync(UserSession session, CancellationToken ct = default) public async Task<UserSession> CreateSessionAsync(UserSession session, CancellationToken ct = default)
{ {
var tid = TenantId();
var db = _scope.Get<AppDbContext>(); var db = _scope.Get<AppDbContext>();
session.TenantId = TenantId(); session.TenantId = tid;
await db.UserSessions.AddAsync(session, ct); await db.UserSessions.AddAsync(session, ct);
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
return session; return session;
} }
public async Task<int> RevokeSessionAsync(Guid userId, Guid sessionId, CancellationToken ct = default) public async Task<UserSession?> FindSessionByRefreshHashAsync(Guid tenantId, string refreshTokenHash, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>(); var db = _scope.Get<AppDbContext>();
return await db.UserSessions
.AsNoTracking()
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.RefreshTokenHash == refreshTokenHash, ct);
}
public async Task<bool> RotateSessionRefreshAsync(Guid tenantId, Guid sessionId, string newRefreshTokenHash, DateTimeOffset newIssuedAt, DateTimeOffset? newExpiresAt, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
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<int> RevokeSessionAsync(Guid userId, Guid sessionId, CancellationToken ct = default)
{
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var s = await db.UserSessions.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == sessionId && x.UserId == userId, ct); var s = await db.UserSessions.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == sessionId && x.UserId == userId, ct);
if (s is null) return 0; if (s is null) return 0;
@@ -315,8 +333,8 @@ public class UserRepository : IUserRepository
public async Task<int> RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default) public async Task<int> RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default)
{ {
var db = _scope.Get<AppDbContext>();
var tid = TenantId(); var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var sessions = await db.UserSessions var sessions = await db.UserSessions
.Where(x => x.TenantId == tid && x.UserId == userId && x.RevokedAt == null) .Where(x => x.TenantId == tid && x.UserId == userId && x.RevokedAt == null)
@@ -326,4 +344,61 @@ public class UserRepository : IUserRepository
return await db.SaveChangesAsync(ct); return await db.SaveChangesAsync(ct);
} }
public async Task<bool> IsSessionActiveAsync(Guid userId, Guid sessionId, CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
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<string> 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<string?> GetUserSecurityStampAsync(Guid userId, CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
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<AppDbContext>();
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);
}
} }

View File

@@ -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<Claim> 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);
}
}

View File

@@ -28,7 +28,6 @@ public sealed class DefaultTenantResolver : ITenantResolver
var cid = http.TraceIdentifier; var cid = http.TraceIdentifier;
var path = http.Request.Path.Value ?? string.Empty; var path = http.Request.Path.Value ?? string.Empty;
// ✅ ทางลัด: ขอ platform ตรง ๆ ด้วย hint
if (hint is string hs && string.Equals(hs, "@platform", StringComparison.OrdinalIgnoreCase)) if (hint is string hs && string.Equals(hs, "@platform", StringComparison.OrdinalIgnoreCase))
{ {
if (_map.TryGetBySlug(PlatformSlug, out var platform)) if (_map.TryGetBySlug(PlatformSlug, out var platform))

View File

@@ -63,7 +63,6 @@ namespace AMREZ.EOP.Infrastructures.Tenancy;
} }
} }
// 3) Bootstrap 'public' ถ้ายังไม่มี (กัน chicken-and-egg)
if (!map.TryGetBySlug("public", out _)) if (!map.TryGetBySlug("public", out _))
{ {
map.UpsertTenant( map.UpsertTenant(

View File

@@ -18,7 +18,6 @@ public sealed class EFUnitOfWork : IUnitOfWork, IAsyncDisposable
private readonly ITenantDbContextFactory _factory; private readonly ITenantDbContextFactory _factory;
private readonly IConnectionMultiplexer? _redis; private readonly IConnectionMultiplexer? _redis;
private AppDbContext? _db;
private IDbContextTransaction? _tx; private IDbContextTransaction? _tx;
private ITenantContext? _tenant; private ITenantContext? _tenant;
@@ -31,40 +30,38 @@ public sealed class EFUnitOfWork : IUnitOfWork, IAsyncDisposable
{ {
_scope = scope; _scope = scope;
_factory = factory; _factory = factory;
_redis = redis; // optional; null ได้ _redis = redis;
} }
public async Task BeginAsync(ITenantContext tenant, IsolationLevel isolation = IsolationLevel.ReadCommitted, CancellationToken ct = default) 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)); _tenant = tenant ?? throw new ArgumentNullException(nameof(tenant));
_scope.EnsureForTenant(tenant); _scope.EnsureForTenant(tenant);
_db = _scope.Get<AppDbContext>(); var db = _scope.Get<AppDbContext>();
_tx = await _db.Database.BeginTransactionAsync(isolation, ct); _tx = await db.Database.BeginTransactionAsync(isolation, ct);
} }
public async Task CommitAsync(CancellationToken ct = default) public async Task CommitAsync(CancellationToken ct = default)
{ {
if (_db is null) return; // ยังไม่ Begin if (_tx is null) return;
// track entities ที่เปลี่ยน (เพื่อ invalidate cache แบบแม่นขึ้น) var db = _scope.Get<AppDbContext>();
var changedUsers = _db.ChangeTracker.Entries<User>()
var changedUsers = db.ChangeTracker.Entries<User>()
.Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)
.Select(e => e.Entity) .Select(e => e.Entity)
.ToList(); .ToList();
var changedIdentities = _db.ChangeTracker.Entries<UserIdentity>() var changedIdentities = db.ChangeTracker.Entries<UserIdentity>()
.Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)
.Select(e => e.Entity) .Select(e => e.Entity)
.ToList(); .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)) if (_redis is not null && _tenant is not null && (changedUsers.Count > 0 || changedIdentities.Count > 0))
{ {
var r = _redis.GetDatabase(); var r = _redis.GetDatabase();
@@ -75,8 +72,6 @@ public sealed class EFUnitOfWork : IUnitOfWork, IAsyncDisposable
{ {
var keyId = $"eop:{tenantId}:user:id:{u.Id:N}"; var keyId = $"eop:{tenantId}:user:id:{u.Id:N}";
tasks.Add(r.KeyDeleteAsync(keyId)); tasks.Add(r.KeyDeleteAsync(keyId));
// refresh cache (ถ้าต้องการ)
var payload = JsonSerializer.Serialize(u); var payload = JsonSerializer.Serialize(u);
var ttl = TimeSpan.FromMinutes(5); var ttl = TimeSpan.FromMinutes(5);
tasks.Add(r.StringSetAsync(keyId, payload, ttl)); tasks.Add(r.StringSetAsync(keyId, payload, ttl));
@@ -88,7 +83,7 @@ public sealed class EFUnitOfWork : IUnitOfWork, IAsyncDisposable
tasks.Add(r.KeyDeleteAsync(k)); tasks.Add(r.KeyDeleteAsync(k));
} }
try { await Task.WhenAll(tasks); } catch { /* swallow */ } try { await Task.WhenAll(tasks); } catch { }
} }
await DisposeAsync(); await DisposeAsync();
@@ -109,11 +104,9 @@ public sealed class EFUnitOfWork : IUnitOfWork, IAsyncDisposable
public ValueTask DisposeAsync() public ValueTask DisposeAsync()
{ {
try { _tx?.Dispose(); } catch { /* ignore */ } try { _tx?.Dispose(); } catch { }
try { _db?.Dispose(); } catch { /* ignore */ } _tx = null;
_tenant = null;
_tx = null; _db = null; _tenant = null;
return ValueTask.CompletedTask; return ValueTask.CompletedTask;
} }
} }