Files
timetracker/timetracker.Server/Data/TenantProvider.cs
T
2026-06-24 23:48:04 +02:00

127 lines
3.7 KiB
C#

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using timetracker.Shared;
namespace timetracker.Data;
public class TenantProvider : ITenantProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IDbContextFactory<TimetrackerDbContext> _dbContextFactory;
private readonly IConfiguration _configuration;
public TenantProvider(
IHttpContextAccessor httpContextAccessor,
IDbContextFactory<TimetrackerDbContext> dbContextFactory,
IConfiguration configuration)
{
_httpContextAccessor = httpContextAccessor;
_dbContextFactory = dbContextFactory;
_configuration = configuration;
}
public int? TenantId
{
get
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null) return null;
if (httpContext.Items.TryGetValue("TenantId", out var cachedId))
{
return (int?)cachedId;
}
// Resolve and cache in HttpContext.Items
ResolveTenantAsync().GetAwaiter().GetResult();
if (httpContext.Items.TryGetValue("TenantId", out cachedId))
{
return (int?)cachedId;
}
return null;
}
}
public async Task<Tenant?> GetCurrentTenantAsync()
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null) return null;
if (httpContext.Items.TryGetValue("CurrentTenant", out var cachedTenant))
{
return (Tenant?)cachedTenant;
}
await ResolveTenantAsync();
if (httpContext.Items.TryGetValue("CurrentTenant", out cachedTenant))
{
return (Tenant?)cachedTenant;
}
return null;
}
private async Task ResolveTenantAsync()
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null) return;
// Prevent double resolution in the same request
if (httpContext.Items.ContainsKey("TenantResolved")) return;
httpContext.Items["TenantResolved"] = true;
var host = httpContext.Request.Host.Host;
var subdomain = GetSubdomain(host);
if (!string.IsNullOrEmpty(subdomain))
{
await using var db = await _dbContextFactory.CreateDbContextAsync();
var tenant = await db.Tenants
.FirstOrDefaultAsync(t => t.Subdomain.ToLower() == subdomain.ToLower());
if (tenant != null)
{
httpContext.Items["CurrentTenant"] = tenant;
httpContext.Items["TenantId"] = tenant.Id;
}
}
}
private string? GetSubdomain(string host)
{
var baseDomain = _configuration["TenantSettings:BaseDomain"] ?? "localhost";
// Ignore port if included in host
var hostName = host.Split(':')[0];
if (hostName.Equals(baseDomain, StringComparison.OrdinalIgnoreCase))
{
return null; // Main domain
}
if (hostName.EndsWith("." + baseDomain, StringComparison.OrdinalIgnoreCase))
{
return hostName.Substring(0, hostName.Length - baseDomain.Length - 1);
}
// Fallback for localhost testing
if (baseDomain.Equals("localhost", StringComparison.OrdinalIgnoreCase))
{
var parts = hostName.Split('.');
if (parts.Length > 1 && !hostName.Equals("localhost", StringComparison.OrdinalIgnoreCase))
{
return parts[0];
}
}
return null;
}
}