Fix Access/Refres Token
This commit is contained in:
@@ -17,12 +17,17 @@ public sealed class TenantRepository : ITenantRepository
|
||||
private readonly IHttpContextAccessor _http;
|
||||
|
||||
public TenantRepository(IDbScope scope, ITenantResolver resolver, IHttpContextAccessor http)
|
||||
{ _scope = scope; _resolver = resolver; _http = 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");
|
||||
var platform = _resolver.Resolve(http, "@platform") ??
|
||||
throw new InvalidOperationException("No platform tenant");
|
||||
_scope.EnsureForTenant(platform);
|
||||
return _scope.Get<AppDbContext>();
|
||||
}
|
||||
@@ -55,7 +60,8 @@ public sealed class TenantRepository : ITenantRepository
|
||||
return await Db().SaveChangesAsync(ct) > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(TenantConfig row, DateTimeOffset? ifUnmodifiedSince, CancellationToken ct = default)
|
||||
public async Task<bool> UpdateAsync(TenantConfig row, DateTimeOffset? ifUnmodifiedSince,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var key = Norm(row.TenantKey);
|
||||
var db = Db();
|
||||
@@ -65,8 +71,12 @@ public sealed class TenantRepository : ITenantRepository
|
||||
if (ifUnmodifiedSince.HasValue && cur.UpdatedAtUtc > ifUnmodifiedSince.Value)
|
||||
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());
|
||||
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;
|
||||
@@ -112,7 +122,13 @@ public sealed class TenantRepository : ITenantRepository
|
||||
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);
|
||||
await db.Set<TenantDomain>()
|
||||
.AddAsync(
|
||||
new TenantDomain
|
||||
{
|
||||
Domain = d, TenantKey = key, IsPlatformBaseDomain = false, IsActive = true,
|
||||
UpdatedAtUtc = DateTimeOffset.UtcNow
|
||||
}, ct);
|
||||
else
|
||||
{
|
||||
ex.TenantKey = key;
|
||||
@@ -120,6 +136,7 @@ public sealed class TenantRepository : ITenantRepository
|
||||
ex.IsActive = true;
|
||||
ex.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
return await db.SaveChangesAsync(ct) > 0;
|
||||
}
|
||||
|
||||
@@ -147,6 +164,7 @@ public sealed class TenantRepository : ITenantRepository
|
||||
var key = Norm(tenantKey!);
|
||||
q = q.Where(x => x.TenantKey == key || x.IsPlatformBaseDomain);
|
||||
}
|
||||
|
||||
return await q.OrderBy(x => x.Domain).ToListAsync(ct);
|
||||
}
|
||||
|
||||
@@ -156,7 +174,13 @@ public sealed class TenantRepository : ITenantRepository
|
||||
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);
|
||||
await db.Set<TenantDomain>()
|
||||
.AddAsync(
|
||||
new TenantDomain
|
||||
{
|
||||
Domain = d, TenantKey = null, IsPlatformBaseDomain = true, IsActive = true,
|
||||
UpdatedAtUtc = DateTimeOffset.UtcNow
|
||||
}, ct);
|
||||
else
|
||||
{
|
||||
ex.TenantKey = null;
|
||||
@@ -164,6 +188,7 @@ public sealed class TenantRepository : ITenantRepository
|
||||
ex.IsActive = true;
|
||||
ex.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
return await db.SaveChangesAsync(ct) > 0;
|
||||
}
|
||||
|
||||
@@ -180,4 +205,36 @@ public sealed class TenantRepository : ITenantRepository
|
||||
|
||||
return await db.SaveChangesAsync(ct) > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// คืนค่า TenantKey จาก TenantId (Guid) ถ้าไม่เจอหรือไม่ active คืน null
|
||||
/// </summary>
|
||||
public async Task<string?> GetTenantKeyByTenantIdAsync(Guid tenantId, CancellationToken ct = default)
|
||||
{
|
||||
var key = await Db().Set<TenantConfig>()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.IsActive)
|
||||
.Select(x => x.TenantKey)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
return string.IsNullOrWhiteSpace(key) ? null : key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// คืนค่าแม็พ TenantId -> TenantKey เฉพาะที่เจอและ active
|
||||
/// </summary>
|
||||
public async Task<Dictionary<Guid, string>> MapTenantKeysAsync(IEnumerable<Guid> tenantIds, CancellationToken ct = default)
|
||||
{
|
||||
var ids = (tenantIds ?? Array.Empty<Guid>()).Distinct().ToArray();
|
||||
if (ids.Length == 0) return new Dictionary<Guid, string>();
|
||||
|
||||
var rows = await Db().Set<TenantConfig>()
|
||||
.AsNoTracking()
|
||||
.Where(x => ids.Contains(x.TenantId) && x.IsActive)
|
||||
.Select(x => new { x.TenantId, x.TenantKey })
|
||||
.ToListAsync(ct);
|
||||
|
||||
return rows.ToDictionary(x => x.TenantId, x => x.TenantKey);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -65,36 +65,33 @@ public class UserRepository : IUserRepository
|
||||
|
||||
public async Task<User?> FindActiveByEmailAsync(string email, CancellationToken ct = default)
|
||||
{
|
||||
var tid = TenantId();
|
||||
var tidStr = tid.ToString();
|
||||
var r = _redis?.GetDatabase();
|
||||
var norm = email.Trim().ToLowerInvariant();
|
||||
|
||||
if (r is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cached = await r.StringGetAsync(IdentityEmailKey(tidStr, email));
|
||||
var cached = await r.StringGetAsync($"uidx:{norm}");
|
||||
if (cached.HasValue)
|
||||
{
|
||||
var hit = JsonSerializer.Deserialize<User>(cached!);
|
||||
if (hit?.IsActive == true) return hit;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch { /* ignore cache errors */ }
|
||||
}
|
||||
|
||||
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);
|
||||
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)
|
||||
{
|
||||
@@ -102,10 +99,10 @@ public class UserRepository : IUserRepository
|
||||
{
|
||||
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);
|
||||
await r.StringSetAsync($"uidx:{norm}", payload, ttl);
|
||||
await r.StringSetAsync($"user:{user.Id:N}", payload, ttl);
|
||||
}
|
||||
catch { }
|
||||
catch { /* ignore cache errors */ }
|
||||
}
|
||||
|
||||
return user;
|
||||
@@ -291,9 +288,7 @@ public class UserRepository : IUserRepository
|
||||
|
||||
public async Task<UserSession> CreateSessionAsync(UserSession session, CancellationToken ct = default)
|
||||
{
|
||||
var tid = TenantId();
|
||||
var db = _scope.Get<AppDbContext>();
|
||||
session.TenantId = tid;
|
||||
await db.UserSessions.AddAsync(session, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return session;
|
||||
@@ -383,11 +378,10 @@ public class UserRepository : IUserRepository
|
||||
|
||||
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)
|
||||
.Where(x => x.Id == userId)
|
||||
.Select(x => x.SecurityStamp)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
}
|
||||
|
||||
@@ -28,23 +28,23 @@ public sealed class JwtFactory : IJwtFactory
|
||||
|
||||
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 tenantId = claims.FirstOrDefault(c => c.Type == "tenant_id")?.Value
|
||||
?? throw new InvalidOperationException("tenant_id claim missing");
|
||||
|
||||
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 keyBytes = SHA256.HashData(Encoding.UTF8.GetBytes(tenantId));
|
||||
var creds = new SigningCredentials(new SymmetricSecurityKey(keyBytes), SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var exp = now.AddMinutes(AccessMinutes);
|
||||
|
||||
var jwt = new JwtSecurityToken(
|
||||
issuer: Issuer,
|
||||
issuer: Issuer,
|
||||
audience: Audience,
|
||||
claims: claims,
|
||||
notBefore: now.UtcDateTime,
|
||||
expires: exp.UtcDateTime,
|
||||
signingCredentials: cred);
|
||||
signingCredentials: creds
|
||||
);
|
||||
|
||||
return (new JwtSecurityTokenHandler().WriteToken(jwt), exp);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ public sealed class DefaultTenantResolver : ITenantResolver
|
||||
private readonly TenantMap _map;
|
||||
private readonly ILogger<DefaultTenantResolver>? _log;
|
||||
|
||||
// แพลตฟอร์มสลัก (ปรับตามระบบจริงได้) — ใช้ "public" เป็นค่าเริ่ม
|
||||
private const string PlatformSlug = "public";
|
||||
|
||||
public DefaultTenantResolver(
|
||||
@@ -75,14 +74,7 @@ public sealed class DefaultTenantResolver : ITenantResolver
|
||||
_log?.LogInformation("Resolved by subdomain: {Sub} cid={Cid}", sub, cid);
|
||||
return bySub;
|
||||
}
|
||||
|
||||
if (hint is LoginRequest body && !string.IsNullOrWhiteSpace(body.Tenant) &&
|
||||
_map.TryGetBySlug(body.Tenant!, out var byBody))
|
||||
{
|
||||
_log?.LogInformation("Resolved by body: {Tenant} cid={Cid}", body.Tenant, cid);
|
||||
return byBody;
|
||||
}
|
||||
|
||||
|
||||
_log?.LogWarning("Resolve FAILED host={Host} header={Header} cid={Cid}",
|
||||
host, string.IsNullOrWhiteSpace(header) ? "<empty>" : header, cid);
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user