feat: harden SaaS authentication and authorization

This commit is contained in:
2026-07-28 12:15:51 +08:00
parent f22f329d33
commit 5d2248efee
123 changed files with 9090 additions and 2822 deletions

View File

@@ -1,10 +1,14 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using Tiku.Application.Auth;
using Tiku.Api.Contracts;
using Tiku.Api.Options;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
using Tiku.Infrastructure.Content;
using Tiku.Domain.Tenancy;
namespace Tiku.Api.Controllers;
@@ -13,97 +17,144 @@ namespace Tiku.Api.Controllers;
[Produces("application/json")]
public sealed class AuthController(
IAuthService authService,
ISessionService sessionService,
ISmsVerificationService smsVerificationService,
IAuthSessionStore sessionStore,
ITenantContext tenantContext,
ITenantContextInitializer tenantContextInitializer,
ITenantDirectory tenantDirectory) : ControllerBase
ITenantDirectory tenantDirectory,
ICurrentUser currentUser,
IOptions<TenantResolutionOptions> tenantResolutionOptions) : ControllerBase
{
[AllowAnonymous]
[EnableRateLimiting(AuthRateLimitPolicies.Sms)]
[HttpPost("sms/send")]
[ProducesResponseType<SmsSendResult>(StatusCodes.Status202Accepted)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status429TooManyRequests)]
public async Task<ActionResult<SmsSendResult>> SendSmsCode(
[FromBody] SendSmsCodeDto request,
CancellationToken cancellationToken)
{
var realm = request.Realm!.Value;
if (realm != AuthRealm.Tenant)
{
throw new RequiredFieldException("SMS authentication is only available in the tenant realm.");
}
var tenantId = await ResolveRealmTenantIdAsync(realm, request.TenantCode, cancellationToken)
?? throw new RequiredFieldException("tenantCode is required for SMS authentication.");
var result = await smsVerificationService.CreateCodeAsync(
new SendSmsCodeRequest(
tenantId,
request.Phone,
SmsPurpose.Login,
GetIpAddress(),
Request.Headers.UserAgent.ToString(),
request.DeviceId),
cancellationToken);
return Accepted(result);
}
[AllowAnonymous]
[EnableRateLimiting(AuthRateLimitPolicies.Password)]
[HttpPost("login/password")]
[EndpointSummary("手机号密码登录")]
[EndpointDescription("使用本地手机号和密码登录,签发 JWT access token 与数据库 refresh/session。")]
[ProducesResponseType<AuthenticatedUserDto>(StatusCodes.Status200OK)]
[ProducesResponseType<AuthenticationResultDto>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<AuthenticatedUserDto>> LoginWithPassword(
public async Task<ActionResult<AuthenticationResultDto>> LoginWithPassword(
[FromBody] PasswordLoginDto request,
CancellationToken cancellationToken)
{
var realm = request.Realm!.Value;
var identifier = request.Identifier ?? request.Phone;
if (string.IsNullOrWhiteSpace(identifier))
{
throw new RequiredFieldException("identifier is required.");
}
var result = await authService.LoginWithPasswordAsync(
new PasswordLoginRequest(
await ResolveTenantIdAsync(request.TenantCode, cancellationToken),
request.Phone,
realm,
await ResolveRealmTenantIdAsync(realm, request.TenantCode, cancellationToken),
identifier,
request.Password,
GetIpAddress(),
Request.Headers.UserAgent.ToString()),
cancellationToken);
return Ok(AuthenticatedUserDto.FromApplication(result));
return Ok(AuthenticationResultDto.FromApplication(result));
}
[AllowAnonymous]
[HttpPost("login/sms")]
[EndpointSummary("短信验证码登录")]
[EndpointDescription("校验已发送的登录用途短信验证码,成功后签发 JWT access token 与数据库 refresh/session。")]
[ProducesResponseType<AuthenticatedUserDto>(StatusCodes.Status200OK)]
[ProducesResponseType<AuthenticationResultDto>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<AuthenticatedUserDto>> LoginWithSms(
public async Task<ActionResult<AuthenticationResultDto>> LoginWithSms(
[FromBody] SmsLoginDto request,
CancellationToken cancellationToken)
{
var realm = request.Realm!.Value;
var result = await authService.LoginWithSmsAsync(
new SmsLoginRequest(
await ResolveTenantIdAsync(request.TenantCode, cancellationToken),
realm,
await ResolveRealmTenantIdAsync(realm, request.TenantCode, cancellationToken),
request.Phone,
request.Code,
GetIpAddress(),
Request.Headers.UserAgent.ToString()),
cancellationToken);
return Ok(AuthenticatedUserDto.FromApplication(result));
return Ok(AuthenticationResultDto.FromApplication(result));
}
[AllowAnonymous]
[HttpPost("oauth/wechat")]
[EndpointSummary("微信网页 OAuth 登录")]
[EndpointDescription("使用微信网页授权 code 换取 openid/unionidupsert 用户身份并创建应用会话。")]
[ProducesResponseType<AuthenticatedUserDto>(StatusCodes.Status200OK)]
[ProducesResponseType<AuthenticationResultDto>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status401Unauthorized)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status503ServiceUnavailable)]
public async Task<ActionResult<AuthenticatedUserDto>> LoginWithWechatWeb(
public async Task<ActionResult<AuthenticationResultDto>> LoginWithWechatWeb(
[FromBody] OAuthCodeDto request,
CancellationToken cancellationToken)
{
var realm = request.Realm!.Value;
var result = await authService.LoginWithWechatWebAsync(
new WechatLoginRequest(
await ResolveTenantIdAsync(request.TenantCode, cancellationToken),
realm,
await ResolveRealmTenantIdAsync(realm, request.TenantCode, cancellationToken),
request.Code,
GetIpAddress(),
Request.Headers.UserAgent.ToString()),
cancellationToken);
return Ok(AuthenticatedUserDto.FromApplication(result));
return Ok(AuthenticationResultDto.FromApplication(result));
}
[AllowAnonymous]
[HttpPost("oauth/wechat-miniapp")]
[EndpointSummary("微信小程序登录")]
[EndpointDescription("使用小程序 wx.login 返回的 code 换取 openid/session_keyupsert 用户身份并创建应用会话。")]
[ProducesResponseType<AuthenticatedUserDto>(StatusCodes.Status200OK)]
[ProducesResponseType<AuthenticationResultDto>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status401Unauthorized)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status503ServiceUnavailable)]
public async Task<ActionResult<AuthenticatedUserDto>> LoginWithWechatMiniApp(
public async Task<ActionResult<AuthenticationResultDto>> LoginWithWechatMiniApp(
[FromBody] OAuthCodeDto request,
CancellationToken cancellationToken)
{
var realm = request.Realm!.Value;
var result = await authService.LoginWithWechatMiniAppAsync(
new WechatLoginRequest(
await ResolveTenantIdAsync(request.TenantCode, cancellationToken),
realm,
await ResolveRealmTenantIdAsync(realm, request.TenantCode, cancellationToken),
request.Code,
GetIpAddress(),
Request.Headers.UserAgent.ToString()),
cancellationToken);
return Ok(AuthenticatedUserDto.FromApplication(result));
return Ok(AuthenticationResultDto.FromApplication(result));
}
[AllowAnonymous]
@@ -142,14 +193,135 @@ public sealed class AuthController(
return NoContent();
}
[HttpPost("logout-all")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> LogoutAll(CancellationToken cancellationToken)
{
if (currentUser.UserId is not { } userId)
{
return Unauthorized();
}
await authService.LogoutAllAsync(userId, cancellationToken);
return NoContent();
}
[AllowAnonymous]
[EnableRateLimiting(AuthRateLimitPolicies.Mfa)]
[HttpPost("mfa/totp/setup")]
public async Task<ActionResult<MfaSetupResult>> SetupTotp(
[FromBody] MfaChallengeDto request,
CancellationToken cancellationToken)
{
ResolveAuthChallengeTenant(request.ChallengeToken);
var result = await authService.SetupTotpAsync(
new MfaChallengeRequest(
request.ChallengeToken, null, GetIpAddress(), Request.Headers.UserAgent.ToString()),
cancellationToken);
return Ok(result);
}
[AllowAnonymous]
[EnableRateLimiting(AuthRateLimitPolicies.Mfa)]
[HttpPost("mfa/totp/confirm")]
public async Task<ActionResult<MfaConfirmDto>> ConfirmTotp(
[FromBody] MfaChallengeDto request,
CancellationToken cancellationToken)
{
ResolveAuthChallengeTenant(request.ChallengeToken);
var result = await authService.ConfirmTotpAsync(
new MfaChallengeRequest(
request.ChallengeToken, request.Code, GetIpAddress(), Request.Headers.UserAgent.ToString()),
cancellationToken);
return Ok(new MfaConfirmDto
{
Authentication = AuthenticationResultDto.FromApplication(result.Authentication),
RecoveryCodes = result.RecoveryCodes
});
}
[AllowAnonymous]
[EnableRateLimiting(AuthRateLimitPolicies.Mfa)]
[HttpPost("mfa/totp/verify")]
public async Task<ActionResult<AuthenticationResultDto>> VerifyTotp(
[FromBody] MfaChallengeDto request,
CancellationToken cancellationToken)
{
ResolveAuthChallengeTenant(request.ChallengeToken);
var result = await authService.VerifyTotpAsync(
new MfaChallengeRequest(
request.ChallengeToken, request.Code, GetIpAddress(), Request.Headers.UserAgent.ToString()),
cancellationToken);
return Ok(AuthenticationResultDto.FromApplication(result));
}
[AllowAnonymous]
[HttpPost("password/change-required")]
[EnableRateLimiting(AuthRateLimitPolicies.Mfa)]
public async Task<ActionResult<AuthenticationResultDto>> ChangeRequiredPassword(
[FromBody] RequiredPasswordChangeDto request,
CancellationToken cancellationToken)
{
ResolveAuthChallengeTenant(request.ChallengeToken);
var result = await authService.ChangeRequiredPasswordAsync(
new PasswordChangeChallengeRequest(
request.ChallengeToken, request.NewPassword, GetIpAddress(), Request.Headers.UserAgent.ToString()),
cancellationToken);
return Ok(AuthenticationResultDto.FromApplication(result));
}
private void ResolveRefreshTokenTenant(string refreshToken)
{
if (!sessionService.TryParseRefreshToken(refreshToken, out var locator))
if (!sessionStore.TryParseRefreshToken(refreshToken, out var locator))
{
return;
}
tenantContextInitializer.Initialize(locator.TenantId, null, TenantResolutionSource.RefreshToken);
if (locator.Realm == AuthRealm.Platform)
{
EnsurePlatformHost();
if (tenantContext.IsResolved)
{
throw new TenantContextConflictException(tenantContext.TenantId!.Value, Guid.Empty);
}
return;
}
if (!tenantContext.IsResolved)
{
throw new RequiredFieldException(
"tenant refresh/logout requires a tenant host or x-tenant-code matching the refresh token.");
}
tenantContextInitializer.Initialize(locator.TenantId!.Value, null, TenantResolutionSource.RefreshToken);
}
private void ResolveAuthChallengeTenant(string challengeToken)
{
var parts = challengeToken.Split('.', 4, StringSplitOptions.None);
if (parts.Length != 4 || parts[0] != "c1")
{
return;
}
if (parts[1] == "p" && parts[2] == "-")
{
EnsurePlatformHost();
if (tenantContext.IsResolved)
{
throw new TenantContextConflictException(tenantContext.TenantId!.Value, Guid.Empty);
}
return;
}
if (parts[1] != "t" || !Guid.TryParseExact(parts[2], "N", out var tenantId) || !tenantContext.IsResolved)
{
throw new RequiredFieldException(
"tenant authentication challenge requires a tenant host or x-tenant-code.");
}
tenantContextInitializer.Initialize(tenantId, null, TenantResolutionSource.RefreshToken);
}
private string? GetIpAddress()
@@ -157,8 +329,22 @@ public sealed class AuthController(
return HttpContext.Connection.RemoteIpAddress?.ToString();
}
private async Task<Guid> ResolveTenantIdAsync(string? tenantCode, CancellationToken cancellationToken)
private async Task<Guid?> ResolveRealmTenantIdAsync(
AuthRealm realm,
string? tenantCode,
CancellationToken cancellationToken)
{
if (realm == AuthRealm.Platform)
{
EnsurePlatformHost();
if (tenantContext.IsResolved || !string.IsNullOrWhiteSpace(tenantCode))
{
throw new RequiredFieldException("platform realm does not accept tenantCode and must use a platform host.");
}
return null;
}
if (tenantContext.TenantId.HasValue)
{
if (!string.IsNullOrWhiteSpace(tenantCode) &&
@@ -189,4 +375,17 @@ public sealed class AuthController(
TenantResolutionSource.TenantCode);
return tenant.TenantId;
}
private void EnsurePlatformHost()
{
var requestHost = Request.Host.Host.Trim().TrimEnd('.');
if (!tenantResolutionOptions.Value.PlatformHosts.Any(host =>
string.Equals(
host.Trim().TrimEnd('.'),
requestHost,
StringComparison.OrdinalIgnoreCase)))
{
throw new RequiredFieldException("platform realm is only available on a configured platform host.");
}
}
}

View File

@@ -8,7 +8,7 @@ namespace Tiku.Api.Controllers;
[ApiController]
[Route("api/backoffice/tenant/jobs")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantJobManage)]
public sealed class BackgroundJobsController(
IBackgroundJobService backgroundJobService,
ITenantContext tenantContext) : ControllerBase

View File

@@ -10,113 +10,117 @@ namespace Tiku.Api.Controllers;
[Route("api/backoffice")]
public sealed class BackofficeController(
IBackofficeService backofficeService,
ICurrentUser currentUser,
ITenantContext tenantContext) : ControllerBase
ICurrentAccessContext currentAccessContext) : ControllerBase
{
[HttpGet("tenant/ui-bootstrap")]
[Authorize(Policy = TikuPolicies.TenantBackofficeBootstrap)]
[ProducesResponseType<BackofficeUiBootstrap>(StatusCodes.Status200OK)]
public async Task<ActionResult<BackofficeUiBootstrap>> GetTenantUiBootstrap(CancellationToken cancellationToken)
{
return Ok(await backofficeService.GetTenantUiBootstrapAsync(
await currentAccessContext.GetAsync(cancellationToken),
cancellationToken));
}
[HttpGet("tenant/bootstrap")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantRoleManage)]
[ProducesResponseType<BackofficeBootstrap>(StatusCodes.Status200OK)]
public async Task<ActionResult<BackofficeBootstrap>> GetTenantBootstrap(CancellationToken cancellationToken)
{
return Ok(await backofficeService.GetTenantBootstrapAsync(ResolveTenantActor(), cancellationToken));
return Ok(await backofficeService.GetTenantBootstrapAsync(await ResolveTenantActorAsync(cancellationToken), cancellationToken));
}
[HttpGet("platform/ui-bootstrap")]
[Authorize(Policy = TikuPolicies.PlatformBackofficeBootstrap)]
[ProducesResponseType<BackofficeUiBootstrap>(StatusCodes.Status200OK)]
public async Task<ActionResult<BackofficeUiBootstrap>> GetPlatformUiBootstrap(CancellationToken cancellationToken)
{
return Ok(await backofficeService.GetPlatformUiBootstrapAsync(
await currentAccessContext.GetAsync(cancellationToken),
cancellationToken));
}
[HttpPost("tenant/roles")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantRoleManage)]
[ProducesResponseType<BackofficeRoleItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<BackofficeRoleItem>> UpsertTenantRole(
UpsertBackofficeRoleDto request,
CancellationToken cancellationToken)
{
return Ok(await backofficeService.UpsertTenantRoleAsync(ResolveTenantActor(), request.ToCommand(), cancellationToken));
return Ok(await backofficeService.UpsertTenantRoleAsync(await ResolveTenantActorAsync(cancellationToken), request.ToCommand(), cancellationToken));
}
[HttpPut("tenant/roles/{roleId:guid}/bindings")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantRoleManage)]
[ProducesResponseType<BackofficeRoleItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<BackofficeRoleItem>> ReplaceTenantRoleBindings(
Guid roleId,
ReplaceRoleBindingsDto request,
CancellationToken cancellationToken)
{
return Ok(await backofficeService.ReplaceTenantRoleBindingsAsync(ResolveTenantActor(), request.ToCommand(roleId), cancellationToken));
return Ok(await backofficeService.ReplaceTenantRoleBindingsAsync(await ResolveTenantActorAsync(cancellationToken), request.ToCommand(roleId), cancellationToken));
}
[HttpPut("tenant/users/{userId:guid}/roles")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantRoleManage)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> ReplaceTenantUserRoles(
Guid userId,
ReplaceUserRolesDto request,
CancellationToken cancellationToken)
{
await backofficeService.ReplaceTenantUserRolesAsync(ResolveTenantActor(), request.ToCommand(userId), cancellationToken);
await backofficeService.ReplaceTenantUserRolesAsync(await ResolveTenantActorAsync(cancellationToken), request.ToCommand(userId), cancellationToken);
return NoContent();
}
[HttpGet("platform/bootstrap")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.PlatformRoleManage)]
[ProducesResponseType<BackofficeBootstrap>(StatusCodes.Status200OK)]
public async Task<ActionResult<BackofficeBootstrap>> GetPlatformBootstrap(CancellationToken cancellationToken)
{
return Ok(await backofficeService.GetPlatformBootstrapAsync(ResolvePlatformActor(), cancellationToken));
return Ok(await backofficeService.GetPlatformBootstrapAsync(await ResolvePlatformActorAsync(cancellationToken), cancellationToken));
}
[HttpPost("platform/roles")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.PlatformRoleManage)]
[ProducesResponseType<BackofficeRoleItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<BackofficeRoleItem>> UpsertPlatformRole(
UpsertBackofficeRoleDto request,
CancellationToken cancellationToken)
{
return Ok(await backofficeService.UpsertPlatformRoleAsync(ResolvePlatformActor(), request.ToCommand(), cancellationToken));
return Ok(await backofficeService.UpsertPlatformRoleAsync(await ResolvePlatformActorAsync(cancellationToken), request.ToCommand(), cancellationToken));
}
[HttpPut("platform/roles/{roleId:guid}/bindings")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.PlatformRoleManage)]
[ProducesResponseType<BackofficeRoleItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<BackofficeRoleItem>> ReplacePlatformRoleBindings(
Guid roleId,
ReplaceRoleBindingsDto request,
CancellationToken cancellationToken)
{
return Ok(await backofficeService.ReplacePlatformRoleBindingsAsync(ResolvePlatformActor(), request.ToCommand(roleId), cancellationToken));
return Ok(await backofficeService.ReplacePlatformRoleBindingsAsync(await ResolvePlatformActorAsync(cancellationToken), request.ToCommand(roleId), cancellationToken));
}
[HttpPut("platform/users/{userId:guid}/roles")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.PlatformRoleManage)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> ReplacePlatformUserRoles(
Guid userId,
ReplaceUserRolesDto request,
CancellationToken cancellationToken)
{
await backofficeService.ReplacePlatformUserRolesAsync(ResolvePlatformActor(), request.ToCommand(userId), cancellationToken);
await backofficeService.ReplacePlatformUserRolesAsync(await ResolvePlatformActorAsync(cancellationToken), request.ToCommand(userId), cancellationToken);
return NoContent();
}
private BackofficeActor ResolveTenantActor()
private async Task<BackofficeActor> ResolveTenantActorAsync(CancellationToken cancellationToken)
{
if (currentUser.UserId is not { } userId || tenantContext.TenantId is not { } tenantId)
{
throw new InvalidOperationException("Tenant backoffice actor was not resolved.");
}
return new BackofficeActor(userId, tenantId, IsPlatformAdmin());
return BackofficeActor.FromTenantAccess(await currentAccessContext.GetAsync(cancellationToken));
}
private BackofficeActor ResolvePlatformActor()
private async Task<BackofficeActor> ResolvePlatformActorAsync(CancellationToken cancellationToken)
{
if (currentUser.UserId is not { } userId)
{
throw new InvalidOperationException("Platform backoffice actor was not resolved.");
}
return new BackofficeActor(userId, tenantContext.TenantId, IsPlatformAdmin());
}
private bool IsPlatformAdmin()
{
return string.Equals(currentUser.TenantRole, "PlatformAdmin", StringComparison.OrdinalIgnoreCase);
return BackofficeActor.FromPlatformAccess(await currentAccessContext.GetAsync(cancellationToken));
}
}

