diff --git a/Controllers/AuthController.cs b/Controllers/AuthController.cs index 888e7b3..a9ed83a 100644 --- a/Controllers/AuthController.cs +++ b/Controllers/AuthController.cs @@ -1,9 +1,10 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; -using line_gestao_api.Data; using line_gestao_api.Dtos; using line_gestao_api.Models; +using line_gestao_api.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -15,80 +16,101 @@ namespace line_gestao_api.Controllers; [Route("auth")] public class AuthController : ControllerBase { - private readonly AppDbContext _db; + private readonly UserManager _userManager; + private readonly ITenantProvider _tenantProvider; private readonly IConfiguration _config; - public AuthController(AppDbContext db, IConfiguration config) + public AuthController(UserManager userManager, ITenantProvider tenantProvider, IConfiguration config) { - _db = db; + _userManager = userManager; + _tenantProvider = tenantProvider; _config = config; } [HttpPost("register")] + [Authorize(Roles = "admin")] public async Task Register(RegisterRequest req) { - // ✅ NOVO: valida confirmação de senha (não vai pro banco, só valida) if (req.Password != req.ConfirmPassword) return BadRequest("As senhas não conferem."); - var email = req.Email.Trim().ToLower(); + var tenantId = _tenantProvider.TenantId; + if (tenantId == null) + { + return Unauthorized("Tenant inválido."); + } - var exists = await _db.Users.AnyAsync(u => u.Email.ToLower() == email); + var email = req.Email.Trim().ToLowerInvariant(); + var normalizedEmail = _userManager.NormalizeEmail(email); + + var exists = await _userManager.Users.AnyAsync(u => u.NormalizedEmail == normalizedEmail && u.TenantId == tenantId); if (exists) return BadRequest("E-mail já cadastrado."); - // ✅ NOVO: normaliza telefone (salva somente dígitos) - var phoneDigits = new string((req.Phone ?? string.Empty).Where(char.IsDigit).ToArray()); - - var user = new User + var user = new ApplicationUser { Name = req.Name.Trim(), Email = email, - Phone = phoneDigits // ✅ NOVO + UserName = email, + TenantId = tenantId.Value, + IsActive = true, + EmailConfirmed = true }; - var hasher = new PasswordHasher(); - user.PasswordHash = hasher.HashPassword(user, req.Password); + var createResult = await _userManager.CreateAsync(user, req.Password); + if (!createResult.Succeeded) + { + return BadRequest(createResult.Errors.Select(e => e.Description).ToList()); + } - _db.Users.Add(user); - await _db.SaveChangesAsync(); + await _userManager.AddToRoleAsync(user, "leitura"); - var token = GenerateJwt(user); + var token = await GenerateJwtAsync(user); return Ok(new AuthResponse(token)); } [HttpPost("login")] public async Task Login(LoginRequest req) { - var email = req.Email.Trim().ToLower(); + var email = req.Email.Trim().ToLowerInvariant(); + var normalizedEmail = _userManager.NormalizeEmail(email); - var user = await _db.Users.FirstOrDefaultAsync(u => u.Email.ToLower() == email); - if (user is null) return Unauthorized("Credenciais inválidas."); + var users = await _userManager.Users + .Where(u => u.NormalizedEmail == normalizedEmail) + .ToListAsync(); - var hasher = new PasswordHasher(); - var result = hasher.VerifyHashedPassword(user, user.PasswordHash, req.Password); - - if (result == PasswordVerificationResult.Failed) + if (users.Count != 1) return Unauthorized("Credenciais inválidas."); - var token = GenerateJwt(user); + var user = users[0]; + if (!user.IsActive) + return Unauthorized("Usuário desativado."); + + var valid = await _userManager.CheckPasswordAsync(user, req.Password); + if (!valid) + return Unauthorized("Credenciais inválidas."); + + var token = await GenerateJwtAsync(user); return Ok(new AuthResponse(token)); } - private string GenerateJwt(User user) + private async Task GenerateJwtAsync(ApplicationUser user) { var key = _config["Jwt:Key"]!; var issuer = _config["Jwt:Issuer"]!; var audience = _config["Jwt:Audience"]!; var expiresMinutes = int.Parse(_config["Jwt:ExpiresMinutes"]!); + var roles = await _userManager.GetRolesAsync(user); var claims = new List { new(JwtRegisteredClaimNames.Sub, user.Id.ToString()), - new(JwtRegisteredClaimNames.Email, user.Email), + new(JwtRegisteredClaimNames.Email, user.Email ?? string.Empty), new("name", user.Name), - new("phone", user.Phone ?? string.Empty) // ✅ NOVO (opcional, mas útil) + new("tenantId", user.TenantId.ToString()) }; + claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r))); + var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); var creds = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); diff --git a/Controllers/UsersController.cs b/Controllers/UsersController.cs new file mode 100644 index 0000000..ee908e2 --- /dev/null +++ b/Controllers/UsersController.cs @@ -0,0 +1,283 @@ +using line_gestao_api.Data; +using line_gestao_api.Dtos; +using line_gestao_api.Models; +using line_gestao_api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace line_gestao_api.Controllers; + +[ApiController] +[Route("api/users")] +[Authorize] +public class UsersController : ControllerBase +{ + private readonly AppDbContext _db; + private readonly UserManager _userManager; + private readonly RoleManager> _roleManager; + private readonly ITenantProvider _tenantProvider; + + public UsersController( + AppDbContext db, + UserManager userManager, + RoleManager> roleManager, + ITenantProvider tenantProvider) + { + _db = db; + _userManager = userManager; + _roleManager = roleManager; + _tenantProvider = tenantProvider; + } + + [HttpPost] + [Authorize(Roles = "admin")] + public async Task> Create([FromBody] UserCreateRequest req) + { + var errors = ValidateCreate(req); + if (errors.Count > 0) + { + return BadRequest(new ValidationErrorResponse { Errors = errors }); + } + + if (_tenantProvider.TenantId == null) + { + return Unauthorized(); + } + + var tenantId = _tenantProvider.TenantId.Value; + var email = req.Email.Trim().ToLowerInvariant(); + var normalizedEmail = _userManager.NormalizeEmail(email); + + var exists = await _userManager.Users + .AnyAsync(u => u.TenantId == tenantId && u.NormalizedEmail == normalizedEmail); + + if (exists) + { + return BadRequest(new ValidationErrorResponse + { + Errors = new List + { + new() { Field = "email", Message = "E-mail já cadastrado." } + } + }); + } + + var role = req.Permissao.Trim().ToLowerInvariant(); + if (!await _roleManager.RoleExistsAsync(role)) + { + return BadRequest(new ValidationErrorResponse + { + Errors = new List + { + new() { Field = "permissao", Message = "Permissão inválida." } + } + }); + } + + var user = new ApplicationUser + { + Name = req.Nome.Trim(), + Email = email, + UserName = email, + TenantId = tenantId, + IsActive = true, + EmailConfirmed = true + }; + + var createResult = await _userManager.CreateAsync(user, req.Senha); + if (!createResult.Succeeded) + { + return BadRequest(new ValidationErrorResponse + { + Errors = createResult.Errors.Select(e => new ValidationErrorDto + { + Field = "senha", + Message = e.Description + }).ToList() + }); + } + + await _userManager.AddToRoleAsync(user, role); + + var response = new UserListItemDto + { + Id = user.Id, + Nome = user.Name, + Email = user.Email ?? string.Empty, + Permissao = role, + TenantId = user.TenantId, + Ativo = user.IsActive + }; + + return CreatedAtAction(nameof(GetById), new { id = user.Id }, response); + } + + [HttpGet] + [Authorize(Roles = "admin")] + public async Task>> GetAll( + [FromQuery] string? search, + [FromQuery] string? permissao, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20) + { + page = page < 1 ? 1 : page; + pageSize = pageSize < 1 ? 20 : pageSize; + + var usersQuery = _userManager.Users.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(search)) + { + var term = search.Trim(); + usersQuery = usersQuery.Where(u => + EF.Functions.ILike(u.Name, $"%{term}%") || + EF.Functions.ILike(u.Email ?? string.Empty, $"%{term}%")); + } + + IQueryable userIdsByRole = Enumerable.Empty().AsQueryable(); + if (!string.IsNullOrWhiteSpace(permissao)) + { + var roleName = permissao.Trim().ToLowerInvariant(); + userIdsByRole = from ur in _db.UserRoles + join r in _db.Roles on ur.RoleId equals r.Id + where r.Name == roleName + select ur.UserId; + usersQuery = usersQuery.Where(u => userIdsByRole.Contains(u.Id)); + } + + var total = await usersQuery.CountAsync(); + + var users = await usersQuery + .OrderBy(u => u.Name) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + var roleLookup = await (from ur in _db.UserRoles + join r in _db.Roles on ur.RoleId equals r.Id + where users.Select(u => u.Id).Contains(ur.UserId) + select new { ur.UserId, Role = r.Name ?? "" }) + .ToListAsync(); + + var roleMap = roleLookup + .GroupBy(x => x.UserId) + .ToDictionary(g => g.Key, g => g.First().Role); + + var items = users.Select(u => new UserListItemDto + { + Id = u.Id, + Nome = u.Name, + Email = u.Email ?? string.Empty, + Permissao = roleMap.GetValueOrDefault(u.Id, string.Empty), + TenantId = u.TenantId, + Ativo = u.IsActive + }).ToList(); + + return Ok(new PagedResult + { + Page = page, + PageSize = pageSize, + Total = total, + Items = items + }); + } + + [HttpGet("{id:guid}")] + [Authorize(Roles = "admin")] + public async Task> GetById(Guid id) + { + var user = await _userManager.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == id); + if (user == null) + { + return NotFound(); + } + + var roles = await _userManager.GetRolesAsync(user); + var role = roles.FirstOrDefault() ?? string.Empty; + + return Ok(new UserListItemDto + { + Id = user.Id, + Nome = user.Name, + Email = user.Email ?? string.Empty, + Permissao = role, + TenantId = user.TenantId, + Ativo = user.IsActive + }); + } + + [HttpPatch("{id:guid}")] + [Authorize(Roles = "admin")] + public async Task Update(Guid id, [FromBody] UserUpdateRequest req) + { + var user = await _userManager.Users.FirstOrDefaultAsync(u => u.Id == id); + if (user == null) + { + return NotFound(); + } + + if (req.Ativo.HasValue) + { + user.IsActive = req.Ativo.Value; + } + + if (!string.IsNullOrWhiteSpace(req.Permissao)) + { + var roleName = req.Permissao.Trim().ToLowerInvariant(); + if (!await _roleManager.RoleExistsAsync(roleName)) + { + return BadRequest(new ValidationErrorResponse + { + Errors = new List + { + new() { Field = "permissao", Message = "Permissão inválida." } + } + }); + } + + var existingRoles = await _userManager.GetRolesAsync(user); + if (existingRoles.Count > 0) + { + await _userManager.RemoveFromRolesAsync(user, existingRoles); + } + + await _userManager.AddToRoleAsync(user, roleName); + } + + await _userManager.UpdateAsync(user); + return NoContent(); + } + + private static List ValidateCreate(UserCreateRequest req) + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(req.Nome)) + { + errors.Add(new ValidationErrorDto { Field = "nome", Message = "Nome é obrigatório." }); + } + + if (string.IsNullOrWhiteSpace(req.Email)) + { + errors.Add(new ValidationErrorDto { Field = "email", Message = "Email é obrigatório." }); + } + + if (string.IsNullOrWhiteSpace(req.Senha) || req.Senha.Length < 6) + { + errors.Add(new ValidationErrorDto { Field = "senha", Message = "Senha deve ter pelo menos 6 caracteres." }); + } + + if (req.Senha != req.ConfirmarSenha) + { + errors.Add(new ValidationErrorDto { Field = "confirmarSenha", Message = "Confirmação de senha inválida." }); + } + + if (string.IsNullOrWhiteSpace(req.Permissao)) + { + errors.Add(new ValidationErrorDto { Field = "permissao", Message = "Permissão é obrigatória." }); + } + + return errors; + } +} diff --git a/Data/AppDbContext.cs b/Data/AppDbContext.cs index ba6c659..8007767 100644 --- a/Data/AppDbContext.cs +++ b/Data/AppDbContext.cs @@ -1,13 +1,21 @@ -using Microsoft.EntityFrameworkCore; -using line_gestao_api.Models; +using line_gestao_api.Models; +using line_gestao_api.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace line_gestao_api.Data; -public class AppDbContext : DbContext +public class AppDbContext : IdentityDbContext, Guid> { - public AppDbContext(DbContextOptions options) : base(options) { } + private readonly ITenantProvider _tenantProvider; - public DbSet Users => Set(); + public AppDbContext(DbContextOptions options, ITenantProvider tenantProvider) : base(options) + { + _tenantProvider = tenantProvider; + } + + public DbSet Tenants => Set(); // ✅ tabela para espelhar a planilha (GERAL) public DbSet MobileLines => Set(); @@ -35,19 +43,22 @@ public class AppDbContext : DbContext base.OnModelCreating(modelBuilder); // ========================= - // ✅ USER + // ✅ USER (Identity) // ========================= - modelBuilder.Entity() - .HasIndex(u => u.Email) - .IsUnique(); + modelBuilder.Entity(e => + { + e.Property(x => x.Name).HasMaxLength(120); + e.HasIndex(x => new { x.TenantId, x.NormalizedEmail }) + .IsUnique(); + }); // ========================= // ✅ GERAL (MobileLine) // ========================= modelBuilder.Entity(e => { - // Mantém UNIQUE por Linha (se Linha puder ser null no banco, Postgres aceita múltiplos nulls) - e.HasIndex(x => x.Linha).IsUnique(); + // Mantém UNIQUE por Linha por tenant (se Linha puder ser null no banco, Postgres aceita múltiplos nulls) + e.HasIndex(x => new { x.TenantId, x.Linha }).IsUnique(); // performance e.HasIndex(x => x.Chip); @@ -66,6 +77,7 @@ public class AppDbContext : DbContext e.HasIndex(x => x.ICCID); e.HasIndex(x => x.LinhaAntiga); e.HasIndex(x => x.LinhaNova); + e.HasIndex(x => x.TenantId); // FK + index e.HasIndex(x => x.MobileLineId); @@ -93,6 +105,7 @@ public class AppDbContext : DbContext e.HasIndex(x => x.Cliente); e.HasIndex(x => new { x.Tipo, x.Cliente }); e.HasIndex(x => x.Item); + e.HasIndex(x => x.TenantId); }); // ========================= @@ -106,6 +119,7 @@ public class AppDbContext : DbContext e.HasIndex(x => x.Linha); e.HasIndex(x => x.Cpf); e.HasIndex(x => x.Email); + e.HasIndex(x => x.TenantId); }); // ========================= @@ -117,6 +131,7 @@ public class AppDbContext : DbContext e.HasIndex(x => x.Cliente); e.HasIndex(x => x.Linha); e.HasIndex(x => x.DtTerminoFidelizacao); + e.HasIndex(x => x.TenantId); }); // ========================= @@ -129,6 +144,7 @@ public class AppDbContext : DbContext e.HasIndex(x => x.LinhaNova); e.HasIndex(x => x.ICCID); e.HasIndex(x => x.DataTroca); + e.HasIndex(x => x.TenantId); }); // ========================= @@ -142,6 +158,7 @@ public class AppDbContext : DbContext e.HasIndex(x => x.Lida); e.HasIndex(x => x.Data); e.HasIndex(x => x.VigenciaLineId); + e.HasIndex(x => x.TenantId); e.HasOne(x => x.User) .WithMany() @@ -153,5 +170,43 @@ public class AppDbContext : DbContext .HasForeignKey(x => x.VigenciaLineId) .OnDelete(DeleteBehavior.Restrict); }); + + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + } + + public override int SaveChanges() + { + ApplyTenantIds(); + return base.SaveChanges(); + } + + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + ApplyTenantIds(); + return base.SaveChangesAsync(cancellationToken); + } + + private void ApplyTenantIds() + { + if (_tenantProvider.TenantId == null) + { + return; + } + + var tenantId = _tenantProvider.TenantId.Value; + foreach (var entry in ChangeTracker.Entries().Where(e => e.State == EntityState.Added)) + { + if (entry.Entity.TenantId == Guid.Empty) + { + entry.Entity.TenantId = tenantId; + } + } } } diff --git a/Data/SeedData.cs b/Data/SeedData.cs new file mode 100644 index 0000000..fe8108a --- /dev/null +++ b/Data/SeedData.cs @@ -0,0 +1,82 @@ +using line_gestao_api.Models; +using line_gestao_api.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace line_gestao_api.Data; + +public class SeedOptions +{ + public string DefaultTenantName { get; set; } = "Default"; + public string AdminName { get; set; } = "Administrador"; + public string AdminEmail { get; set; } = "admin@linegestao.local"; + public string AdminPassword { get; set; } = "Admin123!"; +} + +public static class SeedData +{ + public static readonly Guid DefaultTenantId = Guid.Parse("11111111-1111-1111-1111-111111111111"); + + public static async Task EnsureSeedDataAsync(IServiceProvider services) + { + using var scope = services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + var roleManager = scope.ServiceProvider.GetRequiredService>>(); + var tenantProvider = scope.ServiceProvider.GetRequiredService(); + var options = scope.ServiceProvider.GetRequiredService>().Value; + + await db.Database.MigrateAsync(); + + var roles = new[] { "admin", "gestor", "operador", "leitura" }; + foreach (var role in roles) + { + if (!await roleManager.RoleExistsAsync(role)) + { + await roleManager.CreateAsync(new IdentityRole(role)); + } + } + + var tenant = await db.Tenants.FirstOrDefaultAsync(t => t.Id == DefaultTenantId); + if (tenant == null) + { + tenant = new Tenant + { + Id = DefaultTenantId, + Name = options.DefaultTenantName, + CreatedAt = DateTime.UtcNow + }; + + db.Tenants.Add(tenant); + await db.SaveChangesAsync(); + } + + tenantProvider.SetTenantId(tenant.Id); + + var normalizedEmail = userManager.NormalizeEmail(options.AdminEmail); + var existingAdmin = await userManager.Users + .FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail && u.TenantId == tenant.Id); + + if (existingAdmin == null) + { + var adminUser = new ApplicationUser + { + UserName = options.AdminEmail, + Email = options.AdminEmail, + Name = options.AdminName, + TenantId = tenant.Id, + EmailConfirmed = true, + IsActive = true + }; + + var createResult = await userManager.CreateAsync(adminUser, options.AdminPassword); + if (createResult.Succeeded) + { + await userManager.AddToRoleAsync(adminUser, "admin"); + } + } + + tenantProvider.SetTenantId(null); + } +} diff --git a/Dtos/UserDtos.cs b/Dtos/UserDtos.cs new file mode 100644 index 0000000..97edb4d --- /dev/null +++ b/Dtos/UserDtos.cs @@ -0,0 +1,37 @@ +namespace line_gestao_api.Dtos; + +public class UserCreateRequest +{ + public string Nome { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Senha { get; set; } = string.Empty; + public string ConfirmarSenha { get; set; } = string.Empty; + public string Permissao { get; set; } = string.Empty; +} + +public class UserUpdateRequest +{ + public string? Permissao { get; set; } + public bool? Ativo { get; set; } +} + +public class UserListItemDto +{ + public Guid Id { get; set; } + public string Nome { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Permissao { get; set; } = string.Empty; + public Guid TenantId { get; set; } + public bool Ativo { get; set; } +} + +public class ValidationErrorDto +{ + public string Field { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; +} + +public class ValidationErrorResponse +{ + public List Errors { get; set; } = new(); +} diff --git a/Migrations/20260210120000_AddIdentityAndTenants.Designer.cs b/Migrations/20260210120000_AddIdentityAndTenants.Designer.cs new file mode 100644 index 0000000..aa581be --- /dev/null +++ b/Migrations/20260210120000_AddIdentityAndTenants.Designer.cs @@ -0,0 +1,837 @@ +// +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("20260210120000_AddIdentityAndTenants")] + partial class AddIdentityAndTenants + { + 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("line_gestao_api.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .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("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + 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"); + + b.HasIndex("NormalizedUserName") + .IsUnique(); + + 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("TenantId", "Linha") + .IsUnique(); + + b.HasIndex("Usuario"); + + 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("Linha") + .HasColumnType("text"); + + b.Property("Lida") + .HasColumnType("boolean"); + + b.Property("LidaEm") + .HasColumnType("timestamp with time zone"); + + 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") + .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("TenantId") + .HasColumnType("uuid"); + + b.Property("TelefoneFixo") + .HasColumnType("text"); + + 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.IdentityRole", 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); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", 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); + }); + + 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("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.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("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"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260210120000_AddIdentityAndTenants.cs b/Migrations/20260210120000_AddIdentityAndTenants.cs new file mode 100644 index 0000000..382545f --- /dev/null +++ b/Migrations/20260210120000_AddIdentityAndTenants.cs @@ -0,0 +1,456 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace line_gestao_api.Migrations +{ + /// + public partial class AddIdentityAndTenants : Migration + { + private static readonly Guid DefaultTenantId = Guid.Parse("11111111-1111-1111-1111-111111111111"); + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Notifications_Users_UserId", + table: "Notifications"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.CreateTable( + name: "Tenants", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tenants", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + TenantId = table.Column(type: "uuid", nullable: false, defaultValue: DefaultTenantId), + IsActive = table.Column(type: "boolean", nullable: false, defaultValue: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RoleId = table.Column(type: "uuid", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "uuid", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "text", nullable: false), + ProviderKey = table.Column(type: "text", nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "uuid", nullable: false), + RoleId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "uuid", nullable: false), + LoginProvider = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.AddColumn( + name: "TenantId", + table: "MobileLines", + type: "uuid", + nullable: false, + defaultValue: DefaultTenantId); + + migrationBuilder.AddColumn( + name: "TenantId", + table: "MuregLines", + type: "uuid", + nullable: false, + defaultValue: DefaultTenantId); + + migrationBuilder.AddColumn( + name: "TenantId", + table: "billing_clients", + type: "uuid", + nullable: false, + defaultValue: DefaultTenantId); + + migrationBuilder.AddColumn( + name: "TenantId", + table: "UserDatas", + type: "uuid", + nullable: false, + defaultValue: DefaultTenantId); + + migrationBuilder.AddColumn( + name: "TenantId", + table: "VigenciaLines", + type: "uuid", + nullable: false, + defaultValue: DefaultTenantId); + + migrationBuilder.AddColumn( + name: "TenantId", + table: "TrocaNumeroLines", + type: "uuid", + nullable: false, + defaultValue: DefaultTenantId); + + migrationBuilder.AddColumn( + name: "TenantId", + table: "Notifications", + type: "uuid", + nullable: false, + defaultValue: DefaultTenantId); + + migrationBuilder.DropIndex( + name: "IX_MobileLines_Linha", + table: "MobileLines"); + + migrationBuilder.CreateIndex( + name: "IX_MobileLines_TenantId_Linha", + table: "MobileLines", + columns: new[] { "TenantId", "Linha" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_MuregLines_TenantId", + table: "MuregLines", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_billing_clients_TenantId", + table: "billing_clients", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_UserDatas_TenantId", + table: "UserDatas", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_VigenciaLines_TenantId", + table: "VigenciaLines", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_TrocaNumeroLines_TenantId", + table: "TrocaNumeroLines", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_TenantId", + table: "Notifications", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoles_NormalizedName", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUsers_NormalizedEmail", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUsers_NormalizedUserName", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUsers_TenantId_NormalizedEmail", + table: "AspNetUsers", + columns: new[] { "TenantId", "NormalizedEmail" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.AddForeignKey( + name: "FK_Notifications_AspNetUsers_UserId", + table: "Notifications", + column: "UserId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.InsertData( + table: "Tenants", + columns: new[] { "Id", "Name", "CreatedAt" }, + values: new object[] { DefaultTenantId, "Default", DateTime.UtcNow }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Notifications_AspNetUsers_UserId", + table: "Notifications"); + + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + + migrationBuilder.DropTable( + name: "Tenants"); + + migrationBuilder.DropIndex( + name: "IX_MobileLines_TenantId_Linha", + table: "MobileLines"); + + migrationBuilder.DropIndex( + name: "IX_MuregLines_TenantId", + table: "MuregLines"); + + migrationBuilder.DropIndex( + name: "IX_billing_clients_TenantId", + table: "billing_clients"); + + migrationBuilder.DropIndex( + name: "IX_UserDatas_TenantId", + table: "UserDatas"); + + migrationBuilder.DropIndex( + name: "IX_VigenciaLines_TenantId", + table: "VigenciaLines"); + + migrationBuilder.DropIndex( + name: "IX_TrocaNumeroLines_TenantId", + table: "TrocaNumeroLines"); + + migrationBuilder.DropIndex( + name: "IX_Notifications_TenantId", + table: "Notifications"); + + migrationBuilder.DropColumn( + name: "TenantId", + table: "MobileLines"); + + migrationBuilder.DropColumn( + name: "TenantId", + table: "MuregLines"); + + migrationBuilder.DropColumn( + name: "TenantId", + table: "billing_clients"); + + migrationBuilder.DropColumn( + name: "TenantId", + table: "UserDatas"); + + migrationBuilder.DropColumn( + name: "TenantId", + table: "VigenciaLines"); + + migrationBuilder.DropColumn( + name: "TenantId", + table: "TrocaNumeroLines"); + + migrationBuilder.DropColumn( + name: "TenantId", + table: "Notifications"); + + migrationBuilder.CreateIndex( + name: "IX_MobileLines_Linha", + table: "MobileLines", + column: "Linha", + unique: true); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + Email = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + Phone = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + PasswordHash = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_Email", + table: "Users", + column: "Email", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Notifications_Users_UserId", + table: "Notifications", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/Migrations/AppDbContextModelSnapshot.cs b/Migrations/AppDbContextModelSnapshot.cs index a328ab9..8ba989b 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,85 @@ namespace line_gestao_api.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("line_gestao_api.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .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("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + 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"); + + b.HasIndex("NormalizedUserName") + .IsUnique(); + + b.HasIndex("TenantId", "NormalizedEmail") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + modelBuilder.Entity("line_gestao_api.Models.BillingClient", b => { b.Property("Id") @@ -57,6 +136,9 @@ namespace line_gestao_api.Migrations b.Property("QtdLinhas") .HasColumnType("integer"); + b.Property("TenantId") + .HasColumnType("uuid"); + b.Property("Tipo") .IsRequired() .HasMaxLength(2) @@ -77,6 +159,8 @@ namespace line_gestao_api.Migrations b.HasIndex("Item"); + b.HasIndex("TenantId"); + b.HasIndex("Tipo"); b.HasIndex("Tipo", "Cliente"); @@ -169,6 +253,9 @@ namespace line_gestao_api.Migrations .HasMaxLength(80) .HasColumnType("character varying(80)"); + b.Property("TenantId") + .HasColumnType("uuid"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -204,13 +291,13 @@ namespace line_gestao_api.Migrations b.HasIndex("Cliente"); - b.HasIndex("Linha") - .IsUnique(); - b.HasIndex("Skil"); b.HasIndex("Status"); + b.HasIndex("TenantId", "Linha") + .IsUnique(); + b.HasIndex("Usuario"); b.ToTable("MobileLines"); @@ -246,6 +333,9 @@ namespace line_gestao_api.Migrations b.Property("MobileLineId") .HasColumnType("uuid"); + b.Property("TenantId") + .HasColumnType("uuid"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -261,93 +351,11 @@ namespace line_gestao_api.Migrations b.HasIndex("MobileLineId"); + b.HasIndex("TenantId"); + b.ToTable("MuregLines"); }); - 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("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.ToTable("TrocaNumeroLines"); - }); - - modelBuilder.Entity("line_gestao_api.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(120) - .HasColumnType("character varying(120)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(120) - .HasColumnType("character varying(120)"); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("Phone") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.HasKey("Id"); - - b.HasIndex("Email") - .IsUnique(); - - b.ToTable("Users"); - }); - modelBuilder.Entity("line_gestao_api.Models.Notification", b => { b.Property("Id") @@ -383,6 +391,9 @@ namespace line_gestao_api.Migrations b.Property("ReferenciaData") .HasColumnType("timestamp with time zone"); + b.Property("TenantId") + .HasColumnType("uuid"); + b.Property("Tipo") .IsRequired() .HasColumnType("text"); @@ -411,6 +422,8 @@ namespace line_gestao_api.Migrations b.HasIndex("Lida"); + b.HasIndex("TenantId"); + b.HasIndex("UserId"); b.HasIndex("VigenciaLineId"); @@ -418,6 +431,76 @@ namespace line_gestao_api.Migrations 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") + .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") @@ -454,6 +537,9 @@ namespace line_gestao_api.Migrations b.Property("Rg") .HasColumnType("text"); + b.Property("TenantId") + .HasColumnType("uuid"); + b.Property("TelefoneFixo") .HasColumnType("text"); @@ -472,6 +558,8 @@ namespace line_gestao_api.Migrations b.HasIndex("Linha"); + b.HasIndex("TenantId"); + b.ToTable("UserDatas"); }); @@ -505,6 +593,9 @@ namespace line_gestao_api.Migrations b.Property("PlanoContrato") .HasColumnType("text"); + b.Property("TenantId") + .HasColumnType("uuid"); + b.Property("Total") .HasColumnType("numeric"); @@ -524,12 +615,151 @@ namespace line_gestao_api.Migrations b.HasIndex("Linha"); + b.HasIndex("TenantId"); + b.ToTable("VigenciaLines"); }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", 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); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", 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); + }); + + 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("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.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.User", "User") + b.HasOne("line_gestao_api.Models.ApplicationUser", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Restrict); @@ -544,15 +774,55 @@ namespace line_gestao_api.Migrations b.Navigation("VigenciaLine"); }); - modelBuilder.Entity("line_gestao_api.Models.MuregLine", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { - b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine") - .WithMany("Muregs") - .HasForeignKey("MobileLineId") - .OnDelete(DeleteBehavior.Restrict) + 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.Navigation("MobileLine"); + 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 => diff --git a/Models/ApplicationUser.cs b/Models/ApplicationUser.cs new file mode 100644 index 0000000..1b5bc58 --- /dev/null +++ b/Models/ApplicationUser.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Identity; + +namespace line_gestao_api.Models; + +public class ApplicationUser : IdentityUser, ITenantEntity +{ + public string Name { get; set; } = string.Empty; + public Guid TenantId { get; set; } + public bool IsActive { get; set; } = true; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/Models/BillingClient.cs b/Models/BillingClient.cs index eef23e8..6d7f492 100644 --- a/Models/BillingClient.cs +++ b/Models/BillingClient.cs @@ -1,8 +1,9 @@ namespace line_gestao_api.Models { - public class BillingClient + public class BillingClient : ITenantEntity { public Guid Id { get; set; } + public Guid TenantId { get; set; } public string Tipo { get; set; } = "PF"; // "PF" ou "PJ" public int Item { get; set; } diff --git a/Models/ITenantEntity.cs b/Models/ITenantEntity.cs new file mode 100644 index 0000000..a756f3b --- /dev/null +++ b/Models/ITenantEntity.cs @@ -0,0 +1,6 @@ +namespace line_gestao_api.Models; + +public interface ITenantEntity +{ + Guid TenantId { get; set; } +} diff --git a/Models/MobileLine.cs b/Models/MobileLine.cs index f4e5376..35bf9c5 100644 --- a/Models/MobileLine.cs +++ b/Models/MobileLine.cs @@ -2,10 +2,12 @@ namespace line_gestao_api.Models { - public class MobileLine + public class MobileLine : ITenantEntity { public Guid Id { get; set; } = Guid.NewGuid(); + public Guid TenantId { get; set; } + public int Item { get; set; } [MaxLength(80)] public string? Conta { get; set; } diff --git a/Models/MuregLine.cs b/Models/MuregLine.cs index f076d88..1a54aa0 100644 --- a/Models/MuregLine.cs +++ b/Models/MuregLine.cs @@ -2,10 +2,12 @@ namespace line_gestao_api.Models { - public class MuregLine + public class MuregLine : ITenantEntity { public Guid Id { get; set; } = Guid.NewGuid(); + public Guid TenantId { get; set; } + public int Item { get; set; } // Linha escolhida da GERAL no momento do mureg diff --git a/Models/Notification.cs b/Models/Notification.cs index 8cef548..3947dd0 100644 --- a/Models/Notification.cs +++ b/Models/Notification.cs @@ -2,10 +2,12 @@ namespace line_gestao_api.Models; -public class Notification +public class Notification : ITenantEntity { public Guid Id { get; set; } = Guid.NewGuid(); + public Guid TenantId { get; set; } + [Required] public string Tipo { get; set; } = string.Empty; @@ -29,7 +31,7 @@ public class Notification public string DedupKey { get; set; } = string.Empty; public Guid? UserId { get; set; } - public User? User { get; set; } + public ApplicationUser? User { get; set; } public Guid? VigenciaLineId { get; set; } public VigenciaLine? VigenciaLine { get; set; } diff --git a/Models/Tenant.cs b/Models/Tenant.cs new file mode 100644 index 0000000..7b70ab7 --- /dev/null +++ b/Models/Tenant.cs @@ -0,0 +1,8 @@ +namespace line_gestao_api.Models; + +public class Tenant +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string Name { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/Models/TrocaNumeroLine.cs b/Models/TrocaNumeroLine.cs index 737a90d..8abb071 100644 --- a/Models/TrocaNumeroLine.cs +++ b/Models/TrocaNumeroLine.cs @@ -2,10 +2,12 @@ namespace line_gestao_api.Models { - public class TrocaNumeroLine + public class TrocaNumeroLine : ITenantEntity { public Guid Id { get; set; } + public Guid TenantId { get; set; } + public int Item { get; set; } public string? LinhaAntiga { get; set; } diff --git a/Models/User.cs b/Models/User.cs deleted file mode 100644 index f829c9e..0000000 --- a/Models/User.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace line_gestao_api.Models; - -public class User -{ - public Guid Id { get; set; } = Guid.NewGuid(); - - [Required, MaxLength(120)] - public string Name { get; set; } = string.Empty; - - [Required, MaxLength(120)] - public string Email { get; set; } = string.Empty; - - [MaxLength(20)] - public string Phone { get; set; } = string.Empty; - - - [Required] - public string PasswordHash { get; set; } = string.Empty; - - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; -} diff --git a/Models/UserData.cs b/Models/UserData.cs index 8536fba..3308fcd 100644 --- a/Models/UserData.cs +++ b/Models/UserData.cs @@ -3,11 +3,13 @@ using System.ComponentModel.DataAnnotations.Schema; namespace line_gestao_api.Models { - public class UserData + public class UserData : ITenantEntity { [Key] public Guid Id { get; set; } + public Guid TenantId { get; set; } + public int Item { get; set; } public string? Linha { get; set; } diff --git a/Models/VigenciaLine.cs b/Models/VigenciaLine.cs index 3b63c0c..f8c37c9 100644 --- a/Models/VigenciaLine.cs +++ b/Models/VigenciaLine.cs @@ -2,10 +2,12 @@ namespace line_gestao_api.Models { - public class VigenciaLine + public class VigenciaLine : ITenantEntity { public Guid Id { get; set; } + public Guid TenantId { get; set; } + public int Item { get; set; } public string? Conta { get; set; } diff --git a/Program.cs b/Program.cs index 80288eb..00e7e7d 100644 --- a/Program.cs +++ b/Program.cs @@ -1,8 +1,10 @@ using System.Text; using line_gestao_api.Data; +using line_gestao_api.Models; using line_gestao_api.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; @@ -31,6 +33,18 @@ builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default")) ); +builder.Services.AddHttpContextAccessor(); +builder.Services.AddScoped(); + +builder.Services.AddIdentityCore(options => + { + options.Password.RequiredLength = 6; + options.User.RequireUniqueEmail = false; + }) + .AddRoles>() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + // ✅ Swagger builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -61,6 +75,8 @@ builder.Services.AddAuthorization(); builder.Services.Configure(builder.Configuration.GetSection("Notifications")); builder.Services.AddHostedService(); +builder.Services.Configure(builder.Configuration.GetSection("Seed")); + var app = builder.Build(); if (app.Environment.IsDevelopment()) @@ -75,8 +91,11 @@ app.UseHttpsRedirection(); app.UseCors("Front"); app.UseAuthentication(); +app.UseMiddleware(); app.UseAuthorization(); app.MapControllers(); +await SeedData.EnsureSeedDataAsync(app.Services); + app.Run(); diff --git a/Services/ITenantProvider.cs b/Services/ITenantProvider.cs new file mode 100644 index 0000000..f32efc7 --- /dev/null +++ b/Services/ITenantProvider.cs @@ -0,0 +1,7 @@ +namespace line_gestao_api.Services; + +public interface ITenantProvider +{ + Guid? TenantId { get; } + void SetTenantId(Guid? tenantId); +} diff --git a/Services/TenantMiddleware.cs b/Services/TenantMiddleware.cs new file mode 100644 index 0000000..1806992 --- /dev/null +++ b/Services/TenantMiddleware.cs @@ -0,0 +1,36 @@ +using System.Security.Claims; + +namespace line_gestao_api.Services; + +public class TenantMiddleware +{ + private readonly RequestDelegate _next; + + public TenantMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context, ITenantProvider tenantProvider) + { + Guid? tenantId = null; + var claim = context.User.FindFirst("tenantId")?.Value + ?? context.User.FindFirst("tenant")?.Value; + + if (Guid.TryParse(claim, out var parsed)) + { + tenantId = parsed; + } + + tenantProvider.SetTenantId(tenantId); + + try + { + await _next(context); + } + finally + { + tenantProvider.SetTenantId(null); + } + } +} diff --git a/Services/TenantProvider.cs b/Services/TenantProvider.cs new file mode 100644 index 0000000..1cf34e5 --- /dev/null +++ b/Services/TenantProvider.cs @@ -0,0 +1,30 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace line_gestao_api.Services; + +public class TenantProvider : ITenantProvider +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private static readonly AsyncLocal CurrentTenant = new(); + + public TenantProvider(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public Guid? TenantId => CurrentTenant.Value ?? ResolveFromClaims(); + + public void SetTenantId(Guid? tenantId) + { + CurrentTenant.Value = tenantId; + } + + private Guid? ResolveFromClaims() + { + var claim = _httpContextAccessor.HttpContext?.User?.FindFirst("tenantId")?.Value + ?? _httpContextAccessor.HttpContext?.User?.FindFirst("tenant")?.Value; + + return Guid.TryParse(claim, out var tenantId) ? tenantId : null; + } +} diff --git a/Services/VigenciaNotificationBackgroundService.cs b/Services/VigenciaNotificationBackgroundService.cs index f1c46b5..9b22f1d 100644 --- a/Services/VigenciaNotificationBackgroundService.cs +++ b/Services/VigenciaNotificationBackgroundService.cs @@ -41,6 +41,7 @@ public class VigenciaNotificationBackgroundService : BackgroundService { using var scope = _scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); + var tenantProvider = scope.ServiceProvider.GetRequiredService(); if (!await TableExistsAsync(db, "Notifications", stoppingToken)) { @@ -48,116 +49,20 @@ public class VigenciaNotificationBackgroundService : BackgroundService return; } - var today = DateTime.UtcNow.Date; - var reminderDays = _options.ReminderDays - .Distinct() - .Where(d => d > 0) - .OrderBy(d => d) - .ToList(); - - var users = await db.Users.AsNoTracking() - .Select(u => new { u.Id, u.Name, u.Email }) - .ToListAsync(stoppingToken); - - var userByName = users - .Where(u => !string.IsNullOrWhiteSpace(u.Name)) - .ToDictionary(u => u.Name.Trim().ToLowerInvariant(), u => u.Id); - - var userByEmail = users - .Where(u => !string.IsNullOrWhiteSpace(u.Email)) - .ToDictionary(u => u.Email.Trim().ToLowerInvariant(), u => u.Id); - - var vigencias = await db.VigenciaLines.AsNoTracking() - .Where(v => v.DtTerminoFidelizacao != null) - .ToListAsync(stoppingToken); - - var candidates = new List(); - foreach (var vigencia in vigencias) - { - if (vigencia.DtTerminoFidelizacao is null) - { - continue; - } - - var endDate = vigencia.DtTerminoFidelizacao.Value.Date; - var usuario = vigencia.Usuario?.Trim(); - var cliente = vigencia.Cliente?.Trim(); - var linha = vigencia.Linha?.Trim(); - var usuarioKey = usuario?.ToLowerInvariant(); - - Guid? userId = null; - if (!string.IsNullOrWhiteSpace(usuarioKey)) - { - if (userByEmail.TryGetValue(usuarioKey, out var matchedByEmail)) - { - userId = matchedByEmail; - } - else if (userByName.TryGetValue(usuarioKey, out var matchedByName)) - { - userId = matchedByName; - } - } - - if (endDate < today) - { - var notification = BuildNotification( - tipo: "Vencido", - titulo: $"Linha vencida{FormatLinha(linha)}", - mensagem: $"A linha{FormatLinha(linha)} do cliente {cliente ?? "(sem cliente)"} venceu em {endDate:dd/MM/yyyy}.", - referenciaData: endDate, - diasParaVencer: 0, - userId: userId, - usuario: usuario, - cliente: cliente, - linha: linha, - vigenciaLineId: vigencia.Id); - - candidates.Add(notification); - continue; - } - - var daysUntil = (endDate - today).Days; - if (reminderDays.Contains(daysUntil)) - { - var notification = BuildNotification( - tipo: "AVencer", - titulo: $"Linha a vencer em {daysUntil} dias{FormatLinha(linha)}", - mensagem: $"A linha{FormatLinha(linha)} do cliente {cliente ?? "(sem cliente)"} vence em {endDate:dd/MM/yyyy}.", - referenciaData: endDate, - diasParaVencer: daysUntil, - userId: userId, - usuario: usuario, - cliente: cliente, - linha: linha, - vigenciaLineId: vigencia.Id); - - candidates.Add(notification); - } - } - - if (candidates.Count == 0) + var tenants = await db.Tenants.AsNoTracking().ToListAsync(stoppingToken); + if (tenants.Count == 0) { + _logger.LogWarning("Nenhum tenant encontrado para gerar notificações."); return; } - var dedupKeys = candidates.Select(c => c.DedupKey).Distinct().ToList(); - var existingKeys = await db.Notifications.AsNoTracking() - .Where(n => dedupKeys.Contains(n.DedupKey)) - .Select(n => n.DedupKey) - .ToListAsync(stoppingToken); - - var existingSet = new HashSet(existingKeys); - var toInsert = candidates - .Where(c => !existingSet.Contains(c.DedupKey)) - .ToList(); - - if (toInsert.Count == 0) + foreach (var tenant in tenants) { - return; + tenantProvider.SetTenantId(tenant.Id); + await ProcessTenantAsync(db, tenant.Id, stoppingToken); } - await db.Notifications.AddRangeAsync(toInsert, stoppingToken); - await db.SaveChangesAsync(stoppingToken); + tenantProvider.SetTenantId(null); } catch (Exception ex) { @@ -189,6 +94,122 @@ public class VigenciaNotificationBackgroundService : BackgroundService return result is bool exists && exists; } + private async Task ProcessTenantAsync(AppDbContext db, Guid tenantId, CancellationToken stoppingToken) + { + var today = DateTime.UtcNow.Date; + var reminderDays = _options.ReminderDays + .Distinct() + .Where(d => d > 0) + .OrderBy(d => d) + .ToList(); + + var users = await db.Users.AsNoTracking() + .Select(u => new { u.Id, u.Name, u.Email }) + .ToListAsync(stoppingToken); + + var userByName = users + .Where(u => !string.IsNullOrWhiteSpace(u.Name)) + .ToDictionary(u => u.Name.Trim().ToLowerInvariant(), u => u.Id); + + var userByEmail = users + .Where(u => !string.IsNullOrWhiteSpace(u.Email)) + .ToDictionary(u => u.Email.Trim().ToLowerInvariant(), u => u.Id); + + var vigencias = await db.VigenciaLines.AsNoTracking() + .Where(v => v.DtTerminoFidelizacao != null) + .ToListAsync(stoppingToken); + + var candidates = new List(); + foreach (var vigencia in vigencias) + { + if (vigencia.DtTerminoFidelizacao is null) + { + continue; + } + + var endDate = vigencia.DtTerminoFidelizacao.Value.Date; + var usuario = vigencia.Usuario?.Trim(); + var cliente = vigencia.Cliente?.Trim(); + var linha = vigencia.Linha?.Trim(); + var usuarioKey = usuario?.ToLowerInvariant(); + + Guid? userId = null; + if (!string.IsNullOrWhiteSpace(usuarioKey)) + { + if (userByEmail.TryGetValue(usuarioKey, out var matchedByEmail)) + { + userId = matchedByEmail; + } + else if (userByName.TryGetValue(usuarioKey, out var matchedByName)) + { + userId = matchedByName; + } + } + + if (endDate < today) + { + var notification = BuildNotification( + tipo: "Vencido", + titulo: $"Linha vencida{FormatLinha(linha)}", + mensagem: $"A linha{FormatLinha(linha)} do cliente {cliente ?? "(sem cliente)"} venceu em {endDate:dd/MM/yyyy}.", + referenciaData: endDate, + diasParaVencer: 0, + userId: userId, + usuario: usuario, + cliente: cliente, + linha: linha, + vigenciaLineId: vigencia.Id, + tenantId: tenantId); + + candidates.Add(notification); + continue; + } + + var daysUntil = (endDate - today).Days; + if (reminderDays.Contains(daysUntil)) + { + var notification = BuildNotification( + tipo: "AVencer", + titulo: $"Linha a vencer em {daysUntil} dias{FormatLinha(linha)}", + mensagem: $"A linha{FormatLinha(linha)} do cliente {cliente ?? "(sem cliente)"} vence em {endDate:dd/MM/yyyy}.", + referenciaData: endDate, + diasParaVencer: daysUntil, + userId: userId, + usuario: usuario, + cliente: cliente, + linha: linha, + vigenciaLineId: vigencia.Id, + tenantId: tenantId); + + candidates.Add(notification); + } + } + + if (candidates.Count == 0) + { + return; + } + + var dedupKeys = candidates.Select(c => c.DedupKey).Distinct().ToList(); + var existingKeys = await db.Notifications.AsNoTracking() + .Where(n => dedupKeys.Contains(n.DedupKey)) + .Select(n => n.DedupKey) + .ToListAsync(stoppingToken); + + var existingSet = new HashSet(existingKeys); + var toInsert = candidates + .Where(c => !existingSet.Contains(c.DedupKey)) + .ToList(); + + if (toInsert.Count == 0) + { + return; + } + + await db.Notifications.AddRangeAsync(toInsert, stoppingToken); + await db.SaveChangesAsync(stoppingToken); + } + private static Notification BuildNotification( string tipo, string titulo, @@ -199,7 +220,8 @@ public class VigenciaNotificationBackgroundService : BackgroundService string? usuario, string? cliente, string? linha, - Guid vigenciaLineId) + Guid vigenciaLineId, + Guid tenantId) { return new Notification { @@ -215,7 +237,8 @@ public class VigenciaNotificationBackgroundService : BackgroundService Usuario = usuario, Cliente = cliente, Linha = linha, - VigenciaLineId = vigenciaLineId + VigenciaLineId = vigenciaLineId, + TenantId = tenantId }; } diff --git a/appsettings.Development.json b/appsettings.Development.json index 233e874..2ed524a 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -8,5 +8,11 @@ "Notifications": { "CheckIntervalMinutes": 60, "ReminderDays": [30, 15, 7] + }, + "Seed": { + "DefaultTenantName": "Default", + "AdminName": "Administrador", + "AdminEmail": "admin@linegestao.local", + "AdminPassword": "Admin123!" } } diff --git a/appsettings.json b/appsettings.json index 429939a..93d1a76 100644 --- a/appsettings.json +++ b/appsettings.json @@ -11,5 +11,11 @@ "Notifications": { "CheckIntervalMinutes": 60, "ReminderDays": [30, 15, 7] + }, + "Seed": { + "DefaultTenantName": "Default", + "AdminName": "Administrador", + "AdminEmail": "admin@linegestao.local", + "AdminPassword": "Admin123!" } } diff --git a/line-gestao-api.csproj b/line-gestao-api.csproj index e1c6df6..00b820a 100644 --- a/line-gestao-api.csproj +++ b/line-gestao-api.csproj @@ -25,6 +25,7 @@ +