forked from xiongyuxing/tiku-backend.net
feat: harden SaaS authentication and authorization
This commit is contained in:
@@ -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/unionid,upsert 用户身份并创建应用会话。")]
|
||||
[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_key,upsert 用户身份并创建应用会话。")]
|
||||
[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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user