Init Git
This commit is contained in:
172
AMREZ.EOP.Infrastructures/Repositories/TenantRepository.cs
Normal file
172
AMREZ.EOP.Infrastructures/Repositories/TenantRepository.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
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.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace AMREZ.EOP.Infrastructures.Repositories;
|
||||
|
||||
public sealed class TenantRepository : ITenantRepository
|
||||
{
|
||||
private readonly IDbScope _scope;
|
||||
public TenantRepository(IDbScope scope) => _scope = scope;
|
||||
|
||||
private AppDbContext Db() => _scope.Get<AppDbContext>();
|
||||
|
||||
private static string Norm(string s) => (s ?? string.Empty).Trim().ToLowerInvariant();
|
||||
|
||||
public Task<bool> TenantExistsAsync(string tenantKey, CancellationToken ct = default)
|
||||
{
|
||||
var key = Norm(tenantKey);
|
||||
return Db().Set<TenantConfig>().AsNoTracking().AnyAsync(x => x.TenantKey == key, ct);
|
||||
}
|
||||
|
||||
public Task<TenantConfig?> GetAsync(string tenantKey, CancellationToken ct = default)
|
||||
{
|
||||
var key = Norm(tenantKey);
|
||||
return Db().Set<TenantConfig>().AsNoTracking().FirstOrDefaultAsync(x => x.TenantKey == key, ct);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<TenantConfig>> ListTenantsAsync(CancellationToken ct = default)
|
||||
=> await Db().Set<TenantConfig>().AsNoTracking().OrderBy(x => x.TenantKey).ToListAsync(ct);
|
||||
|
||||
public async Task<bool> CreateAsync(TenantConfig row, CancellationToken ct = default)
|
||||
{
|
||||
row.TenantKey = Norm(row.TenantKey);
|
||||
row.Schema = string.IsNullOrWhiteSpace(row.Schema) ? null : row.Schema!.Trim();
|
||||
row.ConnectionString = string.IsNullOrWhiteSpace(row.ConnectionString) ? null : row.ConnectionString!.Trim();
|
||||
row.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
|
||||
await Db().Set<TenantConfig>().AddAsync(row, ct);
|
||||
return await Db().SaveChangesAsync(ct) > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(TenantConfig row, DateTimeOffset? ifUnmodifiedSince, CancellationToken ct = default)
|
||||
{
|
||||
var key = Norm(row.TenantKey);
|
||||
var db = Db();
|
||||
var cur = await db.Set<TenantConfig>().FirstOrDefaultAsync(x => x.TenantKey == key, ct);
|
||||
if (cur is null) return false;
|
||||
|
||||
if (ifUnmodifiedSince.HasValue && cur.UpdatedAtUtc > ifUnmodifiedSince.Value)
|
||||
return false; // 412
|
||||
|
||||
cur.Schema = row.Schema is null ? cur.Schema : (string.IsNullOrWhiteSpace(row.Schema) ? null : row.Schema.Trim());
|
||||
cur.ConnectionString = row.ConnectionString is null ? cur.ConnectionString : (string.IsNullOrWhiteSpace(row.ConnectionString) ? null : row.ConnectionString.Trim());
|
||||
cur.Mode = row.Mode;
|
||||
cur.IsActive = row.IsActive;
|
||||
cur.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
|
||||
return await db.SaveChangesAsync(ct) > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(string tenantKey, CancellationToken ct = default)
|
||||
{
|
||||
var key = Norm(tenantKey);
|
||||
var db = Db();
|
||||
|
||||
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;
|
||||
t.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
var domains = await db.Set<TenantDomain>()
|
||||
.Where(d => d.TenantKey == key && d.IsActive)
|
||||
.ToListAsync(ct);
|
||||
|
||||
foreach (var d in domains)
|
||||
{
|
||||
d.IsActive = false;
|
||||
d.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
return await db.SaveChangesAsync(ct) > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> MapDomainAsync(string domain, string tenantKey, CancellationToken ct = default)
|
||||
{
|
||||
var d = Norm(domain);
|
||||
var key = Norm(tenantKey);
|
||||
|
||||
if (!await Db().Set<TenantConfig>().AnyAsync(x => x.TenantKey == key && x.IsActive, ct))
|
||||
return false;
|
||||
|
||||
var db = Db();
|
||||
var ex = await db.Set<TenantDomain>().FirstOrDefaultAsync(x => x.Domain == d, ct);
|
||||
if (ex is null)
|
||||
await db.Set<TenantDomain>().AddAsync(new TenantDomain { Domain = d, TenantKey = key, IsPlatformBaseDomain = false, IsActive = true, UpdatedAtUtc = DateTimeOffset.UtcNow }, ct);
|
||||
else
|
||||
{
|
||||
ex.TenantKey = key;
|
||||
ex.IsPlatformBaseDomain = false;
|
||||
ex.IsActive = true;
|
||||
ex.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
return await db.SaveChangesAsync(ct) > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> UnmapDomainAsync(string domain, CancellationToken ct = default)
|
||||
{
|
||||
var d = Norm(domain);
|
||||
var db = Db();
|
||||
var ex = await db.Set<TenantDomain>().FirstOrDefaultAsync(x => x.Domain == d, ct);
|
||||
if (ex is null) return false;
|
||||
|
||||
ex.IsActive = false;
|
||||
ex.TenantKey = null;
|
||||
ex.IsPlatformBaseDomain = false;
|
||||
ex.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
|
||||
return await db.SaveChangesAsync(ct) > 0;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<TenantDomain>> ListDomainsAsync(string? tenantKey, CancellationToken ct = default)
|
||||
{
|
||||
var db = Db();
|
||||
var q = db.Set<TenantDomain>().AsNoTracking();
|
||||
if (!string.IsNullOrWhiteSpace(tenantKey))
|
||||
{
|
||||
var key = Norm(tenantKey!);
|
||||
q = q.Where(x => x.TenantKey == key || x.IsPlatformBaseDomain);
|
||||
}
|
||||
return await q.OrderBy(x => x.Domain).ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<bool> AddBaseDomainAsync(string baseDomain, CancellationToken ct = default)
|
||||
{
|
||||
var d = Norm(baseDomain);
|
||||
var db = Db();
|
||||
var ex = await db.Set<TenantDomain>().FirstOrDefaultAsync(x => x.Domain == d, ct);
|
||||
if (ex is null)
|
||||
await db.Set<TenantDomain>().AddAsync(new TenantDomain { Domain = d, TenantKey = null, IsPlatformBaseDomain = true, IsActive = true, UpdatedAtUtc = DateTimeOffset.UtcNow }, ct);
|
||||
else
|
||||
{
|
||||
ex.TenantKey = null;
|
||||
ex.IsPlatformBaseDomain = true;
|
||||
ex.IsActive = true;
|
||||
ex.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
return await db.SaveChangesAsync(ct) > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveBaseDomainAsync(string baseDomain, CancellationToken ct = default)
|
||||
{
|
||||
var d = Norm(baseDomain);
|
||||
var db = Db();
|
||||
var ex = await db.Set<TenantDomain>()
|
||||
.FirstOrDefaultAsync(x => x.Domain == d && x.IsPlatformBaseDomain, ct);
|
||||
if (ex is null) return false;
|
||||
|
||||
ex.IsActive = false;
|
||||
ex.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
|
||||
return await db.SaveChangesAsync(ct) > 0;
|
||||
}
|
||||
}
|
||||
144
AMREZ.EOP.Infrastructures/Repositories/UserProfileRepository.cs
Normal file
144
AMREZ.EOP.Infrastructures/Repositories/UserProfileRepository.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
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.Infrastructures.Data;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace AMREZ.EOP.Infrastructures.Repositories;
|
||||
|
||||
public class UserProfileRepository : IUserProfileRepository
|
||||
{
|
||||
private readonly IDbScope _scope;
|
||||
private readonly ITenantResolver _tenantResolver;
|
||||
private readonly IHttpContextAccessor _http;
|
||||
|
||||
public UserProfileRepository(IDbScope scope, ITenantResolver tenantResolver, IHttpContextAccessor http)
|
||||
{ _scope = scope; _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;
|
||||
}
|
||||
|
||||
public async Task<UserProfile?> GetByUserIdAsync(Guid userId, CancellationToken ct = default)
|
||||
{
|
||||
var db = _scope.Get<AppDbContext>();
|
||||
var tid = TenantId();
|
||||
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 existing = await db.UserProfiles.FirstOrDefaultAsync(p => p.TenantId == tid && p.UserId == profile.UserId, ct);
|
||||
if (existing is null)
|
||||
{
|
||||
profile.TenantId = tid;
|
||||
await db.UserProfiles.AddAsync(profile, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.FirstName = profile.FirstName;
|
||||
existing.LastName = profile.LastName;
|
||||
existing.MiddleName = profile.MiddleName;
|
||||
existing.Nickname = profile.Nickname;
|
||||
existing.DateOfBirth = profile.DateOfBirth;
|
||||
existing.Gender = profile.Gender;
|
||||
}
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<Employment> AddEmploymentAsync(Employment e, CancellationToken ct = default)
|
||||
{
|
||||
var db = _scope.Get<AppDbContext>();
|
||||
e.TenantId = TenantId();
|
||||
await db.Employments.AddAsync(e, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return e;
|
||||
}
|
||||
|
||||
public async Task EndEmploymentAsync(Guid employmentId, DateTime endDate, CancellationToken ct = default)
|
||||
{
|
||||
var db = _scope.Get<AppDbContext>();
|
||||
var tid = TenantId();
|
||||
var e = await db.Employments.FirstOrDefaultAsync(x => x.TenantId == tid && x.Id == employmentId, ct);
|
||||
if (e is null) return;
|
||||
e.EndDate = endDate;
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<EmployeeAddress> AddAddressAsync(EmployeeAddress a, CancellationToken ct = default)
|
||||
{
|
||||
var db = _scope.Get<AppDbContext>();
|
||||
a.TenantId = TenantId();
|
||||
await db.EmployeeAddresses.AddAsync(a, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return a;
|
||||
}
|
||||
|
||||
public async Task SetPrimaryAddressAsync(Guid userProfileId, Guid addressId, CancellationToken ct = default)
|
||||
{
|
||||
var db = _scope.Get<AppDbContext>();
|
||||
var tid = TenantId();
|
||||
|
||||
var all = await db.EmployeeAddresses.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct);
|
||||
foreach (var x in all) x.IsPrimary = false;
|
||||
|
||||
var target = all.FirstOrDefault(x => x.Id == addressId);
|
||||
if (target is not null) target.IsPrimary = true;
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<EmergencyContact> AddEmergencyContactAsync(EmergencyContact c, CancellationToken ct = default)
|
||||
{
|
||||
var db = _scope.Get<AppDbContext>();
|
||||
c.TenantId = TenantId();
|
||||
await db.EmergencyContacts.AddAsync(c, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return c;
|
||||
}
|
||||
|
||||
public async Task SetPrimaryEmergencyContactAsync(Guid userProfileId, Guid contactId, CancellationToken ct = default)
|
||||
{
|
||||
var db = _scope.Get<AppDbContext>();
|
||||
var tid = TenantId();
|
||||
|
||||
var all = await db.EmergencyContacts.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct);
|
||||
foreach (var x in all) x.IsPrimary = false;
|
||||
|
||||
var target = all.FirstOrDefault(x => x.Id == contactId);
|
||||
if (target is not null) target.IsPrimary = true;
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<EmployeeBankAccount> AddBankAccountAsync(EmployeeBankAccount b, CancellationToken ct = default)
|
||||
{
|
||||
var db = _scope.Get<AppDbContext>();
|
||||
b.TenantId = TenantId();
|
||||
await db.EmployeeBankAccounts.AddAsync(b, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return b;
|
||||
}
|
||||
|
||||
public async Task SetPrimaryBankAccountAsync(Guid userProfileId, Guid bankAccountId, CancellationToken ct = default)
|
||||
{
|
||||
var db = _scope.Get<AppDbContext>();
|
||||
var tid = TenantId();
|
||||
|
||||
var all = await db.EmployeeBankAccounts.Where(x => x.TenantId == tid && x.UserProfileId == userProfileId).ToListAsync(ct);
|
||||
foreach (var x in all) x.IsPrimary = false;
|
||||
|
||||
var target = all.FirstOrDefault(x => x.Id == bankAccountId);
|
||||
if (target is not null) target.IsPrimary = true;
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
329
AMREZ.EOP.Infrastructures/Repositories/UserRepository.cs
Normal file
329
AMREZ.EOP.Infrastructures/Repositories/UserRepository.cs
Normal file
@@ -0,0 +1,329 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user