diff --git a/.gitignore b/.gitignore index cea3bae..a7a8182 100644 --- a/.gitignore +++ b/.gitignore @@ -402,4 +402,6 @@ FodyWeavers.xsd *.msp # JetBrains Rider -*.sln.iml \ No newline at end of file +*.sln.iml +/LIN.Cloud.Identity/appsettings.json +/LIN.Cloud.Identity/appsettings.firebase.json diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2ec3700 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +LIN ACADEMIC LICENSE +Versión 1 - 10 abril 2024 + +Este software y su código fuente están disponibles para su visualización con fines exclusivamente académicos. Se concede permiso a cualquier persona que acceda a este software para ver su código fuente con el propósito de estudio, investigación o enseñanza, siempre y cuando se respeten los términos y condiciones de esta licencia. + +Limitaciones: +1. No se permite la distribución de este software ni de su código fuente, ya sea en su forma original o modificada. +2. No se permite el uso de este software ni de su código fuente con fines comerciales. +3. Cualquier uso fuera del ámbito académico está expresamente prohibido. + +Responsabilidad: +Este software se proporciona "tal cual", sin garantía de ningún tipo, expresa o implícita, incluidas, entre otras, las garantías de comerciabilidad, idoneidad para un propósito particular y no infracción. El titular de los derechos de autor no será responsable de ningún daño directo, indirecto, incidental, especial, ejemplar o consecuencial (incluidos, entre otros, la adquisición de bienes o servicios sustitutos, la pérdida de uso, datos o beneficios o interrupción del negocio) sin importar la causa y bajo cualquier responsabilidad teoría de responsabilidad, ya sea en contrato, responsabilidad estricta o agravio (incluida la negligencia o de otra manera) que surja de cualquier manera del uso de este software, incluso si se ha advertido de la posibilidad de tales daños. + +Aceptación: +Al acceder o utilizar este software, usted acepta cumplir con los términos y condiciones de esta licencia. Si no acepta estos términos y condiciones, no está autorizado a utilizar este software ni su código fuente. \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Contexts/DataContext.cs b/LIN.Cloud.Identity.Persistence/Contexts/DataContext.cs new file mode 100644 index 0000000..1fcbeda --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Contexts/DataContext.cs @@ -0,0 +1,316 @@ +namespace LIN.Cloud.Identity.Persistence.Contexts; + +public class DataContext(DbContextOptions options) : DbContext(options) +{ + + /// + /// Tabla de identidades. + /// + public DbSet Identities { get; set; } + + /// + /// Tabla de cuentas. + /// + public DbSet Accounts { get; set; } + + /// + /// Organizaciones. + /// + public DbSet Organizations { get; set; } + + /// + /// Grupos. + /// + public DbSet Groups { get; set; } + + /// + /// Integrantes de un grupo. + /// + public DbSet GroupMembers { get; set; } + + /// + /// RolesIam de grupos. + /// + public DbSet IdentityRoles { get; set; } + + /// + /// Aplicaciones. + /// + public DbSet Applications { get; set; } + + /// + /// Logs de accounts. + /// + public DbSet AccountLogs { get; set; } + + /// + /// Políticas. + /// + public DbSet Policies { get; set; } + + /// + /// Políticas por tipo de entidad. + /// + public DbSet IdentityTypesPolicies { get; set; } + + /// + /// Políticas por acceso IP. + /// + public DbSet IpAccessPolicies { get; set; } + + /// + /// Políticas por acceso de tiempo. + /// + public DbSet TimeAccessPolicies { get; set; } + + /// + /// Códigos OTPS. + /// + public DbSet OTPs { get; set; } + + /// + /// Correos asociados a las cuentas. + /// + public DbSet Mails { get; set; } + + /// + /// Mail Otp. + /// + public DbSet MailOtp { get; set; } + + /// + /// Mail Otp. + /// + public DbSet IdentityPolicies { get; set; } + + /// + /// Dominios. + /// + public DbSet Domains { get; set; } + + /// + /// Cuentas temporales. + /// + public DbSet TemporalAccounts { get; set; } + + /// + /// Crear el modelo en BD. + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Identity Model. + modelBuilder.Entity(entity => + { + entity.ToTable("identities"); + entity.HasIndex(t => t.Unique).IsUnique(); + + entity.HasOne(o => o.Owner) + .WithMany() + .HasForeignKey(o => o.OwnerId) + .OnDelete(DeleteBehavior.NoAction); + }); + + // Account Model. + modelBuilder.Entity(entity => + { + entity.ToTable("accounts"); + entity.HasIndex(t => t.IdentityId).IsUnique(); + }); + + // Organization Model. + modelBuilder.Entity(entity => + { + entity.ToTable("organizations"); + entity.HasOne(o => o.Directory) + .WithOne() + .HasForeignKey(o => o.DirectoryId) + .OnDelete(DeleteBehavior.NoAction); + }); + + // Group Model. + modelBuilder.Entity(entity => + { + entity.HasOne(t => t.Identity) + .WithMany() + .HasForeignKey(t => t.IdentityId) + .OnDelete(DeleteBehavior.NoAction); + }); + + // Group Member Model + modelBuilder.Entity(entity => + { + entity.ToTable("group_members"); + entity.HasKey(t => new { t.IdentityId, t.GroupId }); + + entity.HasOne(t => t.Identity) + .WithMany() + .HasForeignKey(t => t.IdentityId) + .OnDelete(DeleteBehavior.NoAction); + + entity.HasOne(t => t.Group) + .WithMany(t => t.Members) + .HasForeignKey(t => t.GroupId); + }); + + // Group Member Model + modelBuilder.Entity(entity => + { + entity.ToTable("identity_policy"); + entity.HasKey(t => new { t.IdentityId, t.PolicyId }); + + entity.HasOne(t => t.Identity) + .WithMany() + .HasForeignKey(t => t.IdentityId) + .OnDelete(DeleteBehavior.NoAction); + + entity.HasOne(t => t.Policy) + .WithMany() + .HasForeignKey(t => t.PolicyId); + }); + + // Identity Roles Model + modelBuilder.Entity(entity => + { + entity.ToTable("identity_roles"); + entity.HasKey(t => new { t.Rol, t.IdentityId, t.OrganizationId }); + + entity.HasOne(t => t.Identity) + .WithMany(t => t.Roles) + .HasForeignKey(t => t.IdentityId); + + entity.HasOne(t => t.Organization) + .WithMany() + .HasForeignKey(t => t.OrganizationId); + }); + + // Application Model + modelBuilder.Entity(entity => + { + entity.ToTable("applications"); + entity.HasIndex(t => t.IdentityId).IsUnique(); + entity.HasIndex(t => t.Key).IsUnique(); + + entity.HasOne(t => t.Identity) + .WithMany() + .HasForeignKey(t => t.IdentityId); + + entity.HasOne(t => t.Owner) + .WithMany() + .HasForeignKey(t => t.OwnerId) + .OnDelete(DeleteBehavior.NoAction); + + entity.HasMany(t => t.Policies) + .WithOne(); + }); + + // Account Logs Model + modelBuilder.Entity(entity => + { + entity.ToTable("account_logs"); + entity.HasOne(t => t.Application) + .WithMany() + .HasForeignKey(t => t.ApplicationId) + .OnDelete(DeleteBehavior.NoAction); + + entity.HasOne(t => t.Account) + .WithMany() + .HasForeignKey(t => t.AccountId) + .OnDelete(DeleteBehavior.NoAction); + }); + + // Policy Model + modelBuilder.Entity(entity => + { + entity.ToTable("policies"); + entity.HasOne(t => t.Owner) + .WithMany() + .HasForeignKey(t => t.OwnerId) + .OnDelete(DeleteBehavior.NoAction); + + entity.Property(e => e.Id).IsRequired(); + }); + + // Policy Model + modelBuilder.Entity(entity => + { + entity.ToTable("temporal_accounts"); + entity.HasIndex(e => e.VerificationCode).IsUnique(); + }); + + // Códigos OTPS. + modelBuilder.Entity(entity => + { + entity.ToTable("otp_codes"); + entity.HasOne(t => t.Account) + .WithMany() + .HasForeignKey(t => t.AccountId); + }); + + // Correos. + modelBuilder.Entity(entity => + { + entity.ToTable("mails"); + entity.HasOne(t => t.Account) + .WithMany() + .HasForeignKey(t => t.AccountId); + + entity.HasIndex(t => t.Mail).IsUnique(); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("policy_types_identity"); + entity.HasOne(t => t.Policy) + .WithMany() + .HasForeignKey(t => t.PolicyId); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("ip_access_policy"); + entity.HasOne(t => t.Policy) + .WithMany() + .HasForeignKey(t => t.PolicyId); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("domains"); + entity.HasOne(t => t.Organization) + .WithMany() + .HasForeignKey(t => t.OrganizationId); + + entity.HasIndex(t => t.Domain).IsUnique(); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("time_access_policy"); + entity.HasOne(t => t.Policy) + .WithMany() + .HasForeignKey(t => t.PolicyId); + }); + + // Mail OTP. + modelBuilder.Entity(entity => + { + entity.ToTable("mail_otp"); + + entity.HasOne(t => t.MailModel) + .WithMany() + .HasForeignKey(t => t.MailId) + .OnDelete(DeleteBehavior.NoAction); + + entity.HasOne(t => t.OtpDatabaseModel) + .WithMany() + .HasForeignKey(t => t.OtpId) + .OnDelete(DeleteBehavior.NoAction); + + entity.HasKey(t => new { t.MailId, t.OtpId }); + + }); + + // Base. + base.OnModelCreating(modelBuilder); + } +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Contexts/SeedContext.cs b/LIN.Cloud.Identity.Persistence/Contexts/SeedContext.cs new file mode 100644 index 0000000..45845ed --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Contexts/SeedContext.cs @@ -0,0 +1,55 @@ +using Newtonsoft.Json; + +namespace LIN.Cloud.Identity.Persistence.Contexts; + +internal class SeedContext +{ + + /// + /// Data primaria. + /// + public static void Seed(DataContext context) + { + // Si no hay cuentas. + if (!context.Accounts.Any()) + { + // Obtener la data. + var jsonData = File.ReadAllText("wwwroot/seeds/users.json"); + var users = JsonConvert.DeserializeObject>(jsonData) ?? []; + + foreach (var user in users) + user.Password = Global.Utilities.Cryptography.Encrypt(user.Password); + + // Agregar los modelos. + if (users != null && users.Count > 0) + { + context.Accounts.AddRange(users); + context.SaveChanges(); + } + } + + // Si no hay aplicaciones. + if (!context.Applications.Any()) + { + // Obtener la data. + var jsonData = File.ReadAllText("wwwroot/seeds/applications.json"); + var apps = JsonConvert.DeserializeObject>(jsonData) ?? []; + + // Formatear modelos. + foreach (var app in apps) + { + app.Identity.Type = Types.Cloud.Identity.Enumerations.IdentityType.Service; + app.Owner = new() { Id = app.OwnerId }; + app.Owner = context.AttachOrUpdate(app.Owner)!; + } + + // Agregar aplicaciones. + if (apps != null && apps.Count > 0) + { + context.Applications.AddRange(apps); + context.SaveChanges(); + } + } + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Extensions/DataBaseExtensions.cs b/LIN.Cloud.Identity.Persistence/Extensions/DataBaseExtensions.cs new file mode 100644 index 0000000..e6d1d93 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Extensions/DataBaseExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; + +namespace LIN.Cloud.Identity.Persistence.Extensions; + +public static class DataBaseExtensions +{ + + /// + /// Obtener transacción. + /// + /// Agregar servicios de persistence. + /// + /// Services. + public static IServiceCollection AddPersistence(this IServiceCollection services, IConfigurationManager configuration) + { + string? connectionName = "cloud-v4"; +#if LOCAL + connectionName = "cloud-v4"; +#elif DEBUG_DEV + connectionName = "cloud-v4"; +#elif RELEASE_DEV + connectionName = "cloud-v4"; +#endif + + services.AddDbContextPool(options => + { + options.UseSqlServer(configuration.GetConnectionString(connectionName)); + }); + + services.AddScoped(); + services.AddScoped(); + + // Servicios de datos. + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } + + + /// + /// Habilitar el servicio de base de datos. + /// + public static IApplicationBuilder UseDataBase(this IApplicationBuilder app) + { + var scope = app.ApplicationServices.CreateScope(); + var logger = scope.ServiceProvider.GetService>(); + try + { + var context = scope.ServiceProvider.GetService(); + bool? created = context?.Database.EnsureCreated(); + + // Crear la base de datos si no existe. + SeedContext.Seed(context!); + } + catch (Exception ex) + { + logger?.LogError(ex, "Error al definir la base de datos."); + } + return app; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Formatters/Account.cs b/LIN.Cloud.Identity.Persistence/Formatters/Account.cs new file mode 100644 index 0000000..f82eea6 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Formatters/Account.cs @@ -0,0 +1,38 @@ +using IdentityService = LIN.Types.Cloud.Identity.Enumerations.IdentityService; + +namespace LIN.Cloud.Identity.Persistence.Formatters; + +public class Account +{ + + /// + /// Procesar el modelo. + /// + /// Modelo + public static AccountModel Process(AccountModel baseAccount) + { + return new AccountModel() + { + Id = 0, + Name = baseAccount.Name.Trim(), + Profile = baseAccount.Profile, + Password = Global.Utilities.Cryptography.Encrypt(baseAccount.Password), + Visibility = baseAccount.Visibility, + IdentityId = 0, + AccountType = baseAccount.AccountType, + Identity = new() + { + Id = 0, + Status = IdentityStatus.Enable, + Provider = IdentityService.LIN, + Type = IdentityType.Account, + CreationTime = DateTime.UtcNow, + EffectiveTime = DateTime.UtcNow, + ExpirationTime = DateTime.UtcNow.AddYears(5), + Roles = [], + Unique = baseAccount.Identity.Unique.Trim() + } + }; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/LIN.Cloud.Identity.Persistence.csproj b/LIN.Cloud.Identity.Persistence/LIN.Cloud.Identity.Persistence.csproj new file mode 100644 index 0000000..4ebd4e3 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/LIN.Cloud.Identity.Persistence.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + Debug;Release;Local;Release-dev;Debug-dev + + + + + + + + + + + + \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Models/MailOtpDatabaseModel.cs b/LIN.Cloud.Identity.Persistence/Models/MailOtpDatabaseModel.cs new file mode 100644 index 0000000..0696da7 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Models/MailOtpDatabaseModel.cs @@ -0,0 +1,9 @@ +namespace LIN.Cloud.Identity.Persistence.Models; + +public class MailOtpDatabaseModel +{ + public MailModel MailModel { get; set; } = null!; + public OtpDatabaseModel OtpDatabaseModel { get; set; } = null!; + public int MailId { get; set; } + public int OtpId { get; set; } +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Models/OtpDatabaseModel.cs b/LIN.Cloud.Identity.Persistence/Models/OtpDatabaseModel.cs new file mode 100644 index 0000000..d2882f2 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Models/OtpDatabaseModel.cs @@ -0,0 +1,11 @@ +namespace LIN.Cloud.Identity.Persistence.Models; + +public class OtpDatabaseModel +{ + public int Id { get; set; } + public string Code { get; set; } = string.Empty; + public DateTime ExpireTime { get; set; } + public bool IsUsed { get; set; } + public LIN.Types.Cloud.Identity.Models.Identities.AccountModel Account { get; set; } = null!; + public int AccountId { get; set; } +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Models/QueryIdentityFilter.cs b/LIN.Cloud.Identity.Persistence/Models/QueryIdentityFilter.cs new file mode 100644 index 0000000..6183641 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Models/QueryIdentityFilter.cs @@ -0,0 +1,8 @@ +namespace LIN.Cloud.Identity.Persistence.Models; + +public class QueryIdentityFilter +{ + public FindOn FindOn { get; set; } + public bool IncludeDates { get; set; } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Models/QueryObjectFilter.cs b/LIN.Cloud.Identity.Persistence/Models/QueryObjectFilter.cs new file mode 100644 index 0000000..7ad8798 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Models/QueryObjectFilter.cs @@ -0,0 +1,19 @@ +namespace LIN.Cloud.Identity.Persistence.Models; + +public class QueryObjectFilter +{ + public int AccountContext { get; set; } + public int IdentityContext { get; set; } + public List OrganizationsDirectories { get; set; } = []; + public bool IsAdmin { get; set; } + public bool IncludePhoto { get; set; } = true; + public bool IncludeIdentity { get; set; } + public FindOn FindOn { get; set; } + +} + +public enum FindOn +{ + StableAccounts, + AllAccounts +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Queries/AccountFindable.cs b/LIN.Cloud.Identity.Persistence/Queries/AccountFindable.cs new file mode 100644 index 0000000..e33d087 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Queries/AccountFindable.cs @@ -0,0 +1,86 @@ +using LIN.Cloud.Identity.Persistence.Queries.Interfaces; + +namespace LIN.Cloud.Identity.Persistence.Queries; + +public class AccountFindable(DataContext context) : IFindable +{ + + /// + /// Buscar en la cuentas estables. + /// + /// Contexto de base de datos. + private IQueryable OnStable() + { + + // Hora actual. + var now = DateTime.UtcNow; + + // Consulta. + var query = from account in context.Accounts + where account.Identity.Status != IdentityStatus.Disable + && account.Identity.EffectiveTime < now && account.Identity.ExpirationTime > now + select account; + + // Retornar. + return query; + } + + + /// + /// Buscar en todas las cuentas. + /// + /// Contexto de base de datos. + private IQueryable OnAll() + { + + // Hora actual. + var now = DateTime.UtcNow; + + // Consulta. + var query = from account in context.Accounts + select account; + + // Retornar. + return query; + } + + + /// + /// Obtener las cuentas según el Id. + /// + /// Id de la cuenta. + /// Filtros. + public IQueryable GetAccounts(int id, QueryObjectFilter filters) + { + // Query general + IQueryable accounts; + + accounts = from account in (filters.FindOn == Models.FindOn.StableAccounts) ? OnStable() : OnAll() + where account.Id == id + select account; + + // Retorno + return accounts; + } + + public IQueryable GetAccounts(string user, QueryObjectFilter filters) + { + + // Query general + IQueryable accounts; + + accounts = from account in (filters.FindOn == Models.FindOn.StableAccounts) ? OnStable() : OnAll() + where account.Identity.Unique == user + select account; + + // Retorno + return accounts; + + } + + + public IQueryable GetAccounts(List ids, Models.QueryObjectFilter filter) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Queries/IdentityFindable.cs b/LIN.Cloud.Identity.Persistence/Queries/IdentityFindable.cs new file mode 100644 index 0000000..a9ba2de --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Queries/IdentityFindable.cs @@ -0,0 +1,21 @@ +using LIN.Cloud.Identity.Persistence.Queries.Interfaces; + +namespace LIN.Cloud.Identity.Persistence.Queries; + +public class IdentityFindable : IFindable +{ + public IQueryable GetAccounts(int id, Models.QueryObjectFilter filter) + { + throw new NotImplementedException(); + } + + public IQueryable GetAccounts(List ids, Models.QueryObjectFilter filter) + { + throw new NotImplementedException(); + } + + public IQueryable GetAccounts(string unique, QueryObjectFilter filter) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Queries/Interfaces/IFindable.cs b/LIN.Cloud.Identity.Persistence/Queries/Interfaces/IFindable.cs new file mode 100644 index 0000000..84ec2ac --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Queries/Interfaces/IFindable.cs @@ -0,0 +1,23 @@ +namespace LIN.Cloud.Identity.Persistence.Queries.Interfaces; + +public interface IFindable +{ + + /// + /// Encontrar por Id. + /// + /// Id único. + public IQueryable GetAccounts(int id, Models.QueryObjectFilter filter); + + /// + /// Encontrar por unique. + /// + public IQueryable GetAccounts(string unique, Models.QueryObjectFilter filter); + + /// + /// Encontrar por Ids. + /// + /// Lista de ids únicos. + public IQueryable GetAccounts(List ids, Models.QueryObjectFilter filter); + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/AccountLogRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/AccountLogRepository.cs new file mode 100644 index 0000000..0c64e1d --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/AccountLogRepository.cs @@ -0,0 +1,107 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class AccountLogRepository(DataContext context) : IAccountLogRepository +{ + + /// + /// Crear un log de inicio de sesión. + /// + /// Modelo. + public async Task Create(AccountLog log) + { + // Formato del modelo. + log.Id = 0; + + try + { + // Organizar el modelo. + log.Account = new() { Id = log.AccountId }; + + // Ya existe. + log.Account = context.AttachOrUpdate(log.Account)!; + + // Si hay una app. + if (log.Application is not null) + log.Application = context.AttachOrUpdate(log.Application); + else + log.ApplicationId = 0; + + // Guardar la cuenta. + await context.AccountLogs.AddAsync(log); + context.SaveChanges(); + + return new(Responses.Success, log.Id); + } + catch (Exception) + { + return new(Responses.Undefined); + } + } + + + /// + /// Obtener los logs de inicio de sesión. + /// + /// Id de la cuenta. + /// Fecha de inicio. + /// Fecha de fin. + public async Task> ReadAll(int accountId, DateTime? start, DateTime? end) + { + try + { + var logs = await (from log in context.AccountLogs + where log.AccountId == accountId + && log.Time > start && log.Time < end + select new AccountLog + { + Id = log.Id, + Time = log.Time, + AccountId = log.AccountId, + Application = new() + { + Name = log.Application!.Name, + IdentityId = log.Application.IdentityId, + }, + ApplicationId = log.ApplicationId, + AuthenticationMethod = log.AuthenticationMethod + }).ToListAsync(); + + return new(Responses.Success, logs); + } + catch (Exception) + { + return new(Responses.Undefined); + } + } + + + /// + /// Contar los logs de autenticación del dia. + /// + /// Id de la cuenta. + public async Task> Count(int id) + { + try + { + // Tiempo. + var time = DateTime.UtcNow; + + // Contar. + int count = await (from a in context.AccountLogs + where a.AccountId == id + && a.AuthenticationMethod == AuthenticationMethods.Authenticator + where a.Time.Year == time.Year + && a.Time.Month == time.Month + && a.Time.Day == time.Day + select a).CountAsync(); + + // Success. + return new(Responses.Success, count); + } + catch (Exception) + { + return new(Responses.NotRows); + } + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/AccountRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/AccountRepository.cs new file mode 100644 index 0000000..9333e18 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/AccountRepository.cs @@ -0,0 +1,261 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class AccountRepository(DataContext context, Queries.AccountFindable accountFindable, IGroupMemberRepository groupMemberRepository) : IAccountRepository +{ + + /// + /// Crear nueva cuenta. + /// + /// Modelo de la cuenta. + /// Id de la organización. + public async Task> Create(AccountModel modelo, int organization) + { + modelo.Id = 0; + modelo.IdentityId = 0; + + // Transacción. + using var transaction = context.Database.GetTransaction(); + + try + { + // Guardar la cuenta. + await context.Accounts.AddAsync(modelo); + context.SaveChanges(); + + // Si la organización existe. + if (organization > 0) + { + + var directory = (from org in context.Organizations + where org.Id == organization + select org.DirectoryId).FirstOrDefault(); + + // Si la organización no existe. + if (directory <= 0) + return new(Responses.NotFoundDirectory) + { + Message = "El directorio no existe" + }; + + // Integrarlo a la organización. + var groupMember = new GroupMember() + { + Group = new() + { + Id = directory + }, + Identity = modelo.Identity, + Type = GroupMemberTypes.User + }; + + // Actualizar la identidad. + await context.Identities.Where(t => t.Id == modelo.Identity.Id) + .ExecuteUpdateAsync(Identities => Identities.SetProperty(t => t.OwnerId, organization)); + + await groupMemberRepository.Create([groupMember]); + } + + // Confirmar los cambios. + transaction?.Commit(); + + return new() + { + Response = Responses.Success, + Model = modelo + }; + } + catch (Exception) + { + transaction?.Rollback(); + return new(Responses.ExistAccount); + } + } + + + /// + /// Obtener una cuenta según el Id. + /// + /// Id de la cuenta. + /// Filtros de búsqueda. + public async Task> Read(int id, QueryObjectFilter filters) + { + try + { + // Consulta de las cuentas. + var account = await accountFindable.GetAccounts(id, filters).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (account is null) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, account); + } + catch (Exception) + { + return new(); + } + + } + + + /// + /// Obtener una cuenta según el identificador único. + /// + /// Único. + /// Filtros de búsqueda. + public async Task> Read(string unique, QueryObjectFilter filters) + { + try + { + // Consulta de las cuentas. + var account = await accountFindable.GetAccounts(unique, filters).IncludeIf(filters.IncludeIdentity, t => t.Include(a => a.Identity)).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (account is null) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, account); + } + catch (Exception) + { + return new(); + } + } + + + /// + /// Obtener una cuenta según el id de la identidad. + /// + /// Id de la identidad. + /// Filtros de búsqueda. + public async Task> ReadByIdentity(int id, QueryObjectFilter filters) + { + try + { + // Consulta de las cuentas. + var account = await Builders.Account.GetAccountsByIdentity(id, filters, context).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (account is null) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, account); + } + catch (Exception) + { + return new(); + } + } + + + /// + /// Buscar por patron. + /// + /// patron de búsqueda + /// Filtros + public async Task> Search(string pattern, QueryObjectFilter filters) + { + try + { + List accountModels = await Builders.Account.Search(pattern, filters, context).Take(10).ToListAsync(); + + // Si no existe el modelo + if (accountModels == null || accountModels.Count == 0) + return new(Responses.NotRows); + + return new(Responses.Success, accountModels); + } + catch (Exception) + { + } + + return new(); + } + + + /// + /// Obtiene los usuarios con IDs coincidentes + /// + /// Lista de IDs + public async Task> FindAll(List ids, QueryObjectFilter filters) + { + + // Ejecución + try + { + + var query = Builders.Account.FindAll(ids, filters, context); + + // Ejecuta + var result = await query.ToListAsync(); + + // Si no existe el modelo + if (result == null || result.Count == 0) + return new(Responses.NotRows); + + return new(Responses.Success, result); + } + catch (Exception) + { + } + + return new(); + } + + + /// + /// Obtiene los usuarios con IDs coincidentes + /// + /// Lista de IDs + public async Task> FindAllByIdentities(List ids, QueryObjectFilter filters) + { + // Ejecución + try + { + var query = Builders.Account.FindAllByIdentities(ids, filters, context); + + // Ejecuta + var result = await query.ToListAsync(); + + // Si no existe el modelo + if (result == null || result.Count == 0) + return new(Responses.NotRows); + + return new(Responses.Success, result); + } + catch (Exception) + { + } + + return new(); + } + + + /// + /// Actualizar contraseña de una cuenta. + /// + /// Id de la cuenta. + /// Nueva contraseña. + public async Task UpdatePassword(int accountId, string password) + { + try + { + var account = await (from a in context.Accounts + where a.Id == accountId + select a).ExecuteUpdateAsync(Accounts => Accounts.SetProperty(t => t.Password, password)); + + if (account <= 0) + return new(Responses.NotExistAccount); + + return new(Responses.Success); + } + catch (Exception) + { + return new(); + } + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/ApplicationRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/ApplicationRepository.cs new file mode 100644 index 0000000..c8b0ffe --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/ApplicationRepository.cs @@ -0,0 +1,111 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class ApplicationRepository(DataContext context) : IApplicationRepository +{ + + /// + /// Crear una nueva aplicación. + /// + /// Modelo. + public async Task Create(ApplicationModel modelo) + { + // Pre. + modelo.Id = 0; + + try + { + // Modelo ya existe. + modelo.Owner = context.AttachOrUpdate(modelo.Owner)!; + + // Guardar la identidad. + await context.Applications.AddAsync(modelo); + context.SaveChanges(); + + return new(Responses.Success, modelo.Id); + } + catch (Exception) + { + return new(Responses.Undefined); + } + } + + + /// + /// Obtener una aplicación. + /// + /// Key de la app. + public async Task> Read(string key) + { + try + { + + // Obtener el modelo. + var application = await (from ar in context.Applications + where ar.Key == Guid.Parse(key) + select ar).FirstOrDefaultAsync(); + + // Success. + return new(application is null ? Responses.NotRows : Responses.Success, application!); + + } + catch (Exception) + { + return new(Responses.Undefined); + } + } + + + /// + /// Obtener una aplicación. + /// + /// Key de la app. + public async Task> Read(int id) + { + try + { + + // Obtener el modelo. + var application = await (from ar in context.Applications + where ar.Id == id + select new ApplicationModel + { + Id = ar.Id, + Name = ar.Name, + Identity = ar.Identity + }).FirstOrDefaultAsync(); + + // Success. + return new(application is null ? Responses.NotRows : Responses.Success, application!); + + } + catch (Exception) + { + return new(Responses.Undefined); + } + } + + + /// + /// Validar si existe una app. + /// + /// Key de la app. + public async Task> ExistApp(string key) + { + try + { + + var exist = await (from ar in context.Applications + where ar.Key == Guid.Parse(key) + select ar).AnyAsync(); + + // Success. + return new(Responses.Success, exist); + + } + catch (Exception) + { + return new(Responses.Undefined); + } + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/Builders/Account.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/Builders/Account.cs new file mode 100644 index 0000000..b2e243e --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/Builders/Account.cs @@ -0,0 +1,280 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework.Builders; + +public class Account +{ + + + /// + /// Buscar en la cuentas estables. + /// + /// Contexto de base de datos. + public static IQueryable OnStable(DataContext context) + { + + // Hora actual. + var now = DateTime.UtcNow; + + // Consulta. + var query = from account in context.Accounts + where account.Identity.Status != IdentityStatus.Disable + && account.Identity.EffectiveTime < now && account.Identity.ExpirationTime > now + select account; + + // Retornar. + return query; + } + + + + /// + /// Buscar en todas las cuentas. + /// + /// Contexto de base de datos. + public static IQueryable OnAll(DataContext context) + { + + // Hora actual. + var now = DateTime.UtcNow; + + // Consulta. + var query = from account in context.Accounts + select account; + + // Retornar. + return query; + } + + + + /// + /// Obtener cuentas. + /// + /// Id de la cuenta + /// Filtros + /// Contexto + public static IQueryable GetAccounts(int id, QueryObjectFilter filters, DataContext context) + { + + // Query general + IQueryable accounts; + + if (filters.FindOn == FindOn.StableAccounts) + accounts = from account in OnStable(context) + where account.Id == id + select account; + else + accounts = from account in OnAll(context) + where account.Id == id + select account; + + // Armar el modelo + accounts = BuildModel(accounts, filters, context); + + // Retorno + return accounts; + + } + + + + /// + /// Obtener cuentas. + /// + /// Identidad unica. + /// Filtros + /// Contexto + public static IQueryable GetAccounts(string user, QueryObjectFilter filters, DataContext context) + { + + // Query general + IQueryable accounts; + + if (filters.FindOn == FindOn.StableAccounts) + accounts = from account in OnStable(context) + where account.Identity.Unique == user + select account; + else + accounts = from account in OnAll(context) + where account.Identity.Unique == user + select account; + + // Armar el modelo + accounts = BuildModel(accounts, filters, context); + + // Retorno + return accounts; + + } + + + + /// + /// Obtener cuentas. + /// + /// Id de la Identidad. + /// Filtros + /// Contexto + public static IQueryable GetAccountsByIdentity(int id, QueryObjectFilter filters, DataContext context) + { + + // Query general + IQueryable accounts; + + if (filters.FindOn == FindOn.StableAccounts) + accounts = from account in OnStable(context) + where account.Identity.Id == id + select account; + else + accounts = from account in OnAll(context) + where account.Identity.Id == id + select account; + + // Armar el modelo + accounts = BuildModel(accounts, filters, context); + + // Retorno + return accounts; + + } + + + + + + public static IQueryable Search(string pattern, QueryObjectFilter filters, DataContext context) + { + + // Query general. + IQueryable accounts = from account in OnStable(context) + where account.Identity.Unique.Contains(pattern) + select account; + + // Armar el modelo. + accounts = BuildModel(accounts, filters, context); + + // Retorno + return accounts; + + } + + + + + public static IQueryable FindAll(IEnumerable ids, QueryObjectFilter filters, DataContext context) + { + IQueryable accounts; + + if (filters.FindOn == FindOn.StableAccounts) + { + accounts = from account in OnStable(context) + where ids.Contains(account.Id) + select account; + } + else + { + accounts = from account in OnAll(context) + where ids.Contains(account.Id) + select account; + } + + + // Armar el modelo. + accounts = BuildModel(accounts, filters, context); + + // Retorno + return accounts; + + } + + + + + + + public static IQueryable FindAllByIdentities(IEnumerable ids, QueryObjectFilter filters, DataContext context) + { + IQueryable accounts; + + if (filters.FindOn == FindOn.StableAccounts) + { + accounts = from account in OnStable(context) + where ids.Contains(account.IdentityId) + select account; + } + else + { + accounts = from account in OnAll(context) + where ids.Contains(account.IdentityId) + select account; + } + + + // Armar el modelo. + accounts = BuildModel(accounts, filters, context); + + // Retorno + return accounts; + + } + + private static readonly string selector = string.Empty; + + + + + + + + /// + /// Construir el modelo. + /// + /// Consulta base. + /// Filtros. + private static IQueryable BuildModel(IQueryable query, QueryObjectFilter filters, DataContext context) + { + + byte[] profile = []; + + try + { + profile = File.ReadAllBytes("wwwroot/user.png"); + } + catch { } + + var queryFinal = from account in query + select new AccountModel + { + Id = account.Id, + Name = filters.IsAdmin + || account.Visibility == Visibility.Visible + || filters.AccountContext == account.Id + || context.GroupMembers.FirstOrDefault(t => t.Group.Members.Any(t => t.IdentityId == filters.IdentityContext)) != null + + ? account.Name + : "Usuario privado", + Identity = new() + { + Id = account.Identity.Id, + Unique = account.Identity.Unique, + CreationTime = account.Identity.CreationTime, + EffectiveTime = account.Identity.EffectiveTime, + ExpirationTime = account.Identity.ExpirationTime, + Status = account.Identity.Status, + Provider = account.Identity.Provider, + }, + Password = account.Password, + Visibility = account.Visibility, + Profile = filters.IncludePhoto ? string.Empty : selector, + IdentityId = account.Identity.Id, + AccountType = account.AccountType + }; + + var s = queryFinal.ToQueryString(); + + return queryFinal; + + } + + + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/Builders/Identity.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/Builders/Identity.cs new file mode 100644 index 0000000..38e63e2 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/Builders/Identity.cs @@ -0,0 +1,130 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework.Builders; + +public class Identities +{ + + /// + /// Buscar en la identidad estables. + /// + /// Contexto de base de datos. + public static IQueryable OnStable(DataContext context) + { + + // Hora actual. + var now = DateTime.UtcNow; + + // Consulta. + var query = from identity in context.Identities + where identity.Status != IdentityStatus.Disable + && identity.Status == IdentityStatus.Enable + && identity.EffectiveTime > now && identity.ExpirationTime < now + select identity; + + // Retornar. + return query; + } + + + /// + /// Buscar en todas las identidades. + /// + /// Contexto de base de datos. + public static IQueryable OnAll(DataContext context) + { + + // Hora actual. + var now = DateTime.UtcNow; + + // Consulta. + var query = from identity in context.Identities + select identity; + + // Retornar. + return query; + } + + + /// + /// Obtener identidades. + /// + /// Id + /// Filtros + /// Contexto + public static IQueryable GetIds(int id, QueryIdentityFilter filters, DataContext context) + { + + // Query general + IQueryable ids; + + if (filters.FindOn == FindOn.StableAccounts) + ids = from identity in OnStable(context) + where identity.Id == id + select identity; + else + ids = from account in OnAll(context) + where account.Id == id + select account; + + // Armar el modelo + ids = BuildModel(ids, filters); + + // Retorno + return ids; + + } + + + /// + /// Obtener identidades. + /// + /// Unique + /// Filtros + /// Contexto + public static IQueryable GetIds(string unique, QueryIdentityFilter filters, DataContext context) + { + + // Query general + IQueryable ids; + + if (filters.FindOn == FindOn.StableAccounts) + ids = from identity in OnStable(context) + where identity.Unique == unique + select identity; + else + ids = from identity in OnAll(context) + where identity.Unique == unique + select identity; + + // Armar el modelo + ids = BuildModel(ids, filters); + + // Retorno + return ids; + + } + + + /// + /// Construir el modelo. + /// + /// Consulta base. + /// Filtros. + private static IQueryable BuildModel(IQueryable query, QueryIdentityFilter filters) + { + + var final = from id in query + select new IdentityModel + { + Status = id.Status, + CreationTime = filters.IncludeDates ? id.CreationTime : default, + EffectiveTime = filters.IncludeDates ? id.EffectiveTime : default, + ExpirationTime = filters.IncludeDates ? id.ExpirationTime : default, + Id = id.Id, + Unique = id.Unique + }; + + return final; + + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/DomainRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/DomainRepository.cs new file mode 100644 index 0000000..648d064 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/DomainRepository.cs @@ -0,0 +1,98 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class DomainRepository(DataContext context) : IDomainRepository +{ + + /// + /// Agregar un dominio. + /// + /// Modelo del dominio.. + public async Task Create(DomainModel modelo) + { + try + { + // La organización ya existe. + modelo.Organization = context.AttachOrUpdate(modelo.Organization); + await context.Domains.AddAsync(modelo); + context.SaveChanges(); + return new(Responses.Success, modelo.Id); + } + catch (Exception) + { + return new(); + } + } + + + /// + /// Obtener un dominio por su unique. + /// + /// Dominio. + public async Task> Read(string unique) + { + try + { + // Consultar. + var domain = await (from g in context.Domains + where g.Domain == unique + select g).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (domain is null) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, domain); + } + catch (Exception) + { + return new(Responses.NotRows); + } + + } + + + /// + /// Obtener los dominios. + /// + /// Id de la organización. + public async Task> ReadAll(int id) + { + try + { + // Consultar. + var domain = await (from g in context.Domains + where g.OrganizationId == id + select g).ToListAsync(); + + // Success. + return new(Responses.Success, domain); + } + catch (Exception) + { + return new(Responses.NotRows); + } + } + + + /// + /// Verificar un dominio por su unique. + /// + public async Task Verify(string unique) + { + try + { + var identityId = await (from g in context.Domains + where g.Domain == unique + select g).ExecuteUpdateAsync(t => t.SetProperty(t => t.IsVerified, true)); + + // Success. + return new(Responses.Success); + } + catch (Exception) + { + return new(); + } + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/GroupMemberRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/GroupMemberRepository.cs new file mode 100644 index 0000000..250de8a --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/GroupMemberRepository.cs @@ -0,0 +1,198 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class GroupMemberRepository(DataContext context) : IGroupMemberRepository +{ + + /// + /// Crear nuevo integrante en un grupo. + /// + /// Modelo. + public async Task Create(GroupMember modelo) + { + try + { + // Ya existen. + modelo.Group = context.AttachOrUpdate(modelo.Group)!; + modelo.Identity = context.AttachOrUpdate(modelo.Identity)!; + + // Guardar la identidad. + await context.GroupMembers.AddAsync(modelo); + context.SaveChanges(); + + return new(Responses.Success); + } + catch (Exception) + { + return new(); + } + + } + + + /// + /// Crear nuevos integrantes en un grupo. + /// + /// Modelos. + public async Task Create(IEnumerable modelos) + { + try + { + + // Validar existencia. + foreach (var member in modelos) + { + try + { + await context.Database.ExecuteSqlRawAsync(""" + INSERT INTO [dbo].[group_members] + ([IdentityId] + ,[GroupId] + ,[Type]) + VALUES + ({0} + ,{1} + ,{2}) + """, member.Identity.Id, member.Group.Id, (int)member.Type); + } + + catch (Exception) + { + } + } + + return new() + { + Response = Responses.Success + }; + + } + catch (Exception) + { + return new() + { + Response = Responses.Undefined + }; + } + + } + + + /// + /// Obtener los integrantes de un grupo. + /// + /// Id del grupo. + public async Task> ReadAll(int id) + { + try + { + // Consulta. + var members = await (from gm in context.GroupMembers + where gm.GroupId == id + select new GroupMember + { + GroupId = gm.GroupId, + Identity = gm.Identity, + Type = gm.Type, + IdentityId = gm.IdentityId + }).ToListAsync(); + + + // Si la cuenta no existe. + if (members is null) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, members); + } + catch (Exception) + { + return new(); + } + } + + + /// + /// Buscar en los integrantes de un grupo. + /// + /// Patron de búsqueda. + /// Id del grupo. + public async Task> Search(string pattern, int group) + { + try + { + // Consulta. + var members = await (from g in context.GroupMembers + where g.GroupId == @group + && g.Identity.Unique.Contains(pattern.ToLower()) + select g.Identity).ToListAsync(); + + // Si la cuenta no existe. + if (members is null) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, members); + } + catch (Exception) + { + return new(); + } + } + + + /// + /// Eliminar un integrante de un grupo. + /// + /// Identidad. + /// Id del grupo. + public async Task Delete(int identity, int group) + { + try + { + var response = await (from g in context.GroupMembers + where g.GroupId == @group + && g.IdentityId == identity + select g).ExecuteDeleteAsync(); + + return new(Responses.Success); + } + catch (Exception) + { + return new(); + } + } + + + /// + /// Obtener los grupos donde una identidad esta de integrante + /// + /// ¿ + public async Task> OnMembers(int organization, int identity) + { + try + { + // Consulta. + var groups = await (from g in context.GroupMembers + where g.Group.Identity.OwnerId == organization + && g.IdentityId == identity + select new GroupModel + { + Id = g.Group.Id, + Identity = g.Group.Identity, + Name = g.Group.Name + }).ToListAsync(); + + // Si la cuenta no existe. + if (groups is null) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, groups); + } + catch (Exception) + { + return new(); + } + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/GroupRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/GroupRepository.cs new file mode 100644 index 0000000..089d41b --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/GroupRepository.cs @@ -0,0 +1,237 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class GroupRepository(DataContext context) : IGroupRepository +{ + + /// + /// Crear nuevo grupo. + /// + /// Modelo. + public async Task> Create(GroupModel modelo) + { + modelo.Id = 0; + + // Transacción. + using var transaction = context.Database.BeginTransaction(); + + try + { + // Miembros. + foreach (var e in modelo.Members) + { + e.Group = modelo; + e.Identity = context.AttachOrUpdate(e.Identity)!; + } + + // Fijar la organización. + modelo.Identity.Owner = new() + { + Id = modelo.Identity.OwnerId ?? 0 + }; + + modelo.Identity.Owner = context.AttachOrUpdate(modelo.Identity.Owner); + + // Guardar la identidad. + await context.Groups.AddAsync(modelo); + + // Obtener el directorio general. + var generalGroupInformation = (from org in context.Organizations + where org.Id == modelo.Identity.OwnerId + select new { org.DirectoryId, org.Directory.Identity.Unique }).FirstOrDefault(); + + // Si no se encontró el directorio. + if (generalGroupInformation is null) + { + transaction.Rollback(); + return new(Responses.NotRows); + } + + // Nueva identidad. + modelo.Identity.Unique = $"{modelo.Identity.Unique}@{generalGroupInformation.Unique}"; + + // Guardar. + context.SaveChanges(); + + // Agregar el grupo al directorio general. + var generalDirectory = new GroupModel + { + Id = generalGroupInformation.DirectoryId + }; + + // Si no se encontró el directorio. + generalDirectory = context.AttachOrUpdate(generalDirectory); + + context.GroupMembers.Add(new() + { + Group = generalDirectory!, + Identity = modelo.Identity, + Type = GroupMemberTypes.Group + }); + + context.SaveChanges(); + transaction.Commit(); + + return new(Responses.Success, modelo); + } + catch (Exception) + { + transaction.Rollback(); + return new(); + } + } + + + /// + /// Obtener un grupo según el Id. + /// + /// Id. + public async Task> Read(int id) + { + try + { + // Consulta. + var group = await (from g in context.Groups + where g.Id == id + select new GroupModel + { + Id = g.Id, + Identity = g.Identity, + Name = g.Name, + IdentityId = g.IdentityId, + Description = g.Description + }).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (group is null) + return new(Responses.NotRows); + + return new(Responses.Success, group); + } + catch (Exception) + { + return new(); + } + + } + + + /// + /// Obtener un grupo según el Id de la identidad. + /// + /// Identidad. + public async Task> ReadByIdentity(int id) + { + try + { + // Consulta. + var group = await (from g in context.Groups + where g.IdentityId == id + select new GroupModel + { + Id = g.Id, + Identity = g.Identity, + Name = g.Name, + IdentityId = g.IdentityId, + Description = g.Description + }).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (group is null) + return new(Responses.NotRows); + + return new(Responses.Success, group); + } + catch (Exception) + { + return new(); + } + + } + + + /// + /// Obtener los grupos asociados a una organización. + /// + /// Organización. + public async Task> ReadAll(int organization) + { + try + { + // Consulta. + var groups = await (from g in context.Groups + where g.Identity.OwnerId == organization + select new GroupModel + { + Id = g.Id, + Identity = g.Identity, + Name = g.Name + }).ToListAsync(); + + // Success. + return new(Responses.Success, groups ?? []); + } + catch (Exception) + { + return new(); + } + } + + + /// + /// Obtener la organización propietaria de un grupo. + /// + /// Id del grupo. + public async Task> GetOwner(int id) + { + try + { + + // Consulta. + var ownerId = await (from g in context.Groups + where g.Id == id + select g.Identity.OwnerId).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (ownerId is null || ownerId.Value <= 0) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, ownerId ?? 0); + } + catch (Exception) + { + return new(); + } + + } + + + /// + /// Obtener la organización propietaria de un grupo. + /// + /// Id de la identidad. + public async Task> GetOwnerByIdentity(int id) + { + try + { + + // Consulta. + var ownerId = await (from g in context.Groups + where g.IdentityId == id + select g.Identity.OwnerId).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (ownerId is null || ownerId.Value <= 0) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, ownerId ?? 0); + } + catch (Exception) + { + return new(); + } + + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/IdentityRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/IdentityRepository.cs new file mode 100644 index 0000000..c875c75 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/IdentityRepository.cs @@ -0,0 +1,103 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class IdentityRepository(DataContext context) : IIdentityRepository +{ + + /// + /// Crear nueva identidad. + /// + public async Task> Create(IdentityModel modelo) + { + modelo.Id = 0; + try + { + foreach (var rol in modelo.Roles) + rol.Identity = modelo; + + // Organización propietaria. + if (modelo.Owner is not null) + modelo.Owner = context.AttachOrUpdate(modelo.Owner); + + // Guardar la identidad. + await context.Identities.AddAsync(modelo); + context.SaveChanges(); + + return new(Responses.Success, modelo); + } + catch (Exception) + { + return new(Responses.ResourceExist); + } + } + + + /// + /// Obtener una identidad según el Id. + /// + /// Id de la identidad. + /// Filtros. + public async Task> Read(int id, QueryIdentityFilter filters) + { + try + { + // Consulta de las cuentas. + var identity = await Builders.Identities.GetIds(id, filters, context).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (identity == null) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, identity); + } + catch (Exception) + { + return new(Responses.ExistAccount); + } + + } + + + /// + /// Validar si existe una identidad según el Unique. + /// + public async Task> Exist(string unique) + { + try + { + bool exist = await context.Identities.AnyAsync(x => x.Unique == unique); + return new(Responses.Success, exist); + } + catch (Exception) + { + return new(Responses.Undefined); + } + } + + + /// + /// Obtener una identidad según el Unique. + /// + /// Unique. + /// Filtros de búsqueda. + public async Task> Read(string unique, QueryIdentityFilter filters) + { + try + { + // Consulta de las cuentas. + var identity = await Builders.Identities.GetIds(unique, filters, context).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (identity == null) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, identity); + } + catch (Exception) + { + return new(Responses.ExistAccount); + } + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/IdentityRolesRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/IdentityRolesRepository.cs new file mode 100644 index 0000000..eb85191 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/IdentityRolesRepository.cs @@ -0,0 +1,143 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class IdentityRolesRepository(DataContext context) : IIdentityRolesRepository +{ + + /// + /// Crear nuevo rol en identidad. + /// + /// Modelo. + public async Task Create(IdentityRolesModel modelo) + { + try + { + // Attach. + context.Attach(modelo.Identity); + context.Attach(modelo.Organization); + + // Guardar la identidad. + await context.IdentityRoles.AddAsync(modelo); + context.SaveChanges(); + + return new(Responses.Success); + } + catch (Exception) + { + return new(); + } + + } + + + /// + /// Obtener los roles asociados a una identidad en una organización determinada. + /// + /// Identidad. + /// Organización. + public async Task> ReadAll(int identity, int organization) + { + try + { + List Roles = []; + + await RolesOn(identity, organization, [], Roles); + + return new(Responses.Success, Roles); + } + catch (Exception) + { + return new(); + } + } + + + /// + /// Eliminar el rol de una identidad en una organización. + /// + /// Identidad. + /// Rol. + /// Organización. + public async Task Remove(int identity, Roles rol, int organization) + { + try + { + + // Ejecutar eliminación. + var count = await (from ir in context.IdentityRoles + where ir.IdentityId == identity + && ir.Rol == rol + && ir.OrganizationId == organization + select ir).ExecuteDeleteAsync(); + + return new(Responses.Success); + } + catch (Exception) + { + return new(); + } + + } + + + /// + /// Obtener y buscar las identidades y roles en forma jerárquica. + /// + /// Identidad base. + /// Organización. + /// Identidades recolectadas. + /// Roles recolectados. + private async Task RolesOn(int identity, int organization, List ids, List roles) + { + + var query = from id in context.Identities + where id.Id == identity + select new + { + Id = new { id.Unique, id.ExpirationTime, id.CreationTime }, + In = (from member in context.GroupMembers + where !ids.Contains(member.Group.IdentityId) + && member.IdentityId == identity + select member.Group.IdentityId).ToList(), + + Roles = (from IR in context.IdentityRoles + where IR.IdentityId == identity + where IR.OrganizationId == organization + select IR.Rol).ToList() + }; + + + // Si hay elementos. + if (query.Any()) + { + + // Ejecuta la consulta. + var local = query.ToList(); + + // Obtiene los roles. + var localRoles = local.SelectMany(t => t.Roles); + + // Obtiene las bases. + var bases = local.SelectMany(t => t.In); + + // Agregar a los objetos. + roles.AddRange(localRoles.Select(t => new IdentityRolesModel + { + Identity = new() + { + Id = identity, + Unique = local[0].Id.Unique, + CreationTime = local[0].Id.CreationTime, + ExpirationTime = local[0].Id.ExpirationTime, + }, + Rol = t + })); + + ids.AddRange(bases); + + // Recorrer. + foreach (var @base in bases) + await RolesOn(@base, organization, ids, roles); + } + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/MailRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/MailRepository.cs new file mode 100644 index 0000000..44797fd --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/MailRepository.cs @@ -0,0 +1,100 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class MailRepository(DataContext context) : IMailRepository +{ + + /// + /// Crear un nuevo mail. + /// + public async Task> Create(MailModel model) + { + try + { + + model.Id = 0; + model.Account = new() + { + Id = model.AccountId, + }; + + // Attach. + context.Attach(model.Account); + + // Guardar la cuenta. + await context.Mails.AddAsync(model); + context.SaveChanges(); + + return new(Responses.Success, model); + } + catch (Exception) + { + return new(Responses.ResourceExist); + } + } + + + /// + /// Obtener el correo principal de una identidad. + /// + public async Task> ReadPrincipal(string unique) + { + try + { + + var mailModel = await (from mail in context.Mails + join account in context.Accounts + on mail.Account.IdentityId equals account.IdentityId + where account.Identity.Unique == unique + where mail.IsPrincipal + && mail.IsVerified + select mail).FirstOrDefaultAsync(); + + if (mailModel is null) + return new(Responses.NotRows); + + return new(Responses.Success, mailModel); + } + catch (Exception) + { + return new(); + } + + } + + + /// + /// Validar un código OTP para un correo. + /// + /// Correo electrónico. + /// Código OTP. + public async Task ValidateOtpForMail(string email, string code) + { + try + { + // Obtener model. + var otpModel = (from mail in context.Mails + where mail.Mail == email + join otp in context.MailOtp + on mail.Id equals otp.MailId + where otp.OtpDatabaseModel.Code == code + && otp.OtpDatabaseModel.IsUsed == false + && otp.OtpDatabaseModel.ExpireTime > DateTime.UtcNow + select otp); + + // Actualizar. + await otpModel.Select(t => t.MailModel).ExecuteUpdateAsync(t => t.SetProperty(t => t.IsVerified, true)); + int countUpdate = await otpModel.Select(t => t.OtpDatabaseModel).ExecuteUpdateAsync(t => t.SetProperty(t => t.IsUsed, true)); + + // Si no se actualizaron. + if (countUpdate <= 0) + return new(Responses.NotRows); + + return new(Responses.Success); + } + catch (Exception) + { + return new(Responses.Undefined); + } + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/OrganizationMemberRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/OrganizationMemberRepository.cs new file mode 100644 index 0000000..e1ccee8 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/OrganizationMemberRepository.cs @@ -0,0 +1,217 @@ +using LIN.Types.Cloud.Identity.Abstracts; + +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class OrganizationMemberRepository(DataContext context) : IOrganizationMemberRepository +{ + + /// + /// Valida si una identidad es miembro de una organización. + /// + /// Identidad. + /// Id de la organización. + public async Task> IamIn(int id, int organization) + { + try + { + // Consulta. + var query = await (from org in context.Organizations + where org.Id == organization + join gm in context.GroupMembers + on org.DirectoryId equals gm.GroupId + where gm.IdentityId == id + select new + { + gm.Type + }).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (query is null) + { + + var directory = await (from A in context.Organizations + where A.Directory.IdentityId == id + && A.Id == organization + select A).AnyAsync(); + + if (!directory) + return new(Responses.NotRows); + } + + + // Success. + return new(Responses.Success, query?.Type ?? GroupMemberTypes.Group); + } + catch (Exception) + { + return new(); + } + + } + + + /// /// + /// Valida si una lista de identidades son miembro de una organización. + /// + /// Identidades + /// Id de la organización + /// Contexto + public async Task<(IEnumerable success, List failure)> IamIn(IEnumerable ids, int organization) + { + + try + { + + // Consulta. + var query = await (from org in context.Organizations + where org.Id == organization + join gm in context.GroupMembers + on org.DirectoryId equals gm.GroupId + where ids.Contains(gm.IdentityId) + select gm.IdentityId).ToListAsync(); + + // Lista. + List success = [.. query]; + List failure = [.. ids.Except(success)]; + + return (success, failure); + } + catch (Exception) + { + } + return ([], []); + } + + + /// + /// Expulsar identidades de la organización. + /// + /// Lista de identidades. + /// Id de la organización. + /// Respuesta del proceso. + public async Task Expulse(IEnumerable ids, int organization) + { + try + { + // Desactivar identidades (Solo creadas dentro de la propia organización). + var baseQuery = (from member in context.GroupMembers + where ids.Contains(member.IdentityId) + join org in context.Organizations + on member.Identity.OwnerId equals org.Id + where org.Id == organization + select member); + + // Desactivar identidades (Solo creadas dentro de la propia organización). + await baseQuery.Where(m => m.Type != GroupMemberTypes.Guest).Select(m => m.Identity).ExecuteUpdateAsync(t => t.SetProperty(t => t.Status, IdentityStatus.Disable)); + + // Eliminar accesos (Tanto propios de la organización como los externos). + await baseQuery.ExecuteDeleteAsync(); + + // Eliminar roles asociados. + await (from rol in context.IdentityRoles + where ids.Contains(rol.IdentityId) + && rol.OrganizationId == organization + select rol).ExecuteDeleteAsync(); + + // Success. + return new(Responses.Success); + } + catch (Exception) + { + return new(); + } + + } + + + /// + /// Obtener los integrantes de una organización. + /// + /// Id de la organización. + public async Task> ReadAll(int id) + { + try + { + // Consulta. + var query = await (from gm in context.GroupMembers + where gm.Group.Identity.OwnerId == id + select gm).ToListAsync(); + + // Success. + return new(Responses.Success, query); + } + catch (Exception) + { + return new(); + } + } + + + /// + /// Obtener las organizaciones donde una identidad es integrante + /// + /// Id de la identidad. + public async Task> ReadAllMembers(int identity) + { + try + { + // Consulta. + var query = await (from org in context.Organizations + join gm in context.GroupMembers + on org.DirectoryId equals gm.GroupId + where gm.IdentityId == identity + select org).ToListAsync(); + + // Success. + return new(Responses.Success, query); + } + catch (Exception) + { + return new(); + } + } + + + /// + /// Obtener las cuentas de usuarios de una organización. + /// + /// Id de la organización. + public async Task>> ReadUserAccounts(int id) + { + try + { + var members = await (from org in context.Organizations + where org.Id == id + join gm in context.GroupMembers + on org.DirectoryId equals gm.GroupId + join a in context.Accounts + on gm.IdentityId equals a.IdentityId + select new SessionModel + { + Account = new() + { + Id = a.Id, + Name = a.Name, + Visibility = a.Visibility, + Identity = new() + { + Id = a.Identity.Id, + Unique = a.Identity.Unique, + Provider = a.Identity.Provider, + } + }, + Profile = gm + }).ToListAsync(); + + // Success. + return new(Responses.Success, members); + } + catch (Exception) + { + } + + return new(); + + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/OrganizationRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/OrganizationRepository.cs new file mode 100644 index 0000000..44e8b78 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/OrganizationRepository.cs @@ -0,0 +1,200 @@ +using LIN.Cloud.Identity.Persistence.Formatters; + +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class OrganizationRepository(DataContext context) : IOrganizationRepository +{ + + /// + /// Crear nueva organización. + /// + /// Modelo de la organización. + public async Task Create(OrganizationModel modelo) + { + // Aislar el contexto de la base de datos. + using var transaction = context.Database.BeginTransaction(); + + try + { + // Metadata. + modelo.Directory.Name = "Directorio General"; + modelo.Directory.Description = "Directorio General de la organización"; + + modelo.Directory.Identity.Type = IdentityType.Group; + modelo.Directory.Identity.Owner = null; + modelo.Directory.Identity.OwnerId = null; + // Agregar la organización. + await context.Organizations.AddAsync(modelo); + context.SaveChanges(); + modelo.Directory.Identity.Owner = modelo; + context.SaveChanges(); + + // Crear la cuenta administrativa. + var account = new AccountModel() + { + Id = 0, + Visibility = Visibility.Hidden, + Name = "Admin", + Password = $"pwd@{DateTime.UtcNow.Year}", + Identity = new IdentityModel() + { + Provider = IdentityService.LIN, + Status = IdentityStatus.Enable, + CreationTime = DateTime.UtcNow, + EffectiveTime = DateTime.UtcNow, + ExpirationTime = DateTime.UtcNow.AddYears(10), + Unique = $"admin@{modelo.Directory.Identity.Unique}" + } + }; + + // Formatear la cuenta. + account = Account.Process(account); + + await context.Accounts.AddAsync(account); + + context.SaveChanges(); + + // IamRoles. + var rol = new IdentityRolesModel + { + Identity = account.Identity, + Organization = modelo, + Rol = Roles.Administrator + }; + + await context.IdentityRoles.AddAsync(rol); + + context.SaveChanges(); + + modelo.Directory.Identity.Owner = modelo; + account.Identity.Owner = modelo; + modelo.Directory.Members.Add(new() + { + Group = modelo.Directory, + Identity = account.Identity, + Type = GroupMemberTypes.User + }); + + context.SaveChanges(); + transaction.Commit(); + + return new(Responses.Success, modelo.Id); + + } + catch (Exception) + { + transaction.Rollback(); + return new(); + } + + } + + + /// + /// Obtener una organización por su Id. + /// + /// Id de la organización. + public async Task> Read(int id) + { + try + { + // Consultar. + var org = await (from g in context.Organizations + where g.Id == id + select g).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (org is null) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, org); + } + catch (Exception) + { + return new(Responses.ExistAccount); + } + + } + + + /// + /// Obtener el dominio (Identidad principal) de la organización. + /// + /// Id de la organización. + public async Task> GetDomain(int id) + { + try + { + var org = await (from g in context.Organizations + where g.Id == id + select g.Directory.Identity).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (org is null) + return new(Responses.NotRows); + + return new(Responses.Success, org); + } + catch (Exception) + { + return new(Responses.ExistAccount); + } + + } + + + /// + /// Obtener el id del directorio (Grupo principal) de la organización. + /// + /// Id de la organización. + public async Task> ReadDirectory(int id) + { + try + { + var groupId = await (from g in context.Organizations + where g.Id == id + select g.DirectoryId).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (groupId <= 0) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, groupId); + } + catch (Exception) + { + return new(); + } + + } + + + /// + /// Obtener id de la identidad del directorio (Grupo principal) de la organización. + /// + /// Id de la organización. + public async Task> ReadDirectoryIdentity(int id) + { + try + { + var identityId = await (from g in context.Organizations + where g.Id == id + select g.Directory.IdentityId).FirstOrDefaultAsync(); + + // Si la cuenta no existe. + if (identityId <= 0) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, identityId); + } + catch (Exception) + { + return new(); + } + + } + +} diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/OtpRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/OtpRepository.cs new file mode 100644 index 0000000..db71ca4 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/OtpRepository.cs @@ -0,0 +1,90 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +public class OtpRepository(DataContext context) : IOtpRepository +{ + + /// + /// Crear OTP. + /// + public async Task Create(OtpDatabaseModel model) + { + try + { + model.Account = context.AttachOrUpdate(model.Account); + + // Guardar OTP. + await context.OTPs.AddAsync(model); + context.SaveChanges(); + + return new(Responses.Success); + } + catch (Exception) + { + } + return new(Responses.Undefined); + } + + + /// + /// Crear OTP. + /// + public async Task Create(MailOtpDatabaseModel model) + { + try + { + model.OtpDatabaseModel.Account = context.AttachOrUpdate(model.OtpDatabaseModel.Account); + + model.MailModel = new() + { + Id = model.MailModel.Id + }; + model.MailModel = context.AttachOrUpdate(model.MailModel); + + // Guardar OTP. + await context.MailOtp.AddAsync(model); + context.SaveChanges(); + + return new(Responses.Success); + } + catch (Exception) + { + } + return new(Responses.Undefined); + } + + + /// + /// Leer y actualizar el estado del otp. + /// + /// Cuenta. + /// Código. + public async Task ReadAndUpdate(int accountId, string code) + { + + try + { + + + var update = await (from A in context.OTPs + where A.AccountId == accountId + && A.Code == code + && A.ExpireTime > DateTime.UtcNow + && A.IsUsed == false + select A).ExecuteUpdateAsync(t => t.SetProperty(t => t.IsUsed, true)); + + + if (update <= 0) + return new(Responses.NotRows); + + + return new(Responses.Success); + + } + catch (Exception) + { + } + return new(Responses.Undefined); + + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/PolicyMemberRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/PolicyMemberRepository.cs new file mode 100644 index 0000000..6994e1c --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/PolicyMemberRepository.cs @@ -0,0 +1,53 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class PolicyMemberRepository(DataContext context) : IPolicyMemberRepository +{ + + /// + /// Crear nueva política de acceso general. + /// + public async Task Create(IdentityPolicyModel model) + { + try + { + model.Identity = context.AttachOrUpdate(model.Identity)!; + model.Policy = context.AttachOrUpdate(model.Policy)!; + + // Guardar la cuenta. + await context.IdentityPolicies.AddAsync(model); + context.SaveChanges(); + + return new() + { + Response = Responses.Success + }; + } + catch (Exception) + { + } + return new(); + } + + + public async Task> ReadAll(int id) + { + try + { + + var identities = await (from pl in context.IdentityPolicies + where pl.IdentityId == id + select pl.Policy).ToListAsync(); + + return new() + { + Response = Responses.Success, + Models = identities + }; + } + catch (Exception) + { + } + return new(); + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/PolicyRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/PolicyRepository.cs new file mode 100644 index 0000000..5a81c20 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/PolicyRepository.cs @@ -0,0 +1,252 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class PolicyRepository(DataContext context) : IPolicyRepository +{ + + /// + /// Crear nueva política de acceso general. + /// + public async Task Create(PolicyModel model) + { + try + { + model.CreatedBy = context.AttachOrUpdate(model.CreatedBy)!; + model.Owner = context.AttachOrUpdate(model.Owner); + + foreach (var e in model.TimeAccessPolicies) + { + e.Policy = model; + } + + foreach (var e in model.IdentityTypePolicies) + { + e.Policy = model; + } + + // Guardar la cuenta. + await context.Policies.AddAsync(model); + context.SaveChanges(); + + return new() + { + Response = Responses.Success, + LastId = model.Id + }; + } + catch (Exception) + { + } + return new(); + } + + + /// + /// Agregar una política de acceso por tiempo. + /// + public async Task Add(TimeAccessPolicy policyModel) + { + try + { + policyModel.Policy = context.AttachOrUpdate(policyModel.Policy)!; + + context.TimeAccessPolicies.Add(policyModel); + await context.SaveChangesAsync(); + + return new(Responses.Success, policyModel.Id); + } + catch (Exception) + { + } + return new(); + } + + + /// + /// Agregar una política de acceso por IP. + /// + public async Task Add(IpAccessPolicy policyModel) + { + try + { + policyModel.Policy = context.AttachOrUpdate(policyModel.Policy)!; + + context.IpAccessPolicies.Add(policyModel); + await context.SaveChangesAsync(); + + return new(Responses.Success, policyModel.Id); + } + catch (Exception) + { + } + return new(); + } + + + /// + /// Agregar una política de acceso por tipo de identidad. + /// + public async Task Add(IdentityTypePolicy policyModel) + { + try + { + policyModel.Policy = context.AttachOrUpdate(policyModel.Policy)!; + + context.IdentityTypesPolicies.Add(policyModel); + await context.SaveChangesAsync(); + + return new(Responses.Success, policyModel.Id); + } + catch (Exception) + { + } + return new(); + } + + + /// + /// Eliminar una política de acceso. + /// + /// Id de la política. + public async Task Delete(int id) + { + try + { + // Eliminar las políticas de acceso. + await context.IpAccessPolicies.Where(x => x.PolicyId == id).ExecuteDeleteAsync(); + await context.IdentityTypesPolicies.Where(x => x.PolicyId == id).ExecuteDeleteAsync(); + await context.TimeAccessPolicies.Where(x => x.PolicyId == id).ExecuteDeleteAsync(); + await context.Policies.Where(x => x.Id == id).ExecuteDeleteAsync(); + + return new(Responses.Success); + } + catch (Exception) + { + } + return new(); + } + + + /// + /// Obtener una política de acceso por organization. + /// + /// Id de la política. + public async Task> ReadAll(int organization, bool includeDetails) + { + try + { + var model = await (from p in context.Policies + where p.OwnerId == organization + select p) + .IncludeIf(includeDetails, t => t.Include(t => t.TimeAccessPolicies)) + .IncludeIf(includeDetails, t => t.Include(t => t.IpAccessPolicies)) + .IncludeIf(includeDetails, t => t.Include(t => t.IdentityTypePolicies)).ToListAsync(); + + return new(Responses.Success, model); + } + catch (Exception) + { + } + return new(); + } + + + /// + /// Buscar políticas por nombre. + /// + /// Id de la política. + public async Task> ReadAll(int organization, string query) + { + try + { + var model = await (from p in context.Policies + where p.OwnerId == organization + && (p.Name.Contains(query) || string.IsNullOrWhiteSpace(p.Name)) + select p).ToListAsync(); + + return new(Responses.Success, model); + } + catch (Exception) + { + } + return new(); + } + + + /// + /// Obtener una política de acceso por organization. + /// + /// Id de la política. + public async Task> ReadAllByApp(int application, bool includeDetails) + { + try + { + + var model = await context.Applications + .Where(p => p.Id == application) + .SelectMany(p => p.Policies) + .IncludeIf(includeDetails, t => t.Include(t => t.TimeAccessPolicies)) + .IncludeIf(includeDetails, t => t.Include(t => t.IpAccessPolicies)) + .IncludeIf(includeDetails, t => t.Include(t => t.IdentityTypePolicies)) + .ToListAsync(); + + return new(Responses.Success, model); + } + catch (Exception) + { + } + return new(); + } + + + /// + /// Obtener una política de acceso por organization. + /// + /// Id de la política. + public async Task> ReadAll(IEnumerable identities, int organization, bool includeDetails) + { + try + { + var model = await context.IdentityPolicies + .Where(p => p.Identity.OwnerId == organization && identities.Contains(p.IdentityId)) + .IncludeIf(includeDetails, t => t.Include(t => t.Policy).ThenInclude(p => p.TimeAccessPolicies)) + .IncludeIf(includeDetails, t => t.Include(t => t.Policy).ThenInclude(p => p.IpAccessPolicies)) + .IncludeIf(includeDetails, t => t.Include(t => t.Policy).ThenInclude(p => p.IdentityTypePolicies)) + .Select(p => p.Policy) + .ToListAsync(); + + return new(Responses.Success, model); + } + catch (Exception) + { + } + return new(); + } + + /// + /// Obtener una política de acceso por id. + /// + /// Id de la política. + public async Task> Read(int id, bool includeDetails) + { + try + { + var model = await (from p in context.Policies + where p.Id == id + select p) + .IncludeIf(includeDetails, t => t.Include(t => t.TimeAccessPolicies)) + .IncludeIf(includeDetails, t => t.Include(t => t.IpAccessPolicies)) + .IncludeIf(includeDetails, t => t.Include(t => t.IdentityTypePolicies)) + .FirstOrDefaultAsync(); + + if (model is null) + return new(Responses.NotRows); + + return new(Responses.Success, model); + } + catch (Exception) + { + } + return new(); + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/TemporalAccountRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/TemporalAccountRepository.cs new file mode 100644 index 0000000..8be4185 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/EntityFramework/TemporalAccountRepository.cs @@ -0,0 +1,71 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories.EntityFramework; + +internal class TemporalAccountRepository(DataContext context) : ITemporalAccountRepository +{ + + /// + /// Crear nueva cuenta temporal. + /// + public async Task Create(TemporalAccountModel modelo) + { + modelo.Id = 0; + try + { + // Guardar la identidad. + await context.TemporalAccounts.AddAsync(modelo); + context.SaveChanges(); + + return new(Responses.Success, modelo.Id); + } + catch (Exception) + { + return new(Responses.ResourceExist); + } + } + + + /// + /// Obtener una cuenta temporal por código de verificación. + /// + /// Código temporal. + public async Task> ReadWithCode(string verificationCode) + { + try + { + var temporalAccount = await context.TemporalAccounts + .FirstOrDefaultAsync(x => x.VerificationCode == verificationCode); + + // Si la cuenta no existe. + if (temporalAccount == null) + return new(Responses.NotRows); + + // Success. + return new(Responses.Success, temporalAccount); + } + catch (Exception) + { + return new(); + } + } + + + /// + /// Eliminar una cuenta temporal. + /// + /// Id de la cuenta temporal. + public async Task Delete(int id) + { + try + { + var count = await context.TemporalAccounts.Where(t => t.Id == id).ExecuteDeleteAsync(); + + // Success. + return new(Responses.Success); + } + catch (Exception) + { + return new(); + } + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IAccountLogRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IAccountLogRepository.cs new file mode 100644 index 0000000..8fdc8b9 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IAccountLogRepository.cs @@ -0,0 +1,8 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IAccountLogRepository +{ + Task Create(AccountLog log); + Task> ReadAll(int accountId, DateTime? start, DateTime? end); + Task> Count(int id); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IAccountRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IAccountRepository.cs new file mode 100644 index 0000000..122c49c --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IAccountRepository.cs @@ -0,0 +1,20 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IAccountRepository +{ + Task> Create(AccountModel modelo, int organization); + + Task> Read(int id, QueryObjectFilter filters); + + Task> Read(string unique, QueryObjectFilter filters); + + Task> ReadByIdentity(int id, QueryObjectFilter filters); + + Task> Search(string pattern, QueryObjectFilter filters); + + Task> FindAll(List ids, QueryObjectFilter filters); + + Task> FindAllByIdentities(List ids, QueryObjectFilter filters); + + Task UpdatePassword(int accountId, string password); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IApplicationRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IApplicationRepository.cs new file mode 100644 index 0000000..c27c930 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IApplicationRepository.cs @@ -0,0 +1,9 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IApplicationRepository +{ + Task Create(ApplicationModel modelo); + Task> Read(string key); + Task> Read(int key); + Task> ExistApp(string key); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IDomainRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IDomainRepository.cs new file mode 100644 index 0000000..c6586d1 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IDomainRepository.cs @@ -0,0 +1,9 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IDomainRepository +{ + Task Create(DomainModel modelo); + Task> Read(string unique); + Task> ReadAll(int id); + Task Verify(string unique); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IGroupMemberRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IGroupMemberRepository.cs new file mode 100644 index 0000000..ad3df5a --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IGroupMemberRepository.cs @@ -0,0 +1,11 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IGroupMemberRepository +{ + Task Create(GroupMember modelo); + Task Create(IEnumerable modelos); + Task> ReadAll(int id); + Task> Search(string pattern, int group); + Task Delete(int identity, int group); + Task> OnMembers(int organization, int identity); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IGroupRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IGroupRepository.cs new file mode 100644 index 0000000..88b9841 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IGroupRepository.cs @@ -0,0 +1,41 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IGroupRepository +{ + + /// + /// Crear nuevo grupo. + /// + Task> Create(GroupModel modelo); + + /// + /// Obtener un grupo según el Id. + /// + /// Id del grupo. + Task> Read(int id); + + /// + /// Obtener un grupo según el Id de la identidad. + /// + /// Identidad. + Task> ReadByIdentity(int id); + + /// + /// Obtener los grupos asociados a una organización. + /// + /// Organización. + Task> ReadAll(int organization); + + /// + /// Obtener la organización propietaria de un grupo. + /// + /// Id del grupo. + Task> GetOwner(int id); + + /// + /// Obtener la organización propietaria de un grupo. + /// + /// Id de la identidad del grupo. + Task> GetOwnerByIdentity(int id); + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IIdentityRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IIdentityRepository.cs new file mode 100644 index 0000000..857edb8 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IIdentityRepository.cs @@ -0,0 +1,9 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IIdentityRepository +{ + Task> Create(IdentityModel modelo); + Task> Read(int id, QueryIdentityFilter filters); + Task> Exist(string unique); + Task> Read(string unique, QueryIdentityFilter filters); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IIdentityRolesRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IIdentityRolesRepository.cs new file mode 100644 index 0000000..2f80ad2 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IIdentityRolesRepository.cs @@ -0,0 +1,8 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IIdentityRolesRepository +{ + Task Create(IdentityRolesModel modelo); + Task> ReadAll(int identity, int organization); + Task Remove(int identity, Roles rol, int organization); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IMailRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IMailRepository.cs new file mode 100644 index 0000000..9fdd80e --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IMailRepository.cs @@ -0,0 +1,8 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IMailRepository +{ + Task> Create(MailModel model); + Task> ReadPrincipal(string unique); + Task ValidateOtpForMail(string email, string code); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IOrganizationMemberRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IOrganizationMemberRepository.cs new file mode 100644 index 0000000..2c3f80e --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IOrganizationMemberRepository.cs @@ -0,0 +1,51 @@ +using LIN.Types.Cloud.Identity.Abstracts; + +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IOrganizationMemberRepository +{ + + /// + /// Obtener organizaciones donde una identidad es integrante. + /// + /// Id de la identidad. + Task> ReadAll(int id); + + + /// + /// Validar si una identidad es integrante de una organización. + /// + /// Id de la identidad. + /// Id de la organización. + Task> IamIn(int id, int organization); + + + /// + /// Validar si una identidad es integrante de una organización. + /// + /// Id de la identidad. + /// Id de la organización. + Task<(IEnumerable success, List failure)> IamIn(IEnumerable id, int organization); + + + /// + /// Expulsar a una identidad de una organización. + /// + /// Lista de identidades. + /// Id de la organización. + Task Expulse(IEnumerable ids, int organization); + + + /// + /// Obtener las organizaciones donde una identidad es integrante. + /// + Task> ReadAllMembers(int identity); + + + /// + /// Obtener las cuentas de usuario de una organización. + /// + /// Id de la organización. + Task>> ReadUserAccounts(int id); + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IOrganizationRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IOrganizationRepository.cs new file mode 100644 index 0000000..642e4db --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IOrganizationRepository.cs @@ -0,0 +1,14 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IOrganizationRepository +{ + Task Create(OrganizationModel modelo); + + Task> Read(int id); + + Task> GetDomain(int id); + + Task> ReadDirectory(int id); + + Task> ReadDirectoryIdentity(int id); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IOtpRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IOtpRepository.cs new file mode 100644 index 0000000..58d2cca --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IOtpRepository.cs @@ -0,0 +1,8 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IOtpRepository +{ + Task Create(OtpDatabaseModel model); + Task Create(MailOtpDatabaseModel model); + Task ReadAndUpdate(int accountId, string code); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IPolicyMemberRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IPolicyMemberRepository.cs new file mode 100644 index 0000000..7b07777 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IPolicyMemberRepository.cs @@ -0,0 +1,7 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IPolicyMemberRepository +{ + Task Create(IdentityPolicyModel policyModel); + Task> ReadAll(int identity); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/IPolicyRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/IPolicyRepository.cs new file mode 100644 index 0000000..3483b7b --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/IPolicyRepository.cs @@ -0,0 +1,15 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface IPolicyRepository +{ + Task Create(PolicyModel policyModel); + Task Add(TimeAccessPolicy policyModel); + Task Add(IpAccessPolicy policyModel); + Task Add(IdentityTypePolicy policyModel); + Task> Read(int id, bool includeDetails); + Task> ReadAll(int organization, bool includeDetails); + Task> ReadAll(int organization, string query); + Task> ReadAllByApp(int application, bool includeDetails); + Task> ReadAll(IEnumerable identity, int organization, bool includeDetails); + Task Delete(int id); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Repositories/ITemporalAccountRepository.cs b/LIN.Cloud.Identity.Persistence/Repositories/ITemporalAccountRepository.cs new file mode 100644 index 0000000..d7009ac --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Repositories/ITemporalAccountRepository.cs @@ -0,0 +1,8 @@ +namespace LIN.Cloud.Identity.Persistence.Repositories; + +public interface ITemporalAccountRepository +{ + Task Create(TemporalAccountModel modelo); + Task Delete(int id); + Task> ReadWithCode(string verificationCode); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Persistence/Usings.cs b/LIN.Cloud.Identity.Persistence/Usings.cs new file mode 100644 index 0000000..e9e4957 --- /dev/null +++ b/LIN.Cloud.Identity.Persistence/Usings.cs @@ -0,0 +1,9 @@ +global using LIN.Cloud.Identity.Persistence.Contexts; +global using LIN.Cloud.Identity.Persistence.Extensions; +global using LIN.Cloud.Identity.Persistence.Models; +global using LIN.Types.Cloud.Identity.Enumerations; +global using LIN.Types.Cloud.Identity.Models; +global using LIN.Types.Cloud.Identity.Models.Identities; +global using LIN.Types.Cloud.Identity.Models.Policies; +global using LIN.Types.Responses; +global using Microsoft.EntityFrameworkCore; diff --git a/LIN.Cloud.Identity.Services/Extensions/IamExtensions.cs b/LIN.Cloud.Identity.Services/Extensions/IamExtensions.cs new file mode 100644 index 0000000..1890a06 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Extensions/IamExtensions.cs @@ -0,0 +1,128 @@ +namespace LIN.Cloud.Identity.Services.Extensions; + +public static class ValidateRoles +{ + + public static bool ValidateRead(this IEnumerable roles) + { + List availed = + [ + Roles.Administrator, + Roles.Manager, + Roles.AccountOperator, + Roles.Regular, + Roles.Viewer, + Roles.SecurityViewer + ]; + + + var sets = availed.Intersect(roles); + return sets.Any(); + } + + public static bool ValidateReadSecure(this IEnumerable roles) + { + List availed = + [ + Roles.Administrator, + Roles.Manager, + Roles.AccountOperator, + Roles.Regular, + Roles.SecurityViewer + ]; + + var sets = availed.Intersect(roles); + return sets.Any(); + } + + public static bool ValidateAlterPolicies(this IEnumerable roles) + { + List availed = + [ + Roles.Administrator, + Roles.Manager + ]; + + var sets = availed.Intersect(roles); + + return sets.Any(); + + } + + public static bool ValidateAlterMembers(this IEnumerable roles) + { + List availed = + [ + Roles.Administrator, + Roles.Manager, + Roles.AccountOperator + ]; + + var sets = availed.Intersect(roles); + return sets.Any(); + } + + public static bool ValidateInviteMembers(this IEnumerable roles) + { + List availed = + [ + Roles.Administrator, + Roles.Manager + ]; + + var sets = availed.Intersect(roles); + return sets.Any(); + } + + public static bool ValidateDelete(this IEnumerable roles) + { + List availed = + [ + Roles.Administrator, + Roles.Manager + ]; + + var sets = availed.Intersect(roles); + return sets.Any(); + } + + public static bool ValidateReadPolicies(this IEnumerable roles) + { + List availed = + [ + Roles.Administrator, + Roles.Manager, + Roles.AccountOperator, + Roles.SecurityViewer + ]; + + var sets = availed.Intersect(roles); + return sets.Any(); + } + + public static bool ValidateAlterDomain(this IEnumerable roles) + { + List availed = + [ + Roles.Administrator + ]; + + var sets = availed.Intersect(roles); + return sets.Any(); + } + + public static bool ValidateReadDomains(this IEnumerable roles) + { + List availed = + [ + Roles.Administrator, + Roles.Manager, + Roles.AccountOperator, + Roles.SecurityViewer + ]; + + var sets = availed.Intersect(roles); + return sets.Any(); + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Extensions/ServiceExtensions.cs b/LIN.Cloud.Identity.Services/Extensions/ServiceExtensions.cs new file mode 100644 index 0000000..6c41508 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Extensions/ServiceExtensions.cs @@ -0,0 +1,46 @@ +using FirebaseAdmin; +using Google.Apis.Auth.OAuth2; +using LIN.Cloud.Identity.Services.Services.Authentication; +using LIN.Cloud.Identity.Services.Services.Authentication.ThirdParties; +using LIN.Cloud.Identity.Services.Services.Iam; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace LIN.Cloud.Identity.Services.Extensions; + +public static class ServiceExtensions +{ + + /// + /// Agregar servicios de persistence. + /// + /// Services. + public static IServiceCollection AddAuthenticationServices(this IServiceCollection services, IConfiguration configuration) + { + // Inicializar Firebase + //FirebaseApp.Create(new AppOptions + //{ + // Credential = GoogleCredential.FromFile("appsettings.firebase.json") + //}); + + // Servicios de datos. + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddSingleton(); + + JwtService.Open(configuration); + JwtApplicationsService.Open(configuration); + + return services; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Interfaces/AuthenticationPipelineComponent.cs b/LIN.Cloud.Identity.Services/Interfaces/AuthenticationPipelineComponent.cs new file mode 100644 index 0000000..284c4df --- /dev/null +++ b/LIN.Cloud.Identity.Services/Interfaces/AuthenticationPipelineComponent.cs @@ -0,0 +1,11 @@ +namespace LIN.Cloud.Identity.Services.Interfaces; + +public interface IApplicationValidationService : IAuthenticationService; +public interface IIdentityValidationService : IAuthenticationService; +public interface IAccountValidationService : IAuthenticationService; +public interface IOrganizationValidationService : IAuthenticationService; + +// Proveedores de servicios de autenticación de terceros (Google, Microsoft, etc.) +public interface IGoogleValidationService : IAuthenticationService; +public interface IMicrosoftValidationService : IAuthenticationService; +public interface IGitHubValidationService : IAuthenticationService; \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Interfaces/IAuthenticationAccountService.cs b/LIN.Cloud.Identity.Services/Interfaces/IAuthenticationAccountService.cs new file mode 100644 index 0000000..350ae5f --- /dev/null +++ b/LIN.Cloud.Identity.Services/Interfaces/IAuthenticationAccountService.cs @@ -0,0 +1,9 @@ +using LIN.Types.Cloud.Identity.Models.Identities; + +namespace LIN.Cloud.Identity.Services.Interfaces; + +public interface IAuthenticationAccountService : IAuthenticationService +{ + public AccountModel? Account { get; } + public string GenerateToken(); +} diff --git a/LIN.Cloud.Identity.Services/Interfaces/IAuthenticationService.cs b/LIN.Cloud.Identity.Services/Interfaces/IAuthenticationService.cs new file mode 100644 index 0000000..e0cbb98 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Interfaces/IAuthenticationService.cs @@ -0,0 +1,6 @@ +namespace LIN.Cloud.Identity.Services.Interfaces; + +public interface IAuthenticationService +{ + public Task Authenticate(AuthenticationRequest request); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Interfaces/IDomainService.cs b/LIN.Cloud.Identity.Services/Interfaces/IDomainService.cs new file mode 100644 index 0000000..3642c67 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Interfaces/IDomainService.cs @@ -0,0 +1,7 @@ +namespace LIN.Cloud.Identity.Services.Interfaces; + +public interface IDomainService +{ + Task VerifyDns(string domain, string code); + bool VerifyDomain(string dominio); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Interfaces/IIamService.cs b/LIN.Cloud.Identity.Services/Interfaces/IIamService.cs new file mode 100644 index 0000000..7d66d28 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Interfaces/IIamService.cs @@ -0,0 +1,8 @@ +namespace LIN.Cloud.Identity.Services.Interfaces; + +public interface IIamService +{ + Task> IamIdentity(int identity1, int identity2); + Task> Validate(int identity, int organization); + Task IamPolicy(int identity, int policy); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Interfaces/IIdentityService.cs b/LIN.Cloud.Identity.Services/Interfaces/IIdentityService.cs new file mode 100644 index 0000000..17aa3f0 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Interfaces/IIdentityService.cs @@ -0,0 +1,7 @@ +namespace LIN.Cloud.Identity.Services.Interfaces; + +internal interface IIdentityService +{ + Task> GetIdentities(int identity); + Task>> GetLevel(int identity, int organization); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Interfaces/IPolicyEngine.cs b/LIN.Cloud.Identity.Services/Interfaces/IPolicyEngine.cs new file mode 100644 index 0000000..f4d2c2c --- /dev/null +++ b/LIN.Cloud.Identity.Services/Interfaces/IPolicyEngine.cs @@ -0,0 +1,7 @@ +namespace LIN.Cloud.Identity.Services.Interfaces; + +public interface IPolicyEngine +{ + public Task IsAuthorized(int identity, int organization); + public Task IsAuthorizedForService(int identity, int serviceId); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Interfaces/IPolicyOrchestrator.cs b/LIN.Cloud.Identity.Services/Interfaces/IPolicyOrchestrator.cs new file mode 100644 index 0000000..5313590 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Interfaces/IPolicyOrchestrator.cs @@ -0,0 +1,7 @@ +namespace LIN.Cloud.Identity.Services.Interfaces; + +internal interface IPolicyOrchestrator +{ + Task> ValidatePoliciesForOrganization(AuthenticationRequest request); + Task> ValidatePoliciesForApplication(AuthenticationRequest request, int appId); +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/LIN.Cloud.Identity.Services.csproj b/LIN.Cloud.Identity.Services/LIN.Cloud.Identity.Services.csproj new file mode 100644 index 0000000..31542ae --- /dev/null +++ b/LIN.Cloud.Identity.Services/LIN.Cloud.Identity.Services.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Models/AuthenticationRequest.cs b/LIN.Cloud.Identity.Services/Models/AuthenticationRequest.cs new file mode 100644 index 0000000..144b7b9 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Models/AuthenticationRequest.cs @@ -0,0 +1,16 @@ +namespace LIN.Cloud.Identity.Services.Models; + +public class AuthenticationRequest +{ + public string User { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Application { get; set; } = string.Empty; + public string ThirdPartyToken { get; set; } = string.Empty; + public bool StrictService { get; set; } = false; + public int ApplicationId { get; set; } + public Types.Cloud.Identity.Enumerations.IdentityService Service { get; set; } = Types.Cloud.Identity.Enumerations.IdentityService.LIN; + + public Types.Cloud.Identity.Models.Identities.AccountModel? Account { get; internal set; } + public Types.Cloud.Identity.Models.Identities.ApplicationModel? ApplicationModel { get; internal set; } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Models/JwtModel.cs b/LIN.Cloud.Identity.Services/Models/JwtModel.cs new file mode 100644 index 0000000..58e7ddd --- /dev/null +++ b/LIN.Cloud.Identity.Services/Models/JwtModel.cs @@ -0,0 +1,29 @@ +namespace LIN.Cloud.Identity.Services.Models; + +public class JwtModel +{ + /// + /// El token esta autenticado. + /// + public bool IsAuthenticated { get; set; } + + /// + /// Usuario. + /// + public string Unique { get; set; } = string.Empty; + + /// + /// Id de la cuenta. + /// + public int AccountId { get; set; } + + /// + /// Id de la identidad. + /// + public int IdentityId { get; set; } + + /// + /// Id de la aplicación. + /// + public int ApplicationId { get; set; } +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Models/PolicyValidatorContext.cs b/LIN.Cloud.Identity.Services/Models/PolicyValidatorContext.cs new file mode 100644 index 0000000..622eab3 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Models/PolicyValidatorContext.cs @@ -0,0 +1,8 @@ +namespace LIN.Cloud.Identity.Services.Models; + +internal class PolicyValidatorContext +{ + public AuthenticationRequest AuthenticationRequest { get; set; } = null!; + public List Reasons { get; set; } = []; + public Dictionary Evaluated { get; set; } = []; +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Services/AccountAuthenticationService.cs b/LIN.Cloud.Identity.Services/Services/AccountAuthenticationService.cs new file mode 100644 index 0000000..c381370 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Services/AccountAuthenticationService.cs @@ -0,0 +1,85 @@ +using LIN.Types.Cloud.Identity.Models.Identities; +using Microsoft.Extensions.DependencyInjection; + +namespace LIN.Cloud.Identity.Services.Services; + +internal class AccountAuthenticationService(IServiceProvider provider) : IAuthenticationAccountService +{ + + /// + /// Solicitud de autenticación. + /// + private AuthenticationRequest Request { get; set; } = null!; + + + /// + /// Obtener la cuenta de usuario. + /// + public AccountModel? Account => Request?.Account; + + + /// + /// Autenticar una cuenta de usuario. + /// + public async Task Authenticate(AuthenticationRequest request) + { + Request = request; + + using var scope = provider.CreateScope(); + var serviceProvider = scope.ServiceProvider; + + // Configurar el pipeline de autenticación. + List pipelineSteps = []; + + pipelineSteps.AddRange([ + typeof(IIdentityValidationService) + ]); + + if (request.Service == Types.Cloud.Identity.Enumerations.IdentityService.Google) + { + // Agregar pasos específicos para Google. + pipelineSteps.Insert(0, typeof(IGoogleValidationService)); + } + else if (request.Service == Types.Cloud.Identity.Enumerations.IdentityService.Microsoft) + { + // Agregar pasos específicos para Microsoft. + //pipelineSteps.Insert(0, ); + } + else + { + // Autenticación por defecto para LIN. + pipelineSteps.AddRange([ + typeof(IAccountValidationService), + ]); + } + + // Pasos comunes para todos los servicios. + pipelineSteps.AddRange([ + typeof(IOrganizationValidationService), + typeof(IApplicationValidationService) + ]); + + foreach (var stepType in pipelineSteps) + { + var service = (IAuthenticationService)serviceProvider.GetRequiredService(stepType); + var result = await service.Authenticate(request); + + if (result.Response != Responses.Success) + return result; + } + + return new ResponseBase(Responses.Success); + } + + + /// + /// Generar token de autenticación. + /// + public string GenerateToken() + { + if (Request.Account is null || Request.ApplicationModel is null) + return string.Empty; + + return JwtService.Generate(Request.Account!, Request.ApplicationModel.Id); + } +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Services/Authentication/AccountValidationService.cs b/LIN.Cloud.Identity.Services/Services/Authentication/AccountValidationService.cs new file mode 100644 index 0000000..c805256 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Services/Authentication/AccountValidationService.cs @@ -0,0 +1,23 @@ +namespace LIN.Cloud.Identity.Services.Services.Authentication; + +internal class AccountValidationService : IAccountValidationService +{ + + /// + /// Valida la cuenta de usuario y la contraseña. + /// + public async Task Authenticate(AuthenticationRequest request) + { + + // Validar contraseña. + if (Global.Utilities.Cryptography.Encrypt(request.Password) != request.Account!.Password) + return new ResponseBase + { + Response = Responses.InvalidPassword, + Message = "La contraseña es incorrecta." + }; + + return await Task.FromResult(new ResponseBase(Responses.Success)); + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Services/Authentication/ApplicationValidationService.cs b/LIN.Cloud.Identity.Services/Services/Authentication/ApplicationValidationService.cs new file mode 100644 index 0000000..0cba0f2 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Services/Authentication/ApplicationValidationService.cs @@ -0,0 +1,39 @@ +using LIN.Types.Cloud.Identity.Models.Identities; + +namespace LIN.Cloud.Identity.Services.Services.Authentication; + +internal class ApplicationValidationService(IApplicationRepository applicationRepository, IPolicyOrchestrator policyOrchestrator) : IApplicationValidationService +{ + + /// + /// Valida la cuenta de usuario y la contraseña. + /// + public async Task Authenticate(AuthenticationRequest request) + { + // Obtener la aplicación. + ReadOneResponse applicationResponse; + + if (request.ApplicationId <= 0) + applicationResponse = await applicationRepository.Read(request.Application); + else + applicationResponse = await applicationRepository.Read(request.ApplicationId); + + if (applicationResponse.Response != Responses.Success) + return new ResponseBase(Responses.UnauthorizedByApp) + { + Message = "Application not found.", + }; + + // Validar políticas. + var response = await policyOrchestrator.ValidatePoliciesForApplication(request, applicationResponse.Model.Id); + + if (response.Response != Responses.Success) + return response; + + // Correcto. + request.ApplicationModel = applicationResponse.Model; + return new ResponseBase(Responses.Success); + } + + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Services/Authentication/IdentityValidationService.cs b/LIN.Cloud.Identity.Services/Services/Authentication/IdentityValidationService.cs new file mode 100644 index 0000000..92ce9fc --- /dev/null +++ b/LIN.Cloud.Identity.Services/Services/Authentication/IdentityValidationService.cs @@ -0,0 +1,50 @@ +namespace LIN.Cloud.Identity.Services.Services.Authentication; + +internal class IdentityValidationService(IAccountRepository accountRepository) : IIdentityValidationService +{ + + /// + /// Valida la cuenta de usuario y la contraseña. + /// + public async Task Authenticate(AuthenticationRequest request) + { + + // Obtener la cuenta. + var accountResponse = await accountRepository.Read(request.User, new() + { + IncludeIdentity = true, + FindOn = Persistence.Models.FindOn.AllAccounts + }); + + // Validar respuesta. + if (accountResponse.Response != Responses.Success) + return new ResponseBase + { + Response = Responses.NotExistAccount, + Message = "Account not found" + }; + + var account = accountResponse.Model; + + // Validar estado identidad. + if (account.Identity.Status != IdentityStatus.Enable) + return new ResponseBase + { + Response = Responses.NotExistAccount, + Message = "La identidad de la cuenta de usuario no se encuentra activa." + }; + + if (request.StrictService && account.Identity.Provider != request.Service) + return new ResponseBase + { + Response = Responses.Unauthorized, + Message = $"La cuenta no esta vinculada con el proveedor {request.Service}" + }; + + // Establecer datos en la solicitud. + request.Account = account; + + return new(Responses.Success); + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Services/Authentication/OrganizationValidationService.cs b/LIN.Cloud.Identity.Services/Services/Authentication/OrganizationValidationService.cs new file mode 100644 index 0000000..9615669 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Services/Authentication/OrganizationValidationService.cs @@ -0,0 +1,25 @@ +namespace LIN.Cloud.Identity.Services.Services.Authentication; + +internal class OrganizationValidationService(IPolicyOrchestrator policyOrchestrator) : IOrganizationValidationService +{ + + /// + /// Valida políticas de organización. + /// + public async Task Authenticate(AuthenticationRequest request) + { + // Validar si existe una organización dueña de la cuenta. + if (request.Account!.Identity.OwnerId is null || request.Account!.Identity.OwnerId <= 0) + return new ResponseBase(Responses.Success); + + // Validar políticas. + var response = await policyOrchestrator.ValidatePoliciesForOrganization(request); + + if (response.Response != Responses.Success) + return response; + + // Correcto. + return new ResponseBase(Responses.Success); + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Services/Authentication/ThirdParties/GoogleValidationService.cs b/LIN.Cloud.Identity.Services/Services/Authentication/ThirdParties/GoogleValidationService.cs new file mode 100644 index 0000000..9dc7361 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Services/Authentication/ThirdParties/GoogleValidationService.cs @@ -0,0 +1,86 @@ +using FirebaseAdmin.Auth; +using LIN.Types.Cloud.Identity.Models.Identities; + +namespace LIN.Cloud.Identity.Services.Services.Authentication.ThirdParties; + +public class GoogleValidationService(IAccountRepository accountRepository) : IGoogleValidationService +{ + + /// + /// Valida la cuenta de usuario y la contraseña. + /// + public async Task Authenticate(AuthenticationRequest request) + { + + // Validar token de acceso. + var information = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(request.ThirdPartyToken); + + // Validar que la identidad de usuario exista. + string unique = information.Claims["email"].ToString() ?? string.Empty; + string name = information.Claims["name"].ToString() ?? string.Empty; + + string? provider = information.Claims["firebase"] is Newtonsoft.Json.Linq.JObject firebaseClaims + ? firebaseClaims["sign_in_provider"]?.ToString() + : "unknown"; + + if (string.IsNullOrEmpty(unique) || string.IsNullOrEmpty(name) || provider != "google.com") + return new(Responses.Unauthorized) + { + Message = "El token es invalido o no esta firmado para Google." + }; + + var account = await accountRepository.Read(unique, new() + { + FindOn = Persistence.Models.FindOn.AllAccounts, + IncludeIdentity = true + }); + + switch (account.Response) + { + case Responses.Success: + break; + case Responses.NotRows: + var accountNew = new AccountModel() + { + AccountType = AccountTypes.Personal, + Password = Global.Utilities.KeyGenerator.Generate(20, "pwd"), + Name = name, + Profile = "", + Visibility = Visibility.Visible, + Identity = new() + { + Unique = unique, + Type = IdentityType.Account + } + }; + accountNew = Persistence.Formatters.Account.Process(accountNew); + accountNew.Identity.Provider = Types.Cloud.Identity.Enumerations.IdentityService.Google; + // Crear nueva identidad y cuenta. + var create = await accountRepository.Create(accountNew, 0); + account = create; + break; + default: + return new() + { + Response = Responses.InvalidUser, + Message = "Ocurrió un error al iniciar sesión con Google." + }; + } + + if (account.Response != Responses.Success) + return new(Responses.Unauthorized) + { + Message = "Ocurrió un error en LIN Platform & Google." + }; + + if (account.Model.Identity.Provider != Types.Cloud.Identity.Enumerations.IdentityService.Google) + return new(Responses.Unauthorized) + { + Message = "La cuenta no esta vinculada con Google." + }; + + request.User = unique; + return await Task.FromResult(new ResponseBase(Responses.Success)); + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Services/DomainService.cs b/LIN.Cloud.Identity.Services/Services/DomainService.cs new file mode 100644 index 0000000..6a79230 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Services/DomainService.cs @@ -0,0 +1,54 @@ +using DnsClient; +using System.Text.RegularExpressions; + +namespace LIN.Cloud.Identity.Services.Services; + +internal class DomainService : IDomainService +{ + + /// + /// Verifica si el dominio es válido. + /// + /// Dominio. + public bool VerifyDomain(string dominio) + { + string patron = @"^(?!\-)([a-zA-Z0-9\-]{1,63}(? + /// Validar DNS. + /// + /// Dominio. + /// Código de verificación. + public async Task VerifyDns(string domain, string code) + { + var lookup = new LookupClient(); + + try + { + var resultado = await lookup.QueryAsync(domain, QueryType.TXT); + var txtRecords = resultado.Answers.TxtRecords(); + + if (txtRecords.Any()) + { + foreach (var record in txtRecords) + { + foreach (var text in record.Text) + { + if (text == code) + return true; // Registro TXT encontrado y verificado + } + } + } + return false; + + } + catch (Exception) + { + } + return false; // Registro TXT no encontrado o no coincide con el código + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Services/Iam/Iam.cs b/LIN.Cloud.Identity.Services/Services/Iam/Iam.cs new file mode 100644 index 0000000..7b0eac0 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Services/Iam/Iam.cs @@ -0,0 +1,91 @@ +using LIN.Cloud.Identity.Persistence.Contexts; +using LIN.Cloud.Identity.Services.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace LIN.Cloud.Identity.Services.Services.Iam; + +internal class IamService(DataContext context, IGroupRepository groups, IIdentityService identityService) : IIamService +{ + + /// + /// Obtener los roles de una identidad en una organización. + /// + /// Id de la identidad. + /// Id de la organización. + public async Task> Validate(int identity, int organization) + { + + // Identidades. + var identities = await identityService.GetIdentities(identity); + + // Obtener roles. + var roles = await (from rol in context.IdentityRoles + where identities.Contains(rol.IdentityId) + && rol.OrganizationId == organization + select rol.Rol).ToListAsync(); + + return roles; + + } + + + /// + /// Validar el nivel de acceso de una identidad sobre otra identidad. + /// + public async Task> IamIdentity(int identity1, int identity2) + { + + var organizations = await (from z in context.Groups + where z.Members.Any(x => x.IdentityId == identity1) + && z.Members.Any(x => x.IdentityId == identity2) + select z.Identity.Owner!.Id).Distinct().ToListAsync(); + + // Si es un grupo. + var organization = await groups.GetOwnerByIdentity(identity2); + + if (organization.Response == Responses.Success) + { + organizations.Add(organization.Model); + organizations = organizations.Distinct().ToList(); + } + + List roles = new(); + + foreach (var e in organizations) + { + var x = await Validate(identity1, e); + roles.AddRange(x); + } + + + return roles; + } + + + /// + /// Validar el nivel de acceso a una política. + /// + /// Id de la identidad. + /// Id de la política. + public async Task IamPolicy(int identity, int policy) + { + + // Obtener la identidad del dueño de la política. + var ownerPolicy = await (from pol in context.Policies + where pol.Id == policy + select pol.Owner!.Directory.IdentityId).FirstOrDefaultAsync(); + + // Obtener la organización. + var organizationId = await groups.GetOwnerByIdentity(ownerPolicy); + + // Obtener roles de la identidad sobre la organización. + var roles = await Validate(identity, organizationId.Model); + + // Tiene permisos para modificar la política. + if (ValidateRoles.ValidateAlterPolicies(roles)) + return IamLevels.Privileged; + + return IamLevels.NotAccess; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Services/IdentityService.cs b/LIN.Cloud.Identity.Services/Services/IdentityService.cs new file mode 100644 index 0000000..c4c4d01 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Services/IdentityService.cs @@ -0,0 +1,143 @@ +using LIN.Cloud.Identity.Persistence.Contexts; + +namespace LIN.Cloud.Identity.Services.Services; + +internal class IdentityService(DataContext context) : IIdentityService +{ + + /// + /// Obtener la identidades asociadas a una identidad base. + /// + /// Identidad base. + public async Task> GetIdentities(int identity) + { + List result = [identity]; + await GetIdentities(identity, result); + return result; + } + + + /// + /// Obtener la identidades asociadas a una identidad base. + /// + /// Identidad base. + public async Task>> GetLevel(int identity, int organization) + { + Dictionary> result = new() + { + { + 0, + [ + new() { + Identity = identity, + Level = 0 + }] + } + }; + + await GetIdentities(identity, 1, organization, [], result); + return result; + } + + + /// + /// Obtener la identidades asociadas a una identidad base. + /// + /// Identidad base. + /// Identidades encontradas. + private async Task GetIdentities(int identity, List ids) + { + // Consulta. + var query = from id in context.Identities + where id.Id == identity + select new + { + // Encontrar grupos donde la identidad pertenece. + In = (from member in context.GroupMembers + where !ids.Contains(member.Group.IdentityId) + && member.IdentityId == identity + select member.Group.IdentityId).ToList(), + }; + + // Si no hay elementos. + if (!query.Any()) + return; + + // Ejecuta la consulta. + var local = query.ToList(); + + // Obtiene las bases. + var bases = local.SelectMany(t => t.In); + + // Agregar a los objetos. + ids.AddRange(bases); + + // Recorrer. + foreach (var @base in bases) + await GetIdentities(@base, ids); + + } + + + /// + /// Obtener la identidades asociadas a una identidad base. + /// + /// Identidad base. + /// Identidades encontradas. + private async Task GetIdentities(int identity, int level, int organization, List ids, Dictionary> keys) + { + // Consulta. + var query = from id in context.Identities + where id.Id == identity + && id.OwnerId == organization + select new + { + // Encontrar grupos donde la identidad pertenece. + In = (from member in context.GroupMembers + where !ids.Contains(member.Group.IdentityId) + && member.IdentityId == identity + select member.Group.IdentityId).ToList(), + }; + + // Si no hay elementos. + if (!query.Any()) + return; + + // Ejecuta la consulta. + var local = query.ToList(); + + // Obtiene las bases. + var bases = local.SelectMany(t => t.In); + + // Agregar a los objetos. + ids.AddRange(bases.Select(t => t)); + + keys.TryGetValue(level, out var list); + + if (list == null) + keys.Add(level, bases.Select(t => new IdentityLevelModel + { + Identity = t, + Level = level + }).ToList()); + else + list.AddRange(bases.Select(t => new IdentityLevelModel + { + Identity = t, + Level = level + })); + + // Recorrer. + foreach (var @base in bases) + await GetIdentities(@base, level + 1, organization, ids, keys); + + } + +} + + +class IdentityLevelModel +{ + public int Level { get; set; } + public int Identity { get; set; } +} \ No newline at end of file diff --git a/LIN.Identity/Services/Jwt.cs b/LIN.Cloud.Identity.Services/Services/JwtApplicationsService.cs similarity index 66% rename from LIN.Identity/Services/Jwt.cs rename to LIN.Cloud.Identity.Services/Services/JwtApplicationsService.cs index 2e19028..4617f17 100644 --- a/LIN.Identity/Services/Jwt.cs +++ b/LIN.Cloud.Identity.Services/Services/JwtApplicationsService.cs @@ -1,41 +1,37 @@ -using System.IdentityModel.Tokens.Jwt; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; -using System.Security.Cryptography; +using System.Text; -namespace LIN.Identity.Services; +namespace LIN.Cloud.Identity.Services.Services; - -public class Jwt +public class JwtApplicationsService { - /// /// Llave del token /// private static string JwtKey { get; set; } = string.Empty; - /// - /// Inicia el servicio Jwt + /// Inicia el servicio JwtService /// - public static void Open() + public static void Open(IConfiguration configuration) { - JwtKey = Configuration.GetConfiguration("jwt:key"); + JwtKey = configuration["jwt:keyapp"] ?? string.Empty; } - - /// /// Genera un JSON Web Token /// /// Modelo de usuario - internal static string Generate(AccountModel user, int appID) + public static string Generate(int appID) { // Configuración - var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtKey)); // Credenciales @@ -44,15 +40,11 @@ internal static string Generate(AccountModel user, int appID) // Reclamaciones var claims = new[] { - new Claim(ClaimTypes.PrimarySid, user.ID.ToString()), - new Claim(ClaimTypes.NameIdentifier, user.Usuario), - new Claim(ClaimTypes.Role, ((int)user.Rol).ToString()), - new Claim(ClaimTypes.UserData, (user.OrganizationAccess?.Organization.ID).ToString() ?? ""), new Claim(ClaimTypes.Authentication, appID.ToString()) }; // Expiración del token - var expiración = DateTime.Now.AddHours(5); + var expiración = DateTime.UtcNow.AddMinutes(5); // Token var token = new JwtSecurityToken(null, null, claims, null, expiración, credentials); @@ -62,17 +54,20 @@ internal static string Generate(AccountModel user, int appID) } - /// /// Valida un JSON Web token /// /// Token a validar - internal static (bool isValid, string user, int userID, int orgID, int appID) Validate(string token) + public static int Validate(string token) { try { - // Configurar la clave secreta + // Comprobación + if (string.IsNullOrWhiteSpace(token)) + return 0; + + // Configurar la clave secreta. var key = Encoding.ASCII.GetBytes(JwtKey); // Validar el token @@ -98,27 +93,21 @@ internal static (bool isValid, string user, int userID, int orgID, int appID) Va var user = jwtToken.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier)?.Value; // - _ = int.TryParse(jwtToken.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.PrimarySid)?.Value, out var id); - - _ = int.TryParse(jwtToken.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.UserData)?.Value, out var orgID); - _ = int.TryParse(jwtToken.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Authentication)?.Value, out var appID); // Devuelve una respuesta exitosa - return (true, user ?? string.Empty, id, orgID, appID); - + return appID; } catch (SecurityTokenException) { - } } catch { } - return (false, string.Empty, 0, 0, 0); + return 0; } diff --git a/LIN.Cloud.Identity.Services/Services/JwtService.cs b/LIN.Cloud.Identity.Services/Services/JwtService.cs new file mode 100644 index 0000000..c9da720 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Services/JwtService.cs @@ -0,0 +1,126 @@ +using LIN.Types.Cloud.Identity.Models.Identities; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace LIN.Cloud.Identity.Services.Services; + +public class JwtService +{ + + /// + /// Llave del token + /// + private static string JwtKey { get; set; } = string.Empty; + + + /// + /// Inicia el servicio JwtService + /// + public static void Open(IConfiguration configuration) + { + JwtKey = configuration["jwt:key"] ?? string.Empty; + } + + + /// + /// Genera un JSON Web Token + /// + /// Modelo de usuario + public static string Generate(AccountModel user, int appID) + { + + // Configuración + var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtKey)); + + // Credenciales + var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha512); + + // Reclamaciones + var claims = new[] + { + new Claim(ClaimTypes.PrimarySid, user.Id.ToString()), + new Claim(ClaimTypes.NameIdentifier, user.Identity.Unique), + new Claim(ClaimTypes.GroupSid, user.Identity.Id.ToString() ?? ""), + new Claim(ClaimTypes.Authentication, appID.ToString()) + }; + + // Expiración del token + var expiración = DateTime.UtcNow.AddHours(5); + + // Token + var token = new JwtSecurityToken(null, null, claims, null, expiración, credentials); + + // Genera el token + return new JwtSecurityTokenHandler().WriteToken(token); + } + + + /// + /// Valida un JSON Web token + /// + /// Token a validar + public static JwtModel Validate(string token) + { + try + { + // Comprobación + if (string.IsNullOrWhiteSpace(token)) + return new() + { + IsAuthenticated = false + }; + + // Configurar la clave secreta. + var key = Encoding.ASCII.GetBytes(JwtKey); + + // Validar el token + var tokenHandler = new JwtSecurityTokenHandler(); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false, + RequireExpirationTime = true + }; + + try + { + + var claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out var validatedToken); + var jwtToken = (JwtSecurityToken)validatedToken; + + // Si el token es válido, puedes acceder a los claims (datos) del usuario + var user = jwtToken.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier)?.Value; + + _ = int.TryParse(jwtToken.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.PrimarySid)?.Value, out var id); + _ = int.TryParse(jwtToken.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Authentication)?.Value, out var appID); + _ = int.TryParse(jwtToken.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.GroupSid)?.Value, out var identityId); + + // Devuelve una respuesta exitosa + return new() + { + IsAuthenticated = true, + AccountId = id, + ApplicationId = appID, + IdentityId = identityId, + Unique = user ?? "" + }; + + } + catch (SecurityTokenException) + { + } + } + catch { } + + return new() + { + IsAuthenticated = false + }; + } +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Services/Policies/IdentityTypePolicyValidator.cs b/LIN.Cloud.Identity.Services/Services/Policies/IdentityTypePolicyValidator.cs new file mode 100644 index 0000000..d587263 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Services/Policies/IdentityTypePolicyValidator.cs @@ -0,0 +1,38 @@ +using LIN.Types.Cloud.Identity.Models; +using LIN.Types.Cloud.Identity.Models.Policies; + +namespace LIN.Cloud.Identity.Services.Services.Policies; + +internal class IdentityTypePolicyValidator +{ + + /// + /// Valida las políticas de tipo de identidad. + /// + public static bool Validate(PolicyModel policyBase, PolicyValidatorContext context, IEnumerable identityTypePolicies) + { + + // Si no hay políticas de tipo de identidad, se permite el acceso. + if (identityTypePolicies == null || !identityTypePolicies.Any()) + return true; + + // Tipo de identidad actual. + IdentityType? identityType = context.AuthenticationRequest.Account?.Identity.Type; + + bool result = false; + foreach (var e in identityTypePolicies) + if (e.Type == identityType) + { + result = true; // Se encontró una política válida + break; + } + + bool isValid = (result && policyBase.Effect == PolicyEffect.Allow) + || (!result && policyBase.Effect == PolicyEffect.Deny); + + + context.Evaluated["TIPE"] = isValid; + + return isValid; + } +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Services/Policies/TimeAccessPolicyValidator.cs b/LIN.Cloud.Identity.Services/Services/Policies/TimeAccessPolicyValidator.cs new file mode 100644 index 0000000..fcf4c57 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Services/Policies/TimeAccessPolicyValidator.cs @@ -0,0 +1,38 @@ +using LIN.Types.Cloud.Identity.Models; +using LIN.Types.Cloud.Identity.Models.Policies; + +namespace LIN.Cloud.Identity.Services.Services.Policies; + +internal class TimeAccessPolicyValidator +{ + + /// + /// Valida las políticas de acceso por tiempo. + /// + /// Contexto actual. + /// Lista de políticas. + public static bool Validate(PolicyModel policyBase, PolicyValidatorContext context, IEnumerable policies) + { + if (!policies.Any()) + return true; + + // Hora actual. + var now = TimeOnly.FromDateTime(DateTime.UtcNow); + + bool result = false; + foreach (var policy in policies) + { + if (now >= policy.StartHour && now <= policy.EndHour) + { + result = true; // Se encontró una política válida + break; + } + } + + bool isValid = (result && policyBase.Effect == PolicyEffect.Allow) + || (!result && policyBase.Effect == PolicyEffect.Deny); + + context.Evaluated["TIME"] = isValid; + return true; + } +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/Services/PolicyOrchestrator.cs b/LIN.Cloud.Identity.Services/Services/PolicyOrchestrator.cs new file mode 100644 index 0000000..a6cf962 --- /dev/null +++ b/LIN.Cloud.Identity.Services/Services/PolicyOrchestrator.cs @@ -0,0 +1,102 @@ +using LIN.Cloud.Identity.Services.Services.Policies; +using LIN.Types.Cloud.Identity.Models; + +namespace LIN.Cloud.Identity.Services.Services; + +internal class PolicyOrchestrator(IPolicyRepository policyRepository, IIdentityService identityService) : IPolicyOrchestrator +{ + + /// + /// Valida las políticas de acceso para una organización. + /// + public async Task> ValidatePoliciesForOrganization(AuthenticationRequest request) + { + var context = new PolicyValidatorContext + { + AuthenticationRequest = request + }; + + int organization = request.Account!.Identity.OwnerId!.Value; + + // Obtener las identidades. + var levels = await identityService.GetLevel(request.Account!.Identity.Id, organization); + + foreach (var level in levels.OrderBy(t => t.Key)) + { + // Obtener las políticas asociadas a la identidad. + var policies = await policyRepository.ReadAll(level.Value.Select(t => t.Identity), organization, true); + + foreach (var policy in policies.Models) + { + ValidateSinglePolicyAsync(policy, context); + } + } + + if (!context.Evaluated.Select(t => t.Value).All(t => t == true)) + { + // Si no se valida correctamente, agregar el error a la lista de razones. + return new(Responses.UnauthorizedByOrg) + { + Errors = [.. context.Reasons.Select(reason => new Types.Models.ErrorModel + { + Tittle = "Acceso denegado", + Description = reason + })] + }; + } + + return new(Responses.Success); + } + + + /// + /// Valida las políticas de acceso para una organización. + /// + public async Task> ValidatePoliciesForApplication(AuthenticationRequest request, int appId) + { + var context = new PolicyValidatorContext + { + AuthenticationRequest = request + }; + + // Obtener políticas de la aplicación. + var policies = await policyRepository.ReadAllByApp(appId, true); + + foreach (var policy in policies.Models) + { + ValidateSinglePolicyAsync(policy, context); + } + + if (!context.Evaluated.Select(t => t.Value).All(t => t == true)) + { + return new(Responses.UnauthorizedByApp) + { + Errors = [.. context.Reasons.Select(reason => new Types.Models.ErrorModel + { + Tittle = "Acceso denegado", + Description = reason + })] + }; + } + + return new(Responses.Success); + } + + + /// + /// Validar una sola política. + /// + private static bool ValidateSinglePolicyAsync(PolicyModel policy, PolicyValidatorContext context) + { + // Validar acceso por hora. + if (context.Evaluated.ContainsKey("TIME") || !TimeAccessPolicyValidator.Validate(policy, context, policy.TimeAccessPolicies)) + return false; + + // Validar tipo de identidad + if (context.Evaluated.ContainsKey("TYPE") || !IdentityTypePolicyValidator.Validate(policy, context, policy.IdentityTypePolicies)) + return false; + + return true; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity.Services/usings.cs b/LIN.Cloud.Identity.Services/usings.cs new file mode 100644 index 0000000..c73bcd1 --- /dev/null +++ b/LIN.Cloud.Identity.Services/usings.cs @@ -0,0 +1,7 @@ +global using LIN.Cloud.Identity.Persistence.Repositories; +global using LIN.Cloud.Identity.Services.Interfaces; +global using LIN.Cloud.Identity.Services.Models; +global using LIN.Cloud.Identity.Services.Services; +global using LIN.Types.Cloud.Identity.Enumerations; +global using LIN.Types.Enumerations; +global using LIN.Types.Responses; diff --git a/LIN.Cloud.Identity.sln b/LIN.Cloud.Identity.sln new file mode 100644 index 0000000..f77b4df --- /dev/null +++ b/LIN.Cloud.Identity.sln @@ -0,0 +1,93 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34330.188 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LIN.Cloud.Identity", "LIN.Cloud.Identity\LIN.Cloud.Identity.csproj", "{D7EF6814-1C64-44BC-BEAF-87835D44EEF8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LIN.Cloud.Identity.Persistence", "LIN.Cloud.Identity.Persistence\LIN.Cloud.Identity.Persistence.csproj", "{6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LIN.Cloud.Identity.Services", "LIN.Cloud.Identity.Services\LIN.Cloud.Identity.Services.csproj", "{DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 + Debug-dev|Any CPU = Debug-dev|Any CPU + Debug-dev|x86 = Debug-dev|x86 + Local|Any CPU = Local|Any CPU + Local|x86 = Local|x86 + Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 + Release-dev|Any CPU = Release-dev|Any CPU + Release-dev|x86 = Release-dev|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Debug|x86.ActiveCfg = Debug|x86 + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Debug|x86.Build.0 = Debug|x86 + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Debug-dev|Any CPU.ActiveCfg = Debug-dev|Any CPU + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Debug-dev|Any CPU.Build.0 = Debug-dev|Any CPU + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Debug-dev|x86.ActiveCfg = Debug-dev|Any CPU + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Debug-dev|x86.Build.0 = Debug-dev|Any CPU + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Local|Any CPU.ActiveCfg = Local|Any CPU + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Local|Any CPU.Build.0 = Local|Any CPU + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Local|x86.ActiveCfg = Local|x86 + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Local|x86.Build.0 = Local|x86 + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Release|Any CPU.Build.0 = Release|Any CPU + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Release|x86.ActiveCfg = Release|x86 + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Release|x86.Build.0 = Release|x86 + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Release-dev|Any CPU.ActiveCfg = Release-dev|Any CPU + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Release-dev|Any CPU.Build.0 = Release-dev|Any CPU + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Release-dev|x86.ActiveCfg = Release-dev|Any CPU + {D7EF6814-1C64-44BC-BEAF-87835D44EEF8}.Release-dev|x86.Build.0 = Release-dev|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Debug|x86.ActiveCfg = Debug|x86 + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Debug|x86.Build.0 = Debug|x86 + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Debug-dev|Any CPU.ActiveCfg = Debug-dev|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Debug-dev|Any CPU.Build.0 = Debug-dev|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Debug-dev|x86.ActiveCfg = Debug-dev|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Debug-dev|x86.Build.0 = Debug-dev|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Local|Any CPU.ActiveCfg = Local|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Local|Any CPU.Build.0 = Local|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Local|x86.ActiveCfg = Local|x86 + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Local|x86.Build.0 = Local|x86 + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Release|Any CPU.Build.0 = Release|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Release|x86.ActiveCfg = Release|x86 + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Release|x86.Build.0 = Release|x86 + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Release-dev|Any CPU.ActiveCfg = Release-dev|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Release-dev|Any CPU.Build.0 = Release-dev|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Release-dev|x86.ActiveCfg = Release-dev|Any CPU + {6E05CD1C-E4F0-4BCB-9BDE-CEB9278C6EB4}.Release-dev|x86.Build.0 = Release-dev|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Debug|x86.ActiveCfg = Debug|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Debug|x86.Build.0 = Debug|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Debug-dev|Any CPU.ActiveCfg = Debug|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Debug-dev|Any CPU.Build.0 = Debug|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Debug-dev|x86.ActiveCfg = Debug|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Debug-dev|x86.Build.0 = Debug|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Local|Any CPU.ActiveCfg = Debug|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Local|Any CPU.Build.0 = Debug|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Local|x86.ActiveCfg = Debug|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Local|x86.Build.0 = Debug|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Release|Any CPU.Build.0 = Release|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Release|x86.ActiveCfg = Release|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Release|x86.Build.0 = Release|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Release-dev|Any CPU.ActiveCfg = Release|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Release-dev|Any CPU.Build.0 = Release|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Release-dev|x86.ActiveCfg = Release|Any CPU + {DB8A6B1A-0D70-4FF3-9494-10FFF5649F82}.Release-dev|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {742D8B13-7C99-485E-B3EF-E51281923F66} + EndGlobalSection +EndGlobal diff --git a/LIN.Identity/.config/dotnet-tools.json b/LIN.Cloud.Identity/.config/dotnet-tools.json similarity index 82% rename from LIN.Identity/.config/dotnet-tools.json rename to LIN.Cloud.Identity/.config/dotnet-tools.json index 558293e..fd9a39d 100644 --- a/LIN.Identity/.config/dotnet-tools.json +++ b/LIN.Cloud.Identity/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "7.0.10", + "version": "8.0.1", "commands": [ "dotnet-ef" ] diff --git a/LIN.Cloud.Identity/Areas/Accounts/AccountController.cs b/LIN.Cloud.Identity/Areas/Accounts/AccountController.cs new file mode 100644 index 0000000..5b5ef68 --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Accounts/AccountController.cs @@ -0,0 +1,257 @@ +namespace LIN.Cloud.Identity.Areas.Accounts; + +[Route("[controller]")] +public class AccountController(IAccountRepository accountData, IApplicationRepository applications) : AuthenticationBaseController +{ + + /// + /// Crear cuenta de enlace LIN. + /// + /// Modelo de la cuenta. + /// Retorna el Id asignado a la cuenta. + [HttpPost] + public async Task Create([FromBody] AccountModel? modelo, [FromHeader] string app) + { + // Validaciones del modelo. + if (modelo is null || modelo.Identity is null || modelo.Password.Length < 4 || modelo.Name.Length <= 0 || modelo.Identity.Unique.Length <= 0) + return new(Responses.InvalidParam) + { + Message = "Uno o varios parámetros son inválidos." + }; + + // Validar usuario y nombre. + var errors = Services.Formats.Account.Validate(modelo); + + // Si no fue valido. + if (errors.Count > 0) + return new(Responses.InvalidParam) + { + Message = "Ocurrió un error al crear la cuenta", + Errors = errors + }; + + // Organización del modelo. + modelo.AccountType = AccountTypes.Personal; + modelo = Services.Formats.Account.Process(modelo); + + // Creación del usuario. + var response = await accountData.Create(modelo, 0); + + // Evaluación. + if (response.Response != Responses.Success) + return new(response.Response) + { + Message = "Hubo un error al crear la cuenta." + }; + + // Obtener información de la app. + var application = await applications.Read(app); + + // Obtiene el usuario. + string token = string.Empty; + + // Si la aplicación es valida, generamos un token. + if (application.Response == Responses.Success) + token = JwtService.Generate(response.Model, application.Model.Id); + + // Retorna el resultado. + return new CreateResponse() + { + LastId = response.Model.Identity.Id, + Response = Responses.Success, + Token = token, + Message = "Cuenta creada satisfactoriamente." + }; + } + + + /// + /// Obtener una cuenta. + /// + /// Id de la cuenta. + /// Retorna el modelo de la cuenta. + [HttpGet("read/id")] + [IdentityToken] + public async Task> Read([FromQuery] int id) + { + + // Id es invalido. + if (id <= 0) + return new(Responses.InvalidParam) + { + Message = "Uno o varios parámetros son inválidos." + }; + + // Obtiene el usuario. + var response = await accountData.Read(id, new() + { + AccountContext = UserInformation.AccountId, + FindOn = FindOn.StableAccounts, + IdentityContext = UserInformation.IdentityId, + IsAdmin = false + }); + + // Si es erróneo + if (response.Response != Responses.Success) + return new ReadOneResponse() + { + Response = response.Response + }; + + // Retorna el resultado + return response; + } + + + /// + /// Obtener una cuenta. + /// + /// Unique de la identidad de la cuenta. + /// Retorna el modelo de la cuenta. + [HttpGet("read/user")] + [IdentityToken] + public async Task> Read([FromQuery] string user) + { + + // Usuario es invalido. + if (string.IsNullOrWhiteSpace(user)) + return new(Responses.InvalidParam) + { + Message = "Uno o varios parámetros son inválidos." + }; + + // Obtiene el usuario. + var response = await accountData.Read(user, new() + { + AccountContext = UserInformation.AccountId, + FindOn = FindOn.StableAccounts, + IdentityContext = UserInformation.IdentityId, + IsAdmin = false + }); + + // Si es erróneo + if (response.Response != Responses.Success) + return new ReadOneResponse() + { + Response = response.Response, + Model = new() + }; + + // Retorna el resultado + return response; + + } + + + /// + /// Obtener una cuenta. + /// + /// Id de la identidad. + /// Retorna el modelo de la cuenta. + [HttpGet("read/identity")] + [IdentityToken] + public async Task> ReadByIdentity([FromQuery] int id) + { + + // Id es invalido. + if (id <= 0) + return new(Responses.InvalidParam) + { + Message = "Uno o varios parámetros son inválidos." + }; + + // Obtiene el usuario. + var response = await accountData.ReadByIdentity(id, new() + { + AccountContext = UserInformation.AccountId, + FindOn = FindOn.StableAccounts, + IdentityContext = UserInformation.IdentityId, + IsAdmin = false + }); + + // Si es erróneo + if (response.Response != Responses.Success) + return new ReadOneResponse() + { + Response = response.Response + }; + + // Retorna el resultado + return response; + } + + + /// + /// Obtener una lista de cuentas según las id de las identidades. + /// + /// Id de las identidades + /// Retorna la lista de cuentas. + [HttpPost("read/identity")] + [IdentityToken] + public async Task> ReadByIdentity([FromBody] List ids) + { + // Obtiene el usuario + var response = await accountData.FindAllByIdentities(ids, new() + { + AccountContext = UserInformation.AccountId, + FindOn = FindOn.StableAccounts, + IsAdmin = false, + IdentityContext = UserInformation.IdentityId, + }); + + return response; + } + + + /// + /// Buscar cuentas por medio de un patrón de búsqueda. + /// + /// Patron de búsqueda. + /// Retorna las cuentas encontradas. + [HttpGet("search")] + [IdentityToken] + public async Task> Search([FromQuery] string pattern) + { + + // Comprobación + if (pattern.Trim().Length <= 0 || string.IsNullOrWhiteSpace(pattern)) + return new(Responses.InvalidParam) + { + Message = "Uno o varios parámetros son inválidos." + }; + + // Obtiene el usuario + var response = await accountData.Search(pattern, new() + { + AccountContext = UserInformation.AccountId, + FindOn = FindOn.StableAccounts, + IsAdmin = false, + IdentityContext = UserInformation.IdentityId, + }); + + return response; + } + + + /// + /// Obtener la lista de cuentas. + /// + /// Lista de ids de las cuentas. + /// Retorna una lista de las cuentas encontradas. + [HttpPost("findAll")] + [IdentityToken] + public async Task> ReadAll([FromBody] List ids) + { + // Obtiene el usuario + var response = await accountData.FindAll(ids, new() + { + AccountContext = UserInformation.AccountId, + FindOn = FindOn.StableAccounts, + IsAdmin = false, + IdentityContext = UserInformation.IdentityId, + }); + + return response; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Accounts/AccountLogs.cs b/LIN.Cloud.Identity/Areas/Accounts/AccountLogs.cs new file mode 100644 index 0000000..d3add2d --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Accounts/AccountLogs.cs @@ -0,0 +1,31 @@ +namespace LIN.Cloud.Identity.Areas.Accounts; + +[IdentityToken] +[Route("account/logs")] +public class AccountLogsController(IAccountLogRepository accountData) : AuthenticationBaseController +{ + + /// + /// Obtener los logs asociados a una cuenta. + /// + /// Lista de logs. + [HttpGet] + public async Task> ReadAll(DateTime? start, DateTime? end) + { + // Fechas por defecto. + start ??= DateTime.MinValue; + end ??= DateTime.MaxValue; + + // Validar el rango de fecha. + if (end < start) + return new(Responses.InvalidParam) + { + Message = "La fecha de fin debe ser mayor a la fecha de inicio." + }; + + // Obtiene el usuario + var response = await accountData.ReadAll(UserInformation.AccountId, start, end); + return response; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Accounts/FederatedAccountController.cs b/LIN.Cloud.Identity/Areas/Accounts/FederatedAccountController.cs new file mode 100644 index 0000000..d34fa0d --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Accounts/FederatedAccountController.cs @@ -0,0 +1,176 @@ +namespace LIN.Cloud.Identity.Areas.Accounts; + +[Route("[controller]")] +public class FederatedAccountController(ITemporalAccountRepository temporalAccountRepository, IIamService iamService, IDomainRepository domainRepository, IAccountRepository accountRepository, IIdentityRepository identityRepository, EmailSender emailSender, IGroupMemberRepository groupMemberRepository, IOrganizationRepository organizationRepository) : AuthenticationBaseController +{ + + /// + /// Agrega una cuenta federada a la organización. + /// + /// Id de la organización. + /// Correo electrónico. + /// Proveedor. + [HttpPost] + [IdentityToken] + public async Task AddFederatedAccount([FromHeader] int organization, [FromQuery] string email, [FromQuery] IdentityService provider) + { + // 1. Obtener el dominio del correo + var match = Regex.Match(email, @"@(?[^@]+)$"); + if (!match.Success) + return new(Responses.InvalidParam) + { + Message = "El correo electrónico no es válido." + }; + + var domain = match.Groups["domain"].Value.ToLowerInvariant(); + + // 2. Validar si la organización es dueña del dominio + var domainResponse = await domainRepository.Read(domain); + bool isOrgOwner = domainResponse.Response == Responses.Success && + domainResponse.Model.OrganizationId == organization && + domainResponse.Model.IsVerified; + + // Validar que no exista una cuenta federada con el mismo correo. + var exist = await identityRepository.Exist(email); + + if (exist.Response == Responses.Success && exist.Model) + return new(Responses.ExistAccount) + { + Message = "Ya existe una cuenta federada con este correo." + }; + + if (!isOrgOwner) + { + + // Crear la cuenta temporal. + var tempAccount = new TemporalAccountModel + { + Email = email, + Name = "Cuenta Federada " + email, + Provider = provider, + VerificationCode = Global.Utilities.KeyGenerator.Generate(9, "code."), + OrganizationId = organization + }; + + // Guardar la cuenta temporal. + await temporalAccountRepository.Create(tempAccount); + + // Enviar correo de invitación a la organización. + await emailSender.Send(email, "Verifica tu cuenta federada", $"Por favor verifica tu correo para continuar. {tempAccount.VerificationCode}"); + return new(Responses.Success) + { + Message = "Se ha enviado un correo de verificación a " + email + ". Por favor verifica tu cuenta." + }; + } + + var roles = await iamService.Validate(UserInformation.IdentityId, organization); + if (!ValidateRoles.ValidateAlterMembers(roles)) + return new(Responses.Unauthorized) + { + Message = "No tienes permisos para agregar cuentas federadas internas." + }; + + // Crear la cuenta federada. + var accountModel = new AccountModel + { + AccountType = AccountTypes.Work, + Identity = new IdentityModel + { + Unique = email, + Type = IdentityType.Account + }, + Name = "Cuenta Federada " + email, + Password = Global.Utilities.KeyGenerator.Generate(20, "pwd"), + Visibility = Visibility.Visible + }; + + accountModel = Services.Formats.Account.Process(accountModel); + accountModel.Identity.Provider = provider; + + // Crear la cuenta. + var accountResponse = await accountRepository.Create(accountModel, organization); + + if (accountResponse.Response != Responses.Success) + return new(Responses.Undefined) + { + Message = "Error al crear la cuenta federada." + }; + + return new(Responses.Success) + { + Message = "Cuenta federada creada exitosamente." + }; + } + + + /// + /// Verificar una cuenta de terceros y agregarla al directorio de una organización. + /// + [HttpGet("verify")] + public async Task Verify([FromQuery] string code) + { + // Validar si existe una cuenta temporal con el código. + var temporalResponse = await temporalAccountRepository.ReadWithCode(code); + + if (temporalResponse.Response != Responses.Success) + return new(Responses.NotRows) + { + Message = "No se encontró una cuenta con el código proporcionado." + }; + + // Validar que no exista una cuenta con la misma identidad. + var accountExist = await accountRepository.Read(temporalResponse.Model.Email, new() { FindOn = FindOn.AllAccounts }); + + if (accountExist.Response != Responses.Success) + { + // No existe una cuenta con la misma identidad, se procede a crear la cuenta federada. + var accountModel = new AccountModel + { + AccountType = AccountTypes.Work, + Identity = new IdentityModel + { + Unique = temporalResponse.Model.Email, + Type = IdentityType.Account, + Provider = temporalResponse.Model.Provider + }, + Name = temporalResponse.Model.Name, + Password = Global.Utilities.KeyGenerator.Generate(20, "pwd"), + Visibility = Visibility.Visible, + }; + + accountModel = Services.Formats.Account.Process(accountModel); + accountModel.Identity.Provider = temporalResponse.Model.Provider; + + // Crear la cuenta. + var accountResponse = await accountRepository.Create(accountModel, 0); + accountExist = accountResponse; + } + + // Validar que la cuenta se haya creado correctamente. + if (accountExist.Response != Responses.Success) + return new(Responses.Undefined) + { + Message = "Error al crear la cuenta federada." + }; + + // Eliminar la cuenta temporal. + await temporalAccountRepository.Delete(temporalResponse.Model.Id); + + // Suscribir la cuenta al directorio de la organización. + if (temporalResponse.Model.OrganizationId > 0) + { + // Obtener el directorio de la organización. + var organization = await organizationRepository.ReadDirectory(temporalResponse.Model.OrganizationId); + + // Ingresarla a la organización. + await groupMemberRepository.Create([new() { + Group = new() { Id = organization.Model }, + Identity = new() { Id = accountExist.Model.IdentityId }, + Type = GroupMemberTypes.Guest + }]); + } + + return new(Responses.Success); + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Applications/ApplicationsController.cs b/LIN.Cloud.Identity/Areas/Applications/ApplicationsController.cs new file mode 100644 index 0000000..6459624 --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Applications/ApplicationsController.cs @@ -0,0 +1,102 @@ +namespace LIN.Cloud.Identity.Areas.Applications; + +[Route("applications")] +public class ApplicationsController(IApplicationRepository application) : AuthenticationBaseController +{ + + /// + /// Crear nueva aplicación. + /// + /// App. + [HttpPost] + [IdentityToken] + public async Task Create([FromBody] ApplicationModel app) + { + + // Si el modelo es nulo. + if (app is null) + return new(Responses.InvalidParam) + { + Errors = [new() { Tittle = "Modelo invalido", Description = "El modelo json es invalido." }] + }; + + // Validar identidad. + if (app.Identity is null) + return new(Responses.InvalidParam) + { + Errors = [new() { Tittle = "Identidad invalida", Description = "La identidad es invalida." }] + }; + + if (string.IsNullOrWhiteSpace(app.Identity.Unique)) + return new(Responses.InvalidParam) + { + Errors = [new() { Tittle = "Unique invalido", Description = "La identidad unica es invalida (No puede ser vacío)." }] + }; + + // Validar otros parámetros. + if (string.IsNullOrWhiteSpace(app.Name)) + return new(Responses.InvalidParam) + { + Errors = [new() { Tittle = "Nombre invalido", Description = "El nombre de la aplicación no puede estar vacío." }] + }; + + // Formatear app. + app.Key = Guid.NewGuid(); + app.Policies = []; + app.Identity.Type = IdentityType.Service; + app.Identity.Roles = []; + + app.Owner = new() + { + Id = UserInformation.IdentityId + }; + + Services.Formats.Identities.Process(app.Identity); + + var create = await application.Create(app); + + return create; + } + + + /// + /// Solicitar token de acceso a app. + /// + [HttpGet("token")] + public async Task RequestToken([FromHeader] string key) + { + // Validar key. + var app = await application.Read(key); + + if (app.Response != Responses.Success) + return new(Responses.InvalidParam); + + // Generar token de acceso. + var token = JwtApplicationsService.Generate(app.Model.Id); + + return new ResponseBase + { + Response = Responses.Success, + Token = token + }; + } + + + /// + /// Obtener la información básica de la aplicación. + /// + [HttpGet("information")] + public async Task> RequestInformation([FromHeader] string token) + { + + int id = JwtApplicationsService.Validate(token); + + if (id <= 0) + return new(Responses.InvalidParam); + + // Validar key. + var app = await application.Read(id); + return app; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Authentication/AuthenticationController.cs b/LIN.Cloud.Identity/Areas/Authentication/AuthenticationController.cs new file mode 100644 index 0000000..8fa16bf --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Authentication/AuthenticationController.cs @@ -0,0 +1,165 @@ +namespace LIN.Cloud.Identity.Areas.Authentication; + +[Route("[controller]")] +public class AuthenticationController(IAuthenticationAccountService serviceAuth, IAccountRepository accountData) : AuthenticationBaseController +{ + + /// + /// Iniciar sesión en una cuenta de usuario. + /// + /// Unique. + /// Contraseña. + /// Id de la aplicación. + /// Retorna el modelo de la cuenta y el token de acceso. + [HttpGet("login")] + public async Task> Login([FromQuery] string user, [FromQuery] string password, [FromHeader] string application) + { + + // Validación de parámetros. + if (string.IsNullOrWhiteSpace(user) || string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(application)) + return new(Responses.InvalidParam) + { + Message = "Uno o varios parámetros son invalido." + }; + + // Establecer credenciales. + var response = await serviceAuth.Authenticate(new() + { + User = user, + Password = password, + Application = application + }); + + // Validación al obtener el usuario + switch (response.Response) + { + // Correcto + case Responses.Success: + break; + + // No existe esta cuenta. + case Responses.NotExistAccount: + return new() + { + Response = Responses.NotExistAccount, + Message = "No existe esta cuenta." + }; + + // Contraseña invalida. + case Responses.InvalidPassword: + return new() + { + Response = Responses.InvalidPassword, + Message = "Contraseña incorrecta." + }; + + // Contraseña invalida. + case Responses.UnauthorizedByApp: + return new() + { + Response = Responses.UnauthorizedByApp, + Message = "La aplicación no existe o no permite que inicies sesión en este momento.", + Errors = response.Errors + }; + + // Contraseña invalida. + case Responses.UnauthorizedByOrg: + return new() + { + Response = Responses.UnauthorizedByOrg, + Message = "Tu organización no permite que inicies sesión en este momento.", + Errors = response.Errors + }; + + // Incorrecto + default: + return new() + { + Response = Responses.Undefined, + Message = "Hubo un error grave." + }; + } + + // Genera el token + var token = serviceAuth.GenerateToken(); + + // Respuesta. + var http = new ReadOneResponse + { + Model = serviceAuth.Account!, + Response = Responses.Success, + Token = token + }; + + return http; + } + + + /// + /// Refrescar sesión en una cuenta de usuario. + /// + /// Retorna el modelo de la cuenta. + [HttpGet("LoginWithToken")] + [IdentityToken] + public async Task> LoginWithToken() + { + // Obtiene el usuario. + var response = await accountData.Read(UserInformation.AccountId, new QueryObjectFilter() + { + IsAdmin = true, + FindOn = FindOn.StableAccounts + }); + + if (response.Response != Responses.Success) + return new(response.Response); + + response.Token = Token; + return response; + } + + + /// + /// Iniciar sesión con un tercero. + /// + /// Token de acceso a tercero. + [HttpGet("ThirdParty")] + public async Task> LoginWith([FromHeader] string token, [FromHeader] IdentityService provider, [FromHeader] string application) + { + + if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(application)) + return new(Responses.InvalidParam) + { + Message = "Uno o varios parámetros son invalido." + }; + + // Validar información del token. + var request = new AuthenticationRequest + { + ThirdPartyToken = token, + Service = provider, + StrictService = true, + Application = application + }; + + var response = await serviceAuth.Authenticate(request); + + if (response.Response != Responses.Success) + return new() + { + Response = response.Response, + Message = response.Message, + Errors = response.Errors + }; + + // Respuesta. + var http = new ReadOneResponse + { + Model = serviceAuth.Account!, + Response = Responses.Success, + Token = serviceAuth.GenerateToken() + }; + + return http; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Authentication/AuthenticationV4Controller.cs b/LIN.Cloud.Identity/Areas/Authentication/AuthenticationV4Controller.cs new file mode 100644 index 0000000..55bca60 --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Authentication/AuthenticationV4Controller.cs @@ -0,0 +1,106 @@ +namespace LIN.Cloud.Identity.Areas.Authentication; + +[Route("V4/[controller]")] +public class AuthenticationV4Controller(IAuthenticationAccountService serviceAuth) : AuthenticationBaseController +{ + + /// + /// Iniciar sesión en una cuenta de usuario. + /// + /// Unique. + /// Contraseña. + /// token de la app.. + /// Retorna el modelo de la cuenta y el token de acceso. + [HttpGet("login")] + public async Task> Login([FromQuery] string user, [FromQuery] string password, [FromHeader] string token) + { + + // Validación de parámetros. + if (string.IsNullOrWhiteSpace(user) || string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(token)) + return new(Responses.InvalidParam) + { + Message = "Uno o varios parámetros son invalido." + }; + + // Validar el token + int appId = JwtApplicationsService.Validate(token); + + if (appId <= 0) + return new(Responses.Unauthorized) + { + Message = "El token no es valido." + }; + + // Establecer credenciales. + var response = await serviceAuth.Authenticate(new() + { + User = user, + Password = password, + ApplicationId = appId + }); + + // Validación al obtener el usuario + switch (response.Response) + { + // Correcto + case Responses.Success: + break; + + // No existe esta cuenta. + case Responses.NotExistAccount: + return new() + { + Response = Responses.NotExistAccount, + Message = "No existe esta cuenta." + }; + + // Contraseña invalida. + case Responses.InvalidPassword: + return new() + { + Response = Responses.InvalidPassword, + Message = "Contraseña incorrecta." + }; + + // Contraseña invalida. + case Responses.UnauthorizedByApp: + return new() + { + Response = Responses.UnauthorizedByApp, + Message = "La aplicación no existe o no permite que inicies sesión en este momento.", + Errors = response.Errors + }; + + // Contraseña invalida. + case Responses.UnauthorizedByOrg: + return new() + { + Response = Responses.UnauthorizedByOrg, + Message = "Tu organización no permite que inicies sesión en este momento.", + Errors = response.Errors + }; + + // Incorrecto + default: + return new() + { + Response = Responses.Undefined, + Message = "Hubo un error grave." + }; + } + + // Genera el token + var tokenGen = serviceAuth.GenerateToken(); + + // Respuesta. + var http = new ReadOneResponse + { + Model = serviceAuth.Account!, + Response = Responses.Success, + Token = tokenGen + }; + + return http; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Authentication/IntentsController.cs b/LIN.Cloud.Identity/Areas/Authentication/IntentsController.cs new file mode 100644 index 0000000..10f420a --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Authentication/IntentsController.cs @@ -0,0 +1,58 @@ +using LIN.Cloud.Identity.Services.Realtime; + +namespace LIN.Cloud.Identity.Areas.Authentication; + +[IdentityToken] +[Route("[controller]")] +public class IntentsController(IAccountLogRepository passkeyData) : AuthenticationBaseController +{ + + /// + /// Obtiene la lista de intentos de llaves de paso están activos. + /// + [HttpGet] + public HttpReadAllResponse GetAll() + { + try + { + // Cuenta. + var account = (from a in PassKeyHub.Attempts + where a.Key.Equals(UserInformation.Unique, StringComparison.CurrentCultureIgnoreCase) + select a).FirstOrDefault().Value ?? []; + + // Hora actual. + var timeNow = DateTime.UtcNow; + + // Intentos. + var intentos = (from I in account + where I.Status == PassKeyStatus.Undefined + where I.Expiration > timeNow + select I).ToList(); + + // Retorna. + return new(Responses.Success, intentos); + } + catch (Exception) + { + return new(Responses.Undefined) + { + Message = "Hubo un error al obtener los intentos de passkey" + }; + } + } + + + /// + /// Obtiene la lista de intentos de llaves de paso están activos. + /// + [HttpGet("count")] + public async Task> Count() + { + // Contar. + var countResponse = await passkeyData.Count(UserInformation.AccountId); + + // Retorna + return countResponse; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Authentication/SecurityController.cs b/LIN.Cloud.Identity/Areas/Authentication/SecurityController.cs new file mode 100644 index 0000000..f1aa0a5 --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Authentication/SecurityController.cs @@ -0,0 +1,216 @@ +namespace LIN.Cloud.Identity.Areas.Authentication; + +[Route("[controller]")] +public class SecurityController(IAccountRepository accountsData, IOtpRepository otpService, IMailRepository mailRepository, EmailSender emailSender) : AuthenticationBaseController +{ + + /// + /// Agregar un correo a una cuenta. + /// + /// Correo. + [HttpPost("mail")] + [IdentityToken] + public async Task AddMail([FromQuery] string email) + { + // Generar modelo del correo. + var model = new MailModel() + { + Mail = email, + AccountId = UserInformation.AccountId, + IsPrincipal = false, + IsVerified = false + }; + + // Respuesta. + var responseCreate = await mailRepository.Create(model); + + // Si hubo un error. + switch (responseCreate.Response) + { + // Correcto. + case Responses.Success: + break; + + // Ya estaba registrado. + case Responses.ResourceExist: + return new(responseCreate.Response) + { + Message = $"Hubo un error al agregar el correo <{email}> a la cuenta con identidad: '{UserInformation.IdentityId}'", + Errors = [ + new() { + Tittle = "Mail duplicado", + Description = "El correo ya se encuentra registrado en el sistema." + } + ] + }; + default: + return new(responseCreate.Response) + { + Message = $"Hubo un error al agregar el correo <{email}> a la cuenta {model.Account.Identity.Unique}" + }; + } + + // Generar Otp. + var otpCode = Global.Utilities.KeyGenerator.GenerateOTP(5); + + // Guardar OTP. + var otpCreateResponse = await otpService.Create(new MailOtpDatabaseModel + { + MailModel = responseCreate.Model, + OtpDatabaseModel = new() + { + Account = new() { Id = UserInformation.AccountId }, + Code = otpCode, + ExpireTime = DateTime.UtcNow.AddMinutes(10), + IsUsed = false + } + }); + + // Enviar correo de verificación. + if (otpCreateResponse.Response != Responses.Success) + return new() + { + Message = "Hubo un error al guardar el código OTP." + }; + + // Enviar correo. + var success = await emailSender.Send(email, "Verificar", $"Verificar tu correo {otpCode}"); + + return new(success ? Responses.Success : Responses.UnavailableService); + + } + + + /// + /// Validar un correo. + /// + /// Correo a validar. + /// Código OTP. + [HttpPost("validate")] + public async Task Validate([FromQuery] string mail, [FromQuery] string code) + { + // Validar OTP. + var response = await mailRepository.ValidateOtpForMail(mail, code); + return response; + } + + + /// + /// Si un usuario olvido la contraseña. + /// + /// Usuario que olvido. + [HttpPost("forget/password")] + public async Task ForgetPassword([FromQuery] string user) + { + + // Validar estado del usuario. + var account = await accountsData.Read(user, new() + { + FindOn = FindOn.StableAccounts, + IncludePhoto = false + }); + + if (account.Response != Responses.Success) + return new(Responses.NotExistAccount) + { + Message = "No se puede reestablecer la contraseña de esta cuenta debido a que no existe o esta inactiva." + }; + + // Obtener mail principal. + var mail = await mailRepository.ReadPrincipal(user); + + if (mail.Response != Responses.Success) + return new(Responses.NotRows) + { + Message = "Esta cuenta no tiene un correo principal establecido." + }; + + // Generar OTP. + var otpCode = Global.Utilities.KeyGenerator.GenerateOTP(5); + + // Guardar OTP. + var modelo = new OtpDatabaseModel + { + Account = account.Model, + AccountId = account.Model.Id, + Code = otpCode, + ExpireTime = DateTime.UtcNow.AddMinutes(10), + IsUsed = false + }; + + // Crear OTP. + var created = await otpService.Create(modelo); + + // Si hubo un error. + if (created.Response != Responses.Success) + return new(created.Response) + { + Message = "No se pudo crear el código de verificación." + }; + + // Enviar mail. + var success = await emailSender.Send(mail.Model.Mail, "Recuperación de contraseña", $"Su código de verificación es: {otpCode}"); + + return new(success ? Responses.Success : Responses.UnavailableService); + + } + + + /// + /// Reestablecer una contraseña. + /// + /// Código OTP. + /// Usuario único. + /// Nueva contraseña. + [HttpPost("reset")] + public async Task Reset([FromQuery] string code, [FromQuery] string unique, [FromQuery] string newPassword) + { + + // Validar nueva contraseña. + if (newPassword is null || newPassword.Length <= 0) + return new(Responses.InvalidParam) + { + Errors = [] + }; + + // Validar OTP. + var account = await accountsData.Read(unique, new() + { + FindOn = FindOn.StableAccounts, + IncludePhoto = false + }); + + // Si hubo un error al obtener la cuenta. + if (account.Response != Responses.Success) + return new(Responses.NotExistAccount) + { + Message = "No se puede reestablecer la contraseña de esta cuenta debido a que no existe o esta inactiva." + }; + + // Leer y actualizar OTP. + var response = await otpService.ReadAndUpdate(account.Model.Id, code); + + // Si hubo un error al leer y actualizar el código. + if (response.Response != Responses.Success) + return new(Responses.Unauthorized) + { + Message = "No se pudo reestablecer la contraseña.", + Errors = [ + new() + { + Tittle = "Código OTP invalido", + Description = "El código es invalido o ya venció." + } + ] + }; + + // Encriptar contraseña. + newPassword = Global.Utilities.Cryptography.Encrypt(newPassword); + + // Actualizar contraseña. + var update = await accountsData.UpdatePassword(account.Model.Id, newPassword); + + return update; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/AuthenticationBaseController.cs b/LIN.Cloud.Identity/Areas/AuthenticationBaseController.cs new file mode 100644 index 0000000..d2df135 --- /dev/null +++ b/LIN.Cloud.Identity/Areas/AuthenticationBaseController.cs @@ -0,0 +1,17 @@ +namespace LIN.Cloud.Identity.Areas; + +public class AuthenticationBaseController : ControllerBase +{ + + /// + /// Información de autenticación. + /// + public JwtModel UserInformation => HttpContext.Items["authentication"] as JwtModel ?? new(); + + + /// + /// Obtener el token desde el header. + /// + public string Token => HttpContext.Request.Headers["token"].ToString() ?? string.Empty; + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Groups/GroupsController.cs b/LIN.Cloud.Identity/Areas/Groups/GroupsController.cs new file mode 100644 index 0000000..7f5d7c0 --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Groups/GroupsController.cs @@ -0,0 +1,191 @@ +namespace LIN.Cloud.Identity.Areas.Groups; + +[IdentityToken] +[Route("[controller]")] +public class GroupsController(IGroupRepository groupData, IIamService rolesIam) : AuthenticationBaseController +{ + + /// + /// Crear nuevo grupo. + /// + /// Modelo del grupo. + [HttpPost] + public async Task Create([FromBody] GroupModel group) + { + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, group.Identity.OwnerId ?? 0); + + // Iam. + bool iam = ValidateRoles.ValidateAlterMembers(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso para crear grupos.", + Response = Responses.Unauthorized + }; + + // Formato de la identidad. + group.Identity.Type = IdentityType.Group; + Services.Formats.Identities.Process(group.Identity); + + // Obtener el modelo. + var response = await groupData.Create(group); + + // Si es erróneo + if (response.Response != Responses.Success) + return new() + { + Response = response.Response + }; + + // Retorna el resultado + return new CreateResponse() + { + Response = Responses.Success, + LastId = response.Model.Id + }; + + } + + + /// + /// Obtener todos los grupos de una organización. + /// + /// Id de la organización. + /// Retorna la lista de grupos. + [HttpGet("all")] + public async Task> ReadAll([FromHeader] int organization) + { + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, organization); + + // Iam. + bool iam = ValidateRoles.ValidateRead(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso para leer grupos.", + Response = Responses.Unauthorized + }; + + // Obtener el modelo. + var response = await groupData.ReadAll(organization); + + // Si es erróneo + if (response.Response != Responses.Success) + return new() + { + Response = response.Response + }; + + // Retorna el resultado + return response; + + } + + + /// + /// Obtener un grupo. + /// + /// Id del grupo. + /// Retorna el modelo del grupo. + [HttpGet] + public async Task> ReadOne([FromHeader] int id) + { + + // Obtener la organización. + var orgId = await groupData.GetOwner(id); + + // Si hubo un error. + if (orgId.Response != Responses.Success) + return new() + { + Message = "Hubo un error al encontrar la organización dueña de este grupo.", + Response = Responses.Unauthorized + }; + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, orgId.Model); + + // Iam. + bool iam = ValidateRoles.ValidateRead(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso para leer grupos.", + Response = Responses.Unauthorized + }; + + // Obtener el modelo. + var response = await groupData.Read(id); + + // Si es erróneo + if (response.Response != Responses.Success) + return new() + { + Response = response.Response + }; + + // Retorna el resultado + return response; + + } + + + /// + /// Obtener un grupo. + /// + /// Id de la identidad del grupo. + /// Retorna el modelo del grupo. + [HttpGet("identity")] + public async Task> ReadIdentity([FromHeader] int id) + { + + // Obtener la organización. + var orgId = await groupData.GetOwnerByIdentity(id); + + // Si hubo un error. + if (orgId.Response != Responses.Success) + return new() + { + Message = "Hubo un error al encontrar la organización dueña de este grupo.", + Response = Responses.Unauthorized + }; + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, orgId.Model); + + // Iam. + bool iam = ValidateRoles.ValidateRead(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso para leer grupos.", + Response = Responses.Unauthorized + }; + + // Obtener el modelo. + var response = await groupData.ReadByIdentity(id); + + // Si es erróneo + if (response.Response != Responses.Success) + return new() + { + Response = response.Response + }; + + // Retorna el resultado + return response; + + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Groups/GroupsMembersController.cs b/LIN.Cloud.Identity/Areas/Groups/GroupsMembersController.cs new file mode 100644 index 0000000..5583b85 --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Groups/GroupsMembersController.cs @@ -0,0 +1,368 @@ +namespace LIN.Cloud.Identity.Areas.Groups; + +[IdentityToken] +[Route("Groups/members")] +public class GroupsMembersController(IGroupRepository groupsData, IOrganizationMemberRepository directoryMembersData, IGroupMemberRepository groupMembers, IIamService rolesIam) : AuthenticationBaseController +{ + + /// + /// Agregar un integrante a un grupo. + /// + /// Modelo del integrante. + /// Retorna el id del nuevo integrante. + [HttpPost] + public async Task Create([FromBody] GroupMember model) + { + + // Obtener la organización. + var orgId = await groupsData.GetOwner(model.GroupId); + + // Si hubo un error. + if (orgId.Response != Responses.Success) + return new() + { + Message = "Hubo un error al encontrar la organización dueña de este grupo.", + Response = Responses.Unauthorized + }; + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, orgId.Model); + + // Iam. + bool iam = ValidateRoles.ValidateAlterMembers(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso a la información este directorio.", + Response = Responses.Unauthorized + }; + + // Valida si el usuario pertenece a la organización. + var idIsIn = await directoryMembersData.IamIn(model.IdentityId, orgId.Model); + + // Si no existe. + if (idIsIn.Response != Responses.Success) + return new() + { + Message = $"La identidad {model.IdentityId} no pertenece al directorio de la organización {orgId.Model}.", + Response = Responses.Unauthorized + }; + + // Crear el usuario. + var response = await groupMembers.Create(model); + + // Retorna el resultado + return response; + + } + + + /// + /// Agregar integrantes a un grupo. + /// + /// Id del grupo. + /// Lista de las identidades. + /// Retorna la respuesta del proceso. + [HttpPost("list")] + public async Task Create([FromHeader] int group, [FromBody] List ids) + { + + // Obtener la organización. + var orgId = await groupsData.GetOwner(group); + + // Si hubo un error. + if (orgId.Response != Responses.Success) + return new() + { + Message = "Hubo un error al encontrar la organización dueña de este grupo.", + Response = Responses.Unauthorized + }; + + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, orgId.Model); + + // Iam. + bool iam = ValidateRoles.ValidateAlterMembers(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso a la información este directorio.", + Response = Responses.Unauthorized + }; + + // Solo elementos distintos. + ids = ids.Distinct().ToList(); + + //// Valida si el usuario pertenece a la organización. + //var (successIds, failureIds) = await directoryMembersData.IamIn(ids, orgId.Model); + + //// Crear el usuario. + //var response = await groupMembers.Create(successIds.Select(id => new GroupMember + //{ + // Group = new() + // { + // Id = group, + // }, + // Identity = new() + // { + // Id = id + // } + //})); + + // response.Message = $"Se agregaron {successIds.Count()} integrantes y se omitieron {failureIds.Count} debido a que no pertenecen a esta organización."; + + // Retorna el resultado + // return response; + return new() + { + Message = "No se implementó la función de agregar múltiples integrantes." + }; + } + + + /// + /// Obtener los integrantes asociados a un grupo. + /// + /// ID del grupo. + [HttpGet("read/all")] + public async Task> ReadMembers([FromQuery] int group) + { + + // Obtener la organización. + var orgId = await groupsData.GetOwner(group); + + // Si hubo un error. + if (orgId.Response != Responses.Success) + return new() + { + Message = "Hubo un error al encontrar la organización dueña de este grupo.", + Response = Responses.Unauthorized + }; + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, orgId.Model); + + // Iam. + bool iam = ValidateRoles.ValidateRead(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso a la información este directorio.", + Response = Responses.Unauthorized + }; + + + // Obtiene el usuario. + var response = await groupMembers.ReadAll(group); + + // Si es erróneo + if (response.Response != Responses.Success) + return new() + { + Response = response.Response + }; + + // Retorna el resultado + return response; + + } + + + /// + /// Buscar en los integrantes de un grupo. + /// + /// Grupo. + /// Patron de búsqueda. + [HttpGet("search")] + public async Task> Search([FromHeader] int group, [FromQuery] string pattern) + { + // Obtener la organización. + var orgId = await groupsData.GetOwner(group); + + // Si hubo un error. + if (orgId.Response != Responses.Success) + return new() + { + Message = "Hubo un error al encontrar la organización dueña de este grupo.", + Response = Responses.Unauthorized + }; + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, orgId.Model); + + // Iam. + bool iam = ValidateRoles.ValidateRead(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso a la información este directorio.", + Response = Responses.Unauthorized + }; + + + // Obtiene los miembros. + var members = await groupMembers.Search(pattern, group); + + // Error al obtener los integrantes. + if (members.Response != Responses.Success) + return new() + { + Message = "Error.", + Response = Responses.NotRows + }; + + // Retorna el resultado + return members; + + } + + + /// + /// Buscar en los grupos de un grupo. + /// + /// Grupo. + /// Patron de búsqueda. + [HttpGet("search/groups")] + public async Task> SearchOnGroups([FromHeader] int group, [FromQuery] string pattern) + { + + // Obtener la organización. + var orgId = await groupsData.GetOwner(group); + + // Si hubo un error. + if (orgId.Response != Responses.Success) + return new() + { + Message = "Hubo un error al encontrar la organización dueña de este grupo.", + Response = Responses.Unauthorized + }; + + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, orgId.Model); + + // Iam. + bool iam = ValidateRoles.ValidateRead(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso a la información este directorio.", + Response = Responses.Unauthorized + }; + + // Obtiene los miembros. + var members = await groupMembers.Search(pattern, group); + + // Error al obtener los integrantes. + if (members.Response != Responses.Success) + return new() + { + Message = "Error.", + Response = Responses.NotRows + }; + + // Retorna el resultado + return members; + + } + + + /// + /// Eliminar un integrante + /// + /// ID del grupo. + [HttpDelete("remove")] + public async Task DeleteMembers([FromQuery] int identity, [FromQuery] int group) + { + + // Obtener la organización. + var orgId = await groupsData.GetOwner(group); + + // Si hubo un error. + if (orgId.Response != Responses.Success) + return new() + { + Message = "Hubo un error al encontrar la organización dueña de este grupo.", + Response = Responses.Unauthorized + }; + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, orgId.Model); + + // Iam. + bool iam = ValidateRoles.ValidateAlterMembers(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso a la información este directorio.", + Response = Responses.Unauthorized + }; + + // Obtiene el usuario. + var response = await groupMembers.Delete(identity, group); + + // Si es erróneo + if (response.Response != Responses.Success) + return new() + { + Response = response.Response + }; + + // Retorna el resultado + return response; + + } + + + /// + /// Obtener los grupos a los que una identidad pertenece. + /// + /// ID del grupo. + [HttpGet("read/on/all")] + public async Task> OnMembers([FromQuery] int organization, [FromQuery] int identity) + { + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, organization); + + // Iam. + bool iam = ValidateRoles.ValidateRead(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso a la información este directorio.", + Response = Responses.Unauthorized + }; + + + // Obtiene el usuario. + var response = await groupMembers.OnMembers(organization, identity); + + // Si es erróneo + if (response.Response != Responses.Success) + return new() + { + Response = response.Response + }; + + // Retorna el resultado + return response; + + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Organizations/DomainController.cs b/LIN.Cloud.Identity/Areas/Organizations/DomainController.cs new file mode 100644 index 0000000..5cd51f3 --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Organizations/DomainController.cs @@ -0,0 +1,138 @@ +namespace LIN.Cloud.Identity.Areas.Organizations; + +[IdentityToken] +[Route("[controller]")] +public class DomainController(IDomainRepository domainRepository, IIamService iamService, IDomainService domainService) : AuthenticationBaseController +{ + + /// + /// Agrega un dominio a una organización. + /// + [HttpPost] + public async Task AddDomain([FromHeader] int organization, [FromQuery] string domain) + { + + // Validar dominio sea valido. + if (!domainService.VerifyDomain(domain)) + return new(Responses.InvalidParam) + { + Message = "El dominio no es valido." + }; + + // Validar si el usuario tiene permisos sobre la organización. + var roles = await iamService.Validate(base.UserInformation.IdentityId, organization); + + if (!roles.ValidateAlterDomain()) + return new(Responses.Unauthorized) + { + Message = "No tienes permisos para agregar un dominio a esta organización." + }; + + // Código de verificación para el dominio. + string verificationCode = Guid.NewGuid().ToString("N").ToLowerInvariant(); + + // Crear el dominio en la organización. + var modelo = new DomainModel + { + Organization = new() { Id = organization }, + Domain = domain.ToLowerInvariant(), + IsVerified = false, + VerificationCode = verificationCode + }; + + var response = await domainRepository.Create(modelo); + + if (response.Response != Responses.Success) + return new(Responses.Unauthorized) + { + Message = "No se pudo crear el dominio. Intenta nuevamente." + }; + + // Retornar el código para el TXT del dominio. + return new(Responses.Success) + { + LastUnique = verificationCode, + Message = "Dominio creado correctamente. Agrega el TXT con el código de verificación en tu dominio." + }; + } + + + /// + /// Obtener los dominios de una organización. + /// + [HttpGet] + public async Task> ReadAll([FromHeader] int organization) + { + + // Validar si el usuario tiene permisos sobre la organización. + var roles = await iamService.Validate(UserInformation.IdentityId, organization); + + if (!roles.ValidateAlterDomain()) + return new(Responses.Unauthorized) + { + Message = "No tienes permisos para agregar un dominio a esta organización." + }; + + // Obtener los dominios de la organización. + var domainsResponse = await domainRepository.ReadAll(organization); + + return domainsResponse; + } + + + /// + /// Verifica un dominio de una organización. + /// + [HttpPatch] + public async Task Verify([FromHeader] int organization, [FromQuery] string domain) + { + // Validar dominio sea valido. + if (!domainService.VerifyDomain(domain)) + return new(Responses.InvalidParam) + { + Message = "El dominio no es valido." + }; + + // Validar si el usuario tiene permisos sobre la organización. + var roles = await iamService.Validate(base.UserInformation.IdentityId, organization); + + if (!roles.ValidateAlterDomain()) + return new(Responses.Unauthorized) + { + Message = "No tienes permisos para agregar un dominio a esta organización." + }; + + // Obtener el dominio desde la base de datos. + var response = await domainRepository.Read(domain.ToLowerInvariant()); + + if (response.Response != Responses.Success) + return new(Responses.NotRows) + { + Message = "El dominio no existe o no pertenece a la organización." + }; + + // Verificar el TXT. + var isSuccess = await domainService.VerifyDns(domain, response.Model.VerificationCode); + + if (!isSuccess) + return new(Responses.Unauthorized) + { + Message = "El dominio no ha sido verificado. Asegúrate de agregar el registro TXT con el código de verificación." + }; + + // Es valido, se marca el dominio como verificado. + var verifyResponse = await domainRepository.Verify(domain.ToLowerInvariant()); + + // Si no es valido, se retorna un error. + return verifyResponse.Response == Responses.Success + ? new(Responses.Success) + { + Message = "Dominio verificado correctamente." + } + : new(Responses.Unauthorized) + { + Message = "No se pudo verificar el dominio. Intenta nuevamente." + }; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Organizations/IdentityController.cs b/LIN.Cloud.Identity/Areas/Organizations/IdentityController.cs new file mode 100644 index 0000000..9ffd149 --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Organizations/IdentityController.cs @@ -0,0 +1,159 @@ +namespace LIN.Cloud.Identity.Areas.Organizations; + +[IdentityToken] +[Route("[controller]")] +public class IdentityController(IOrganizationMemberRepository directoryMembersData, IIdentityRolesRepository identityRolesData, IIamService rolesIam) : AuthenticationBaseController +{ + + /// + /// Crear nuevo rol en una identidad. + /// + [HttpPost] + public async Task Create([FromBody] IdentityRolesModel rolModel) + { + // Validar el modelo. + if (rolModel.Rol == Roles.None) + return new(Responses.InvalidParam); + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, rolModel.OrganizationId); + + // Iam. + bool iam = ValidateRoles.ValidateAlterMembers(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso para crear grupos.", + Response = Responses.Unauthorized + }; + + // Identidad. + rolModel.Identity = new() + { + Id = rolModel.IdentityId + }; + + // Organización. + rolModel.Organization = new() + { + Id = rolModel.OrganizationId + }; + + // Obtener el modelo. + var response = await identityRolesData.Create(rolModel); + + // Si es erróneo + if (response.Response != Responses.Success) + return new() + { + Response = response.Response + }; + + // Retorna el resultado + return new() + { + Response = Responses.Success + }; + + } + + + /// + /// Obtener los roles asociados a una identidad. + /// + /// Identidad + /// Id de la organización. + [HttpGet("roles/all")] + public async Task> ReadAll([FromHeader] int identity, [FromHeader] int organization) + { + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, organization); + + // Iam. + bool iam = ValidateRoles.ValidateRead(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso para leer permisos.", + Response = Responses.Unauthorized + }; + + var isIn = await directoryMembersData.IamIn(identity, organization); + + if (isIn.Response != Responses.Success) + return new() + { + Message = $"La identidad {identity} no pertenece a la organización de contexto.", + Response = Responses.NotFoundDirectory + }; + + // Obtener el modelo. + var response = await identityRolesData.ReadAll(identity, organization); + + // Si es erróneo + if (response.Response != Responses.Success) + return new() + { + Response = response.Response + }; + + // Retorna el resultado + return response; + + } + + + /// + /// Eliminar los roles asociados a una identidad. + /// + /// Identidad + /// Id de la organización. + /// Rol. + [HttpDelete("roles")] + public async Task ReadAll([FromHeader] int identity, [FromHeader] int organization, [FromHeader] Roles rol) + { + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, organization); + + // Iam. + bool iam = ValidateRoles.ValidateAlterMembers(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso para leer grupos.", + Response = Responses.Unauthorized + }; + + var isIn = await directoryMembersData.IamIn(identity, organization); + + if (isIn.Response != Responses.Success) + return new() + { + Message = $"La identidad {identity} no pertenece a la organización de contexto.", + Response = Responses.NotFoundDirectory + }; + + // Obtener el modelo. + var response = await identityRolesData.Remove(identity, rol, organization); + + // Si es erróneo + if (response.Response != Responses.Success) + return new() + { + Response = response.Response + }; + + // Retorna el resultado + return response; + + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Organizations/OrganizationController.cs b/LIN.Cloud.Identity/Areas/Organizations/OrganizationController.cs new file mode 100644 index 0000000..90dc81f --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Organizations/OrganizationController.cs @@ -0,0 +1,109 @@ +namespace LIN.Cloud.Identity.Areas.Organizations; + +[Route("[controller]")] +public class OrganizationsController(IOrganizationRepository organizationsData, IOrganizationMemberRepository directoryMembersData) : AuthenticationBaseController +{ + + /// + /// Crea una nueva organización. + /// + /// Modelo de la organización y el usuario administrador + [HttpPost] + public async Task Create([FromBody] OrganizationModel modelo) + { + + // Validar el modelo. + if (modelo == null || string.IsNullOrWhiteSpace(modelo.Name) || modelo.Directory == null || modelo.Directory.Identity == null || string.IsNullOrWhiteSpace(modelo.Directory.Identity.Unique)) + return new() + { + Response = Responses.InvalidParam, + Message = "Parámetros inválidos." + }; + + // Ordenar el modelo. + { + modelo.Id = 0; + modelo.Name = modelo.Name.Trim(); + modelo.Creation = DateTime.UtcNow; + modelo.Directory.Members = []; + modelo.Directory.Name = modelo.Directory.Name.Trim(); + modelo.Directory.Identity.EffectiveTime = DateTime.UtcNow; + modelo.Directory.Identity.CreationTime = DateTime.UtcNow; + modelo.Directory.Identity.EffectiveTime = DateTime.UtcNow.AddYears(10); + modelo.Directory.Identity.Status = IdentityStatus.Enable; + } + + // Creación de la organización. + var response = await organizationsData.Create(modelo); + + // Evaluación. + if (response.Response != Responses.Success) + return new(response.Response); + + // Retorna el resultado. + return new CreateResponse(Responses.Success, response.LastId); + } + + + /// + /// Obtiene una organización por medio del Id. + /// + /// ID de la organización + [HttpGet("read/id")] + [IdentityToken] + public async Task> ReadOneByID([FromQuery] int id) + { + + // Parámetros + if (id <= 0) + return new(Responses.InvalidParam); + + // Obtiene la organización + var response = await organizationsData.Read(id); + + // Organización no encontrada. + if (response.Response != Responses.Success) + return new ReadOneResponse() + { + Response = Responses.NotRows, + Message = "No se encontró la organización." + }; + + // No es publica y no pertenece a ella + if (!response.Model.IsPublic) + { + + var iamIn = await directoryMembersData.IamIn(UserInformation.IdentityId, response.Model.Id); + + if (iamIn.Response != Responses.Success) + return new ReadOneResponse() + { + Response = Responses.Unauthorized, + Message = "Esta organización es privada y tu usuario no esta vinculado a ella.", + Model = new() + { + Id = response.Model.Id, + IsPublic = false, + Name = "Organización privada" + } + }; + } + + return new ReadOneResponse(Responses.Success, response.Model); + } + + + /// + /// Obtiene las organizaciones donde un usuario es miembro. + /// + [HttpGet("read/all")] + [IdentityToken] + public async Task> ReadAll() + { + // Obtiene la organización + var response = await directoryMembersData.ReadAllMembers(UserInformation.IdentityId); + + return response; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Organizations/OrganizationMembersController.cs b/LIN.Cloud.Identity/Areas/Organizations/OrganizationMembersController.cs new file mode 100644 index 0000000..94685aa --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Organizations/OrganizationMembersController.cs @@ -0,0 +1,213 @@ +using LIN.Types.Cloud.Identity.Abstracts; + +namespace LIN.Cloud.Identity.Areas.Organizations; + +[IdentityToken] +[Route("orgs/members")] +public class OrganizationMembersController(IOrganizationRepository organizationsData, IAccountRepository accountsData, IOrganizationMemberRepository directoryMembersData, IGroupMemberRepository groupMembers, IIamService rolesIam) : AuthenticationBaseController +{ + + /// + /// Agregar una identidad externa a la organización. + /// + /// Id de la organización. + /// Lista de ids a agregar. + /// Retorna el resultado del proceso. + [HttpPost("invite")] + public async Task AddExternalMembers([FromQuery] int organization, [FromBody] IEnumerable ids) + { + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, organization); + + // Iam. + bool iam = ValidateRoles.ValidateInviteMembers(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes autorización para invitar entidades externas en esta organización.", + Response = Responses.Unauthorized + }; + + // Solo elementos distintos. + ids = ids.Distinct(); + + // Valida si el usuario pertenece a la organización. + var (existentes, noUpdated) = await directoryMembersData.IamIn(ids, organization); + + var directoryId = await organizationsData.ReadDirectory(organization); + + // Crear el usuario. + var response = await groupMembers.Create(noUpdated.Select(id => new GroupMember + { + Group = new() + { + Id = directoryId.Model, + }, + Identity = new() + { + Id = id + }, + Type = GroupMemberTypes.Guest + })); + + response.Message = $"Se agregaron {noUpdated.Count} integrantes como invitados y se omitieron {existentes.Count()} debido a que ya pertenecen a esta organización."; + + //// Retorna el resultado + return response; + } + + + /// + /// Crea un nuevo miembro en una organización. + /// + /// Modelo de la cuenta. + /// Id de la organización. + [HttpPost] + public async Task Create([FromBody] AccountModel modelo, [FromHeader] int organization) + { + + // Validar el modelo. + if (modelo == null || modelo.Identity == null || string.IsNullOrWhiteSpace(modelo.Identity.Unique) || string.IsNullOrWhiteSpace(modelo.Name)) + return new() + { + Response = Responses.InvalidParam, + Message = "Uno o varios parámetros inválidos." + }; + + // Ajustar el modelo. + modelo.Visibility = Visibility.Hidden; + modelo.Password = $"pwd@{DateTime.UtcNow.Year}"; + modelo = Services.Formats.Account.Process(modelo); + modelo.AccountType = AccountTypes.Work; + + // Organización. + var orgIdentity = await organizationsData.GetDomain(organization); + + // Validar. + if (orgIdentity.Response != Responses.Success) + return new(Responses.NotRows) + { + Message = $"No se encontró la organización con Id '{organization}'" + }; + + // Validar usuario y nombre. + var errors = Services.Formats.Account.Validate(modelo); + + // Si no fue valido. + if (errors.Count > 0) + return new(Responses.InvalidParam) + { + Message = "Error al crear la cuenta", + Errors = errors + }; + + // Agregar la identidad. + modelo.Identity.Unique = $"{modelo.Identity.Unique}@{orgIdentity.Model.Unique}"; + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, organization); + + // Iam. + bool iam = ValidateRoles.ValidateAlterMembers(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso para crear nuevos usuarios en esta organización.", + Response = Responses.Unauthorized + }; + + // Creación del usuario + var response = await accountsData.Create(modelo, organization); + + // Evaluación + if (response.Response != Responses.Success) + return new(response.Response); + + // Retorna el resultado + return new CreateResponse() + { + LastId = response.Model.Id, + Response = Responses.Success, + Message = "Success" + }; + + } + + + /// + /// Obtiene la lista de integrantes asociados a una organización. + /// + [HttpGet("accounts")] + public async Task>> ReadAll([FromHeader] int organization) + { + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, organization); + + // Iam. + bool iam = ValidateRoles.ValidateRead(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes acceso a la información este directorio.", + Response = Responses.Unauthorized + }; + + // Obtiene los miembros. + var members = await directoryMembersData.ReadUserAccounts(organization); + + // Error al obtener los integrantes. + if (members.Response != Responses.Success) + return new ReadAllResponse> + { + Message = "No se encontró la organización.", + Response = Responses.Unauthorized + }; + + // Retorna el resultado + return members; + } + + + /// + /// Agregar una identidad externa a la organización. + /// + /// Id de la organización. + /// Lista de ids a agregar. + /// Retorna el resultado del proceso. + [HttpPost("expulse")] + public async Task Expulse([FromQuery] int organization, [FromBody] IEnumerable ids) + { + + // Confirmar el rol. + var roles = await rolesIam.Validate(UserInformation.IdentityId, organization); + + // Iam. + bool iam = ValidateRoles.ValidateDelete(roles); + + // Si no tiene permisos. + if (!iam) + return new() + { + Message = "No tienes autorización para eliminar entidades externas en esta organización.", + Response = Responses.Unauthorized + }; + + // Solo elementos distintos. + ids = ids.Distinct(); + + // Valida si el usuario pertenece a la organización. + var (existentes, _) = await directoryMembersData.IamIn(ids, organization); + + // Expulsar a los miembros. + var response = await directoryMembersData.Expulse(existentes, organization); + return response; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Policies/PoliciesController.cs b/LIN.Cloud.Identity/Areas/Policies/PoliciesController.cs new file mode 100644 index 0000000..66ca81b --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Policies/PoliciesController.cs @@ -0,0 +1,103 @@ +namespace LIN.Cloud.Identity.Areas.Policies; + +[IdentityToken] +[Route("[controller]")] +public class PoliciesController(IPolicyRepository policiesData, IIamService iam) : AuthenticationBaseController +{ + + /// + /// Crear nueva política. + /// + /// Modelo de la identidad. + [HttpPost] + public async Task Create([FromBody] PolicyModel modelo, [FromHeader] int organization) + { + // Validar nivel de acceso y roles sobre la organización. + var validate = await iam.Validate(UserInformation.IdentityId, organization); + + if (!validate.ValidateAlterPolicies()) + return new(Responses.Unauthorized) + { + Message = $"No tienes permisos para crear políticas a titulo de la organización #{organization}." + }; + + // Limpiar modelo. + modelo.Owner = new() { Id = organization }; + modelo.CreatedBy = new() { Id = UserInformation.IdentityId }; + modelo.CreatedAt = DateTime.UtcNow; + modelo.Id = 0; + modelo.Name = modelo.Name.Trim(); + + // Crear la política. + var response = await policiesData.Create(modelo); + return response; + } + + + /// + /// Obtener una política. + /// + /// Id. + [HttpGet] + public async Task> Read([FromHeader] int policyId) + { + // Validar nivel de acceso y roles sobre la organización. + var validate = await iam.IamPolicy(UserInformation.IdentityId, policyId); + + if (validate != IamLevels.Privileged) + return new(Responses.Unauthorized) + { + Message = $"No tienes permisos para obtener esta política." + }; + + // Crear la política. + var response = await policiesData.Read(policyId, true); + return response; + } + + + /// + /// Buscar políticas por nombre. + /// + [HttpGet("search")] + public async Task> Search([FromQuery] string query, [FromHeader] int organization) + { + + // Validar nivel de acceso y roles sobre la organización. + var validate = await iam.Validate(UserInformation.IdentityId, organization); + + if (!validate.ValidateReadPolicies()) + return new(Responses.Unauthorized) + { + Message = $"No tienes permisos para obtener políticas a titulo de la organización #{organization}." + }; + + // Crear la política. + var response = await policiesData.ReadAll(organization, query); + return response; + } + + + /// + /// Obtener las políticas asociadas a una organización. + /// + /// Id de la organización. + [HttpGet("all")] + public async Task> ReadAll([FromHeader] int organization) + { + + // Validar nivel de acceso y roles sobre la organización. + var validate = await iam.Validate(UserInformation.IdentityId, organization); + + if (!validate.ValidateReadPolicies()) + return new(Responses.Unauthorized) + { + Message = $"No tienes permisos para obtener políticas a titulo de la organización #{organization}." + }; + + // Crear la política. + var response = await policiesData.ReadAll(organization, false); + return response; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Areas/Policies/PoliciesIdentityController.cs b/LIN.Cloud.Identity/Areas/Policies/PoliciesIdentityController.cs new file mode 100644 index 0000000..e622e36 --- /dev/null +++ b/LIN.Cloud.Identity/Areas/Policies/PoliciesIdentityController.cs @@ -0,0 +1,54 @@ +namespace LIN.Cloud.Identity.Areas.Policies; + +[IdentityToken] +[Route("[controller]")] +public class PoliciesIdentityController(IPolicyMemberRepository policiesData, IIamService iam) : AuthenticationBaseController +{ + + /// + /// Asociar política. + /// + /// Modelo de la identidad. + [HttpPost] + public async Task Create([FromBody] IdentityPolicyModel modelo) + { + // Validar nivel de acceso y roles sobre la organización. + var validate = await iam.IamIdentity(UserInformation.IdentityId, modelo.IdentityId); + + if (!validate.ValidateAlterPolicies()) + return new(Responses.Unauthorized) + { + Message = $"No tienes permisos modificar la identidad y agregarla a una política." + }; + + // Ajustar modelo. + modelo.Policy = new() { Id = modelo.PolicyId }; + modelo.Identity = new() { Id = modelo.IdentityId }; + + // Crear la política. + var response = await policiesData.Create(modelo); + return response; + } + + + /// + /// Obtener las políticas asociadas a una identidad. + /// + [HttpGet("all")] + public async Task> ReadAll([FromHeader] int identity) + { + // Validar nivel de acceso y roles sobre la organización. + var validate = await iam.IamIdentity(UserInformation.IdentityId, identity); + + if (!validate.ValidateReadPolicies()) + return new(Responses.Unauthorized) + { + Message = $"No tienes permisos para obtener políticas a titulo de la organización." + }; + + // Crear la política. + var response = await policiesData.ReadAll(identity); + return response; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/LIN.Cloud.Identity.csproj b/LIN.Cloud.Identity/LIN.Cloud.Identity.csproj new file mode 100644 index 0000000..7286381 --- /dev/null +++ b/LIN.Cloud.Identity/LIN.Cloud.Identity.csproj @@ -0,0 +1,23 @@ + + + + net9.0 + enable + enable + false + Debug;Release;Local;Release-dev;Debug-dev + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LIN.Cloud.Identity/Program.cs b/LIN.Cloud.Identity/Program.cs new file mode 100644 index 0000000..96196c6 --- /dev/null +++ b/LIN.Cloud.Identity/Program.cs @@ -0,0 +1,39 @@ +using Http.Extensions; +using Http.Extensions.OpenApi; +using LIN.Cloud.Identity.Services.Realtime; + +var builder = WebApplication.CreateBuilder(args); + +// Servicios de contenedor. +builder.Services.AddLINHttp(true, (options) => +{ + options.OperationFilter>("token", "Token de acceso a LIN Cloud Identity"); +}); + +builder.Services.AddSignalR(); +builder.Services.AddLocalServices(); +builder.Services.AddAuthenticationServices(builder.Configuration); + +// Servicio de autenticación. +builder.Services.AddPersistence(builder.Configuration); + +var app = builder.Build(); +app.UseLINHttp(); + +// Base de datos. +app.UseDataBase(); + +// Hub. +app.MapHub("/realTime/auth/passkey"); + +app.UseAuthorization(); +app.MapControllers(); + +builder.Services.AddDatabaseAction(() => +{ + var context = app.Services.GetRequiredService(); + context.Accounts.Where(x => x.Id == 0).FirstOrDefaultAsync(); + return "Success"; +}); + +app.Run(); \ No newline at end of file diff --git a/LIN.Cloud.Identity/Properties/launchSettings.json b/LIN.Cloud.Identity/Properties/launchSettings.json new file mode 100644 index 0000000..932be78 --- /dev/null +++ b/LIN.Cloud.Identity/Properties/launchSettings.json @@ -0,0 +1,39 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5166" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7064;http://localhost:5166" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true + }, + "WSL": { + "commandName": "WSL2", + "launchBrowser": true, + "launchUrl": "https://localhost:7064/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "https://localhost:7064;http://localhost:5166" + }, + "distributionName": "" + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:11415", + "sslPort": 44359 + } + } +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Services/Extensions/LocalServices.cs b/LIN.Cloud.Identity/Services/Extensions/LocalServices.cs new file mode 100644 index 0000000..fa8d71f --- /dev/null +++ b/LIN.Cloud.Identity/Services/Extensions/LocalServices.cs @@ -0,0 +1,17 @@ +namespace LIN.Cloud.Identity.Services.Extensions; + +public static class LocalServices +{ + + /// + /// Agregar servicios locales. + /// + /// Services. + public static IServiceCollection AddLocalServices(this IServiceCollection services) + { + // Externos + services.AddSingleton(); + return services; + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Services/Filters/IdentityTokenAttribute.cs b/LIN.Cloud.Identity/Services/Filters/IdentityTokenAttribute.cs new file mode 100644 index 0000000..184b2e1 --- /dev/null +++ b/LIN.Cloud.Identity/Services/Filters/IdentityTokenAttribute.cs @@ -0,0 +1,48 @@ +using LIN.Types.Models; + +namespace LIN.Cloud.Identity.Services.Filters; + +public class IdentityTokenAttribute : ActionFilterAttribute +{ + + /// + /// Filtro del token. + /// + /// Contexto HTTP. + /// Siguiente. + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // Contexto HTTP. + var httpContext = context.HttpContext; + + // Obtiene el valor. + bool can = httpContext.Request.Headers.TryGetValue("token", out Microsoft.Extensions.Primitives.StringValues value); + + // Información del token. + var tokenInfo = JwtService.Validate(value.ToString()); + + // Error de autenticación. + if (!can || !tokenInfo.IsAuthenticated) + { + httpContext.Response.StatusCode = 401; + await httpContext.Response.WriteAsJsonAsync(new ResponseBase() + { + Message = "Token invalido.", + Errors = [ + new ErrorModel() + { + Tittle = "Token invalido", + Description = "El token proporcionado en el header es invalido." + } + ], + Response = Responses.Unauthorized + }); + return; + } + + // Agrega la información del token. + context.HttpContext.Items.Add("authentication", tokenInfo); + await base.OnActionExecutionAsync(context, next); + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Services/Formats/Account.cs b/LIN.Cloud.Identity/Services/Formats/Account.cs new file mode 100644 index 0000000..bf96e14 --- /dev/null +++ b/LIN.Cloud.Identity/Services/Formats/Account.cs @@ -0,0 +1,91 @@ +using LIN.Types.Models; +using IdentityService = LIN.Types.Cloud.Identity.Enumerations.IdentityService; + +namespace LIN.Cloud.Identity.Services.Formats; + +public class Account +{ + + /// + /// Procesar el modelo. + /// + /// Modelo + public static List Validate(AccountModel baseAccount) + { + + List errors = []; + + if (string.IsNullOrWhiteSpace(baseAccount.Name)) + errors.Add(new ErrorModel() + { + Tittle = "Nombre invalido", + Description = "El nombre del usuario no puede estar vacío.", + }); + + if (baseAccount.Identity == null || string.IsNullOrWhiteSpace(baseAccount.Identity.Unique)) + errors.Add(new ErrorModel() + { + Tittle = "Identidad no valida", + Description = "La cuenta debe tener un identificador único valido.", + }); + + if (!ValidarCadena(baseAccount.Identity?.Unique)) + errors.Add(new ErrorModel() + { + Tittle = "Identidad no valida", + Description = "La identidad de la cuenta no puede contener símbolos NO alfanuméricos." + }); + + return errors; + } + + + + static bool ValidarCadena(string? cadena) + { + // Si la cadena es nula o vacía, no es válida + if (string.IsNullOrWhiteSpace(cadena)) + return false; + + // Patrón de expresión regular para permitir solo letras o números + string patron = "^[a-zA-Z0-9]*$"; + + // Comprobar la coincidencia con el patrón + return Regex.IsMatch(cadena, patron); + } + + + + /// + /// Procesar el modelo. + /// + /// Modelo + public static AccountModel Process(AccountModel baseAccount) + { + return new AccountModel() + { + Id = 0, + Name = baseAccount.Name.Trim(), + Profile = baseAccount.Profile, + Password = Global.Utilities.Cryptography.Encrypt(baseAccount.Password), + Visibility = baseAccount.Visibility, + IdentityId = 0, + AccountType = baseAccount.AccountType, + Identity = new() + { + Id = 0, + Status = IdentityStatus.Enable, + Type = IdentityType.Account, + CreationTime = DateTime.UtcNow, + EffectiveTime = DateTime.UtcNow, + ExpirationTime = DateTime.UtcNow.AddYears(5), + Roles = [], + Provider = IdentityService.LIN, + Unique = baseAccount.Identity.Unique.Trim() + } + }; + + } + + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Services/Formats/Identities.cs b/LIN.Cloud.Identity/Services/Formats/Identities.cs new file mode 100644 index 0000000..637c2d2 --- /dev/null +++ b/LIN.Cloud.Identity/Services/Formats/Identities.cs @@ -0,0 +1,20 @@ +namespace LIN.Cloud.Identity.Services.Formats; + +public class Identities +{ + + /// + /// Procesar el modelo. + /// + /// Modelo + public static void Process(IdentityModel id) + { + id.Id = 0; + id.ExpirationTime = DateTime.UtcNow.AddYears(10); + id.EffectiveTime = DateTime.UtcNow; + id.CreationTime = DateTime.UtcNow; + id.Status = IdentityStatus.Enable; + id.Unique = id.Unique.Trim(); + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Services/Realtime/PassKeyHub.cs b/LIN.Cloud.Identity/Services/Realtime/PassKeyHub.cs new file mode 100644 index 0000000..707ece0 --- /dev/null +++ b/LIN.Cloud.Identity/Services/Realtime/PassKeyHub.cs @@ -0,0 +1,156 @@ +namespace LIN.Cloud.Identity.Services.Realtime; + +public partial class PassKeyHub(IAccountLogRepository accountLogs) : Hub +{ + + /// + /// Lista de intentos Passkey. + /// String: Usuario. + /// PasskeyModels: Lista de intentos. + /// + public static readonly Dictionary> Attempts = []; + + + /// + /// Canal de intentos. + /// + public const string AttemptsChannel = "#attempts"; + + + /// + /// Canal de respuestas. + /// + public const string ResponseChannel = "#responses"; + + + /// + /// Evento cuando se desconecta. + /// + public override Task OnDisconnectedAsync(Exception? exception) + { + + // Obtener el intento. + var attempt = Attempts.Values.Where(T => T.Where(T => T.HubKey == Context.ConnectionId).Any()).FirstOrDefault() ?? new(); + + _ = attempt.Where(T => + { + if (T.HubKey == Context.ConnectionId && T.Status == PassKeyStatus.Undefined) + T.Status = PassKeyStatus.Failed; + + return false; + }); + + return base.OnDisconnectedAsync(exception); + } + + + //=========== Dispositivos ===========// + + + /// + /// Envía la solicitud a los admins. + /// + public async Task SendRequest(PassKeyModel modelo) + { + var pass = new PassKeyModel() + { + Expiration = modelo.Expiration, + Time = modelo.Time, + Status = modelo.Status, + User = modelo.User, + HubKey = modelo.HubKey + }; + + await Clients.Group(BuildGroupName(modelo.User)).SendAsync(AttemptsChannel, pass); + } + + + /// + /// Recibe una respuesta de passkey + /// + public async Task ReceiveRequest(PassKeyModel modelo) + { + try + { + // Obtener información del token. + JwtModel accountJwt = JwtService.Validate(modelo.Token); + + // Validar el token. + if (!accountJwt.IsAuthenticated || modelo.Status != PassKeyStatus.Success) + { + // Modelo de falla + PassKeyModel badPass = new() + { + Status = modelo.Status, + User = modelo.User + }; + + // Enviar respuesta. + await Clients.Groups($"dbo.{modelo.HubKey}").SendAsync(ResponseChannel, badPass); + return; + } + + // Obtiene los intentos. + var attempt = (from intento in Attempts[modelo.User.ToLower()].Where(A => A.HubKey == modelo.HubKey) + where intento.HubKey == modelo.HubKey + select intento).FirstOrDefault(); + + // No se encontró. + if (attempt is null) + return; + + // Cambiar el estado del intento. + attempt.Status = modelo.Status; + + // Si el tiempo de expiración ya paso + if (DateTime.UtcNow > modelo.Expiration) + { + attempt.Status = PassKeyStatus.Expired; + attempt.Token = string.Empty; + } + else + { + // Generar nuevo token. + string token = JwtService.Generate(new() + { + Id = accountJwt.AccountId, + IdentityId = accountJwt.IdentityId, + Identity = new() + { + Id = accountJwt.IdentityId, + Unique = accountJwt.Unique + }, + }, 0); + + attempt.Token = token; + } + + // Respuesta passkey. + var responsePasskey = new PassKeyModel() + { + Expiration = modelo.Expiration, + Status = attempt.Status, + User = attempt.User, + Token = attempt.Token, + Time = DateTime.UtcNow, + HubKey = string.Empty, + Key = string.Empty + }; + + // Crear log. + await accountLogs.Create(new() + { + AccountId = accountJwt.AccountId, + AuthenticationMethod = AuthenticationMethods.Authenticator, + Time = DateTime.UtcNow, + }); + + // Respuesta al cliente. + await Clients.Groups($"dbo.{modelo.HubKey}").SendAsync(ResponseChannel, responsePasskey); + + } + catch (Exception) + { + } + } +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Services/Realtime/PassKeyHubActions.cs b/LIN.Cloud.Identity/Services/Realtime/PassKeyHubActions.cs new file mode 100644 index 0000000..28b15cc --- /dev/null +++ b/LIN.Cloud.Identity/Services/Realtime/PassKeyHubActions.cs @@ -0,0 +1,67 @@ +namespace LIN.Cloud.Identity.Services.Realtime; + +public partial class PassKeyHub +{ + + /// + /// Agregar un dispositivo administrador. + /// + public async Task JoinAdmin(string token) + { + // Obtener información del token. + var tokenInformation = JwtService.Validate(token); + + // Validar. + if (!tokenInformation.IsAuthenticated) + return; + + // Grupo de la cuenta. + await Groups.AddToGroupAsync(Context.ConnectionId, BuildGroupName(tokenInformation.Unique)); + } + + + /// + /// Nuevo intento de inicio. + /// + /// Modelo. + public async Task JoinIntent(PassKeyModel attempt) + { + + //// Aplicación + //var application = await Data.Applications.Read(attempt.Application.Key); + + //// Si la app no existe o no esta activa + //if (application.Response != Responses.Success) + // return; + + //// Preparar el modelo + //attempt.Application ??= new(); + //attempt.Application.Name = application.Model.Name; + //attempt.Application.Badge = application.Model.Badge; + //attempt.Application.Key = application.Model.Key; + //attempt.Application.ID = application.Model.ID; + + // Vencimiento + var expiración = DateTime.UtcNow.AddMinutes(2); + + // Caducidad el modelo + attempt.HubKey = Context.ConnectionId; + attempt.Status = PassKeyStatus.Undefined; + attempt.Time = DateTime.UtcNow; + attempt.Expiration = expiración; + + // Agrega el modelo + if (!Attempts.ContainsKey(attempt.User.ToLower())) + Attempts.Add(attempt.User.ToLower(), [attempt]); + + else + Attempts[attempt.User.ToLower()].Add(attempt); + + // Yo + await Groups.AddToGroupAsync(Context.ConnectionId, $"dbo.{Context.ConnectionId}"); + + await SendRequest(attempt); + + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Services/Realtime/PassKeyHubFunctions.cs b/LIN.Cloud.Identity/Services/Realtime/PassKeyHubFunctions.cs new file mode 100644 index 0000000..d491595 --- /dev/null +++ b/LIN.Cloud.Identity/Services/Realtime/PassKeyHubFunctions.cs @@ -0,0 +1,12 @@ +namespace LIN.Cloud.Identity.Services.Realtime; + +public partial class PassKeyHub +{ + + /// + /// Construir el nombre de un grupo. + /// + /// Usuario. + public string BuildGroupName(string user) => $"gr.{user.ToLower().Trim()}"; + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Services/Utils/EmailSender.cs b/LIN.Cloud.Identity/Services/Utils/EmailSender.cs new file mode 100644 index 0000000..e6d6f71 --- /dev/null +++ b/LIN.Cloud.Identity/Services/Utils/EmailSender.cs @@ -0,0 +1,38 @@ +namespace LIN.Cloud.Identity.Services.Utils; + +public class EmailSender(ILogger logger, IConfiguration configuration) +{ + + /// + /// Enviar correo. + /// + /// A. + /// Asunto. + /// Cuerpo HTML. + public async Task Send(string to, string subject, string body) + { + try + { + // Servicio. + Global.Http.Services.Client client = new(configuration["hangfire:mail"]) + { + TimeOut = 10 + }; + + client.AddParameter("subject", subject); + client.AddParameter("mail", to); + + var result = await client.Post(body); + + logger.LogInformation(result); + + return true; + } + catch (Exception ex) + { + logger.LogError(ex, "Error al enviar correo."); + return false; + } + } + +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/Usings.cs b/LIN.Cloud.Identity/Usings.cs new file mode 100644 index 0000000..8f5c997 --- /dev/null +++ b/LIN.Cloud.Identity/Usings.cs @@ -0,0 +1,25 @@ +// Sistema. +global using Http.ResponsesList; +// Tipos locales. +global using LIN.Cloud.Identity.Persistence.Contexts; +global using LIN.Cloud.Identity.Persistence.Extensions; +global using LIN.Cloud.Identity.Persistence.Models; +global using LIN.Cloud.Identity.Persistence.Repositories; +global using LIN.Cloud.Identity.Services.Extensions; +global using LIN.Cloud.Identity.Services.Filters; +global using LIN.Cloud.Identity.Services.Interfaces; +global using LIN.Cloud.Identity.Services.Models; +global using LIN.Cloud.Identity.Services.Services; +global using LIN.Cloud.Identity.Services.Utils; +global using LIN.Types.Cloud.Identity.Enumerations; +global using LIN.Types.Cloud.Identity.Models; +global using LIN.Types.Cloud.Identity.Models.Identities; +global using LIN.Types.Enumerations; +// Tipos Generales +global using LIN.Types.Responses; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.AspNetCore.Mvc.Filters; +global using Microsoft.AspNetCore.SignalR; +// SQL. +global using Microsoft.EntityFrameworkCore; +global using System.Text.RegularExpressions; diff --git a/LIN.Identity/appsettings.Development.json b/LIN.Cloud.Identity/appsettings.Development.json similarity index 50% rename from LIN.Identity/appsettings.Development.json rename to LIN.Cloud.Identity/appsettings.Development.json index 0c208ae..4822bfc 100644 --- a/LIN.Identity/appsettings.Development.json +++ b/LIN.Cloud.Identity/appsettings.Development.json @@ -4,5 +4,13 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "AllowedHosts": "*", + "jwt": { + "key": "" + }, + "ConnectionStrings": { + "local": "", + "cloud": "" } -} +} \ No newline at end of file diff --git a/LIN.Cloud.Identity/wwwroot/seeds/applications.json b/LIN.Cloud.Identity/wwwroot/seeds/applications.json new file mode 100644 index 0000000..7b6e591 --- /dev/null +++ b/LIN.Cloud.Identity/wwwroot/seeds/applications.json @@ -0,0 +1,200 @@ + [ + { + "id": 0, + "name": "LIN Inventory", + "key": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "identity": { + "id": 0, + "unique": "inventory#lin", + "creationTime": "2025-02-16T10:00:28.998Z", + "effectiveTime": "2025-02-16T10:00:28.998Z", + "expirationTime": "2045-02-16T15:41:28.998Z", + "status": 0, + "type": 0, + "roles": [] + }, + "ownerId": 1, + "restriction": { + "id": 0, + "allowPersonalAccounts": true, + "allowWorkAccounts": true, + "allowEducationsAccounts": true + } + }, + { + "id": 0, + "name": "LIN Allo", + "key": "829fa4ce-7e74-4c62-8a1e-9ba04ee7433a", + "identity": { + "id": 0, + "unique": "allo#lin", + "creationTime": "2025-02-16T10:00:28.998Z", + "effectiveTime": "2025-02-16T10:00:28.998Z", + "expirationTime": "2045-02-16T15:41:28.998Z", + "status": 0, + "type": 0, + "roles": [] + }, + "ownerId": 1, + "restriction": { + "id": 0, + "allowPersonalAccounts": true, + "allowWorkAccounts": true, + "allowEducationsAccounts": true + } + }, + { + "id": 0, + "name": "LIN Calendar", + "key": "03fcf406-c931-4454-b935-1cd9563b277f", + "identity": { + "id": 0, + "unique": "calendar#lin", + "creationTime": "2025-02-16T10:00:28.998Z", + "effectiveTime": "2025-02-16T10:00:28.998Z", + "expirationTime": "2045-02-16T15:41:28.998Z", + "status": 0, + "type": 0, + "roles": [] + }, + "ownerId": 1, + "restriction": { + "id": 0, + "allowPersonalAccounts": true, + "allowWorkAccounts": true, + "allowEducationsAccounts": true + } + }, + { + "id": 0, + "name": "LIN Notes", + "key": "3d103901-f3c9-49fd-8b86-e6cf5f22b5d6", + "identity": { + "id": 0, + "unique": "notes#lin", + "creationTime": "2025-02-16T10:00:28.998Z", + "effectiveTime": "2025-02-16T10:00:28.998Z", + "expirationTime": "2045-02-16T15:41:28.998Z", + "status": 0, + "type": 0, + "roles": [] + }, + "ownerId": 1, + "restriction": { + "id": 0, + "allowPersonalAccounts": true, + "allowWorkAccounts": true, + "allowEducationsAccounts": true + } + }, + { + "id": 0, + "name": "LIN Mail", + "key": "30ba6fd1-dff3-4e72-91d3-fb4a46fcbf60", + "identity": { + "id": 0, + "unique": "mail#lin", + "creationTime": "2025-02-16T10:00:28.998Z", + "effectiveTime": "2025-02-16T10:00:28.998Z", + "expirationTime": "2045-02-16T15:41:28.998Z", + "status": 0, + "type": 0, + "roles": [] + }, + "ownerId": 1, + "restriction": { + "id": 0, + "allowPersonalAccounts": true, + "allowWorkAccounts": false, + "allowEducationsAccounts": false + } + }, + { + "id": 0, + "name": "LIN Vault", + "key": "de7c4f00-3ad0-4911-9c9b-b8a2fbe079bc", + "identity": { + "id": 0, + "unique": "vault#lin", + "creationTime": "2025-02-16T10:00:28.998Z", + "effectiveTime": "2025-02-16T10:00:28.998Z", + "expirationTime": "2045-02-16T15:41:28.998Z", + "status": 0, + "type": 0, + "roles": [] + }, + "ownerId": 1, + "restriction": { + "id": 0, + "allowPersonalAccounts": true, + "allowWorkAccounts": true, + "allowEducationsAccounts": true + } + }, + { + "id": 0, + "name": "LIN Cloud Service", + "key": "ce85a537-41a8-46ad-9ff0-86039850a343", + "identity": { + "id": 0, + "unique": "console#lin", + "creationTime": "2025-02-16T10:00:28.998Z", + "effectiveTime": "2025-02-16T10:00:28.998Z", + "expirationTime": "2045-02-16T15:41:28.998Z", + "status": 0, + "type": 0, + "roles": [] + }, + "ownerId": 1, + "restriction": { + "id": 0, + "allowPersonalAccounts": true, + "allowWorkAccounts": true, + "allowEducationsAccounts": true + } + }, + { + "id": 0, + "name": "LIN Contacts", + "key": "6101a3bd-b1fb-4815-afc2-1e9a625ba09d", + "identity": { + "id": 0, + "unique": "contacts#lin", + "creationTime": "2025-02-16T10:00:28.998Z", + "effectiveTime": "2025-02-16T10:00:28.998Z", + "expirationTime": "2045-02-16T15:41:28.998Z", + "status": 0, + "type": 0, + "roles": [] + }, + "ownerId": 1, + "restriction": { + "id": 0, + "allowPersonalAccounts": true, + "allowWorkAccounts": true, + "allowEducationsAccounts": true + } + }, + { + "id": 0, + "name": "LIN Human Directory", + "key": "bc8dc14f-55cb-4f73-839b-95b5480ff774", + "identity": { + "id": 0, + "unique": "humandirectory#lin", + "creationTime": "2025-02-16T10:00:28.998Z", + "effectiveTime": "2025-02-16T10:00:28.998Z", + "expirationTime": "2045-02-16T15:41:28.998Z", + "status": 0, + "type": 0, + "roles": [] + }, + "ownerId": 1, + "restriction": { + "id": 0, + "allowPersonalAccounts": true, + "allowWorkAccounts": true, + "allowEducationsAccounts": true + } + } +] \ No newline at end of file diff --git a/LIN.Cloud.Identity/wwwroot/seeds/users.json b/LIN.Cloud.Identity/wwwroot/seeds/users.json new file mode 100644 index 0000000..818628c --- /dev/null +++ b/LIN.Cloud.Identity/wwwroot/seeds/users.json @@ -0,0 +1,21 @@ +[ + { + "id": 0, + "name": "LIN General Admin", + "profile": "", + "password": "default", + "visibility": 0, + "identityService": 0, + "islinadmin": true, + "identity": { + "id": 0, + "unique": "linadmin", + "creationTime": "2025-02-16T10:00:37.735Z", + "effectiveTime": "2025-02-16T10:00:37.735Z", + "expirationTime": "2035-02-16T15:20:37.735Z", + "status": 0, + "type": 0, + "roles": [] + } + } +] \ No newline at end of file diff --git a/LIN.Cloud.Identity/wwwroot/swagger/somee.css b/LIN.Cloud.Identity/wwwroot/swagger/somee.css new file mode 100644 index 0000000..4596980 --- /dev/null +++ b/LIN.Cloud.Identity/wwwroot/swagger/somee.css @@ -0,0 +1,19 @@ +a[href*="http://somee.com"] { + visibility: hidden !important; + display: none !important; +} + +div[style="opacity: 0.9; z-index: 2147483647; position: fixed; left: 0px; bottom: 0px; height: 65px; right: 0px; display: block; width: 100%; background-color: #202020; margin: 0px; padding: 0px;"] { + visibility: hidden !important; + display: none !important; +} + +div[style="height: 65px;"] { + visibility: hidden !important; + display: none !important; +} + +div[onmouseover="S_ssac();"][onmouseout="D_ssac();"] { + visibility: hidden !important; + display: none !important; + } \ No newline at end of file diff --git a/LIN.Identity.sln b/LIN.Identity.sln deleted file mode 100644 index e7e7e59..0000000 --- a/LIN.Identity.sln +++ /dev/null @@ -1,81 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.7.34003.232 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LIN.Identity", "LIN.Identity\LIN.Identity.csproj", "{56E0DD97-DE1B-4F71-BC58-C43BC17CC0A1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LIN.Types", "..\..\Tipos\LIN.Types\LIN.Types.csproj", "{5154207E-1CF9-4065-A4DE-456D8E369524}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LIN.Modules", "..\..\Tipos\LIN.Modules\LIN.Modules.csproj", "{7A5043D3-B280-49D6-8C69-9E02594141E5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Http", "..\..\Tipos\Http\Http.csproj", "{13CF4A38-8857-442E-A496-76982F0A1D48}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LIN.Types.Auth", "..\..\Tipos\LIN.Types.Auth\LIN.Types.Auth.csproj", "{CDED4C37-3998-4F64-8C87-85A9D0AB6CBD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LIN.Access.Logger", "..\..\AccesoAPI\LIN.Access.Logger\LIN.Access.Logger.csproj", "{6CB65EA1-51C6-4450-B174-F0A04C489FB5}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {56E0DD97-DE1B-4F71-BC58-C43BC17CC0A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {56E0DD97-DE1B-4F71-BC58-C43BC17CC0A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {56E0DD97-DE1B-4F71-BC58-C43BC17CC0A1}.Debug|x86.ActiveCfg = Debug|Any CPU - {56E0DD97-DE1B-4F71-BC58-C43BC17CC0A1}.Debug|x86.Build.0 = Debug|Any CPU - {56E0DD97-DE1B-4F71-BC58-C43BC17CC0A1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {56E0DD97-DE1B-4F71-BC58-C43BC17CC0A1}.Release|Any CPU.Build.0 = Release|Any CPU - {56E0DD97-DE1B-4F71-BC58-C43BC17CC0A1}.Release|x86.ActiveCfg = Release|Any CPU - {56E0DD97-DE1B-4F71-BC58-C43BC17CC0A1}.Release|x86.Build.0 = Release|Any CPU - {5154207E-1CF9-4065-A4DE-456D8E369524}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5154207E-1CF9-4065-A4DE-456D8E369524}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5154207E-1CF9-4065-A4DE-456D8E369524}.Debug|x86.ActiveCfg = Debug|Any CPU - {5154207E-1CF9-4065-A4DE-456D8E369524}.Debug|x86.Build.0 = Debug|Any CPU - {5154207E-1CF9-4065-A4DE-456D8E369524}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5154207E-1CF9-4065-A4DE-456D8E369524}.Release|Any CPU.Build.0 = Release|Any CPU - {5154207E-1CF9-4065-A4DE-456D8E369524}.Release|x86.ActiveCfg = Release|Any CPU - {5154207E-1CF9-4065-A4DE-456D8E369524}.Release|x86.Build.0 = Release|Any CPU - {7A5043D3-B280-49D6-8C69-9E02594141E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7A5043D3-B280-49D6-8C69-9E02594141E5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7A5043D3-B280-49D6-8C69-9E02594141E5}.Debug|x86.ActiveCfg = Debug|Any CPU - {7A5043D3-B280-49D6-8C69-9E02594141E5}.Debug|x86.Build.0 = Debug|Any CPU - {7A5043D3-B280-49D6-8C69-9E02594141E5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7A5043D3-B280-49D6-8C69-9E02594141E5}.Release|Any CPU.Build.0 = Release|Any CPU - {7A5043D3-B280-49D6-8C69-9E02594141E5}.Release|x86.ActiveCfg = Release|Any CPU - {7A5043D3-B280-49D6-8C69-9E02594141E5}.Release|x86.Build.0 = Release|Any CPU - {13CF4A38-8857-442E-A496-76982F0A1D48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {13CF4A38-8857-442E-A496-76982F0A1D48}.Debug|Any CPU.Build.0 = Debug|Any CPU - {13CF4A38-8857-442E-A496-76982F0A1D48}.Debug|x86.ActiveCfg = Debug|x86 - {13CF4A38-8857-442E-A496-76982F0A1D48}.Debug|x86.Build.0 = Debug|x86 - {13CF4A38-8857-442E-A496-76982F0A1D48}.Release|Any CPU.ActiveCfg = Release|Any CPU - {13CF4A38-8857-442E-A496-76982F0A1D48}.Release|Any CPU.Build.0 = Release|Any CPU - {13CF4A38-8857-442E-A496-76982F0A1D48}.Release|x86.ActiveCfg = Release|x86 - {13CF4A38-8857-442E-A496-76982F0A1D48}.Release|x86.Build.0 = Release|x86 - {CDED4C37-3998-4F64-8C87-85A9D0AB6CBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CDED4C37-3998-4F64-8C87-85A9D0AB6CBD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CDED4C37-3998-4F64-8C87-85A9D0AB6CBD}.Debug|x86.ActiveCfg = Debug|Any CPU - {CDED4C37-3998-4F64-8C87-85A9D0AB6CBD}.Debug|x86.Build.0 = Debug|Any CPU - {CDED4C37-3998-4F64-8C87-85A9D0AB6CBD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CDED4C37-3998-4F64-8C87-85A9D0AB6CBD}.Release|Any CPU.Build.0 = Release|Any CPU - {CDED4C37-3998-4F64-8C87-85A9D0AB6CBD}.Release|x86.ActiveCfg = Release|Any CPU - {CDED4C37-3998-4F64-8C87-85A9D0AB6CBD}.Release|x86.Build.0 = Release|Any CPU - {6CB65EA1-51C6-4450-B174-F0A04C489FB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6CB65EA1-51C6-4450-B174-F0A04C489FB5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6CB65EA1-51C6-4450-B174-F0A04C489FB5}.Debug|x86.ActiveCfg = Debug|Any CPU - {6CB65EA1-51C6-4450-B174-F0A04C489FB5}.Debug|x86.Build.0 = Debug|Any CPU - {6CB65EA1-51C6-4450-B174-F0A04C489FB5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6CB65EA1-51C6-4450-B174-F0A04C489FB5}.Release|Any CPU.Build.0 = Release|Any CPU - {6CB65EA1-51C6-4450-B174-F0A04C489FB5}.Release|x86.ActiveCfg = Release|Any CPU - {6CB65EA1-51C6-4450-B174-F0A04C489FB5}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {15082DA4-9040-4FA7-A6C6-D1446CB629B9} - EndGlobalSection -EndGlobal diff --git a/LIN.Identity/Areas/Accounts/AccountController.cs b/LIN.Identity/Areas/Accounts/AccountController.cs deleted file mode 100644 index c9d76a9..0000000 --- a/LIN.Identity/Areas/Accounts/AccountController.cs +++ /dev/null @@ -1,463 +0,0 @@ -using LIN.Identity.Validations; -namespace LIN.Identity.Areas.Accounts; - - -[Route("account")] -public class AccountController : ControllerBase -{ - - - /// - /// Crear nueva cuenta (Cuenta de LIN) - /// - /// Modelo de la cuenta - [HttpPost("create")] - public async Task Create([FromBody] AccountModel? modelo) - { - - // Comprobaciones - if (modelo == null || modelo.Contraseña.Length < 4 || modelo.Nombre.Length <= 0 || modelo.Usuario.Length <= 0) - return new(Responses.InvalidParam); - - // Organización del modelo - modelo = Account.Process(modelo); - - // Creación del usuario - var response = await Data.Accounts.Create(modelo); - - // Evaluación - if (response.Response != Responses.Success) - return new(response.Response); - - // Obtiene el usuario - var token = Jwt.Generate(response.Model, 0); - - // Retorna el resultado - return new CreateResponse() - { - LastID = response.Model.ID, - Response = Responses.Success, - Token = token, - Message = "Success" - }; - - } - - - - /// - /// Obtiene la información de usuario. - /// - /// ID del usuario - /// Token de acceso - [HttpGet("read/id")] - public async Task> Read([FromQuery] int id, [FromHeader] string token) - { - - if (id <= 0) - return new(Responses.InvalidParam); - - var (isValid, _, user, orgId, _) = Jwt.Validate(token); - - if (!isValid) - { - return new ReadOneResponse() - { - Response = Responses.Unauthorized, - Message = "Token invalido." - }; - } - - // Obtiene el usuario - var response = await Data.Accounts.Read(id, new() - { - ContextOrg = orgId, - ContextUser = user, - FindOn = FilterModels.FindOn.StableAccounts, - IncludeOrg = FilterModels.IncludeOrg.IncludeIf, - IsAdmin = false, - OrgLevel = FilterModels.IncludeOrgLevel.Advance - }); - - // Si es erróneo - if (response.Response != Responses.Success) - return new ReadOneResponse() - { - Response = response.Response, - Model = new() - }; - - // Retorna el resultado - return response; - - } - - - - /// - /// Obtiene la información de usuario. - /// - /// Usuario único - /// Token de acceso - [HttpGet("read/user")] - public async Task> Read([FromQuery] string user, [FromHeader] string token) - { - - if (string.IsNullOrWhiteSpace(user)) - return new(Responses.InvalidParam); - - - var (isValid, _, userId, orgId, _) = Jwt.Validate(token); - - if (!isValid) - { - return new ReadOneResponse() - { - Response = Responses.Unauthorized, - Message = "Token invalido." - }; - } - - - - var response = await Data.Accounts.Read(user, new() - { - ContextOrg = orgId, - ContextUser = userId, - FindOn = FilterModels.FindOn.StableAccounts, - IncludeOrg = FilterModels.IncludeOrg.IncludeIf, - OrgLevel = FilterModels.IncludeOrgLevel.Advance - }); - - - - // Si es erróneo - if (response.Response != Responses.Success) - return new ReadOneResponse() - { - Response = response.Response, - Model = new() - }; - - // Retorna el resultado - return response; - - } - - - - /// - /// Obtiene una lista de diez (10) usuarios que coincidan con un patron - /// - /// Patron - /// Token de acceso - [HttpGet("search")] - public async Task> Search([FromQuery] string pattern, [FromHeader] string token) - { - - // Comprobación - if (pattern.Trim().Length <= 0) - return new(Responses.InvalidParam); - - // Info del token - var (isValid, _, userId, orgId, _) = Jwt.Validate(token); - - // Token es invalido - if (!isValid) - { - return new ReadAllResponse - { - Message = "Token es invalido", - Response = Responses.Unauthorized - }; - } - - // Obtiene el usuario - var response = await Data.Accounts.Search(pattern, new() - { - ContextOrg = orgId, - ContextUser = userId, - FindOn = FilterModels.FindOn.StableAccounts, - IncludeOrg = FilterModels.IncludeOrg.IncludeIf, - OrgLevel = FilterModels.IncludeOrgLevel.Advance - }); - - return response; - } - - - - /// - /// Obtiene una lista cuentas - /// - /// IDs de las cuentas - /// Token de acceso - [HttpPost("findAll")] - public async Task> ReadAll([FromBody] List ids, [FromHeader] string token) - { - - var (isValid, _, userId, orgId, _) = Jwt.Validate(token); - - if (!isValid) - { - return new(Responses.Unauthorized); - } - - // Obtiene el usuario - var response = await Data.Accounts.FindAll(ids, new() - { - ContextOrg = orgId, - ContextUser = userId, - FindOn = FilterModels.FindOn.StableAccounts, - IncludeOrg = FilterModels.IncludeOrg.Include, - OrgLevel = FilterModels.IncludeOrgLevel.Advance - }); - - return response; - - } - - - - /// - /// Actualiza la contraseña de una cuenta - /// - /// Modelo de actualización - /// Token de acceso - [HttpPatch("update/password")] - public async Task Update([FromBody] UpdatePasswordModel modelo, [FromHeader] string token) - { - - // Validar parámetros. - if (modelo == null) - return new ResponseBase() - { - Message = "Parámetro para nueva actualización de contraseña es invalido.", - Response = Responses.InvalidParam - }; - - // Validar de la nueva contraseña. - if (modelo.OldPassword.Length < 4 || modelo.NewPassword.Length < 4) - return new ResponseBase(Responses.InvalidParam) - { - Message = "Antigua contraseña o nueva contraseña tienen una longitud invalida." - }; - - // Validar el token. - var (isValid, _, userId, _, _) = Jwt.Validate(token); - - // No es valido. - if (!isValid) - return new ResponseBase(Responses.Unauthorized) - { - Message = "Token invalido" - }; - - // Obtener datos antiguos. - var actualData = await Data.Accounts.ReadBasic(userId); - - // Error al encontrar usuario. - if (actualData.Response != Responses.Success) - return new ResponseBase(Responses.Unauthorized) - { - Message = $"Error al encontrar el usuario con ID '{userId}'" - }; - - - // Encriptar la contraseña - modelo.OldPassword = EncryptClass.Encrypt(modelo.OldPassword); - - // Valida la contraseña actual - if (modelo.OldPassword != actualData.Model.Contraseña) - return new ResponseBase(Responses.InvalidPassword) - { - Message = $"Las contraseñas no coinciden." - }; - - // Actualiza el ID. - modelo.Account = userId; - - // Actualizar la contraseña - return await Data.Accounts.Update(modelo); - - } - - - - /// - /// Elimina una cuenta - /// - /// Token de acceso - [HttpDelete("delete")] - public async Task Delete([FromHeader] string token) - { - - var (isValid, _, userId, _, _) = Jwt.Validate(token); - - if (!isValid) - return new ResponseBase - { - Response = Responses.Unauthorized, - Message = "Token invalido" - }; - - if (userId <= 0) - return new(Responses.InvalidParam); - - var response = await Data.Accounts.Delete(userId); - return response; - } - - - - /// - /// Desactiva una cuenta - /// - /// Modelo - [HttpPatch("disable")] - public async Task Disable([FromBody] AccountModel user) - { - - if (user.ID <= 0) - { - return new(Responses.ExistAccount); - } - - // Modelo de usuario de la BD - var userModel = await Data.Accounts.ReadBasic(user.ID); - - if (userModel.Model.Contraseña != EncryptClass.Encrypt(user.Contraseña)) - { - return new(Responses.InvalidPassword); - } - - - return await Data.Accounts.Update(user.ID, AccountStatus.Disable); - - } - - - - /// - /// (ADMIN) encuentra diez (10) usuarios que coincidan con el patron - /// - /// - /// - [HttpGet("admin/search")] - public async Task> FindAll([FromQuery] string pattern, [FromHeader] string token) - { - - var (isValid, _, id, _, _) = Jwt.Validate(token); - - - if (!isValid) - { - return new(Responses.Unauthorized); - } - - - var rol = (await Data.Accounts.Read(id, new() - { - FindOn = FilterModels.FindOn.StableAccounts, - IncludeOrg = FilterModels.IncludeOrg.None - })).Model.Rol; - - - if (rol != AccountRoles.Admin) - return new(Responses.Unauthorized); - - // Obtiene el usuario - var response = await Data.Accounts.Search(pattern, new() - { - ContextOrg = 0, - OrgLevel = FilterModels.IncludeOrgLevel.Advance, - ContextUser = 0, - FindOn = FilterModels.FindOn.AllAccount, - IncludeOrg = FilterModels.IncludeOrg.Include, - IsAdmin = true - }); - - return response; - - } - - - - /// - /// Actualiza la información de una cuenta - /// - /// Modelo - /// Token de acceso - [HttpPut("update")] - public async Task Update([FromBody] AccountModel modelo, [FromHeader] string token) - { - - var (isValid, _, userId, _, _) = Jwt.Validate(token); - - if (!isValid) - return new ResponseBase - { - Response = Responses.Unauthorized, - Message = "Token Invalido" - }; - - modelo.ID = userId; - modelo.Perfil = Image.Zip(modelo.Perfil); - - if (modelo.ID <= 0 || modelo.Nombre.Any()) - return new(Responses.InvalidParam); - - return await Data.Accounts.Update(modelo); - - } - - - - /// - /// Actualiza el genero de un usuario - /// - /// Token de acceso - /// Nuevo genero - [HttpPatch("update/gender")] - public async Task Update([FromHeader] string token, [FromHeader] Genders genero) - { - - - var (isValid, _, id, _, _) = Jwt.Validate(token); - - - if (!isValid) - { - return new(Responses.Unauthorized); - } - - return await Data.Accounts.Update(id, genero); - - } - - - - /// - /// Actualiza la visibilidad de una cuenta - /// - /// Token de acceso - /// Nueva visibilidad - [HttpPatch("update/visibility")] - public async Task Update([FromHeader] string token, [FromHeader] AccountVisibility visibility) - { - - - var (isValid, _, id, _, _) = Jwt.Validate(token); - - if (!isValid) - { - return new(Responses.Unauthorized); - } - - return await Data.Accounts.Update(id, visibility); - - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Areas/Accounts/AdminController.cs b/LIN.Identity/Areas/Accounts/AdminController.cs deleted file mode 100644 index 03efe4f..0000000 --- a/LIN.Identity/Areas/Accounts/AdminController.cs +++ /dev/null @@ -1,266 +0,0 @@ -namespace LIN.Identity.Areas.Accounts; - - -[Route("administrator")] -public class AdminController : ControllerBase -{ - - - - /// - /// Obtiene la información de usuario. - /// - /// ID del usuario - /// Token de acceso - [HttpGet("read/id")] - public async Task> Read([FromQuery] int id, [FromHeader] string token) - { - - if (id <= 0) - return new(Responses.InvalidParam); - - - var (isValid, _, user, orgID, _) = Jwt.Validate(token); - - if (!isValid) - { - return new ReadOneResponse() - { - Response = Responses.Unauthorized, - Message = "Token invalido." - }; - } - - var rol = (await Data.Accounts.Read(user, new() - { - IncludeOrg = FilterModels.IncludeOrg.None, - FindOn = FilterModels.FindOn.StableAccounts - })).Model.Rol; - - if (rol != AccountRoles.Admin) - { - return new ReadOneResponse() - { - Response = Responses.Unauthorized, - Message = "Tienes que ser un administrador." - }; - } - - - // Obtiene el usuario - var response = await Data.Accounts.Read(id, new() - { - SensibleInfo = false, - ContextOrg = orgID, - ContextUser = user, - IsAdmin = true, - FindOn = FilterModels.FindOn.StableAccounts, - IncludeOrg = FilterModels.IncludeOrg.Include, - OrgLevel = FilterModels.IncludeOrgLevel.Advance - }); - - // Si es erróneo - if (response.Response != Responses.Success) - return new ReadOneResponse() - { - Response = response.Response, - Model = new() - }; - - // Retorna el resultado - return response; - - } - - - - /// - /// Obtiene la información de usuario. - /// - /// Usuario único - /// Token de acceso - [HttpGet("read/user")] - public async Task> Read([FromQuery] string user, [FromHeader] string token) - { - - if (string.IsNullOrWhiteSpace(user)) - return new(Responses.InvalidParam); - - - var (isValid, _, userId, orgId, _) = Jwt.Validate(token); - - if (!isValid) - { - return new ReadOneResponse() - { - Response = Responses.Unauthorized, - Message = "Token invalido." - }; - } - - - var rol = (await Data.Accounts.Read(userId, new() - { - IncludeOrg = FilterModels.IncludeOrg.None, - FindOn = FilterModels.FindOn.StableAccounts - })).Model.Rol; - - if (rol != AccountRoles.Admin) - { - return new ReadOneResponse() - { - Response = Responses.Unauthorized, - Message = "Tienes que ser un administrador." - }; - } - - - - var response = await Data.Accounts.Read(user, new() - { - SensibleInfo = false, - ContextOrg = orgId, - IsAdmin = true, - ContextUser = userId, - FindOn = FilterModels.FindOn.StableAccounts, - IncludeOrg = FilterModels.IncludeOrg.Include, - OrgLevel = FilterModels.IncludeOrgLevel.Advance - }); - - - - // Si es erróneo - if (response.Response != Responses.Success) - return new ReadOneResponse() - { - Response = response.Response, - Model = new() - }; - - // Retorna el resultado - return response; - - } - - - - /// - /// Actualiza la contraseña de una cuenta - /// - /// Modelo de actualización - /// Token de acceso - [HttpPatch("update/password")] - public async Task Update([FromBody] UpdatePasswordModel modelo, [FromHeader] string token) - { - - if (modelo.OldPassword.Length < 4 || modelo.NewPassword.Length < 4) - return new(Responses.InvalidParam); - - - var (isValid, _, userId, _, _) = Jwt.Validate(token); - - - if (!isValid) - { - return new(Responses.Unauthorized); - } - - modelo.Account = userId; - - var actualData = await Data.Accounts.ReadBasic(modelo.Account); - - if (actualData.Response != Responses.Success) - return new(Responses.NotExistAccount); - - var oldEncrypted = actualData.Model.Contraseña; - - - if (oldEncrypted != actualData.Model.Contraseña) - { - return new ResponseBase(Responses.InvalidPassword); - } - - return await Data.Accounts.Update(modelo); - - } - - - - /// - /// Elimina una cuenta - /// - /// Token de acceso - [HttpDelete("delete")] - public async Task Delete([FromHeader] string token) - { - - var (isValid, _, userId, _, _) = Jwt.Validate(token); - - if (!isValid) - return new ResponseBase - { - Response = Responses.Unauthorized, - Message = "Token invalido" - }; - - if (userId <= 0) - return new(Responses.InvalidParam); - - var response = await Data.Accounts.Delete(userId); - return response; - } - - - - /// - /// Desactiva una cuenta - /// - /// Modelo - [HttpPatch("disable")] - public async Task Disable([FromBody] AccountModel user) - { - - if (user.ID <= 0) - { - return new(Responses.ExistAccount); - } - - // Modelo de usuario de la BD - var userModel = await Data.Accounts.ReadBasic(user.ID); - - if (userModel.Model.Contraseña != EncryptClass.Encrypt(user.Contraseña)) - { - return new(Responses.InvalidPassword); - } - - - return await Data.Accounts.Update(user.ID, AccountStatus.Disable); - - } - - - - /// - /// Actualiza la visibilidad de una cuenta - /// - /// Token de acceso - /// Nueva visibilidad - [HttpPatch("update/visibility")] - public async Task Update([FromHeader] string token, [FromHeader] AccountVisibility visibility) - { - - - var (isValid, _, id, _, _) = Jwt.Validate(token); - - if (!isValid) - { - return new(Responses.Unauthorized); - } - - return await Data.Accounts.Update(id, visibility); - - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Areas/Organizations/ApplicationOrgsController.cs b/LIN.Identity/Areas/Organizations/ApplicationOrgsController.cs deleted file mode 100644 index 4d00d6d..0000000 --- a/LIN.Identity/Areas/Organizations/ApplicationOrgsController.cs +++ /dev/null @@ -1,164 +0,0 @@ -namespace LIN.Identity.Areas.Organizations; - - -[Route("orgs/applications")] -public class ApplicationOrgsController : ControllerBase -{ - - - /// - /// Obtiene la lista de aplicaciones asociadas a una organización - /// - /// Token de acceso - [HttpGet] - public async Task> ReadApps([FromHeader] string token) - { - - // Token - var (isValid, _, _, orgId, _) = Jwt.Validate(token); - - // Token es invalido - if (!isValid) - return new ReadAllResponse - { - Message = "Token invalido.", - Response = Responses.Unauthorized - }; - - - // Si no tiene ninguna organización - if (orgId <= 0) - return new ReadAllResponse - { - Message = "No estas vinculado con ninguna organización.", - Response = Responses.Unauthorized - }; - - - // Obtiene las aplicaciones - var org = await Data.Organizations.Organizations.ReadApps(orgId); - - // Su no se encontraron aplicaciones - if (org.Response != Responses.Success) - return new ReadAllResponse - { - Message = "No found Organization", - Response = Responses.Unauthorized - }; - - // Conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - context.CloseActions(connectionKey); - - // Retorna el resultado - return org; - - } - - - - /// - /// Insertar una aplicación en una organización - /// - /// UId de la aplicación - /// Token de acceso - [HttpPost("insert")] - public async Task InsertApp([FromQuery] string appUid, [FromHeader] string token) - { - - // Token - var (isValid, _, userId, _, _) = Jwt.Validate(token); - - - // Si el token es invalido - if (!isValid) - return new CreateResponse - { - Message = "Token invalido", - Response = Responses.Unauthorized - }; - - // Información del usuario - var userData = await Data.Accounts.ReadBasic(userId); - - // Si no existe el usuario - if (userData.Response != Responses.Success) - return new CreateResponse - { - Message = "No se encontró el usuario, talvez fue eliminado o desactivado.", - Response = Responses.NotExistAccount - }; - - - // Si no tiene organización - if (userData.Model.OrganizationAccess == null || userData.Model.OrganizationAccess?.Organization == null) - return new CreateResponse - { - Message = $"El usuario '{userData.Model.Usuario}' no pertenece a una organización.", - Response = Responses.Unauthorized - }; - - // Si el usuario no es admin en la organización - if (!userData.Model.OrganizationAccess.Rol.IsAdmin()) - return new CreateResponse - { - Message = $"El usuario '{userData.Model.Usuario}' no tiene un rol administrador en la organización '{userData.Model.OrganizationAccess.Organization.Name}'", - Response = Responses.Unauthorized - }; - - // Crea la aplicación en la organización - var res = await Data.Organizations.Applications.Create(appUid, userData.Model.OrganizationAccess.Organization.ID); - - // Si hubo une error - if (res.Response != Responses.Success) - return new CreateResponse - { - Message = $"Hubo un error al insertar esta aplicación en la lista blanca permitidas de {userData.Model.OrganizationAccess.Organization.Name}", - Response = Responses.Unauthorized - }; - - - // Conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - context.CloseActions(connectionKey); - - // Retorna el resultado - return new CreateResponse - { - LastID = res.LastID, - Message = "", - Response = Responses.Success - }; - } - - - - /// - /// Buscar aplicaciones que no están vinculadas a una organización por medio del un parámetro - /// - /// Parámetro de búsqueda - /// Token de acceso - [HttpGet("search")] - public async Task> Search([FromQuery] string param, [FromHeader] string token) - { - - // Token - var (isValid, _, _, orgId, _) = Jwt.Validate(token); - - // Valida el token - if (!isValid || orgId <= 0) - { - return new(Responses.Unauthorized); - } - - // Encuentra las apps - var finds = await Data.Organizations.Applications.Search(param, orgId); - - return finds; - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Areas/Organizations/MemberController.cs b/LIN.Identity/Areas/Organizations/MemberController.cs deleted file mode 100644 index 030a2ef..0000000 --- a/LIN.Identity/Areas/Organizations/MemberController.cs +++ /dev/null @@ -1,178 +0,0 @@ -using LIN.Identity.Validations; - -namespace LIN.Identity.Areas.Organizations; - - -[Route("orgs/members")] -public class MemberController : ControllerBase -{ - - - /// - /// Crea un nuevo miembro en una organización - /// - /// Modelo de la cuenta - /// Token de acceso de un administrador - /// Rol asignado - [HttpPost("create")] - public async Task Create([FromBody] AccountModel modelo, [FromHeader] string token, [FromHeader] OrgRoles rol) - { - - // Validación del modelo. - if (modelo == null || !modelo.Usuario.Trim().Any() || !modelo.Nombre.Trim().Any()) - { - return new CreateResponse - { - Response = Responses.InvalidParam, - Message = "Uno o varios parámetros inválidos." - }; - } - - // Visibilidad oculta - modelo.Visibilidad = AccountVisibility.Hidden; - - // Organización del modelo - modelo = Account.Process(modelo); - - - // Establece la contraseña default - var password = $"ChangePwd@{modelo.Creación:dd.MM.yyyy}"; - - // Contraseña default - modelo.Contraseña = EncryptClass.Encrypt(password); - - // Validación del token - var (isValid, _, userID, _, _) = Jwt.Validate(token); - - // Token es invalido - if (!isValid) - { - return new CreateResponse - { - Message = "Token invalido.", - Response = Responses.Unauthorized - }; - } - - - // Obtiene el usuario - var userContext = await Data.Accounts.ReadBasic(userID); - - // Error al encontrar el usuario - if (userContext.Response != Responses.Success) - { - return new CreateResponse - { - Message = "No se encontró un usuario valido.", - Response = Responses.Unauthorized - }; - } - - // Si el usuario no tiene una organización - if (userContext.Model.OrganizationAccess == null) - { - return new CreateResponse - { - Message = $"El usuario '{userContext.Model.Usuario}' no pertenece a una organización.", - Response = Responses.Unauthorized - }; - } - - // Verificación del rol dentro de la organización - if (!userContext.Model.OrganizationAccess.Rol.IsAdmin()) - { - return new CreateResponse - { - Message = $"El usuario '{userContext.Model.Usuario}' no puede crear nuevos usuarios en esta organización.", - Response = Responses.Unauthorized - }; - } - - - // Verificación del rol dentro de la organización - if (userContext.Model.OrganizationAccess.Rol.IsGretter(rol)) - { - return new CreateResponse - { - Message = $"El '{userContext.Model.Usuario}' no puede crear nuevos usuarios con mas privilegios de los propios.", - Response = Responses.Unauthorized - }; - } - - - // ID de la organización - var org = userContext.Model.OrganizationAccess.Organization.ID; - - - // Conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - // Creación del usuario - var response = await Data.Organizations.Members.Create(modelo, org, rol, context); - - // Evaluación - if (response.Response != Responses.Success) - return new(response.Response); - - // Cierra la conexión - context.CloseActions(connectionKey); - - // Retorna el resultado - return new CreateResponse() - { - LastID = response.Model.ID, - Response = Responses.Success, - Message = "Success" - }; - - } - - - - /// - /// Obtiene la lista de miembros asociados a una organización - /// - /// Token de acceso - [HttpGet] - public async Task> ReadAll([FromHeader] string token) - { - - var (isValid, _, _, orgID, _) = Jwt.Validate(token); - - - if (!isValid) - { - return new ReadAllResponse - { - Message = "", - Response = Responses.Unauthorized - }; - } - - var members = await Data.Organizations.Members.ReadAll(orgID); - - - if (members.Response != Responses.Success) - { - return new ReadAllResponse - { - Message = "No found Organization", - Response = Responses.Unauthorized - }; - } - - - - // Conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - context.CloseActions(connectionKey); - - // Retorna el resultado - return members; - - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Areas/Organizations/OrganizationController.cs b/LIN.Identity/Areas/Organizations/OrganizationController.cs deleted file mode 100644 index a7e2eb9..0000000 --- a/LIN.Identity/Areas/Organizations/OrganizationController.cs +++ /dev/null @@ -1,233 +0,0 @@ -using LIN.Identity.Validations; - -namespace LIN.Identity.Areas.Organizations; - - -[Route("orgs")] -public class OrganizationsController : ControllerBase -{ - - - /// - /// Crea una nueva organizacion - /// - /// Modelo de la organizaci�n y el usuario administrador - [HttpPost("create")] - public async Task Create([FromBody] OrganizationModel modelo) - { - - // Comprobaciones - if (modelo == null || modelo.Domain.Length <= 0 || modelo.Name.Length <= 0 || modelo.Members.Count <= 0) - return new(Responses.InvalidParam); - - - // Conexi�n - var (context, connectionKey) = Conexión.GetOneConnection(); - - - // Organizaci�n del modelo - modelo.ID = 0; - modelo.AppList = new(); - - modelo.Members[0].Member = Account.Process(modelo.Members[0].Member); - foreach (var member in modelo.Members) - { - member.Rol = OrgRoles.SuperManager; - member.Organization = modelo; - } - - // Creaci�n de la organizaci�n - var response = await Data.Organizations.Organizations.Create(modelo, context); - - // Evaluaci�n - if (response.Response != Responses.Success) - return new(response.Response); - - context.CloseActions(connectionKey); - - // Retorna el resultado - return new CreateResponse() - { - LastID = response.Model.ID, - Response = Responses.Success, - Message = "Success" - }; - - } - - - - /// - /// Obtiene una organizaci�n por medio del ID - /// - /// ID de la organización - [HttpGet("read/id")] - public async Task> ReadOneByID([FromQuery] int id, [FromHeader] string token) - { - - // Parámetros - if (id <= 0) - return new(Responses.InvalidParam); - - // Validar el token - var (isValid, _, _, orgID, _) = Jwt.Validate(token); - - - if (!isValid) - return new(Responses.Unauthorized); - - - // Obtiene la organización - var response = await Data.Organizations.Organizations.Read(id); - - // Organización no encontrada. - if (response.Response != Responses.Success) - return new ReadOneResponse() - { - Response = Responses.NotRows, - Message = "No se encontró la organización." - }; - - // No es publica y no pertenece a ella - if (!response.Model.IsPublic && orgID != response.Model.ID) - return new ReadOneResponse() - { - Response = Responses.Unauthorized, - Message = "Esta organización es privada y tu usuario no esta vinculado a ella.", - Model = new() - { - ID = response.Model.ID, - IsPublic = false, - Name = "Organización privada" - } - }; - - return new ReadOneResponse() - { - Response = Responses.Success, - Model = response.Model - }; - - - } - - - - /// - /// Actualiza si una organizaci�n tiene lista blanca - /// - /// Toke de acceso administrador - /// Nuevo estado - [HttpPatch("update/whitelist")] - public async Task Update([FromHeader] string token, [FromQuery] bool haveWhite) - { - - - var (isValid, _, userID, _, _) = Jwt.Validate(token); - - - if (!isValid) - return new(Responses.Unauthorized); - - - var userContext = await Data.Accounts.ReadBasic(userID); - - // Error al encontrar el usuario - if (userContext.Response != Responses.Success) - { - return new ResponseBase - { - Message = "No se encontr� un usuario valido.", - Response = Responses.Unauthorized - }; - } - - // Si el usuario no tiene una organizaci�n - if (userContext.Model.OrganizationAccess == null) - { - return new ResponseBase - { - Message = $"El usuario '{userContext.Model.Usuario}' no pertenece a una organizaci�n.", - Response = Responses.Unauthorized - }; - } - - // Verificaci�n del rol dentro de la organizaci�n - if (!userContext.Model.OrganizationAccess.Rol.IsAdmin()) - { - return new ResponseBase - { - Message = $"El usuario '{userContext.Model.Usuario}' no puede actualizar el estado de la lista blanca de esta organizaci�n.", - Response = Responses.Unauthorized - }; - } - - - var response = await Data.Organizations.Organizations.UpdateState(userContext.Model.OrganizationAccess.Organization.ID, haveWhite); - - // Retorna el resultado - return response; - - } - - - - /// - /// Actualiza si los usuarios no admins de una organizaci�n tienen acceso a su cuenta - /// - /// Token de acceso administrador - /// Nuevo estado - [HttpPatch("update/access")] - public async Task UpdateAccess([FromHeader] string token, [FromQuery] bool state) - { - - - var (isValid, _, userID, _, _) = Jwt.Validate(token); - - - if (!isValid) - return new(Responses.Unauthorized); - - - var userContext = await Data.Accounts.ReadBasic(userID); - - // Error al encontrar el usuario - if (userContext.Response != Responses.Success) - { - return new ResponseBase - { - Message = "No se encontr� un usuario valido.", - Response = Responses.Unauthorized - }; - } - - // Si el usuario no tiene una organizaci�n - if (userContext.Model.OrganizationAccess == null) - { - return new ResponseBase - { - Message = $"El usuario '{userContext.Model.Usuario}' no pertenece a una organizaci�n.", - Response = Responses.Unauthorized - }; - } - - // Verificaci�n del rol dentro de la organizaci�n - if (userContext.Model.OrganizationAccess.Rol != OrgRoles.SuperManager) - { - return new ResponseBase - { - Message = $"El usuario '{userContext.Model.Usuario}' no puede actualizar el estado de accesos de esta organizaci�n.", - Response = Responses.Unauthorized - }; - } - - - var response = await Data.Organizations.Organizations.UpdateAccess(userContext.Model.OrganizationAccess.Organization.ID, state); - - // Retorna el resultado - return response; - - } - - -} \ No newline at end of file diff --git a/LIN.Identity/Conexion.cs b/LIN.Identity/Conexion.cs deleted file mode 100644 index deae4fa..0000000 --- a/LIN.Identity/Conexion.cs +++ /dev/null @@ -1,235 +0,0 @@ -namespace LIN.Identity; - - -/// -/// Conexión con la base de datos -/// -public sealed class Conexión -{ - - - //===== Estáticas =====// - - /// - /// String de Conexión - /// - private static string _connection = string.Empty; - - - /// - /// Contador de conexiones abiertas - /// - private static volatile int _counter = 0; - - - /// - /// Cantidad de conexiones que se pueden almacenar en cache - /// - private static int _cantidad = 1; - - - - //===== Propiedades =====// - - - /// - /// Obtiene o establece si la Conexión esta en uso - /// - private volatile bool OnUse = false; - - - - /// - /// Obtiene si la Conexión esta en uso y la pone en uso - /// - private bool OnUseAction - { - - get - { - lock (this) - { - if (!OnUse) - { - OnUse = true; - return false; - } - - return true; - - } - - } - - } - - - - /// - /// Obtiene el numero de Conexión - /// - public readonly int ConnectionNumber; - - - - /// - /// Cache de conexiones - /// - private static List CacheConnections { get; set; } = new(); - - - - - - - /// - /// Obtiene la base de datos - /// - public Data.Context DataBase { get; private set; } - - - - /// - /// Nueva Conexión - /// - private Conexión() - { - - DbContextOptionsBuilder optionsBuilder = new(); - optionsBuilder.UseSqlServer(_connection); - - DataBase = new(optionsBuilder.Options); - - _counter++; - ConnectionNumber = _counter; - - - if (CacheConnections.Count <= _cantidad) - CacheConnections.Add(this); - - } - - - - /// - /// Destructor - /// - ~Conexión() - { - this?.DataBase?.Dispose(); - } - - - - - /// - /// Establece que la conexión esta en uso - /// - public void SetOnUse() - { - lock (this) - { - OnUse = true; - } - - } - - - - private string mykey = string.Empty; - - public void CloseActions(string key) - { - lock (this) - { - if (mykey != key) - return; - - DataBase.ChangeTracker.Clear(); - mykey = string.Empty; - OnUse = false; - } - } - - - - /// - /// Inicia las conexiones del cache - /// - public static async Task StartConnections() - { - - _cantidad = 5; -#if AZURE - _cantidad = 30; -#elif SOMEE - _cantidad = 10; -#elif DEBUG - _cantidad = 50; -#endif - - await Task.Run(() => - { - for (var i = 0; i < _cantidad; i++) - { - _ = new Conexión(); - } - }); - - } - - - - /// - /// Establece el string de Conexión - /// - /// string de Conexión - public static void SetStringConnection(string connectionString) - { - _connection = connectionString; - } - - - - /// - /// Obtiene una Conexión a la base de datos - /// - public static (Conexión context, string contextKey) GetOneConnection() - { - - // Obtiene una Conexión de la pool - var con = CacheConnections.FirstOrDefault(T => !T.OnUseAction); - - if (con != null && con.mykey == string.Empty) - { - lock (con) - { - con.SetOnUse(); - var key = KeyGen.Generate(10, "con."); - con.mykey = key; - return (con, key); - } - } - - // Retorna la Conexión - var conexión = new Conexión - { - mykey = KeyGen.Generate(10, "con.") - }; - conexión.SetOnUse(); - return (conexión, conexión.mykey); - - } - - - - /// - /// Obtiene una Conexión alterna a la base de datos - /// - public static Conexión GetForcedConnection(string? message = null) - { - return new(); - } - - -} \ No newline at end of file diff --git a/LIN.Identity/Controllers/ApiController.cs b/LIN.Identity/Controllers/ApiController.cs deleted file mode 100644 index d5440fd..0000000 --- a/LIN.Identity/Controllers/ApiController.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace LIN.Identity.Controllers; - - -[Route("/")] -public class ApiVersion : Controller -{ - - - - /// - /// Obtiene el estado del servidor - /// - [HttpGet("status")] - public dynamic Status() - { - return StatusCode(200, new - { - Status = "Open" - }); - } - - - - - - [HttpGet] - public dynamic Index() - { - return Ok("Abierto"); - } - - -} \ No newline at end of file diff --git a/LIN.Identity/Controllers/ApplicationController.cs b/LIN.Identity/Controllers/ApplicationController.cs deleted file mode 100644 index 7138d53..0000000 --- a/LIN.Identity/Controllers/ApplicationController.cs +++ /dev/null @@ -1,119 +0,0 @@ -namespace LIN.Identity.Controllers; - - -[Route("applications")] -public class ApplicationController : ControllerBase -{ - - - /// - /// Crear nueva aplicación. - /// - /// Modelo. - /// Token de acceso. - [HttpPost("create")] - public async Task Create([FromBody] ApplicationModel applicationModel, [FromHeader] string token) - { - - // Información del token. - var (isValid, _, userID, _, _) = Jwt.Validate(token); - - // Si el token es invalido. - if (!isValid) - return new CreateResponse() - { - Response = Responses.Unauthorized, - Message = "El token es invalido." - }; - - // Validaciones. - if (applicationModel == null || applicationModel.ApplicationUid.Trim().Length < 4 || applicationModel.Name.Trim().Length < 4) - return new CreateResponse() - { - Response = Responses.InvalidParam, - Message = "Parámetros inválidos." - }; - - // Preparar el modelo - applicationModel.ApplicationUid = applicationModel.ApplicationUid.Trim().ToLower(); - applicationModel.Name = applicationModel.Name.Trim().ToLower(); - applicationModel.AccountID = userID; - - // Crear la aplicación. - return await Data.Applications.Create(applicationModel); - - } - - - - /// - /// Obtener las aplicaciones asociadas - /// - /// Token de acceso - [HttpGet] - public async Task> GetAll([FromHeader] string token) - { - - // Información del token. - var (isValid, _, userID, _, _) = Jwt.Validate(token); - - // Si el token es invalido. - if (!isValid) - return new ReadAllResponse() - { - Response = Responses.Unauthorized, - Message = "El token es invalido." - }; - - // Obtiene la data. - var data = await Data.Applications.ReadAll(userID); - - return data; - - } - - - - /// - /// Crear acceso permitido a una app. - /// - /// Token de acceso. - /// ID de la aplicación. - /// ID del integrante. - [HttpPut] - public async Task> InsertAllow([FromHeader] string token, [FromHeader] int appId, [FromHeader] int accountId) - { - - // Información del token. - var (isValid, _, userId, _, _) = Jwt.Validate(token); - - // Si el token es invalido. - if (!isValid) - return new ReadOneResponse() - { - Response = Responses.Unauthorized, - Message = "El token es invalido." - }; - - // Respuesta de Iam. - var iam = await Services.Iam.Applications.ValidateAccess(userId, appId); - - // Validación de Iam - if (iam.Model != IamLevels.Privileged) - return new ReadOneResponse() - { - Response = Responses.Unauthorized, - Message = "No tienes autorización para modificar este recurso." - }; - - - // Enviar la actualización - var data = await Data.Applications.AllowTo(appId, accountId); - - return data; - - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Controllers/AuthenticationController.cs b/LIN.Identity/Controllers/AuthenticationController.cs deleted file mode 100644 index 925bef4..0000000 --- a/LIN.Identity/Controllers/AuthenticationController.cs +++ /dev/null @@ -1,117 +0,0 @@ -using LIN.Identity.Services.Login; - -namespace LIN.Identity.Controllers; - - -[Route("authentication")] -public class AuthenticationController : ControllerBase -{ - - - /// - /// Inicia una sesión de usuario - /// - /// Usuario único - /// Contraseña del usuario - /// Key de aplicación - [HttpGet("login")] - public async Task> Login([FromQuery] string user, [FromQuery] string password, [FromHeader] string application) - { - - // Validación de parámetros. - if (!user.Any() || !password.Any() || !application.Any()) - return new(Responses.InvalidParam); - - // Obtiene el usuario. - var response = await Data.Accounts.Read(user, new() - { - SensibleInfo = true, - IsAdmin = true, - IncludeOrg = FilterModels.IncludeOrg.Include, - OrgLevel = FilterModels.IncludeOrgLevel.Advance, - FindOn = FilterModels.FindOn.StableAccounts - }); - - // Validación al obtener el usuario - switch (response.Response) - { - // Correcto - case Responses.Success: - break; - - // Incorrecto - default: - return new(response.Response); - } - - - // Estrategia de login - LoginService strategy; - - // Definir la estrategia - strategy = response.Model.OrganizationAccess == null ? new LoginNormal(response.Model, application, password) - : new LoginOnOrg(response.Model, application, password); - - // Respuesta del login - var loginResponse = await strategy.Login(); - - - // Respuesta - if (loginResponse.Response != Responses.Success) - return new ReadOneResponse() - { - Message = loginResponse.Message, - Response = loginResponse.Response - }; - - - // Genera el token - var token = Jwt.Generate(response.Model, 0); - - - response.Token = token; - return response; - - } - - - - /// - /// Inicia una sesión de usuario por medio del token - /// - /// Token de acceso - [HttpGet("LoginWithToken")] - public async Task> LoginWithToken([FromHeader] string token) - { - - // Valida el token - var (isValid, _, user, _, _) = Jwt.Validate(token); - - if (!isValid) - return new(Responses.InvalidParam); - - - // Obtiene el usuario - var response = await Data.Accounts.Read(user, new() - { - SensibleInfo = true, - IsAdmin = true, - IncludeOrg = FilterModels.IncludeOrg.Include, - OrgLevel = FilterModels.IncludeOrgLevel.Advance, - FindOn = FilterModels.FindOn.StableAccounts - }); - - if (response.Response != Responses.Success) - return new(response.Response); - - if (response.Model.Estado != AccountStatus.Normal) - return new(Responses.NotExistAccount); - - response.Token = token; - return response; - - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Controllers/DeviceController.cs b/LIN.Identity/Controllers/DeviceController.cs deleted file mode 100644 index bf93b48..0000000 --- a/LIN.Identity/Controllers/DeviceController.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace LIN.Identity.Controllers; - - -[Route("devices")] -public class DeviceController : ControllerBase -{ - - - /// - /// Obtiene la lista de dispositivos asociados a una cuenta en tiempo real - /// - /// Token de acceso - [HttpGet] - public HttpReadAllResponse GetAll([FromHeader] string token) - { - try - { - - var (isValid, _, userId, _, _) = Jwt.Validate(token); - - if (!isValid) - { - return new ReadAllResponse - { - Message = "Token Invalido", - Response = Responses.Unauthorized - }; - } - - - var devices = (from account in AccountHub.Cuentas - where account.Key == userId - select account).FirstOrDefault().Value ?? new(); - - - var filter = (from device in devices - where device.Estado == DeviceState.Actived - select device).ToList(); - - // Retorna - return new(Responses.Success, filter ?? new()); - } - catch - { - return new(Responses.Undefined) - { - Message = "Hubo un error al obtener los dispositivos asociados." - }; - } - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Controllers/IntentsController.cs b/LIN.Identity/Controllers/IntentsController.cs deleted file mode 100644 index 3315034..0000000 --- a/LIN.Identity/Controllers/IntentsController.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace LIN.Identity.Controllers; - - -[Route("Intents")] -public class IntentsController : ControllerBase -{ - - /// - /// Obtiene la lista de intentos Passkey activos - /// - /// Token de acceso - [HttpGet] - public HttpReadAllResponse GetAll([FromHeader] string token) - { - try - { - - // Info del token - var (isValid, user, _, _, _) = Jwt.Validate(token); - - // Si el token es invalido - if (!isValid) - return new ReadAllResponse - { - Message = "Invalid Token", - Response = Responses.Unauthorized - }; - - - // Cuenta - var account = (from a in PassKeyHub.Attempts - where a.Key == user.ToLower() - select a).FirstOrDefault().Value ?? new(); - - // Hora actual - var timeNow = DateTime.Now; - - // Intentos - var intentos = (from I in account - where I.Status == PassKeyStatus.Undefined - where I.Expiración > timeNow - select I).ToList(); - - // Retorna - return new(Responses.Success, intentos); - } - catch - { - return new(Responses.Undefined) - { - Message = "Hubo un error al obtener los intentos de passkey" - }; - } - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Controllers/LoginLogController.cs b/LIN.Identity/Controllers/LoginLogController.cs deleted file mode 100644 index 9bed542..0000000 --- a/LIN.Identity/Controllers/LoginLogController.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace LIN.Identity.Controllers; - - -[Route("Account/logs")] -public class LoginLogController : ControllerBase -{ - - - /// - /// Obtienes la lista de accesos asociados a una cuenta - /// - /// Token de acceso - [HttpGet("read/all")] - public async Task> GetAll([FromHeader] string token) - { - - // JWT. - var (isValid, _, userId, _, _) = Jwt.Validate(token); - - // Validación. - if (!isValid) - return new(Responses.Unauthorized) - { - Message = "Token invalido." - }; - - // Obtiene el usuario. - var result = await Data.Logins.ReadAll(userId); - - // Retorna el resultado. - return result ?? new(); - - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Controllers/MailController.cs b/LIN.Identity/Controllers/MailController.cs deleted file mode 100644 index 7b9e870..0000000 --- a/LIN.Identity/Controllers/MailController.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace LIN.Identity.Controllers; - - -[Route("mails")] -public class MailController : ControllerBase -{ - - - /// - /// Obtiene los mails asociados a una cuenta - /// - /// Token de acceso - [HttpGet("all")] - public async Task> GetMails([FromHeader] string token) - { - - // Información del token. - var (isValid, _, id, _, _) = Jwt.Validate(token); - - // Validación del token - if (!isValid) - return new(Responses.Unauthorized) - { - Message = "Token invalido." - }; - - return await Data.Mails.ReadAll(id); - - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Controllers/Security/Security.cs b/LIN.Identity/Controllers/Security/Security.cs deleted file mode 100644 index d23d1a0..0000000 --- a/LIN.Identity/Controllers/Security/Security.cs +++ /dev/null @@ -1,414 +0,0 @@ -namespace LIN.Identity.Controllers.Security; - - -[Route("security")] -public class Security : ControllerBase -{ - - - /// - /// Olvidar contraseña - /// - /// Usuario - [HttpPost("password/forget")] - public async Task> ForgetPassword([FromQuery] string user) - { - - // Nulo o vacío - if (string.IsNullOrWhiteSpace(user)) - return new(Responses.InvalidParam); - - - // Obtiene la conexión - var (context, contextKey) = Conexión.GetOneConnection(); - - // Obtiene la información de usuario - var userResponse = await Data.Accounts.ReadBasic(user, context); - - // Evalúa la respuesta - if (userResponse.Response != Responses.Success) - { - context.CloseActions(contextKey); - return new(userResponse.Response); - } - - // Modelo del usuario - var userData = userResponse.Model; - - // Obtiene los emails asociados - var emailResponse = await Data.Mails.ReadVerifiedEmails(userData.ID, context); - - // Evalúa la respuesta - if (emailResponse.Response != Responses.Success) - { - context.CloseActions(contextKey); - return new(emailResponse.Response); - } - - // Modelos de emails - var emailsData = emailResponse.Models; - - // Emails verificados - var verifiedMail = emailsData.FirstOrDefault(mail => mail.IsDefault); - - // Valida - if (verifiedMail == null || !Mail.Validar(verifiedMail.Email ?? string.Empty)) - { - context.CloseActions(contextKey); - return new(); - } - - // Modelo del link - var link = new UniqueLink() - { - Key = KeyGen.Generate(30, string.Empty), - AccountID = userData.ID, - Status = MagicLinkStatus.Activated, - Vencimiento = DateTime.Now.AddMinutes(30) - }; - - // Crea el nuevo link - var linkResponse = await Data.Links.Create(link); - - // Evalúa - if (linkResponse.Response != Responses.Success) - { - context.CloseActions(contextKey); - return new(Responses.NotRows); - } - - - // Envía el correo - await EmailWorker.SendPassword(verifiedMail?.Email!, userData.Usuario, $"http://linaccount.somee.com/resetpassword/{userData.ID}/{link.Key}"); - - - return new(Responses.Success, new() - { - Email = CensorEmail(verifiedMail?.Email ?? "") - }); - } - - - - private static string CensorEmail(string email) - { - var atIndex = email.IndexOf('@'); - - if (atIndex >= 0) - { - var dotIndex = email.IndexOf('.'); - if (dotIndex > atIndex + 3) - { - var username = email.Substring(0, atIndex - 3); // Censura los últimos 3 caracteres antes del "@". - var domain = email.Substring(atIndex); // Censura el dominio antes del primer punto. - return $"{username}***{domain}"; - } - } - return email; - } - - - - - - - - /// - /// Reestablece la contraseña - /// - /// Llave del link - /// Nuevo modelo - [HttpPatch("password/reset")] - public async Task ResetPassword([FromHeader] string key, [FromBody] UpdatePasswordModel modelo) - { - - // Nulo o vacío - if (string.IsNullOrWhiteSpace(key)) - return new(Responses.InvalidParam); - - // Link - var link = await Data.Links.ReadOneAnChange(key); - - // Evalúa - if (link.Response != Responses.Success) - { - return new(Responses.Unauthorized); - } - - // Establece el id - modelo.Account = link.Model.AccountID; - - // Respuesta - var updateResponse = await Data.Accounts.Update(modelo); - - if (updateResponse.Response != Responses.Success) - return new(); - - return new(Responses.Success); - } - - - - /// - /// Verifica un correo - /// - /// Key de acceso LINK - [HttpPost("mails/verify")] - public async Task VerifyEmail([FromHeader] string key) - { - - // Obtiene una conexión - var (context, contextKey) = Conexión.GetOneConnection(); - - // Obtiene un link - var linkResponse = await Data.MailLinks.ReadAndDisable(key); - - // Evalúa - if (linkResponse.Response != Responses.Success) - { - context.CloseActions(contextKey); - return new(); - } - - // Modelo del link - var linkData = linkResponse.Model; - - - // Obtiene el email - var emailResponse = await Data.Mails.Read(linkData.Email, context); - - // Evalúa - if (emailResponse.Response != Responses.Success) - { - context.CloseActions(contextKey); - return new(); - } - - // Modelo del mail - var emailData = emailResponse.Model; - - // Actualiza el estado del mail - var updateState = await Data.Mails.UpdateState(emailData.ID, EmailStatus.Verified, context); - - // Evalúa - if (updateState.Response != Responses.Success) - { - context.CloseActions(contextKey); - return new(); - } - - - // Comprobación de email default - { - - // Respuesta - var emails = await Data.Mails.ReadVerifiedEmails(emailData.UserID); - - // Evalúa - if (emails.Response == Responses.Success) - { - // Existe el mail default - var haveDefault = emails.Models.Where(E => E.IsDefault).Any(); - - // Si no existe - if (!haveDefault) - { - // Establece el mail actual - var res = await Data.Mails.SetDefaultEmail(emailData.UserID, emailData.ID); - - } - - } - - } - - return new(Responses.Success); - } - - - - /// - /// Agrega un nuevo email a una cuenta - /// - /// Contraseña de la cuenta - /// Modelo del email - [HttpPost("mails/add")] - public async Task EmailAdd([FromHeader] string password, [FromBody] EmailModel model) - { - - // Obtener el usuario - var userData = await Data.Accounts.ReadBasic(model.UserID); - - // Evaluación de la respuesta - if (userData.Response != Responses.Success) - { - return new(Responses.NotExistAccount); - } - - // Evaluación de la contraseña - if (userData.Model.Contraseña != EncryptClass.Encrypt(password)) - return new(Responses.Unauthorized); - - // Conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - // Obtiene los mails actuales - var mails = await Data.Mails.ReadAll(userData.Model.ID, context); - - // Evalúa la respuesta - if (mails.Response != Responses.Success) - { - context.CloseActions(connectionKey); - return new(); - } - - // Este email ya esta en la cuenta? - var countEmail = mails.Models.Where(T => T.Email == model.Email).Count(); - - // Si ya existía - if (countEmail > 0) - { - context.CloseActions(connectionKey); - return new(); - } - - // Agrega el mail - var addMail = await Data.Mails.Create(new() - { - Status = EmailStatus.Unverified, - Email = model.Email, - ID = 0, - UserID = userData.Model.ID - }, context); - - - // Evalúa la respuesta - if (addMail.Response != Responses.Success) - { - context.CloseActions(connectionKey); - return new(); - } - - //Crea el LINK - var emailLink = new MailMagicLink() - { - Email = addMail.LastID, - Status = MagicLinkStatus.Activated, - Vencimiento = DateTime.Now.AddMinutes(10), - Key = KeyGen.Generate(20, "eml") - }; - - try - { - // Agrega y guarda el link - context.DataBase.MailMagicLinks.Add(emailLink); - - - var bytes = Encoding.UTF8.GetBytes(model.Email); - var mail64 = Convert.ToBase64String(bytes); - - bytes = Encoding.UTF8.GetBytes(userData.Model.Usuario); - var user64 = Convert.ToBase64String(bytes); - - await EmailWorker.SendVerification(model.Email, $"http://linaccount.somee.com/verificate/{user64}/{mail64}/{emailLink.Key}", model.Email); - return new(Responses.Success); - - } - catch - { - - } - finally - { - context.DataBase.SaveChanges(); - } - - return new(); - - } - - - - /// - /// Reenvía el correo para la activación - /// - /// Contraseña de la cuenta - /// Modelo del email - [HttpPost("mails/resend")] - public async Task EmailResend([FromHeader] int mailID, [FromHeader] string token) - { - - - var (isValid, _, userID, _, _) = Jwt.Validate(token); - - if (!isValid) - { - return new(Responses.Unauthorized); - } - - var mailResponse = await Data.Mails.Read(mailID); - - // Evaluación de la respuesta - if (mailResponse.Response != Responses.Success) - { - return new(Responses.NotRows); - } - - var mailData = mailResponse.Model; - - if (mailData.Status == EmailStatus.Verified) - { - return new(Responses.Undefined); - } - - - - - // Crea el LINK - var emailLink = new MailMagicLink() - { - Email = mailData.ID, - Status = MagicLinkStatus.Activated, - Vencimiento = DateTime.Now.AddMinutes(10), - Key = KeyGen.Generate(20, "ml") - }; - - - try - { - - var add = await Data.MailLinks.Create(emailLink); - - - if (add.Response != Responses.Success) - { - return new(); - } - - var user = (await Data.Accounts.ReadBasic(userID)).Model; - - var bytes = Encoding.UTF8.GetBytes(mailData.Email); - var mail64 = Convert.ToBase64String(bytes); - - bytes = Encoding.UTF8.GetBytes(user.Usuario); - var user64 = Convert.ToBase64String(bytes); - - await EmailWorker.SendVerification(mailData.Email, $"http://linaccount.somee.com/verificate/{user64}/{mail64}/{emailLink.Key}", mailData.Email); - return new(Responses.Success); - - } - catch - { - - } - finally - { - } - - return new(); - - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Data/Accounts/AccountsGet.cs b/LIN.Identity/Data/Accounts/AccountsGet.cs deleted file mode 100644 index bca3115..0000000 --- a/LIN.Identity/Data/Accounts/AccountsGet.cs +++ /dev/null @@ -1,346 +0,0 @@ -namespace LIN.Identity.Data; - - -internal static partial class Accounts -{ - - - #region Abstracciones - - - public static async Task> Read(int id, FilterModels.Account filters) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - var res = await Read(id, filters, context); - context.CloseActions(connectionKey); - return res; - - } - - - - - - - /// - /// Obtiene una cuenta - /// - /// ID de la cuenta - /// Incluir la organización - public static async Task> Read(string user, FilterModels.Account filters) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - var res = await Read(user, filters, context); - context.CloseActions(connectionKey); - return res; - - } - - - - /// - /// Obtiene la información básica de un usuario - /// - /// ID del usuario - public static async Task> ReadBasic(int id) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - var res = await ReadBasic(id, context); - context.CloseActions(connectionKey); - return res; - - } - - - - /// - /// Obtiene la información básica de un usuario - /// - /// Usuario único - public static async Task> ReadBasic(string user) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - var res = await ReadBasic(user, context); - context.CloseActions(connectionKey); - return res; - - } - - - - - /// - /// Obtiene una lista de diez (10) usuarios que coincidan con un patron - /// - /// Patron de búsqueda - /// Mi ID - /// ID de la org de contexto - public static async Task> Search(string pattern, FilterModels.Account filters) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - var res = await Search(pattern, filters, context); - context.CloseActions(connectionKey); - return res; - } - - - - - - - /// - /// Obtiene una lista de usuarios por medio del ID - /// - /// Lista de IDs - /// ID del usuario contexto - /// ID de organización - public static async Task> FindAll(List ids, FilterModels.Account filters) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - var res = await FindAll(ids, filters, context); - context.CloseActions(connectionKey); - return res; - } - - - #endregion - - - - - /// - /// Obtiene un usuario - /// - /// ID del usuario - /// Filtros de búsqueda - /// Contexto de base de datos - public static async Task> Read(int id, FilterModels.Account filters, Conexión context) - { - - // Ejecución - try - { - - var query = Queries.Accounts.GetAccounts(id, filters, context); - - // Obtiene el usuario - var result = await query.FirstOrDefaultAsync(); - - // Si no existe el modelo - if (result == null) - return new(Responses.NotExistAccount); - - return new(Responses.Success, result); - } - catch - { - } - - return new(); - } - - - - /// - /// Obtiene un usuario - /// - /// Usuario único - /// Filtros de búsqueda - /// Contexto de base de datos - public static async Task> Read(string user, FilterModels.Account filters, Conexión context) - { - - // Ejecución - try - { - - var query = Queries.Accounts.GetAccounts(user, filters, context); - - // Obtiene el usuario - var result = await query.FirstOrDefaultAsync(); - - // Si no existe el modelo - if (result == null) - return new(Responses.NotExistAccount); - - return new(Responses.Success, result); - } - catch - { - } - - return new(); - } - - - - - /// - /// Buscar usuarios por patron de búsqueda. - /// - /// Patron de búsqueda - /// ID del usuario contexto - /// ID de la organización de contexto - /// Es administrador - /// Contexto de base de datos - public static async Task> Search(string pattern, FilterModels.Account filters, Conexión context) - { - - // Ejecución - try - { - - List accountModels = await Queries.Accounts.Search(pattern, filters, context).Take(10).ToListAsync(); - - // Si no existe el modelo - if (accountModels == null) - return new(Responses.NotRows); - - return new(Responses.Success, accountModels); - } - catch - { - } - - return new(); - } - - - - /// - /// Obtiene los usuarios con IDs coincidentes - /// - /// Lista de IDs - /// ID del usuario contexto - /// ID de la organización de contexto - /// Contexto de base de datos - public static async Task> FindAll(List ids, FilterModels.Account filters, Conexión context) - { - - // Ejecución - try - { - - var query = Queries.Accounts.GetAccounts(ids, filters, context); - - // Ejecuta - var result = await query.ToListAsync(); - - // Si no existe el modelo - if (result == null) - return new(Responses.NotRows); - - return new(Responses.Success, result); - } - catch - { - } - - return new(); - } - - - - - - /// - /// Obtiene la información básica de un usuario - /// - /// ID del usuario - /// Contexto de base de datos - public static async Task> ReadBasic(int id, Conexión context) - { - - // Ejecución - try - { - - var query = from account in Queries.Accounts.GetValidAccounts(context) - where account.ID == id - select new AccountModel - { - ID = account.ID, - Usuario = account.Usuario, - Contraseña = account.Contraseña, - Estado = account.Estado, - Nombre = account.Nombre, - OrganizationAccess = account.OrganizationAccess - }; - - // Obtiene el usuario - var result = await query.FirstOrDefaultAsync(); - - // Si no existe el modelo - if (result == null) - return new(Responses.NotExistAccount); - - return new(Responses.Success, result); - } - catch - { - } - - return new(); - } - - - - /// - /// Obtiene la información básica de un usuario - /// - /// Usuario único - /// Contexto de base de datos - public static async Task> ReadBasic(string user, Conexión context) - { - - // Ejecución - try - { - - var query = from account in Queries.Accounts.GetValidAccounts(context) - where account.Usuario == user - select new AccountModel - { - ID = account.ID, - Usuario = account.Usuario, - Contraseña = account.Contraseña, - Estado = account.Estado, - Nombre = account.Nombre, - OrganizationAccess = account.OrganizationAccess - }; - - // Obtiene el usuario - var result = await query.FirstOrDefaultAsync(); - - // Si no existe el modelo - if (result == null) - return new(Responses.NotExistAccount); - - return new(Responses.Success, result); - } - catch - { - } - - return new(); - } - -} \ No newline at end of file diff --git a/LIN.Identity/Data/Accounts/AccountsPost.cs b/LIN.Identity/Data/Accounts/AccountsPost.cs deleted file mode 100644 index af6c5b6..0000000 --- a/LIN.Identity/Data/Accounts/AccountsPost.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace LIN.Identity.Data; - - -internal static partial class Accounts -{ - - - #region Abstracciones - - - /// - /// Crea una nueva cuenta - /// - /// Modelo de la nueva cuenta - public static async Task> Create(AccountModel data) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - var res = await Create(data, context); - context.CloseActions(connectionKey); - return res; - - } - - - #endregion - - - /// - /// Crea una nueva cuenta - /// - /// Modelo de la cuenta - /// Contexto de base de datos - public static async Task> Create(AccountModel data, Conexión context) - { - - data.ID = 0; - - // Ejecución - try - { - var res = await context.DataBase.Accounts.AddAsync(data); - context.DataBase.SaveChanges(); - - return new(Responses.Success, data); - } - catch (Exception ex) - { - if ((ex.InnerException?.Message.Contains("Violation of UNIQUE KEY constraint") ?? false) || (ex.InnerException?.Message.Contains("duplicate key") ?? false)) - return new(Responses.ExistAccount); - - } - - return new(); - } - - -} \ No newline at end of file diff --git a/LIN.Identity/Data/Accounts/AccountsUpdate.cs b/LIN.Identity/Data/Accounts/AccountsUpdate.cs deleted file mode 100644 index c21cb6d..0000000 --- a/LIN.Identity/Data/Accounts/AccountsUpdate.cs +++ /dev/null @@ -1,346 +0,0 @@ -namespace LIN.Identity.Data; - - -internal static partial class Accounts -{ - - - #region Abstracciones - - - /// - /// Elimina una cuenta - /// - /// ID de la cuenta - public static async Task Delete(int id) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - var res = await Delete(id, context); - context.CloseActions(connectionKey); - return res; - } - - - - /// - /// Actualiza la información de una cuenta - /// - /// Modelo nuevo de la cuenta - public static async Task Update(AccountModel modelo) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - var res = await Update(modelo, context); - context.CloseActions(connectionKey); - return res; - } - - - - /// - /// Actualiza las credenciales (Contraseña de un usuario) - /// - /// Nuevas credenciales - public static async Task Update(UpdatePasswordModel newData) - { - - var (context, key) = Conexión.GetOneConnection(); - - var res = await Update(newData, context); - context.CloseActions(key); - return res; - - } - - - - /// - /// Actualiza el estado de un usuario - /// - /// ID del usuario - /// Nuevo estado - public static async Task Update(int id, AccountStatus status) - { - - var (context, key) = Conexión.GetOneConnection(); - - var res = await Update(id, status, context); - context.CloseActions(key); - return res; - - } - - - - /// - /// Actualiza el genero de un usuario - /// - /// ID del usuario - /// Nuevo genero - public static async Task Update(int id, Genders gender) - { - - var (context, key) = Conexión.GetOneConnection(); - - var res = await Update(id, gender, context); - context.CloseActions(key); - return res; - - } - - - - /// - /// Actualiza la visibilidad de un usuario - /// - /// ID del usuario - /// Nueva visibilidad - public static async Task Update(int id, AccountVisibility visibility) - { - - var (context, key) = Conexión.GetOneConnection(); - - var res = await Update(id, visibility, context); - context.CloseActions(key); - return res; - - } - - - #endregion - - - /// - /// Elimina una cuenta. - /// - /// ID de la cuenta - /// Contexto de conexión - public static async Task Delete(int id, Conexión context) - { - - // Ejecución - try - { - - // Obtiene el usuario - var user = await context.DataBase.Accounts.FindAsync(id); - - // Si no existe - if (user == null) - return new(Responses.Success); - - // Cambia el estado - user.Estado = AccountStatus.Deleted; - context.DataBase.SaveChanges(); - - // Retorna - return new(Responses.Success); - } - catch - { - } - - return new(); - } - - - - /// - /// Actualiza la información de una cuenta - /// ** No actualiza datos sensibles - /// - /// Modelo nuevo de la cuenta - /// Contexto de conexión - public static async Task Update(AccountModel modelo, Conexión context) - { - - // Ejecución - try - { - var user = await context.DataBase.Accounts.FindAsync(modelo.ID); - - // Si el usuario no se encontró - if (user == null || user.Estado != AccountStatus.Normal) - { - return new(Responses.NotExistAccount); - } - - // Nuevos datos - user.Perfil = modelo.Perfil; - user.Nombre = modelo.Nombre; - user.Genero = modelo.Genero; - - context.DataBase.SaveChanges(); - return new(Responses.Success); - } - catch - { - } - - return new(); - } - - - - /// - /// Actualiza la contraseña - /// - /// Nuevas credenciales - /// Contexto de conexión con la BD - public static async Task Update(UpdatePasswordModel newData, Conexión context) - { - - // Encontrar el usuario - var usuario = await (from U in context.DataBase.Accounts - where U.ID == newData.Account - select U).FirstOrDefaultAsync(); - - // Si el usuario no existe - if (usuario == null) - { - return new(Responses.NotExistAccount); - } - - // Confirmar contraseña - var newEncrypted = EncryptClass.Encrypt(newData.NewPassword); - - // Cambiar Contraseña - usuario.Contraseña = newEncrypted; - - context.DataBase.SaveChanges(); - return new(Responses.Success); - - } - - - - /// - /// Actualiza la organización de una cuenta - /// - /// organización - /// Contexto de conexión con la BD - public static async Task Update(OrganizationModel newData, int id, Conexión context) - { - - // Encontrar el usuario - var usuario = await (from U in context.DataBase.OrganizationAccess - where U.Member.ID == id - select U).FirstOrDefaultAsync(); - - - var org = await (from U in context.DataBase.Organizations - where U.ID == newData.ID - select U - ).FirstOrDefaultAsync(); - - - - // Si el usuario no existe - if (usuario == null || org == null) - { - return new(Responses.NotExistAccount); - } - - // Cambiar Contraseña - usuario.Organization = org; - - context.DataBase.SaveChanges(); - return new(Responses.Success); - - } - - - - /// - /// Actualiza el estado - /// - /// ID - /// Nuevo estado - /// Contexto de conexión con la BD - public static async Task Update(int user, AccountStatus status, Conexión context) - { - - // Encontrar el usuario - var usuario = await (from U in context.DataBase.Accounts - where U.ID == user - select U).FirstOrDefaultAsync(); - - // Si el usuario no existe - if (usuario == null) - { - return new(Responses.NotExistAccount); - } - - // Cambiar Contraseña - usuario.Estado = status; - - context.DataBase.SaveChanges(); - return new(Responses.Success); - - } - - - - /// - /// Actualiza el genero - /// - /// ID - /// Nuevo genero - /// Contexto de conexión con la BD - public static async Task Update(int user, Genders genero, Conexión context) - { - - // Encontrar el usuario - var usuario = await (from U in context.DataBase.Accounts - where U.ID == user - select U).FirstOrDefaultAsync(); - - // Si el usuario no existe - if (usuario == null) - { - return new(Responses.NotExistAccount); - } - - // Cambiar Contraseña - usuario.Genero = genero; - - context.DataBase.SaveChanges(); - return new(Responses.Success); - - } - - - - /// - /// Actualiza la visibilidad - /// - /// ID - /// Nueva visibilidad - /// Contexto de conexión con la BD - public static async Task Update(int user, AccountVisibility visibility, Conexión context) - { - - // Encontrar el usuario - var usuario = await (from U in context.DataBase.Accounts - where U.ID == user - select U).FirstOrDefaultAsync(); - - // Si el usuario no existe - if (usuario == null) - { - return new(Responses.NotExistAccount); - } - - // Cambiar visibilidad - usuario.Visibilidad = visibility; - - context.DataBase.SaveChanges(); - return new(Responses.Success); - - } - - -} \ No newline at end of file diff --git a/LIN.Identity/Data/Applications.cs b/LIN.Identity/Data/Applications.cs deleted file mode 100644 index c65ff84..0000000 --- a/LIN.Identity/Data/Applications.cs +++ /dev/null @@ -1,339 +0,0 @@ -namespace LIN.Identity.Data; - - -public class Applications -{ - - - #region Abstracciones - - - /// - /// Crea una app - /// - /// Modelo - public static async Task Create(ApplicationModel data) - { - var (context, contextKey) = Conexión.GetOneConnection(); - var response = await Create(data, context); - context.CloseActions(contextKey); - return response; - } - - - - /// - /// Obtiene la lista de apps asociados a una cuenta - /// - /// ID de la cuenta - public static async Task> ReadAll(int id) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await ReadAll(id, context); - context.CloseActions(contextKey); - return res; - } - - - - - public static async Task> AllowTo(int appId, int accountId) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await AllowTo(appId, accountId, context); - context.CloseActions(contextKey); - return res; - } - - - public static async Task> IsAllow(int appId, int accountId) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await IsAllow(appId, accountId, context); - context.CloseActions(contextKey); - return res; - } - - - - - - - - - - /// - /// Obtiene una app - /// - /// ID de la app - public static async Task> Read(int id) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await Read(id, context); - context.CloseActions(contextKey); - return res; - } - - - /// - /// Obtiene una app - /// - /// Key de la app - public static async Task> Read(string key) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await Read(key, context); - context.CloseActions(contextKey); - return res; - } - - - #endregion - - - - /// - /// Crear aplicación - /// - /// Modelo - /// Contexto de conexión - public static async Task Create(ApplicationModel data, Conexión context) - { - - data.ID = 0; - - // Ejecución - try - { - - foreach (var account in data.Allowed) - { - context.DataBase.Attach(account.Account); - account.App = data; - } - - var res = await context.DataBase.Applications.AddAsync(data); - context.DataBase.SaveChanges(); - - return new(Responses.Success, data.ID); - } - catch (Exception ex) - { - - if ((ex.InnerException?.Message.Contains("Violation of UNIQUE KEY constraint") ?? false) || (ex.InnerException?.Message.Contains("duplicate key") ?? false)) - return new(Responses.Undefined); - - } - - return new(); - } - - - - /// - /// Obtiene la lista de apps asociados a una cuenta - /// - /// ID de la cuenta - /// Contexto de conexión - public static async Task> ReadAll(int id, Conexión context) - { - - // Ejecución - try - { - - // Query - var emails = await (from E in context.DataBase.Applications - where E.AccountID == id - select E).ToListAsync(); - - return new(Responses.Success, emails); - } - catch - { - } - - return new(); - } - - - - /// - /// Obtiene una app - /// - /// ID de la app - /// Contexto de conexión - public static async Task> Read(int id, Conexión context) - { - - // Ejecución - try - { - - // Query - var email = await (from E in context.DataBase.Applications - where E.ID == id - select E).FirstOrDefaultAsync(); - - // Email no existe - if (email == null) - { - return new(Responses.NotRows); - } - - return new(Responses.Success, email); - } - catch - { - } - - return new(); - } - - - - /// - /// Obtiene una app - /// - /// Key de la app - /// Contexto de conexión - public static async Task> Read(string key, Conexión context) - { - - // Ejecución - try - { - - // Query - var email = await (from E in context.DataBase.Applications - where E.Key == key - select E).FirstOrDefaultAsync(); - - // Email no existe - if (email == null) - { - return new(Responses.NotRows); - } - - return new(Responses.Success, email); - } - catch - { - } - - return new(); - } - - - - /// - /// Obtiene una app - /// - /// UId de la app - /// Contexto de conexión - public static async Task> ReadByAppUid(string uid, Conexión context) - { - - // Ejecución - try - { - - // Query - var app = await (from E in context.DataBase.Applications - where E.ApplicationUid == uid - select E).FirstOrDefaultAsync(); - - // Email no existe - if (app == null) - return new(Responses.NotRows); - - return new(Responses.Success, app); - } - catch - { - } - - return new(); - } - - - - - public static async Task> IsAllow(int appId, int accountId, Conexión context) - { - - // Ejecución - try - { - - // Query - var has = from access in context.DataBase.ApplicationAccess - where access.AppID == appId && access.AccountID == accountId - select access; - - var s = has.ToQueryString(); - - var result = await has.FirstOrDefaultAsync(); - - // Email no existe - if (result == null) - { - return new(Responses.Unauthorized); - } - - return new(Responses.Success, true); - } - catch - { - } - - return new(); - } - - - - public static async Task> AllowTo(int appId, int accountId, Conexión context) - { - - // Ejecución - try - { - - - var access = new AppAccessModel() - { - Account = new() - { - ID = accountId - }, - App = new() - { - ID = appId - } - }; - - context.DataBase.Attach(access.Account); - context.DataBase.Attach(access.App); - - await context.DataBase.AddAsync(access); - context.DataBase.SaveChanges(); - - return new(Responses.Success, true); - } - catch - { - } - - return new(); - } - - - - - -} \ No newline at end of file diff --git a/LIN.Identity/Data/Context.cs b/LIN.Identity/Data/Context.cs deleted file mode 100644 index 3b38c4c..0000000 --- a/LIN.Identity/Data/Context.cs +++ /dev/null @@ -1,193 +0,0 @@ -namespace LIN.Identity.Data; - - -public class Context : DbContext -{ - - - /// - /// Tabla de cuentas - /// - public DbSet Accounts { get; set; } - - - /// - /// Tabla de organizaciones - /// - public DbSet Organizations { get; set; } - - - - /// - /// Tabla de accesos a organizaciones - /// - public DbSet OrganizationAccess { get; set; } - - - - - public DbSet ApplicationAccess { get; set; } - - - - /// - /// Tabla de aplicaciones - /// - public DbSet Applications { get; set; } - - - - /// - /// Tabla de aplicaciones - /// - public DbSet AppOnOrg { get; set; } - - - /// - /// Tabla de registros de login - /// - public DbSet LoginLogs { get; set; } - - - - /// - /// Tabla de correos - /// - public DbSet Emails { get; set; } - - - - /// - /// Tabla de links únicos - /// - public DbSet UniqueLinks { get; set; } - - - - /// - /// Tabla de links únicos para email - /// - public DbSet MailMagicLinks { get; set; } - - - - - - - /// - /// Nuevo contexto a la base de datos - /// - public Context(DbContextOptions options) : base(options) { } - - - - /// - /// Naming DB - /// - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - - // Indices y identidad - modelBuilder.Entity() - .HasIndex(e => e.Usuario) - .IsUnique(); - - // Indices y identidad - modelBuilder.Entity() - .HasIndex(e => e.Domain) - .IsUnique(); - - // Indices y identidad - modelBuilder.Entity() - .HasIndex(e => e.Key) - .IsUnique(); - - // Indices y identidad - modelBuilder.Entity() - .HasIndex(e => e.ApplicationUid) - .IsUnique(); - - - // Indices y identidad - modelBuilder.Entity() - .HasIndex(e => e.Email) - .IsUnique(); - - // Indices y identidad - modelBuilder.Entity() - .HasIndex(e => e.Key) - .IsUnique(); - - // Indices y identidad - modelBuilder.Entity() - .HasIndex(e => e.Key) - .IsUnique(); - - // Indices - modelBuilder.Entity().HasIndex(e => e.ID); - - - modelBuilder.Entity() - .HasOne(a => a.OrganizationAccess) - .WithOne(oa => oa.Member) - .HasForeignKey(oa => oa.ID); - - - modelBuilder.Entity() - .HasKey(a => new - { - a.AppID, - a.OrgID - }); - - modelBuilder.Entity() - .HasOne(p => p.App) - .WithMany() - .HasForeignKey(p => p.AppID); - - - modelBuilder.Entity() - .HasOne(p => p.Organization) - .WithMany() - .HasForeignKey(p => p.OrgID); - - - - - - modelBuilder.Entity() - .HasKey(a => new - { - a.AppID, - a.AccountID - }); - - modelBuilder.Entity() - .HasOne(p => p.App) - .WithMany() - .HasForeignKey(p => p.AppID); - - - modelBuilder.Entity() - .HasOne(p => p.Account) - .WithMany() - .HasForeignKey(p => p.AccountID); - - - - - - - // Nombre de la tablas - modelBuilder.Entity().ToTable("ACCOUNTS"); - modelBuilder.Entity().ToTable("ORGANIZATIONS"); - modelBuilder.Entity().ToTable("APPLICATIONS"); - modelBuilder.Entity().ToTable("EMAILS"); - modelBuilder.Entity().ToTable("LOGIN_LOGS"); - modelBuilder.Entity().ToTable("UNIQUE_LINKS"); - modelBuilder.Entity().ToTable("EMAIL_MAGIC_LINKS"); - - } - - -} \ No newline at end of file diff --git a/LIN.Identity/Data/Links.cs b/LIN.Identity/Data/Links.cs deleted file mode 100644 index febf93c..0000000 --- a/LIN.Identity/Data/Links.cs +++ /dev/null @@ -1,164 +0,0 @@ -namespace LIN.Identity.Data; - - -public class Links -{ - - - #region Abstracciones - - - /// - /// Crea un nuevo LINK - /// - /// Modelo del link - public static async Task Create(UniqueLink data) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - var res = await Create(data, context); - context.CloseActions(connectionKey); - return res; - } - - - - /// - /// Obtiene la lista de links asociados a una cuenta - /// - /// ID de la cuenta - public static async Task> ReadAll(int id) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - var res = await ReadAll(id, context); - context.CloseActions(connectionKey); - return res; - - } - - - - /// - /// Obtiene un link y cambia su estado - /// - /// - public static async Task> ReadOneAnChange(string value) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - var res = await ReadOneAnChange(value, context); - context.CloseActions(connectionKey); - return res; - - } - - - #endregion - - - /// - /// Crea un nuevo enlace - /// - /// Modelo del enlace - /// Contexto de conexión - public static async Task Create(UniqueLink data, Conexión context) - { - // ID en 0 - data.ID = 0; - - // Ejecución - try - { - var res = context.DataBase.UniqueLinks.Add(data); - await context.DataBase.SaveChangesAsync(); - return new(Responses.Success, data.ID); - } - catch - { - context.DataBase.Remove(data); - } - return new(); - } - - - - /// - /// Obtiene la lista de links activos asociados a una cuenta - /// - /// ID de la cuenta - /// Contexto de conexión - public static async Task> ReadAll(int id, Conexión context) - { - - // Ejecución - try - { - - var now = DateTime.Now; - - var activos = await (from L in context.DataBase.UniqueLinks - where L.AccountID == id - where L.Vencimiento > now - where L.Status == MagicLinkStatus.Activated - select L).ToListAsync(); - - var lista = activos; - - return new(Responses.Success, lista); - } - catch - { - } - return new(); - - } - - - - /// - /// Obtiene un Magic Link y cambia su estado - /// - /// - /// Contexto de conexión - public static async Task> ReadOneAnChange(string value, Conexión context) - { - - // Ejecución - try - { - - // Fecha actual - var now = DateTime.Now; - - // Consulta - var elemento = await (from L in context.DataBase.UniqueLinks - where L.Vencimiento > now - where L.Status == MagicLinkStatus.Activated - where L.Key == value - select L).FirstOrDefaultAsync(); - // SI es null - if (elemento == null) - return new(Responses.NotRows); - - // Cambia el estado - elemento.Status = MagicLinkStatus.None; - context.DataBase.SaveChanges(); - - return new(Responses.Success, elemento); - } - catch - { - } - return new(); - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Data/Logins.cs b/LIN.Identity/Data/Logins.cs deleted file mode 100644 index d90072d..0000000 --- a/LIN.Identity/Data/Logins.cs +++ /dev/null @@ -1,120 +0,0 @@ -namespace LIN.Identity.Data; - - -public class Logins -{ - - - - #region Abstracciones - - - /// - /// Crea un registro de Login - /// - /// Modelo del login - public static async Task Create(LoginLogModel data) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - var res = await Create(data, context); - context.CloseActions(connectionKey); - return res; - } - - - - /// - /// Obtiene la lista de registros login de una cuenta - /// - /// ID de la cuenta - public static async Task> ReadAll(int id) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - var res = await ReadAll(id, context); - context.CloseActions(connectionKey); - return res; - - } - - - #endregion - - - - /// - /// Crea un registro de Login - /// - /// Modelo del login - /// Contexto de conexión - public static async Task Create(LoginLogModel data, Conexión context) - { - // ID en 0 - data.ID = 0; - - // Ejecución - try - { - - // - context.DataBase.Attach(data.Application); - - var res = context.DataBase.LoginLogs.Add(data); - await context.DataBase.SaveChangesAsync(); - return new(Responses.Success, data.ID); - } - catch - { - } - - return new(); - } - - - - /// - /// Obtiene la lista de registros de acceso de una cuenta - /// - /// ID de la cuenta - /// Contexto de conexión - public static async Task> ReadAll(int id, Conexión context) - { - - // Ejecución - try - { - - var logins = from L in context.DataBase.LoginLogs - where L.AccountID == id - orderby L.Date descending - select new LoginLogModel - { - ID = L.ID, - Type = L.Type, - Date = L.Date, - Platform = L.Platform, - Application = new() - { - Name = L.Application.Name, - Badge = L.Application.Badge - } - }; - - - var result = await logins.Take(50).ToListAsync(); - - return new(Responses.Success, result); - } - catch - { - } - return new(); - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Data/MailLinks.cs b/LIN.Identity/Data/MailLinks.cs deleted file mode 100644 index 5547bfc..0000000 --- a/LIN.Identity/Data/MailLinks.cs +++ /dev/null @@ -1,115 +0,0 @@ -namespace LIN.Identity.Data; - - -public class MailLinks -{ - - - #region Abstracciones - - - /// - /// Crea un nuevo LINK - /// - /// Modelo del link - public static async Task Create(MailMagicLink data) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - var res = await Create(data, context); - context.CloseActions(connectionKey); - return res; - } - - - - /// - /// Obtiene un link activo según su key - /// - /// - public static async Task> ReadAndDisable(string value) - { - - // Obtiene la conexión - var (context, connectionKey) = Conexión.GetOneConnection(); - - var res = await ReadAndDisable(value, context); - context.CloseActions(connectionKey); - return res; - - } - - - #endregion - - - - /// - /// Crea un nuevo enlace para email - /// - /// Modelo del link - /// Contexto de conexión - public static async Task Create(MailMagicLink data, Conexión context) - { - // ID en 0 - data.ID = 0; - - // Ejecución - try - { - var res = context.DataBase.MailMagicLinks.Add(data); - await context.DataBase.SaveChangesAsync(); - - return new(Responses.Success, data.ID); - } - catch - { - context.DataBase.Remove(data); - } - return new(); - } - - - - /// - /// Obtiene un link activo según su key - /// - /// ID de la cuenta - /// Contexto de conexión - /// Llave para cerrar la conexión - public static async Task> ReadAndDisable(string key, Conexión context) - { - - // Ejecución - try - { - - var now = DateTime.Now; - var verification = await (from L in context.DataBase.MailMagicLinks - where L.Key == key - where L.Vencimiento > now - where L.Status == MagicLinkStatus.Activated - select L).FirstOrDefaultAsync(); - - - if (verification == null) - { - return new(); - } - - verification.Status = MagicLinkStatus.Deactivated; - context.DataBase.SaveChanges(); - - return new(Responses.Success, verification); - } - catch - { - } - - return new(); - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Data/Mails.cs b/LIN.Identity/Data/Mails.cs deleted file mode 100644 index 0ef3902..0000000 --- a/LIN.Identity/Data/Mails.cs +++ /dev/null @@ -1,319 +0,0 @@ -namespace LIN.Identity.Data; - - -public class Mails -{ - - - #region Abstracciones - - - /// - /// Crea un nuevo email - /// - /// Modelo - public static async Task Create(EmailModel data) - { - var (context, contextKey) = Conexión.GetOneConnection(); - var response = await Create(data, context); - context.CloseActions(contextKey); - return response; - } - - - - /// - /// Obtiene la lista de emails asociados a una cuenta - /// - /// ID de la cuenta - public static async Task> ReadAll(int id) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await ReadAll(id, context); - context.CloseActions(contextKey); - return res; - } - - - - /// - /// Obtiene un email - /// - /// ID del email - public static async Task> Read(int id) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await Read(id, context); - context.CloseActions(contextKey); - return res; - } - - - - /// - /// Obtiene la lista de emails verificados asociados a una cuenta - /// - /// ID de la cuenta - public static async Task> ReadVerifiedEmails(int id) - { - var (context, contextKey) = Conexión.GetOneConnection(); - var res = await ReadVerifiedEmails(id, context); - context.CloseActions(contextKey); - return res; - } - - - - /// - /// Establece el email default de una cuenta - /// - /// ID de la cuenta - /// ID de el email - public static async Task SetDefaultEmail(int id, int emailId) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await SetDefaultEmail(id, emailId, context); - context.CloseActions(contextKey); - return res; - } - - - - /// - /// Actualiza el estado de un email - /// - /// ID del email - /// Nuevo estado - public static async Task UpdateState(int id, EmailStatus state) - { - var (context, contextKey) = Conexión.GetOneConnection(); - var res = await UpdateState(id, state, context); - context.CloseActions(contextKey); - return res; - } - - - #endregion - - - - /// - /// Crea un nuevo email - /// - /// Modelo - /// Contexto de conexión - public static async Task Create(EmailModel data, Conexión context) - { - - data.ID = 0; - - // Ejecución - try - { - - var res = await context.DataBase.Emails.AddAsync(data); - context.DataBase.SaveChanges(); - - return new(Responses.Success, data.ID); - } - catch (Exception ex) - { - - if ((ex.InnerException?.Message.Contains("Violation of UNIQUE KEY constraint") ?? false) || (ex.InnerException?.Message.Contains("duplicate key") ?? false)) - return new(Responses.Undefined); - - } - - return new(); - } - - - - /// - /// Obtiene la lista de emails asociados a una cuenta - /// - /// ID de la cuenta - /// Contexto de conexión - public static async Task> ReadAll(int id, Conexión context) - { - - // Ejecución - try - { - - // Query - var emails = await (from email in context.DataBase.Emails - where email.UserID == id - where email.Status != EmailStatus.Delete - select email).ToListAsync(); - - return new(Responses.Success, emails); - } - catch (Exception ex) - { - // Notificar a LIN Error Logger. - } - - return new(); - } - - - - /// - /// Obtiene un email - /// - /// ID de el email - /// Contexto de conexión - public static async Task> Read(int id, Conexión context) - { - - // Ejecución - try - { - - // Query - var email = await (from mail in context.DataBase.Emails - where mail.ID == id - select mail).FirstOrDefaultAsync(); - - // Email no existe - if (email == null) - { - return new(Responses.NotRows); - } - - return new(Responses.Success, email); - } - catch (Exception ex) - { - _ = Logger.Log(ex, 1); - } - - return new(); - } - - - - /// - /// Obtiene la lista de emails verificados asociados a una cuenta - /// - /// ID de la cuenta - /// Contexto de conexión - public static async Task> ReadVerifiedEmails(int id, Conexión context) - { - - // Ejecución - try - { - - // Query - var emails = await (from E in context.DataBase.Emails - where E.UserID == id && E.Status == EmailStatus.Verified - select E).ToListAsync(); - - return new(Responses.Success, emails); - } - catch (Exception ex) - { - _ = Logger.Log(ex, 1); - } - - return new(); - } - - - - /// - /// Establece el email default de una cuenta - /// - /// ID de la cuenta - /// ID de el email - /// Contexto de conexión - public static async Task SetDefaultEmail(int id, int emailID, Conexión context) - { - - // Ejecución (Transacción) - using (var transaction = context.DataBase.Database.BeginTransaction(System.Data.IsolationLevel.ReadCommitted)) - { - try - { - // Query - var emails = await (from E in context.DataBase.Emails - where E.UserID == id && E.Status == EmailStatus.Verified - select E).ToListAsync(); - - - var actualDefault = emails.Where(T => T.IsDefault).FirstOrDefault(); - if (actualDefault != null) - { - actualDefault.IsDefault = false; - } - - var setted = emails.Where(T => T.ID == emailID && T.Status == EmailStatus.Verified).FirstOrDefault(); - - if (setted == null) - { - transaction.Rollback(); - return new(Responses.NotRows); - } - - setted.IsDefault = true; - context.DataBase.SaveChanges(); - transaction.Commit(); - - return new(Responses.Success); - } - catch - { - transaction.Rollback(); - } - } - - return new(); - } - - - - /// - /// Actualiza el estado de un email - /// - /// ID de el email - /// Nuevo estado - /// Contexto de conexión - public static async Task UpdateState(int id, EmailStatus state, Conexión context) - { - - // Ejecución - try - { - - // Query - var email = await (from E in context.DataBase.Emails - where E.ID == id - select E).FirstOrDefaultAsync(); - - // Email no existe - if (email == null) - { - return new(Responses.NotRows); - } - - email.Status = state; - context.DataBase.SaveChanges(); - - return new(Responses.Success); - } - catch (Exception ex) - { - _ = Logger.Log(ex, 1); - } - - return new(); - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Data/Organizations/Applications.cs b/LIN.Identity/Data/Organizations/Applications.cs deleted file mode 100644 index bea9507..0000000 --- a/LIN.Identity/Data/Organizations/Applications.cs +++ /dev/null @@ -1,183 +0,0 @@ -namespace LIN.Identity.Data.Organizations; - - -public class Applications -{ - - - - #region Abstractions - - - /// - /// Obtiene la lista de apps que coincidan con un patron y que no estén agregadas a una organización - /// - /// Parámetro de búsqueda - /// ID de la organización - public static async Task> Search(string param, int org) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await Search(param, org, context); - context.CloseActions(contextKey); - return res; - } - - - - /// - /// Crea una pp en la lista blanca de una organización - /// - /// UID de la aplicación - /// ID de la organización - public static async Task Create(string appUid, int org) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await Create(appUid, org, context); - context.CloseActions(contextKey); - return res; - } - - - - /// - /// Encuentra una app en una organización - /// - /// Key de la app - /// ID de la organización - public static async Task> AppOnOrg(string key, int org) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await AppOnOrg(key, org, context); - context.CloseActions(contextKey); - return res; - } - - - #endregion - - - - /// - /// Encuentra una app en una organización - /// - /// Key de la app - /// ID de la organización - /// Contexto de conexión - public static async Task> AppOnOrg(string key, int org, Conexión context) - { - - // Ejecución - try - { - - // Query - var app = await (from E in context.DataBase.AppOnOrg - where E.Organization.ID == org - where E.App.Key == key - select E).FirstOrDefaultAsync(); - - if (app == null) - return new(Responses.NotRows); - - return new(Responses.Success, app); - } - catch - { - } - - return new(); - } - - - - /// - /// Crea una app en una organización - /// - /// Key de la app - /// ID de la organización - /// Contexto de conexión - public static async Task Create(string appUid, int org, Conexión context) - { - - // Ejecución - try - { - - // Query - var app = await (from A in context.DataBase.Applications - where A.ApplicationUid == appUid - select A).FirstOrDefaultAsync(); - - - if (app == null) - return new(Responses.NotRows); - - - var onOrg = new AppOnOrgModel() - { - State = AppOnOrgStates.Activated, - App = app, - Organization = new() - { - ID = org - } - }; - context.DataBase.Attach(onOrg.Organization); - - await context.DataBase.AddAsync(onOrg); - - context.DataBase.SaveChanges(); - - return new(Responses.Success, onOrg.ID); - } - catch - { - } - - return new(); - } - - - - /// - /// Obtiene la lista de apps que coincidan con un patron y que no estén agregadas a una organización - /// - /// Parámetro de búsqueda - /// ID de la organización - /// Contexto de conexión - public static async Task> Search(string param, int org, Conexión context) - { - - // Ejecución - try - { - - // Query - var apps = await (from A in context.DataBase.Applications - where !context.DataBase.AppOnOrg.Any(aog => aog.AppID == A.ID && aog.OrgID == org) - where A.Name.ToLower().Contains(param.ToLower()) - || A.ApplicationUid.ToLower().Contains(param.ToLower()) - select new ApplicationModel - { - ID = A.ID, - ApplicationUid = A.ApplicationUid, - Badge = A.Badge, - Name = A.Name - }).Take(10).ToListAsync(); - - - return new(Responses.Success, apps); - } - catch - { - } - - return new(); - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Data/Organizations/Members.cs b/LIN.Identity/Data/Organizations/Members.cs deleted file mode 100644 index 8aec9e0..0000000 --- a/LIN.Identity/Data/Organizations/Members.cs +++ /dev/null @@ -1,152 +0,0 @@ -namespace LIN.Identity.Data.Organizations; - - -public class Members -{ - - - #region Abstracciones - - - /// - /// Obtiene la lista de integrantes de una organización - /// - /// ID de la organización - public static async Task> ReadAll(int id) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await ReadAll(id, context); - context.CloseActions(contextKey); - return res; - } - - - - - // - /// Crea una cuenta en una organización - /// - /// Modelo - /// ID de la organización - /// Rol dentro de la organización - public static async Task> Create(AccountModel data, int orgID, OrgRoles rol) - { - - var (context, contextKey) = Conexión.GetOneConnection(); - var res = await Create(data, orgID, rol, context); - context.CloseActions(contextKey); - return res; - } - - - #endregion - - - - - /// - /// Crea una cuenta en una organización - /// - /// Modelo - /// ID de la organización - /// Rol dentro de la organización - /// Contexto de conexión - public static async Task> Create(AccountModel data, int orgID, OrgRoles rol, Conexión context) - { - - data.ID = 0; - - // Ejecución - using (var transaction = context.DataBase.Database.BeginTransaction()) - { - try - { - - // Obtiene la organización - var org = await (from U in context.DataBase.Organizations - where U.ID == orgID - select U).FirstOrDefaultAsync(); - - // No existe la organización - if (org == null) - { - transaction.Rollback(); - return new(Responses.NotRows); - } - - // Modelo de acceso - data.OrganizationAccess = new() - { - Member = data, - Rol = rol, - Organization = org - }; - - var res = await context.DataBase.Accounts.AddAsync(data); - context.DataBase.SaveChanges(); - - transaction.Commit(); - - return new(Responses.Success, data); - } - catch (Exception ex) - { - transaction.Rollback(); - if ((ex.InnerException?.Message.Contains("Violation of UNIQUE KEY constraint") ?? false) || (ex.InnerException?.Message.Contains("duplicate key") ?? false)) - return new(Responses.ExistAccount); - - } - } - - return new(); - } - - - - /// - /// Obtiene la lista de integrantes de una organización. - /// - /// ID de la organización - /// Contexto de conexión - public static async Task> ReadAll(int id, Conexión context) - { - - // Ejecución - try - { - - // Organización - var org = from O in context.DataBase.OrganizationAccess - where O.Organization.ID == id - select new AccountModel - { - Creación = O.Member.Creación, - ID = O.Member.ID, - Nombre = O.Member.Nombre, - Genero = O.Member.Genero, - Usuario = O.Member.Usuario, - OrganizationAccess = new() - { - ID = O.Member.OrganizationAccess == null ? 0 : O.Member.OrganizationAccess.ID, - Rol = O.Member.OrganizationAccess == null ? OrgRoles.Undefine : O.Member.OrganizationAccess.Rol - } - }; - - var orgList = await org.ToListAsync(); - - // Email no existe - if (org == null) - return new(Responses.NotRows); - - return new(Responses.Success, orgList); - } - catch - { - } - - return new(); - } - - -} \ No newline at end of file diff --git a/LIN.Identity/Data/Organizations/Organizations.cs b/LIN.Identity/Data/Organizations/Organizations.cs deleted file mode 100644 index c895447..0000000 --- a/LIN.Identity/Data/Organizations/Organizations.cs +++ /dev/null @@ -1,302 +0,0 @@ -namespace LIN.Identity.Data.Organizations; - - -public class Organizations -{ - - - #region Abstracciones - - - /// - /// Crea una organización - /// - /// Modelo - public static async Task> Create(OrganizationModel data) - { - var (context, contextKey) = Conexión.GetOneConnection(); - var response = await Create(data, context); - context.CloseActions(contextKey); - return response; - } - - - - /// - /// Obtiene una organización - /// - /// ID de la organización - public static async Task> Read(int id) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await Read(id, context); - context.CloseActions(contextKey); - return res; - } - - - - /// - /// Obtiene la lista de aplicaciones permitidas en una organización. - /// - /// ID de la organización - public static async Task> ReadApps(int id) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await ReadApps(id, context); - context.CloseActions(contextKey); - return res; - } - - - - /// - /// Actualiza el estado de la lista blanca de una organización - /// - /// ID de la organización - /// Nuevo estado - public static async Task UpdateState(int id, bool estado) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await UpdateState(id, estado, context); - context.CloseActions(contextKey); - return res; - } - - - - - /// - /// Actualiza el estado de logins una organización - /// - /// ID de la organización - /// Nuevo estado - public static async Task UpdateAccess(int id, bool estado) - { - var (context, contextKey) = Conexión.GetOneConnection(); - - var res = await UpdateAccess(id, estado, context); - context.CloseActions(contextKey); - return res; - } - - - #endregion - - - - /// - /// Crear una organización - /// - /// Modelo - /// Contexto de conexión - public static async Task> Create(OrganizationModel data, Conexión context) - { - - data.ID = 0; - - // Ejecución - using (var transaction = context.DataBase.Database.BeginTransaction()) - { - try - { - - string[] defaultApps = - { - "linauthenticator", "linorgsapp" - }; - - foreach (var app in defaultApps) - { - var appData = await Data.Applications.ReadByAppUid(app, context); - - if (appData.Response == Responses.Success) - data.AppList.Add(new() - { - Organization = data, - State = AppOnOrgStates.Activated, - App = appData.Model - }); - } - - var res = await context.DataBase.Organizations.AddAsync(data); - - context.DataBase.SaveChanges(); - - transaction.Commit(); - - return new(Responses.Success, data); - } - catch (Exception ex) - { - transaction.Rollback(); - if ((ex.InnerException?.Message.Contains("Violation of UNIQUE KEY constraint") ?? false) || (ex.InnerException?.Message.Contains("duplicate key") ?? false)) - return new(Responses.Undefined); - - } - } - return new(); - } - - - - - /// - /// Obtiene una organización. - /// - /// ID de la organización - /// Contexto de conexión - public static async Task> Read(int id, Conexión context) - { - - // Ejecución - try - { - - // Query - var org = await (from E in context.DataBase.Organizations - where E.ID == id - select E).FirstOrDefaultAsync(); - - // Email no existe - if (org == null) - { - return new(Responses.NotRows); - } - - return new(Responses.Success, org); - } - catch - { - } - - return new(); - } - - - - - /// - /// Obtiene la lista de aplicaciones permitidas en una organización. - /// - /// ID de la organización - /// Contexto de conexión - public static async Task> ReadApps(int id, Conexión context) - { - - // Ejecución - try - { - - // Organización - var apps = from ORG in context.DataBase.AppOnOrg - where ORG.Organization.ID == id - select new ApplicationModel - { - ID = ORG.App.ID, - Name = ORG.App.Name, - Badge = ORG.App.Badge, - ApplicationUid = ORG.App.ApplicationUid - }; - - - var lista = await apps.ToListAsync(); - - // Email no existe - if (lista == null) - return new(Responses.NotRows); - - return new(Responses.Success, lista); - } - catch - { - } - - return new(); - } - - - - - /// - /// Actualiza el estado de la lista blanca de una organización - /// - /// ID de la organización - /// Nuevo estado - /// Contexto de conexión - public static async Task UpdateState(int id, bool estado, Conexión context) - { - - // Ejecución - try - { - - // Query - var org = await (from E in context.DataBase.Organizations - where E.ID == id - select E).FirstOrDefaultAsync(); - - // Email no existe - if (org == null) - { - return new(Responses.NotRows); - } - - org.HaveWhiteList = estado; - context.DataBase.SaveChanges(); - - return new(Responses.Success); - } - catch - { - } - - return new(); - } - - - - - - /// - /// Actualiza el estado de accesos de una organización - /// - /// ID de la organización - /// Nuevo estado - /// Contexto de conexión - public static async Task UpdateAccess(int id, bool estado, Conexión context) - { - - // Ejecución - try - { - - // Query - var org = await (from E in context.DataBase.Organizations - where E.ID == id - select E).FirstOrDefaultAsync(); - - // Email no existe - if (org == null) - { - return new(Responses.NotRows); - } - - org.LoginAccess = estado; - context.DataBase.SaveChanges(); - - return new(Responses.Success); - } - catch - { - } - - return new(); - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/FilterModels/Account.cs b/LIN.Identity/FilterModels/Account.cs deleted file mode 100644 index df16daf..0000000 --- a/LIN.Identity/FilterModels/Account.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace LIN.Identity.FilterModels; - - -public class Account -{ - - public int ContextUser { get; set; } - public int ContextOrg { get; set; } - public bool IsAdmin { get; set; } - public bool SensibleInfo { get; set; } - public IncludeOrg IncludeOrg { get; set; } = IncludeOrg.None; - public FindOn FindOn { get; set; } = FindOn.AllAccount; - public IncludeOrgLevel OrgLevel { get; set; } = IncludeOrgLevel.Basic; - -} - -public enum IncludeOrg -{ - - /// - /// No incluir la organización - /// - None, - - /// - /// Incluir la organización - /// - Include, - - /// - /// Incluir la organización según el contexto - /// - IncludeIf - -} - -public enum IncludeOrgLevel -{ - - /// - /// Incluir el rol - /// - Basic, - - /// - /// Incluir la organización y su nombre - /// - Advance - -} - -public enum FindOn -{ - - /// - /// En todas las cuentas - /// - AllAccount, - - /// - /// En las cuentas estables - /// - StableAccounts - -} \ No newline at end of file diff --git a/LIN.Identity/Hubs/AccountHub.cs b/LIN.Identity/Hubs/AccountHub.cs deleted file mode 100644 index b293497..0000000 --- a/LIN.Identity/Hubs/AccountHub.cs +++ /dev/null @@ -1,209 +0,0 @@ -using LIN.Access.Logger; - -namespace LIN.Identity.Hubs; - - -public class AccountHub : Hub -{ - - - /// - /// Lista de cuentas y dispositivos - /// - public readonly static Dictionary> Cuentas = new(); - - - - /// - /// Unir un dispositivo a la lista - /// - /// Modelo del dispositivo - public async Task Join(DeviceModel modelo) - { - - // Validación del token - var (isValid, _, id, _, _) = Jwt.Validate(modelo.Token); - - if (!isValid) - return; - - // Estados - modelo.ID = Context.ConnectionId; - modelo.Estado = DeviceState.Actived; - modelo.Cuenta = id; - - // Agrega el modelo - if (!Cuentas.ContainsKey(modelo.Cuenta)) - Cuentas.Add(modelo.Cuenta, new() - { - modelo - }); - - else - { - - var models = Cuentas[modelo.Cuenta]; - - var devices = models.Where(x => x.DeviceKey == modelo.DeviceKey).ToList(); - - foreach (var device in devices) - device.Estado = DeviceState.Disconnected; - - Cuentas[modelo.Cuenta].Add(modelo); - - } - - // Grupo de la cuenta - await Groups.AddToGroupAsync(Context.ConnectionId, modelo.Cuenta.ToString()); - - // Envía el nuevo dispositivo - await RegisterDevice(modelo); - - // Testea la conexión - await TestDevices(modelo.Cuenta); - - } - - - - /// - /// Evento Salir - /// - /// ID de la cuenta - public async Task Leave(int cuenta) - { - Cuentas[cuenta] = Cuentas[cuenta].Where(T => T.ID != Context.ConnectionId).ToList(); - await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"{cuenta}"); - await Clients.Group(cuenta.ToString()).SendAsync("leaveevent", Context.ConnectionId); - } - - - - /// - /// Obtener la lista de dispositivos de una cuenta - /// - /// ID de la cuenta - public async Task GetDevicesList(int cuenta) - { - var devices = Cuentas[cuenta] ?? new(); - await Clients.Caller.SendAsync("devicesList", devices.Where(model => model.Estado == DeviceState.Actived), Context.ConnectionId); - } - - - - /// - /// Envía a los clientes cuando se agrega un nuevo dispositivo - /// - /// Modelo del nuevo dispositivo - public async Task RegisterDevice(DeviceModel modelo) - { - await Clients.Group(modelo.Cuenta.ToString()).SendAsync("newdevice", modelo); - } - - - - /// - /// Testea la conexión de los dispositivos - /// - /// ID de la cuenta - public async Task TestDevices(int account) - { - // Cambia el estado de los dispositivos - Cuentas[account].ForEach(model => model.Estado = DeviceState.WaitingResponse); - await Clients.Group(account.ToString()).SendAsync("ontest"); - } - - - - - public async void ReceiveTestStatus(int account, int battery, bool bateryConected) - { - - try - { - // Obtiene la cuenta - var cuenta = Cuentas[account]; - - // Obtiene el dispositivo - var device = cuenta.Where(T => T.ID == Context.ConnectionId).ToList().FirstOrDefault(); - - if (device == null) - return; - - // Cambia los estados - device.Estado = DeviceState.Actived; - device.BateryLevel = battery; - device.BateryConected = bateryConected; - - // Envía el cambio de dispositivos - await RegisterDevice(device); - } - catch (Exception ex) - { - _ = Logger.Log(ex, 2); - } - - - - } - - - - public async Task SendDeviceCommand(string receiver, string command) - { - await Clients.Client(receiver).SendAsync("devicecommand", command); - } - - - - public async Task SendAccountCommand(int receiver, string command) - { - await Clients.GroupExcept(receiver.ToString(), new[] - { - Context.ConnectionId - }).SendAsync("accountcommand", command); - } - - - - - - - public void AddInvitation(List users) - { - try - { - - foreach (var user in users) - { - _ = Clients.Group(user.ToString()).SendAsync("invitate", "retrievedata"); - } - - } - catch (Exception ex) - { - _ = Logger.Log(ex, 2); - } - - } - - - public override Task OnDisconnectedAsync(Exception? exception) - { - - var cuenta = Cuentas.Values.Where(T => T.Where(T => T.ID == Context.ConnectionId).Any()).FirstOrDefault(); - - if (cuenta == null) - return base.OnDisconnectedAsync(exception); - - var xx = cuenta.FirstOrDefault()?.Cuenta ?? 0; - _ = Leave(xx); - return base.OnDisconnectedAsync(exception); - - } - - - - - -} \ No newline at end of file diff --git a/LIN.Identity/Hubs/PasskeyHub.cs b/LIN.Identity/Hubs/PasskeyHub.cs deleted file mode 100644 index 8fb0e38..0000000 --- a/LIN.Identity/Hubs/PasskeyHub.cs +++ /dev/null @@ -1,306 +0,0 @@ -namespace LIN.Identity.Hubs; - - -public class PassKeyHub : Hub -{ - - - /// - /// Lista de intentos Passkey - /// - public readonly static Dictionary> Attempts = new(); - - - - /// - /// Nuevo intento passkey - /// - /// Intento passkey - public async Task JoinIntent(PassKeyModel attempt) - { - - // Aplicación - var application = await Data.Applications.Read(attempt.Application.Key); - - // Si la app no existe o no esta activa - if (application.Response != Responses.Success) - return; - - // Preparar el modelo - attempt.Application ??= new(); - attempt.Application.Name = application.Model.Name; - attempt.Application.Badge = application.Model.Badge; - attempt.Application.Key = application.Model.Key; - attempt.Application.ID = application.Model.ID; - - // Vencimiento - var expiración = DateTime.Now.AddMinutes(2); - - // Caducidad el modelo - attempt.HubKey = Context.ConnectionId; - attempt.Status = PassKeyStatus.Undefined; - attempt.Hora = DateTime.Now; - attempt.Expiración = expiración; - - // Agrega el modelo - if (!Attempts.ContainsKey(attempt.User.ToLower())) - Attempts.Add(attempt.User.ToLower(), new() - { - attempt - }); - else - Attempts[attempt.User.ToLower()].Add(attempt); - - // Yo - await Groups.AddToGroupAsync(Context.ConnectionId, $"dbo.{Context.ConnectionId}"); - - await SendRequest(attempt); - - } - - - - - - - - - - - - - - - - - - - - - - - - - public override Task OnDisconnectedAsync(Exception? exception) - { - - var e = Attempts.Values.Where(T => T.Where(T => T.HubKey == Context.ConnectionId).Any()).FirstOrDefault() ?? new(); - - - _ = e.Where(T => - { - if (T.HubKey == Context.ConnectionId && T.Status == PassKeyStatus.Undefined) - T.Status = PassKeyStatus.Failed; - - return false; - }); - - - return base.OnDisconnectedAsync(exception); - } - - - /// - /// Un dispositivo envia el PassKey intent - /// - public async Task JoinAdmin(string usuario) - { - - // Grupo de la cuenta - await Groups.AddToGroupAsync(Context.ConnectionId, usuario.ToLower()); - - } - - - - - - - - //=========== Dispositivos ===========// - - - /// - /// Envía la solicitud a los admins - /// - public async Task SendRequest(PassKeyModel modelo) - { - - var pass = new PassKeyModel() - { - Expiración = modelo.Expiración, - Hora = modelo.Hora, - Status = modelo.Status, - User = modelo.User, - HubKey = modelo.HubKey, - Application = new() - { - Name = modelo.Application.Name, - Badge = modelo.Application.Badge - } - }; - - await Clients.Group(modelo.User.ToLower()).SendAsync("newintent", pass); - } - - - - - /// - /// Recibe una respuesta de passkey - /// - public async Task ReceiveRequest(PassKeyModel modelo) - { - try - { - - // Validación del token recibido - var (isValid, userUnique, userID, orgID, _) = Jwt.Validate(modelo.Token); - - // No es valido el token - if (!isValid || modelo.Status != PassKeyStatus.Success) - { - // Modelo de falla - PassKeyModel badPass = new() - { - Status = modelo.Status, - User = modelo.User - }; - - // comunica la respuesta - await Clients.Groups($"dbo.{modelo.HubKey}").SendAsync("#response", badPass); - return; - } - - // Obtiene el attempt - List attempts = Attempts[modelo.User.ToLower()].Where(A => A.HubKey == modelo.HubKey).ToList(); - - // Elemento - var attempt = attempts.Where(A => A.HubKey == modelo.HubKey).FirstOrDefault(); - - // Validación del intento - if (attempt == null) - return; - - // Eliminar el attempt de la lista - attempts.Remove(attempt); - - // Cambiar el estado del intento - attempt.Status = modelo.Status; - - // Si el tiempo de expiración ya paso - if (DateTime.Now > modelo.Expiración) - { - attempt.Status = PassKeyStatus.Expired; - attempt.Token = string.Empty; - } - - // Validación de la organización - if (orgID > 0) - { - // Obtiene la organización - var organization = await Data.Organizations.Organizations.Read(orgID); - - // Si tiene lista blanca - if (organization.Model.HaveWhiteList) - { - // Validación de la app - var applicationOnOrg = await Data.Organizations.Applications.AppOnOrg(attempt.Application.Key, orgID); - - // Si la app no existe o no esta activa - if (applicationOnOrg.Response != Responses.Success) - { - // Modelo de falla - PassKeyModel badPass = new() - { - Status = PassKeyStatus.BlockedByOrg, - User = modelo.User - }; - - // comunica la respuesta - await Clients.Groups($"dbo.{modelo.HubKey}").SendAsync("#response", badPass); - return; - - } - } - - - } - - // Aplicación - var app = await Data.Applications.Read(attempt.Application.Key); - - // Si la app no existe - if (app.Response != Responses.Success) - { - // Modelo de falla - PassKeyModel badPass = new() - { - Status = PassKeyStatus.Failed, - User = modelo.User - }; - - // comunica la respuesta - await Clients.Groups($"dbo.{modelo.HubKey}").SendAsync("#response", badPass); - return; - } - - // Guarda el acceso. - LoginLogModel loginLog = new() - { - AccountID = userID, - Application = new() - { - ID = app.Model.ID - }, - Date = DateTime.Now, - Platform = Platforms.Undefined, - Type = LoginTypes.Passkey, - ID = 0 - }; - - _ = Data.Logins.Create(loginLog); - - - // Nuevo token - var newToken = Jwt.Generate(new() - { - ID = userID, - Usuario = userUnique, - OrganizationAccess = new() - { - Organization = new() - { - ID = orgID - } - } - }, app.Model.ID); - - // nuevo pass - var pass = new PassKeyModel() - { - Expiración = modelo.Expiración, - Status = modelo.Status, - User = modelo.User, - Token = newToken, - Hora = modelo.Hora, - Application = new(), - HubKey = "", - Key = "" - }; - - // Respuesta al cliente - await Clients.Groups($"dbo.{modelo.HubKey}").SendAsync("#response", pass); - - } - catch - { - } - - } - - - - - -} \ No newline at end of file diff --git a/LIN.Identity/LIN.Identity.csproj b/LIN.Identity/LIN.Identity.csproj deleted file mode 100644 index 74bdea4..0000000 --- a/LIN.Identity/LIN.Identity.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - diff --git a/LIN.Identity/Program.cs b/LIN.Identity/Program.cs deleted file mode 100644 index 72f9b92..0000000 --- a/LIN.Identity/Program.cs +++ /dev/null @@ -1,78 +0,0 @@ -using LIN.Identity.Data; -{ - - LIN.Access.Logger.Logger.AppName = "LIN.IDENTITY"; - - var builder = WebApplication.CreateBuilder(args); - - // Add services to the container. - builder.Services.AddSignalR(); - - - builder.Services.AddCors(options => - { - options.AddPolicy("AllowAnyOrigin", - builder => - { - builder.AllowAnyOrigin() - .AllowAnyHeader() - .AllowAnyMethod(); - }); - }); - - - - - var sqlConnection = builder.Configuration["ConnectionStrings:somee"] ?? string.Empty; - - // Servicio de BD - builder.Services.AddDbContext(options => - { - options.UseSqlServer(sqlConnection); - }); - - - - builder.Services.AddControllers(); - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); - - var app = builder.Build(); - - - try - { - // Si la base de datos no existe - using var scope = app.Services.CreateScope(); - var dataContext = scope.ServiceProvider.GetRequiredService(); - var res = dataContext.Database.EnsureCreated(); - } - catch (Exception ex) - { - _ = LIN.Access.Logger.Logger.Log(ex, 3); - } - - - app.UseCors("AllowAnyOrigin"); - - app.MapHub("/realTime/service"); - app.MapHub("/realTime/auth/passkey"); - - app.UseSwagger(); - app.UseSwaggerUI(); - - Conexión.SetStringConnection(sqlConnection); - - app.UseStaticFiles(); - app.UseHttpsRedirection(); - Jwt.Open(); - EmailWorker.StarService(); - - - app.UseAuthorization(); - - app.MapControllers(); - - app.Run(); -} \ No newline at end of file diff --git a/LIN.Identity/Properties/launchSettings.json b/LIN.Identity/Properties/launchSettings.json deleted file mode 100644 index 54ba7df..0000000 --- a/LIN.Identity/Properties/launchSettings.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:6638", - "sslPort": 44316 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:5225", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:7265;http://localhost:5225", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/LIN.Identity/Queries/Accounts.cs b/LIN.Identity/Queries/Accounts.cs deleted file mode 100644 index 64b81e7..0000000 --- a/LIN.Identity/Queries/Accounts.cs +++ /dev/null @@ -1,208 +0,0 @@ -namespace LIN.Identity.Queries; - - -public class Accounts -{ - - - /// - /// Query general para todas las cuentas - /// - /// Contexto DB - public static IQueryable GetAccounts(Conexión context) - { - // Query general - IQueryable accounts = from account in context.DataBase.Accounts - select account; - - // Retorno - return accounts; - - } - - - - /// - /// Solo tiene en cuenta cuentas validas - /// - /// Contexto DB - public static IQueryable GetValidAccounts(Conexión context) - { - // Query general - IQueryable accounts = from account in GetAccounts(context) - where account.Estado == AccountStatus.Normal - select account; - - // Retorno - return accounts; - - } - - - - - - - - - - public static IQueryable GetAccounts(int id, FilterModels.Account filters, Conexión context) - { - - // Query general - IQueryable accounts; - - if (filters.FindOn == FilterModels.FindOn.StableAccounts) - accounts = from account in GetValidAccounts(context) - where account.ID == id - select account; - else - accounts = from account in GetAccounts(context) - where account.ID == id - select account; - - // Armar el modelo - accounts = BuildModel(accounts, filters); - - // Retorno - return accounts; - - } - - - public static IQueryable GetAccounts(string user, FilterModels.Account filters, Conexión context) - { - - // Query general - IQueryable accounts; - - if (filters.FindOn == FilterModels.FindOn.StableAccounts) - accounts = from account in GetValidAccounts(context) - where account.Usuario == user - select account; - else - accounts = from account in GetAccounts(context) - where account.Usuario == user - select account; - - // Armar el modelo - accounts = BuildModel(accounts, filters); - - // Retorno - return accounts; - - } - - - public static IQueryable GetAccounts(IEnumerable ids, FilterModels.Account filters, Conexión context) - { - - // Query general - IQueryable accounts = from account in GetValidAccounts(context) - where ids.Contains(account.ID) - select account; - - // Armar el modelo - accounts = BuildModel(accounts, filters); - - // Retorno - return accounts; - - } - - - public static IQueryable Search(string pattern, FilterModels.Account filters, Conexión context) - { - - // Query general - IQueryable accounts = from account in GetValidAccounts(context) - where account.Usuario.Contains(pattern) - select account; - - // Armar el modelo - accounts = BuildModel(accounts, filters); - - // Retorno - return accounts; - - } - - - - - - - - - - - - - /// - /// Construir la consulta - /// - /// Query base - /// Filtros - private static IQueryable BuildModel(IQueryable query, FilterModels.Account filters) - { - - byte[] profile = - { - }; - try - { - profile = File.ReadAllBytes("wwwroot/user.png"); - } - catch { } - - return from account in query - select new AccountModel - { - ID = account.ID, - Nombre = account.Visibilidad == AccountVisibility.Visible || account.OrganizationAccess != null && account.OrganizationAccess.Organization.ID == filters.ContextOrg || account.ID == filters.ContextUser || filters.IsAdmin - ? account.Nombre - : "Usuario privado", - Rol = account.Rol, - Insignia = account.Insignia, - Estado = account.Estado, - Usuario = account.Usuario, - Contraseña = filters.SensibleInfo ? account.Contraseña : "", - Visibilidad = account.Visibilidad, - Birthday = account.Visibilidad == AccountVisibility.Visible || account.OrganizationAccess != null && account.OrganizationAccess.Organization.ID == filters.ContextOrg || account.ID == filters.ContextUser || filters.IsAdmin - ? account.Birthday - : default, - Genero = account.Visibilidad == AccountVisibility.Visible || account.OrganizationAccess != null && account.OrganizationAccess.Organization.ID == filters.ContextOrg || account.ID == filters.ContextUser || filters.IsAdmin - ? account.Genero - : Genders.Undefined, - Creación = account.Visibilidad == AccountVisibility.Visible || account.OrganizationAccess != null && account.OrganizationAccess.Organization.ID == filters.ContextOrg || account.ID == filters.ContextUser || filters.IsAdmin - ? account.Creación - : default, - Perfil = account.Visibilidad == AccountVisibility.Visible || account.OrganizationAccess != null && account.OrganizationAccess.Organization.ID == filters.ContextOrg || account.ID == filters.ContextUser || filters.IsAdmin - ? account.Perfil - : profile, - OrganizationAccess = account.OrganizationAccess != null && filters.IncludeOrg != FilterModels.IncludeOrg.None && (account.Visibilidad == AccountVisibility.Visible && filters.IncludeOrg == FilterModels.IncludeOrg.Include - || account.OrganizationAccess.Organization.ID == filters.ContextOrg || account.ID == filters.ContextUser - || filters.IsAdmin) - ? new OrganizationAccessModel() - { - ID = account.ID, - Rol = account.OrganizationAccess.Rol, - Organization = filters.OrgLevel == FilterModels.IncludeOrgLevel.Advance ? new() - { - ID = account.OrganizationAccess.Organization.ID, - Domain = !account.OrganizationAccess.Organization.IsPublic && !filters.IsAdmin && filters.IncludeOrg == FilterModels.IncludeOrg.IncludeIf && filters.ContextOrg != account.OrganizationAccess.Organization.ID ? "" - : account.OrganizationAccess.Organization.Domain, - Name = !account.OrganizationAccess.Organization.IsPublic && !filters.IsAdmin && filters.IncludeOrg == FilterModels.IncludeOrg.IncludeIf && filters.ContextOrg != account.OrganizationAccess.Organization.ID - ? "Organización privada" : account.OrganizationAccess.Organization.Name - } : new() - } - : null - }; - } - - - - - - -} \ No newline at end of file diff --git a/LIN.Identity/Services/Configuration.cs b/LIN.Identity/Services/Configuration.cs deleted file mode 100644 index 9ab901d..0000000 --- a/LIN.Identity/Services/Configuration.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace LIN.Identity.Services; - - -public class Configuration -{ - - - private static IConfiguration? Config; - - private static bool _isStart = false; - - - public static string GetConfiguration(string route) - { - - if (_isStart && Config != null) - return Config[route] ?? string.Empty; - - var configBuilder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", false, true); - - Config = configBuilder.Build(); - _isStart = true; - - return Config[route] ?? string.Empty; - - } - -} \ No newline at end of file diff --git a/LIN.Identity/Services/EmailWorker.cs b/LIN.Identity/Services/EmailWorker.cs deleted file mode 100644 index fcbe704..0000000 --- a/LIN.Identity/Services/EmailWorker.cs +++ /dev/null @@ -1,132 +0,0 @@ -namespace LIN.Identity.Services; - - -public class EmailWorker -{ - - - /// - /// Email de salida - /// - private static string Password { get; set; } = string.Empty; - - - /// - /// Inicia el servicio - /// - public static void StarService() - { - Password = Configuration.GetConfiguration("resend:key"); - } - - - - /// - /// Enviar un correo - /// - /// Destinatario - /// url - /// Info del mail - public static async Task SendVerification(string to, string url, string mail) - { - - // Obtiene la plantilla - var body = File.ReadAllText("wwwroot/Plantillas/Plantilla.html"); - - // Remplaza - body = body.Replace("@@Titulo", "Verificación de correo electrónico"); - body = body.Replace("@@Subtitulo", $"{mail}"); - body = body.Replace("@@Url", url); - body = body.Replace("@@Mensaje", "Hemos recibido tu solicitud para agregar una dirección de correo electrónico adicional a tu cuenta. Para completar este proceso, da click en el siguiente botón"); - body = body.Replace("@@ButtonMessage", "Verificar"); - - - // Envía el email - return await SendMail(to, "Verifica el email", body); - - } - - - - /// - /// Enviar un correo - /// - /// Destinatario - /// Nombre del usuario - /// URL - public static async Task SendPassword(string to, string nombre, string url) - { - - // Obtiene la plantilla - var body = File.ReadAllText("wwwroot/Plantillas/Plantilla.html"); - - // Remplaza - body = body.Replace("@@Titulo", "Reestablecer contraseña"); - body = body.Replace("@@Subtitulo", $"Hola, {nombre}"); - body = body.Replace("@@Url", url); - body = body.Replace("@@Mensaje", "Recibimos tu solicitud para restablecer la contraseña de tu cuenta LIN. Para completar este proceso, simplemente haz clic en el siguiente botón"); - body = body.Replace("@@ButtonMessage", "Cambiar contraseña"); - - // Envía el email - return await SendMail(to, "Cambiar contraseña", body); - - } - - - - /// - /// Enviar un correo - /// - /// Destinatario - /// Asunto - /// Cuerpo del correo - public static async Task SendMail(string to, string asunto, string body) - { - try - { - - using (var client = new HttpClient()) - { - var url = "https://api.resend.com/emails"; - var accessToken = Password; // Reemplaza con tu token de acceso - - var requestData = new - { - from = "onboarding@resend.dev", - to = new[] - { - to - }, - subject = asunto - }; - - - - var json = Newtonsoft.Json.JsonConvert.SerializeObject(requestData); - - using var content = new StringContent(json, Encoding.UTF8, "application/json"); - client.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken); - - var response = await client.PostAsync(url, content); - - if (response.IsSuccessStatusCode) - { - return true; - } - else - { - _ = Logger.Log("Error al enviar un correo: ",response.StatusCode.ToString(), 3); - } - } - return true; - } - catch (Exception ex) - { - _ = Logger.Log(ex, 2); - } - return false; - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Services/Iam/Applications.cs b/LIN.Identity/Services/Iam/Applications.cs deleted file mode 100644 index e07564c..0000000 --- a/LIN.Identity/Services/Iam/Applications.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace LIN.Identity.Services.Iam; - - -public static class Applications -{ - - - /// - /// Validar acceso a un recurso de aplicación. - /// - /// ID de la cuenta - /// ID de la aplicación - public static async Task> ValidateAccess(int account, int app) - { - - // Obtiene el recurso. - var resource = await Data.Applications.Read(app); - - // Si no existe el recurso. - if (resource == null) - return new() - { - Message = "No se encontró el recurso.", - Response = Responses.NotRows, - Model = IamLevels.NotAccess - }; - - // Si es admin del recurso / creador - if (resource.Model.AccountID == account) - return new() - { - Response = Responses.Success, - Model = IamLevels.Privileged - }; - - // Si es un recurso publico. - if (resource.Model.AllowAnyAccount) - return new() - { - Response = Responses.Success, - Model = IamLevels.Visualizer - }; - - // Validar acceso al recurso privado. - var isAllowed = await Data.Applications.IsAllow(app, account); - - // No es permitido. - if (!isAllowed.Model) - return new() - { - Response = Responses.Success, - Model = IamLevels.NotAccess - }; - - // Acceso visualizador. - return new() - { - Response = Responses.Success, - Model = IamLevels.Visualizer - }; - - - } - - -} \ No newline at end of file diff --git a/LIN.Identity/Services/Image.cs b/LIN.Identity/Services/Image.cs deleted file mode 100644 index b051ee2..0000000 --- a/LIN.Identity/Services/Image.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Drawing; -using System.Drawing.Imaging; - -namespace LIN.Identity.Services; - - -public class Image -{ - - - /// - /// Comprime una imagen - /// - /// Original image - /// Ancho - /// Alto - /// Maximo - public static byte[] Zip(byte[] originalImage, int width = 50, int height = 50, int max = 1900) - { - try - { - - // Si la imagen ya esta comprimida o pesa muy poco - if (originalImage.Length <= max) - return originalImage; - - // Cargar la imagen a memoria - MemoryStream memoryStream = new(originalImage); - - // Crear la imagen - var image = System.Drawing.Image.FromStream(memoryStream); - - // Imagen redimensionada - Bitmap nuevaImagen = new(width, height); - - // Crea un objeto Graphics para dibujar la imagen original en el Bitmap redimensionado - using (var graphics = Graphics.FromImage(nuevaImagen)) - { - // Dibuja la imagen original en el nuevo Bitmap con las dimensiones deseadas - graphics.DrawImage(image, 0, 0, 50, 50); - } - - - byte[] imagenBytes; - using (MemoryStream stream = new()) - { - nuevaImagen.Save(stream, ImageFormat.Png); - imagenBytes = stream.ToArray(); - } - - nuevaImagen.Dispose(); - image.Dispose(); - - return imagenBytes; - } - catch - { - } - return Array.Empty(); - } - - -} \ No newline at end of file diff --git a/LIN.Identity/Services/Login/LoginNormal.cs b/LIN.Identity/Services/Login/LoginNormal.cs deleted file mode 100644 index aefd96c..0000000 --- a/LIN.Identity/Services/Login/LoginNormal.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace LIN.Identity.Services.Login; - - -public class LoginNormal : LoginService -{ - - - /// - /// Nuevo login - /// - /// Datos de la cuenta - /// Llave - /// Contraseña - /// Tipo de inicio - public LoginNormal(AccountModel? account, string? application, string password, LoginTypes loginType = LoginTypes.Credentials) : base(account, application, password, loginType) - { - } - - - - /// - /// Iniciar sesión - /// - public override async Task Login() - { - - // Validar credenciales y estado - var validateAccount = Validate(); - - // Retorna el error - if (validateAccount.Response != Responses.Success) - return validateAccount; - - - // Valida la aplicación - var validateApp = await ValidateApp(); - - // Retorna el error - if (validateApp.Response != Responses.Success) - return validateApp; - - - // Genera el login - GenerateLogin(); - - return new(Responses.Success); - - } - - -} \ No newline at end of file diff --git a/LIN.Identity/Services/Login/LoginOnOrg.cs b/LIN.Identity/Services/Login/LoginOnOrg.cs deleted file mode 100644 index f0f7d33..0000000 --- a/LIN.Identity/Services/Login/LoginOnOrg.cs +++ /dev/null @@ -1,176 +0,0 @@ -namespace LIN.Identity.Services.Login; - - -public class LoginOnOrg : LoginService -{ - - - /// - /// Acceso a la organización - /// - private OrganizationAccessModel OrganizationAccess { get; set; } = new(); - - - - - /// - /// Nuevo login - /// - /// Datos de la cuenta - /// Llave - /// Contraseña - /// Tipo de inicio - public LoginOnOrg(AccountModel? account, string? application, string password, LoginTypes loginType = LoginTypes.Credentials) : base(account, application, password, loginType) - { - } - - - - /// - /// Valida parámetros necesarios para iniciar sesión en una organización - /// - private bool ValidateParams() - { - return Account.OrganizationAccess != null; - } - - - - /// - /// Valida parámetros necesarios para iniciar sesión en una organización - /// - private async Task LoadOrganization() - { - - var z = Account.OrganizationAccess?.Organization.ID; - - var orgResponse = await Data.Organizations.Organizations.Read(z ?? 0); - - - if (orgResponse.Response != Responses.Success) - { - return false; - } - - OrganizationAccess.Rol = Account.OrganizationAccess!.Rol; - OrganizationAccess.ID = Account.OrganizationAccess.ID; - OrganizationAccess.Organization = orgResponse.Model; - - return true; - - } - - - - /// - /// Valida las políticas de la organización - /// - private async Task ValidatePolicies() - { - - // Si el inicio de sesión fue desactivado por la organización - if (!OrganizationAccess!.Organization.LoginAccess && !OrganizationAccess.Rol.IsAdmin()) - return new() - { - Message = "Tu organización a deshabilitado el inicio de sesión temporalmente.", - Response = Responses.LoginBlockedByOrg - }; - - - // Si la organización tiene lista blanca - if (OrganizationAccess.Organization.HaveWhiteList) - { - var whiteList = await ValidateWhiteList(); - if (!whiteList) - return new() - { - Message = "Tu organización no permite iniciar sesión en esta aplicación.", - Response = Responses.UnauthorizedByOrg - }; - } - - return new(Responses.Success); - - } - - - - /// - /// Valida la app en la lista blanca - /// - private async Task ValidateWhiteList() - { - - // Busca la app en la organización - var appOnOrg = await Data.Organizations.Applications.AppOnOrg(ApplicationKey, OrganizationAccess!.Organization.ID); - - return appOnOrg.Response == Responses.Success; - - } - - - - /// - /// Iniciar sesión - /// - public override async Task Login() - { - - // Valida la aplicación - var validateParams = ValidateParams(); - - // Retorna el error - if (!validateParams) - return new() - { - Message = "Este usuario no pertenece a una organización.", - Response = Responses.Undefined - }; - - - // Validar la organización - var validateAccess = await LoadOrganization(); - - // Retorna el error - if (!validateAccess) - return new() - { - Message = "Hubo un error al validar la organización.", - Response = Responses.Undefined - }; - - - - - // Validar credenciales y estado - var validateAccount = Validate(); - - // Retorna el error - if (validateAccount.Response != Responses.Success) - return validateAccount; - - - // Valida la aplicación - var validateApp = await ValidateApp(); - - // Retorna el error - if (validateApp.Response != Responses.Success) - return validateApp; - - // Validar las políticas - var validatePolicies = await ValidatePolicies(); - - // Retorna el error - if (validatePolicies.Response != Responses.Success) - return validatePolicies; - - // Genera el login - GenerateLogin(); - - return new(Responses.Success); - - } - - - -} \ No newline at end of file diff --git a/LIN.Identity/Services/Login/LoginService.cs b/LIN.Identity/Services/Login/LoginService.cs deleted file mode 100644 index e49ecc9..0000000 --- a/LIN.Identity/Services/Login/LoginService.cs +++ /dev/null @@ -1,162 +0,0 @@ -namespace LIN.Identity.Services.Login; - - -public abstract class LoginService -{ - - - /// - /// Llave de la aplicación - /// - private protected string ApplicationKey { get; set; } - - - - /// - /// Tipo del login - /// - private protected LoginTypes LoginType { get; set; } - - - - /// - /// Contraseña - /// - private protected string Password { get; set; } - - - - /// - /// Modelo de la aplicación obtenida - /// - private protected ApplicationModel Application { get; set; } - - - - /// - /// Datos de la cuenta - /// - private protected AccountModel Account { get; set; } - - - - /// - /// Nuevo login - /// - /// Datos de la cuenta - /// Llave - /// Contraseña - /// Tipo de inicio - protected LoginService(AccountModel? account, string? application, string? password, LoginTypes loginType = LoginTypes.Credentials) - { - ApplicationKey = application ?? string.Empty; - Account = account ?? new(); - Application = new(); - Password = password ?? string.Empty; - LoginType = loginType; - } - - - - - /// - /// Valida los datos obtenidos de la cuenta - /// - public ResponseBase Validate() - { - - // Si la cuenta no esta activa - if (Account.Estado != AccountStatus.Normal) - return new() - { - Response = Responses.NotExistAccount, - Message = "Esta cuenta fue eliminada o desactivada." - }; - - // Valida la contraseña - if (Account.Contraseña != EncryptClass.Encrypt(Password)) - return new() - { - Response = Responses.InvalidPassword, - Message = "La contraseña es incorrecta." - }; - - // Correcto - return new(Responses.Success); - - } - - - - - /// - /// Valida los datos de la aplicación - /// - public async Task ValidateApp() - { - // Obtiene la App. - var app = await Data.Applications.Read(ApplicationKey); - - // Verifica si la app existe. - if (app.Response != Responses.Success) - return new ReadOneResponse - { - Message = "La aplicación no esta autorizada para iniciar sesión en LIN Identity", - Response = Responses.Unauthorized - }; - - // Si es una app privada. - if (!app.Model.AllowAnyAccount) - { - var allow = await Data.Applications.IsAllow(app.Model.ID, Account.ID, Conexión.GetOneConnection().context); - - if (allow.Response != Responses.Success) - return new ReadOneResponse - { - Message = $"No tienes permiso para acceder a la aplicación '{app.Model.Name}'", - Response = Responses.Unauthorized - }; - } - - - // Establece la aplicación - Application = app.Model; - - // Correcto - return new(Responses.Success); - - } - - - - /// - /// Genera el login - /// - public async void GenerateLogin() - { - - - var app = await Data.Applications.Read(ApplicationKey); - - // Crea registro del login - _ = Data.Logins.Create(new() - { - Date = DateTime.Now, - AccountID = Account.ID, - Type = LoginType, - Application = new() - { - ID = app.Model.ID - } - }); - } - - - - /// - /// Iniciar sesión - /// - public abstract Task Login(); - - -} \ No newline at end of file diff --git a/LIN.Identity/Services/OrgRoleExt.cs b/LIN.Identity/Services/OrgRoleExt.cs deleted file mode 100644 index 63f42bd..0000000 --- a/LIN.Identity/Services/OrgRoleExt.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace LIN.Identity.Services; - - -public static class OrgRoleExt -{ - - - - public static bool IsGretter(this OrgRoles me, OrgRoles other) - { - - switch (me) - { - - case OrgRoles.SuperManager: - { - return false; - } - - case OrgRoles.Manager: - { - return other == OrgRoles.SuperManager; - } - - case OrgRoles.Regular: - { - return other == OrgRoles.SuperManager || other == OrgRoles.Manager; - } - - case OrgRoles.Guest: - { - return other == OrgRoles.SuperManager || other == OrgRoles.Manager; - } - - case OrgRoles.Undefine: - { - return true; - } - - } - - return false; - - - - } - - - - public static bool IsAdmin(this OrgRoles rol) - { - return rol == OrgRoles.SuperManager || rol == OrgRoles.Manager; - } - - -} \ No newline at end of file diff --git a/LIN.Identity/Usings.cs b/LIN.Identity/Usings.cs deleted file mode 100644 index e4c8642..0000000 --- a/LIN.Identity/Usings.cs +++ /dev/null @@ -1,15 +0,0 @@ -global using Http.ResponsesList; -global using LIN.Identity; -global using LIN.Identity.Hubs; -global using LIN.Identity.Services; -global using LIN.Modules; -global using LIN.Types.Auth.Enumerations; -global using LIN.Types.Auth.Models; -global using LIN.Types.Enumerations; -global using LIN.Types.Responses; -global using Microsoft.AspNetCore.Mvc; -global using Microsoft.AspNetCore.SignalR; -global using Microsoft.EntityFrameworkCore; -global using Microsoft.IdentityModel.Tokens; -global using System.Text; -global using LIN.Access.Logger; \ No newline at end of file diff --git a/LIN.Identity/Validations/Account.cs b/LIN.Identity/Validations/Account.cs deleted file mode 100644 index a88f8f5..0000000 --- a/LIN.Identity/Validations/Account.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace LIN.Identity.Validations; - - -public class Account -{ - - - /// - /// Procesa la información de un Account - /// - /// Modelo - public static AccountModel Process(AccountModel modelo) - { - - var model = new AccountModel - { - ID = 0, - Nombre = modelo.Nombre, - Genero = modelo.Genero, - OrganizationAccess = modelo.OrganizationAccess, - Usuario = modelo.Usuario, - Visibilidad = modelo.Visibilidad, - Contraseña = modelo.Contraseña = EncryptClass.Encrypt(modelo.Contraseña), - Creación = modelo.Creación = DateTime.Now, - Estado = modelo.Estado = AccountStatus.Normal, - Birthday = modelo.Birthday, - Insignia = modelo.Insignia = AccountBadges.None, - Rol = modelo.Rol = AccountRoles.User, - Perfil = modelo.Perfil = modelo.Perfil.Length == 0 - ? File.ReadAllBytes("wwwroot/profile.png") - : modelo.Perfil - }; - - model.Perfil = Image.Zip(model.Perfil); - return model; - - } - - -} \ No newline at end of file diff --git a/LIN.Identity/wwwroot/Plantillas/Email.html b/LIN.Identity/wwwroot/Plantillas/Email.html deleted file mode 100644 index c78d0fa..0000000 --- a/LIN.Identity/wwwroot/Plantillas/Email.html +++ /dev/null @@ -1,151 +0,0 @@ -
-
- -
- -
- - - - - - -
-
- - - - - - -
- - - - - - -
- - - - -
-
-
-
-
- -
-
- - - - - - -
-
- - - - - - - - - - -
-
- -

- Hola @Nombre,

-

- Estas intentanto agregar este correo a tu cuenta LIN. -

- -
-
- - - - - - -
- Confirmar -
-
-
-
-
-
- - -
- - - - - - -
-
- - - - - - - - - -
-
- Enviado por LIN Services • - Pagina - principal - -
-
-
- Medellin, Colombia -
-
-
-
-
-
- -
-
\ No newline at end of file diff --git a/LIN.Identity/wwwroot/Plantillas/Password.html b/LIN.Identity/wwwroot/Plantillas/Password.html deleted file mode 100644 index bc2e999..0000000 --- a/LIN.Identity/wwwroot/Plantillas/Password.html +++ /dev/null @@ -1,153 +0,0 @@ -
-
- -
- -
- - - - - - -
-
- - - - - - -
- - - - - - -
- - - - -
-
-
-
-
- -
-
- - - - - - -
-
- - - - - - - - - - -
-
- -

- Hola @Nombre,

-

- Puede restablecer su contraseña de LIN haciendo clic en - el botón de abajo. Si no has solicitado una nueva - contraseña, ignora este correo electrónico. -

- -
-
- - - - - - -
- Cambiar la contraseña -
-
-
-
-
-
- - -
- - - - - - -
-
- - - - - - - - - -
-
- Enviado por LIN Services • - Pagina - principal - -
-
-
- Medellin, Colombia -
-
-
-
-
-
- -
-
\ No newline at end of file diff --git a/LIN.Identity/wwwroot/Plantillas/Plantilla.html b/LIN.Identity/wwwroot/Plantillas/Plantilla.html deleted file mode 100644 index 19d54cb..0000000 --- a/LIN.Identity/wwwroot/Plantillas/Plantilla.html +++ /dev/null @@ -1,68 +0,0 @@ -
-
-
- - - - - - - - - - -
- - - - - - - - - -
-
-
-
@@Titulo
- - - - @@Subtitulo - - - - - -
-
-
- @@Mensaje - -
-
- También puedes ver toda la informacion de tu cuenta en
- - Tu Cuenta -
-
- -
-
-
-
-
\ No newline at end of file diff --git a/LIN.Identity/wwwroot/profile.png b/LIN.Identity/wwwroot/profile.png deleted file mode 100644 index 50f642e..0000000 Binary files a/LIN.Identity/wwwroot/profile.png and /dev/null differ diff --git a/LIN.Identity/wwwroot/user.png b/LIN.Identity/wwwroot/user.png deleted file mode 100644 index 5f62251..0000000 Binary files a/LIN.Identity/wwwroot/user.png and /dev/null differ diff --git a/README.md b/README.md index 53efb6a..186f2df 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Proyecto de Autenticación de Usuarios LIN -Este es un proyecto desarrollado en C# y .NET 7 que proporciona funcionalidades de autenticación de usuarios para sistemas LIN. El proyecto se centra en garantizar la seguridad y la gestión de usuarios en entornos LIN, permitiendo un acceso controlado a la información. +Este es un proyecto desarrollado en C# y .NET 9 que proporciona funcionalidades de autenticación de usuarios para sistemas LIN. El proyecto se centra en garantizar la seguridad y la gestión de usuarios en entornos LIN, permitiendo un acceso controlado a la información. # Características @@ -15,7 +15,7 @@ Este es un proyecto desarrollado en C# y .NET 7 que proporciona funcionalidades ## Requisitos del Sistema -- [.NET 7 Runtime](https://dotnet.microsoft.com/download/dotnet/7.0) +- [.NET 9 Runtime](https://dotnet.microsoft.com/download/dotnet/9.0) - Base de datos compatible con Entity Framework (SQL Server) ## Configuración @@ -23,7 +23,7 @@ Este es un proyecto desarrollado en C# y .NET 7 que proporciona funcionalidades 1. Clona este repositorio en tu máquina local. ``` - git clone https://github.com/LINServices/LIN.Auth.git + git clone https://github.com/LINServices/LIN.Identity.git ``` 2. Abre la solución en Visual Studio o tu IDE preferido. @@ -31,15 +31,3 @@ Este es un proyecto desarrollado en C# y .NET 7 que proporciona funcionalidades 3. Configura la conexión a la base de datos en el archivo `appsettings.json`, proporcionando la cadena de conexión correcta. 4. Compila y ejecuta la aplicación. - -## Contribución - -Si deseas contribuir a este proyecto, ¡serás bienvenido! Puedes contribuir de las siguientes formas: - -- Reportando problemas y errores. -- Proponiendo nuevas características. -- Enviando solicitudes de extracción para solucionar problemas o agregar características. - -## Licencia - -Este proyecto se distribuye bajo la Licencia MIT. Siéntete libre de utilizar, modificar y distribuir este proyecto de acuerdo con los términos de la licencia. diff --git a/global.json b/global.json deleted file mode 100644 index 7cd6a1f..0000000 --- a/global.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "sdk": { - "version": "7.0.0", - "rollForward": "latestMajor", - "allowPrerelease": true - } -} \ No newline at end of file