Add Master Data

This commit is contained in:
Thanakarn Klangkasame
2025-10-10 16:22:06 +07:00
parent ad0d9e41ba
commit d4ab1cb592
55 changed files with 16058 additions and 197 deletions

View File

@@ -65,47 +65,16 @@ public class UserRepository : IUserRepository
public async Task<User?> FindActiveByEmailAsync(string email, CancellationToken ct = default)
{
var r = _redis?.GetDatabase();
var norm = email.Trim().ToLowerInvariant();
if (r is not null)
{
try
{
var cached = await r.StringGetAsync($"uidx:{norm}");
if (cached.HasValue)
{
var hit = JsonSerializer.Deserialize<User>(cached!);
if (hit?.IsActive == true) return hit;
}
}
catch { /* ignore cache errors */ }
}
var db = _scope.Get<AppDbContext>();
var user = await (
from u in db.Users.AsNoTracking()
join i in db.UserIdentities.AsNoTracking() on u.Id equals i.UserId
where u.IsActive
&& i.Type == IdentityType.Email
&& i.Identifier == norm
select u
).FirstOrDefaultAsync(ct);
if (user is not null && r is not null)
{
try
{
var payload = JsonSerializer.Serialize(user);
var ttl = TimeSpan.FromMinutes(5);
await r.StringSetAsync($"uidx:{norm}", payload, ttl);
await r.StringSetAsync($"user:{user.Id:N}", payload, ttl);
}
catch { /* ignore cache errors */ }
}
return user;
return await db.Users
.AsNoTracking()
.Include(u => u.UserRoles)
.ThenInclude(ur => ur.Role)
.Where(u => u.IsActive &&
u.Identities.Any(i => i.Type == IdentityType.Email && i.Identifier == norm))
.FirstOrDefaultAsync(ct);
}
public async Task<bool> EmailExistsAsync(string email, CancellationToken ct = default)
@@ -142,11 +111,61 @@ public class UserRepository : IUserRepository
if (!string.IsNullOrWhiteSpace(email))
await r.StringSetAsync(IdentityEmailKey(tid.ToString(), email!), payload, ttl);
}
catch { }
catch
{
}
}
}
public async Task AddIdentityAsync(Guid userId, IdentityType type, string identifier, bool isPrimary, CancellationToken ct = default)
public async Task<string[]> GetRoleCodesByUserIdAsync(Guid userId, Guid tenantId, CancellationToken ct = default)
{
var r = _redis?.GetDatabase();
var cacheKey = $"urole:{tenantId:N}:{userId:N}";
// ---- cache hit ----
if (r is not null)
{
try
{
var cached = await r.StringGetAsync(cacheKey);
if (cached.HasValue)
{
var arr = JsonSerializer.Deserialize<string[]>(cached!) ?? Array.Empty<string>();
if (arr.Length > 0) return arr;
}
}
catch { /* ignore cache errors */ }
}
var db = _scope.Get<AppDbContext>();
// NOTE:
// - ไม่อ้างอิง r.TenantId เพื่อให้คอมไพล์ได้แม้ Role ยังไม่มีฟิลด์ TenantId
// - ถ้าคุณเพิ่ม Role.TenantId แล้ว และต้องการกรองตาม tenant ให้เติม .Where(role => role.TenantId == tenantId)
var roles = await db.UserRoles
.AsNoTracking()
.Where(ur => ur.UserId == userId)
.Select(ur => ur.Role)
.Where(role => role != null)
.Select(role => role!.Code) // ใช้ Code เป็นค่าของ claim "role"
.Distinct()
.ToArrayAsync(ct);
// ---- cache miss → set ----
if (r is not null)
{
try
{
await r.StringSetAsync(cacheKey, JsonSerializer.Serialize(roles), TimeSpan.FromMinutes(5));
}
catch { /* ignore cache errors */ }
}
return roles;
}
public async Task AddIdentityAsync(Guid userId, IdentityType type, string identifier, bool isPrimary,
CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
@@ -162,18 +181,19 @@ public class UserRepository : IUserRepository
var entity = new UserIdentity
{
TenantId = tid,
UserId = userId,
Type = type,
TenantId = tid,
UserId = userId,
Type = type,
Identifier = norm,
IsPrimary = isPrimary
IsPrimary = isPrimary
};
await db.UserIdentities.AddAsync(entity, ct);
await db.SaveChangesAsync(ct);
}
public async Task VerifyIdentityAsync(Guid userId, IdentityType type, string identifier, DateTimeOffset verifiedAt, CancellationToken ct = default)
public async Task VerifyIdentityAsync(Guid userId, IdentityType type, string identifier, DateTimeOffset verifiedAt,
CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
@@ -191,7 +211,8 @@ public class UserRepository : IUserRepository
await db.SaveChangesAsync(ct);
}
public async Task<UserIdentity?> GetPrimaryIdentityAsync(Guid userId, IdentityType type, CancellationToken ct = default)
public async Task<UserIdentity?> GetPrimaryIdentityAsync(Guid userId, IdentityType type,
CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
@@ -234,7 +255,8 @@ public class UserRepository : IUserRepository
await db.SaveChangesAsync(ct);
}
public async Task<UserMfaFactor> AddTotpFactorAsync(Guid userId, string label, string secret, CancellationToken ct = default)
public async Task<UserMfaFactor> AddTotpFactorAsync(Guid userId, string label, string secret,
CancellationToken ct = default)
{
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
@@ -294,7 +316,8 @@ public class UserRepository : IUserRepository
return session;
}
public async Task<UserSession?> FindSessionByRefreshHashAsync(Guid tenantId, string refreshTokenHash, CancellationToken ct = default)
public async Task<UserSession?> FindSessionByRefreshHashAsync(Guid tenantId, string refreshTokenHash,
CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
return await db.UserSessions
@@ -302,7 +325,8 @@ public class UserRepository : IUserRepository
.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)
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);
@@ -319,7 +343,8 @@ public class UserRepository : IUserRepository
var tid = TenantId();
var db = _scope.Get<AppDbContext>();
var s = await db.UserSessions.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == sessionId && x.UserId == userId, ct);
var s = await db.UserSessions.FirstOrDefaultAsync(
x => x.TenantId == tid && x.Id == sessionId && x.UserId == userId, ct);
if (s is null) return 0;
s.RevokedAt = DateTimeOffset.UtcNow;