Feature/request type access (#23)
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/23
This commit was merged in pull request #23.
This commit is contained in:
@@ -6,6 +6,7 @@ using Tnsu.ApprovalRoute.Application.Reminders;
|
||||
using Tnsu.ApprovalRoute.Application.ListPartitioning;
|
||||
using Tnsu.ApprovalRoute.Application.Reports;
|
||||
using Tnsu.ApprovalRoute.Application.RequestManage;
|
||||
using Tnsu.ApprovalRoute.Application.RequestTypeAccesses;
|
||||
using Tnsu.ApprovalRoute.Application.RequestTypes;
|
||||
using Tnsu.ApprovalRoute.Application.TemplateRoutes;
|
||||
using Tnsu.ApprovalRoute.Application.TemplateRouteSteps;
|
||||
@@ -18,6 +19,7 @@ public static class ApplicationServiceCollectionExtensions
|
||||
{
|
||||
services.AddScoped<ReminderService>();
|
||||
services.AddScoped<RequestTypeService>();
|
||||
services.AddScoped<RequestTypeAccessService>();
|
||||
services.AddScoped<RequestManageService>();
|
||||
services.AddScoped<ListPartitionRegistry>();
|
||||
services.AddScoped<ListPartitionService>();
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using Tnsu.ApprovalRoute.Application.ListPartitioning;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Application.RequestTypeAccesses;
|
||||
|
||||
/// <param name="SharePointLists">
|
||||
/// Вся серия SharePoint-списков данных (архив + текущая партиция) типа заявки —
|
||||
/// именно на них нужно выдать email'у право чтения, иначе поиск по порталу
|
||||
/// продолжит отдавать только его собственные заявки: доступ здесь настраивает
|
||||
/// видимость в UI approval-route, но не права SharePoint, которые остаются
|
||||
/// отдельным ручным шагом.
|
||||
/// </param>
|
||||
public sealed record RequestTypeAccessDto(
|
||||
int Id,
|
||||
int RequestTypeId,
|
||||
string Email,
|
||||
IReadOnlyList<RequestListPartitionDto> SharePointLists);
|
||||
|
||||
public sealed record CreateRequestTypeAccessRequest(int RequestTypeId, string Email);
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
using FluentResults;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Tnsu.ApprovalRoute.Application.Common.Errors;
|
||||
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
|
||||
using Tnsu.ApprovalRoute.Application.ListPartitioning;
|
||||
using Tnsu.ApprovalRoute.Domain.Entities;
|
||||
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Application.RequestTypeAccesses;
|
||||
|
||||
public sealed class RequestTypeAccessService(
|
||||
ApprovalRouteDbContext db,
|
||||
ListPartitionService partitions,
|
||||
ICurrentUser currentUser,
|
||||
ILogger<RequestTypeAccessService> logger)
|
||||
{
|
||||
public async Task<Result<IReadOnlyList<RequestTypeAccessDto>>> ListAsync(
|
||||
int? requestTypeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var entities = await db.RequestTypeAccesses.AsNoTracking()
|
||||
.Where(x => requestTypeId == null || x.RequestTypeId == requestTypeId)
|
||||
.OrderBy(x => x.RequestTypeId).ThenBy(x => x.Email)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// Партиции запрашиваются РАЗ на тип заявки, а не на строку — иначе список
|
||||
// из N email по одному типу заново обходил бы SharePoint N раз подряд.
|
||||
var listsByType = new Dictionary<int, IReadOnlyList<RequestListPartitionDto>>();
|
||||
var items = new List<RequestTypeAccessDto>(entities.Count);
|
||||
foreach (var e in entities)
|
||||
{
|
||||
var lists = await GetListsCachedAsync(listsByType, e.RequestTypeId, cancellationToken);
|
||||
items.Add(new RequestTypeAccessDto(e.Id, e.RequestTypeId, e.Email, lists));
|
||||
}
|
||||
|
||||
return Result.Ok<IReadOnlyList<RequestTypeAccessDto>>(items);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<RequestListPartitionDto>> GetListsCachedAsync(
|
||||
Dictionary<int, IReadOnlyList<RequestListPartitionDto>> cache,
|
||||
int requestTypeId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (cache.TryGetValue(requestTypeId, out var cached))
|
||||
return cached;
|
||||
|
||||
var result = await partitions.ListAsync(requestTypeId, cancellationToken);
|
||||
var lists = result.IsSuccess ? result.Value : Array.Empty<RequestListPartitionDto>();
|
||||
cache[requestTypeId] = lists;
|
||||
return lists;
|
||||
}
|
||||
|
||||
/// <summary>Список email'ов с доступом к типу заявок — дёргает Power Automate.</summary>
|
||||
public async Task<Result<IReadOnlyList<string>>> ListEmailsAsync(int requestTypeId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await db.RequestTypes.AsNoTracking().AnyAsync(x => x.Id == requestTypeId, cancellationToken))
|
||||
return Result.Fail<IReadOnlyList<string>>(new NotFoundError("Тип заявки", requestTypeId));
|
||||
|
||||
var emails = await db.RequestTypeAccesses.AsNoTracking()
|
||||
.Where(x => x.RequestTypeId == requestTypeId)
|
||||
.OrderBy(x => x.Email)
|
||||
.Select(x => x.Email)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Result.Ok<IReadOnlyList<string>>(emails);
|
||||
}
|
||||
|
||||
public async Task<Result<RequestTypeAccessDto>> CreateAsync(
|
||||
CreateRequestTypeAccessRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await db.RequestTypes.AsNoTracking().AnyAsync(x => x.Id == request.RequestTypeId, cancellationToken))
|
||||
return Result.Fail<RequestTypeAccessDto>(new NotFoundError("Тип заявки", request.RequestTypeId));
|
||||
|
||||
var email = request.Email.Trim();
|
||||
var exists = await db.RequestTypeAccesses.AsNoTracking()
|
||||
.AnyAsync(x => x.RequestTypeId == request.RequestTypeId && x.Email == email, cancellationToken);
|
||||
if (exists)
|
||||
return Result.Fail<RequestTypeAccessDto>(
|
||||
new ConflictError($"У {email} уже есть доступ к этому типу заявок."));
|
||||
|
||||
var entity = new RequestTypeAccess { RequestTypeId = request.RequestTypeId, Email = email };
|
||||
db.RequestTypeAccesses.Add(entity);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation(
|
||||
"Доступ к типу заявки {RequestTypeId} выдан {Email} пользователем {Author}",
|
||||
entity.RequestTypeId, entity.Email, currentUser.Email);
|
||||
|
||||
var listsResult = await partitions.ListAsync(entity.RequestTypeId, cancellationToken);
|
||||
var lists = listsResult.IsSuccess ? listsResult.Value : Array.Empty<RequestListPartitionDto>();
|
||||
return Result.Ok(new RequestTypeAccessDto(entity.Id, entity.RequestTypeId, entity.Email, lists));
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.RequestTypeAccesses.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (entity is null)
|
||||
return Result.Fail(new NotFoundError("Доступ к типу заявки", id));
|
||||
|
||||
db.RequestTypeAccesses.Remove(entity);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation(
|
||||
"Доступ к типу заявки {RequestTypeId} для {Email} отозван пользователем {Author}",
|
||||
entity.RequestTypeId, entity.Email, currentUser.Email);
|
||||
|
||||
return Result.Ok();
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Application.RequestTypeAccesses;
|
||||
|
||||
public sealed class CreateRequestTypeAccessRequestValidator : AbstractValidator<CreateRequestTypeAccessRequest>
|
||||
{
|
||||
public CreateRequestTypeAccessRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.RequestTypeId).GreaterThan(0).WithMessage("Тип заявки обязателен.");
|
||||
RuleFor(x => x.Email).NotEmpty().EmailAddress().WithMessage("Укажите корректный email.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user