This commit is contained in:
2025-08-28 16:01:55 +02:00
parent 68b74284c7
commit c7a94893a2
63 changed files with 633 additions and 200 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,5 @@
{
"email": "sys@t.em",
"key": "aa0e0979-99db-42e7-8b60-91c2d055b9d0",
"password": "+z1L[oYUupZ>L{4a"
"key": "b60e166e-d4a5-416e-a7c9-142d05fb7f31",
"password": "8C3,uTÑ<hñ61qQs3"
}

View File

@@ -2,8 +2,8 @@ using back.DataModels;
namespace back.DTO;
public class UserDto
public class UserDto
{
public string Id { get; set; } = null!;
public ICollection<Role> Roles { get; set; } = [];
public ICollection<RoleDto> Roles { get; set; } = [];
}

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using MCVIngenieros.Transactional.Abstractions;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Transactional.Abstractions;
namespace back.DataModels;

View File

@@ -1,6 +1,6 @@
using MCVIngenieros.Transactional.Abstractions;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Transactional.Abstractions;
namespace back.DataModels;

View File

@@ -1,10 +1,16 @@
using MCVIngenieros.Transactional.Abstractions.Interfaces;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace back.DataModels;
public record PermissionDto
{
public string Id { get; set; } = null!;
}
[Table("Permissions")]
public partial class Permission: IEquatable<Permission>
public partial class Permission : IEntity<Permission>
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; } = null!;
@@ -26,6 +32,32 @@ public partial class Permission: IEquatable<Permission>
Id == other.Id || GetHashCode() == other.GetHashCode();
}
public bool IsNull => this is null;
public object Clone() => (Permission)MemberwiseClone();
public int CompareTo(object? obj)
{
if (obj is null) return 1;
if (obj is not Permission other) throw new ArgumentException("Object is not a Person");
return CompareTo(other);
}
public int CompareTo(Permission? other)
{
if (other is null) return 1;
if (ReferenceEquals(this, other)) return 0;
return string.Compare(Id, other.Id, StringComparison.OrdinalIgnoreCase);
}
public PermissionDto ToDto()
{
return new PermissionDto
{
Id = Id
};
}
// Static permissions
public static readonly Permission ViewContentPermission = new() { Id = "1", Name = "VIEW_CONTENT", Description = "Permission to view content" };
public static readonly Permission LikeContentPermission = new() { Id = "2", Name = "LIKE_CONTENT", Description = "Permission to like content" };

View File

@@ -1,7 +1,7 @@
using MCVIngenieros.Transactional.Abstractions;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Transactional.Abstractions;
using Transactional.Abstractions.Interfaces;
namespace back.DataModels;

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Transactional.Abstractions.Interfaces;
namespace back.DataModels;

View File

