healthchecks

This commit is contained in:
2025-08-25 18:52:59 +02:00
parent 5777e351bf
commit 0560a40876
33 changed files with 317 additions and 127 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
*.db *.db
back/data/ back/.program_data/
## Ignore Visual Studio temporary files, build results, and ## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons. ## files generated by popular Visual Studio add-ons.
## ##

Binary file not shown.

Binary file not shown.

View File

@@ -1 +1,5 @@
{"Email":"@system","Key":"caeae1bc-3761-4b30-8627-d86af99b0a4f","Password":"M8I^7b,UF!)PIQ.A"} {
"email": "sys@t.em",
"key": "c1d6bd4e-ac32-4859-b2f5-fcda1c190934",
"password": "Tx,bA%8KPn_dç8v["
}

9
back/DTO/UserDto.cs Normal file
View File

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

View File

@@ -1,11 +1,12 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Transactional.Abstractions; using Transactional.Abstractions;
using Transactional.Abstractions.Interfaces;
namespace back.DataModels; namespace back.DataModels;
[Table("Persons")] [Table("Persons")]
public partial class Person: IEquatable<Person>, ISoftDeletable public partial class Person: IEntity<Person>, ISoftDeletable
{ {
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; } = null!; public string Id { get; set; } = null!;
@@ -26,6 +27,7 @@ public partial class Person: IEquatable<Person>, ISoftDeletable
public virtual User? User { get; set; } public virtual User? User { get; set; }
public virtual ICollection<Photo> PhotosNavigation { get; set; } = []; public virtual ICollection<Photo> PhotosNavigation { get; set; } = [];
public override int GetHashCode() => HashCode.Combine(Id, Name); public override int GetHashCode() => HashCode.Combine(Id, Name);
public override bool Equals(object? obj) public override bool Equals(object? obj)
@@ -38,6 +40,23 @@ public partial class Person: IEquatable<Person>, ISoftDeletable
return return
Id == other.Id || GetHashCode() == other.GetHashCode(); Id == other.Id || GetHashCode() == other.GetHashCode();
} }
public bool IsNull => this is null;
public object Clone() => (Person)MemberwiseClone();
public int CompareTo(object? obj)
{
if(obj is null) return 1;
if (obj is not Person other) throw new ArgumentException("Object is not a Person");
return CompareTo(other);
}
public int CompareTo(Person? other)
{
if (other is null) return 1;
if (ReferenceEquals(this, other)) return 0;
return string.Compare(Id, other.Id, StringComparison.OrdinalIgnoreCase);
}
public const string SystemPersonId = "00000000-0000-0000-0000-000000000001"; public const string SystemPersonId = "00000000-0000-0000-0000-000000000001";

View File

@@ -1,10 +1,11 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Transactional.Abstractions.Interfaces;
namespace back.DataModels; namespace back.DataModels;
[Table("Photos")] [Table("Photos")]
public partial class Photo : IEquatable<Photo> public partial class Photo : IEntity<Photo>
{ {
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; } = null!; public string Id { get; set; } = null!;
@@ -44,4 +45,22 @@ public partial class Photo : IEquatable<Photo>
return return
Id == other.Id || GetHashCode() == other.GetHashCode(); Id == other.Id || GetHashCode() == other.GetHashCode();
} }
public bool IsNull => this is null;
public object Clone() => (Photo)MemberwiseClone();
public int CompareTo(object? obj)
{
if (obj is null) return 1;
if (obj is not Photo other) throw new ArgumentException("Object is not a Person");
return CompareTo(other);
}
public int CompareTo(Photo? other)
{
if (other is null) return 1;
if (ReferenceEquals(this, other)) return 0;
return string.Compare(Id, other.Id, StringComparison.OrdinalIgnoreCase);
}
} }

View File

