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.DisableMfa;
using AMREZ.EOP.Contracts.DTOs.Authentications.EnableTotp;
using AMREZ.EOP.Contracts.DTOs.Authentications.IssueTokenPair;
using AMREZ.EOP.Contracts.DTOs.Authentications.Login;
using AMREZ.EOP.Contracts.DTOs.Authentications.Logout;
using AMREZ.EOP.Contracts.DTOs.Authentications.LogoutAll;
using AMREZ.EOP.Contracts.DTOs.Authentications.Refresh;
using AMREZ.EOP.Contracts.DTOs.Authentications.Register;
using AMREZ.EOP.Contracts.DTOs.Authentications.VerifyEmail;
using AMREZ.EOP.Domain.Shared.Contracts;
@@ -32,14 +34,33 @@ public class AuthenticationController : ControllerBase
private readonly ILogoutUseCase _logout;
private readonly ILogoutAllUseCase _logoutAll;
private readonly IIssueTokenPairUseCase _issueTokens;
private readonly IRefreshUseCase _refresh;
public AuthenticationController(
ILoginUseCase login,
IRegisterUseCase register,
IChangePasswordUseCase changePassword)
IChangePasswordUseCase changePassword,
IAddEmailIdentityUseCase addEmail,
IVerifyEmailUseCase verifyEmail,
IEnableTotpUseCase enableTotp,
IDisableMfaUseCase disableMfa,
ILogoutUseCase logout,
ILogoutAllUseCase logoutAll,
IIssueTokenPairUseCase issueTokens,
IRefreshUseCase refresh)
{
_login = login;
_register = register;
_changePassword = changePassword;
_addEmail = addEmail;
_verifyEmail = verifyEmail;
_enableTotp = enableTotp;
_disableMfa = disableMfa;
_logout = logout;
_logoutAll = logoutAll;
_issueTokens = issueTokens;
_refresh = refresh;
}
[HttpPost("login")]
@@ -51,7 +72,7 @@ public class AuthenticationController : ControllerBase
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, res.UserId.ToString()),
new(ClaimTypes.Name, string.IsNullOrWhiteSpace(res.DisplayName) ? res.Email : res.DisplayName),
new(ClaimTypes.Name, res.Email),
new(ClaimTypes.Email, res.Email),
new("tenant", res.TenantId)
};
@@ -59,9 +80,71 @@ public class AuthenticationController : ControllerBase
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthPolicies.Scheme));
await HttpContext.SignInAsync(AuthPolicies.Scheme, principal);
return Ok(res);
var tokenPair = await _issueTokens.ExecuteAsync(new IssueTokenPairRequest()
{
UserId = res.UserId,
Tenant = res.TenantId,
Email = res.Email
}, ct);
if (!string.IsNullOrWhiteSpace(tokenPair.RefreshToken))
{
Response.Cookies.Append(
"refresh_token",
tokenPair.RefreshToken!,
new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = tokenPair.RefreshExpiresAt?.UtcDateTime
});
}
return Ok(new
{
user = res,
access_token = tokenPair.AccessToken,
token_type = "Bearer",
expires_at = tokenPair.AccessExpiresAt
});
}
[HttpPost("refresh")]
public async Task<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")]
public async Task<IActionResult> Register([FromBody] RegisterRequest body, CancellationToken ct)
{

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
{
// ===== Users/Identities/Password/MFA (ของเดิม) =====
Task<User?> FindByIdAsync(Guid userId, CancellationToken ct = default);
Task<User?> FindActiveByEmailAsync(string email, CancellationToken ct = default);
Task<bool> EmailExistsAsync(string email, CancellationToken ct = default);
Task AddAsync(User user, CancellationToken ct = default);
// Identities
Task AddIdentityAsync(Guid userId, IdentityType type, string identifier, bool isPrimary, CancellationToken ct = default);
Task VerifyIdentityAsync(Guid userId, IdentityType type, string identifier, DateTimeOffset verifiedAt, CancellationToken ct = default);
Task<UserIdentity?> GetPrimaryIdentityAsync(Guid userId, IdentityType type, CancellationToken ct = default);
// Password
Task ChangePasswordAsync(Guid userId, string newPasswordHash, CancellationToken ct = default);
Task AddPasswordHistoryAsync(Guid userId, string passwordHash, CancellationToken ct = default);
// MFA
Task<UserMfaFactor> AddTotpFactorAsync(Guid userId, string label, string secret, CancellationToken ct = default);
Task DisableMfaFactorAsync(Guid factorId, 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?> 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> 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);
// NOTE: ไม่ใช้ DisplayName ใน Entity แล้ว — ส่งกลับเป็นค่าว่าง/ไปดึงจาก HR ฝั่ง API
return new LoginResponse(user.Id, string.Empty, email, tenant.Id);
return new LoginResponse(user.Id , email, tenant.Id);
}
catch
{

View File

@@ -16,12 +16,7 @@ public sealed class LogoutAllUseCase : ILogoutAllUseCase
private readonly IHttpContextAccessor _http;
public LogoutAllUseCase(ITenantResolver r, IUnitOfWork uow, IUserRepository users, IHttpContextAccessor http)
{
_resolver = r;
_uow = uow;
_users = users;
_http = http;
}
{ _resolver = r; _uow = uow; _users = users; _http = http; }
public async Task<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.Repositories;
using AMREZ.EOP.Contracts.DTOs.Authentications.Logout;
using AMREZ.EOP.Domain.Entities.Authentications;
using Microsoft.AspNetCore.Http;
namespace AMREZ.EOP.Application.UseCases.Authentications;
@@ -31,6 +32,10 @@ public sealed class LogoutUseCase : ILogoutUseCase
await _uow.CommitAsync(ct);
return n > 0;
}
catch { await _uow.RollbackAsync(ct); throw; }
catch
{
await _uow.RollbackAsync(ct);
throw;
}
}
}

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

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(
Guid UserId,
string DisplayName,
string Email,
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 Guid TenantId { get; set; }
public string Code { get; set; } = default!; // e.g. "auth:session:read"
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 Guid TenantId { get; set; }
public string Code { get; set; } = default!; // system code, unique per tenant
public string Name { get; set; } = default!;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="StackExchange.Redis" Version="2.9.17" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
</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.Tenancy;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace AMREZ.EOP.Infrastructures.Data;
@@ -37,11 +38,14 @@ public class AppDbContext : DbContext
protected override void OnModelCreating(ModelBuilder model)
{
// ====== Global Tenancy Config (meta schema) — ไม่สืบทอด BaseEntity ======
// ====== Tenancy (meta) ======
model.Entity<TenantConfig>(b =>
{
b.ToTable("tenants", schema: "meta");
b.HasKey(x => x.TenantKey);
b.HasKey(x => x.TenantKey); // PK = key (slug)
b.HasAlternateKey(x => x.TenantId); // AK = GUID
b.HasIndex(x => x.TenantId).IsUnique();
b.Property(x => x.TenantKey).HasMaxLength(128).IsRequired();
b.Property(x => x.Schema).HasMaxLength(128);
b.Property(x => x.ConnectionString);
@@ -58,7 +62,7 @@ public class AppDbContext : DbContext
b.ToTable("tenant_domains", schema: "meta");
b.HasKey(x => x.Domain);
b.Property(x => x.Domain).HasMaxLength(253).IsRequired();
b.Property(x => x.TenantKey).HasMaxLength(128);
b.Property(x => x.TenantKey).HasMaxLength(128); // optional
b.Property(x => x.IsPlatformBaseDomain).HasDefaultValue(false);
b.Property(x => x.IsActive).HasDefaultValue(true);
b.Property(x => x.UpdatedAtUtc)
@@ -81,36 +85,13 @@ public class AppDbContext : DbContext
b.ToTable("users");
b.HasKey(x => x.Id);
// principal key สำหรับ composite FK จากลูก ๆ
b.HasAlternateKey(u => new { u.TenantId, u.Id });
b.Property(x => x.PasswordHash).IsRequired();
b.Property(x => x.IsActive).HasDefaultValue(true);
b.Property(x => x.AccessFailedCount).HasDefaultValue(0);
b.Property(x => x.MfaEnabled).HasDefaultValue(false);
b.HasMany(x => x.Identities)
.WithOne(i => i.User)
.HasForeignKey(i => i.UserId)
.OnDelete(DeleteBehavior.Cascade);
b.HasMany(x => x.MfaFactors)
.WithOne(i => i.User)
.HasForeignKey(i => i.UserId)
.OnDelete(DeleteBehavior.Cascade);
b.HasMany(x => x.Sessions)
.WithOne(s => s.User)
.HasForeignKey(s => s.UserId)
.OnDelete(DeleteBehavior.Cascade);
b.HasMany(x => x.PasswordHistories)
.WithOne(ph => ph.User)
.HasForeignKey(ph => ph.UserId)
.OnDelete(DeleteBehavior.Cascade);
b.HasMany(x => x.ExternalAccounts)
.WithOne(ea => ea.User)
.HasForeignKey(ea => ea.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<UserIdentity>(b =>
@@ -122,11 +103,16 @@ public class AppDbContext : DbContext
b.Property(x => x.Identifier).IsRequired().HasMaxLength(256);
b.Property(x => x.IsPrimary).HasDefaultValue(false);
b.HasIndex(x => new { x.TenantId, x.Type, x.Identifier })
.IsUnique();
b.HasIndex(x => new { x.TenantId, x.Type, x.Identifier }).IsUnique();
b.HasIndex(x => new { x.TenantId, x.UserId, x.Type, x.IsPrimary })
.HasDatabaseName("ix_user_identity_primary_per_type");
// (TenantId, UserId) -> User.(TenantId, Id)
b.HasOne(i => i.User)
.WithMany(u => u.Identities)
.HasForeignKey(i => new { i.TenantId, i.UserId })
.HasPrincipalKey(nameof(User.TenantId), nameof(User.Id))
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<UserMfaFactor>(b =>
@@ -136,8 +122,13 @@ public class AppDbContext : DbContext
b.Property(x => x.Type).IsRequired();
b.Property(x => x.Enabled).HasDefaultValue(true);
b.HasIndex(x => new { x.TenantId, x.UserId });
b.HasOne(x => x.User)
.WithMany(u => u.MfaFactors)
.HasForeignKey(x => new { x.TenantId, x.UserId })
.HasPrincipalKey(nameof(User.TenantId), nameof(User.Id))
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<UserSession>(b =>
@@ -148,6 +139,12 @@ public class AppDbContext : DbContext
b.Property(x => x.RefreshTokenHash).IsRequired();
b.HasIndex(x => new { x.TenantId, x.UserId });
b.HasIndex(x => new { x.TenantId, x.DeviceId });
b.HasOne(x => x.User)
.WithMany(u => u.Sessions)
.HasForeignKey(x => new { x.TenantId, x.UserId })
.HasPrincipalKey(nameof(User.TenantId), nameof(User.Id))
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<UserPasswordHistory>(b =>
@@ -156,6 +153,12 @@ public class AppDbContext : DbContext
b.HasKey(x => x.Id);
b.Property(x => x.PasswordHash).IsRequired();
b.HasIndex(x => new { x.TenantId, x.UserId, x.ChangedAt });
b.HasOne(x => x.User)
.WithMany(u => u.PasswordHistories)
.HasForeignKey(x => new { x.TenantId, x.UserId })
.HasPrincipalKey(nameof(User.TenantId), nameof(User.Id))
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<UserExternalAccount>(b =>
@@ -165,15 +168,20 @@ public class AppDbContext : DbContext
b.Property(x => x.Provider).IsRequired();
b.Property(x => x.Subject).IsRequired();
b.HasIndex(x => new { x.TenantId, x.Provider, x.Subject }).IsUnique();
b.HasIndex(x => new { x.TenantId, x.Provider, x.Subject })
.IsUnique();
b.HasOne(x => x.User)
.WithMany(u => u.ExternalAccounts)
.HasForeignKey(x => new { x.TenantId, x.UserId })
.HasPrincipalKey(nameof(User.TenantId), nameof(User.Id))
.OnDelete(DeleteBehavior.Cascade);
});
model.Entity<Role>(b =>
{
b.ToTable("roles");
b.HasKey(x => x.Id);
b.HasAlternateKey(r => new { r.TenantId, r.Id });
b.Property(x => x.Code).IsRequired().HasMaxLength(128);
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
@@ -185,6 +193,7 @@ public class AppDbContext : DbContext
{
b.ToTable("permissions");
b.HasKey(x => x.Id);
b.HasAlternateKey(p => new { p.TenantId, p.Id });
b.Property(x => x.Code).IsRequired().HasMaxLength(256);
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
@@ -198,6 +207,18 @@ public class AppDbContext : DbContext
b.HasKey(x => x.Id);
b.HasIndex(x => new { x.TenantId, x.UserId, x.RoleId }).IsUnique();
b.HasOne<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 =>
@@ -206,6 +227,18 @@ public class AppDbContext : DbContext
b.HasKey(x => x.Id);
b.HasIndex(x => new { x.TenantId, x.RoleId, x.PermissionId }).IsUnique();
b.HasOne<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 ======
@@ -213,22 +246,25 @@ public class AppDbContext : DbContext
{
b.ToTable("user_profiles");
b.HasKey(x => x.Id);
b.HasAlternateKey(p => new { p.TenantId, p.Id });
b.Property(x => x.FirstName).IsRequired().HasMaxLength(128);
b.Property(x => x.LastName).IsRequired().HasMaxLength(128);
b.HasOne(x => x.User)
.WithOne()
.HasForeignKey<UserProfile>(x => x.UserId)
.OnDelete(DeleteBehavior.Cascade);
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 =>
{
b.ToTable("departments");
b.HasKey(x => x.Id);
b.HasAlternateKey(d => new { d.TenantId, d.Id });
b.Property(x => x.Code).IsRequired().HasMaxLength(64);
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
@@ -237,7 +273,8 @@ public class AppDbContext : DbContext
b.HasOne(x => x.Parent)
.WithMany(x => x.Children)
.HasForeignKey(x => x.ParentDepartmentId)
.HasForeignKey(x => new { x.TenantId, x.ParentDepartmentId })
.HasPrincipalKey(nameof(Department.TenantId), nameof(Department.Id))
.OnDelete(DeleteBehavior.Restrict);
});
@@ -245,6 +282,7 @@ public class AppDbContext : DbContext
{
b.ToTable("positions");
b.HasKey(x => x.Id);
b.HasAlternateKey(p => new { p.TenantId, p.Id });
b.Property(x => x.Code).IsRequired().HasMaxLength(64);
b.Property(x => x.Title).IsRequired().HasMaxLength(256);
@@ -264,17 +302,20 @@ public class AppDbContext : DbContext
b.HasOne(x => x.UserProfile)
.WithMany(p => p.Employments)
.HasForeignKey(x => x.UserProfileId)
.HasForeignKey(x => new { x.TenantId, x.UserProfileId })
.HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id))
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.Department)
.WithMany()
.HasForeignKey(x => x.DepartmentId)
.HasForeignKey(x => new { x.TenantId, x.DepartmentId })
.HasPrincipalKey(nameof(Department.TenantId), nameof(Department.Id))
.OnDelete(DeleteBehavior.Restrict);
b.HasOne(x => x.Position)
.WithMany()
.HasForeignKey(x => x.PositionId)
.HasForeignKey(x => new { x.TenantId, x.PositionId })
.HasPrincipalKey(nameof(Position.TenantId), nameof(Position.Id))
.OnDelete(DeleteBehavior.Restrict);
});
@@ -290,7 +331,8 @@ public class AppDbContext : DbContext
b.HasOne(x => x.UserProfile)
.WithMany(p => p.Addresses)
.HasForeignKey(x => x.UserProfileId)
.HasForeignKey(x => new { x.TenantId, x.UserProfileId })
.HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id))
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary });
@@ -306,7 +348,8 @@ public class AppDbContext : DbContext
b.HasOne(x => x.UserProfile)
.WithMany(p => p.EmergencyContacts)
.HasForeignKey(x => x.UserProfileId)
.HasForeignKey(x => new { x.TenantId, x.UserProfileId })
.HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id))
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary });
@@ -323,7 +366,8 @@ public class AppDbContext : DbContext
b.HasOne(x => x.UserProfile)
.WithMany(p => p.BankAccounts)
.HasForeignKey(x => x.UserProfileId)
.HasForeignKey(x => new { x.TenantId, x.UserProfileId })
.HasPrincipalKey(nameof(UserProfile.TenantId), nameof(UserProfile.Id))
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.TenantId, x.UserProfileId, x.IsPrimary });
@@ -347,10 +391,18 @@ public class AppDbContext : DbContext
.HasColumnName("tenant_id")
.HasColumnType("uuid")
.IsRequired()
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.ValueGeneratedNever();
b.HasIndex(nameof(BaseEntity.TenantId));
b.HasCheckConstraint($"ck_{et.GetTableName()}_tenant_not_null", "tenant_id is not null");
// ชื่อ constraint สร้างแบบ concat แทน string interpolation กัน ambiguous handler
var tn = et.GetTableName();
if (!string.IsNullOrEmpty(tn))
{
b.HasCheckConstraint(string.Concat("ck_", tn, "_tenant_not_null"), "tenant_id is not null");
b.HasCheckConstraint(string.Concat("ck_", tn, "_tenant_not_zero"),
"tenant_id <> '00000000-0000-0000-0000-000000000000'");
}
// Audit
b.Property<DateTimeOffset>("CreatedAt")