@@ -1,10 +1,17 @@
using MCVIngenieros.Transactional.Abstractions.Interfaces;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace back.DataModels;
public class RoleDto
{
public string Id { get; set; } = null!;
public List<PermissionDto> Permissions { get; set; } = [];
}
[Table("Roles")]
public partial class Role : IEquatable<Role>
public partial class Role : IEntity<Role>
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; } = null!;
@@ -18,7 +25,6 @@ public partial class Role : IEquatable<Role>
public virtual ICollection<Permission> Permissions { get; set; } = new HashSet<Permission>();
public virtual ICollection<User> Users { get; set; } = [];
public bool IsAdmin() => BaseRoleModel != null ? BaseRoleModel.IsAdmin() : Id == AdminRole.Id;
public bool IsContentManager() => BaseRoleModel != null ? BaseRoleModel.IsContentManager() : Id == ContentManagerRole.Id;
public bool IsUser() => BaseRoleModel != null ? BaseRoleModel.IsUser() : Id == UserRole.Id;
@@ -43,7 +49,7 @@ public partial class Role : IEquatable<Role>
BaseRoleModelId = baseRoleModel.Id;
foreach (var permission in baseRoleModel.Permissions)
{
if (!Permissions.Any(p => p.Id == permission.Id))
if (!Permissions.Any(p => p.Id == permission.Id))
{
Permissions.Add(permission);
}
@@ -62,6 +68,33 @@ public partial class Role : IEquatable<Role>
return Id == other.Id || GetHashCode() == other.GetHashCode();
}
public bool IsNull => this is null;
public object Clone() => (Role)MemberwiseClone();
public int CompareTo(object? obj)
{
if (obj is null) return 1;
if (obj is not Role other) throw new ArgumentException("Object is not a Person");
return CompareTo(other);
}
public int CompareTo(Role? other)
{
if (other is null) return 1;
if (ReferenceEquals(this, other)) return 0;
return string.Compare(Id, other.Id, StringComparison.OrdinalIgnoreCase);
}
public RoleDto ToDto()
{
return new RoleDto
{
Id = Id,
Permissions = [.. Permissions.Select(p => p.ToDto())]
};
}
public static readonly Role UserRole = new(
"1", "User", "Role for regular users",
[

View File

@@ -1,7 +1,7 @@
using back.DTO;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Transactional.Abstractions.Interfaces;
namespace back.DataModels;
@@ -36,7 +36,7 @@ public class User : IEntity<User>
public UserDto ToDto() => new()
{
Id = Id,
Roles = Roles
Roles = [.. Roles.Select(r => r.ToDto())]
};
public bool IsAdmin() => Roles.Any(r => r.IsAdmin());
@@ -80,7 +80,7 @@ public class User : IEntity<User>
password: "",
createdAt: DateTime.UtcNow
)
{
{
Roles = [Role.AdminRole, Role.ContentManagerRole, Role.UserRole]
};
}

View File

@@ -1,5 +1,5 @@
using back.ServicesExtensions;
using healthchecks;
using MCVIngenieros.Healthchecks;
namespace back;
@@ -13,11 +13,7 @@ public class Program
builder.Services.AddControllers();
builder.Services.AddHealthChecks(options => {
options.CacheDuration = TimeSpan.FromMinutes(30);
options.Timeout = TimeSpan.FromSeconds(5);
options.AssembliesToScan = [typeof(Program).Assembly];
}).DiscoverHealthChecks();
builder.Services.AddHealthChecksSupport().DiscoverHealthChecks();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddSwaggerGen();

View File

@@ -3,8 +3,8 @@ using System.Text.Json.Serialization;
using back.services.engine.SystemUser;
using DependencyInjector;
using System.Text.Json;
using Transactional.Abstractions.Interfaces;
using Transactional.Implementations.EntityFramework;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
using MCVIngenieros.Transactional.Implementations.EntityFramework;
namespace back.ServicesExtensions;

View File

@@ -19,10 +19,15 @@
"EnableSsl": true
},
"HealthChecksConfigs": {
"CacheDuration": "00:30:00",
"Timeout": "00:00:05",
"AssembliesToScan": [
"back"
],
"Sqlite": {
"RetryAttempts" : 2,
"Timeout" : "00:05:00",
"RetryDelay" : "00:00:10",
"RetryAttempts": 2,
"Timeout": "00:05:00",
"RetryDelay": "00:00:10",
"Severity": "Info"
}
}

View File

@@ -12,6 +12,7 @@
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.1" />
<PackageReference Include="Mapster.EFCore" Version="5.1.1" />
<PackageReference Include="MCVIngenieros.Healthchecks" Version="0.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
@@ -33,7 +34,6 @@
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Oracle.EntityFrameworkCore" Version="9.23.90" />
@@ -48,8 +48,7 @@
<ItemGroup>
<ProjectReference Include="..\..\nuget\DependencyInjector\DependencyInjector.csproj" />
<ProjectReference Include="..\..\nuget\healthchecks\healthchecks.csproj" />
<ProjectReference Include="..\..\nuget\Transactional\Transactional.csproj" />
<ProjectReference Include="..\..\nuget\Transactional\MCVIngenieros.Transactional.csproj" />
</ItemGroup>
</Project>

View File