@@ -2,7 +2,7 @@
public class SystemKey public class SystemKey
{ {
public string Email { get; set; } = "@system"; public string Email { get; set; } = User.SystemUser.Email;
public string Key { get; set; } = Guid.NewGuid().ToString(); public string Key { get; set; } = Guid.NewGuid().ToString();
public required string Password { get; set; } public required string Password { get; set; }

View File

@@ -1,10 +1,12 @@
using back.DTO;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Transactional.Abstractions.Interfaces;
namespace back.DataModels; namespace back.DataModels;
[Table("Users")] [Table("Users")]
public class User : IEquatable<User> public class User : IEntity<User>
{ {
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; } = null!; public string Id { get; set; } = null!;
@@ -31,6 +33,12 @@ public class User : IEquatable<User>
CreatedAt = createdAt.ToString("dd-MM-yyyy HH:mm:ss zz"); CreatedAt = createdAt.ToString("dd-MM-yyyy HH:mm:ss zz");
} }
public UserDto ToDto() => new()
{
Id = Id,
Roles = Roles
};
public bool IsAdmin() => Roles.Any(r => r.IsAdmin()); public bool IsAdmin() => Roles.Any(r => r.IsAdmin());
public bool IsContentManager() => Roles.Any(r => r.IsContentManager()); public bool IsContentManager() => Roles.Any(r => r.IsContentManager());
public bool IsUser() => Roles.Any(r => r.IsUser()); public bool IsUser() => Roles.Any(r => r.IsUser());
@@ -46,12 +54,33 @@ public class User : IEquatable<User>
return Id == other.Id && Email == other.Email; return Id == other.Id && Email == other.Email;
} }
public bool IsNull => this is null;
public object Clone() => (User)MemberwiseClone();
public int CompareTo(object? obj)
{
if (obj is null) return 1;
if (obj is not User other) throw new ArgumentException("Object is not a Person");
return CompareTo(other);
}
public int CompareTo(User? other)
{
if (other is null) return 1;
if (ReferenceEquals(this, other)) return 0;
return string.Compare(Id, other.Id, StringComparison.OrdinalIgnoreCase);
}
public const string SystemUserId = "00000000-0000-0000-0000-000000000001"; public const string SystemUserId = "00000000-0000-0000-0000-000000000001";
public static readonly User SystemUser = new( public static readonly User SystemUser = new(
id: SystemUserId, id: SystemUserId,
email: "@system", email: "sys@t.em",
password: "", password: "",
createdAt: DateTime.UtcNow createdAt: DateTime.UtcNow
); )
{
Roles = [Role.AdminRole, Role.ContentManagerRole, Role.UserRole]
};
} }

View File

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

View File

@@ -1,8 +1,8 @@
using back.persistance.data; using back.persistance.data;
using back.persistance.data.repositories; using System.Text.Json.Serialization;
using back.persistance.data.repositories.Abstracts;
using back.services.engine.SystemUser; using back.services.engine.SystemUser;
using DependencyInjector; using DependencyInjector;
using System.Text.Json;
using Transactional.Abstractions.Interfaces; using Transactional.Abstractions.Interfaces;
using Transactional.Implementations.EntityFramework; using Transactional.Implementations.EntityFramework;
@@ -21,6 +21,23 @@ public static partial class ServicesExtensions
services.AddServices(); services.AddServices();
services.AddScoped<ITransactionalService<DataContext>, EntityFrameworkTransactionalService<DataContext>>(); services.AddScoped<ITransactionalService<DataContext>, EntityFrameworkTransactionalService<DataContext>>();
services.AddSingleton(new JsonSerializerOptions {
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
Converters = {
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),
},
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString,
ReadCommentHandling = JsonCommentHandling.Skip,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement,
});
using var scope = services.BuildServiceProvider().CreateScope(); using var scope = services.BuildServiceProvider().CreateScope();
scope.ServiceProvider scope.ServiceProvider

View File

