forked from xiongyuxing/tiku-backend.net
89 lines
2.6 KiB
C#
89 lines
2.6 KiB
C#
namespace Tiku.Application.Security;
|
|
|
|
public enum TenantResolutionSource
|
|
{
|
|
None,
|
|
Host,
|
|
TenantCode,
|
|
Jwt,
|
|
RefreshToken,
|
|
Worker,
|
|
System
|
|
}
|
|
|
|
public interface ITenantContext
|
|
{
|
|
Guid? TenantId { get; }
|
|
string? TenantCode { get; }
|
|
TenantResolutionSource ResolutionSource { get; }
|
|
bool IsResolved { get; }
|
|
bool IsSystem { get; }
|
|
}
|
|
|
|
public interface ITenantContextInitializer
|
|
{
|
|
void Initialize(Guid tenantId, string? tenantCode, TenantResolutionSource source);
|
|
void InitializeSystem(Guid? targetTenantId, string reason);
|
|
}
|
|
|
|
public sealed class TenantContext : ITenantContext, ITenantContextInitializer
|
|
{
|
|
public Guid? TenantId { get; private set; }
|
|
public string? TenantCode { get; private set; }
|
|
public TenantResolutionSource ResolutionSource { get; private set; }
|
|
public bool IsResolved => TenantId.HasValue;
|
|
public bool IsSystem { get; private set; }
|
|
internal string? SystemReason { get; private set; }
|
|
|
|
public void Initialize(Guid tenantId, string? tenantCode, TenantResolutionSource source)
|
|
{
|
|
if (tenantId == Guid.Empty)
|
|
{
|
|
throw new ArgumentException("Tenant ID cannot be empty.", nameof(tenantId));
|
|
}
|
|
|
|
if (IsSystem)
|
|
{
|
|
throw new InvalidOperationException("A system tenant context cannot be replaced.");
|
|
}
|
|
|
|
if (TenantId.HasValue && TenantId.Value != tenantId)
|
|
{
|
|
throw new TenantContextConflictException(TenantId.Value, tenantId);
|
|
}
|
|
|
|
var wasResolved = TenantId.HasValue;
|
|
TenantId = tenantId;
|
|
TenantCode = string.IsNullOrWhiteSpace(tenantCode) ? TenantCode : tenantCode.Trim();
|
|
if (!wasResolved)
|
|
{
|
|
ResolutionSource = source;
|
|
}
|
|
}
|
|
|
|
public void InitializeSystem(Guid? targetTenantId, string reason)
|
|
{
|
|
if (IsResolved || IsSystem)
|
|
{
|
|
throw new InvalidOperationException("Tenant context has already been initialized.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(reason))
|
|
{
|
|
throw new ArgumentException("A system scope requires an audit reason.", nameof(reason));
|
|
}
|
|
|
|
TenantId = targetTenantId;
|
|
ResolutionSource = TenantResolutionSource.System;
|
|
IsSystem = true;
|
|
SystemReason = reason.Trim();
|
|
}
|
|
}
|
|
|
|
public sealed class TenantContextConflictException(Guid expectedTenantId, Guid actualTenantId)
|
|
: Exception($"Resolved tenant '{expectedTenantId}' conflicts with authenticated tenant '{actualTenantId}'.")
|
|
{
|
|
public Guid ExpectedTenantId { get; } = expectedTenantId;
|
|
public Guid ActualTenantId { get; } = actualTenantId;
|
|
}
|