Compare commits

...

2 Commits

Author SHA1 Message Date
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.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<ApplicationUser> _userManager;
private readonly ITenantProvider _tenantProvider;
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;
}
[HttpPost("register")]
[Authorize(Roles = "admin")]
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)
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>();
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<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);
if (user is null) return Unauthorized("Credenciais inválidas.");
var users = await _userManager.Users
.Where(u => u.NormalizedEmail == normalizedEmail)
.ToListAsync();
var hasher = new PasswordHasher<User>();
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<string> 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<Claim>
{
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);

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;
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)
public DbSet<MobileLine> MobileLines => Set<MobileLine>();
@ -35,19 +43,22 @@ public class AppDbContext : DbContext
base.OnModelCreating(modelBuilder);
// =========================
// ✅ USER
// ✅ USER (Identity)
// =========================
modelBuilder.Entity<User>()
.HasIndex(u => u.Email)
.IsUnique();
modelBuilder.Entity<ApplicationUser>(e =>
{
e.Property(x => x.Name).HasMaxLength(120);
e.HasIndex(x => new { x.TenantId, x.NormalizedEmail })
.IsUnique();
});
// =========================
// ✅ GERAL (MobileLine)
// =========================
modelBuilder.Entity<MobileLine>(e =>
{
// Mantém UNIQUE por Linha (se Linha puder ser null no banco, Postgres aceita múltiplos nulls)
e.HasIndex(x => x.Linha).IsUnique();
// Mantém UNIQUE por Linha por tenant (se Linha puder ser null no banco, Postgres aceita múltiplos nulls)
e.HasIndex(x => new { x.TenantId, x.Linha }).IsUnique();
// performance
e.HasIndex(x => x.Chip);
@ -66,6 +77,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.ICCID);
e.HasIndex(x => x.LinhaAntiga);
e.HasIndex(x => x.LinhaNova);
e.HasIndex(x => x.TenantId);
// FK + index
e.HasIndex(x => x.MobileLineId);
@ -93,6 +105,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.Cliente);
e.HasIndex(x => new { x.Tipo, x.Cliente });
e.HasIndex(x => x.Item);
e.HasIndex(x => x.TenantId);
});
// =========================
@ -106,6 +119,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.Linha);
e.HasIndex(x => x.Cpf);
e.HasIndex(x => x.Email);
e.HasIndex(x => x.TenantId);
});
// =========================
@ -117,6 +131,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.Cliente);
e.HasIndex(x => x.Linha);
e.HasIndex(x => x.DtTerminoFidelizacao);
e.HasIndex(x => x.TenantId);
});
// =========================
@ -129,6 +144,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.LinhaNova);
e.HasIndex(x => x.ICCID);
e.HasIndex(x => x.DataTroca);
e.HasIndex(x => x.TenantId);
});
// =========================
@ -142,6 +158,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.Lida);
e.HasIndex(x => x.Data);
e.HasIndex(x => x.VigenciaLineId);
e.HasIndex(x => x.TenantId);
e.HasOne(x => x.User)
.WithMany()
@ -153,5 +170,43 @@ public class AppDbContext : DbContext
.HasForeignKey(x => x.VigenciaLineId)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<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 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<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")
@ -57,6 +136,9 @@ namespace line_gestao_api.Migrations
b.Property<int?>("QtdLinhas")
.HasColumnType("integer");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<string>("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<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("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<Guid>("MobileLineId")
.HasColumnType("uuid");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
@ -261,93 +351,11 @@ namespace line_gestao_api.Migrations
b.HasIndex("MobileLineId");
b.HasIndex("TenantId");
b.ToTable("MuregLines");
});
modelBuilder.Entity("line_gestao_api.Models.TrocaNumeroLine", b =>
{
b.Property<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 =>
{
b.Property<Guid>("Id")
@ -383,6 +391,9 @@ namespace line_gestao_api.Migrations
b.Property<DateTime?>("ReferenciaData")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<string>("Tipo")
.IsRequired()
.HasColumnType("text");
@ -411,6 +422,8 @@ namespace line_gestao_api.Migrations
b.HasIndex("Lida");
b.HasIndex("TenantId");
b.HasIndex("UserId");
b.HasIndex("VigenciaLineId");
@ -418,6 +431,76 @@ namespace line_gestao_api.Migrations
b.ToTable("Notifications");
});
modelBuilder.Entity("line_gestao_api.Models.Tenant", b =>
{
b.Property<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")
@ -454,6 +537,9 @@ namespace line_gestao_api.Migrations
b.Property<string>("Rg")
.HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<string>("TelefoneFixo")
.HasColumnType("text");
@ -472,6 +558,8 @@ namespace line_gestao_api.Migrations
b.HasIndex("Linha");
b.HasIndex("TenantId");
b.ToTable("UserDatas");
});
@ -505,6 +593,9 @@ namespace line_gestao_api.Migrations
b.Property<string>("PlanoContrato")
.HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<decimal?>("Total")
.HasColumnType("numeric");
@ -524,12 +615,151 @@ namespace line_gestao_api.Migrations
b.HasIndex("Linha");
b.HasIndex("TenantId");
b.ToTable("VigenciaLines");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<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.User", "User")
b.HasOne("line_gestao_api.Models.ApplicationUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict);
@ -544,15 +774,55 @@ namespace line_gestao_api.Migrations
b.Navigation("VigenciaLine");
});
modelBuilder.Entity("line_gestao_api.Models.MuregLine", b =>
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<Guid>", b =>
{
b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine")
.WithMany("Muregs")
.HasForeignKey("MobileLineId")
.OnDelete(DeleteBehavior.Restrict)
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.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 =>

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

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

View File

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

View File

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

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

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

View File

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

View File

@ -1,8 +1,10 @@
using System.Text;
using line_gestao_api.Data;
using line_gestao_api.Models;
using line_gestao_api.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
@ -31,6 +33,18 @@ builder.Services.AddDbContext<AppDbContext>(options =>
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
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
@ -61,6 +75,8 @@ builder.Services.AddAuthorization();
builder.Services.Configure<NotificationOptions>(builder.Configuration.GetSection("Notifications"));
builder.Services.AddHostedService<VigenciaNotificationBackgroundService>();
builder.Services.Configure<SeedOptions>(builder.Configuration.GetSection("Seed"));
var app = builder.Build();
if (app.Environment.IsDevelopment())
@ -75,8 +91,11 @@ app.UseHttpsRedirection();
app.UseCors("Front");
app.UseAuthentication();
app.UseMiddleware<TenantMiddleware>();
app.UseAuthorization();
app.MapControllers();
await SeedData.EnsureSeedDataAsync(app.Services);
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();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var tenantProvider = scope.ServiceProvider.GetRequiredService<ITenantProvider>();
if (!await TableExistsAsync(db, "Notifications", stoppingToken))
{
@ -48,116 +49,20 @@ public class VigenciaNotificationBackgroundService : BackgroundService
return;
}
var today = DateTime.UtcNow.Date;
var reminderDays = _options.ReminderDays
.Distinct()
.Where(d => d > 0)
.OrderBy(d => d)
.ToList();
var users = await db.Users.AsNoTracking()
.Select(u => new { u.Id, u.Name, u.Email })
.ToListAsync(stoppingToken);
var userByName = users
.Where(u => !string.IsNullOrWhiteSpace(u.Name))
.ToDictionary(u => u.Name.Trim().ToLowerInvariant(), u => u.Id);
var userByEmail = users
.Where(u => !string.IsNullOrWhiteSpace(u.Email))
.ToDictionary(u => u.Email.Trim().ToLowerInvariant(), u => u.Id);
var vigencias = await db.VigenciaLines.AsNoTracking()
.Where(v => v.DtTerminoFidelizacao != null)
.ToListAsync(stoppingToken);
var candidates = new List<Notification>();
foreach (var vigencia in vigencias)
{
if (vigencia.DtTerminoFidelizacao is null)
{
continue;
}
var endDate = vigencia.DtTerminoFidelizacao.Value.Date;
var usuario = vigencia.Usuario?.Trim();
var cliente = vigencia.Cliente?.Trim();
var linha = vigencia.Linha?.Trim();
var usuarioKey = usuario?.ToLowerInvariant();
Guid? userId = null;
if (!string.IsNullOrWhiteSpace(usuarioKey))
{
if (userByEmail.TryGetValue(usuarioKey, out var matchedByEmail))
{
userId = matchedByEmail;
}
else if (userByName.TryGetValue(usuarioKey, out var matchedByName))
{
userId = matchedByName;
}
}
if (endDate < today)
{
var notification = BuildNotification(
tipo: "Vencido",
titulo: $"Linha vencida{FormatLinha(linha)}",
mensagem: $"A linha{FormatLinha(linha)} do cliente {cliente ?? "(sem cliente)"} venceu em {endDate:dd/MM/yyyy}.",
referenciaData: endDate,
diasParaVencer: 0,
userId: userId,
usuario: usuario,
cliente: cliente,
linha: linha,
vigenciaLineId: vigencia.Id);
candidates.Add(notification);
continue;
}
var daysUntil = (endDate - today).Days;
if (reminderDays.Contains(daysUntil))
{
var notification = BuildNotification(
tipo: "AVencer",
titulo: $"Linha a vencer em {daysUntil} dias{FormatLinha(linha)}",
mensagem: $"A linha{FormatLinha(linha)} do cliente {cliente ?? "(sem cliente)"} vence em {endDate:dd/MM/yyyy}.",
referenciaData: endDate,
diasParaVencer: daysUntil,
userId: userId,
usuario: usuario,
cliente: cliente,
linha: linha,
vigenciaLineId: vigencia.Id);
candidates.Add(notification);
}
}
if (candidates.Count == 0)
var tenants = await db.Tenants.AsNoTracking().ToListAsync(stoppingToken);
if (tenants.Count == 0)
{
_logger.LogWarning("Nenhum tenant encontrado para gerar notificações.");
return;
}
var dedupKeys = candidates.Select(c => c.DedupKey).Distinct().ToList();
var existingKeys = await db.Notifications.AsNoTracking()
.Where(n => dedupKeys.Contains(n.DedupKey))
.Select(n => n.DedupKey)
.ToListAsync(stoppingToken);
var existingSet = new HashSet<string>(existingKeys);
var toInsert = candidates
.Where(c => !existingSet.Contains(c.DedupKey))
.ToList();
if (toInsert.Count == 0)
foreach (var tenant in tenants)
{
return;
tenantProvider.SetTenantId(tenant.Id);
await ProcessTenantAsync(db, tenant.Id, stoppingToken);
}
await db.Notifications.AddRangeAsync(toInsert, stoppingToken);
await db.SaveChangesAsync(stoppingToken);
tenantProvider.SetTenantId(null);
}
catch (Exception ex)
{
@ -189,6 +94,122 @@ public class VigenciaNotificationBackgroundService : BackgroundService
return result is bool exists && exists;
}
private async Task ProcessTenantAsync(AppDbContext db, Guid tenantId, CancellationToken stoppingToken)
{
var today = DateTime.UtcNow.Date;
var reminderDays = _options.ReminderDays
.Distinct()
.Where(d => d > 0)
.OrderBy(d => d)
.ToList();
var users = await db.Users.AsNoTracking()
.Select(u => new { u.Id, u.Name, u.Email })
.ToListAsync(stoppingToken);
var userByName = users
.Where(u => !string.IsNullOrWhiteSpace(u.Name))
.ToDictionary(u => u.Name.Trim().ToLowerInvariant(), u => u.Id);
var userByEmail = users
.Where(u => !string.IsNullOrWhiteSpace(u.Email))
.ToDictionary(u => u.Email.Trim().ToLowerInvariant(), u => u.Id);
var vigencias = await db.VigenciaLines.AsNoTracking()
.Where(v => v.DtTerminoFidelizacao != null)
.ToListAsync(stoppingToken);
var candidates = new List<Notification>();
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<string>(existingKeys);
var toInsert = candidates
.Where(c => !existingSet.Contains(c.DedupKey))
.ToList();
if (toInsert.Count == 0)
{
return;
}
await db.Notifications.AddRangeAsync(toInsert, stoppingToken);
await db.SaveChangesAsync(stoppingToken);
}
private static Notification BuildNotification(
string tipo,
string titulo,
@ -199,7 +220,8 @@ public class VigenciaNotificationBackgroundService : BackgroundService
string? usuario,
string? cliente,
string? linha,
Guid vigenciaLineId)
Guid vigenciaLineId,
Guid tenantId)
{
return new Notification
{
@ -215,7 +237,8 @@ public class VigenciaNotificationBackgroundService : BackgroundService
Usuario = usuario,
Cliente = cliente,
Linha = linha,
VigenciaLineId = vigenciaLineId
VigenciaLineId = vigenciaLineId,
TenantId = tenantId
};
}

View File

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

View File

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

View File

@ -25,6 +25,7 @@
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" />
<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.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">