forked from gongxuegit/tiku-backend.net
feat: enforce tenant isolation and shared question bank
This commit is contained in:
151
Tiku.Infrastructure/Catalog/TaxonomyService.cs
Normal file
151
Tiku.Infrastructure/Catalog/TaxonomyService.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.QuestionBanks;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Catalog;
|
||||
|
||||
public sealed class TaxonomyService(
|
||||
TikuDbContext dbContext,
|
||||
IPublicQuestionAccessPolicy accessPolicy,
|
||||
ITenantExecutionScope tenantExecutionScope) : ITaxonomyService
|
||||
{
|
||||
public async Task<IReadOnlyCollection<TaxonomyNodeItem>> ListAsync(
|
||||
Guid tenantId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var includePlatform = true;
|
||||
try
|
||||
{
|
||||
await accessPolicy.EnsureCanStartAsync(tenantId, cancellationToken);
|
||||
}
|
||||
catch (PublicQuestionAccessDeniedException)
|
||||
{
|
||||
includePlatform = false;
|
||||
}
|
||||
|
||||
return await tenantExecutionScope.ExecuteAsync(
|
||||
tenantId,
|
||||
"List platform taxonomy with tenant extensions",
|
||||
async (provider, token) =>
|
||||
{
|
||||
var systemDbContext = provider.GetRequiredService<TikuDbContext>();
|
||||
var platformTenantId = includePlatform
|
||||
? await systemDbContext.Tenants.AsNoTracking()
|
||||
.Where(tenant => tenant.Mode == TenantMode.PlatformOwned)
|
||||
.Select(tenant => (Guid?)tenant.Id)
|
||||
.SingleOrDefaultAsync(token)
|
||||
: null;
|
||||
return await systemDbContext.TaxonomyNodes.AsNoTracking()
|
||||
.Where(node =>
|
||||
node.IsActive &&
|
||||
(node.TenantId == tenantId ||
|
||||
(platformTenantId.HasValue && node.TenantId == platformTenantId.Value)))
|
||||
.OrderBy(node => node.Depth)
|
||||
.ThenBy(node => node.SortOrder)
|
||||
.Select(node => new TaxonomyNodeItem(
|
||||
node.Id,
|
||||
node.TenantId == tenantId ? QuestionSource.Tenant : QuestionSource.Platform,
|
||||
node.ParentId,
|
||||
!node.ParentOwnerTenantId.HasValue
|
||||
? null
|
||||
: node.ParentOwnerTenantId == tenantId
|
||||
? QuestionSource.Tenant
|
||||
: QuestionSource.Platform,
|
||||
node.NodeType,
|
||||
node.Code,
|
||||
node.Name,
|
||||
node.Path,
|
||||
node.Depth,
|
||||
node.SortOrder,
|
||||
node.Metadata))
|
||||
.ToArrayAsync(token);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<TaxonomyNodeItem> CreateAsync(
|
||||
Guid tenantId,
|
||||
CreateTaxonomyNodeCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Guid? parentOwnerTenantId = null;
|
||||
TaxonomyParent? parent = null;
|
||||
if (command.ParentId.HasValue)
|
||||
{
|
||||
parentOwnerTenantId = command.ParentSource switch
|
||||
{
|
||||
QuestionSource.Tenant => tenantId,
|
||||
QuestionSource.Platform => await ResolvePlatformTenantIdAsync(tenantId, cancellationToken),
|
||||
_ => throw new InvalidOperationException("A parent source is required when parentId is provided.")
|
||||
};
|
||||
parent = await tenantExecutionScope.ExecuteAsync(
|
||||
tenantId,
|
||||
"Validate taxonomy extension parent ownership",
|
||||
async (provider, token) =>
|
||||
{
|
||||
var systemDbContext = provider.GetRequiredService<TikuDbContext>();
|
||||
return await systemDbContext.TaxonomyNodes.AsNoTracking()
|
||||
.Where(node =>
|
||||
node.TenantId == parentOwnerTenantId &&
|
||||
node.Id == command.ParentId.Value &&
|
||||
node.IsActive)
|
||||
.Select(node => new TaxonomyParent(node.Path, node.Depth))
|
||||
.SingleOrDefaultAsync(token);
|
||||
},
|
||||
cancellationToken)
|
||||
?? throw new InvalidOperationException("Taxonomy parent was not found.");
|
||||
}
|
||||
|
||||
var node = new TaxonomyNode
|
||||
{
|
||||
TenantId = tenantId,
|
||||
ParentOwnerTenantId = parentOwnerTenantId,
|
||||
ParentId = command.ParentId,
|
||||
NodeType = command.NodeType,
|
||||
Code = command.Code.Trim(),
|
||||
Name = command.Name.Trim(),
|
||||
Depth = parent is null ? 0 : parent.Depth + 1,
|
||||
SortOrder = command.SortOrder,
|
||||
Metadata = command.Metadata.Clone()
|
||||
};
|
||||
node.Path = parent is null
|
||||
? $"n{node.Id:N}"
|
||||
: $"{parent.Path}.n{node.Id:N}";
|
||||
dbContext.TaxonomyNodes.Add(node);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return new TaxonomyNodeItem(
|
||||
node.Id,
|
||||
QuestionSource.Tenant,
|
||||
node.ParentId,
|
||||
node.ParentId.HasValue ? command.ParentSource : null,
|
||||
node.NodeType,
|
||||
node.Code,
|
||||
node.Name,
|
||||
node.Path,
|
||||
node.Depth,
|
||||
node.SortOrder,
|
||||
node.Metadata);
|
||||
}
|
||||
|
||||
private async Task<Guid> ResolvePlatformTenantIdAsync(Guid tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
await accessPolicy.EnsureCanStartAsync(tenantId, cancellationToken);
|
||||
return await tenantExecutionScope.ExecuteAsync(
|
||||
tenantId,
|
||||
"Resolve platform taxonomy owner",
|
||||
async (provider, token) => await provider.GetRequiredService<TikuDbContext>()
|
||||
.Tenants.AsNoTracking()
|
||||
.Where(tenant => tenant.Mode == TenantMode.PlatformOwned)
|
||||
.Select(tenant => tenant.Id)
|
||||
.SingleAsync(token),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private sealed record TaxonomyParent(string? Path, int Depth);
|
||||
}
|
||||
Reference in New Issue
Block a user