Files
amrez-nova-eop-services-api/AMREZ.EOP.Infrastructures/Repositories/UserRepository.cs
Thanakarn Klangkasame 92e614674c Init Git
2025-09-30 11:01:02 +07:00

329 lines
11 KiB
C#

using System.Text.Json;
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.Shared._Users;
using AMREZ.EOP.Infrastructures.Data;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
namespace AMREZ.EOP.Infrastructures.Repositories;
public class UserRepository : IUserRepository
{
private readonly IDbScope _scope;
private readonly IConnectionMultiplexer? _redis;
private readonly ITenantResolver _tenantResolver;
private readonly IHttpContextAccessor _http;
public UserRepository(
IDbScope scope,
IConnectionMultiplexer? redis,
ITenantResolver tenantResolver,
IHttpContextAccessor http)
{
_scope = scope;
_redis = redis; // null = ไม่มี/ต่อไม่ได้ → ข้าม cache
_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;
}
private static string IdentityEmailKey(string tenantId, string email)
=> $"eop:{tenantId}:user:identity:email:{email.Trim().ToLowerInvariant()}";
private static string UserIdKey(string tenantId, Guid id)
=> $"eop:{tenantId}:user:id:{id:N}";
public async Task<User?> FindByIdAsync(Guid userId, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
return await db.Users.AsNoTracking()
.FirstOrDefaultAsync(u => u.TenantId == tid && u.Id == userId, ct);
}
public async Task<User?> FindActiveByEmailAsync(string email, CancellationToken ct = default)
{
var tid = TenantId();
var tidStr = tid.ToString();
var r = _redis?.GetDatabase();
// 1) Redis
if (r is not null)
{
try
{
var cached = await r.StringGetAsync(IdentityEmailKey(tidStr, email));
if (cached.HasValue)
{
var hit = JsonSerializer.Deserialize<User>(cached!);
if (hit?.IsActive == true) return hit;
}
}
catch { /* ignore → query DB */ }
}
// 2) EF: join identities
var db = _scope.Get<AppDbContext>();
var norm = email.Trim().ToLowerInvariant();
var user = await db.Users
.AsNoTracking()
.Where(u => u.TenantId == tid && u.IsActive)
.Where(u => db.UserIdentities.Any(i =>
i.TenantId == tid &&
i.UserId == u.Id &&
i.Type == IdentityType.Email &&
i.Identifier == norm))
.FirstOrDefaultAsync(ct);
// 3) cache
if (user is not null && r is not null)
{
try
{
var payload = JsonSerializer.Serialize(user);
var ttl = TimeSpan.FromMinutes(5);
await r.StringSetAsync(IdentityEmailKey(tidStr, norm), payload, ttl);
await r.StringSetAsync(UserIdKey(tidStr, user.Id), payload, ttl);
}
catch { /* ignore */ }
}
return user;
}
public async Task<bool> EmailExistsAsync(string email, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var norm = email.Trim().ToLowerInvariant();
return await db.UserIdentities.AsNoTracking()
.AnyAsync(i => i.TenantId == tid && i.Type == IdentityType.Email && i.Identifier == norm, ct);
}
public async Task AddAsync(User user, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
user.TenantId = TenantId();
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);
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);
}
catch { /* ignore */ }
}
}
// ========= 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 norm = identifier.Trim().ToLowerInvariant();
if (isPrimary)
{
var existingPrimary = await db.UserIdentities
.Where(i => i.TenantId == tid && i.UserId == userId && i.Type == type && i.IsPrimary)
.ToListAsync(ct);
foreach (var x in existingPrimary) x.IsPrimary = false;
}
var entity = new UserIdentity
{
TenantId = tid,
UserId = userId,
Type = type,
Identifier = norm,
IsPrimary = isPrimary
};
await db.UserIdentities.AddAsync(entity, ct);
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 norm = identifier.Trim().ToLowerInvariant();
var id = await db.UserIdentities
.FirstOrDefaultAsync(i =>
i.TenantId == tid &&
i.UserId == userId &&
i.Type == type &&
i.Identifier == norm, ct);
if (id is null) return;
id.VerifiedAt = verifiedAt;
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();
return await db.UserIdentities.AsNoTracking()
.FirstOrDefaultAsync(i =>
i.TenantId == tid &&
i.UserId == userId &&
i.Type == type &&
i.IsPrimary, ct);
}
// ========= Password =========
public async Task ChangePasswordAsync(Guid userId, string newPasswordHash, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var user = await db.Users.FirstOrDefaultAsync(u => u.TenantId == tid && u.Id == userId, ct);
if (user is null) return;
user.PasswordHash = newPasswordHash;
user.SecurityStamp = Guid.NewGuid().ToString("N");
await db.SaveChangesAsync(ct);
}
public async Task AddPasswordHistoryAsync(Guid userId, string passwordHash, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var h = new UserPasswordHistory
{
TenantId = tid,
UserId = userId,
PasswordHash = passwordHash,
ChangedAt = DateTimeOffset.UtcNow
};
await db.UserPasswordHistories.AddAsync(h, ct);
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 f = new UserMfaFactor
{
TenantId = tid,
UserId = userId,
Type = MfaType.Totp,
Label = label,
Secret = secret,
Enabled = true,
AddedAt = DateTimeOffset.UtcNow
};
await db.UserMfaFactors.AddAsync(f, ct);
var u = await db.Users.FirstAsync(x => x.TenantId == tid && x.Id == userId, ct);
u.MfaEnabled = true;
await db.SaveChangesAsync(ct);
return f;
}
public async Task DisableMfaFactorAsync(Guid factorId, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var f = await db.UserMfaFactors.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == factorId, ct);
if (f is null) return;
f.Enabled = false;
var still = await db.UserMfaFactors.AnyAsync(x => x.TenantId == tid && x.UserId == f.UserId && x.Enabled, ct);
if (!still)
{
var u = await db.Users.FirstAsync(x => x.TenantId == tid && x.Id == f.UserId, ct);
u.MfaEnabled = false;
}
await db.SaveChangesAsync(ct);
}
public async Task<bool> HasAnyMfaAsync(Guid userId, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
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 db = _scope.Get<AppDbContext>();
session.TenantId = TenantId();
await db.UserSessions.AddAsync(session, ct);
await db.SaveChangesAsync(ct);
return session;
}
public async Task<int> RevokeSessionAsync(Guid userId, Guid sessionId, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
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;
return await db.SaveChangesAsync(ct);
}
public async Task<int> RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default)
{
var db = _scope.Get<AppDbContext>();
var tid = TenantId();
var sessions = await db.UserSessions
.Where(x => x.TenantId == tid && x.UserId == userId && x.RevokedAt == null)
.ToListAsync(ct);
foreach (var s in sessions) s.RevokedAt = DateTimeOffset.UtcNow;
return await db.SaveChangesAsync(ct);
}
}