Compare commits

..

3 Commits

Author SHA1 Message Date
Eduardo Lopes a5c16cf881
Merge 53665eae05 into 94908ead00 2026-01-23 12:30:19 -03:00
Eduardo Lopes 53665eae05 Add safe index rename migration 2026-01-23 12:20:11 -03:00
Eduardo Lopes 1184f97a88 Add Identity and multi-tenant support 2026-01-23 12:08:55 -03:00
28 changed files with 2477 additions and 270 deletions

View File

@ -1,9 +1,10 @@
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
using line_gestao_api.Data;
using line_gestao_api.Dtos; using line_gestao_api.Dtos;
using line_gestao_api.Models; using line_gestao_api.Models;
using line_gestao_api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -15,80 +16,101 @@ namespace line_gestao_api.Controllers;
[Route("auth")] [Route("auth")]
public class AuthController : ControllerBase public class AuthController : ControllerBase
{ {
private readonly AppDbContext _db; private readonly UserManager<ApplicationUser> _userManager;
private readonly ITenantProvider _tenantProvider;
private readonly IConfiguration _config; private readonly IConfiguration _config;
public AuthController(AppDbContext db, IConfiguration config) public AuthController(UserManager<ApplicationUser> userManager, ITenantProvider tenantProvider, IConfiguration config)
{ {
_db = db; _userManager = userManager;
_tenantProvider = tenantProvider;
_config = config; _config = config;
} }
[HttpPost("register")] [HttpPost("register")]
[Authorize(Roles = "admin")]
public async Task<IActionResult> Register(RegisterRequest req) public async Task<IActionResult> Register(RegisterRequest req)
{ {
// ✅ NOVO: valida confirmação de senha (não vai pro banco, só valida)
if (req.Password != req.ConfirmPassword) if (req.Password != req.ConfirmPassword)
return BadRequest("As senhas não conferem."); 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."); if (exists) return BadRequest("E-mail já cadastrado.");
// ✅ NOVO: normaliza telefone (salva somente dígitos) var user = new ApplicationUser
var phoneDigits = new string((req.Phone ?? string.Empty).Where(char.IsDigit).ToArray());
var user = new User
{ {
Name = req.Name.Trim(), Name = req.Name.Trim(),
Email = email, Email = email,
Phone = phoneDigits // ✅ NOVO UserName = email,
TenantId = tenantId.Value,
IsActive = true,
EmailConfirmed = true
}; };
var hasher = new PasswordHasher<User>(); var createResult = await _userManager.CreateAsync(user, req.Password);
user.PasswordHash = hasher.HashPassword(user, req.Password); if (!createResult.Succeeded)
{
return BadRequest(createResult.Errors.Select(e => e.Description).ToList());
}
_db.Users.Add(user); await _userManager.AddToRoleAsync(user, "leitura");
await _db.SaveChangesAsync();
var token = GenerateJwt(user); var token = await GenerateJwtAsync(user);
return Ok(new AuthResponse(token)); return Ok(new AuthResponse(token));
} }
[HttpPost("login")] [HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest req) public async Task<IActionResult> 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); var users = await _userManager.Users
if (user is null) return Unauthorized("Credenciais inválidas."); .Where(u => u.NormalizedEmail == normalizedEmail)
.ToListAsync();
var hasher = new PasswordHasher<User>(); if (users.Count != 1)
var result = hasher.VerifyHashedPassword(user, user.PasswordHash, req.Password);
if (result == PasswordVerificationResult.Failed)
return Unauthorized("Credenciais inválidas."); 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)); return Ok(new AuthResponse(token));
} }
private string GenerateJwt(User user) private async Task<string> GenerateJwtAsync(ApplicationUser user)
{ {
var key = _config["Jwt:Key"]!; var key = _config["Jwt:Key"]!;
var issuer = _config["Jwt:Issuer"]!; var issuer = _config["Jwt:Issuer"]!;
var audience = _config["Jwt:Audience"]!; var audience = _config["Jwt:Audience"]!;
var expiresMinutes = int.Parse(_config["Jwt:ExpiresMinutes"]!); var expiresMinutes = int.Parse(_config["Jwt:ExpiresMinutes"]!);
var roles = await _userManager.GetRolesAsync(user);
var claims = new List<Claim> var claims = new List<Claim>
{ {
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new(JwtRegisteredClaimNames.Email, user.Email), new(JwtRegisteredClaimNames.Email, user.Email ?? string.Empty),
new("name", user.Name), 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 signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
var creds = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); var creds = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);

View File

@ -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<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole<Guid>> _roleManager;
private readonly ITenantProvider _tenantProvider;
public UsersController(
AppDbContext db,
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole<Guid>> roleManager,
ITenantProvider tenantProvider)
{
_db = db;
_userManager = userManager;
_roleManager = roleManager;
_tenantProvider = tenantProvider;
}
[HttpPost]
[Authorize(Roles = "admin")]
public async Task<ActionResult<UserListItemDto>> 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<ValidationErrorDto>
{
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<ValidationErrorDto>
{
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<ActionResult<PagedResult<UserListItemDto>>> 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<Guid> userIdsByRole = Enumerable.Empty<Guid>().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<UserListItemDto>
{
Page = page,
PageSize = pageSize,
Total = total,
Items = items
});
}
[HttpGet("{id:guid}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<UserListItemDto>> 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<IActionResult> 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<ValidationErrorDto>
{
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<ValidationErrorDto> ValidateCreate(UserCreateRequest req)
{
var errors = new List<ValidationErrorDto>();
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;
}
}

View File

@ -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; namespace line_gestao_api.Data;
public class AppDbContext : DbContext public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
{ {
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } private readonly ITenantProvider _tenantProvider;
public DbSet<User> Users => Set<User>(); public AppDbContext(DbContextOptions<AppDbContext> options, ITenantProvider tenantProvider) : base(options)
{
_tenantProvider = tenantProvider;
}
public DbSet<Tenant> Tenants => Set<Tenant>();
// ✅ tabela para espelhar a planilha (GERAL) // ✅ tabela para espelhar a planilha (GERAL)
public DbSet<MobileLine> MobileLines => Set<MobileLine>(); public DbSet<MobileLine> MobileLines => Set<MobileLine>();
@ -35,19 +43,22 @@ public class AppDbContext : DbContext
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
// ========================= // =========================
// ✅ USER // ✅ USER (Identity)
// ========================= // =========================
modelBuilder.Entity<User>() modelBuilder.Entity<ApplicationUser>(e =>
.HasIndex(u => u.Email) {
e.Property(x => x.Name).HasMaxLength(120);
e.HasIndex(x => new { x.TenantId, x.NormalizedEmail })
.IsUnique(); .IsUnique();
});
// ========================= // =========================
// ✅ GERAL (MobileLine) // ✅ GERAL (MobileLine)
// ========================= // =========================
modelBuilder.Entity<MobileLine>(e => modelBuilder.Entity<MobileLine>(e =>
{ {
// Mantém UNIQUE por Linha (se Linha puder ser null no banco, Postgres aceita múltiplos nulls) // Mantém UNIQUE por Linha por tenant (se Linha puder ser null no banco, Postgres aceita múltiplos nulls)
e.HasIndex(x => x.Linha).IsUnique(); e.HasIndex(x => new { x.TenantId, x.Linha }).IsUnique();
// performance // performance
e.HasIndex(x => x.Chip); e.HasIndex(x => x.Chip);
@ -66,6 +77,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.ICCID); e.HasIndex(x => x.ICCID);
e.HasIndex(x => x.LinhaAntiga); e.HasIndex(x => x.LinhaAntiga);
e.HasIndex(x => x.LinhaNova); e.HasIndex(x => x.LinhaNova);
e.HasIndex(x => x.TenantId);
// FK + index // FK + index
e.HasIndex(x => x.MobileLineId); e.HasIndex(x => x.MobileLineId);
@ -93,6 +105,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.Cliente); e.HasIndex(x => x.Cliente);
e.HasIndex(x => new { x.Tipo, x.Cliente }); e.HasIndex(x => new { x.Tipo, x.Cliente });
e.HasIndex(x => x.Item); e.HasIndex(x => x.Item);
e.HasIndex(x => x.TenantId);
}); });
// ========================= // =========================
@ -106,6 +119,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.Linha); e.HasIndex(x => x.Linha);
e.HasIndex(x => x.Cpf); e.HasIndex(x => x.Cpf);
e.HasIndex(x => x.Email); e.HasIndex(x => x.Email);
e.HasIndex(x => x.TenantId);
}); });
// ========================= // =========================
@ -117,6 +131,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.Cliente); e.HasIndex(x => x.Cliente);
e.HasIndex(x => x.Linha); e.HasIndex(x => x.Linha);
e.HasIndex(x => x.DtTerminoFidelizacao); e.HasIndex(x => x.DtTerminoFidelizacao);
e.HasIndex(x => x.TenantId);
}); });
// ========================= // =========================
@ -129,6 +144,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.LinhaNova); e.HasIndex(x => x.LinhaNova);
e.HasIndex(x => x.ICCID); e.HasIndex(x => x.ICCID);
e.HasIndex(x => x.DataTroca); e.HasIndex(x => x.DataTroca);
e.HasIndex(x => x.TenantId);
}); });
// ========================= // =========================
@ -142,6 +158,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.Lida); e.HasIndex(x => x.Lida);
e.HasIndex(x => x.Data); e.HasIndex(x => x.Data);
e.HasIndex(x => x.VigenciaLineId); e.HasIndex(x => x.VigenciaLineId);
e.HasIndex(x => x.TenantId);
e.HasOne(x => x.User) e.HasOne(x => x.User)
.WithMany() .WithMany()
@ -153,5 +170,43 @@ public class AppDbContext : DbContext
.HasForeignKey(x => x.VigenciaLineId) .HasForeignKey(x => x.VigenciaLineId)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
}); });
modelBuilder.Entity<MobileLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<MuregLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<BillingClient>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<UserData>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<VigenciaLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<TrocaNumeroLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<Notification>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ApplicationUser>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
}
public override int SaveChanges()
{
ApplyTenantIds();
return base.SaveChanges();
}
public override Task<int> 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<ITenantEntity>().Where(e => e.State == EntityState.Added))
{
if (entry.Entity.TenantId == Guid.Empty)
{
entry.Entity.TenantId = tenantId;
}
}
} }
} }

