forked from xiongyuxing/tiku-backend.net
feat: add tenant admin configuration and governance endpoints
This commit is contained in:
@@ -67,6 +67,49 @@ public sealed class TenantAdminStudentActivityQueryDto
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TenantAdminMemberQueryDto
|
||||
{
|
||||
public string? Role { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
public TenantAdminMemberFilter ToFilter()
|
||||
{
|
||||
return new TenantAdminMemberFilter(Role, Status, Keyword, Limit);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TenantAdminAuditLogQueryDto
|
||||
{
|
||||
public string? Action { get; set; }
|
||||
public string? TargetType { get; set; }
|
||||
public Guid? ActorUserId { get; set; }
|
||||
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
public TenantAdminAuditLogFilter ToFilter()
|
||||
{
|
||||
return new TenantAdminAuditLogFilter(Action, TargetType, ActorUserId, Limit);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TenantAdminRoleTemplateQueryDto
|
||||
{
|
||||
public string? Status { get; set; }
|
||||
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
public TenantAdminRoleTemplateFilter ToFilter()
|
||||
{
|
||||
return new TenantAdminRoleTemplateFilter(Status, Limit);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UpsertTenantAdminClassDto
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
@@ -215,3 +258,151 @@ public sealed class UpsertTenantAdminStudentFollowupDto
|
||||
Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UpsertTenantAdminMemberDto
|
||||
{
|
||||
public Guid? MembershipId { get; set; }
|
||||
public TenantAdminUserLookupDto User { get; set; } = new();
|
||||
public string? Role { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public Guid? RoleTemplateId { get; set; }
|
||||
public JsonElement Permissions { get; set; } = JsonDefaults.Object();
|
||||
public string? PrimaryRole { get; set; }
|
||||
|
||||
public UpsertTenantAdminMemberCommand ToCommand()
|
||||
{
|
||||
return new UpsertTenantAdminMemberCommand(MembershipId, User.ToCommand(), Role, Status, RoleTemplateId, Permissions, PrimaryRole);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DisableTenantAdminMemberDto
|
||||
{
|
||||
[Required]
|
||||
public Guid MembershipId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpsertTenantAdminRoleTemplateDto
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
public string? Code { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? BaseRole { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public JsonElement Permissions { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement MenuPermissions { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement ModulePermissions { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement FieldPermissions { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement DataScope { get; set; } = JsonDefaults.Object();
|
||||
public int? Order { get; set; }
|
||||
|
||||
public UpsertTenantAdminRoleTemplateCommand ToCommand()
|
||||
{
|
||||
return new UpsertTenantAdminRoleTemplateCommand(
|
||||
Id,
|
||||
Code,
|
||||
Name,
|
||||
Description,
|
||||
BaseRole,
|
||||
Status,
|
||||
Permissions,
|
||||
MenuPermissions,
|
||||
ModulePermissions,
|
||||
FieldPermissions,
|
||||
DataScope,
|
||||
Order);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DisableTenantAdminRoleTemplateDto
|
||||
{
|
||||
[Required]
|
||||
public Guid RoleTemplateId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpsertTenantBrandingDto
|
||||
{
|
||||
public required string BrandName { get; set; }
|
||||
public string? ShortName { get; set; }
|
||||
public string? Slogan { get; set; }
|
||||
public string? OrganizationName { get; set; }
|
||||
public string? LogoUrl { get; set; }
|
||||
public string? FaviconUrl { get; set; }
|
||||
public string? ServiceWechat { get; set; }
|
||||
public string? ServiceAccountName { get; set; }
|
||||
|
||||
public UpsertTenantBrandingCommand ToCommand()
|
||||
{
|
||||
return new UpsertTenantBrandingCommand(
|
||||
BrandName,
|
||||
ShortName,
|
||||
Slogan,
|
||||
OrganizationName,
|
||||
LogoUrl,
|
||||
FaviconUrl,
|
||||
ServiceWechat,
|
||||
ServiceAccountName);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UpsertTenantSettingsDto
|
||||
{
|
||||
public JsonElement FeatureFlags { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement AdminFeatureFlags { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement PublicConfig { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public UpsertTenantSettingsCommand ToCommand()
|
||||
{
|
||||
return new UpsertTenantSettingsCommand(FeatureFlags, AdminFeatureFlags, PublicConfig);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PreviewTenantThemeDto
|
||||
{
|
||||
public required string TemplateCode { get; set; }
|
||||
public JsonElement Theme { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement PublicAssets { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public PreviewTenantThemeCommand ToCommand()
|
||||
{
|
||||
return new PreviewTenantThemeCommand(TemplateCode, Theme, PublicAssets);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PublishTenantThemeDto
|
||||
{
|
||||
public bool? UseDraft { get; set; }
|
||||
public string? TemplateCode { get; set; }
|
||||
public JsonElement Theme { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement PublicAssets { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public PublishTenantThemeCommand ToCommand()
|
||||
{
|
||||
return new PublishTenantThemeCommand(UseDraft ?? true, TemplateCode, Theme, PublicAssets);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CreateTenantDomainDto
|
||||
{
|
||||
public required string Host { get; set; }
|
||||
public string? DomainType { get; set; }
|
||||
public bool IsPrimary { get; set; }
|
||||
|
||||
public CreateTenantDomainCommand ToCommand()
|
||||
{
|
||||
return new CreateTenantDomainCommand(Host, DomainType, IsPrimary);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UpsertTenantAuthProviderDto
|
||||
{
|
||||
public required string Provider { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
public JsonElement ConfigPublic { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public UpsertTenantAuthProviderCommand ToCommand()
|
||||
{
|
||||
return new UpsertTenantAuthProviderCommand(Provider, Status, DisplayName, ConfigPublic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.TenantAdmin;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
@@ -147,6 +148,176 @@ public sealed class TenantAdminDirectController(
|
||||
return Ok(await tenantAdminService.UpsertStudentFollowupAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("members")]
|
||||
[EndpointSummary("查询租户成员")]
|
||||
[ProducesResponseType<CatalogList<TenantAdminMemberItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<TenantAdminMemberItem>>> GetMembers(
|
||||
[FromQuery] TenantAdminMemberQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.GetMembersAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("members")]
|
||||
[EndpointSummary("新增或更新租户成员")]
|
||||
[ProducesResponseType<ContentManagementResult<TenantAdminMemberItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<TenantAdminMemberItem>>> UpsertMember(
|
||||
UpsertTenantAdminMemberDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.UpsertMemberAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("members/disable")]
|
||||
[EndpointSummary("停用租户成员并撤销会话")]
|
||||
[ProducesResponseType<ContentManagementResult<TenantAdminMemberItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<TenantAdminMemberItem>>> DisableMember(
|
||||
DisableTenantAdminMemberDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.DisableMemberAsync(ResolveActor(), request.MembershipId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("audit-logs")]
|
||||
[EndpointSummary("查询租户审计日志")]
|
||||
[ProducesResponseType<CatalogList<TenantAdminAuditLogItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<TenantAdminAuditLogItem>>> GetAuditLogs(
|
||||
[FromQuery] TenantAdminAuditLogQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
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")]
|
||||
[EndpointSummary("更新租户品牌信息")]
|
||||
[ProducesResponseType<ContentManagementResult<TenantBrandingItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<TenantBrandingItem>>> UpsertBranding(
|
||||
UpsertTenantBrandingDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.UpsertBrandingAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("settings")]
|
||||
[EndpointSummary("更新租户公开设置与功能开关")]
|
||||
[ProducesResponseType<ContentManagementResult<TenantSettingsItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<TenantSettingsItem>>> UpsertSettings(
|
||||
UpsertTenantSettingsDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.UpsertSettingsAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("theme-templates")]
|
||||
[EndpointSummary("查询可用租户主题模板")]
|
||||
[ProducesResponseType<CatalogList<TenantThemeTemplateItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<TenantThemeTemplateItem>>> GetThemeTemplates(CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.GetThemeTemplatesAsync(ResolveActor(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("theme")]
|
||||
[EndpointSummary("查询租户当前主题与草稿")]
|
||||
[ProducesResponseType<ContentManagementResult<TenantThemeItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> GetTheme(CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.GetThemeAsync(ResolveActor(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("theme/preview")]
|
||||
[EndpointSummary("生成租户主题草稿")]
|
||||
[ProducesResponseType<ContentManagementResult<TenantThemeItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> PreviewTheme(
|
||||
PreviewTenantThemeDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.PreviewThemeAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("theme/publish")]
|
||||
[EndpointSummary("发布租户主题")]
|
||||
[ProducesResponseType<ContentManagementResult<TenantThemeItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> PublishTheme(
|
||||
PublishTenantThemeDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.PublishThemeAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("domains")]
|
||||
[EndpointSummary("查询租户域名")]
|
||||
[ProducesResponseType<CatalogList<TenantDomainItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<TenantDomainItem>>> GetDomains(CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.GetDomainsAsync(ResolveActor(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("domains")]
|
||||
[EndpointSummary("添加租户域名")]
|
||||
[ProducesResponseType<ContentManagementResult<TenantDomainItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<TenantDomainItem>>> CreateDomain(
|
||||
CreateTenantDomainDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.CreateDomainAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("auth-providers")]
|
||||
[EndpointSummary("查询租户登录 Provider 公开配置")]
|
||||
[ProducesResponseType<CatalogList<TenantAuthProviderItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<TenantAuthProviderItem>>> GetAuthProviders(CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.GetAuthProvidersAsync(ResolveActor(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("auth-providers")]
|
||||
[EndpointSummary("新增或更新租户登录 Provider")]
|
||||
[ProducesResponseType<ContentManagementResult<TenantAuthProviderItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<TenantAuthProviderItem>>> UpsertAuthProvider(
|
||||
UpsertTenantAuthProviderDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.UpsertAuthProviderAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
private TenantAdminActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
@@ -154,6 +325,12 @@ public sealed class TenantAdminDirectController(
|
||||
throw new TenantAdminDirectException("Tenant admin actor was not resolved.", "tenant_admin_access_denied");
|
||||
}
|
||||
|
||||
return new TenantAdminActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
var role = Enum.TryParse<TenantRole>(
|
||||
currentTenant.Role?.Replace("_", string.Empty, StringComparison.Ordinal),
|
||||
ignoreCase: true,
|
||||
out var parsedRole)
|
||||
? parsedRole
|
||||
: TenantRole.TenantAdmin;
|
||||
return new TenantAdminActor(currentTenant.TenantId.Value, currentUser.UserId.Value, role);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,4 +69,89 @@ public interface ITenantAdminDirectService
|
||||
TenantAdminActor actor,
|
||||
UpsertTenantAdminStudentFollowupCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<CatalogList<TenantAdminMemberItem>> GetMembersAsync(
|
||||
TenantAdminActor actor,
|
||||
TenantAdminMemberFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ContentManagementResult<TenantAdminMemberItem>> UpsertMemberAsync(
|
||||
TenantAdminActor actor,
|
||||
UpsertTenantAdminMemberCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ContentManagementResult<TenantAdminMemberItem>> DisableMemberAsync(
|
||||
TenantAdminActor actor,
|
||||
Guid membershipId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<CatalogList<TenantAdminAuditLogItem>> GetAuditLogsAsync(
|
||||
TenantAdminActor actor,
|
||||
TenantAdminAuditLogFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<TenantAdminPermissionMatrix> GetPermissionMatrixAsync(
|
||||
TenantAdminActor actor,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<CatalogList<TenantAdminRoleTemplateItem>> GetRoleTemplatesAsync(
|
||||
TenantAdminActor actor,
|
||||
TenantAdminRoleTemplateFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ContentManagementResult<TenantAdminRoleTemplateItem>> UpsertRoleTemplateAsync(
|
||||
TenantAdminActor actor,
|
||||
UpsertTenantAdminRoleTemplateCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ContentManagementResult<TenantAdminRoleTemplateItem>> DisableRoleTemplateAsync(
|
||||
TenantAdminActor actor,
|
||||
Guid roleTemplateId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ContentManagementResult<TenantBrandingItem>> UpsertBrandingAsync(
|
||||
TenantAdminActor actor,
|
||||
UpsertTenantBrandingCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ContentManagementResult<TenantSettingsItem>> UpsertSettingsAsync(
|
||||
TenantAdminActor actor,
|
||||
UpsertTenantSettingsCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<CatalogList<TenantThemeTemplateItem>> GetThemeTemplatesAsync(
|
||||
TenantAdminActor actor,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ContentManagementResult<TenantThemeItem>> GetThemeAsync(
|
||||
TenantAdminActor actor,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ContentManagementResult<TenantThemeItem>> PreviewThemeAsync(
|
||||
TenantAdminActor actor,
|
||||
PreviewTenantThemeCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ContentManagementResult<TenantThemeItem>> PublishThemeAsync(
|
||||
TenantAdminActor actor,
|
||||
PublishTenantThemeCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<CatalogList<TenantDomainItem>> GetDomainsAsync(
|
||||
TenantAdminActor actor,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ContentManagementResult<TenantDomainItem>> CreateDomainAsync(
|
||||
TenantAdminActor actor,
|
||||
CreateTenantDomainCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<CatalogList<TenantAuthProviderItem>> GetAuthProvidersAsync(
|
||||
TenantAdminActor actor,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ContentManagementResult<TenantAuthProviderItem>> UpsertAuthProviderAsync(
|
||||
TenantAdminActor actor,
|
||||
UpsertTenantAuthProviderCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.Text.Json;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Application.TenantAdmin;
|
||||
|
||||
public sealed record TenantAdminActor(Guid TenantId, Guid UserId);
|
||||
public sealed record TenantAdminActor(Guid TenantId, Guid UserId, TenantRole Role = TenantRole.TenantAdmin);
|
||||
|
||||
public sealed record TenantAdminClassFilter(
|
||||
Guid? RegionId = null,
|
||||
@@ -30,6 +31,22 @@ public sealed record TenantAdminStudentActivityFilter(
|
||||
string? Status = null,
|
||||
int? Limit = null);
|
||||
|
||||
public sealed record TenantAdminMemberFilter(
|
||||
string? Role = null,
|
||||
string? Status = null,
|
||||
string? Keyword = null,
|
||||
int? Limit = null);
|
||||
|
||||
public sealed record TenantAdminAuditLogFilter(
|
||||
string? Action = null,
|
||||
string? TargetType = null,
|
||||
Guid? ActorUserId = null,
|
||||
int? Limit = null);
|
||||
|
||||
public sealed record TenantAdminRoleTemplateFilter(
|
||||
string? Status = null,
|
||||
int? Limit = null);
|
||||
|
||||
public sealed record UpsertTenantAdminClassCommand(
|
||||
Guid? Id,
|
||||
Guid? RegionId,
|
||||
@@ -94,6 +111,66 @@ public sealed record UpsertTenantAdminStudentFollowupCommand(
|
||||
DateTimeOffset? DueAt,
|
||||
JsonElement Metadata);
|
||||
|
||||
public sealed record UpsertTenantAdminMemberCommand(
|
||||
Guid? MembershipId,
|
||||
UserLookupCommand User,
|
||||
string? Role,
|
||||
string? Status,
|
||||
Guid? RoleTemplateId,
|
||||
JsonElement Permissions,
|
||||
string? PrimaryRole);
|
||||
|
||||
public sealed record UpsertTenantAdminRoleTemplateCommand(
|
||||
Guid? Id,
|
||||
string? Code,
|
||||
string Name,
|
||||
string? Description,
|
||||
string? BaseRole,
|
||||
string? Status,
|
||||
JsonElement Permissions,
|
||||
JsonElement MenuPermissions,
|
||||
JsonElement ModulePermissions,
|
||||
JsonElement FieldPermissions,
|
||||
JsonElement DataScope,
|
||||
int? Order);
|
||||
|
||||
public sealed record UpsertTenantBrandingCommand(
|
||||
string BrandName,
|
||||
string? ShortName,
|
||||
string? Slogan,
|
||||
string? OrganizationName,
|
||||
string? LogoUrl,
|
||||
string? FaviconUrl,
|
||||
string? ServiceWechat,
|
||||
string? ServiceAccountName);
|
||||
|
||||
public sealed record UpsertTenantSettingsCommand(
|
||||
JsonElement FeatureFlags,
|
||||
JsonElement AdminFeatureFlags,
|
||||
JsonElement PublicConfig);
|
||||
|
||||
public sealed record PreviewTenantThemeCommand(
|
||||
string TemplateCode,
|
||||
JsonElement Theme,
|
||||
JsonElement PublicAssets);
|
||||
|
||||
public sealed record PublishTenantThemeCommand(
|
||||
bool UseDraft,
|
||||
string? TemplateCode,
|
||||
JsonElement Theme,
|
||||
JsonElement PublicAssets);
|
||||
|
||||
public sealed record CreateTenantDomainCommand(
|
||||
string Host,
|
||||
string? DomainType,
|
||||
bool IsPrimary);
|
||||
|
||||
public sealed record UpsertTenantAuthProviderCommand(
|
||||
string Provider,
|
||||
string? Status,
|
||||
string? DisplayName,
|
||||
JsonElement ConfigPublic);
|
||||
|
||||
public sealed record TenantAdminClassList(
|
||||
IReadOnlyCollection<TenantAdminClassItem> Items,
|
||||
bool Scoped);
|
||||
@@ -204,6 +281,128 @@ public sealed record TenantAdminStudentFollowupItem(
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset UpdatedAt);
|
||||
|
||||
public sealed record TenantAdminMemberItem(
|
||||
Guid MembershipId,
|
||||
Guid UserId,
|
||||
TenantRole Role,
|
||||
MembershipStatus Status,
|
||||
JsonElement Permissions,
|
||||
Guid? RoleTemplateId,
|
||||
string? RoleTemplateCode,
|
||||
string? RoleTemplateName,
|
||||
string? LegacyRole,
|
||||
TenantAdminUserSummary User,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset UpdatedAt);
|
||||
|
||||
public sealed record TenantAdminAuditLogItem(
|
||||
Guid Id,
|
||||
Guid? ActorUserId,
|
||||
string Action,
|
||||
string? TargetType,
|
||||
string? TargetId,
|
||||
JsonElement Details,
|
||||
string? IpAddress,
|
||||
string? UserAgent,
|
||||
string? ActorName,
|
||||
string? ActorPhone,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
public sealed record TenantAdminPermissionMatrix(
|
||||
TenantAdminCurrentPermission Current,
|
||||
IReadOnlyCollection<TenantAdminPermissionCatalogItem> Permissions,
|
||||
IReadOnlyDictionary<string, IReadOnlyCollection<string>> RoleDefaults);
|
||||
|
||||
public sealed record TenantAdminCurrentPermission(
|
||||
Guid UserId,
|
||||
Guid TenantId,
|
||||
TenantRole Role);
|
||||
|
||||
public sealed record TenantAdminPermissionCatalogItem(string Key, string Label);
|
||||
|
||||
public sealed record TenantAdminRoleTemplateItem(
|
||||
Guid Id,
|
||||
string Code,
|
||||
string Name,
|
||||
string? Description,
|
||||
TenantRole BaseRole,
|
||||
TenantRoleTemplateStatus Status,
|
||||
JsonElement Permissions,
|
||||
JsonElement MenuPermissions,
|
||||
JsonElement ModulePermissions,
|
||||
JsonElement FieldPermissions,
|
||||
JsonElement DataScope,
|
||||
bool IsSystem,
|
||||
int Order,
|
||||
Guid? CreatedBy,
|
||||
Guid? UpdatedBy,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset UpdatedAt);
|
||||
|
||||
public sealed record TenantBrandingItem(
|
||||
Guid TenantId,
|
||||
string BrandName,
|
||||
string? ShortName,
|
||||
string? Slogan,
|
||||
string? OrganizationName,
|
||||
string? LogoUrl,
|
||||
string? FaviconUrl,
|
||||
string? ServiceWechat,
|
||||
string? ServiceAccountName,
|
||||
JsonElement Theme,
|
||||
JsonElement PublicAssets,
|
||||
DateTimeOffset UpdatedAt);
|
||||
|
||||
public sealed record TenantSettingsItem(
|
||||
Guid TenantId,
|
||||
JsonElement FeatureFlags,
|
||||
JsonElement AdminFeatureFlags,
|
||||
JsonElement PublicConfig,
|
||||
DateTimeOffset UpdatedAt);
|
||||
|
||||
public sealed record TenantThemeTemplateItem(
|
||||
string Code,
|
||||
string Name,
|
||||
string? Description,
|
||||
string? PreviewImageUrl,
|
||||
JsonElement Theme,
|
||||
JsonElement PublicAssets,
|
||||
int Order);
|
||||
|
||||
public sealed record TenantThemeItem(
|
||||
Guid TenantId,
|
||||
string? ActiveTemplateCode,
|
||||
JsonElement ActiveTheme,
|
||||
JsonElement ActivePublicAssets,
|
||||
string? DraftTemplateCode,
|
||||
JsonElement DraftTheme,
|
||||
JsonElement DraftPublicAssets,
|
||||
TenantThemeConfigStatus Status,
|
||||
DateTimeOffset? PublishedAt,
|
||||
Guid? PublishedBy,
|
||||
Guid? DraftUpdatedBy,
|
||||
DateTimeOffset UpdatedAt);
|
||||
|
||||
public sealed record TenantDomainItem(
|
||||
Guid Id,
|
||||
string Host,
|
||||
TenantDomainType DomainType,
|
||||
TenantDomainStatus Status,
|
||||
bool IsPrimary,
|
||||
string? VerificationToken,
|
||||
DateTimeOffset? VerifiedAt,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset UpdatedAt);
|
||||
|
||||
public sealed record TenantAuthProviderItem(
|
||||
Guid Id,
|
||||
string Provider,
|
||||
TenantAuthProviderStatus Status,
|
||||
string? DisplayName,
|
||||
JsonElement ConfigPublic,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset UpdatedAt);
|
||||
|
||||
public sealed class TenantAdminDirectException(string message, string code) : Exception(message)
|
||||
{
|
||||
public string Code { get; } = code;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ using Tiku.Api.Contracts;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Auth;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
@@ -164,6 +165,140 @@ public sealed class TenantAdminDirectEndpointTests
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tenant_admin_can_manage_branding_settings_theme_domains_and_auth_providers()
|
||||
{
|
||||
await using var factory = new ApiTestFactory();
|
||||
var seed = await SeedAdminAsync(factory);
|
||||
await factory.SeedAsync(new TenantThemeTemplate
|
||||
{
|
||||
Code = "classic",
|
||||
Name = "经典主题",
|
||||
Theme = JsonSerializer.SerializeToElement(new { color = "blue" }),
|
||||
PublicAssets = JsonSerializer.SerializeToElement(new { logo = "/logo.png" }),
|
||||
Status = TenantThemeTemplateStatus.Active
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
await LoginAsync(client, seed);
|
||||
|
||||
var brandingResponse = await client.PutAsJsonAsync(
|
||||
"/api/tenant-admin/branding",
|
||||
new UpsertTenantBrandingDto
|
||||
{
|
||||
BrandName = "机构题库",
|
||||
ShortName = "题库",
|
||||
ServiceWechat = "wechat"
|
||||
});
|
||||
var settingsResponse = await client.PutAsJsonAsync(
|
||||
"/api/tenant-admin/settings",
|
||||
new UpsertTenantSettingsDto
|
||||
{
|
||||
FeatureFlags = JsonSerializer.SerializeToElement(new { catalog = true }),
|
||||
PublicConfig = JsonSerializer.SerializeToElement(new { icp = "蜀ICP备" })
|
||||
});
|
||||
var previewResponse = await client.PostAsJsonAsync(
|
||||
"/api/tenant-admin/theme/preview",
|
||||
new PreviewTenantThemeDto
|
||||
{
|
||||
TemplateCode = "classic",
|
||||
Theme = JsonSerializer.SerializeToElement(new { color = "red" })
|
||||
});
|
||||
var publishResponse = await client.PostAsJsonAsync(
|
||||
"/api/tenant-admin/theme/publish",
|
||||
new PublishTenantThemeDto { UseDraft = true });
|
||||
var domainResponse = await client.PostAsJsonAsync(
|
||||
"/api/tenant-admin/domains",
|
||||
new CreateTenantDomainDto
|
||||
{
|
||||
Host = "learn.example.com",
|
||||
DomainType = "custom",
|
||||
IsPrimary = true
|
||||
});
|
||||
var authProviderResponse = await client.PutAsJsonAsync(
|
||||
"/api/tenant-admin/auth-providers",
|
||||
new UpsertTenantAuthProviderDto
|
||||
{
|
||||
Provider = "wechat_mp",
|
||||
Status = "active",
|
||||
DisplayName = "微信公众号",
|
||||
ConfigPublic = JsonSerializer.SerializeToElement(new { appId = "wx-test" })
|
||||
});
|
||||
var secretSettingsResponse = await client.PutAsJsonAsync(
|
||||
"/api/tenant-admin/settings",
|
||||
new UpsertTenantSettingsDto
|
||||
{
|
||||
PublicConfig = JsonSerializer.SerializeToElement(new { appSecret = "nope" })
|
||||
});
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, brandingResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, settingsResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, previewResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, publishResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, domainResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, authProviderResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.BadRequest, secretSettingsResponse.StatusCode);
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
Assert.True(await dbContext.TenantBrandings.AnyAsync(item => item.TenantId == seed.TenantId && item.BrandName == "机构题库"));
|
||||
Assert.True(await dbContext.TenantThemeConfigs.AnyAsync(item => item.TenantId == seed.TenantId && item.ActiveTemplateCode == "classic"));
|
||||
Assert.True(await dbContext.TenantDomains.AnyAsync(item => item.TenantId == seed.TenantId && item.Host == "learn.example.com" && item.IsPrimary));
|
||||
Assert.True(await dbContext.TenantAuthProviders.AnyAsync(item => item.TenantId == seed.TenantId && item.Provider == "wechat_mp"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tenant_admin_can_manage_role_templates_members_and_audit_logs()
|
||||
{
|
||||
await using var factory = new ApiTestFactory();
|
||||
var seed = await SeedAdminAsync(factory);
|
||||
using var client = factory.CreateClient();
|
||||
await LoginAsync(client, seed);
|
||||
|
||||
var roleResponse = await client.PutAsJsonAsync(
|
||||
"/api/tenant-admin/role-templates",
|
||||
new UpsertTenantAdminRoleTemplateDto
|
||||
{
|
||||
Code = "teacher-basic",
|
||||
Name = "教师基础权限",
|
||||
BaseRole = "teacher",
|
||||
Permissions = JsonSerializer.SerializeToElement(new Dictionary<string, bool>
|
||||
{
|
||||
["classes:read"] = true,
|
||||
["students:read"] = true
|
||||
}),
|
||||
FieldPermissions = JsonSerializer.SerializeToElement(new Dictionary<string, bool>
|
||||
{
|
||||
["student.phone"] = false
|
||||
})
|
||||
});
|
||||
var roleJson = await ReadJsonAsync(roleResponse);
|
||||
var roleTemplateId = roleJson.RootElement.GetProperty("item").GetProperty("id").GetGuid();
|
||||
|
||||
var memberResponse = await client.PutAsJsonAsync(
|
||||
"/api/tenant-admin/members",
|
||||
new UpsertTenantAdminMemberDto
|
||||
{
|
||||
RoleTemplateId = roleTemplateId,
|
||||
User = new TenantAdminUserLookupDto
|
||||
{
|
||||
Phone = "13900000004",
|
||||
Name = "教师"
|
||||
}
|
||||
});
|
||||
var membersResponse = await client.GetAsync("/api/tenant-admin/members?role=teacher");
|
||||
var permissionsResponse = await client.GetAsync("/api/tenant-admin/permissions");
|
||||
var auditResponse = await client.GetAsync("/api/tenant-admin/audit-logs?action=tenant.role_template");
|
||||
var membersJson = await ReadJsonAsync(membersResponse);
|
||||
var auditJson = await ReadJsonAsync(auditResponse);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, roleResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, memberResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, membersResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, permissionsResponse.StatusCode);
|
||||
Assert.Single(membersJson.RootElement.GetProperty("items").EnumerateArray());
|
||||
Assert.NotEmpty(auditJson.RootElement.GetProperty("items").EnumerateArray());
|
||||
}
|
||||
|
||||
private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedAdminAsync(
|
||||
ApiTestFactory factory,
|
||||
TenantRole role = TenantRole.TenantAdmin)
|
||||
|
||||
Reference in New Issue
Block a user