@@ -7,8 +7,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.14.2" /> <PackageReference Include="Azure.Identity" Version="1.15.0" />
<PackageReference Include="MailKit" Version="4.13.0" /> <PackageReference Include="MailKit" Version="4.13.0" />
<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="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
@@ -31,9 +34,10 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" 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.DependencyInjection" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" 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="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Oracle.EntityFrameworkCore" Version="9.23.90" /> <PackageReference Include="Oracle.EntityFrameworkCore" Version="9.23.90" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="9.0.3" /> <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="9.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.3" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.3" />
@@ -44,6 +48,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\nuget\DependencyInjector\DependencyInjector.csproj" /> <ProjectReference Include="..\..\nuget\DependencyInjector\DependencyInjector.csproj" />
<ProjectReference Include="..\..\nuget\healthchecks\healthchecks.csproj" />
<ProjectReference Include="..\..\nuget\Transactional\Transactional.csproj" /> <ProjectReference Include="..\..\nuget\Transactional\Transactional.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Transactional", "..\..\nuge
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyInjector", "..\..\nuget\DependencyInjector\DependencyInjector.csproj", "{DBDF84A4-235C-4F29-8626-5BD1DC255970}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyInjector", "..\..\nuget\DependencyInjector\DependencyInjector.csproj", "{DBDF84A4-235C-4F29-8626-5BD1DC255970}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "healthchecks", "..\..\nuget\healthchecks\healthchecks.csproj", "{B21E2BEF-17B7-4981-9843-C0CC36D67010}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -27,6 +29,10 @@ Global
{DBDF84A4-235C-4F29-8626-5BD1DC255970}.Debug|Any CPU.Build.0 = Debug|Any CPU {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.ActiveCfg = Release|Any CPU
{DBDF84A4-235C-4F29-8626-5BD1DC255970}.Release|Any CPU.Build.0 = 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
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -1,6 +1,8 @@
using back.DataModels; using back.DataModels;
using back.DTO;
using back.services.bussines; using back.services.bussines;
using back.services.bussines.UserService; using back.services.bussines.UserService;
using Mapster;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace back.controllers; namespace back.controllers;
@@ -44,14 +46,14 @@ public class UsersController(IUserService user) : ControllerBase
if (user == null || string.IsNullOrEmpty(user.Email) || string.IsNullOrEmpty(user.Password)) if (user == null || string.IsNullOrEmpty(user.Email) || string.IsNullOrEmpty(user.Password))
return BadRequest(Errors.BadRequest.Description); return BadRequest(Errors.BadRequest.Description);
if (user.Email.Equals("@system", StringComparison.InvariantCultureIgnoreCase)) if (user.Email.Equals(DataModels.User.SystemUser.Email, StringComparison.InvariantCultureIgnoreCase))
{ {
if (string.IsNullOrEmpty(user.SystemKey)) if (string.IsNullOrEmpty(user.SystemKey))
return Unauthorized(Errors.Unauthorized.Description); return Unauthorized(Errors.Unauthorized.Description);
var systemUser = await _user.ValidateSystemUser(user.Email, user.Password, user.SystemKey, clientId); var systemUser = await _user.ValidateSystemUser(user.Email, user.Password, user.SystemKey, clientId);
if (systemUser == null) if (systemUser == null)
return Unauthorized(Errors.Unauthorized.Description); return Unauthorized(Errors.Unauthorized.Description);
return Ok(systemUser); return Ok(systemUser.Adapt<UserDto>());
} }
var existingUser = await _user.Login(user.Email, user.Password, clientId); var existingUser = await _user.Login(user.Email, user.Password, clientId);

View File

@@ -0,0 +1,50 @@
using back.Options;
using healthchecks;
using Microsoft.Extensions.Options;
namespace back.healthchecks;
[HealthCheckExecutionOptions(retryAttempts: 2, timeout: "00:00:05", retryDelay: "00:00:01", severity: HealthCheckSeverity.Critical)]
public class SqliteHealthCheck : IHealthCheck
{
private readonly DatabaseConfig config;
public SqliteHealthCheck(IOptionsMonitor<DatabaseConfig> optionsSnapshot)
{
config = optionsSnapshot.Get(DatabaseConfig.DataStorage);
}
public Task<HealthCheckResult> CheckAsync(CancellationToken cancellationToken = default)
{
// check if can connect to sqlite database
// then run a query to Users table to see if User.SystemUser exists
var isHealthy = false;
var details = string.Empty;
try
{
using var connection = new Microsoft.Data.Sqlite.SqliteConnection(config.ConnectionString);
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = $"SELECT COUNT(1) FROM Users WHERE Id = '{DataModels.User.SystemUserId}';";
var result = command.ExecuteScalar();
if (result != null && Convert.ToInt32(result) == 1)
{
isHealthy = true;
details = "Connection to SQLite database successful and SystemUser exists.";
}
else
{
details = "Connection to SQLite database successful but SystemUser does not exist.";
}
}
catch (Exception ex)
{
details = $"Failed to connect to SQLite database: {ex.Message}";
}
return Task.FromResult(new HealthCheckResult(isHealthy, null)
{
Details = details,
Severity = isHealthy ? HealthCheckSeverity.Info : HealthCheckSeverity.Critical
});
}
}

View File

@@ -87,8 +87,15 @@ public class FileSystemImageStorageService(
{ {
throw new InvalidOperationException($"File {fileName} already exists. Use Update for updating file info."); throw new InvalidOperationException($"File {fileName} already exists. Use Update for updating file info.");
} }
using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read); using var fileStream = new FileStream(path, options: new FileStreamOptions {
Access = FileAccess.Write,
BufferSize = 4096,
Mode = FileMode.OpenOrCreate,
Share = FileShare.Read,
});
blobStream.Seek(0, SeekOrigin.Begin);
await blobStream.CopyToAsync(fileStream); await blobStream.CopyToAsync(fileStream);
blobStream.Seek(0, SeekOrigin.Begin);
} }
public async Task Update(Stream blobStream, string fileName) public async Task Update(Stream blobStream, string fileName)

