diff --git a/backend/Tnsu.ApprovalRoute.Api/Controllers/RequestTypeAccessController.cs b/backend/Tnsu.ApprovalRoute.Api/Controllers/RequestTypeAccessController.cs new file mode 100644 index 0000000..5e8b8f9 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Api/Controllers/RequestTypeAccessController.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tnsu.ApprovalRoute.Api.Auth; +using Tnsu.ApprovalRoute.Api.Common; +using Tnsu.ApprovalRoute.Application.RequestTypeAccesses; + +namespace Tnsu.ApprovalRoute.Api.Controllers; + +/// +/// Настройка доступа «видеть все заявки типа», а не только свои. Список +/// email'ов по типу дёргает и портал заявок (проверить текущего пользователя), +/// и Power Automate — поэтому политика AnyAuth, как у RemindersController. +/// +[ApiController] +[Authorize(Policy = AuthPolicies.Any)] +[Route("api/request-type-access")] +[Produces("application/json")] +public sealed class RequestTypeAccessController(RequestTypeAccessService service) : ControllerBase +{ + [HttpGet] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task List([FromQuery] int? requestTypeId, CancellationToken ct) => + (await service.ListAsync(requestTypeId, ct)).ToActionResult(); + + [HttpGet("{requestTypeId:int}/emails")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)] + public async Task ListEmails(int requestTypeId, CancellationToken ct) => + (await service.ListEmailsAsync(requestTypeId, ct)).ToActionResult(); + + [HttpPost] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ApiError), StatusCodes.Status409Conflict)] + public async Task Create(CreateRequestTypeAccessRequest body, CancellationToken ct) => + (await service.CreateAsync(body, ct)).ToActionResult(); + + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)] + public async Task Delete(int id, CancellationToken ct) => + (await service.DeleteAsync(id, ct)).ToActionResult(); +} diff --git a/backend/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs b/backend/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs index 5d50eff..4ae58c7 100644 --- a/backend/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs +++ b/backend/Tnsu.ApprovalRoute.Application/ApplicationServiceCollectionExtensions.cs @@ -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(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessDto.cs b/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessDto.cs new file mode 100644 index 0000000..3cb7950 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessDto.cs @@ -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); diff --git a/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessService.cs b/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessService.cs new file mode 100644 index 0000000..6de3477 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessService.cs @@ -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 logger) +{ + public async Task>> 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>(items); + } + + /// Список email'ов с доступом к типу заявок — дёргает Power Automate. + public async Task>> ListEmailsAsync(int requestTypeId, CancellationToken cancellationToken) + { + if (!await db.RequestTypes.AsNoTracking().AnyAsync(x => x.Id == requestTypeId, cancellationToken)) + return Result.Fail>(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>(emails); + } + + public async Task> CreateAsync( + CreateRequestTypeAccessRequest request, CancellationToken cancellationToken) + { + if (!await db.RequestTypes.AsNoTracking().AnyAsync(x => x.Id == request.RequestTypeId, cancellationToken)) + return Result.Fail(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( + 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 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(); + } +} diff --git a/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessValidators.cs b/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessValidators.cs new file mode 100644 index 0000000..44fb104 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessValidators.cs @@ -0,0 +1,12 @@ +using FluentValidation; + +namespace Tnsu.ApprovalRoute.Application.RequestTypeAccesses; + +public sealed class CreateRequestTypeAccessRequestValidator : AbstractValidator +{ + public CreateRequestTypeAccessRequestValidator() + { + RuleFor(x => x.RequestTypeId).GreaterThan(0).WithMessage("Тип заявки обязателен."); + RuleFor(x => x.Email).NotEmpty().EmailAddress().WithMessage("Укажите корректный email."); + } +} diff --git a/backend/Tnsu.ApprovalRoute.Domain/Entities/RequestTypeAccess.cs b/backend/Tnsu.ApprovalRoute.Domain/Entities/RequestTypeAccess.cs new file mode 100644 index 0000000..8aa457a --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Domain/Entities/RequestTypeAccess.cs @@ -0,0 +1,14 @@ +namespace Tnsu.ApprovalRoute.Domain.Entities; + +/// +/// Право одного пользователя видеть ВСЕ заявки одного типа в портале заявок +/// (а не только свои). У одного человека может быть по строке на каждый тип +/// заявок, к которому ему открыт полный доступ. +/// +public class RequestTypeAccess +{ + public int Id { get; set; } + public int RequestTypeId { get; set; } + public RequestType? RequestType { get; set; } + public string Email { get; set; } = string.Empty; +} diff --git a/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs index 733319c..cec17fd 100644 --- a/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs @@ -7,6 +7,7 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence; public class ApprovalRouteDbContext(DbContextOptions options) : DbContext(options) { public DbSet RequestTypes => Set(); + public DbSet RequestTypeAccesses => Set(); public DbSet RequestListPartitions => Set(); public DbSet TemplateRoutes => Set(); public DbSet TemplateRouteSteps => Set(); @@ -35,6 +36,15 @@ public class ApprovalRouteDbContext(DbContextOptions opt e.Property(x => x.Section).HasMaxLength(256).IsRequired(); }); + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Email).HasMaxLength(256).IsRequired(); + e.HasOne(x => x.RequestType).WithMany().HasForeignKey(x => x.RequestTypeId); + // Одна и та же пара тип+email не должна выдаваться дважды. + e.HasIndex(x => new { x.RequestTypeId, x.Email }).IsUnique(); + }); + modelBuilder.Entity(e => { e.HasKey(x => x.Id); diff --git a/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260925092352_AddRequestTypeAccess.Designer.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260925092352_AddRequestTypeAccess.Designer.cs new file mode 100644 index 0000000..0dc3b82 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260925092352_AddRequestTypeAccess.Designer.cs @@ -0,0 +1,411 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Tnsu.ApprovalRoute.Infrastructure.Persistence; + +#nullable disable + +namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApprovalRouteDbContext))] + [Migration("20260925092352_AddRequestTypeAccess")] + partial class AddRequestTypeAccess + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestListPartition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ListId") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("list_id"); + + b.Property("Number") + .HasColumnType("integer") + .HasColumnName("number"); + + b.Property("RequestTypeId") + .HasColumnType("integer") + .HasColumnName("request_type_id"); + + b.Property("Title") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("pk_request_list_partitions"); + + b.HasIndex("RequestTypeId", "Number") + .IsUnique() + .HasDatabaseName("ix_request_list_partitions_request_type_id_number"); + + b.ToTable("request_list_partitions", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AskOverdueReason") + .HasColumnType("boolean") + .HasColumnName("ask_overdue_reason"); + + b.Property("ListId") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("list_id"); + + b.Property("ListPartitioningStartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("list_partitioning_started_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("PageUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("page_url"); + + b.Property("Section") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("section"); + + b.Property("SiteUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("site_url"); + + b.Property("SlaHours") + .HasColumnType("integer") + .HasColumnName("sla_hours"); + + b.HasKey("Id") + .HasName("pk_request_types"); + + b.ToTable("request_types", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestTypeAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("RequestTypeId") + .HasColumnType("integer") + .HasColumnName("request_type_id"); + + b.HasKey("Id") + .HasName("pk_request_type_accesses"); + + b.HasIndex("RequestTypeId", "Email") + .IsUnique() + .HasDatabaseName("ix_request_type_accesses_request_type_id_email"); + + b.ToTable("request_type_accesses", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ByProject") + .HasColumnType("boolean") + .HasColumnName("by_project"); + + b.Property("CompanyId") + .HasColumnType("uuid") + .HasColumnName("company_id"); + + b.Property("DepartmentId") + .HasColumnType("uuid") + .HasColumnName("department_id"); + + b.Property("RequestTypeId") + .HasColumnType("integer") + .HasColumnName("request_type_id"); + + b.Property("RequiresControl") + .HasColumnType("boolean") + .HasColumnName("requires_control"); + + b.HasKey("Id") + .HasName("pk_template_routes"); + + b.HasIndex("RequestTypeId", "DepartmentId", "CompanyId", "ByProject") + .IsUnique() + .HasDatabaseName("ix_template_routes_request_type_id_department_id_company_id_by"); + + b.ToTable("template_routes", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRouteStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowReassignToOtherApprovers") + .HasColumnType("boolean") + .HasColumnName("allow_reassign_to_other_approvers"); + + b.Property("AskOverdueReason") + .HasColumnType("boolean") + .HasColumnName("ask_overdue_reason"); + + b.Property("AutoApprove") + .HasColumnType("boolean") + .HasColumnName("auto_approve"); + + b.Property("ConditionsLogic") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasColumnName("conditions_logic"); + + b.Property("ExecutorEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("executor_email"); + + b.Property("HeadType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("head_type"); + + b.Property("OrderNo") + .HasColumnType("integer") + .HasColumnName("order_no"); + + b.Property("Position") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("position"); + + b.Property("ProjectRole") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("project_role"); + + b.Property("RouteId") + .HasColumnType("integer") + .HasColumnName("route_id"); + + b.Property("SlaHours") + .HasColumnType("integer") + .HasColumnName("sla_hours"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_template_route_steps"); + + b.HasIndex("RouteId", "OrderNo") + .HasDatabaseName("ix_template_route_steps_route_id_order_no"); + + b.ToTable("template_route_steps", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRouteStepCondition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FieldName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("field_name"); + + b.Property("Operation") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("operation"); + + b.Property("OrderNo") + .HasColumnType("integer") + .HasColumnName("order_no"); + + b.Property("StepId") + .HasColumnType("integer") + .HasColumnName("step_id"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_template_route_step_conditions"); + + b.HasIndex("StepId", "OrderNo") + .HasDatabaseName("ix_template_route_step_conditions_step_id_order_no"); + + b.ToTable("template_route_step_conditions", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Infrastructure.Persistence.Entities.AppSetting", b => + { + b.Property("OptionName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("option_name"); + + b.Property("FileData") + .HasColumnType("bytea") + .HasColumnName("file_data"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("OptionName") + .HasName("pk_settings"); + + b.ToTable("settings", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestListPartition", b => + { + b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.RequestType", "RequestType") + .WithMany("ListPartitions") + .HasForeignKey("RequestTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_request_list_partitions_request_types_request_type_id"); + + b.Navigation("RequestType"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestTypeAccess", b => + { + b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.RequestType", "RequestType") + .WithMany() + .HasForeignKey("RequestTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_request_type_accesses_request_types_request_type_id"); + + b.Navigation("RequestType"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => + { + b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.RequestType", "RequestType") + .WithMany("Routes") + .HasForeignKey("RequestTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_template_routes_request_types_request_type_id"); + + b.Navigation("RequestType"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRouteStep", b => + { + b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", "Route") + .WithMany("Steps") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_template_route_steps_template_routes_route_id"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRouteStepCondition", b => + { + b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.TemplateRouteStep", "Step") + .WithMany("Conditions") + .HasForeignKey("StepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_template_route_step_conditions_template_route_steps_step_id"); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestType", b => + { + b.Navigation("ListPartitions"); + + b.Navigation("Routes"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRouteStep", b => + { + b.Navigation("Conditions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260925092352_AddRequestTypeAccess.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260925092352_AddRequestTypeAccess.cs new file mode 100644 index 0000000..a378d54 --- /dev/null +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260925092352_AddRequestTypeAccess.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddRequestTypeAccess : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "request_type_accesses", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + request_type_id = table.Column(type: "integer", nullable: false), + email = table.Column(type: "character varying(256)", maxLength: 256, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_request_type_accesses", x => x.id); + table.ForeignKey( + name: "fk_request_type_accesses_request_types_request_type_id", + column: x => x.request_type_id, + principalTable: "request_types", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_request_type_accesses_request_type_id_email", + table: "request_type_accesses", + columns: new[] { "request_type_id", "email" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "request_type_accesses"); + } + } +} diff --git a/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs index da9803b..34002d7 100644 --- a/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs +++ b/backend/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs @@ -119,6 +119,35 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations b.ToTable("request_types", (string)null); }); + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestTypeAccess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("RequestTypeId") + .HasColumnType("integer") + .HasColumnName("request_type_id"); + + b.HasKey("Id") + .HasName("pk_request_type_accesses"); + + b.HasIndex("RequestTypeId", "Email") + .IsUnique() + .HasDatabaseName("ix_request_type_accesses_request_type_id_email"); + + b.ToTable("request_type_accesses", (string)null); + }); + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => { b.Property("Id") @@ -309,6 +338,18 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations b.Navigation("RequestType"); }); + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestTypeAccess", b => + { + b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.RequestType", "RequestType") + .WithMany() + .HasForeignKey("RequestTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_request_type_accesses_request_types_request_type_id"); + + b.Navigation("RequestType"); + }); + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => { b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.RequestType", "RequestType")