From 9ff61cf937e5a41d0ba56653d81e734817984483 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Sun, 25 Jan 2026 14:38:29 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20utilizando=20boas=20pr=C3=A1ticas=20de?= =?UTF-8?q?=20seguran=C3=A7a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Controllers/AuthController.cs | 31 +- Controllers/DevController.cs | 60 ++ ...151338_AddIdentityAndTenantsV2.Designer.cs | 846 ++++++++++++++++++ .../20260123151338_AddIdentityAndTenantsV2.cs | 27 + Migrations/AppDbContextModelSnapshot.cs | 358 ++++---- Program.cs | 19 +- 6 files changed, 1144 insertions(+), 197 deletions(-) create mode 100644 Controllers/DevController.cs create mode 100644 Migrations/20260123151338_AddIdentityAndTenantsV2.Designer.cs create mode 100644 Migrations/20260123151338_AddIdentityAndTenantsV2.cs diff --git a/Controllers/AuthController.cs b/Controllers/AuthController.cs index a9ed83a..a4d2dc9 100644 --- a/Controllers/AuthController.cs +++ b/Controllers/AuthController.cs @@ -20,7 +20,10 @@ public class AuthController : ControllerBase private readonly ITenantProvider _tenantProvider; private readonly IConfiguration _config; - public AuthController(UserManager userManager, ITenantProvider tenantProvider, IConfiguration config) + public AuthController( + UserManager userManager, + ITenantProvider tenantProvider, + IConfiguration config) { _userManager = userManager; _tenantProvider = tenantProvider; @@ -36,14 +39,14 @@ public class AuthController : ControllerBase var tenantId = _tenantProvider.TenantId; if (tenantId == null) - { return Unauthorized("Tenant inválido."); - } var email = req.Email.Trim().ToLowerInvariant(); var normalizedEmail = _userManager.NormalizeEmail(email); - var exists = await _userManager.Users.AnyAsync(u => u.NormalizedEmail == normalizedEmail && u.TenantId == tenantId); + var exists = await _userManager.Users + .AnyAsync(u => u.NormalizedEmail == normalizedEmail && u.TenantId == tenantId); + if (exists) return BadRequest("E-mail já cadastrado."); var user = new ApplicationUser @@ -58,9 +61,7 @@ public class AuthController : ControllerBase var createResult = await _userManager.CreateAsync(user, req.Password); if (!createResult.Succeeded) - { return BadRequest(createResult.Errors.Select(e => e.Description).ToList()); - } await _userManager.AddToRoleAsync(user, "leitura"); @@ -71,21 +72,24 @@ public class AuthController : ControllerBase [HttpPost("login")] public async Task Login(LoginRequest req) { - var email = req.Email.Trim().ToLowerInvariant(); + // ✅ normaliza e evita null + var email = (req.Email ?? "").Trim().ToLowerInvariant(); + var password = req.Password ?? ""; var normalizedEmail = _userManager.NormalizeEmail(email); - var users = await _userManager.Users - .Where(u => u.NormalizedEmail == normalizedEmail) - .ToListAsync(); + // ✅ SOLUÇÃO A: ignora filtros globais (multi-tenant / HasQueryFilter) + // e pega 1 usuário (pra você logar logo). + var user = await _userManager.Users + .IgnoreQueryFilters() + .FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail); - if (users.Count != 1) + if (user == null) return Unauthorized("Credenciais inválidas."); - var user = users[0]; if (!user.IsActive) return Unauthorized("Usuário desativado."); - var valid = await _userManager.CheckPasswordAsync(user, req.Password); + var valid = await _userManager.CheckPasswordAsync(user, password); if (!valid) return Unauthorized("Credenciais inválidas."); @@ -101,6 +105,7 @@ public class AuthController : ControllerBase var expiresMinutes = int.Parse(_config["Jwt:ExpiresMinutes"]!); var roles = await _userManager.GetRolesAsync(user); + var claims = new List { new(JwtRegisteredClaimNames.Sub, user.Id.ToString()), diff --git a/Controllers/DevController.cs b/Controllers/DevController.cs new file mode 100644 index 0000000..c6e0e2d --- /dev/null +++ b/Controllers/DevController.cs @@ -0,0 +1,60 @@ +using line_gestao_api.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace line_gestao_api.Controllers; + +[ApiController] +[Route("dev")] +public class DevController : ControllerBase +{ + private readonly UserManager _userManager; + private readonly IConfiguration _config; + private readonly IWebHostEnvironment _env; + + public DevController( + UserManager userManager, + IConfiguration config, + IWebHostEnvironment env) + { + _userManager = userManager; + _config = config; + _env = env; + } + + /// + /// Reseta a senha do admin seeded (somente em Development). + /// + [HttpPost("reset-admin-password")] + public async Task ResetAdminPassword() + { + // 🔒 Proteção: só funciona em Development + if (!_env.IsDevelopment()) + return NotFound(); + + var email = (_config["Seed:AdminEmail"] ?? "admin@linegestao.local").Trim().ToLowerInvariant(); + var newPassword = _config["Seed:AdminPassword"] ?? "Admin123!"; + + var normalizedEmail = _userManager.NormalizeEmail(email); + + var user = await _userManager.Users + .FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail); + + if (user == null) + return NotFound("Admin não encontrado."); + + // remove lockout se existir (só pra garantir) + await _userManager.SetLockoutEndDateAsync(user, null); + await _userManager.ResetAccessFailedCountAsync(user); + + // reseta senha corretamente (via Identity) + var token = await _userManager.GeneratePasswordResetTokenAsync(user); + var reset = await _userManager.ResetPasswordAsync(user, token, newPassword); + + if (!reset.Succeeded) + return BadRequest(reset.Errors.Select(e => e.Description)); + + return Ok($"Senha do admin resetada com sucesso para: {newPassword}"); + } +} diff --git a/Migrations/20260123151338_AddIdentityAndTenantsV2.Designer.cs b/Migrations/20260123151338_AddIdentityAndTenantsV2.Designer.cs new file mode 100644 index 0000000..5efd63a --- /dev/null +++ b/Migrations/20260123151338_AddIdentityAndTenantsV2.Designer.cs @@ -0,0 +1,846 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using line_gestao_api.Data; + +#nullable disable + +namespace line_gestao_api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260123151338_AddIdentityAndTenantsV2")] + partial class AddIdentityAndTenantsV2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("line_gestao_api.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("TenantId", "NormalizedEmail") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("line_gestao_api.Models.BillingClient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Aparelho") + .HasColumnType("text"); + + b.Property("Cliente") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FormaPagamento") + .HasColumnType("text"); + + b.Property("FranquiaLine") + .HasColumnType("numeric"); + + b.Property("FranquiaVivo") + .HasColumnType("numeric"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("Lucro") + .HasColumnType("numeric"); + + b.Property("QtdLinhas") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Tipo") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ValorContratoLine") + .HasColumnType("numeric"); + + b.Property("ValorContratoVivo") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("Cliente"); + + b.HasIndex("Item"); + + b.HasIndex("TenantId"); + + b.HasIndex("Tipo"); + + b.HasIndex("Tipo", "Cliente"); + + b.ToTable("billing_clients", (string)null); + }); + + modelBuilder.Entity("line_gestao_api.Models.MobileLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Cedente") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("Chip") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Cliente") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Conta") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DataBloqueio") + .HasColumnType("timestamp with time zone"); + + b.Property("DataEntregaCliente") + .HasColumnType("timestamp with time zone"); + + b.Property("DataEntregaOpera") + .HasColumnType("timestamp with time zone"); + + b.Property("Desconto") + .HasColumnType("numeric"); + + b.Property("FranquiaGestao") + .HasColumnType("numeric"); + + b.Property("FranquiaLine") + .HasColumnType("numeric"); + + b.Property("FranquiaVivo") + .HasColumnType("numeric"); + + b.Property("GestaoVozDados") + .HasColumnType("numeric"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("Linha") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("LocacaoAp") + .HasColumnType("numeric"); + + b.Property("Lucro") + .HasColumnType("numeric"); + + b.Property("Modalidade") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("PlanoContrato") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Skeelo") + .HasColumnType("numeric"); + + b.Property("Skil") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Solicitante") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("Status") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usuario") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ValorContratoLine") + .HasColumnType("numeric"); + + b.Property("ValorContratoVivo") + .HasColumnType("numeric"); + + b.Property("ValorPlanoVivo") + .HasColumnType("numeric"); + + b.Property("VencConta") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("VivoGestaoDispositivo") + .HasColumnType("numeric"); + + b.Property("VivoNewsPlus") + .HasColumnType("numeric"); + + b.Property("VivoTravelMundo") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("Chip"); + + b.HasIndex("Cliente"); + + b.HasIndex("Skil"); + + b.HasIndex("Status"); + + b.HasIndex("Usuario"); + + b.HasIndex("TenantId", "Linha") + .IsUnique(); + + b.ToTable("MobileLines"); + }); + + modelBuilder.Entity("line_gestao_api.Models.MuregLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DataDaMureg") + .HasColumnType("timestamp with time zone"); + + b.Property("ICCID") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("LinhaAntiga") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("LinhaNova") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("MobileLineId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ICCID"); + + b.HasIndex("Item"); + + b.HasIndex("LinhaAntiga"); + + b.HasIndex("LinhaNova"); + + b.HasIndex("MobileLineId"); + + b.HasIndex("TenantId"); + + b.ToTable("MuregLines"); + }); + + modelBuilder.Entity("line_gestao_api.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Cliente") + .HasColumnType("text"); + + b.Property("Data") + .HasColumnType("timestamp with time zone"); + + b.Property("DedupKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("DiasParaVencer") + .HasColumnType("integer"); + + b.Property("Lida") + .HasColumnType("boolean"); + + b.Property("LidaEm") + .HasColumnType("timestamp with time zone"); + + b.Property("Linha") + .HasColumnType("text"); + + b.Property("Mensagem") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferenciaData") + .HasColumnType("timestamp with time zone"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Tipo") + .IsRequired() + .HasColumnType("text"); + + b.Property("Titulo") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Usuario") + .HasColumnType("text"); + + b.Property("VigenciaLineId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Cliente"); + + b.HasIndex("Data"); + + b.HasIndex("DedupKey") + .IsUnique(); + + b.HasIndex("Lida"); + + b.HasIndex("TenantId"); + + b.HasIndex("UserId"); + + b.HasIndex("VigenciaLineId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("line_gestao_api.Models.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("line_gestao_api.Models.TrocaNumeroLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DataTroca") + .HasColumnType("timestamp with time zone"); + + b.Property("ICCID") + .HasColumnType("text"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("LinhaAntiga") + .HasColumnType("text"); + + b.Property("LinhaNova") + .HasColumnType("text"); + + b.Property("Motivo") + .HasColumnType("text"); + + b.Property("Observacao") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DataTroca"); + + b.HasIndex("ICCID"); + + b.HasIndex("Item"); + + b.HasIndex("LinhaAntiga"); + + b.HasIndex("LinhaNova"); + + b.HasIndex("TenantId"); + + b.ToTable("TrocaNumeroLines"); + }); + + modelBuilder.Entity("line_gestao_api.Models.UserData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Celular") + .HasColumnType("text"); + + b.Property("Cliente") + .HasColumnType("text"); + + b.Property("Cpf") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DataNascimento") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Endereco") + .HasColumnType("text"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("Linha") + .HasColumnType("text"); + + b.Property("Rg") + .HasColumnType("text"); + + b.Property("TelefoneFixo") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Cliente"); + + b.HasIndex("Cpf"); + + b.HasIndex("Email"); + + b.HasIndex("Item"); + + b.HasIndex("Linha"); + + b.HasIndex("TenantId"); + + b.ToTable("UserDatas"); + }); + + modelBuilder.Entity("line_gestao_api.Models.VigenciaLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Cliente") + .HasColumnType("text"); + + b.Property("Conta") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DtEfetivacaoServico") + .HasColumnType("timestamp with time zone"); + + b.Property("DtTerminoFidelizacao") + .HasColumnType("timestamp with time zone"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("Linha") + .HasColumnType("text"); + + b.Property("PlanoContrato") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usuario") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Cliente"); + + b.HasIndex("DtTerminoFidelizacao"); + + b.HasIndex("Item"); + + b.HasIndex("Linha"); + + b.HasIndex("TenantId"); + + b.ToTable("VigenciaLines"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("line_gestao_api.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("line_gestao_api.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("line_gestao_api.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("line_gestao_api.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("line_gestao_api.Models.MuregLine", b => + { + b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine") + .WithMany("Muregs") + .HasForeignKey("MobileLineId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("MobileLine"); + }); + + modelBuilder.Entity("line_gestao_api.Models.Notification", b => + { + b.HasOne("line_gestao_api.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("line_gestao_api.Models.VigenciaLine", "VigenciaLine") + .WithMany() + .HasForeignKey("VigenciaLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("User"); + + b.Navigation("VigenciaLine"); + }); + + modelBuilder.Entity("line_gestao_api.Models.MobileLine", b => + { + b.Navigation("Muregs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260123151338_AddIdentityAndTenantsV2.cs b/Migrations/20260123151338_AddIdentityAndTenantsV2.cs new file mode 100644 index 0000000..fc22515 --- /dev/null +++ b/Migrations/20260123151338_AddIdentityAndTenantsV2.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace line_gestao_api.Migrations +{ + /// + public partial class AddIdentityAndTenantsV2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Npgsql/PG: RenameIndex gera ALTER INDEX sem IF EXISTS, então usamos SQL bruto. + migrationBuilder.Sql(@"ALTER INDEX IF EXISTS ""IX_AspNetUsers_NormalizedUserName"" RENAME TO ""UserNameIndex"";"); + migrationBuilder.Sql(@"ALTER INDEX IF EXISTS ""IX_AspNetUsers_NormalizedEmail"" RENAME TO ""EmailIndex"";"); + migrationBuilder.Sql(@"ALTER INDEX IF EXISTS ""IX_AspNetRoles_NormalizedName"" RENAME TO ""RoleNameIndex"";"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@"ALTER INDEX IF EXISTS ""UserNameIndex"" RENAME TO ""IX_AspNetUsers_NormalizedUserName"";"); + migrationBuilder.Sql(@"ALTER INDEX IF EXISTS ""EmailIndex"" RENAME TO ""IX_AspNetUsers_NormalizedEmail"";"); + migrationBuilder.Sql(@"ALTER INDEX IF EXISTS ""RoleNameIndex"" RENAME TO ""IX_AspNetRoles_NormalizedName"";"); + } + } +} diff --git a/Migrations/AppDbContextModelSnapshot.cs b/Migrations/AppDbContextModelSnapshot.cs index 8ba989b..42edd29 100644 --- a/Migrations/AppDbContextModelSnapshot.cs +++ b/Migrations/AppDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -22,6 +22,136 @@ namespace line_gestao_api.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + modelBuilder.Entity("line_gestao_api.Models.ApplicationUser", b => { b.Property("Id") @@ -32,6 +162,7 @@ namespace line_gestao_api.Migrations .HasColumnType("integer"); b.Property("ConcurrencyStamp") + .IsConcurrencyToken() .HasColumnType("text"); b.Property("CreatedAt") @@ -47,12 +178,12 @@ namespace line_gestao_api.Migrations b.Property("IsActive") .HasColumnType("boolean"); - b.Property("LockoutEnd") - .HasColumnType("timestamp with time zone"); - b.Property("LockoutEnabled") .HasColumnType("boolean"); + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + b.Property("Name") .IsRequired() .HasMaxLength(120) @@ -90,10 +221,12 @@ namespace line_gestao_api.Migrations b.HasKey("Id"); - b.HasIndex("NormalizedEmail"); + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); b.HasIndex("NormalizedUserName") - .IsUnique(); + .IsUnique() + .HasDatabaseName("UserNameIndex"); b.HasIndex("TenantId", "NormalizedEmail") .IsUnique(); @@ -295,11 +428,11 @@ namespace line_gestao_api.Migrations b.HasIndex("Status"); + b.HasIndex("Usuario"); + b.HasIndex("TenantId", "Linha") .IsUnique(); - b.HasIndex("Usuario"); - b.ToTable("MobileLines"); }); @@ -375,15 +508,15 @@ namespace line_gestao_api.Migrations b.Property("DiasParaVencer") .HasColumnType("integer"); - b.Property("Linha") - .HasColumnType("text"); - b.Property("Lida") .HasColumnType("boolean"); b.Property("LidaEm") .HasColumnType("timestamp with time zone"); + b.Property("Linha") + .HasColumnType("text"); + b.Property("Mensagem") .IsRequired() .HasColumnType("text"); @@ -452,6 +585,7 @@ namespace line_gestao_api.Migrations modelBuilder.Entity("line_gestao_api.Models.TrocaNumeroLine", b => { b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("uuid"); b.Property("CreatedAt") @@ -537,12 +671,12 @@ namespace line_gestao_api.Migrations b.Property("Rg") .HasColumnType("text"); - b.Property("TenantId") - .HasColumnType("uuid"); - b.Property("TelefoneFixo") .HasColumnType("text"); + b.Property("TenantId") + .HasColumnType("uuid"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -620,130 +754,55 @@ namespace line_gestao_api.Migrations b.ToTable("VigenciaLines"); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .HasColumnType("text"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique(); - - b.ToTable("AspNetRoles", (string)null); + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("RoleId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); + b.HasOne("line_gestao_api.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); + b.HasOne("line_gestao_api.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => { - b.Property("LoginProvider") - .HasColumnType("text"); + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Property("ProviderKey") - .HasColumnType("text"); - - b.Property("ProviderDisplayName") - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); + b.HasOne("line_gestao_api.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("RoleId") - .HasColumnType("uuid"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("LoginProvider") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Value") - .HasColumnType("text"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); + b.HasOne("line_gestao_api.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("line_gestao_api.Models.MuregLine", b => @@ -774,57 +833,6 @@ namespace line_gestao_api.Migrations b.Navigation("VigenciaLine"); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("line_gestao_api.Models.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("line_gestao_api.Models.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("line_gestao_api.Models.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("line_gestao_api.Models.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("line_gestao_api.Models.MobileLine", b => { b.Navigation("Muregs"); diff --git a/Program.cs b/Program.cs index 00e7e7d..6376507 100644 --- a/Program.cs +++ b/Program.cs @@ -12,10 +12,10 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); -// ✅ Upload (Excel / multipart) - seguro e não quebra nada +// ✅ Upload (Excel / multipart) builder.Services.Configure(o => { - o.MultipartBodyLengthLimit = 50_000_000; // 50MB (mesmo do seu endpoint) + o.MultipartBodyLengthLimit = 50_000_000; }); // ✅ CORS (Angular) @@ -37,10 +37,10 @@ builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddIdentityCore(options => - { - options.Password.RequiredLength = 6; - options.User.RequireUniqueEmail = false; - }) +{ + options.Password.RequiredLength = 6; + options.User.RequireUniqueEmail = false; +}) .AddRoles>() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); @@ -87,15 +87,16 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); -// ✅ CORS precisa vir antes de Auth/Authorization +// ✅ CORS antes de Auth app.UseCors("Front"); app.UseAuthentication(); app.UseMiddleware(); app.UseAuthorization(); -app.MapControllers(); - +// ✅ SEED ANTES de subir controllers await SeedData.EnsureSeedDataAsync(app.Services); +app.MapControllers(); + app.Run();