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/MuregController.cs b/Controllers/MuregController.cs index b0b5492..937464f 100644 --- a/Controllers/MuregController.cs +++ b/Controllers/MuregController.cs @@ -340,6 +340,22 @@ namespace line_gestao_api.Controllers return NoContent(); } + // ========================================================== + // ✅ DELETE: /api/mureg/{id} + // Exclui registro MUREG + // ========================================================== + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var entity = await _db.MuregLines.FirstOrDefaultAsync(x => x.Id == id); + if (entity == null) return NotFound(); + + _db.MuregLines.Remove(entity); + await _db.SaveChangesAsync(); + + return NoContent(); + } + // ========================================================== // ✅ POST: /api/mureg/import-excel (mantido) // ========================================================== diff --git a/Controllers/NotificationsController.cs b/Controllers/NotificationsController.cs new file mode 100644 index 0000000..d842ae2 --- /dev/null +++ b/Controllers/NotificationsController.cs @@ -0,0 +1,71 @@ +using line_gestao_api.Data; +using line_gestao_api.Dtos; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace line_gestao_api.Controllers; + +[ApiController] +[Route("api/notifications")] +[Authorize] +public class NotificationsController : ControllerBase +{ + private readonly AppDbContext _db; + + public NotificationsController(AppDbContext db) + { + _db = db; + } + + [HttpGet] + [HttpGet("/notifications")] + public async Task>> GetNotifications() + { + var query = _db.Notifications.AsNoTracking(); + + var items = await query + .OrderByDescending(n => n.Data) + .Select(n => new NotificationDto + { + Id = n.Id, + Tipo = n.Tipo, + Titulo = n.Titulo, + Mensagem = n.Mensagem, + Data = n.Data, + ReferenciaData = n.ReferenciaData, + DiasParaVencer = n.DiasParaVencer, + Lida = n.Lida, + LidaEm = n.LidaEm, + VigenciaLineId = n.VigenciaLineId, + Cliente = n.Cliente, + Linha = n.Linha + }) + .ToListAsync(); + + return Ok(items); + } + + [HttpPatch("{id:guid}/read")] + [HttpPatch("/notifications/{id:guid}/read")] + public async Task MarkAsRead(Guid id) + { + var notification = await _db.Notifications + .FirstOrDefaultAsync(n => n.Id == id); + + if (notification is null) + { + return NotFound(); + } + + if (!notification.Lida) + { + notification.Lida = true; + notification.LidaEm = DateTime.UtcNow; + await _db.SaveChangesAsync(); + } + + return NoContent(); + } + +} 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 4591868..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(); @@ -27,24 +35,30 @@ public class AppDbContext : DbContext // ✅ tabela TROCA DE NÚMERO public DbSet TrocaNumeroLines => Set(); + // ✅ tabela NOTIFICAÇÕES + public DbSet Notifications => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { 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); @@ -63,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); @@ -90,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); }); // ========================= @@ -103,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); }); // ========================= @@ -114,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); }); // ========================= @@ -126,6 +144,69 @@ public class AppDbContext : DbContext e.HasIndex(x => x.LinhaNova); e.HasIndex(x => x.ICCID); e.HasIndex(x => x.DataTroca); + e.HasIndex(x => x.TenantId); }); + + // ========================= + // ✅ NOTIFICAÇÕES + // ========================= + modelBuilder.Entity(e => + { + e.HasIndex(x => x.DedupKey).IsUnique(); + e.HasIndex(x => x.UserId); + e.HasIndex(x => x.Cliente); + 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() + .HasForeignKey(x => x.UserId) + .OnDelete(DeleteBehavior.Restrict); + + e.HasOne(x => x.VigenciaLine) + .WithMany() + .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/NotificationDto.cs b/Dtos/NotificationDto.cs new file mode 100644 index 0000000..0d7c7e7 --- /dev/null +++ b/Dtos/NotificationDto.cs @@ -0,0 +1,17 @@ +namespace line_gestao_api.Dtos; + +public class NotificationDto +{ + public Guid Id { get; set; } + public string Tipo { get; set; } = string.Empty; + public string Titulo { get; set; } = string.Empty; + public string Mensagem { get; set; } = string.Empty; + public DateTime Data { get; set; } + public DateTime? ReferenciaData { get; set; } + public int? DiasParaVencer { get; set; } + public bool Lida { get; set; } + public DateTime? LidaEm { get; set; } + public Guid? VigenciaLineId { get; set; } + public string? Cliente { get; set; } + public string? Linha { get; set; } +} 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/20260205120000_AddNotifications.Designer.cs b/Migrations/20260205120000_AddNotifications.Designer.cs new file mode 100644 index 0000000..e163e89 --- /dev/null +++ b/Migrations/20260205120000_AddNotifications.Designer.cs @@ -0,0 +1,567 @@ +// +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("20260205120000_AddNotifications")] + partial class AddNotifications + { + 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.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("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("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("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("Linha") + .IsUnique(); + + b.HasIndex("Skil"); + + b.HasIndex("Status"); + + 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("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.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") + .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("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("UserId"); + + b.HasIndex("VigenciaLineId"); + + b.ToTable("Notifications"); + }); + + 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("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.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("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.ToTable("VigenciaLines"); + }); + + modelBuilder.Entity("line_gestao_api.Models.Notification", b => + { + b.HasOne("line_gestao_api.Models.User", "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.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.MobileLine", b => + { + b.Navigation("Muregs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260205120000_AddNotifications.cs b/Migrations/20260205120000_AddNotifications.cs new file mode 100644 index 0000000..2f73225 --- /dev/null +++ b/Migrations/20260205120000_AddNotifications.cs @@ -0,0 +1,90 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace line_gestao_api.Migrations +{ + /// + public partial class AddNotifications : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Notifications", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Tipo = table.Column(type: "text", nullable: false), + Titulo = table.Column(type: "text", nullable: false), + Mensagem = table.Column(type: "text", nullable: false), + Data = table.Column(type: "timestamp with time zone", nullable: false), + ReferenciaData = table.Column(type: "timestamp with time zone", nullable: true), + DiasParaVencer = table.Column(type: "integer", nullable: true), + Lida = table.Column(type: "boolean", nullable: false), + LidaEm = table.Column(type: "timestamp with time zone", nullable: true), + DedupKey = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: true), + VigenciaLineId = table.Column(type: "uuid", nullable: true), + Usuario = table.Column(type: "text", nullable: true), + Cliente = table.Column(type: "text", nullable: true), + Linha = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Notifications", x => x.Id); + table.ForeignKey( + name: "FK_Notifications_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Notifications_VigenciaLines_VigenciaLineId", + column: x => x.VigenciaLineId, + principalTable: "VigenciaLines", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_Cliente", + table: "Notifications", + column: "Cliente"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_Data", + table: "Notifications", + column: "Data"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_DedupKey", + table: "Notifications", + column: "DedupKey", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_Lida", + table: "Notifications", + column: "Lida"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_UserId", + table: "Notifications", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_VigenciaLineId", + table: "Notifications", + column: "VigenciaLineId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Notifications"); + } + } +} 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/20260210120001_RenameUserNameIndexSafe.cs b/Migrations/20260210120001_RenameUserNameIndexSafe.cs new file mode 100644 index 0000000..f3eeb3a --- /dev/null +++ b/Migrations/20260210120001_RenameUserNameIndexSafe.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace line_gestao_api.Migrations +{ + /// + public partial class RenameUserNameIndexSafe : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("ALTER INDEX IF EXISTS \"IX_AspNetUsers_NormalizedUserName\" RENAME TO \"UserNameIndex\""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("ALTER INDEX IF EXISTS \"UserNameIndex\" RENAME TO \"IX_AspNetUsers_NormalizedUserName\""); + } + } +} diff --git a/Migrations/AppDbContextModelSnapshot.cs b/Migrations/AppDbContextModelSnapshot.cs index 545aaff..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,13 +351,107 @@ namespace line_gestao_api.Migrations 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") - .ValueGeneratedOnAdd() .HasColumnType("uuid"); b.Property("CreatedAt") @@ -294,6 +478,9 @@ namespace line_gestao_api.Migrations b.Property("Observacao") .HasColumnType("text"); + b.Property("TenantId") + .HasColumnType("uuid"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -309,45 +496,11 @@ namespace line_gestao_api.Migrations b.HasIndex("LinhaNova"); + b.HasIndex("TenantId"); + 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.UserData", b => { b.Property("Id") @@ -384,6 +537,9 @@ namespace line_gestao_api.Migrations b.Property("Rg") .HasColumnType("text"); + b.Property("TenantId") + .HasColumnType("uuid"); + b.Property("TelefoneFixo") .HasColumnType("text"); @@ -402,6 +558,8 @@ namespace line_gestao_api.Migrations b.HasIndex("Linha"); + b.HasIndex("TenantId"); + b.ToTable("UserDatas"); }); @@ -435,6 +593,9 @@ namespace line_gestao_api.Migrations b.Property("PlanoContrato") .HasColumnType("text"); + b.Property("TenantId") + .HasColumnType("uuid"); + b.Property("Total") .HasColumnType("numeric"); @@ -454,9 +615,137 @@ 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") @@ -468,6 +757,74 @@ namespace line_gestao_api.Migrations 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"); 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 new file mode 100644 index 0000000..3947dd0 --- /dev/null +++ b/Models/Notification.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; + +namespace line_gestao_api.Models; + +public class Notification : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + [Required] + public string Tipo { get; set; } = string.Empty; + + [Required] + public string Titulo { get; set; } = string.Empty; + + [Required] + public string Mensagem { get; set; } = string.Empty; + + public DateTime Data { get; set; } = DateTime.UtcNow; + + public DateTime? ReferenciaData { get; set; } + + public int? DiasParaVencer { get; set; } + + public bool Lida { get; set; } + + public DateTime? LidaEm { get; set; } + + [Required] + public string DedupKey { get; set; } = string.Empty; + + public Guid? UserId { get; set; } + public ApplicationUser? User { get; set; } + + public Guid? VigenciaLineId { get; set; } + public VigenciaLine? VigenciaLine { get; set; } + + public string? Usuario { get; set; } + public string? Cliente { get; set; } + public string? Linha { 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 1de23df..00e7e7d 100644 --- a/Program.cs +++ b/Program.cs @@ -1,7 +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; @@ -30,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(); @@ -57,6 +72,11 @@ builder.Services 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()) @@ -71,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/NotificationOptions.cs b/Services/NotificationOptions.cs new file mode 100644 index 0000000..6627a5f --- /dev/null +++ b/Services/NotificationOptions.cs @@ -0,0 +1,7 @@ +namespace line_gestao_api.Services; + +public class NotificationOptions +{ + public int CheckIntervalMinutes { get; set; } = 60; + public List ReminderDays { get; set; } = new() { 30, 15, 7 }; +} 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 new file mode 100644 index 0000000..9b22f1d --- /dev/null +++ b/Services/VigenciaNotificationBackgroundService.cs @@ -0,0 +1,272 @@ +using System.Globalization; +using line_gestao_api.Data; +using line_gestao_api.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace line_gestao_api.Services; + +public class VigenciaNotificationBackgroundService : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + private readonly NotificationOptions _options; + + public VigenciaNotificationBackgroundService( + IServiceScopeFactory scopeFactory, + IOptions options, + ILogger logger) + { + _scopeFactory = scopeFactory; + _logger = logger; + _options = options.Value; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var intervalMinutes = _options.CheckIntervalMinutes <= 0 ? 60 : _options.CheckIntervalMinutes; + using var timer = new PeriodicTimer(TimeSpan.FromMinutes(intervalMinutes)); + + await RunOnceAsync(stoppingToken); + + while (await timer.WaitForNextTickAsync(stoppingToken)) + { + await RunOnceAsync(stoppingToken); + } + } + + private async Task RunOnceAsync(CancellationToken stoppingToken) + { + try + { + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var tenantProvider = scope.ServiceProvider.GetRequiredService(); + + if (!await TableExistsAsync(db, "Notifications", stoppingToken)) + { + _logger.LogWarning("Tabela Notifications ainda não existe. Aguardando migrations."); + return; + } + + var tenants = await db.Tenants.AsNoTracking().ToListAsync(stoppingToken); + if (tenants.Count == 0) + { + _logger.LogWarning("Nenhum tenant encontrado para gerar notificações."); + return; + } + + foreach (var tenant in tenants) + { + tenantProvider.SetTenantId(tenant.Id); + await ProcessTenantAsync(db, tenant.Id, stoppingToken); + } + + tenantProvider.SetTenantId(null); + } + catch (Exception ex) + { + _logger.LogError(ex, "Erro ao gerar notificações de vigência."); + } + } + + private static async Task TableExistsAsync(AppDbContext db, string tableName, CancellationToken stoppingToken) + { + if (!db.Database.IsRelational()) + { + return true; + } + + var connection = db.Database.GetDbConnection(); + if (connection.State != System.Data.ConnectionState.Open) + { + await connection.OpenAsync(stoppingToken); + } + + await using var command = connection.CreateCommand(); + command.CommandText = "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = @tableName)"; + var parameter = command.CreateParameter(); + parameter.ParameterName = "tableName"; + parameter.Value = tableName; + command.Parameters.Add(parameter); + + var result = await command.ExecuteScalarAsync(stoppingToken); + 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, + string mensagem, + DateTime referenciaData, + int diasParaVencer, + Guid? userId, + string? usuario, + string? cliente, + string? linha, + Guid vigenciaLineId, + Guid tenantId) + { + return new Notification + { + Tipo = tipo, + Titulo = titulo, + Mensagem = mensagem, + Data = DateTime.UtcNow, + ReferenciaData = referenciaData, + DiasParaVencer = diasParaVencer, + Lida = false, + DedupKey = BuildDedupKey(tipo, vigenciaLineId, referenciaData, diasParaVencer, usuario, cliente, linha), + UserId = userId, + Usuario = usuario, + Cliente = cliente, + Linha = linha, + VigenciaLineId = vigenciaLineId, + TenantId = tenantId + }; + } + + private static string BuildDedupKey( + string tipo, + Guid vigenciaLineId, + DateTime referenciaData, + int diasParaVencer, + string? usuario, + string? cliente, + string? linha) + { + var parts = new[] + { + tipo.Trim().ToLowerInvariant(), + vigenciaLineId.ToString(), + referenciaData.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), + diasParaVencer.ToString(CultureInfo.InvariantCulture), + (usuario ?? string.Empty).Trim().ToLowerInvariant(), + (cliente ?? string.Empty).Trim().ToLowerInvariant(), + (linha ?? string.Empty).Trim().ToLowerInvariant() + }; + + return string.Join('|', parts); + } + + private static string FormatLinha(string? linha) + { + return string.IsNullOrWhiteSpace(linha) ? string.Empty : $" {linha}"; + } +} diff --git a/appsettings.Development.json b/appsettings.Development.json index 0c208ae..2ed524a 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -4,5 +4,15 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "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 823e51b..93d1a76 100644 --- a/appsettings.json +++ b/appsettings.json @@ -7,5 +7,15 @@ "Issuer": "LineGestao", "Audience": "LineGestao", "ExpiresMinutes": 120 + }, + "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 @@ +