View File

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

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
{
[DbContext(typeof(AppDbContext))]
[Migration("20250930101327_Add_Tenant_Guid")]
partial class Add_Tenant_Guid
[Migration("20251002040013_InitDatabase")]
partial class InitDatabase
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -58,10 +58,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("character varying(256)");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -81,6 +79,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("permissions", null, t =>
{
t.HasCheckConstraint("ck_permissions_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -117,10 +117,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("character varying(256)");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -140,6 +138,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("roles", null, t =>
{
t.HasCheckConstraint("ck_roles_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -172,10 +172,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("uuid");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -193,12 +191,16 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("TenantId", "PermissionId");
b.HasIndex("TenantId", "RoleId", "PermissionId")
.IsUnique();
b.ToTable("role_permissions", null, t =>
{
t.HasCheckConstraint("ck_role_permissions_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_role_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -250,10 +252,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("text");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -270,6 +270,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("users", null, t =>
{
t.HasCheckConstraint("ck_users_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_users_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -309,10 +311,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("text");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -329,7 +329,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "UserId");
b.HasIndex("TenantId", "Provider", "Subject")
.IsUnique();
@@ -337,6 +337,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("user_external_accounts", null, t =>
{
t.HasCheckConstraint("ck_user_external_accounts_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_external_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -373,10 +375,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasDefaultValue(false);
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<int>("Type")
.HasColumnType("integer");
@@ -399,8 +399,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "Type", "Identifier")
.IsUnique();
@@ -410,6 +408,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("user_identities", null, t =>
{
t.HasCheckConstraint("ck_user_identities_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_identities_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -465,10 +465,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("text");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<int>("Type")
.HasColumnType("integer");
@@ -488,13 +486,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "UserId");
b.ToTable("user_mfa_factors", null, t =>
{
t.HasCheckConstraint("ck_user_mfa_factors_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_mfa_factors_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -528,10 +526,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("text");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -548,13 +544,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "UserId", "ChangedAt");
b.ToTable("user_password_histories", null, t =>
{
t.HasCheckConstraint("ck_user_password_histories_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_password_histories_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -584,10 +580,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("uuid");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -608,12 +602,16 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("UserId");
b.HasIndex("TenantId", "RoleId");
b.HasIndex("TenantId", "UserId", "RoleId")
.IsUnique();
b.ToTable("user_roles", null, t =>
{
t.HasCheckConstraint("ck_user_roles_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -659,10 +657,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -682,8 +678,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "DeviceId");
b.HasIndex("TenantId", "UserId");
@@ -691,6 +685,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("user_sessions", null, t =>
{
t.HasCheckConstraint("ck_user_sessions_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_sessions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -730,10 +726,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("uuid");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -745,16 +739,18 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasKey("Id");
b.HasIndex("ParentDepartmentId");
b.HasIndex("TenantId");
b.HasIndex("TenantId", "Code")
.IsUnique();
b.HasIndex("TenantId", "ParentDepartmentId");
b.ToTable("departments", null, t =>
{
t.HasCheckConstraint("ck_departments_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_departments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -800,10 +796,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("character varying(64)");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -820,13 +814,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "UserProfileId", "IsPrimary");
b.ToTable("emergency_contacts", null, t =>
{
t.HasCheckConstraint("ck_emergency_contacts_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_emergency_contacts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -882,10 +876,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("text");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<int>("Type")
.HasColumnType("integer");
@@ -905,13 +897,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "UserProfileId", "IsPrimary");
b.ToTable("employee_addresses", null, t =>
{
t.HasCheckConstraint("ck_employee_addresses_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_employee_addresses_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -962,10 +954,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("text");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -982,13 +972,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "UserProfileId", "IsPrimary");
b.ToTable("employee_bank_accounts", null, t =>
{
t.HasCheckConstraint("ck_employee_bank_accounts_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_employee_bank_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -1033,10 +1023,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -1057,19 +1045,19 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasKey("Id");
b.HasIndex("DepartmentId");
b.HasIndex("PositionId");
b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "DepartmentId");
b.HasIndex("TenantId", "PositionId");
b.HasIndex("TenantId", "UserProfileId", "StartDate");
b.ToTable("employments", null, t =>
{
t.HasCheckConstraint("ck_employments_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_employments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -1104,10 +1092,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("integer");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<string>("Title")
.IsRequired()
@@ -1132,6 +1118,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("positions", null, t =>
{
t.HasCheckConstraint("ck_positions_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_positions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -1186,10 +1174,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("uuid");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -1210,15 +1196,14 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserId")
.IsUnique();
b.HasIndex("TenantId", "UserId")
.IsUnique();
b.ToTable("user_profiles", null, t =>
{
t.HasCheckConstraint("ck_user_profiles_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_profiles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -1254,8 +1239,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasKey("TenantKey");
b.HasAlternateKey("TenantId");
b.HasIndex("IsActive");
b.HasIndex("TenantId")
.IsUnique();
b.ToTable("tenants", "meta");
});
@@ -1310,6 +1300,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Permission", null)
.WithMany()
.HasForeignKey("TenantId", "PermissionId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Role", null)
.WithMany()
.HasForeignKey("TenantId", "RoleId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Permission");
b.Navigation("Role");
@@ -1319,7 +1323,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("ExternalAccounts")
.HasForeignKey("UserId")
.HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1330,7 +1335,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("Identities")
.HasForeignKey("UserId")
.HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1341,7 +1347,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("MfaFactors")
.HasForeignKey("UserId")
.HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1352,7 +1359,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("PasswordHistories")
.HasForeignKey("UserId")
.HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1373,6 +1381,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Role", null)
.WithMany()
.HasForeignKey("TenantId", "RoleId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", null)
.WithMany()
.HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
@@ -1382,7 +1404,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("Sessions")
.HasForeignKey("UserId")
.HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1393,7 +1416,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Parent")
.WithMany("Children")
.HasForeignKey("ParentDepartmentId")
.HasForeignKey("TenantId", "ParentDepartmentId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Parent");
@@ -1403,7 +1427,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("EmergencyContacts")
.HasForeignKey("UserProfileId")
.HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1414,7 +1439,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("Addresses")
.HasForeignKey("UserProfileId")
.HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1425,7 +1451,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("BankAccounts")
.HasForeignKey("UserProfileId")
.HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1436,17 +1463,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Department")
.WithMany()
.HasForeignKey("DepartmentId")
.HasForeignKey("TenantId", "DepartmentId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Position", "Position")
.WithMany()
.HasForeignKey("PositionId")
.HasForeignKey("TenantId", "PositionId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("Employments")
.HasForeignKey("UserProfileId")
.HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1469,7 +1499,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithOne()
.HasForeignKey("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserId")
.HasForeignKey("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "TenantId", "UserId")
.HasPrincipalKey("AMREZ.EOP.Domain.Entities.Authentications.User", "TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

View File

@@ -19,10 +19,10 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
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),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
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_by = table.Column<string>(type: "text", 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 =>
{
table.PrimaryKey("PK_departments", x => x.Id);
table.UniqueConstraint("AK_departments_tenant_id_Id", x => new { x.tenant_id, x.Id });
table.CheckConstraint("ck_departments_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_departments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_departments_departments_ParentDepartmentId",
column: x => x.ParentDepartmentId,
name: "FK_departments_departments_tenant_id_ParentDepartmentId",
columns: x => new { x.tenant_id, x.ParentDepartmentId },
principalTable: "departments",
principalColumn: "Id",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Restrict);
});
@@ -46,9 +48,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
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_by = table.Column<string>(type: "text", 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 =>
{
table.PrimaryKey("PK_permissions", x => x.Id);
table.UniqueConstraint("AK_permissions_tenant_id_Id", x => new { x.tenant_id, x.Id });
table.CheckConstraint("ck_permissions_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
migrationBuilder.CreateTable(
@@ -66,10 +70,10 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
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_by = table.Column<string>(type: "text", 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 =>
{
table.PrimaryKey("PK_positions", x => x.Id);
table.UniqueConstraint("AK_positions_tenant_id_Id", x => new { x.tenant_id, x.Id });
table.CheckConstraint("ck_positions_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_positions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
migrationBuilder.CreateTable(
@@ -87,9 +93,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
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_by = table.Column<string>(type: "text", 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 =>
{
table.PrimaryKey("PK_roles", x => x.Id);
table.UniqueConstraint("AK_roles_tenant_id_Id", x => new { x.tenant_id, x.Id });
table.CheckConstraint("ck_roles_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
migrationBuilder.CreateTable(
@@ -108,6 +116,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
TenantKey = table.Column<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),
ConnectionString = table.Column<string>(type: "text", nullable: true),
Mode = table.Column<int>(type: "integer", nullable: false),
@@ -117,6 +126,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
constraints: table =>
{
table.PrimaryKey("PK_tenants", x => x.TenantKey);
table.UniqueConstraint("AK_tenants_TenantId", x => x.TenantId);
});
migrationBuilder.CreateTable(
@@ -124,13 +134,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
IsActive = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
AccessFailedCount = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
LockoutEndUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
MfaEnabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
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_by = table.Column<string>(type: "text", 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 =>
{
table.PrimaryKey("PK_users", x => x.Id);
table.UniqueConstraint("AK_users_tenant_id_Id", x => new { x.tenant_id, x.Id });
table.CheckConstraint("ck_users_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_users_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
migrationBuilder.CreateTable(
@@ -148,9 +160,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
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_by = table.Column<string>(type: "text", 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.CheckConstraint("ck_role_permissions_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_role_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_role_permissions_permissions_PermissionId",
column: x => x.PermissionId,
principalTable: "permissions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_role_permissions_permissions_tenant_id_PermissionId",
columns: x => new { x.tenant_id, x.PermissionId },
principalTable: "permissions",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_role_permissions_roles_RoleId",
column: x => x.RoleId,
principalTable: "roles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_role_permissions_roles_tenant_id_RoleId",
columns: x => new { x.tenant_id, x.RoleId },
principalTable: "roles",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
@@ -203,12 +228,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
Provider = table.Column<int>(type: "integer", nullable: false),
Subject = table.Column<string>(type: "text", nullable: false),
Email = table.Column<string>(type: "text", nullable: true),
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_by = table.Column<string>(type: "text", 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.CheckConstraint("ck_user_external_accounts_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_user_external_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_user_external_accounts_users_UserId",
column: x => x.UserId,
name: "FK_user_external_accounts_users_tenant_id_UserId",
columns: x => new { x.tenant_id, x.UserId },
principalTable: "users",
principalColumn: "Id",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
@@ -232,12 +258,12 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
Type = table.Column<int>(type: "integer", nullable: false),
Identifier = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
IsPrimary = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
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_by = table.Column<string>(type: "text", 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.CheckConstraint("ck_user_identities_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_user_identities_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_user_identities_users_UserId",
column: x => x.UserId,
name: "FK_user_identities_users_tenant_id_UserId",
columns: x => new { x.tenant_id, x.UserId },
principalTable: "users",
principalColumn: "Id",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
@@ -261,7 +288,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
Type = table.Column<int>(type: "integer", nullable: false),
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),
AddedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
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_by = table.Column<string>(type: "text", 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.CheckConstraint("ck_user_mfa_factors_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_user_mfa_factors_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_user_mfa_factors_users_UserId",
column: x => x.UserId,
name: "FK_user_mfa_factors_users_tenant_id_UserId",
columns: x => new { x.tenant_id, x.UserId },
principalTable: "users",
principalColumn: "Id",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
@@ -296,10 +324,10 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
PasswordHash = table.Column<string>(type: "text", 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_by = table.Column<string>(type: "text", 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.CheckConstraint("ck_user_password_histories_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_user_password_histories_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_user_password_histories_users_UserId",
column: x => x.UserId,
name: "FK_user_password_histories_users_tenant_id_UserId",
columns: x => new { x.tenant_id, x.UserId },
principalTable: "users",
principalColumn: "Id",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
@@ -323,7 +352,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
FirstName = 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),
DepartmentId = 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_by = table.Column<string>(type: "text", 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 =>
{
table.PrimaryKey("PK_user_profiles", x => x.Id);
table.UniqueConstraint("AK_user_profiles_tenant_id_Id", x => new { x.tenant_id, x.Id });
table.CheckConstraint("ck_user_profiles_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_user_profiles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_user_profiles_departments_DepartmentId",
column: x => x.DepartmentId,
@@ -354,10 +385,10 @@ namespace AMREZ.EOP.Infrastructures.Migrations
principalTable: "positions",
principalColumn: "Id");
table.ForeignKey(
name: "FK_user_profiles_users_UserId",
column: x => x.UserId,
name: "FK_user_profiles_users_tenant_id_UserId",
columns: x => new { x.tenant_id, x.UserId },
principalTable: "users",
principalColumn: "Id",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
@@ -366,9 +397,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
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_by = table.Column<string>(type: "text", 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.CheckConstraint("ck_user_roles_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_user_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_user_roles_roles_RoleId",
column: x => x.RoleId,
principalTable: "roles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_user_roles_roles_tenant_id_RoleId",
columns: x => new { x.tenant_id, x.RoleId },
principalTable: "roles",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_user_roles_users_UserId",
column: x => x.UserId,
principalTable: "users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_user_roles_users_tenant_id_UserId",
columns: x => new { x.tenant_id, x.UserId },
principalTable: "users",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
@@ -398,7 +442,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
RefreshTokenHash = table.Column<string>(type: "text", 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),
UserAgent = 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_by = table.Column<string>(type: "text", 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.CheckConstraint("ck_user_sessions_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_user_sessions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_user_sessions_users_UserId",
column: x => x.UserId,
name: "FK_user_sessions_users_tenant_id_UserId",
columns: x => new { x.tenant_id, x.UserId },
principalTable: "users",
principalColumn: "Id",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
@@ -430,13 +475,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Relationship = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Phone = table.Column<string>(type: "text", nullable: true),
Email = table.Column<string>(type: "text", nullable: true),
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_by = table.Column<string>(type: "text", 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.CheckConstraint("ck_emergency_contacts_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_emergency_contacts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_emergency_contacts_user_profiles_UserProfileId",
column: x => x.UserProfileId,
name: "FK_emergency_contacts_user_profiles_tenant_id_UserProfileId",
columns: x => new { x.tenant_id, x.UserProfileId },
principalTable: "user_profiles",
principalColumn: "Id",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
@@ -460,7 +506,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
Type = table.Column<int>(type: "integer", 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),
Country = table.Column<string>(type: "character varying(64)", maxLength: 64, 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_by = table.Column<string>(type: "text", 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.CheckConstraint("ck_employee_addresses_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_employee_addresses_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_employee_addresses_user_profiles_UserProfileId",
column: x => x.UserProfileId,
name: "FK_employee_addresses_user_profiles_tenant_id_UserProfileId",
columns: x => new { x.tenant_id, x.UserProfileId },
principalTable: "user_profiles",
principalColumn: "Id",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
@@ -493,7 +540,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
BankName = table.Column<string>(type: "character varying(128)", maxLength: 128, 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),
Note = table.Column<string>(type: "text", nullable: true),
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_by = table.Column<string>(type: "text", 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.CheckConstraint("ck_employee_bank_accounts_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_employee_bank_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_employee_bank_accounts_user_profiles_UserProfileId",
column: x => x.UserProfileId,
name: "FK_employee_bank_accounts_user_profiles_tenant_id_UserProfileId",
columns: x => new { x.tenant_id, x.UserProfileId },
principalTable: "user_profiles",
principalColumn: "Id",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
@@ -524,7 +572,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: table => new
{
Id = table.Column<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),
EmploymentType = table.Column<int>(type: "integer", 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),
WorkEmail = 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_by = table.Column<string>(type: "text", 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.CheckConstraint("ck_employments_tenant_not_null", "tenant_id is not null");
table.CheckConstraint("ck_employments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
table.ForeignKey(
name: "FK_employments_departments_DepartmentId",
column: x => x.DepartmentId,
name: "FK_employments_departments_tenant_id_DepartmentId",
columns: x => new { x.tenant_id, x.DepartmentId },
principalTable: "departments",
principalColumn: "Id",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_employments_positions_PositionId",
column: x => x.PositionId,
name: "FK_employments_positions_tenant_id_PositionId",
columns: x => new { x.tenant_id, x.PositionId },
principalTable: "positions",
principalColumn: "Id",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_employments_user_profiles_UserProfileId",
column: x => x.UserProfileId,
name: "FK_employments_user_profiles_tenant_id_UserProfileId",
columns: x => new { x.tenant_id, x.UserProfileId },
principalTable: "user_profiles",
principalColumn: "Id",
principalColumns: new[] { "tenant_id", "Id" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_departments_ParentDepartmentId",
table: "departments",
column: "ParentDepartmentId");
migrationBuilder.CreateIndex(
name: "IX_departments_tenant_id",
table: "departments",
@@ -580,6 +624,11 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: new[] { "tenant_id", "Code" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_departments_tenant_id_ParentDepartmentId",
table: "departments",
columns: new[] { "tenant_id", "ParentDepartmentId" });
migrationBuilder.CreateIndex(
name: "IX_emergency_contacts_tenant_id",
table: "emergency_contacts",
@@ -590,11 +639,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "emergency_contacts",
columns: new[] { "tenant_id", "UserProfileId", "IsPrimary" });
migrationBuilder.CreateIndex(
name: "IX_emergency_contacts_UserProfileId",
table: "emergency_contacts",
column: "UserProfileId");
migrationBuilder.CreateIndex(
name: "IX_employee_addresses_tenant_id",
table: "employee_addresses",
@@ -605,11 +649,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "employee_addresses",
columns: new[] { "tenant_id", "UserProfileId", "IsPrimary" });
migrationBuilder.CreateIndex(
name: "IX_employee_addresses_UserProfileId",
table: "employee_addresses",
column: "UserProfileId");
migrationBuilder.CreateIndex(
name: "IX_employee_bank_accounts_tenant_id",
table: "employee_bank_accounts",
@@ -620,36 +659,26 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "employee_bank_accounts",
columns: new[] { "tenant_id", "UserProfileId", "IsPrimary" });
migrationBuilder.CreateIndex(
name: "IX_employee_bank_accounts_UserProfileId",
table: "employee_bank_accounts",
column: "UserProfileId");
migrationBuilder.CreateIndex(
name: "IX_employments_DepartmentId",
table: "employments",
column: "DepartmentId");
migrationBuilder.CreateIndex(
name: "IX_employments_PositionId",
table: "employments",
column: "PositionId");
migrationBuilder.CreateIndex(
name: "IX_employments_tenant_id",
table: "employments",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "IX_employments_tenant_id_DepartmentId",
table: "employments",
columns: new[] { "tenant_id", "DepartmentId" });
migrationBuilder.CreateIndex(
name: "IX_employments_tenant_id_PositionId",
table: "employments",
columns: new[] { "tenant_id", "PositionId" });
migrationBuilder.CreateIndex(
name: "IX_employments_tenant_id_UserProfileId_StartDate",
table: "employments",
columns: new[] { "tenant_id", "UserProfileId", "StartDate" });
migrationBuilder.CreateIndex(
name: "IX_employments_UserProfileId",
table: "employments",
column: "UserProfileId");
migrationBuilder.CreateIndex(
name: "IX_permissions_tenant_id",
table: "permissions",
@@ -687,6 +716,11 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "role_permissions",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "IX_role_permissions_tenant_id_PermissionId",
table: "role_permissions",
columns: new[] { "tenant_id", "PermissionId" });
migrationBuilder.CreateIndex(
name: "IX_role_permissions_tenant_id_RoleId_PermissionId",
table: "role_permissions",
@@ -728,6 +762,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "tenants",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_tenants_TenantId",
schema: "meta",
table: "tenants",
column: "TenantId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_user_external_accounts_tenant_id",
table: "user_external_accounts",
@@ -740,9 +781,9 @@ namespace AMREZ.EOP.Infrastructures.Migrations
unique: true);
migrationBuilder.CreateIndex(
name: "IX_user_external_accounts_UserId",
name: "IX_user_external_accounts_tenant_id_UserId",
table: "user_external_accounts",
column: "UserId");
columns: new[] { "tenant_id", "UserId" });
migrationBuilder.CreateIndex(
name: "IX_user_identities_tenant_id",
@@ -755,11 +796,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: new[] { "tenant_id", "Type", "Identifier" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_user_identities_UserId",
table: "user_identities",
column: "UserId");
migrationBuilder.CreateIndex(
name: "ix_user_identity_primary_per_type",
table: "user_identities",
@@ -775,11 +811,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "user_mfa_factors",
columns: new[] { "tenant_id", "UserId" });
migrationBuilder.CreateIndex(
name: "IX_user_mfa_factors_UserId",
table: "user_mfa_factors",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_user_password_histories_tenant_id",
table: "user_password_histories",
@@ -790,11 +821,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "user_password_histories",
columns: new[] { "tenant_id", "UserId", "ChangedAt" });
migrationBuilder.CreateIndex(
name: "IX_user_password_histories_UserId",
table: "user_password_histories",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_user_profiles_DepartmentId",
table: "user_profiles",
@@ -816,12 +842,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
columns: new[] { "tenant_id", "UserId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_user_profiles_UserId",
table: "user_profiles",
column: "UserId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_user_roles_RoleId",
table: "user_roles",
@@ -832,6 +852,11 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "user_roles",
column: "tenant_id");
migrationBuilder.CreateIndex(
name: "IX_user_roles_tenant_id_RoleId",
table: "user_roles",
columns: new[] { "tenant_id", "RoleId" });
migrationBuilder.CreateIndex(
name: "IX_user_roles_tenant_id_UserId_RoleId",
table: "user_roles",
@@ -858,11 +883,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
table: "user_sessions",
columns: new[] { "tenant_id", "UserId" });
migrationBuilder.CreateIndex(
name: "IX_user_sessions_UserId",
table: "user_sessions",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_users_tenant_id",
table: "users",

View File

@@ -55,10 +55,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("character varying(256)");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -78,6 +76,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("permissions", null, t =>
{
t.HasCheckConstraint("ck_permissions_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -114,10 +114,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("character varying(256)");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -137,6 +135,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("roles", null, t =>
{
t.HasCheckConstraint("ck_roles_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -169,10 +169,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("uuid");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -190,12 +188,16 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("TenantId", "PermissionId");
b.HasIndex("TenantId", "RoleId", "PermissionId")
.IsUnique();
b.ToTable("role_permissions", null, t =>
{
t.HasCheckConstraint("ck_role_permissions_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_role_permissions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -247,10 +249,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("text");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -267,6 +267,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("users", null, t =>
{
t.HasCheckConstraint("ck_users_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_users_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -306,10 +308,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("text");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -326,7 +326,7 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "UserId");
b.HasIndex("TenantId", "Provider", "Subject")
.IsUnique();
@@ -334,6 +334,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("user_external_accounts", null, t =>
{
t.HasCheckConstraint("ck_user_external_accounts_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_external_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -370,10 +372,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasDefaultValue(false);
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<int>("Type")
.HasColumnType("integer");
@@ -396,8 +396,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "Type", "Identifier")
.IsUnique();
@@ -407,6 +405,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("user_identities", null, t =>
{
t.HasCheckConstraint("ck_user_identities_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_identities_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -462,10 +462,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("text");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<int>("Type")
.HasColumnType("integer");
@@ -485,13 +483,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "UserId");
b.ToTable("user_mfa_factors", null, t =>
{
t.HasCheckConstraint("ck_user_mfa_factors_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_mfa_factors_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -525,10 +523,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("text");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -545,13 +541,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "UserId", "ChangedAt");
b.ToTable("user_password_histories", null, t =>
{
t.HasCheckConstraint("ck_user_password_histories_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_password_histories_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -581,10 +577,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("uuid");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -605,12 +599,16 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("UserId");
b.HasIndex("TenantId", "RoleId");
b.HasIndex("TenantId", "UserId", "RoleId")
.IsUnique();
b.ToTable("user_roles", null, t =>
{
t.HasCheckConstraint("ck_user_roles_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_roles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -656,10 +654,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -679,8 +675,6 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("TenantId", "DeviceId");
b.HasIndex("TenantId", "UserId");
@@ -688,6 +682,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("user_sessions", null, t =>
{
t.HasCheckConstraint("ck_user_sessions_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_sessions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -727,10 +723,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("uuid");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -742,16 +736,18 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasKey("Id");
b.HasIndex("ParentDepartmentId");
b.HasIndex("TenantId");
b.HasIndex("TenantId", "Code")
.IsUnique();
b.HasIndex("TenantId", "ParentDepartmentId");
b.ToTable("departments", null, t =>
{
t.HasCheckConstraint("ck_departments_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_departments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -797,10 +793,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("character varying(64)");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -817,13 +811,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "UserProfileId", "IsPrimary");
b.ToTable("emergency_contacts", null, t =>
{
t.HasCheckConstraint("ck_emergency_contacts_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_emergency_contacts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -879,10 +873,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("text");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<int>("Type")
.HasColumnType("integer");
@@ -902,13 +894,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "UserProfileId", "IsPrimary");
b.ToTable("employee_addresses", null, t =>
{
t.HasCheckConstraint("ck_employee_addresses_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_employee_addresses_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -959,10 +951,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("text");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -979,13 +969,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "UserProfileId", "IsPrimary");
b.ToTable("employee_bank_accounts", null, t =>
{
t.HasCheckConstraint("ck_employee_bank_accounts_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_employee_bank_accounts_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -1030,10 +1020,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -1054,19 +1042,19 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasKey("Id");
b.HasIndex("DepartmentId");
b.HasIndex("PositionId");
b.HasIndex("TenantId");
b.HasIndex("UserProfileId");
b.HasIndex("TenantId", "DepartmentId");
b.HasIndex("TenantId", "PositionId");
b.HasIndex("TenantId", "UserProfileId", "StartDate");
b.ToTable("employments", null, t =>
{
t.HasCheckConstraint("ck_employments_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_employments_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -1101,10 +1089,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("integer");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<string>("Title")
.IsRequired()
@@ -1129,6 +1115,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.ToTable("positions", null, t =>
{
t.HasCheckConstraint("ck_positions_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_positions_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -1183,10 +1171,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.HasColumnType("uuid");
b.Property<Guid>("TenantId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("tenant_id")
.HasDefaultValueSql("nullif(current_setting('app.tenant_id', true),'')::uuid");
.HasColumnName("tenant_id");
b.Property<DateTimeOffset?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
@@ -1207,15 +1193,14 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasIndex("TenantId");
b.HasIndex("UserId")
.IsUnique();
b.HasIndex("TenantId", "UserId")
.IsUnique();
b.ToTable("user_profiles", null, t =>
{
t.HasCheckConstraint("ck_user_profiles_tenant_not_null", "tenant_id is not null");
t.HasCheckConstraint("ck_user_profiles_tenant_not_zero", "tenant_id <> '00000000-0000-0000-0000-000000000000'");
});
});
@@ -1251,8 +1236,13 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasKey("TenantKey");
b.HasAlternateKey("TenantId");
b.HasIndex("IsActive");
b.HasIndex("TenantId")
.IsUnique();
b.ToTable("tenants", "meta");
});
@@ -1307,6 +1297,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Permission", null)
.WithMany()
.HasForeignKey("TenantId", "PermissionId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Role", null)
.WithMany()
.HasForeignKey("TenantId", "RoleId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Permission");
b.Navigation("Role");
@@ -1316,7 +1320,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("ExternalAccounts")
.HasForeignKey("UserId")
.HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1327,7 +1332,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("Identities")
.HasForeignKey("UserId")
.HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1338,7 +1344,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("MfaFactors")
.HasForeignKey("UserId")
.HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1349,7 +1356,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("PasswordHistories")
.HasForeignKey("UserId")
.HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1370,6 +1378,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.Role", null)
.WithMany()
.HasForeignKey("TenantId", "RoleId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", null)
.WithMany()
.HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
@@ -1379,7 +1401,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithMany("Sessions")
.HasForeignKey("UserId")
.HasForeignKey("TenantId", "UserId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1390,7 +1413,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Parent")
.WithMany("Children")
.HasForeignKey("ParentDepartmentId")
.HasForeignKey("TenantId", "ParentDepartmentId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Parent");
@@ -1400,7 +1424,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("EmergencyContacts")
.HasForeignKey("UserProfileId")
.HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1411,7 +1436,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("Addresses")
.HasForeignKey("UserProfileId")
.HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1422,7 +1448,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("BankAccounts")
.HasForeignKey("UserProfileId")
.HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1433,17 +1460,20 @@ namespace AMREZ.EOP.Infrastructures.Migrations
{
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Department", "Department")
.WithMany()
.HasForeignKey("DepartmentId")
.HasForeignKey("TenantId", "DepartmentId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.Position", "Position")
.WithMany()
.HasForeignKey("PositionId")
.HasForeignKey("TenantId", "PositionId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserProfile")
.WithMany("Employments")
.HasForeignKey("UserProfileId")
.HasForeignKey("TenantId", "UserProfileId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1466,7 +1496,8 @@ namespace AMREZ.EOP.Infrastructures.Migrations
b.HasOne("AMREZ.EOP.Domain.Entities.Authentications.User", "User")
.WithOne()
.HasForeignKey("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "UserId")
.HasForeignKey("AMREZ.EOP.Domain.Entities.HumanResources.UserProfile", "TenantId", "UserId")
.HasPrincipalKey("AMREZ.EOP.Domain.Entities.Authentications.User", "TenantId", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

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.Storage;
using AMREZ.EOP.Domain.Entities.Tenancy;
using AMREZ.EOP.Infrastructures.Data;
using AMREZ.EOP.Infrastructures.Options;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
@@ -11,9 +13,19 @@ namespace AMREZ.EOP.Infrastructures.Repositories;
public sealed class TenantRepository : ITenantRepository
{
private readonly IDbScope _scope;
public TenantRepository(IDbScope scope) => _scope = scope;
private readonly ITenantResolver _resolver;
private readonly IHttpContextAccessor _http;
private AppDbContext Db() => _scope.Get<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();
@@ -51,7 +63,7 @@ public sealed class TenantRepository : ITenantRepository
if (cur is null) return false;
if (ifUnmodifiedSince.HasValue && cur.UpdatedAtUtc > ifUnmodifiedSince.Value)
return false; // 412
return false;
cur.Schema = row.Schema is null ? cur.Schema : (string.IsNullOrWhiteSpace(row.Schema) ? null : row.Schema.Trim());
cur.ConnectionString = row.ConnectionString is null ? cur.ConnectionString : (string.IsNullOrWhiteSpace(row.ConnectionString) ? null : row.ConnectionString.Trim());
@@ -70,7 +82,6 @@ public sealed class TenantRepository : ITenantRepository
var t = await db.Set<TenantConfig>().FirstOrDefaultAsync(x => x.TenantKey == key, ct);
if (t is null) return false;
// ❌ ไม่ลบนะ — ✅ deactivate
if (t.IsActive)
{
t.IsActive = false;
@@ -120,7 +131,7 @@ public sealed class TenantRepository : ITenantRepository
if (ex is null) return false;
ex.IsActive = false;
ex.TenantKey = null;
ex.TenantKey = null;
ex.IsPlatformBaseDomain = false;
ex.UpdatedAtUtc = DateTimeOffset.UtcNow;

View File

@@ -2,6 +2,7 @@ using AMREZ.EOP.Abstractions.Applications.Tenancy;
using AMREZ.EOP.Abstractions.Infrastructures.Repositories;
using AMREZ.EOP.Abstractions.Storage;
using AMREZ.EOP.Domain.Entities.HumanResources;
using AMREZ.EOP.Domain.Entities.Tenancy;
using AMREZ.EOP.Infrastructures.Data;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
@@ -19,22 +20,33 @@ public class UserProfileRepository : IUserProfileRepository
private Guid TenantId()
{
var http = _http.HttpContext;
var tc = http is not null ? _tenantResolver.Resolve(http) : null;
return Guid.TryParse(tc?.Id, out var g) ? g : Guid.Empty;
var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext");
var tc = _tenantResolver.Resolve(http) ?? throw new InvalidOperationException("No tenant");
_scope.EnsureForTenant(tc);
if (!string.IsNullOrWhiteSpace(tc.Id) && Guid.TryParse(tc.Id, out var g) && g != Guid.Empty)
return g;
var key = (http.Items.TryGetValue("TargetTenantKey", out var v) ? v as string : null) ?? tc.TenantKey;
if (string.IsNullOrWhiteSpace(key)) throw new InvalidOperationException("Tenant key missing");
var db = _scope.Get<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)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
return await db.UserProfiles.FirstOrDefaultAsync(p => p.TenantId == tid && p.UserId == userId, ct);
}
public async Task UpsertAsync(UserProfile profile, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var existing = await db.UserProfiles.FirstOrDefaultAsync(p => p.TenantId == tid && p.UserId == profile.UserId, ct);
if (existing is null)
@@ -56,8 +68,9 @@ public class UserProfileRepository : IUserProfileRepository
public async Task<Employment> AddEmploymentAsync(Employment e, CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
e.TenantId = TenantId();
e.TenantId = tid;
await db.Employments.AddAsync(e, ct);
await db.SaveChangesAsync(ct);
return e;
@@ -65,8 +78,8 @@ public class UserProfileRepository : IUserProfileRepository
public async Task EndEmploymentAsync(Guid employmentId, DateTime endDate, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var e = await db.Employments.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == employmentId, ct);
if (e is null) return;
e.EndDate = endDate;
@@ -75,8 +88,9 @@ public class UserProfileRepository : IUserProfileRepository
public async Task<EmployeeAddress> AddAddressAsync(EmployeeAddress a, CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
a.TenantId = TenantId();
a.TenantId = tid;
await db.EmployeeAddresses.AddAsync(a, ct);
await db.SaveChangesAsync(ct);
return a;
@@ -84,8 +98,8 @@ public class UserProfileRepository : IUserProfileRepository
public async Task SetPrimaryAddressAsync(Guid userProfileId, Guid addressId, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var all = await db.EmployeeAddresses.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct);
foreach (var x in all) x.IsPrimary = false;
@@ -98,8 +112,9 @@ public class UserProfileRepository : IUserProfileRepository
public async Task<EmergencyContact> AddEmergencyContactAsync(EmergencyContact c, CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
c.TenantId = TenantId();
c.TenantId = tid;
await db.EmergencyContacts.AddAsync(c, ct);
await db.SaveChangesAsync(ct);
return c;
@@ -107,8 +122,8 @@ public class UserProfileRepository : IUserProfileRepository
public async Task SetPrimaryEmergencyContactAsync(Guid userProfileId, Guid contactId, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var all = await db.EmergencyContacts.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct);
foreach (var x in all) x.IsPrimary = false;
@@ -121,8 +136,9 @@ public class UserProfileRepository : IUserProfileRepository
public async Task<EmployeeBankAccount> AddBankAccountAsync(EmployeeBankAccount b, CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
b.TenantId = TenantId();
b.TenantId = tid;
await db.EmployeeBankAccounts.AddAsync(b, ct);
await db.SaveChangesAsync(ct);
return b;
@@ -130,8 +146,8 @@ public class UserProfileRepository : IUserProfileRepository
public async Task SetPrimaryBankAccountAsync(Guid userProfileId, Guid bankAccountId, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var all = await db.EmployeeBankAccounts.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct);
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.Storage;
using AMREZ.EOP.Domain.Entities.Authentications;
using AMREZ.EOP.Domain.Entities.Tenancy;
using AMREZ.EOP.Domain.Shared._Users;
using AMREZ.EOP.Infrastructures.Data;
using Microsoft.AspNetCore.Http;
@@ -25,16 +26,27 @@ public class UserRepository : IUserRepository
IHttpContextAccessor http)
{
_scope = scope;
_redis = redis; // null = ไม่มี/ต่อไม่ได้ → ข้าม cache
_redis = redis;
_tenantResolver = tenantResolver;
_http = http;
}
private Guid TenantId()
{
var http = _http.HttpContext;
var tc = http is not null ? _tenantResolver.Resolve(http) : null;
return Guid.TryParse(tc?.Id, out var g) ? g : Guid.Empty;
var http = _http.HttpContext ?? throw new InvalidOperationException("No HttpContext");
var tc = _tenantResolver.Resolve(http) ?? throw new InvalidOperationException("No tenant");
_scope.EnsureForTenant(tc);
if (!string.IsNullOrWhiteSpace(tc.Id) && Guid.TryParse(tc.Id, out var g) && g != Guid.Empty)
return g;
var key = (http.Items.TryGetValue("TargetTenantKey", out var v) ? v as string : null) ?? tc.Id;
if (string.IsNullOrWhiteSpace(key)) throw new InvalidOperationException("Tenant key missing");
var db = _scope.Get<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)
@@ -45,8 +57,8 @@ public class UserRepository : IUserRepository
public async Task<User?> FindByIdAsync(Guid userId, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
return await db.Users.AsNoTracking()
.FirstOrDefaultAsync(u => u.TenantId == tid && u.Id == userId, ct);
}
@@ -57,7 +69,6 @@ public class UserRepository : IUserRepository
var tidStr = tid.ToString();
var r = _redis?.GetDatabase();
// 1) Redis
if (r is not null)
{
try
@@ -69,10 +80,9 @@ public class UserRepository : IUserRepository
if (hit?.IsActive == true) return hit;
}
}
catch { /* ignore → query DB */ }
catch { }
}
// 2) EF: join identities
var db = _scope.Get<AppDbContext>();
var norm = email.Trim().ToLowerInvariant();
@@ -86,7 +96,6 @@ public class UserRepository : IUserRepository
i.Identifier == norm))
.FirstOrDefaultAsync(ct);
// 3) cache
if (user is not null && r is not null)
{
try
@@ -96,7 +105,7 @@ public class UserRepository : IUserRepository
await r.StringSetAsync(IdentityEmailKey(tidStr, norm), payload, ttl);
await r.StringSetAsync(UserIdKey(tidStr, user.Id), payload, ttl);
}
catch { /* ignore */ }
catch { }
}
return user;
@@ -104,8 +113,8 @@ public class UserRepository : IUserRepository
public async Task<bool> EmailExistsAsync(string email, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var norm = email.Trim().ToLowerInvariant();
return await db.UserIdentities.AsNoTracking()
@@ -114,40 +123,36 @@ public class UserRepository : IUserRepository
public async Task AddAsync(User user, CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
user.TenantId = TenantId();
user.TenantId = tid;
await db.Users.AddAsync(user, ct);
await db.SaveChangesAsync(ct);
// cache
var r = _redis?.GetDatabase();
if (r is not null)
{
try
{
var tid = user.TenantId.ToString();
var payload = JsonSerializer.Serialize(user);
var ttl = TimeSpan.FromMinutes(5);
await r.StringSetAsync(UserIdKey(tid, user.Id), payload, ttl);
await r.StringSetAsync(UserIdKey(tid.ToString(), user.Id), payload, ttl);
var email = user.Identities
.FirstOrDefault(i => i.Type == IdentityType.Email && i.IsPrimary)?.Identifier;
if (!string.IsNullOrWhiteSpace(email))
await r.StringSetAsync(IdentityEmailKey(tid, email!), payload, ttl);
await r.StringSetAsync(IdentityEmailKey(tid.ToString(), email!), payload, ttl);
}
catch { /* ignore */ }
catch { }
}
}
// ========= Identities =========
// (1) IMPLEMENTED: AddIdentityAsync
public async Task AddIdentityAsync(Guid userId, IdentityType type, string identifier, bool isPrimary, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var norm = identifier.Trim().ToLowerInvariant();
if (isPrimary)
@@ -171,11 +176,10 @@ public class UserRepository : IUserRepository
await db.SaveChangesAsync(ct);
}
// (2) IMPLEMENTED: VerifyIdentityAsync
public async Task VerifyIdentityAsync(Guid userId, IdentityType type, string identifier, DateTimeOffset verifiedAt, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var norm = identifier.Trim().ToLowerInvariant();
var id = await db.UserIdentities
@@ -190,11 +194,10 @@ public class UserRepository : IUserRepository
await db.SaveChangesAsync(ct);
}
// (3) IMPLEMENTED: GetPrimaryIdentityAsync
public async Task<UserIdentity?> GetPrimaryIdentityAsync(Guid userId, IdentityType type, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
return await db.UserIdentities.AsNoTracking()
.FirstOrDefaultAsync(i =>
@@ -204,12 +207,10 @@ public class UserRepository : IUserRepository
i.IsPrimary, ct);
}
// ========= Password =========
public async Task ChangePasswordAsync(Guid userId, string newPasswordHash, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var user = await db.Users.FirstOrDefaultAsync(u => u.TenantId == tid && u.Id == userId, ct);
if (user is null) return;
@@ -221,8 +222,8 @@ public class UserRepository : IUserRepository
public async Task AddPasswordHistoryAsync(Guid userId, string passwordHash, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var h = new UserPasswordHistory
{
@@ -236,12 +237,10 @@ public class UserRepository : IUserRepository
await db.SaveChangesAsync(ct);
}
// ========= MFA =========
public async Task<UserMfaFactor> AddTotpFactorAsync(Guid userId, string label, string secret, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var f = new UserMfaFactor
{
@@ -265,8 +264,8 @@ public class UserRepository : IUserRepository
public async Task DisableMfaFactorAsync(Guid factorId, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var f = await db.UserMfaFactors.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == factorId, ct);
if (f is null) return;
@@ -285,26 +284,45 @@ public class UserRepository : IUserRepository
public async Task<bool> HasAnyMfaAsync(Guid userId, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
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)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
session.TenantId = TenantId();
session.TenantId = tid;
await db.UserSessions.AddAsync(session, ct);
await db.SaveChangesAsync(ct);
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>();
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 db = _scope.Get<AppDbContext>();
var s = await db.UserSessions.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == sessionId && x.UserId == userId, ct);
if (s is null) return 0;
@@ -315,8 +333,8 @@ public class UserRepository : IUserRepository
public async Task<int> RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var sessions = await db.UserSessions
.Where(x => x.TenantId == tid && x.UserId == userId && x.RevokedAt == null)
@@ -326,4 +344,61 @@ public class UserRepository : IUserRepository
return await db.SaveChangesAsync(ct);
}
public async Task<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 path = http.Request.Path.Value ?? string.Empty;
// ✅ ทางลัด: ขอ platform ตรง ๆ ด้วย hint
if (hint is string hs && string.Equals(hs, "@platform", StringComparison.OrdinalIgnoreCase))
{
if (_map.TryGetBySlug(PlatformSlug, out var platform))

View File

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

View File

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