Добавить таблицу и API доступа к типу заявок

Настраиваемый список email по типу заявки: кто может видеть все заявки
этого типа, а не только свои. Отдельная ручка со списком email по типу —
её будет дёргать Power Automate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-09-25 14:24:30 +05:00
parent 1bf8d4ca11
commit 5bf38cd66f
10 changed files with 665 additions and 0 deletions
@@ -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,5 @@
namespace Tnsu.ApprovalRoute.Application.RequestTypeAccesses;
public sealed record RequestTypeAccessDto(int Id, int RequestTypeId, string Email);
public sealed record CreateRequestTypeAccessRequest(int RequestTypeId, string Email);
@@ -0,0 +1,78 @@
using FluentResults;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Tnsu.ApprovalRoute.Application.Common.Errors;
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
using Tnsu.ApprovalRoute.Domain.Entities;
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
namespace Tnsu.ApprovalRoute.Application.RequestTypeAccesses;
public sealed class RequestTypeAccessService(
ApprovalRouteDbContext db, ICurrentUser currentUser, ILogger<RequestTypeAccessService> logger)
{
public async Task<Result<IReadOnlyList<RequestTypeAccessDto>>> ListAsync(
int? requestTypeId, CancellationToken cancellationToken)
{
var items = await db.RequestTypeAccesses.AsNoTracking()
.Where(x => requestTypeId == null || x.RequestTypeId == requestTypeId)
.OrderBy(x => x.RequestTypeId).ThenBy(x => x.Email)
.Select(x => new RequestTypeAccessDto(x.Id, x.RequestTypeId, x.Email))
.ToListAsync(cancellationToken);
return Result.Ok<IReadOnlyList<RequestTypeAccessDto>>(items);
}
/// <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);
return Result.Ok(new RequestTypeAccessDto(entity.Id, entity.RequestTypeId, entity.Email));
}
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();
}
}
@@ -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.");
}
}