@@ -5,11 +5,11 @@ VisualStudioVersion = 17.14.36401.2
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "back", "back.csproj", "{392278F3-4B36-47F4-AD31-5FBFCC181AD4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Transactional", "..\..\nuget\Transactional\Transactional.csproj", "{ED76105A-5E6F-4997-86FE-6A7902A2AEBA}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCVIngenieros.Transactional", "..\..\nuget\Transactional\MCVIngenieros.Transactional.csproj", "{ED76105A-5E6F-4997-86FE-6A7902A2AEBA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyInjector", "..\..\nuget\DependencyInjector\DependencyInjector.csproj", "{DBDF84A4-235C-4F29-8626-5BD1DC255970}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "healthchecks", "..\..\nuget\healthchecks\healthchecks.csproj", "{B21E2BEF-17B7-4981-9843-C0CC36D67010}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Presentation", "..\backend\Presentation\Presentation.csproj", "{F1DD9D2A-0467-41EE-B3BB-303F1A0C18D6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -29,10 +29,10 @@ Global
{DBDF84A4-235C-4F29-8626-5BD1DC255970}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DBDF84A4-235C-4F29-8626-5BD1DC255970}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DBDF84A4-235C-4F29-8626-5BD1DC255970}.Release|Any CPU.Build.0 = Release|Any CPU
{B21E2BEF-17B7-4981-9843-C0CC36D67010}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B21E2BEF-17B7-4981-9843-C0CC36D67010}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B21E2BEF-17B7-4981-9843-C0CC36D67010}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B21E2BEF-17B7-4981-9843-C0CC36D67010}.Release|Any CPU.Build.0 = Release|Any CPU
{F1DD9D2A-0467-41EE-B3BB-303F1A0C18D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1DD9D2A-0467-41EE-B3BB-303F1A0C18D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1DD9D2A-0467-41EE-B3BB-303F1A0C18D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1DD9D2A-0467-41EE-B3BB-303F1A0C18D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -0,0 +1,44 @@
using Microsoft.AspNetCore.Mvc;
namespace back.controllers;
public class ValidationErrors
{
public string? Field { get; set; }
public string? Message { get; set; }
}
public class ExecutionErrors
{
public Exception? Exception { get; set; }
public string? Message { get; set; }
}
public abstract class ResponseBase
{
public object? Data { get; set; }
public string? Message { get; set; }
public bool Success { get; set; }
public int StatusCode { get; set; }
public ValidationErrors[] ValidationErrors { get; set; }
public ExecutionErrors[] ExecutionErrors { get; set; }
}
public record LoginRequest(string Username, string Password);
[ApiController, Route("api/[controller]")]
public class AuthController(IAuthService authService) : ControllerBase
{
private readonly IAuthService _authService = authService;
[HttpPost, Route("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest loginRequest)
{
// validar que el usuario y la contraseña sean correctos
// obtener el token JWT encriptado
// obtener el refresh token
// devolver el token JWT y el refresh token en los headers de las respuestas
// devolver datos del usuario en el body de la respuesta
}
}

View File

@@ -1,8 +1,6 @@
using back.DataModels;
using back.DTO;
using back.services.bussines;
using back.services.bussines.UserService;
using Mapster;
using Microsoft.AspNetCore.Mvc;
namespace back.controllers;
@@ -46,14 +44,14 @@ public class UsersController(IUserService user) : ControllerBase
if (user == null || string.IsNullOrEmpty(user.Email) || string.IsNullOrEmpty(user.Password))
return BadRequest(Errors.BadRequest.Description);
if (user.Email.Equals(DataModels.User.SystemUser.Email, StringComparison.InvariantCultureIgnoreCase))
if (user.Email.Equals(DataModels.User.SystemUser.Email, StringComparison.InvariantCultureIgnoreCase))
{
if (string.IsNullOrEmpty(user.SystemKey))
return Unauthorized(Errors.Unauthorized.Description);
var systemUser = await _user.ValidateSystemUser(user.Email, user.Password, user.SystemKey, clientId);
if (systemUser == null)
return Unauthorized(Errors.Unauthorized.Description);
return Ok(systemUser.Adapt<UserDto>());
return Ok(systemUser.ToDto());
}
var existingUser = await _user.Login(user.Email, user.Password, clientId);
@@ -74,7 +72,7 @@ public class UsersController(IUserService user) : ControllerBase
// POST api/<UsersController>
[HttpPost("[action]")]
public async Task<IActionResult> Register(
[FromHeader(Name = "X-client-thumbprint")] string clientId,
[FromHeader(Name = "X-client-thumbprint")] string clientId,
[FromBody] RegisterFromModel user)
{
if (user == null)

View File

@@ -1,8 +1,6 @@
using HealthChecksConfigsBase = healthchecks.Options.HealthChecksConfigs;
namespace back.healthchecks.Options;
namespace back.healthchecks.Options;
public partial class HealthChecksConfigs : HealthChecksConfigsBase
public partial class HealthChecksConfigs : MCVIngenieros.Healthchecks.Options.HealthChecksConfigs
{
public const string Sqlite = "Sqlite";
}

View File

@@ -1,12 +1,12 @@
using back.Options;
using healthchecks;
using healthchecks.Abstracts;
using back.healthchecks.Options;
using back.healthchecks.Options;
using back.Options;
using MCVIngenieros.Healthchecks;
using MCVIngenieros.Healthchecks.Abstracts;
using Microsoft.Extensions.Options;
namespace back.healthchecks;
public class SqliteHealthCheck(IOptionsMonitor<DatabaseConfig> databaseConfig, IOptionsMonitor<HealthChecksConfigs> healthchecksConfig) : IHealthCheck
public class SqliteHealthCheck(IOptionsMonitor<DatabaseConfig> databaseConfig, IOptionsMonitor<HealthChecksConfigs> healthchecksConfig) : HealthCheck
{
private readonly DatabaseConfig databaseConfig = databaseConfig.Get(DatabaseConfig.DataStorage);
private readonly HealthChecksConfigs hcConfig = healthchecksConfig.Get(HealthChecksConfigs.Sqlite);
@@ -17,7 +17,7 @@ public class SqliteHealthCheck(IOptionsMonitor<DatabaseConfig> databaseConfig, I
public TimeSpan? RetryDelay => hcConfig.RetryDelay ?? TimeSpan.FromSeconds(1);
public HealthCheckSeverity? Severity => hcConfig.Severity ?? HealthCheckSeverity.Critical;
public Task<HealthCheckResult> CheckAsync(CancellationToken cancellationToken = default)
public override Task<HealthCheckResult> CheckAsync(CancellationToken cancellationToken = default)
{
var isHealthy = false;
var details = string.Empty;
@@ -43,7 +43,7 @@ public class SqliteHealthCheck(IOptionsMonitor<DatabaseConfig> databaseConfig, I
details = $"Failed to connect to SQLite database: {ex.Message}";
}
return Task.FromResult(new HealthCheckResult(isHealthy, null)
return Task.FromResult(new HealthCheckResult(isHealthy)
{
Details = details,
Severity = isHealthy ? HealthCheckSeverity.Info : HealthCheckSeverity.Critical

View File

@@ -1,4 +1,4 @@
using DependencyInjector.Lifetimes;
using DependencyInjector.Abstractions.Lifetimes;
namespace back.persistance.blob;

View File

@@ -49,15 +49,6 @@ public partial class DataContext : DbContext
relationEstablisher?.EstablishRelation(modelBuilder);
});
//typeof(ISeeder).Assembly.GetExportedTypes()
// .Where(t => typeof(ISeeder).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract)
// .ToList()
// .ForEach(seederType =>
// {
// var seeder = (ISeeder?)Activator.CreateInstance(seederType);
// seeder?.Seed(modelBuilder);
// });
OnModelCreatingPartial(modelBuilder);
}

View File

@@ -0,0 +1,10 @@
using back.DataModels;
using DependencyInjector.Abstractions.Lifetimes;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
namespace back.persistance.data.repositories.Abstracts;
public interface IPermissionRepository : IRepository<Permission>, IScoped
{
Task SeedDefaultPermissions();
}

View File

@@ -1,6 +1,6 @@
using back.DataModels;
using DependencyInjector.Lifetimes;
using Transactional.Abstractions.Interfaces;
using DependencyInjector.Abstractions.Lifetimes;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
namespace back.persistance.data.repositories.Abstracts;

View File

@@ -1,8 +1,8 @@
using back.DataModels;
using DependencyInjector.Lifetimes;
using Transactional.Abstractions.Interfaces;
using DependencyInjector.Abstractions.Lifetimes;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
namespace back.persistance.data.repositories.Abstracts;
public interface IPhotoRepository : IRepository<Photo>, IScoped
public interface IPhotoRepository : IRepository<Photo>, IScoped
{ }

View File

@@ -0,0 +1,10 @@
using back.DataModels;
using DependencyInjector.Abstractions.Lifetimes;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
namespace back.persistance.data.repositories.Abstracts;
public interface IRoleRepository : IRepository<Role>, IScoped
{
Task SeedDefaultRoles();
}

View File

@@ -1,6 +1,6 @@
using back.DataModels;
using DependencyInjector.Lifetimes;
using Transactional.Abstractions.Interfaces;
using DependencyInjector.Abstractions.Lifetimes;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
namespace back.persistance.data.repositories.Abstracts;

View File

@@ -0,0 +1,34 @@
using back.DataModels;
using back.persistance.data.repositories.Abstracts;
using MCVIngenieros.Transactional.Implementations.EntityFramework;
namespace back.persistance.data.repositories;
public class PermissionRepository(DataContext context) : ReadWriteRepository<Permission>(context), IPermissionRepository
{
// Implement methods specific to Photo repository if needed
public async Task SeedDefaultPermissions()
{
var defaultPermissions = new List<Permission>
{
Permission.ViewContentPermission,
Permission.LikeContentPermission,
Permission.EditContentPermission,
Permission.DeleteContentPermission,
Permission.CreateContentPermission,
Permission.EditUserPermission,
Permission.DeleteUserPermission,
Permission.DisableUserPermission,
Permission.CreateUserPermission,
Permission.EditWebConfigPermission
};
foreach (var permission in defaultPermissions)
{
if (!Entities.Any(p => p.Id == permission.Id))
{
Entities.Add(permission);
}
}
await SaveChanges();
}
}

View File

@@ -1,6 +1,6 @@
using back.DataModels;
using back.persistance.data.repositories.Abstracts;
using Transactional.Implementations.EntityFramework;
using MCVIngenieros.Transactional.Implementations.EntityFramework;
namespace back.persistance.data.repositories;

View File

@@ -1,6 +1,6 @@
using back.DataModels;
using back.persistance.data.repositories.Abstracts;
using Transactional.Implementations.EntityFramework;
using MCVIngenieros.Transactional.Implementations.EntityFramework;
namespace back.persistance.data.repositories;

View File

@@ -0,0 +1,27 @@
using back.DataModels;
using back.persistance.data.repositories.Abstracts;
using MCVIngenieros.Transactional.Implementations.EntityFramework;
namespace back.persistance.data.repositories;
public class RoleRepository(DataContext context) : ReadWriteRepository<Role>(context), IRoleRepository
{
// Implement methods specific to Photo repository if needed
public async Task SeedDefaultRoles()
{
var defaultRoles = new List<Role>
{
Role.AdminRole,
Role.UserRole,
Role.ContentManagerRole
};
foreach (var role in defaultRoles)
{
if (!Entities.Any(p => p.Id == role.Id))
{
Entities.Add(role);
}
}
await SaveChanges();
}
}

View File

@@ -1,20 +1,22 @@
using back.DataModels;
using back.persistance.data.repositories.Abstracts;
using MCVIngenieros.Transactional.Implementations.EntityFramework;
using Microsoft.EntityFrameworkCore;
using Transactional.Implementations.EntityFramework;
namespace back.persistance.data.repositories;
public class UserRepository(DataContext context) : ReadWriteRepository<User>(context), IUserRepository
public class UserRepository(
DataContext context
) : ReadWriteRepository<User>(context), IUserRepository
{
public async Task<User?> GetByEmail(string email)
{
try
try
{
if (string.IsNullOrEmpty(email)) return null;
return await Entities.FirstOrDefaultAsync(u => u.Email == email);
}
catch
catch
{
return null;
}
@@ -37,26 +39,29 @@ public class UserRepository(DataContext context) : ReadWriteRepository<User>(con
public async Task<User?> Login(string email, string password)
{
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password)) return null;
try
try
{
return await Entities.FirstOrDefaultAsync(u => u.Email == email && u.Password == password);
return await Entities
.Include(u => u.Roles)
.ThenInclude(r => r.Permissions)
.FirstOrDefaultAsync(u => u.Email == email && u.Password == password);
}
catch
catch
{
return null;
return null;
}
}
public async Task<bool> ExistsByEmail(string email)
{
try
try
{
if (string.IsNullOrEmpty(email)) return false;
return await Entities.AnyAsync(u => u.Email == email);
}
catch
{
return false;
}
catch
{
return false;
}
}

View File

@@ -1,23 +1,23 @@
//using back.DataModels;
//using Microsoft.EntityFrameworkCore;
using back.DataModels;
using Microsoft.EntityFrameworkCore;
//namespace back.persistance.data.seeders;
namespace back.persistance.data.seeders;
//public class PermissionSeeder : ISeeder
//{
// public void Seed(ModelBuilder modelBuilder)
// {
// modelBuilder.Entity<Permission>().HasData(
// Permission.ViewContentPermission,
// Permission.LikeContentPermission,
// Permission.EditContentPermission,
// Permission.DeleteContentPermission,
// Permission.CreateContentPermission,
// Permission.EditUserPermission,
// Permission.DeleteUserPermission,
// Permission.DisableUserPermission,
// Permission.CreateUserPermission,
// Permission.EditWebConfigPermission
// );
// }
//}
public class PermissionSeeder : ISeeder
{
public void Seed(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Permission>().HasData(
Permission.ViewContentPermission,
Permission.LikeContentPermission,
Permission.EditContentPermission,
Permission.DeleteContentPermission,
Permission.CreateContentPermission,
Permission.EditUserPermission,
Permission.DeleteUserPermission,
Permission.DisableUserPermission,
Permission.CreateUserPermission,
Permission.EditWebConfigPermission
);
}
}

View File

@@ -1,16 +1,16 @@
//using back.DataModels;
//using Microsoft.EntityFrameworkCore;
using back.DataModels;
using Microsoft.EntityFrameworkCore;
//namespace back.persistance.data.seeders;
namespace back.persistance.data.seeders;
//public class RoleSeeder : ISeeder
//{
// public void Seed(ModelBuilder modelBuilder)
// {
// modelBuilder.Entity<Permission>().HasData(
// new Role { Id = "1", Name = "User", Description = "Role for regular users", BaseRoleModelId = null },
// new Role { Id = "2", Name = "Content Manager", Description = "Role for managing content", BaseRoleModelId = "1" },
// new Role { Id = "3", Name = "Admin", Description = "Administrator role with full permissions", BaseRoleModelId = "2" }
// );
// }
//}
public class RoleSeeder : ISeeder
{
public void Seed(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Permission>().HasData(
new Role { Id = "1", Name = "User", Description = "Role for regular users", BaseRoleModelId = null },
new Role { Id = "2", Name = "Content Manager", Description = "Role for managing content", BaseRoleModelId = "1" },
new Role { Id = "3", Name = "Admin", Description = "Administrator role with full permissions", BaseRoleModelId = "2" }
);
}
}

View File

@@ -1,14 +1,14 @@
using back.DataModels;
using Microsoft.EntityFrameworkCore;
//using back.DataModels;
//using Microsoft.EntityFrameworkCore;
namespace back.persistance.data.seeders;
//namespace back.persistance.data.seeders;
public class SystemUserSeeder : ISeeder
{
public void Seed(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Permission>().HasData(
User.SystemUser
);
}
}
//public class SystemUserSeeder : ISeeder
//{
// public void Seed(ModelBuilder modelBuilder)
// {
// modelBuilder.Entity<Permission>().HasData(
// User.SystemUser
// );
// }
//}

View File

@@ -1,6 +1,6 @@
using back.DataModels;
using back.DTO;
using DependencyInjector.Lifetimes;
using DependencyInjector.Abstractions.Lifetimes;
namespace back.services.bussines.PhotoService;

View File

@@ -1,5 +1,5 @@
using back.DataModels;
using DependencyInjector.Lifetimes;
using DependencyInjector.Abstractions.Lifetimes;
namespace back.services.bussines.UserService;

View File

@@ -68,7 +68,7 @@ public class UserService(
return existingUser;
}
public async Task<User?> Login(string email, string decryptedPass)
public async Task<User?> Login(string email, string decryptedPass)
{
var salt = await _repository.GetUserSaltByEmail(email);
var hashedPassword = _cryptoService.HashPassword(decryptedPass, salt);
@@ -136,6 +136,7 @@ public class UserService(
{
return null;
}
return await Login(user.Email!, decryptedPassword);
var loggedUser = await Login(user.Email!, decryptedPassword);
return loggedUser;
}
}

View File

@@ -1,4 +1,4 @@
using DependencyInjector.Lifetimes;
using DependencyInjector.Abstractions.Lifetimes;
namespace back.services.engine.Crypto;

View File

@@ -1,4 +1,4 @@
using DependencyInjector.Lifetimes;
using DependencyInjector.Abstractions.Lifetimes;
namespace back.services.engine.ImageResizer;

View File

@@ -1,4 +1,4 @@
using DependencyInjector.Lifetimes;
using DependencyInjector.Abstractions.Lifetimes;
namespace back.services.engine.PasswordGenerator;

View File

@@ -1,4 +1,4 @@
using DependencyInjector.Lifetimes;
using DependencyInjector.Abstractions.Lifetimes;
namespace back.services.engine.SystemUser;

View File

@@ -4,8 +4,8 @@ using back.persistance.data;
using back.persistance.data.repositories.Abstracts;
using back.services.engine.Crypto;
using back.services.engine.PasswordGenerator;
using MCVIngenieros.Transactional.Abstractions.Interfaces;
using System.Text.Json;
using Transactional.Abstractions.Interfaces;
namespace back.services.engine.SystemUser;
@@ -14,17 +14,20 @@ public class SystemUserGenerator(
JsonSerializerOptions jsonSerializerOptions,
IUserRepository userRepository,
IPersonRepository personRepository,
IRoleRepository roleRepository,
IPermissionRepository permissionRepository,
ICryptoService cryptoService,
IBlobStorageService blobStorageService,
IPasswordGenerator passwordGenerator) : ISystemUserGenerator
{
public async Task GenerateAsync()
{
var systemKey = new SystemKey() {
var systemKey = new SystemKey()
{
Password = passwordGenerator.Generate(16),
};
var systemKeyJson = JsonSerializer.Serialize(systemKey, options: jsonSerializerOptions);
using Stream stream = new MemoryStream(new System.Text.UTF8Encoding(true).GetBytes(systemKeyJson));
await blobStorageService.Delete("systemkey.lock");
@@ -42,11 +45,13 @@ public class SystemUserGenerator(
{
await transactional.DoTransaction(async () =>
{
await permissionRepository.SeedDefaultPermissions();
await roleRepository.SeedDefaultRoles();
await personRepository.Insert(Person.SystemPerson);
await userRepository.Insert(User.SystemUser);
});
}
else
else
{
await userRepository.Update(User.SystemUser);
await userRepository.SaveChanges();

View File

@@ -1,4 +1,4 @@
using DependencyInjector.Lifetimes;
using DependencyInjector.Abstractions.Lifetimes;
namespace back.services.engine.mailing;

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Mvc;
namespace Presentation.Controllers;
public class AuthController : Controller
{
}

View File

@@ -0,0 +1,6 @@
namespace Presentation.Infraestructura.Responses;
public sealed class ExecutionError(string message)
{
public string Message { get; set; } = message;
}

View File

@@ -0,0 +1,29 @@
using System.Net;
namespace Presentation.Infraestructura.Responses;
public sealed class Response<T>
{
public bool IsSuccess { get; set; }
public HttpStatusCode StatusCode { get; set; }
public T? Data { get; set; }
public ValidationError[]? ValidationErrors { get; set; }
public ExecutionError[]? ExecutionErrors { get; set; }
public static Response<T> Success(T result, HttpStatusCode statusCode = HttpStatusCode.OK) =>
new()
{
IsSuccess = true,
StatusCode = statusCode,
Data = result
};
public static Response<T> Failure(HttpStatusCode statusCode = HttpStatusCode.InternalServerError, ValidationError[]? validationErrors = null, ExecutionError[]? executionErrors = null) =>
new()
{
IsSuccess = false,
StatusCode = statusCode,
ValidationErrors = validationErrors,
ExecutionErrors = executionErrors
};
}

View File

@@ -0,0 +1,7 @@
namespace Presentation.Infraestructura.Responses;
public sealed class ValidationError(string fieldName, string message)
{
public string Field { get; set; } = fieldName;
public string Message { get; set; } = message;
}

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8" />
<PackageReference Include="OpenTelemetry" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Api" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Api.ProviderBuilderExtensions" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.OpenTelemetry" Version="4.2.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,96 @@
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.OpenTelemetry;
namespace Presentation
{
public class Program
{
public static void Main(string[] args)
{
// Configura Serilog como logger global antes de crear el builder
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.OpenTelemetry(options =>
{
options.Endpoint = "http://localhost:4317"; // OTLP endpoint
options.Protocol = OtlpProtocol.Grpc;
options.ResourceAttributes = new Dictionary<string, object>
{
["service.name"] = "mmorales.photo-backend"
};
})
.CreateLogger();
try
{
Log.Information("Starting up");
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog(); // Usa Serilog como proveedor de logs por defecto
builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails =
ctx => ctx.ProblemDetails.Extensions.Add("traceId", ctx.HttpContext.TraceIdentifier)
);
builder.Services.AddOpenTelemetry()
.WithTracing(tracerProviderBuilder =>
{
tracerProviderBuilder
.SetResourceBuilder(ResourceBuilder.CreateDefault()
.AddService(builder.Environment.ApplicationName))
.AddAspNetCoreInstrumentation() // Traza todas las peticiones HTTP entrantes
.AddHttpClientInstrumentation() // Traza las llamadas HttpClient salientes
.AddOtlpExporter(opt =>
{
opt.Endpoint = new Uri("http://localhost:4317"); // Direcci<63>n del colector OTel
})
.AddConsoleExporter(); // Exporta trazas tambi<62>n a consola para desarrollo
});
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
app.UseHsts();
}
app.UseStatusCodePages();
app.UseAuthentication(); // Habilita autenticaci<63>n
app.UseAuthorization(); // Habilita autorizaci<63>n
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application start-up failed");
throw;
}
finally
{
Log.CloseAndFlush();
}
}
}
}

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5101",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7265;http://localhost:5101",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -1,3 +1,6 @@
<div class="admin-panel-link">
<a href="/admin">Admin</a>
</div>
<svg-button
label="Panel de Administración"
routerLink="/admin"
text="Panel de Administración"
icon="assets/icons/book-section-svgrepo-com.svg"
></svg-button>

Before

Width:  |  Height:  |  Size: 67 B

After

Width:  |  Height:  |  Size: 169 B

View File

@@ -1,11 +1,10 @@
import { Component } from '@angular/core';
import { SvgButton } from '../../../utils/svg-button/svg-button';
@Component({
selector: 'admin-panel-link',
imports: [],
imports: [SvgButton],
templateUrl: './admin-panel-link.html',
styleUrl: './admin-panel-link.scss'
styleUrl: './admin-panel-link.scss',
})
export class AdminPanelLink {
}
export class AdminPanelLink {}

View File

@@ -1,3 +1,6 @@
<div class="content-manager-panel-link">
<a href="/content-manager">Contenido</a>
</div>
<svg-button
label="Panel de Contenido"
routerLink="/content-manager"
text="Panel de Contenido"
icon="assets/icons/book-section-svgrepo-com.svg"
></svg-button>

Before

Width:  |  Height:  |  Size: 91 B

After

Width:  |  Height:  |  Size: 167 B

View File

@@ -1,11 +1,10 @@
import { Component } from '@angular/core';
import { SvgButton } from '../../../utils/svg-button/svg-button';
@Component({
selector: 'content-manager-panel-link',
imports: [],
imports: [SvgButton],
templateUrl: './content-manager-panel-link.html',
styleUrl: './content-manager-panel-link.scss'
styleUrl: './content-manager-panel-link.scss',
})
export class ContentManagerPanelLink {
}
export class ContentManagerPanelLink {}

View File

@@ -12,7 +12,6 @@ import { OnInit } from '@angular/core';
import { userModel } from '../../../models/userModel';
import { ServicesLink } from '../services-link/services-link';
import { Router, NavigationEnd } from '@angular/router';
import { R } from '@angular/cdk/keycodes';
@Component({
selector: 'custom-header',

View File

@@ -1,3 +1,6 @@
<div class="user-galleries-link">
<a href="/galeria">Galería</a>
</div>
<svg-button
label="Galerías"
routerLink="/my-galleries"
text="Galerías"
icon="assets/icons/book-section-svgrepo-com.svg"
></svg-button>

Before

Width:  |  Height:  |  Size: 75 B

After

Width:  |  Height:  |  Size: 146 B

View File

@@ -1,11 +1,10 @@
import { Component } from '@angular/core';
import { Component, inject, signal } from '@angular/core';
import { SvgButton } from '../../../utils/svg-button/svg-button';
@Component({
selector: 'user-galleries-link',
imports: [],
imports: [SvgButton],
templateUrl: './user-galleries-link.html',
styleUrl: './user-galleries-link.scss'
styleUrl: './user-galleries-link.scss',
})
export class UserGalleriesLink {
}
export class UserGalleriesLink {}

View File

@@ -39,9 +39,10 @@ export class userService {
systemKey: encryptedSystemKey,
})
.then((response) => {
const { jwt, refresh, usermodel } = response.data;
localStorage.setItem('jwt', jwt);
localStorage.setItem('refresh', refresh);
const { id, roles } = response.data;
const usermodel = new userModel(id, roles, true);
// localStorage.setItem('jwt', jwt);
// localStorage.setItem('refresh', refresh);
this.setUser(usermodel);
return usermodel;
});

View File

@@ -8,6 +8,13 @@ export class roleModel {
public permissions: permissionModel[] = [],
public baseRoleModel: roleModel | null = null
) {
this.id = id;
this.name = name;
this.description = description;
this.permissions = [];
for (const p of permissions) {
this.permissions.push(new permissionModel(p.id, '', ''));
}
if (baseRoleModel) {
this.permissions = baseRoleModel.permissions.concat(this.permissions);
}

View File

@@ -11,47 +11,59 @@ enum SigningMethods {
Microsoft = 'microsoft',
}
class userModel extends personModel {
class userModel {
constructor(
public override id: string,
public email: string,
public password: string,
public override name: string,
public id: string,
public role: roleModel[],
public createdAt: Date,
public updatedAt: Date,
public isLoggedIn: boolean,
public preferredSigningMethod: SigningMethods = SigningMethods.Password
public isLoggedIn: boolean
) {
super({
id,
name,
profilePicture: null,
avatar: null,
socialMedia: null,
});
this.id = id;
for (const r of role) {
this.role.push(new roleModel(r.id, '', '', r.permissions, null));
}
this.isLoggedIn = isLoggedIn;
}
// constructor(
// public override id: string,
// public email: string,
// public password: string,
// public override name: string,
// public role: roleModel[],
// public createdAt: Date,
// public updatedAt: Date,
// public isLoggedIn: boolean,
// public preferredSigningMethod: SigningMethods = SigningMethods.Password
// ) {
// super({
// id,
// name,
// profilePicture: null,
// avatar: null,
// socialMedia: null,
// });
// }
get isAdmin(): boolean {
return this.role.some((role) => role.isAdmin);
return this.role.some((r) => r.isAdmin);
}
get isContentManager(): boolean {
return this.role.some((role) => role.isContentManager);
return this.role.some((r) => r.isContentManager);
}
get isUser(): boolean {
return this.role.some((role) => role.isUser);
return this.role.some((r) => r.isUser);
}
public static readonly DefaultUser: userModel = new userModel(
'0',
'default@example.com',
'',
'Default User',
// 'default@example.com',
// '',
// 'Default User',
[roleModel.UserRole],
new Date(),
new Date(),
// new Date(),
// new Date(),
false
);
}