forked from gongxuegit/tiku-backend.net
feat: enforce tenant isolation and shared question bank
This commit is contained in:
@@ -15,7 +15,7 @@ namespace Tiku.Api.Controllers;
|
||||
[Route("api/assets")]
|
||||
public sealed class AssetsController(
|
||||
IAssetAccessService assetAccessService,
|
||||
ICurrentTenant currentTenant,
|
||||
ITenantContext currentTenant,
|
||||
ICurrentUser currentUser,
|
||||
TikuDbContext dbContext) : ControllerBase
|
||||
{
|
||||
|
||||
@@ -2,13 +2,21 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Infrastructure.Content;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
[Produces("application/json")]
|
||||
public sealed class AuthController(IAuthService authService) : ControllerBase
|
||||
public sealed class AuthController(
|
||||
IAuthService authService,
|
||||
ISessionService sessionService,
|
||||
ITenantContext tenantContext,
|
||||
ITenantContextInitializer tenantContextInitializer,
|
||||
ITenantDirectory tenantDirectory) : ControllerBase
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[HttpPost("login/password")]
|
||||
@@ -22,7 +30,7 @@ public sealed class AuthController(IAuthService authService) : ControllerBase
|
||||
{
|
||||
var result = await authService.LoginWithPasswordAsync(
|
||||
new PasswordLoginRequest(
|
||||
request.TenantId,
|
||||
await ResolveTenantIdAsync(request.TenantCode, cancellationToken),
|
||||
request.Phone,
|
||||
request.Password,
|
||||
GetIpAddress(),
|
||||
@@ -44,7 +52,7 @@ public sealed class AuthController(IAuthService authService) : ControllerBase
|
||||
{
|
||||
var result = await authService.LoginWithSmsAsync(
|
||||
new SmsLoginRequest(
|
||||
request.TenantId,
|
||||
await ResolveTenantIdAsync(request.TenantCode, cancellationToken),
|
||||
request.Phone,
|
||||
request.Code,
|
||||
GetIpAddress(),
|
||||
@@ -67,7 +75,7 @@ public sealed class AuthController(IAuthService authService) : ControllerBase
|
||||
{
|
||||
var result = await authService.LoginWithWechatWebAsync(
|
||||
new WechatLoginRequest(
|
||||
request.TenantId,
|
||||
await ResolveTenantIdAsync(request.TenantCode, cancellationToken),
|
||||
request.Code,
|
||||
GetIpAddress(),
|
||||
Request.Headers.UserAgent.ToString()),
|
||||
@@ -89,7 +97,7 @@ public sealed class AuthController(IAuthService authService) : ControllerBase
|
||||
{
|
||||
var result = await authService.LoginWithWechatMiniAppAsync(
|
||||
new WechatLoginRequest(
|
||||
request.TenantId,
|
||||
await ResolveTenantIdAsync(request.TenantCode, cancellationToken),
|
||||
request.Code,
|
||||
GetIpAddress(),
|
||||
Request.Headers.UserAgent.ToString()),
|
||||
@@ -106,6 +114,7 @@ public sealed class AuthController(IAuthService authService) : ControllerBase
|
||||
[FromBody] RefreshSessionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ResolveRefreshTokenTenant(request.RefreshToken);
|
||||
var result = await authService.RefreshAsync(
|
||||
new RefreshSessionRequest(
|
||||
request.RefreshToken,
|
||||
@@ -125,6 +134,7 @@ public sealed class AuthController(IAuthService authService) : ControllerBase
|
||||
[FromBody] RefreshSessionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ResolveRefreshTokenTenant(request.RefreshToken);
|
||||
await authService.LogoutAsync(
|
||||
new LogoutSessionRequest(request.RefreshToken),
|
||||
cancellationToken);
|
||||
@@ -132,8 +142,51 @@ public sealed class AuthController(IAuthService authService) : ControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private void ResolveRefreshTokenTenant(string refreshToken)
|
||||
{
|
||||
if (!sessionService.TryParseRefreshToken(refreshToken, out var locator))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tenantContextInitializer.Initialize(locator.TenantId, null, TenantResolutionSource.RefreshToken);
|
||||
}
|
||||
|
||||
private string? GetIpAddress()
|
||||
{
|
||||
return HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
}
|
||||
|
||||
private async Task<Guid> ResolveTenantIdAsync(string? tenantCode, CancellationToken cancellationToken)
|
||||
{
|
||||
if (tenantContext.TenantId.HasValue)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tenantCode) &&
|
||||
!string.Equals(tenantContext.TenantCode, tenantCode.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var supplied = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken);
|
||||
if (supplied?.TenantId != tenantContext.TenantId.Value)
|
||||
{
|
||||
throw new TenantContextConflictException(
|
||||
tenantContext.TenantId.Value,
|
||||
supplied?.TenantId ?? Guid.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
return tenantContext.TenantId.Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantCode))
|
||||
{
|
||||
throw new RequiredFieldException("tenantCode is required when the request host does not resolve a tenant.");
|
||||
}
|
||||
|
||||
var tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken)
|
||||
?? throw new TenantNotFoundException();
|
||||
tenantContextInitializer.Initialize(
|
||||
tenant.TenantId,
|
||||
tenant.TenantCode,
|
||||
TenantResolutionSource.TenantCode);
|
||||
return tenant.TenantId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed class CatalogController(
|
||||
IQuestionBankQueryService questionBankQueryService,
|
||||
IStudyContentQueryService studyContentQueryService,
|
||||
IAssetQueryService assetQueryService,
|
||||
ICurrentTenant currentTenant,
|
||||
ITenantContext currentTenant,
|
||||
TikuDbContext dbContext) : ControllerBase
|
||||
{
|
||||
[HttpGet("regions")]
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Text.Json;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Application.Commerce;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
@@ -16,7 +17,9 @@ namespace Tiku.Api.Controllers;
|
||||
public sealed class CommerceController(
|
||||
ICommerceService commerceService,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant) : ControllerBase
|
||||
ITenantContext currentTenant,
|
||||
ITenantContextInitializer tenantInitializer,
|
||||
ITenantDirectory tenantDirectory) : ControllerBase
|
||||
{
|
||||
[HttpPost("orders")]
|
||||
[EndpointSummary("创建学生端订单")]
|
||||
@@ -125,10 +128,13 @@ public sealed class CommerceController(
|
||||
[EndpointSummary("微信支付回调")]
|
||||
[ProducesResponseType<PaymentNotificationProcessResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PaymentNotificationProcessResult>> WechatPayNotify(
|
||||
[FromQuery] Guid tenantId,
|
||||
[FromQuery] string? tenantCode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await ProcessNotificationAsync(tenantId, PaymentProviders.WechatPay, cancellationToken));
|
||||
return Ok(await ProcessNotificationAsync(
|
||||
await ResolveNotificationTenantAsync(tenantCode, cancellationToken),
|
||||
PaymentProviders.WechatPay,
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
@@ -136,10 +142,33 @@ public sealed class CommerceController(
|
||||
[EndpointSummary("支付宝支付回调")]
|
||||
[ProducesResponseType<PaymentNotificationProcessResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PaymentNotificationProcessResult>> AlipayNotify(
|
||||
[FromQuery] Guid tenantId,
|
||||
[FromQuery] string? tenantCode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await ProcessNotificationAsync(tenantId, PaymentProviders.Alipay, cancellationToken));
|
||||
return Ok(await ProcessNotificationAsync(
|
||||
await ResolveNotificationTenantAsync(tenantCode, cancellationToken),
|
||||
PaymentProviders.Alipay,
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
private async Task<Guid> ResolveNotificationTenantAsync(
|
||||
string? tenantCode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentTenant.TenantId.HasValue)
|
||||
{
|
||||
return currentTenant.TenantId.Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantCode))
|
||||
{
|
||||
throw new CommerceException("Tenant code is required for payment notification.", "tenant_required");
|
||||
}
|
||||
|
||||
var tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken)
|
||||
?? throw new CommerceException("Tenant was not found.", "tenant_not_found");
|
||||
tenantInitializer.Initialize(tenant.TenantId, tenant.TenantCode, TenantResolutionSource.TenantCode);
|
||||
return tenant.TenantId;
|
||||
}
|
||||
|
||||
private CommerceActor ResolveActor()
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Tiku.Api.Controllers;
|
||||
public sealed class CommissionController(
|
||||
ICommissionService commissionService,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant) : ControllerBase
|
||||
ITenantContext currentTenant) : ControllerBase
|
||||
{
|
||||
[HttpGet("settings")]
|
||||
public async Task<ActionResult<CommissionSettingsItem>> Settings(CancellationToken cancellationToken) =>
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Tiku.Api.Controllers;
|
||||
public sealed class CrmController(
|
||||
ICrmService crmService,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant) : ControllerBase
|
||||
ITenantContext currentTenant) : ControllerBase
|
||||
{
|
||||
[HttpGet("config")]
|
||||
[EndpointSummary("查询 CRM 推送配置")]
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Tiku.Api.Controllers;
|
||||
public sealed class LearningController(
|
||||
ILearningActivityService learningActivityService,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant) : ControllerBase
|
||||
ITenantContext currentTenant) : ControllerBase
|
||||
{
|
||||
[HttpGet("stats")]
|
||||
[EndpointSummary("查询学习统计")]
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Tiku.Api.Controllers;
|
||||
public sealed class PointsController(
|
||||
IPointService pointService,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant) : ControllerBase
|
||||
ITenantContext currentTenant) : ControllerBase
|
||||
{
|
||||
[HttpGet("summary")]
|
||||
[EndpointSummary("查询当前用户积分摘要")]
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Tiku.Api.Controllers;
|
||||
public sealed class ProfileController(
|
||||
IProfileService profileService,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant) : ControllerBase
|
||||
ITenantContext currentTenant) : ControllerBase
|
||||
{
|
||||
[HttpGet("me")]
|
||||
[EndpointSummary("获取当前学生资料")]
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Application.Growth;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
@@ -15,8 +16,9 @@ namespace Tiku.Api.Controllers;
|
||||
public sealed class ReferralController(
|
||||
IReferralService referralService,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant,
|
||||
TikuDbContext dbContext) : ControllerBase
|
||||
ITenantContext currentTenant,
|
||||
ITenantContextInitializer tenantInitializer,
|
||||
ITenantDirectory tenantDirectory) : ControllerBase
|
||||
{
|
||||
[HttpPost("invite-code")]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
@@ -223,13 +225,9 @@ public sealed class ReferralController(
|
||||
throw new TenantNotFoundException();
|
||||
}
|
||||
|
||||
var tenantId = await dbContext.Tenants
|
||||
.Where(tenant =>
|
||||
tenant.Slug == resolvedTenantCode.Trim() &&
|
||||
tenant.Status == TenantStatus.Active)
|
||||
.Select(tenant => (Guid?)tenant.Id)
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
return tenantId ?? throw new TenantNotFoundException();
|
||||
var tenant = await tenantDirectory.FindByCodeAsync(resolvedTenantCode.Trim(), cancellationToken)
|
||||
?? throw new TenantNotFoundException();
|
||||
tenantInitializer.Initialize(tenant.TenantId, tenant.TenantCode, TenantResolutionSource.TenantCode);
|
||||
return tenant.TenantId;
|
||||
}
|
||||
}
|
||||
|
||||
45
Tiku.Api/Controllers/RuntimeController.cs
Normal file
45
Tiku.Api/Controllers/RuntimeController.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[AllowAnonymous]
|
||||
[Produces("application/json")]
|
||||
[Route("api/runtime")]
|
||||
public sealed class RuntimeController(
|
||||
ITenantContext tenantContext,
|
||||
ITenantFrontendConfigService frontendConfigService) : ControllerBase
|
||||
{
|
||||
[HttpGet("bootstrap")]
|
||||
[EndpointSummary("获取租户前端运行时配置")]
|
||||
[ProducesResponseType<TenantRuntimeBootstrap>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status304NotModified)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<TenantRuntimeBootstrap>> Bootstrap(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!tenantContext.TenantId.HasValue)
|
||||
{
|
||||
return NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Tenant was not found.",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
var runtime = await frontendConfigService.GetRuntimeAsync(
|
||||
tenantContext.TenantId.Value,
|
||||
cancellationToken);
|
||||
var etag = $"\"{runtime.TenantCode}-{runtime.ConfigVersion}\"";
|
||||
if (Request.Headers.IfNoneMatch.Any(value => string.Equals(value, etag, StringComparison.Ordinal)))
|
||||
{
|
||||
return StatusCode(StatusCodes.Status304NotModified);
|
||||
}
|
||||
|
||||
Response.Headers.ETag = etag;
|
||||
Response.Headers.CacheControl = "public,max-age=60,must-revalidate";
|
||||
return Ok(runtime);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace Tiku.Api.Controllers;
|
||||
[Route("api/scoreline")]
|
||||
public sealed class ScorelineController(
|
||||
IScorelineQueryService scorelineQueryService,
|
||||
ICurrentTenant currentTenant,
|
||||
ITenantContext currentTenant,
|
||||
TikuDbContext dbContext) : ControllerBase
|
||||
{
|
||||
private static readonly string[] DynamicPrefixes = ["field.", "min.", "max."];
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Tiku.Api.Controllers;
|
||||
[Route("api/_security")]
|
||||
public sealed class SecurityDiagnosticsController(
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant) : ControllerBase
|
||||
ITenantContext currentTenant) : ControllerBase
|
||||
{
|
||||
[Authorize(Policy = TikuPolicies.AuthenticatedUser)]
|
||||
[HttpGet("authenticated")]
|
||||
@@ -29,7 +29,7 @@ public sealed class SecurityDiagnosticsController(
|
||||
return Ok(new
|
||||
{
|
||||
currentTenant.TenantId,
|
||||
currentTenant.Role
|
||||
currentUser.TenantRole
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class SecurityDiagnosticsController(
|
||||
return Ok(new
|
||||
{
|
||||
currentTenant.TenantId,
|
||||
currentTenant.Role
|
||||
currentUser.TenantRole
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
36
Tiku.Api/Controllers/TaxonomyController.cs
Normal file
36
Tiku.Api/Controllers/TaxonomyController.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/taxonomy/nodes")]
|
||||
public sealed class TaxonomyController(
|
||||
ITenantContext tenantContext,
|
||||
ITaxonomyService taxonomyService) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public Task<IReadOnlyCollection<TaxonomyNodeItem>> List(CancellationToken cancellationToken)
|
||||
{
|
||||
return taxonomyService.ListAsync(RequireTenantId(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Policy = TikuPolicies.TenantAdmin)]
|
||||
public Task<TaxonomyNodeItem> Create(
|
||||
CreateTaxonomyNodeDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return taxonomyService.CreateAsync(RequireTenantId(), request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
private Guid RequireTenantId()
|
||||
{
|
||||
return tenantContext.TenantId ?? throw new InvalidOperationException("Tenant was not resolved.");
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace Tiku.Api.Controllers;
|
||||
public sealed class TenantAdminDirectController(
|
||||
ITenantAdminDirectService tenantAdminService,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant) : ControllerBase
|
||||
ITenantContext currentTenant) : ControllerBase
|
||||
{
|
||||
[HttpGet("classes")]
|
||||
[EndpointSummary("查询租户班级")]
|
||||
@@ -406,7 +406,7 @@ public sealed class TenantAdminDirectController(
|
||||
}
|
||||
|
||||
var role = Enum.TryParse<TenantRole>(
|
||||
currentTenant.Role?.Replace("_", string.Empty, StringComparison.Ordinal),
|
||||
currentUser.TenantRole?.Replace("_", string.Empty, StringComparison.Ordinal),
|
||||
ignoreCase: true,
|
||||
out var parsedRole)
|
||||
? parsedRole
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Tiku.Api.Controllers;
|
||||
public sealed class TenantCommerceController(
|
||||
ICommerceAdminService commerceAdminService,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant) : ControllerBase
|
||||
ITenantContext currentTenant) : ControllerBase
|
||||
{
|
||||
[HttpGet("payment-accounts")]
|
||||
[EndpointSummary("查询租户支付账号")]
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class TenantContentController(
|
||||
IAssetManagementService assetManagementService,
|
||||
IContentManagementService contentManagementService,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant) : ControllerBase
|
||||
ITenantContext currentTenant) : ControllerBase
|
||||
{
|
||||
[HttpGet("entries")]
|
||||
[EndpointSummary("查询租户内容入口")]
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Tiku.Api.Controllers;
|
||||
public sealed class TenantContentDirectController(
|
||||
IDirectContentService directContentService,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant) : ControllerBase
|
||||
ITenantContext currentTenant) : ControllerBase
|
||||
{
|
||||
[HttpPost("questions")]
|
||||
[EndpointSummary("创建题目及首个版本")]
|
||||
|
||||
51
Tiku.Api/Controllers/TenantFrontendConfigController.cs
Normal file
51
Tiku.Api/Controllers/TenantFrontendConfigController.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = TikuPolicies.TenantAdmin)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant-admin/frontend-config")]
|
||||
public sealed class TenantFrontendConfigController(
|
||||
ITenantContext tenantContext,
|
||||
ITenantFrontendConfigService frontendConfigService) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public Task<TenantFrontendConfigItem> Get(CancellationToken cancellationToken)
|
||||
{
|
||||
return frontendConfigService.GetAsync(RequireTenantId(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("draft")]
|
||||
public Task<TenantFrontendConfigItem> SaveDraft(
|
||||
SaveTenantFrontendConfigDraftDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return frontendConfigService.SaveDraftAsync(
|
||||
RequireTenantId(),
|
||||
request.ToDraft(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("publish")]
|
||||
public Task<TenantFrontendConfigItem> Publish(
|
||||
PublishTenantFrontendConfigDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return frontendConfigService.PublishAsync(
|
||||
RequireTenantId(),
|
||||
request.ExpectedVersion,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private Guid RequireTenantId()
|
||||
{
|
||||
return tenantContext.TenantId ?? throw new TenantFrontendConfigException(
|
||||
"tenant_not_resolved",
|
||||
"Tenant was not resolved.");
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
@@ -14,7 +16,11 @@ namespace Tiku.Api.Controllers;
|
||||
[AllowAnonymous]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant")]
|
||||
public sealed class TenantPublicController(TikuDbContext dbContext) : ControllerBase
|
||||
public sealed class TenantPublicController(
|
||||
TikuDbContext dbContext,
|
||||
ITenantContext tenantContext,
|
||||
ITenantContextInitializer tenantContextInitializer,
|
||||
ITenantDirectory tenantDirectory) : ControllerBase
|
||||
{
|
||||
[HttpGet("resolve")]
|
||||
[EndpointSummary("解析当前租户")]
|
||||
@@ -28,9 +34,23 @@ public sealed class TenantPublicController(TikuDbContext dbContext) : Controller
|
||||
var tenantCode = NormalizeTenantCode(query.TenantCode ?? Request.Headers["x-tenant-code"].FirstOrDefault());
|
||||
var host = NormalizeHost(query.Host ?? Request.Host.Host);
|
||||
|
||||
var tenant = !string.IsNullOrWhiteSpace(tenantCode)
|
||||
? await FindActiveTenantByCodeAsync(tenantCode, cancellationToken)
|
||||
: await FindActiveTenantByHostAsync(host, cancellationToken);
|
||||
var directoryEntry = tenantContext.ResolutionSource == TenantResolutionSource.Host
|
||||
? await tenantDirectory.FindByHostAsync(Request.Host.Host, cancellationToken)
|
||||
: !string.IsNullOrWhiteSpace(tenantCode)
|
||||
? await tenantDirectory.FindByCodeAsync(tenantCode, cancellationToken)
|
||||
: string.IsNullOrWhiteSpace(host)
|
||||
? null
|
||||
: await tenantDirectory.FindByHostAsync(host, cancellationToken);
|
||||
|
||||
TenantLookupResult? tenant = directoryEntry is null
|
||||
? null
|
||||
: new TenantLookupResult(
|
||||
directoryEntry.TenantId,
|
||||
directoryEntry.TenantCode,
|
||||
directoryEntry.Name,
|
||||
directoryEntry.Status,
|
||||
directoryEntry.Mode,
|
||||
directoryEntry.Host);
|
||||
|
||||
if (tenant is null)
|
||||
{
|
||||
@@ -42,6 +62,11 @@ public sealed class TenantPublicController(TikuDbContext dbContext) : Controller
|
||||
});
|
||||
}
|
||||
|
||||
tenantContextInitializer.Initialize(
|
||||
tenant.Id,
|
||||
tenant.Slug,
|
||||
tenant.Host is null ? TenantResolutionSource.TenantCode : TenantResolutionSource.Host);
|
||||
|
||||
return Ok(await BuildResponseAsync(tenant, tenant.Host, cancellationToken));
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Tiku.Api.Controllers;
|
||||
[Route("api/tenants")]
|
||||
public sealed class TenantsController(
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant,
|
||||
ITenantContext currentTenant,
|
||||
TikuDbContext dbContext) : ControllerBase
|
||||
{
|
||||
[HttpGet("current")]
|
||||
|
||||
Reference in New Issue
Block a user