View File

@@ -7,7 +7,7 @@ using Tiku.Application.Security;
namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantCommissionManage)]
[Produces("application/json")]
[Route("api/commission")]
public sealed class CommissionController(

View File

@@ -7,7 +7,7 @@ using Tiku.Application.Security;
namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantCrmManage)]
[Produces("application/json")]
[Route("api/crm")]
public sealed class CrmController(

View File

@@ -95,7 +95,7 @@ public sealed class ReferralController(
}
[HttpGet("stats")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantCrmManage)]
[EndpointSummary("查询推荐人个人统计")]
[ProducesResponseType<ReferralStatsItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralStatsItem>> Stats(
@@ -109,7 +109,7 @@ public sealed class ReferralController(
}
[HttpGet("sales-stats")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantCrmManage)]
[EndpointSummary("查询销售推荐统计排行")]
[ProducesResponseType<ReferralList<ReferralStatsItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralList<ReferralStatsItem>>> SalesStats(
@@ -123,7 +123,7 @@ public sealed class ReferralController(
}
[HttpGet("conversion-report")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantCrmManage)]
[EndpointSummary("查询推荐转化报告")]
[ProducesResponseType<ReferralConversionReport>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralConversionReport>> ConversionReport(
@@ -137,7 +137,7 @@ public sealed class ReferralController(
}
[HttpGet("sales-clients")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantCrmManage)]
[EndpointSummary("查询推荐人名下客户")]
[ProducesResponseType<ReferralList<ReferralLeadItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralList<ReferralLeadItem>>> SalesClients(
@@ -151,7 +151,7 @@ public sealed class ReferralController(
}
[HttpPost("manual-bind")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantCrmManage)]
[EndpointSummary("人工调整学生推荐归属")]
[ProducesResponseType<ReferralBindResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralBindResult>> ManualBind(
@@ -165,7 +165,7 @@ public sealed class ReferralController(
}
[HttpGet("team")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantCrmManage)]
[EndpointSummary("查询推荐团队成员")]
[ProducesResponseType<ReferralList<ReferralTeamItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralList<ReferralTeamItem>>> Team(
@@ -179,7 +179,7 @@ public sealed class ReferralController(
}
[HttpPut("team")]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantCrmManage)]
[EndpointSummary("新增或更新推荐团队关系")]
[ProducesResponseType<ReferralTeamItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<ReferralTeamItem>> UpsertTeam(

View File

@@ -28,8 +28,7 @@ public sealed class SecurityDiagnosticsController(
{
return Ok(new
{
currentTenant.TenantId,
currentUser.TenantRole
currentTenant.TenantId
});
}
@@ -39,8 +38,7 @@ public sealed class SecurityDiagnosticsController(
{
return Ok(new
{
currentTenant.TenantId,
currentUser.TenantRole
currentTenant.TenantId
});
}
}

View File

@@ -21,7 +21,7 @@ public sealed class TaxonomyController(
}
[HttpPost]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantContentManage)]
public Task<TaxonomyNodeItem> Create(
CreateTaxonomyNodeDto request,
CancellationToken cancellationToken)

View File

@@ -10,7 +10,6 @@ using Tiku.Domain.Tenancy;
namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Produces("application/json")]
[Route("api/tenant-admin")]
public sealed class TenantAdminDirectController(
@@ -19,6 +18,7 @@ public sealed class TenantAdminDirectController(
ITenantContext currentTenant) : ControllerBase
{
[HttpGet("classes")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询租户班级")]
[ProducesResponseType<TenantAdminClassList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminClassList>> GetClasses(
@@ -29,6 +29,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPut("classes")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新租户班级")]
[ProducesResponseType<ContentManagementResult<TenantAdminClassItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminClassItem>>> UpsertClass(
@@ -39,6 +40,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPost("classes/disable")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("停用租户班级")]
[ProducesResponseType<ContentManagementResult<TenantAdminClassItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminClassItem>>> DisableClass(
@@ -49,6 +51,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("classes/members")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询班级成员")]
[ProducesResponseType<CatalogList<TenantAdminClassMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminClassMemberItem>>> GetClassMembers(
@@ -59,6 +62,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPut("classes/members")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新班级成员")]
[ProducesResponseType<ContentManagementResult<TenantAdminClassMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminClassMemberItem>>> UpsertClassMember(
@@ -69,6 +73,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPost("classes/members/remove")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("移除班级成员")]
[ProducesResponseType<ContentManagementResult<TenantAdminClassMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminClassMemberItem>>> RemoveClassMember(
@@ -79,6 +84,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("students")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询租户学生")]
[ProducesResponseType<TenantAdminStudentList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminStudentList>> GetStudents(
@@ -89,6 +95,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPut("students")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新租户学生档案")]
[ProducesResponseType<ContentManagementResult<TenantAdminStudentItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminStudentItem>>> UpsertStudent(
@@ -99,6 +106,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPost("students/status")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("更新租户学生状态")]
[ProducesResponseType<ContentManagementResult<TenantAdminStudentStatusItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminStudentStatusItem>>> UpdateStudentStatus(
@@ -109,6 +117,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("student-notes")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询学生备注")]
[ProducesResponseType<CatalogList<TenantAdminStudentNoteItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminStudentNoteItem>>> GetStudentNotes(
@@ -119,6 +128,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPut("student-notes")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新学生备注")]
[ProducesResponseType<ContentManagementResult<TenantAdminStudentNoteItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminStudentNoteItem>>> UpsertStudentNote(
@@ -129,6 +139,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("student-followups")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询学生跟进")]
[ProducesResponseType<CatalogList<TenantAdminStudentFollowupItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminStudentFollowupItem>>> GetStudentFollowups(
@@ -139,6 +150,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPut("student-followups")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新学生跟进")]
[ProducesResponseType<ContentManagementResult<TenantAdminStudentFollowupItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminStudentFollowupItem>>> UpsertStudentFollowup(
@@ -149,6 +161,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("members")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("查询租户成员")]
[ProducesResponseType<CatalogList<TenantAdminMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminMemberItem>>> GetMembers(
@@ -159,6 +172,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPut("members")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("新增或更新租户成员")]
[ProducesResponseType<ContentManagementResult<TenantAdminMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminMemberItem>>> UpsertMember(
@@ -169,6 +183,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPost("members/disable")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("停用租户成员并撤销会话")]
[ProducesResponseType<ContentManagementResult<TenantAdminMemberItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminMemberItem>>> DisableMember(
@@ -179,6 +194,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("audit-logs")]
[Authorize(Policy = BackendPermissions.TenantStaffManage)]
[EndpointSummary("查询租户审计日志")]
[ProducesResponseType<CatalogList<TenantAdminAuditLogItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminAuditLogItem>>> GetAuditLogs(
@@ -188,45 +204,8 @@ public sealed class TenantAdminDirectController(
return Ok(await tenantAdminService.GetAuditLogsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpGet("permissions")]
[EndpointSummary("查询租户后台权限矩阵")]
[ProducesResponseType<TenantAdminPermissionMatrix>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantAdminPermissionMatrix>> GetPermissions(CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetPermissionMatrixAsync(ResolveActor(), cancellationToken));
}
[HttpGet("role-templates")]
[EndpointSummary("查询租户角色模板")]
[ProducesResponseType<CatalogList<TenantAdminRoleTemplateItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminRoleTemplateItem>>> GetRoleTemplates(
[FromQuery] TenantAdminRoleTemplateQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.GetRoleTemplatesAsync(ResolveActor(), query.ToFilter(), cancellationToken));
}
[HttpPut("role-templates")]
[EndpointSummary("新增或更新租户角色模板")]
[ProducesResponseType<ContentManagementResult<TenantAdminRoleTemplateItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminRoleTemplateItem>>> UpsertRoleTemplate(
UpsertTenantAdminRoleTemplateDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.UpsertRoleTemplateAsync(ResolveActor(), request.ToCommand(), cancellationToken));
}
[HttpPost("role-templates/disable")]
[EndpointSummary("停用租户角色模板")]
[ProducesResponseType<ContentManagementResult<TenantAdminRoleTemplateItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminRoleTemplateItem>>> DisableRoleTemplate(
DisableTenantAdminRoleTemplateDto request,
CancellationToken cancellationToken)
{
return Ok(await tenantAdminService.DisableRoleTemplateAsync(ResolveActor(), request.RoleTemplateId, cancellationToken));
}
[HttpPut("branding")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("更新租户品牌信息")]
[ProducesResponseType<ContentManagementResult<TenantBrandingItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantBrandingItem>>> UpsertBranding(
@@ -237,6 +216,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPut("settings")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("更新租户公开设置与功能开关")]
[ProducesResponseType<ContentManagementResult<TenantSettingsItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantSettingsItem>>> UpsertSettings(
@@ -247,6 +227,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("theme-templates")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("查询可用租户主题模板")]
[ProducesResponseType<CatalogList<TenantThemeTemplateItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantThemeTemplateItem>>> GetThemeTemplates(CancellationToken cancellationToken)
@@ -255,6 +236,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("theme")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("查询租户当前主题与草稿")]
[ProducesResponseType<ContentManagementResult<TenantThemeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> GetTheme(CancellationToken cancellationToken)
@@ -263,6 +245,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPost("theme/preview")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("生成租户主题草稿")]
[ProducesResponseType<ContentManagementResult<TenantThemeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> PreviewTheme(
@@ -273,6 +256,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPost("theme/publish")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("发布租户主题")]
[ProducesResponseType<ContentManagementResult<TenantThemeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> PublishTheme(
@@ -283,6 +267,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("domains")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("查询租户域名")]
[ProducesResponseType<CatalogList<TenantDomainItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantDomainItem>>> GetDomains(CancellationToken cancellationToken)
@@ -291,6 +276,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPost("domains")]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[EndpointSummary("添加租户域名")]
[ProducesResponseType<ContentManagementResult<TenantDomainItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantDomainItem>>> CreateDomain(
@@ -301,6 +287,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("auth-providers")]
[Authorize(Policy = BackendPermissions.TenantProviderManage)]
[EndpointSummary("查询租户登录 Provider 公开配置")]
[ProducesResponseType<CatalogList<TenantIdentityProviderItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantIdentityProviderItem>>> GetAuthProviders(CancellationToken cancellationToken)
@@ -309,6 +296,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPut("auth-providers")]
[Authorize(Policy = BackendPermissions.TenantProviderManage)]
[EndpointSummary("新增或更新租户登录 Provider")]
[ProducesResponseType<ContentManagementResult<TenantIdentityProviderItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantIdentityProviderItem>>> UpsertAuthProvider(
@@ -319,6 +307,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("badges")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询租户勋章")]
[ProducesResponseType<CatalogList<TenantAdminBadgeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminBadgeItem>>> GetBadges(
@@ -329,6 +318,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPut("badges")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新租户勋章")]
[ProducesResponseType<ContentManagementResult<TenantAdminBadgeItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminBadgeItem>>> UpsertBadge(
@@ -339,6 +329,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("badge-grants")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询勋章发放记录")]
[ProducesResponseType<CatalogList<TenantAdminBadgeGrantItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminBadgeGrantItem>>> GetBadgeGrants(
@@ -349,6 +340,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPost("badge-grants")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("向租户成员发放勋章")]
[ProducesResponseType<ContentManagementResult<TenantAdminBadgeGrantItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminBadgeGrantItem>>> GrantBadge(
@@ -359,6 +351,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("notifications")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询用户站内通知")]
[ProducesResponseType<CatalogList<TenantAdminNotificationItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminNotificationItem>>> GetNotifications(
@@ -369,6 +362,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPut("notifications")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("新增或更新用户站内通知")]
[ProducesResponseType<ContentManagementResult<TenantAdminNotificationItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminNotificationItem>>> UpsertNotification(
@@ -379,6 +373,7 @@ public sealed class TenantAdminDirectController(
}
[HttpGet("feedbacks")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("查询用户反馈")]
[ProducesResponseType<CatalogList<TenantAdminFeedbackItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<TenantAdminFeedbackItem>>> GetFeedbacks(
@@ -389,6 +384,7 @@ public sealed class TenantAdminDirectController(
}
[HttpPost("feedbacks/status")]
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
[EndpointSummary("处理用户反馈")]
[ProducesResponseType<ContentManagementResult<TenantAdminFeedbackItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<TenantAdminFeedbackItem>>> UpdateFeedback(
@@ -400,17 +396,13 @@ public sealed class TenantAdminDirectController(
private TenantAdminActor ResolveActor()
{
if (currentTenant.TenantId is null || currentUser.UserId is null)
try
{
throw new TenantAdminDirectException("Tenant admin actor was not resolved.", "tenant_admin_access_denied");
return TenantAdminActor.FromResolvedIdentity(currentTenant.TenantId, currentUser.UserId);
}
catch (InvalidOperationException exception)
{
throw new TenantAdminDirectException(exception.Message, "tenant_admin_access_denied");
}
var role = Enum.TryParse<TenantRole>(
currentUser.TenantRole?.Replace("_", string.Empty, StringComparison.Ordinal),
ignoreCase: true,
out var parsedRole)
? parsedRole
: TenantRole.TenantAdmin;
return new TenantAdminActor(currentTenant.TenantId.Value, currentUser.UserId.Value, role);
}
}

View File

@@ -8,7 +8,7 @@ using Tiku.Domain.Commerce;
namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantCommerceOperate)]
[Produces("application/json")]
[Route("api/tenant-commerce")]
public sealed class TenantCommerceController(
@@ -17,6 +17,7 @@ public sealed class TenantCommerceController(
ITenantContext currentTenant) : ControllerBase
{
[HttpGet("payment-accounts")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("查询租户支付账号")]
[ProducesResponseType<IReadOnlyCollection<TenantPaymentProviderItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyCollection<TenantPaymentProviderItem>>> PaymentAccounts(
@@ -30,6 +31,7 @@ public sealed class TenantCommerceController(
}
[HttpPut("payment-accounts")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("新增或更新租户支付账号")]
[ProducesResponseType<TenantPaymentProviderItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantPaymentProviderItem>> UpsertPaymentAccount(
@@ -43,6 +45,7 @@ public sealed class TenantCommerceController(
}
[HttpPut("secrets")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("写入或轮换租户密钥")]
[ProducesResponseType<TenantSecretItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantSecretItem>> UpsertSecret(
@@ -82,6 +85,7 @@ public sealed class TenantCommerceController(
}
[HttpPost("code-batches")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("创建兑换码批次")]
[ProducesResponseType<CodeBatchItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<CodeBatchItem>> CreateCodeBatch(
@@ -95,6 +99,7 @@ public sealed class TenantCommerceController(
}
[HttpGet("activation-codes")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("查询兑换码")]
[ProducesResponseType<ActivationCodeList>(StatusCodes.Status200OK)]
public async Task<ActionResult<ActivationCodeList>> ActivationCodes(
@@ -108,6 +113,7 @@ public sealed class TenantCommerceController(
}
[HttpPost("activation-codes/redeem")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("后台核销兑换码")]
[ProducesResponseType<ActivationCodeItem>(StatusCodes.Status200OK)]
public async Task<ActionResult<ActivationCodeItem>> RedeemActivationCode(
@@ -121,6 +127,7 @@ public sealed class TenantCommerceController(
}
[HttpGet("point-activity-tasks")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("查询积分活动任务")]
[ProducesResponseType<TenantPointTaskList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantPointTaskList>> PointTasks(
@@ -134,6 +141,7 @@ public sealed class TenantCommerceController(
}
[HttpPut("point-activity-tasks")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("新增或更新积分活动任务")]
[ProducesResponseType<object>(StatusCodes.Status200OK)]
public async Task<ActionResult<object>> UpsertPointTask(
@@ -147,6 +155,7 @@ public sealed class TenantCommerceController(
}
[HttpGet("point-activity-claims")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("查询积分任务领取记录")]
[ProducesResponseType<TenantPointClaimList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantPointClaimList>> PointClaims(
@@ -160,6 +169,7 @@ public sealed class TenantCommerceController(
}
[HttpGet("point-exchange-items")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("查询积分兑换项")]
[ProducesResponseType<TenantPointExchangeItemList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantPointExchangeItemList>> PointExchangeItems(
@@ -173,6 +183,7 @@ public sealed class TenantCommerceController(
}
[HttpPut("point-exchange-items")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("新增或更新积分兑换项")]
[ProducesResponseType<object>(StatusCodes.Status200OK)]
public async Task<ActionResult<object>> UpsertPointExchangeItem(
@@ -186,6 +197,7 @@ public sealed class TenantCommerceController(
}
[HttpGet("point-exchange-orders")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("查询积分兑换订单")]
[ProducesResponseType<TenantPointExchangeOrderList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantPointExchangeOrderList>> PointExchangeOrders(
@@ -199,6 +211,7 @@ public sealed class TenantCommerceController(
}
[HttpPost("point-exchange-orders/status")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("更新积分兑换订单状态")]
[ProducesResponseType<object>(StatusCodes.Status200OK)]
public async Task<ActionResult<object>> UpdatePointExchangeOrderStatus(
@@ -212,6 +225,7 @@ public sealed class TenantCommerceController(
}
[HttpGet("coupons")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("查询租户优惠券")]
[ProducesResponseType<TenantCouponList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantCouponList>> Coupons(
@@ -225,6 +239,7 @@ public sealed class TenantCommerceController(
}
[HttpPut("coupons")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("新增或更新租户优惠券")]
[ProducesResponseType<object>(StatusCodes.Status200OK)]
public async Task<ActionResult<object>> UpsertCoupon(
@@ -238,6 +253,7 @@ public sealed class TenantCommerceController(
}
[HttpGet("coupons/redemptions")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("查询优惠券领取和核销记录")]
[ProducesResponseType<TenantCouponRedemptionList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantCouponRedemptionList>> CouponRedemptions(
@@ -251,6 +267,7 @@ public sealed class TenantCommerceController(
}
[HttpGet("coupons/report")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("查询优惠券基础报表")]
[ProducesResponseType<TenantCouponReport>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantCouponReport>> CouponReport(
@@ -316,6 +333,7 @@ public sealed class TenantCommerceController(
}
[HttpGet("reconciliation/batches")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("查询对账批次")]
[ProducesResponseType<TenantReconciliationBatchList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantReconciliationBatchList>> ReconciliationBatches(
@@ -329,6 +347,7 @@ public sealed class TenantCommerceController(
}
[HttpPost("reconciliation/batches")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("创建对账批次")]
[ProducesResponseType<CommerceReconciliationBatch>(StatusCodes.Status200OK)]
public async Task<ActionResult<CommerceReconciliationBatch>> CreateReconciliationBatch(
@@ -342,6 +361,7 @@ public sealed class TenantCommerceController(
}
[HttpGet("reconciliation/issues")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("查询对账异常")]
[ProducesResponseType<TenantReconciliationIssueList>(StatusCodes.Status200OK)]
public async Task<ActionResult<TenantReconciliationIssueList>> ReconciliationIssues(
@@ -355,6 +375,7 @@ public sealed class TenantCommerceController(
}
[HttpPost("reconciliation/issues/status")]
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
[EndpointSummary("更新对账异常状态")]
[ProducesResponseType<CommerceReconciliationIssue>(StatusCodes.Status200OK)]
public async Task<ActionResult<CommerceReconciliationIssue>> UpdateReconciliationIssue(

View File

@@ -9,7 +9,7 @@ using Tiku.Application.Security;
namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantContentManage)]
[Produces("application/json")]
[Route("api/tenant-content")]
public sealed class TenantContentController(

View File

@@ -11,7 +11,7 @@ using Tiku.Domain.Content;
namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantContentManage)]
[Produces("application/json")]
[Route("api/tenant-content")]
public sealed class TenantContentDirectController(
@@ -20,6 +20,7 @@ public sealed class TenantContentDirectController(
ITenantContext currentTenant) : ControllerBase
{
[HttpPost("questions")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("创建题目及首个版本")]
[ProducesResponseType<ContentManagementResult<QuestionManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<QuestionManagementItem>>> CreateQuestion(
@@ -30,6 +31,7 @@ public sealed class TenantContentDirectController(
}
[HttpPatch("questions")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("更新题目并可选择创建新版本")]
[ProducesResponseType<ContentManagementResult<QuestionManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<QuestionManagementItem>>> UpdateQuestion(
@@ -60,6 +62,7 @@ public sealed class TenantContentDirectController(
}
[HttpGet("vocabulary-words")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("查询管理侧词汇")]
[ProducesResponseType<CatalogList<VocabularyWord>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<VocabularyWord>>> GetVocabularyWords(
@@ -70,6 +73,7 @@ public sealed class TenantContentDirectController(
}
[HttpPut("vocabulary-words")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("新增或更新词汇")]
[ProducesResponseType<ContentManagementResult<VocabularyWord>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<VocabularyWord>>> UpsertVocabularyWord(
@@ -100,6 +104,7 @@ public sealed class TenantContentDirectController(
}
[HttpGet("handbook-chapters")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("查询管理侧知识手册章节")]
[ProducesResponseType<CatalogList<HandbookChapter>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<HandbookChapter>>> GetHandbookChapters(
@@ -110,6 +115,7 @@ public sealed class TenantContentDirectController(
}
[HttpPut("handbook-chapters")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("新增或更新知识手册章节")]
[ProducesResponseType<ContentManagementResult<HandbookChapter>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<HandbookChapter>>> UpsertHandbookChapter(
@@ -120,6 +126,7 @@ public sealed class TenantContentDirectController(
}
[HttpGet("handbook-entries")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("查询管理侧知识手册条目")]
[ProducesResponseType<CatalogList<HandbookEntry>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<HandbookEntry>>> GetHandbookEntries(
@@ -130,6 +137,7 @@ public sealed class TenantContentDirectController(
}
[HttpPut("handbook-entries")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("新增或更新知识手册条目")]
[ProducesResponseType<ContentManagementResult<HandbookEntry>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<HandbookEntry>>> UpsertHandbookEntry(
@@ -240,6 +248,7 @@ public sealed class TenantContentDirectController(
}
[HttpGet("videos")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("查询租户视频解析")]
[ProducesResponseType<CatalogList<VideoManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<VideoManagementItem>>> GetVideos(
@@ -250,6 +259,7 @@ public sealed class TenantContentDirectController(
}
[HttpPut("videos")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("新增或更新视频解析")]
[ProducesResponseType<ContentManagementResult<VideoManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<VideoManagementItem>>> UpsertVideo(
@@ -260,6 +270,7 @@ public sealed class TenantContentDirectController(
}
[HttpPost("question-videos")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("绑定题目与解析视频")]
[ProducesResponseType<ContentManagementResult<QuestionVideoManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<QuestionVideoManagementItem>>> BindQuestionVideo(
@@ -270,6 +281,7 @@ public sealed class TenantContentDirectController(
}
[HttpGet("operations/{kind}")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("查询运营内容")]
[ProducesResponseType<CatalogList<OperationContentItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<OperationContentItem>>> GetOperationContent(
@@ -281,6 +293,7 @@ public sealed class TenantContentDirectController(
}
[HttpPut("operations/{kind}")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("新增或更新运营内容")]
[ProducesResponseType<ContentManagementResult<OperationContentItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<ContentManagementResult<OperationContentItem>>> UpsertOperationContent(
@@ -292,6 +305,7 @@ public sealed class TenantContentDirectController(
}
[HttpPost("imports/preview/{importType}")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("预览内容导入数据")]
[ProducesResponseType<SimpleImportResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<SimpleImportResult>> PreviewImport(
@@ -303,6 +317,7 @@ public sealed class TenantContentDirectController(
}
[HttpPost("imports/{importType}")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("执行同步内容导入")]
[ProducesResponseType<SimpleImportResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<SimpleImportResult>> ExecuteImport(
@@ -314,6 +329,7 @@ public sealed class TenantContentDirectController(
}
[HttpGet("imports/issues")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("查询内容导入问题明细")]
[ProducesResponseType<CatalogList<ContentImportIssueModel>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<ContentImportIssueModel>>> GetImportIssues(
@@ -324,6 +340,7 @@ public sealed class TenantContentDirectController(
}
[HttpPost("imports/post-check")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("执行内容导入后完整性检查")]
[ProducesResponseType<ImportPostCheckResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<ImportPostCheckResult>> RunImportPostCheck(
@@ -334,6 +351,7 @@ public sealed class TenantContentDirectController(
}
[HttpGet("imports/post-check")]
[Authorize(Policy = TikuPolicies.TenantContentManageAllScope)]
[EndpointSummary("查询内容导入后检查状态")]
[ProducesResponseType<ImportPostCheckResult>(StatusCodes.Status200OK)]
public async Task<ActionResult<ImportPostCheckResult>> GetImportPostCheck(

View File

@@ -7,7 +7,7 @@ using Tiku.Application.Tenancy;
namespace Tiku.Api.Controllers;
[ApiController]
[Authorize(Policy = TikuPolicies.TenantAdmin)]
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
[Produces("application/json")]
[Route("api/tenant-admin/frontend-config")]
public sealed class TenantFrontendConfigController(

View File

@@ -1,7 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using Tiku.Application.Auth;
using Tiku.Application.Security;
using Tiku.Domain.Tenancy;
@@ -39,8 +38,7 @@ public sealed class TenantsController(
tenant.Name,
tenant.Slug,
tenant.Status,
membership.Role,
membership.Permissions))
membership.Role))
.SingleOrDefaultAsync(cancellationToken);
if (result is null)
@@ -60,11 +58,9 @@ public sealed class TenantsController(
/// <param name="TenantSlug">租户编码。</param>
/// <param name="Status">租户状态。</param>
/// <param name="Role">当前用户在租户内的角色。</param>
/// <param name="Permissions">当前用户在租户内的权限扩展。</param>
public sealed record CurrentTenantResponse(
Guid TenantId,
string TenantName,
string TenantSlug,
TenantStatus Status,
TenantRole Role,
JsonElement Permissions);
TenantRole Role);