82
Data/SeedData.cs Normal file
View File

@ -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<AppDbContext>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole<Guid>>>();
var tenantProvider = scope.ServiceProvider.GetRequiredService<ITenantProvider>();
var options = scope.ServiceProvider.GetRequiredService<IOptions<SeedOptions>>().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<Guid>(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);
}
}

37
Dtos/UserDtos.cs Normal file
View File

@ -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<ValidationErrorDto> Errors { get; set; } = new();
}

View File

@ -0,0 +1,837 @@
// <auto-generated />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Aparelho")
.HasColumnType("text");
b.Property<string>("Cliente")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("FormaPagamento")
.HasColumnType("text");
b.Property<decimal?>("FranquiaLine")
.HasColumnType("numeric");
b.Property<decimal?>("FranquiaVivo")
.HasColumnType("numeric");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<decimal?>("Lucro")
.HasColumnType("numeric");
b.Property<int?>("QtdLinhas")
.HasColumnType("integer");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<string>("Tipo")
.IsRequired()
.HasMaxLength(2)
.HasColumnType("character varying(2)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("ValorContratoLine")
.HasColumnType("numeric");
b.Property<decimal?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Cedente")
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<string>("Chip")
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("Cliente")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Conta")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataBloqueio")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataEntregaCliente")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataEntregaOpera")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("Desconto")
.HasColumnType("numeric");
b.Property<decimal?>("FranquiaGestao")
.HasColumnType("numeric");
b.Property<decimal?>("FranquiaLine")
.HasColumnType("numeric");
b.Property<decimal?>("FranquiaVivo")
.HasColumnType("numeric");
b.Property<decimal?>("GestaoVozDados")
.HasColumnType("numeric");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<string>("Linha")
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<decimal?>("LocacaoAp")
.HasColumnType("numeric");
b.Property<decimal?>("Lucro")
.HasColumnType("numeric");
b.Property<string>("Modalidade")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<string>("PlanoContrato")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<decimal?>("Skeelo")
.HasColumnType("numeric");
b.Property<string>("Skil")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<string>("Solicitante")
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<string>("Status")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Usuario")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<decimal?>("ValorContratoLine")
.HasColumnType("numeric");
b.Property<decimal?>("ValorContratoVivo")
.HasColumnType("numeric");
b.Property<decimal?>("ValorPlanoVivo")
.HasColumnType("numeric");
b.Property<string>("VencConta")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<decimal?>("VivoGestaoDispositivo")
.HasColumnType("numeric");
b.Property<decimal?>("VivoNewsPlus")
.HasColumnType("numeric");
b.Property<decimal?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataDaMureg")
.HasColumnType("timestamp with time zone");
b.Property<string>("ICCID")
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<string>("LinhaAntiga")
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<string>("LinhaNova")
.HasMaxLength(30)
.HasColumnType("character varying(30)");
b.Property<Guid>("MobileLineId")
.HasColumnType("uuid");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Cliente")
.HasColumnType("text");
b.Property<DateTime>("Data")
.HasColumnType("timestamp with time zone");
b.Property<string>("DedupKey")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("DiasParaVencer")
.HasColumnType("integer");
b.Property<string>("Linha")
.HasColumnType("text");
b.Property<bool>("Lida")
.HasColumnType("boolean");
b.Property<DateTime?>("LidaEm")
.HasColumnType("timestamp with time zone");
b.Property<string>("Mensagem")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("ReferenciaData")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<string>("Tipo")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Titulo")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("UserId")
.HasColumnType("uuid");
b.Property<string>("Usuario")
.HasColumnType("text");
b.Property<Guid?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Tenants");
});
modelBuilder.Entity("line_gestao_api.Models.TrocaNumeroLine", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataTroca")
.HasColumnType("timestamp with time zone");
b.Property<string>("ICCID")
.HasColumnType("text");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<string>("LinhaAntiga")
.HasColumnType("text");
b.Property<string>("LinhaNova")
.HasColumnType("text");
b.Property<string>("Motivo")
.HasColumnType("text");
b.Property<string>("Observacao")
.HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Celular")
.HasColumnType("text");
b.Property<string>("Cliente")
.HasColumnType("text");
b.Property<string>("Cpf")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataNascimento")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<string>("Endereco")
.HasColumnType("text");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<string>("Linha")
.HasColumnType("text");
b.Property<string>("Rg")
.HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<string>("TelefoneFixo")
.HasColumnType("text");
b.Property<DateTime>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Cliente")
.HasColumnType("text");
b.Property<string>("Conta")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DtEfetivacaoServico")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DtTerminoFidelizacao")
.HasColumnType("timestamp with time zone");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<string>("Linha")
.HasColumnType("text");
b.Property<string>("PlanoContrato")
.HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<decimal?>("Total")
.HasColumnType("numeric");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("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<Guid>", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("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<Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("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<Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<Guid>", b =>
{
b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<Guid>", b =>
{
b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<Guid>", 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<Guid>", 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
}
}
}

View File

@ -0,0 +1,456 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace line_gestao_api.Migrations
{
/// <inheritdoc />
public partial class AddIdentityAndTenants : Migration
{
private static readonly Guid DefaultTenantId = Guid.Parse("11111111-1111-1111-1111-111111111111");
/// <inheritdoc />
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<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(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<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
TenantId = table.Column<Guid>(type: "uuid", nullable: false, defaultValue: DefaultTenantId),
IsActive = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: true),
SecurityStamp = table.Column<string>(type: "text", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
PhoneNumber = table.Column<string>(type: "text", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RoleId = table.Column<Guid>(type: "uuid", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(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<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(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<string>(type: "text", nullable: false),
ProviderKey = table.Column<string>(type: "text", nullable: false),
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<Guid>(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<Guid>(type: "uuid", nullable: false),
RoleId = table.Column<Guid>(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<Guid>(type: "uuid", nullable: false),
LoginProvider = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Value = table.Column<string>(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<Guid>(
name: "TenantId",
table: "MobileLines",
type: "uuid",
nullable: false,
defaultValue: DefaultTenantId);
migrationBuilder.AddColumn<Guid>(
name: "TenantId",
table: "MuregLines",
type: "uuid",
nullable: false,
defaultValue: DefaultTenantId);
migrationBuilder.AddColumn<Guid>(
name: "TenantId",
table: "billing_clients",
type: "uuid",
nullable: false,
defaultValue: DefaultTenantId);
migrationBuilder.AddColumn<Guid>(
name: "TenantId",
table: "UserDatas",
type: "uuid",
nullable: false,
defaultValue: DefaultTenantId);
migrationBuilder.AddColumn<Guid>(
name: "TenantId",
table: "VigenciaLines",
type: "uuid",
nullable: false,
defaultValue: DefaultTenantId);
migrationBuilder.AddColumn<Guid>(
name: "TenantId",
table: "TrocaNumeroLines",
type: "uuid",
nullable: false,
defaultValue: DefaultTenantId);
migrationBuilder.AddColumn<Guid>(
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 });
}
/// <inheritdoc />
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<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Email = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Phone = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(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);
}
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace line_gestao_api.Migrations
{
/// <inheritdoc />
public partial class RenameUserNameIndexSafe : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("ALTER INDEX IF EXISTS \"IX_AspNetUsers_NormalizedUserName\" RENAME TO \"UserNameIndex\"");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("ALTER INDEX IF EXISTS \"UserNameIndex\" RENAME TO \"IX_AspNetUsers_NormalizedUserName\"");
}
}
}

View File

@ -1,4 +1,4 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
@ -22,6 +22,85 @@ namespace line_gestao_api.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("line_gestao_api.Models.ApplicationUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("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 => modelBuilder.Entity("line_gestao_api.Models.BillingClient", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -57,6 +136,9 @@ namespace line_gestao_api.Migrations
b.Property<int?>("QtdLinhas") b.Property<int?>("QtdLinhas")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<string>("Tipo") b.Property<string>("Tipo")
.IsRequired() .IsRequired()
.HasMaxLength(2) .HasMaxLength(2)
@ -77,6 +159,8 @@ namespace line_gestao_api.Migrations
b.HasIndex("Item"); b.HasIndex("Item");
b.HasIndex("TenantId");
b.HasIndex("Tipo"); b.HasIndex("Tipo");
b.HasIndex("Tipo", "Cliente"); b.HasIndex("Tipo", "Cliente");
@ -169,6 +253,9 @@ namespace line_gestao_api.Migrations
.HasMaxLength(80) .HasMaxLength(80)
.HasColumnType("character varying(80)"); .HasColumnType("character varying(80)");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt") b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@ -204,13 +291,13 @@ namespace line_gestao_api.Migrations
b.HasIndex("Cliente"); b.HasIndex("Cliente");
b.HasIndex("Linha")
.IsUnique();
b.HasIndex("Skil"); b.HasIndex("Skil");
b.HasIndex("Status"); b.HasIndex("Status");
b.HasIndex("TenantId", "Linha")
.IsUnique();
b.HasIndex("Usuario"); b.HasIndex("Usuario");
b.ToTable("MobileLines"); b.ToTable("MobileLines");
@ -246,6 +333,9 @@ namespace line_gestao_api.Migrations
b.Property<Guid>("MobileLineId") b.Property<Guid>("MobileLineId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt") b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@ -261,93 +351,11 @@ namespace line_gestao_api.Migrations
b.HasIndex("MobileLineId"); b.HasIndex("MobileLineId");
b.HasIndex("TenantId");
b.ToTable("MuregLines"); b.ToTable("MuregLines");
}); });
modelBuilder.Entity("line_gestao_api.Models.TrocaNumeroLine", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataTroca")
.HasColumnType("timestamp with time zone");
b.Property<string>("ICCID")
.HasColumnType("text");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<string>("LinhaAntiga")
.HasColumnType("text");
b.Property<string>("LinhaNova")
.HasColumnType("text");
b.Property<string>("Motivo")
.HasColumnType("text");
b.Property<string>("Observacao")
.HasColumnType("text");
b.Property<DateTime>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("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 => modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -383,6 +391,9 @@ namespace line_gestao_api.Migrations
b.Property<DateTime?>("ReferenciaData") b.Property<DateTime?>("ReferenciaData")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<string>("Tipo") b.Property<string>("Tipo")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
@ -411,6 +422,8 @@ namespace line_gestao_api.Migrations
b.HasIndex("Lida"); b.HasIndex("Lida");
b.HasIndex("TenantId");
b.HasIndex("UserId"); b.HasIndex("UserId");
b.HasIndex("VigenciaLineId"); b.HasIndex("VigenciaLineId");
@ -418,6 +431,76 @@ namespace line_gestao_api.Migrations
b.ToTable("Notifications"); b.ToTable("Notifications");
}); });
modelBuilder.Entity("line_gestao_api.Models.Tenant", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Tenants");
});
modelBuilder.Entity("line_gestao_api.Models.TrocaNumeroLine", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DataTroca")
.HasColumnType("timestamp with time zone");
b.Property<string>("ICCID")
.HasColumnType("text");
b.Property<int>("Item")
.HasColumnType("integer");
b.Property<string>("LinhaAntiga")
.HasColumnType("text");
b.Property<string>("LinhaNova")
.HasColumnType("text");
b.Property<string>("Motivo")
.HasColumnType("text");
b.Property<string>("Observacao")
.HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("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 => modelBuilder.Entity("line_gestao_api.Models.UserData", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -454,6 +537,9 @@ namespace line_gestao_api.Migrations
b.Property<string>("Rg") b.Property<string>("Rg")
.HasColumnType("text"); .HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<string>("TelefoneFixo") b.Property<string>("TelefoneFixo")
.HasColumnType("text"); .HasColumnType("text");
@ -472,6 +558,8 @@ namespace line_gestao_api.Migrations
b.HasIndex("Linha"); b.HasIndex("Linha");
b.HasIndex("TenantId");
b.ToTable("UserDatas"); b.ToTable("UserDatas");
}); });
@ -505,6 +593,9 @@ namespace line_gestao_api.Migrations
b.Property<string>("PlanoContrato") b.Property<string>("PlanoContrato")
.HasColumnType("text"); .HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<decimal?>("Total") b.Property<decimal?>("Total")
.HasColumnType("numeric"); .HasColumnType("numeric");
@ -524,12 +615,151 @@ namespace line_gestao_api.Migrations
b.HasIndex("Linha"); b.HasIndex("Linha");
b.HasIndex("TenantId");
b.ToTable("VigenciaLines"); b.ToTable("VigenciaLines");
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<Guid>", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("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<Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("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 => modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
{ {
b.HasOne("line_gestao_api.Models.User", "User") b.HasOne("line_gestao_api.Models.ApplicationUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
@ -544,15 +774,55 @@ namespace line_gestao_api.Migrations
b.Navigation("VigenciaLine"); b.Navigation("VigenciaLine");
}); });
modelBuilder.Entity("line_gestao_api.Models.MuregLine", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<Guid>", b =>
{ {
b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine") b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<Guid>", null)
.WithMany("Muregs") .WithMany()
.HasForeignKey("MobileLineId") .HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict) .OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<Guid>", b =>
{
b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<Guid>", b =>
{
b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("MobileLine"); b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<Guid>", b =>
{
b.HasOne("line_gestao_api.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
}); });
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b => modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>

11
Models/ApplicationUser.cs Normal file
View File

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Identity;
namespace line_gestao_api.Models;
public class ApplicationUser : IdentityUser<Guid>, 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;
}