View File

@@ -4,6 +4,7 @@ using Transactional.Abstractions.Interfaces;
namespace back.persistance.data.repositories.Abstracts; namespace back.persistance.data.repositories.Abstracts;
public interface IPersonRepository : IRepository<Person, string>, IScoped public interface IPersonRepository : IRepository<Person>, IScoped
{ {
} }

View File

@@ -4,5 +4,5 @@ using Transactional.Abstractions.Interfaces;
namespace back.persistance.data.repositories.Abstracts; namespace back.persistance.data.repositories.Abstracts;
public interface IPhotoRepository : IRepository<Photo, string>, IScoped public interface IPhotoRepository : IRepository<Photo>, IScoped
{ } { }

View File

@@ -4,7 +4,7 @@ using Transactional.Abstractions.Interfaces;
namespace back.persistance.data.repositories.Abstracts; namespace back.persistance.data.repositories.Abstracts;
public interface IUserRepository : IRepository<User, string>, IScoped public interface IUserRepository : IRepository<User>, IScoped
{ {
Task<User?> GetByEmail(string email); Task<User?> GetByEmail(string email);
Task<string?> GetUserSaltByEmail(string email); Task<string?> GetUserSaltByEmail(string email);

View File

@@ -4,7 +4,7 @@ using Transactional.Implementations.EntityFramework;
namespace back.persistance.data.repositories; namespace back.persistance.data.repositories;
public class PersonRepository(DataContext context) : ReadWriteRepository<Person, string>(context), IPersonRepository public class PersonRepository(DataContext context) : ReadWriteRepository<Person>(context), IPersonRepository
{ {
// Implement methods specific to Photo repository if needed // Implement methods specific to Photo repository if needed
} }

View File

@@ -4,7 +4,7 @@ using Transactional.Implementations.EntityFramework;
namespace back.persistance.data.repositories; namespace back.persistance.data.repositories;
public class PhotoRepository(DataContext context) : ReadWriteRepository<Photo, string>(context), IPhotoRepository public class PhotoRepository(DataContext context) : ReadWriteRepository<Photo>(context), IPhotoRepository
{ {
// Implement methods specific to Photo repository if needed // Implement methods specific to Photo repository if needed
} }

View File

@@ -5,14 +5,14 @@ using Transactional.Implementations.EntityFramework;
namespace back.persistance.data.repositories; namespace back.persistance.data.repositories;
public class UserRepository(DataContext context) : ReadWriteRepository<User, string>(context), IUserRepository public class UserRepository(DataContext context) : ReadWriteRepository<User>(context), IUserRepository
{ {
public async Task<User?> GetByEmail(string email) public async Task<User?> GetByEmail(string email)
{ {
try try
{ {
if (string.IsNullOrEmpty(email)) return null; if (string.IsNullOrEmpty(email)) return null;
return await Entity.FirstOrDefaultAsync(u => u.Email == email); return await Entities.FirstOrDefaultAsync(u => u.Email == email);
} }
catch catch
{ {
@@ -25,7 +25,7 @@ public class UserRepository(DataContext context) : ReadWriteRepository<User, str
try try
{ {
if (string.IsNullOrEmpty(email)) return string.Empty; if (string.IsNullOrEmpty(email)) return string.Empty;
var user = await Entity.FirstOrDefaultAsync(u => u.Email == email); var user = await Entities.FirstOrDefaultAsync(u => u.Email == email);
return user?.Salt ?? string.Empty; return user?.Salt ?? string.Empty;
} }
catch catch
@@ -39,7 +39,7 @@ public class UserRepository(DataContext context) : ReadWriteRepository<User, str
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password)) return null; if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password)) return null;
try try
{ {
return await Entity.FirstOrDefaultAsync(u => u.Email == email && u.Password == password); return await Entities.FirstOrDefaultAsync(u => u.Email == email && u.Password == password);
} }
catch catch
{ {
@@ -52,7 +52,7 @@ public class UserRepository(DataContext context) : ReadWriteRepository<User, str
try try
{ {
if (string.IsNullOrEmpty(email)) return false; if (string.IsNullOrEmpty(email)) return false;
return await Entity.AnyAsync(u => u.Email == email); return await Entities.AnyAsync(u => u.Email == email);
} }
catch catch
{ {

View File

@@ -4,13 +4,15 @@ using back.persistance.data.repositories.Abstracts;
using back.services.engine.Crypto; using back.services.engine.Crypto;
using back.services.engine.mailing; using back.services.engine.mailing;
using System.Text; using System.Text;
using System.Text.Json;
namespace back.services.bussines.UserService; namespace back.services.bussines.UserService;
public class UserService( public class UserService(
IUserRepository userRepository, ICryptoService cryptoService, IUserRepository userRepository, ICryptoService cryptoService,
IEmailService emailService, IEmailService emailService,
IBlobStorageService blobStorageService IBlobStorageService blobStorageService,
JsonSerializerOptions jsonSerializerOptions
) : IUserService ) : IUserService
{ {
private readonly IUserRepository _repository = userRepository ?? throw new ArgumentNullException(nameof(userRepository)); private readonly IUserRepository _repository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
@@ -66,6 +68,14 @@ public class UserService(
return existingUser; return existingUser;
} }
public async Task<User?> Login(string email, string decryptedPass)
{
var salt = await _repository.GetUserSaltByEmail(email);
var hashedPassword = _cryptoService.HashPassword(decryptedPass, salt);
var user = await _repository.Login(email, hashedPassword ?? string.Empty);
return user;
}
public async Task<User?> Login(string email, string password, string clientId) public async Task<User?> Login(string email, string password, string clientId)
{ {
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password)) return null; if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password)) return null;
@@ -73,9 +83,7 @@ public class UserService(
try try
{ {
var decryptedPass = _cryptoService.Decrypt(clientId, password); var decryptedPass = _cryptoService.Decrypt(clientId, password);
var salt = await _repository.GetUserSaltByEmail(email); var user = await Login(email, decryptedPass ?? string.Empty);
var hashedPassword = _cryptoService.HashPassword(decryptedPass, salt);
var user = await _repository.Login(email, hashedPassword ?? string.Empty);
return user; return user;
} }
catch catch
@@ -101,21 +109,21 @@ public class UserService(
public async Task<User?> ValidateSystemUser(string email, string password, string systemKey, string clientId) public async Task<User?> ValidateSystemUser(string email, string password, string systemKey, string clientId)
{ {
password = _cryptoService.Decrypt(clientId, password) ?? string.Empty; var decryptedPassword = _cryptoService.Decrypt(clientId, password) ?? string.Empty;
systemKey = _cryptoService.Decrypt(clientId, systemKey) ?? string.Empty; var decryptedsystemKey = _cryptoService.Decrypt(clientId, systemKey) ?? string.Empty;
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(systemKey)) if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(decryptedPassword) || string.IsNullOrEmpty(decryptedsystemKey))
{ {
return null; return null;
} }
if (!email.Equals("@system", StringComparison.InvariantCultureIgnoreCase)) if (!email.Equals(User.SystemUser.Email, StringComparison.InvariantCultureIgnoreCase))
{ {
return null; return null;
} }
var systemKeyBytes = await _blobStorageService.GetBytes("systemkey.lock"); var systemKeyBytes = await _blobStorageService.GetBytes("systemkey.lock");
var systemKeyString = Encoding.UTF8.GetString(systemKeyBytes ?? []); var systemKeyString = Encoding.UTF8.GetString(systemKeyBytes ?? []);
var systemKeyObject = System.Text.Json.JsonSerializer.Deserialize<SystemKey>(systemKeyString); var systemKeyObject = JsonSerializer.Deserialize<SystemKey>(systemKeyString, jsonSerializerOptions);
if (systemKeyObject == null || !systemKeyObject.IsValid(email, password, systemKey)) if (systemKeyObject == null || !systemKeyObject.IsValid(email, decryptedPassword, decryptedsystemKey))
{ {
return null; return null;
} }
@@ -128,6 +136,6 @@ public class UserService(
{ {
return null; return null;
} }
return await Login(user.Email!, user.Password!, clientId); return await Login(user.Email!, decryptedPassword);
} }
} }

View File

@@ -5,8 +5,8 @@ public class PasswordGenerator : IPasswordGenerator
public string Generate(int length, bool includeNumbers = true, bool includeMayus = true, bool includeMinus = true, bool includeSpecials = true) public string Generate(int length, bool includeNumbers = true, bool includeMayus = true, bool includeMinus = true, bool includeSpecials = true)
{ {
const string numbers = "0123456789"; const string numbers = "0123456789";
const string mayus = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const string mayus = "ABCÇDEFGHIJKLMNÑOPQRSTUVWXYZ";
const string minus = "abcdefghijklmnopqrstuvwxyz"; const string minus = "abcçdefghijklmnñopqrstuvwxyz";
const string specials = "!@#$%^&*()_+[]{}|;:,.<>?"; const string specials = "!@#$%^&*()_+[]{}|;:,.<>?";
var characters = minus; var characters = minus;
if (includeNumbers) characters += numbers; if (includeNumbers) characters += numbers;
@@ -19,6 +19,22 @@ public class PasswordGenerator : IPasswordGenerator
{ {
password[i] = characters[random.Next(characters.Length)]; password[i] = characters[random.Next(characters.Length)];
} }
var positionPool = new List<int>();
for (int i = 0; i < length; i++) positionPool.Add(i);
var forcedRandomNumber = random.Next(0, positionPool.Count);
positionPool.RemoveAt(forcedRandomNumber);
var forcedRandomMayus = random.Next(0, positionPool.Count);
positionPool.RemoveAt(forcedRandomMayus);
var forcedRandomMinus = random.Next(0, positionPool.Count);
positionPool.RemoveAt(forcedRandomMinus);
var forcedRandomSpecial = random.Next(0, positionPool.Count);
positionPool.RemoveAt(forcedRandomSpecial);
password[forcedRandomNumber] = numbers[random.Next(numbers.Length)];
password[forcedRandomMayus] = mayus[random.Next(mayus.Length)];
password[forcedRandomMinus] = minus[random.Next(minus.Length)];
password[forcedRandomSpecial] = specials[random.Next(specials.Length)];
return new string(password); return new string(password);
} }
} }

View File

@@ -4,12 +4,14 @@ using back.persistance.data;
using back.persistance.data.repositories.Abstracts; using back.persistance.data.repositories.Abstracts;
using back.services.engine.Crypto; using back.services.engine.Crypto;
using back.services.engine.PasswordGenerator; using back.services.engine.PasswordGenerator;
using System.Text.Json;
using Transactional.Abstractions.Interfaces; using Transactional.Abstractions.Interfaces;
namespace back.services.engine.SystemUser; namespace back.services.engine.SystemUser;
public class SystemUserGenerator( public class SystemUserGenerator(
ITransactionalService<DataContext> transactional, ITransactionalService<DataContext> transactional,
JsonSerializerOptions jsonSerializerOptions,
IUserRepository userRepository, IUserRepository userRepository,
IPersonRepository personRepository, IPersonRepository personRepository,
ICryptoService cryptoService, ICryptoService cryptoService,
@@ -21,9 +23,9 @@ public class SystemUserGenerator(
var systemKey = new SystemKey() { var systemKey = new SystemKey() {
Password = passwordGenerator.Generate(16), Password = passwordGenerator.Generate(16),
}; };
var systemKeyJson = System.Text.Json.JsonSerializer.Serialize(systemKey); var systemKeyJson = JsonSerializer.Serialize(systemKey, options: jsonSerializerOptions);
using Stream stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(systemKeyJson)); using Stream stream = new MemoryStream(new System.Text.UTF8Encoding(true).GetBytes(systemKeyJson));
await blobStorageService.Delete("systemkey.lock"); await blobStorageService.Delete("systemkey.lock");
@@ -38,10 +40,16 @@ public class SystemUserGenerator(
if (!await userRepository.Exists(User.SystemUser.Id!)) if (!await userRepository.Exists(User.SystemUser.Id!))
{ {
await transactional.DoTransaction(async () => { await transactional.DoTransaction(async () =>
{
await personRepository.Insert(Person.SystemPerson); await personRepository.Insert(Person.SystemPerson);
await userRepository.Insert(User.SystemUser); await userRepository.Insert(User.SystemUser);
}); });
} }
else
{
await userRepository.Update(User.SystemUser);
await userRepository.SaveChanges();
}
} }
} }

View File

@@ -1,20 +1,38 @@
{ {
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "ng serve", "name": "FRONT: DEBUG(Edge)",
"type": "chrome",
"request": "launch", "request": "launch",
"preLaunchTask": "npm: start", "type": "msedge",
"url": "http://localhost:4200/" "url": "http://localhost:4200",
"webRoot": "${workspaceFolder}",
"preLaunchTask": "Start Node server with nvs latest"
}, },
{ {
"name": "ng test", "name": "Attach Edge",
"type": "chrome", "type": "msedge",
"request": "attach",
"url": "http://localhost:4200/#",
"webRoot": "${workspaceFolder}"
},
{
"name": "Launch Edge (Test)",
"type": "msedge",
"request": "launch", "request": "launch",
"preLaunchTask": "npm: test", "url": "http://localhost:9876/debug.html",
"url": "http://localhost:9876/debug.html" "webRoot": "${workspaceFolder}"
},
{
"name": "Launch Edge (E2E)",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/protractor/bin/protractor",
"protocol": "inspector",
"args": ["${workspaceFolder}/protractor.conf.js"]
} }
] ]
} }

