245 lines
11 KiB
C#
245 lines
11 KiB
C#
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Tiku.Application.Catalog;
|
|
using Tiku.Application.Content;
|
|
using Tiku.Application.TenantAdmin;
|
|
using Tiku.Domain.Common;
|
|
using Tiku.Domain.Operations;
|
|
using Tiku.Domain.Tenancy;
|
|
|
|
namespace Tiku.Infrastructure.TenantAdmin;
|
|
|
|
public sealed partial class TenantAdminDirectService
|
|
{
|
|
public async Task<CatalogList<TenantAdminAuditLogItem>> GetAuditLogsAsync(
|
|
TenantAdminActor actor,
|
|
TenantAdminAuditLogFilter filter,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await RequireAllDataScopeAsync(actor, cancellationToken);
|
|
var query = dbContext.AuditLogs.AsNoTracking().Where(item => item.TenantId == actor.TenantId);
|
|
if (!string.IsNullOrWhiteSpace(filter.Action))
|
|
{
|
|
var action = filter.Action.Trim();
|
|
query = query.Where(item => item.Action.StartsWith(action));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(filter.TargetType))
|
|
query = query.Where(item => item.TargetType == filter.TargetType.Trim());
|
|
|
|
if (filter.ActorUserId.HasValue) query = query.Where(item => item.ActorUserId == filter.ActorUserId.Value);
|
|
|
|
var logs = await query
|
|
.OrderByDescending(item => item.CreatedAt)
|
|
.Take(ResolveLimit(filter.Limit))
|
|
.ToArrayAsync(cancellationToken);
|
|
var actorIds = logs.Where(item => item.ActorUserId.HasValue).Select(item => item.ActorUserId!.Value).ToArray();
|
|
var users = await dbContext.Users.AsNoTracking()
|
|
.Where(user => actorIds.Contains(user.Id))
|
|
.ToDictionaryAsync(user => user.Id, cancellationToken);
|
|
|
|
return new CatalogList<TenantAdminAuditLogItem>(logs.Select(item =>
|
|
{
|
|
var user = item.ActorUserId.HasValue && users.TryGetValue(item.ActorUserId.Value, out var actorUser)
|
|
? actorUser
|
|
: null;
|
|
return new TenantAdminAuditLogItem(
|
|
item.Id,
|
|
item.ActorUserId,
|
|
item.Action,
|
|
item.TargetType,
|
|
item.TargetId,
|
|
item.Details,
|
|
item.IpAddress,
|
|
item.UserAgent,
|
|
user?.Name ?? user?.UserName,
|
|
user?.Phone,
|
|
item.CreatedAt);
|
|
}).ToArray());
|
|
}
|
|
|
|
public async Task<ContentManagementResult<TenantBrandingItem>> UpsertBrandingAsync(
|
|
TenantAdminActor actor,
|
|
UpsertTenantBrandingCommand command,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await RequireAllDataScopeAsync(actor, cancellationToken);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(command.BrandName);
|
|
var item = await dbContext.TenantBrandings.FirstOrDefaultAsync(branding => branding.TenantId == actor.TenantId,
|
|
cancellationToken);
|
|
if (item is null)
|
|
{
|
|
item = new TenantBranding { TenantId = actor.TenantId };
|
|
dbContext.TenantBrandings.Add(item);
|
|
}
|
|
|
|
item.BrandName = command.BrandName.Trim();
|
|
item.ShortName = Normalize(command.ShortName);
|
|
item.Slogan = Normalize(command.Slogan);
|
|
item.OrganizationName = Normalize(command.OrganizationName);
|
|
item.LogoUrl = Normalize(command.LogoUrl);
|
|
item.FaviconUrl = Normalize(command.FaviconUrl);
|
|
item.ServiceWechat = Normalize(command.ServiceWechat);
|
|
item.ServiceAccountName = Normalize(command.ServiceAccountName);
|
|
await AddAuditAsync(actor, "tenant.branding.updated", "tenant_branding", actor.TenantId, cancellationToken);
|
|
await dbContext.SaveChangesAsync(cancellationToken);
|
|
return new ContentManagementResult<TenantBrandingItem>(ToBrandingItem(item));
|
|
}
|
|
|
|
public async Task<ContentManagementResult<TenantSettingsItem>> UpsertSettingsAsync(
|
|
TenantAdminActor actor,
|
|
UpsertTenantSettingsCommand command,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await RequireAllDataScopeAsync(actor, cancellationToken);
|
|
AssertNoSecrets(command.PublicConfig, "public_config");
|
|
var item = await dbContext.TenantSettings.FirstOrDefaultAsync(settings => settings.TenantId == actor.TenantId,
|
|
cancellationToken);
|
|
if (item is null)
|
|
{
|
|
item = new TenantSettings { TenantId = actor.TenantId };
|
|
dbContext.TenantSettings.Add(item);
|
|
}
|
|
|
|
item.FeatureFlags = JsonObjectOrDefault(command.FeatureFlags);
|
|
item.AdminFeatureFlags = JsonObjectOrDefault(command.AdminFeatureFlags);
|
|
item.PublicConfig = JsonObjectOrDefault(command.PublicConfig);
|
|
await AddAuditAsync(actor, "tenant.settings.updated", "tenant_settings", actor.TenantId, cancellationToken);
|
|
await dbContext.SaveChangesAsync(cancellationToken);
|
|
return new ContentManagementResult<TenantSettingsItem>(ToSettingsItem(item));
|
|
}
|
|
|
|
public async Task<CatalogList<TenantThemeTemplateItem>> GetThemeTemplatesAsync(
|
|
TenantAdminActor actor,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await RequireAllDataScopeAsync(actor, cancellationToken);
|
|
await Task.CompletedTask.WaitAsync(cancellationToken);
|
|
var items = await dbContext.TenantThemeTemplates.AsNoTracking()
|
|
.Where(item => item.Status == TenantThemeTemplateStatus.Active)
|
|
.OrderBy(item => item.SortOrder)
|
|
.ThenBy(item => item.Code)
|
|
.Select(item => new TenantThemeTemplateItem(
|
|
item.Code,
|
|
item.Name,
|
|
item.Description,
|
|
item.PreviewImageUrl,
|
|
item.Theme,
|
|
item.PublicAssets,
|
|
item.SortOrder))
|
|
.ToArrayAsync(cancellationToken);
|
|
return new CatalogList<TenantThemeTemplateItem>(items);
|
|
}
|
|
|
|
public async Task<ContentManagementResult<TenantThemeItem>> GetThemeAsync(
|
|
TenantAdminActor actor,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await RequireAllDataScopeAsync(actor, cancellationToken);
|
|
var item = await dbContext.TenantThemeConfigs.AsNoTracking()
|
|
.FirstOrDefaultAsync(theme => theme.TenantId == actor.TenantId, cancellationToken);
|
|
if (item is not null) return new ContentManagementResult<TenantThemeItem>(ToThemeItem(item));
|
|
|
|
var branding = await dbContext.TenantBrandings.AsNoTracking()
|
|
.FirstOrDefaultAsync(tenantBranding => tenantBranding.TenantId == actor.TenantId, cancellationToken);
|
|
return new ContentManagementResult<TenantThemeItem>(new TenantThemeItem(
|
|
actor.TenantId,
|
|
null,
|
|
branding?.Theme ?? JsonDefaults.Object(),
|
|
branding?.PublicAssets ?? JsonDefaults.Object(),
|
|
null,
|
|
JsonDefaults.Object(),
|
|
JsonDefaults.Object(),
|
|
TenantThemeConfigStatus.Published,
|
|
null,
|
|
null,
|
|
null,
|
|
branding?.UpdatedAt ?? DateTimeOffset.UtcNow));
|
|
}
|
|
|
|
public async Task<ContentManagementResult<TenantThemeItem>> PreviewThemeAsync(
|
|
TenantAdminActor actor,
|
|
PreviewTenantThemeCommand command,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await RequireAllDataScopeAsync(actor, cancellationToken);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(command.TemplateCode);
|
|
var template = await dbContext.TenantThemeTemplates.FirstOrDefaultAsync(
|
|
item => item.Code == command.TemplateCode && item.Status == TenantThemeTemplateStatus.Active,
|
|
cancellationToken);
|
|
if (template is null)
|
|
throw new TenantAdminDirectException("Theme template was not found.", "theme_template_not_found");
|
|
|
|
var item = await dbContext.TenantThemeConfigs.FirstOrDefaultAsync(theme => theme.TenantId == actor.TenantId,
|
|
cancellationToken);
|
|
if (item is null)
|
|
{
|
|
item = new TenantThemeConfig { TenantId = actor.TenantId };
|
|
dbContext.TenantThemeConfigs.Add(item);
|
|
}
|
|
|
|
item.DraftTemplateCode = template.Code;
|
|
item.DraftTheme = MergeJsonObjects(template.Theme, command.Theme);
|
|
item.DraftPublicAssets = MergeJsonObjects(template.PublicAssets, command.PublicAssets);
|
|
item.Status = TenantThemeConfigStatus.Draft;
|
|
item.DraftUpdatedBy = actor.UserId;
|
|
await AddAuditAsync(actor, "tenant.theme.previewed", "tenant_theme_configs", actor.TenantId, cancellationToken);
|
|
await dbContext.SaveChangesAsync(cancellationToken);
|
|
return new ContentManagementResult<TenantThemeItem>(ToThemeItem(item));
|
|
}
|
|
|
|
public async Task<ContentManagementResult<TenantThemeItem>> PublishThemeAsync(
|
|
TenantAdminActor actor,
|
|
PublishTenantThemeCommand command,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await RequireAllDataScopeAsync(actor, cancellationToken);
|
|
var item = await dbContext.TenantThemeConfigs.FirstOrDefaultAsync(theme => theme.TenantId == actor.TenantId,
|
|
cancellationToken);
|
|
JsonElement activeTheme;
|
|
JsonElement activeAssets;
|
|
string? templateCode;
|
|
if (command.UseDraft)
|
|
{
|
|
if (item is null || string.IsNullOrWhiteSpace(item.DraftTemplateCode))
|
|
throw new TenantAdminDirectException("No draft theme to publish.", "theme_draft_not_found");
|
|
|
|
activeTheme = item.DraftTheme;
|
|
activeAssets = item.DraftPublicAssets;
|
|
templateCode = item.DraftTemplateCode;
|
|
}
|
|
else
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(command.TemplateCode);
|
|
var template = await dbContext.TenantThemeTemplates.FirstOrDefaultAsync(
|
|
theme => theme.Code == command.TemplateCode && theme.Status == TenantThemeTemplateStatus.Active,
|
|
cancellationToken);
|
|
if (template is null)
|
|
throw new TenantAdminDirectException("Theme template was not found.", "theme_template_not_found");
|
|
|
|
item ??= new TenantThemeConfig { TenantId = actor.TenantId };
|
|
if (dbContext.Entry(item).State == EntityState.Detached) dbContext.TenantThemeConfigs.Add(item);
|
|
|
|
activeTheme = MergeJsonObjects(template.Theme, command.Theme);
|
|
activeAssets = MergeJsonObjects(template.PublicAssets, command.PublicAssets);
|
|
templateCode = template.Code;
|
|
}
|
|
|
|
item ??= new TenantThemeConfig { TenantId = actor.TenantId };
|
|
if (dbContext.Entry(item).State == EntityState.Detached) dbContext.TenantThemeConfigs.Add(item);
|
|
|
|
item.ActiveTemplateCode = templateCode;
|
|
item.ActiveTheme = activeTheme.Clone();
|
|
item.ActivePublicAssets = activeAssets.Clone();
|
|
item.DraftTemplateCode = null;
|
|
item.DraftTheme = JsonDefaults.Object();
|
|
item.DraftPublicAssets = JsonDefaults.Object();
|
|
item.Status = TenantThemeConfigStatus.Published;
|
|
item.PublishedAt = DateTimeOffset.UtcNow;
|
|
item.PublishedBy = actor.UserId;
|
|
await EnsureBrandingThemeAsync(actor.TenantId, activeTheme, activeAssets, cancellationToken);
|
|
await AddAuditAsync(actor, "tenant.theme.published", "tenant_theme_configs", actor.TenantId, cancellationToken);
|
|
await dbContext.SaveChangesAsync(cancellationToken);
|
|
return new ContentManagementResult<TenantThemeItem>(ToThemeItem(item));
|
|
}
|
|
} |