View File

@ -1,8 +1,9 @@
namespace line_gestao_api.Models namespace line_gestao_api.Models
{ {
public class BillingClient public class BillingClient : ITenantEntity
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid TenantId { get; set; }
public string Tipo { get; set; } = "PF"; // "PF" ou "PJ" public string Tipo { get; set; } = "PF"; // "PF" ou "PJ"
public int Item { get; set; } public int Item { get; set; }

6
Models/ITenantEntity.cs Normal file
View File

@ -0,0 +1,6 @@
namespace line_gestao_api.Models;
public interface ITenantEntity
{
Guid TenantId { get; set; }
}

View File

@ -2,10 +2,12 @@
namespace line_gestao_api.Models namespace line_gestao_api.Models
{ {
public class MobileLine public class MobileLine : ITenantEntity
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
public Guid TenantId { get; set; }
public int Item { get; set; } public int Item { get; set; }
[MaxLength(80)] [MaxLength(80)]
public string? Conta { get; set; } public string? Conta { get; set; }

View File

@ -2,10 +2,12 @@
namespace line_gestao_api.Models namespace line_gestao_api.Models
{ {
public class MuregLine public class MuregLine : ITenantEntity
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
public Guid TenantId { get; set; }
public int Item { get; set; } public int Item { get; set; }
// Linha escolhida da GERAL no momento do mureg // Linha escolhida da GERAL no momento do mureg

View File

@ -2,10 +2,12 @@
namespace line_gestao_api.Models; namespace line_gestao_api.Models;
public class Notification public class Notification : ITenantEntity
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
public Guid TenantId { get; set; }
[Required] [Required]
public string Tipo { get; set; } = string.Empty; public string Tipo { get; set; } = string.Empty;
@ -29,7 +31,7 @@ public class Notification
public string DedupKey { get; set; } = string.Empty; public string DedupKey { get; set; } = string.Empty;
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
public User? User { get; set; } public ApplicationUser? User { get; set; }
public Guid? VigenciaLineId { get; set; } public Guid? VigenciaLineId { get; set; }
public VigenciaLine? VigenciaLine { get; set; } public VigenciaLine? VigenciaLine { get; set; }

8
Models/Tenant.cs Normal file
View File

@ -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;
}

View File

@ -2,10 +2,12 @@
namespace line_gestao_api.Models namespace line_gestao_api.Models
{ {
public class TrocaNumeroLine public class TrocaNumeroLine : ITenantEntity
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid TenantId { get; set; }
public int Item { get; set; } public int Item { get; set; }
public string? LinhaAntiga { get; set; } public string? LinhaAntiga { get; set; }

View File

@ -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;
}

View File

@ -3,11 +3,13 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace line_gestao_api.Models namespace line_gestao_api.Models
{ {
public class UserData public class UserData : ITenantEntity
{ {
[Key] [Key]
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid TenantId { get; set; }
public int Item { get; set; } public int Item { get; set; }
public string? Linha { get; set; } public string? Linha { get; set; }

View File

@ -2,10 +2,12 @@
namespace line_gestao_api.Models namespace line_gestao_api.Models
{ {
public class VigenciaLine public class VigenciaLine : ITenantEntity
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid TenantId { get; set; }
public int Item { get; set; } public int Item { get; set; }
public string? Conta { get; set; } public string? Conta { get; set; }

View File

@ -1,8 +1,10 @@
using System.Text; using System.Text;
using line_gestao_api.Data; using line_gestao_api.Data;
using line_gestao_api.Models;
using line_gestao_api.Services; using line_gestao_api.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -31,6 +33,18 @@ builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")) options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))
); );
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenantProvider, TenantProvider>();
builder.Services.AddIdentityCore<ApplicationUser>(options =>
{
options.Password.RequiredLength = 6;
options.User.RequireUniqueEmail = false;
})
.AddRoles<IdentityRole<Guid>>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
// ✅ Swagger // ✅ Swagger
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
@ -61,6 +75,8 @@ builder.Services.AddAuthorization();
builder.Services.Configure<NotificationOptions>(builder.Configuration.GetSection("Notifications")); builder.Services.Configure<NotificationOptions>(builder.Configuration.GetSection("Notifications"));
builder.Services.AddHostedService<VigenciaNotificationBackgroundService>(); builder.Services.AddHostedService<VigenciaNotificationBackgroundService>();
builder.Services.Configure<SeedOptions>(builder.Configuration.GetSection("Seed"));
var app = builder.Build(); var app = builder.Build();
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
@ -75,8 +91,11 @@ app.UseHttpsRedirection();
app.UseCors("Front"); app.UseCors("Front");
app.UseAuthentication(); app.UseAuthentication();
app.UseMiddleware<TenantMiddleware>();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();
await SeedData.EnsureSeedDataAsync(app.Services);
app.Run(); app.Run();

View File

@ -0,0 +1,7 @@
namespace line_gestao_api.Services;
public interface ITenantProvider
{
Guid? TenantId { get; }
void SetTenantId(Guid? tenantId);
}

View File

@ -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);
}
}
}

View File

@ -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<Guid?> 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;
}
}

View File

@ -41,6 +41,7 @@ public class VigenciaNotificationBackgroundService : BackgroundService
{ {
using var scope = _scopeFactory.CreateScope(); using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var tenantProvider = scope.ServiceProvider.GetRequiredService<ITenantProvider>();
if (!await TableExistsAsync(db, "Notifications", stoppingToken)) if (!await TableExistsAsync(db, "Notifications", stoppingToken))
{ {
@ -48,6 +49,53 @@ public class VigenciaNotificationBackgroundService : BackgroundService
return; 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<bool> 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 today = DateTime.UtcNow.Date;
var reminderDays = _options.ReminderDays var reminderDays = _options.ReminderDays
.Distinct() .Distinct()
@ -110,7 +158,8 @@ public class VigenciaNotificationBackgroundService : BackgroundService
usuario: usuario, usuario: usuario,
cliente: cliente, cliente: cliente,
linha: linha, linha: linha,
vigenciaLineId: vigencia.Id); vigenciaLineId: vigencia.Id,
tenantId: tenantId);
candidates.Add(notification); candidates.Add(notification);
continue; continue;
@ -129,7 +178,8 @@ public class VigenciaNotificationBackgroundService : BackgroundService
usuario: usuario, usuario: usuario,
cliente: cliente, cliente: cliente,
linha: linha, linha: linha,
vigenciaLineId: vigencia.Id); vigenciaLineId: vigencia.Id,
tenantId: tenantId);
candidates.Add(notification); candidates.Add(notification);
} }
@ -159,35 +209,6 @@ public class VigenciaNotificationBackgroundService : BackgroundService
await db.Notifications.AddRangeAsync(toInsert, stoppingToken); await db.Notifications.AddRangeAsync(toInsert, stoppingToken);
await db.SaveChangesAsync(stoppingToken); await db.SaveChangesAsync(stoppingToken);
} }
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao gerar notificações de vigência.");
}
}
private static async Task<bool> 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 static Notification BuildNotification( private static Notification BuildNotification(
string tipo, string tipo,
@ -199,7 +220,8 @@ public class VigenciaNotificationBackgroundService : BackgroundService
string? usuario, string? usuario,
string? cliente, string? cliente,
string? linha, string? linha,
Guid vigenciaLineId) Guid vigenciaLineId,
Guid tenantId)
{ {
return new Notification return new Notification
{ {
@ -215,7 +237,8 @@ public class VigenciaNotificationBackgroundService : BackgroundService
Usuario = usuario, Usuario = usuario,
Cliente = cliente, Cliente = cliente,
Linha = linha, Linha = linha,
VigenciaLineId = vigenciaLineId VigenciaLineId = vigenciaLineId,
TenantId = tenantId
}; };
} }

View File

@ -8,5 +8,11 @@
"Notifications": { "Notifications": {
"CheckIntervalMinutes": 60, "CheckIntervalMinutes": 60,
"ReminderDays": [30, 15, 7] "ReminderDays": [30, 15, 7]
},
"Seed": {
"DefaultTenantName": "Default",
"AdminName": "Administrador",
"AdminEmail": "admin@linegestao.local",
"AdminPassword": "Admin123!"
} }
} }

View File

@ -11,5 +11,11 @@
"Notifications": { "Notifications": {
"CheckIntervalMinutes": 60, "CheckIntervalMinutes": 60,
"ReminderDays": [30, 15, 7] "ReminderDays": [30, 15, 7]
},
"Seed": {
"DefaultTenantName": "Default",
"AdminName": "Administrador",
"AdminEmail": "admin@linegestao.local",
"AdminPassword": "Admin123!"
} }
} }

View File

@ -25,6 +25,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" /> <PackageReference Include="ClosedXML" Version="0.105.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">