View File

@@ -1,41 +1,20 @@
{ {
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"type": "npm", "label": "Start Node server with nvs latest",
"script": "start", "type": "shell",
"command": "nvs use latest && npm run start",
"options": {
"cwd": "${workspaceFolder}"
},
"isBackground": true, "isBackground": true,
"problemMatcher": { "problemMatcher": [],
"owner": "typescript", "presentation": {
"pattern": "$tsc", "echo": true,
"background": { "reveal": "always",
"activeOnStart": true, "focus": false,
"beginsPattern": { "panel": "shared"
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
} }
} }
] ]

View File

@@ -5,15 +5,15 @@
<events-link></events-link> <events-link></events-link>
<tags-link></tags-link> <tags-link></tags-link>
<services-link></services-link> <services-link></services-link>
@if (user.isLoggedIn) { @if (currentUser().isLoggedIn) {
<user-galleries-link></user-galleries-link> <user-galleries-link></user-galleries-link>
} @if (user.isContentManager) { } @if (currentUser().isContentManager) {
<content-manager-panel-link></content-manager-panel-link> <content-manager-panel-link></content-manager-panel-link>
} @if (user.isAdmin) { } @if (currentUser().isAdmin) {
<admin-panel-link></admin-panel-link> <admin-panel-link></admin-panel-link>
} }
<div class="user-profile"> <div class="user-profile">
@if (user.isLoggedIn) { @if (currentUser().isLoggedIn) {
<user-profile-link></user-profile-link> <user-profile-link></user-profile-link>
} @else { } @else {
<login-link></login-link> <login-link></login-link>

View File

@@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core'; import { Component, inject, signal } from '@angular/core';
import { Logo } from '../logo/logo'; import { Logo } from '../logo/logo';
import { EventsLink } from '../events-link/events-link'; import { EventsLink } from '../events-link/events-link';
import { UserGalleriesLink } from '../user-galleries-link/user-galleries-link'; import { UserGalleriesLink } from '../user-galleries-link/user-galleries-link';
@@ -35,12 +35,12 @@ export class Header implements OnInit {
constructor(private userService: userService) {} constructor(private userService: userService) {}
private currentUser: userModel = userModel.DefaultUser; currentUser = signal<userModel>(userModel.DefaultUser);
hideButtons = false; hideButtons = false;
ngOnInit() { ngOnInit() {
this.userService.getUser().subscribe((user) => { this.userService.getUser().subscribe((user) => {
this.currentUser = user; this.currentUser.set(user);
}); });
// Escucha cambios de ruta // Escucha cambios de ruta
@@ -51,8 +51,4 @@ export class Header implements OnInit {
} }
}); });
} }
get user() {
return this.currentUser;
}
} }

View File

@@ -30,18 +30,21 @@ export class userService {
if (email == null || password == null || systemKey == null) { if (email == null || password == null || systemKey == null) {
return; return;
} }
const encryptedPassword = this.cryptoService.encryptData(password); const encryptedPassword = await this.cryptoService.encryptData(password);
const encryptedSystemKey = this.cryptoService.encryptData(systemKey); const encryptedSystemKey = await this.cryptoService.encryptData(systemKey);
const response = await axios.post('/users/login', { return axios
email, .post('/users/login', {
password: await encryptedPassword, email,
systemKey: await encryptedSystemKey, password: encryptedPassword,
}); systemKey: encryptedSystemKey,
const { jwt, refresh, usermodel } = response.data; })
localStorage.setItem('jwt', jwt); .then((response) => {
localStorage.setItem('refresh', refresh); const { jwt, refresh, usermodel } = response.data;
this.setUser(usermodel); localStorage.setItem('jwt', jwt);
return usermodel; localStorage.setItem('refresh', refresh);
this.setUser(usermodel);
return usermodel;
});
} }
async login( async login(

View File

@@ -80,7 +80,7 @@ export class LoginView {
isLoginSystem(): boolean { isLoginSystem(): boolean {
const emailValue = this.email?.value; const emailValue = this.email?.value;
if (emailValue && emailValue == '@system') { if (emailValue && emailValue == 'sys@t.em') {
return true; return true;
} }
return false; return false;
@@ -119,19 +119,16 @@ export class LoginView {
}, 1000); }, 1000);
} }
onSubmit() { async onSubmit() {
const email = this.loginForm.value.email; const email = this.loginForm.value.email;
const password = this.loginForm.value.password; const password = this.loginForm.value.password;
if (this.isLoginSystem()) { if (this.isLoginSystem()) {
const systemKey = this.systemKey?.value; const systemKey = this.systemKey?.value;
from(this.userService.systemLogin(email, password, systemKey)).subscribe({ await this.userService
next: (user) => { .systemLogin(email, password, systemKey)
.finally(() => {
this.router.navigate(['/']); this.router.navigate(['/']);
}, });
error: (error) => {
this.router.navigate(['/']);
},
});
} else if (this.loginForm.valid) { } else if (this.loginForm.valid) {
this.entrando.set(true); this.entrando.set(true);
from(this.userService.login(email, password)).subscribe({ from(this.userService.login(email, password)).subscribe({

View File

@@ -54,17 +54,6 @@ class userModel extends personModel {
new Date(), new Date(),
false false
); );
public static readonly TestAdminUser: userModel = new userModel(
'-1',
'testadmin@example.com',
'password',
'Test Admin User',
[roleModel.AdminRole],
new Date(),
new Date(),
true
);
} }
export { SigningMethods, userModel }; export { SigningMethods, userModel };

View File

@@ -1,8 +1,8 @@
@use "./styles/variables"; @use "./styles/variables";
@use "./styles/buttons"; @use "./styles/buttons";
@use "./styles/inputs"; @use "./styles/inputs";
@use "./styles/themes/light"; // @use "./styles/themes/light";
@use "./styles/themes/dark"; // @use "./styles/themes/dark";
@use "./styles/loaders"; @use "./styles/loaders";
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */