From 050cf9af0fa6bd58150dfc84fe6933bb861ca63a Mon Sep 17 00:00:00 2001 From: Leandro Hernan Rojas Date: Tue, 29 Apr 2025 18:52:49 -0300 Subject: [PATCH] Update Usuarios Extended Fields --- .../Controllers/AuthenticationController.cs | 243 ++++++++------ ...29190347_ExtendApplicationUser.Designer.cs | 302 ++++++++++++++++++ .../20250429190347_ExtendApplicationUser.cs | 112 +++++++ .../phronCareDbContextModelSnapshot.cs | 179 ++++++----- .../Authentication/SingUp/RegisterUser.cs | 32 +- .../Models/Seurity/ApplicationUser.cs | 20 ++ phronCare.API/Models/phronCareDbContext.cs | 41 ++- phronCare.API/Program.cs | 199 ++++++------ phronCare.API/phronCare.API.csproj | 2 +- 9 files changed, 843 insertions(+), 287 deletions(-) create mode 100644 phronCare.API/Migrations/20250429190347_ExtendApplicationUser.Designer.cs create mode 100644 phronCare.API/Migrations/20250429190347_ExtendApplicationUser.cs create mode 100644 phronCare.API/Models/Seurity/ApplicationUser.cs diff --git a/phronCare.API/Controllers/AuthenticationController.cs b/phronCare.API/Controllers/AuthenticationController.cs index 2ddaec7..22ccf38 100644 --- a/phronCare.API/Controllers/AuthenticationController.cs +++ b/phronCare.API/Controllers/AuthenticationController.cs @@ -10,6 +10,7 @@ using Services.Models; using Services.Interfaces; using phronCare.API.Models.Authentication.Login; using phronCare.API.Models.Authentication.SingUp; +using phronCare.API.Models.Security; using Google.Authenticator; using QRCoder; using System.ComponentModel.DataAnnotations; @@ -18,51 +19,52 @@ namespace phronCare.API.Controllers { [Route("api/[controller]")] [ApiController] - public class AuthenticationController(UserManager userManager, RoleManager roleManager, IEmailService emailService, IConfiguration configuration, SignInManager signInManager, TwoFactorAuthenticator twoFactorAuthenticator) : ControllerBase + public class AuthenticationController( + UserManager userManager, + RoleManager roleManager, + IEmailService emailService, + IConfiguration configuration, + SignInManager signInManager, + TwoFactorAuthenticator twoFactorAuthenticator) : ControllerBase { private const int JWT_TOKEN_VALIDITY_HOURS = 48; #region Declaration Section - private readonly UserManager userManager = userManager; + private readonly UserManager userManager = userManager; private readonly RoleManager roleManager = roleManager; - private readonly SignInManager signInManager = signInManager; + private readonly SignInManager signInManager = signInManager; private readonly TwoFactorAuthenticator authenticator = twoFactorAuthenticator; private readonly IEmailService emailService = emailService; private readonly IConfiguration configuration = configuration; - #endregion - #region Segurity Endpoints + #region Security Endpoints + [HttpPost] [Route("generate-qr-code")] public async Task GenerateQRCodeAsync() { try { - var user = await signInManager.GetTwoFactorAuthenticationUserAsync(); // Obtén el nombre de usuario actual - + var user = await signInManager.GetTwoFactorAuthenticationUserAsync(); if (user == null) { return BadRequest(new Response { Status = "Error", Message = "Usuario no autenticado." }); } - var setupInfo = authenticator.GenerateSetupCode("MiApp", user.Email, user.Id, false, 10); - // Genera el código QR como una imagen + var setupInfo = authenticator.GenerateSetupCode("MiApp", user.Email, user.Id, false, 10); var qrCodeGenerator = new QRCodeGenerator(); var qrCodeData = qrCodeGenerator.CreateQrCode(setupInfo.ManualEntryKey, QRCodeGenerator.ECCLevel.Q); var qrCode = new PngByteQRCode(qrCodeData); - - // Convierte la imagen del código QR en bytes - var qrCodeImage = qrCode.GetGraphic(10); // Ajusta el tamaño según tus necesidades + var qrCodeImage = qrCode.GetGraphic(10); var qrCodeImageData = Convert.ToBase64String(qrCodeImage); - // Devuelve el userId (clave secreta) y la imagen del código QR en la respuesta return Ok(new AuthResponse { Status = "Success", Message = "Código QR generado satisfactoriamente.", - ManualSetupKey = setupInfo.ManualEntryKey, // Proporciona esto al usuario para configuración manual en Google Authenticator - QRCodeImage = qrCodeImageData // Devuelve la imagen del código QR + ManualSetupKey = setupInfo.ManualEntryKey, + QRCodeImage = qrCodeImageData }); } catch (Exception) @@ -75,23 +77,25 @@ namespace phronCare.API.Controllers [Route("register")] public async Task Register([FromBody] RegisterUser registerUser) { - //Chequeo Correo existente var userExist = await userManager.FindByEmailAsync(registerUser.EmailAddress); if (userExist != null) - { - return StatusCode(StatusCodes.Status409Conflict, "El correo ingresado ya esta en uso."); - } + return StatusCode(StatusCodes.Status409Conflict, "El correo ingresado ya está en uso."); + userExist = await userManager.FindByNameAsync(registerUser.UserName); if (userExist != null) - { return StatusCode(StatusCodes.Status409Conflict, "El usuario ingresado ya existe."); - } - IdentityUser user = new() + var user = new ApplicationUser { Email = registerUser.EmailAddress, + UserName = registerUser.UserName, SecurityStamp = Guid.NewGuid().ToString(), - UserName = registerUser.UserName + FirstName = registerUser.FirstName, + LastName = registerUser.LastName, + Address = registerUser.Address, + Department = registerUser.Department, + CompanyName = registerUser.CompanyName, + BirthDate = registerUser.BirthDate }; if (await roleManager.RoleExistsAsync(registerUser.Role)) @@ -99,22 +103,20 @@ namespace phronCare.API.Controllers var result = await userManager.CreateAsync(user, registerUser.Password); if (!result.Succeeded) { - return StatusCode(StatusCodes.Status500InternalServerError, $"Creacion de usuario fallida: {result}"); + return StatusCode(StatusCodes.Status500InternalServerError, $"Creación de usuario fallida: {string.Join(", ", result.Errors.Select(e => e.Description))}"); } + await userManager.AddToRoleAsync(user, registerUser.Role); - //Add Token to Verify the email var token = await userManager.GenerateEmailConfirmationTokenAsync(user); var confirmationLink = Url.Action(nameof(ConfirmEmail), "Authentication", new { token, email = user.Email }, Request.Scheme); - var message = new Message([user.Email!], "phronCare - Link de verificacion", confirmationLink!); + var message = new Message(new[] { user.Email! }, "phronCare - Link de verificación", confirmationLink!); emailService.SendEmail(message); - return StatusCode(StatusCodes.Status201Created, $"Usuario creado satisfactoriamente. Debe autenticar la cuenta generada desde la direccion de correo registrada: {user.Email}"); - } - else - { - return StatusCode(StatusCodes.Status500InternalServerError, "El rol no existe. Intentalo nuevamente!"); + return StatusCode(StatusCodes.Status201Created, $"Usuario creado satisfactoriamente. Debe autenticar la cuenta desde su correo: {user.Email}"); } + + return StatusCode(StatusCodes.Status500InternalServerError, "El rol no existe. ¡Inténtalo nuevamente!"); } [HttpGet("confirmemail")] @@ -125,13 +127,9 @@ namespace phronCare.API.Controllers { var result = await userManager.ConfirmEmailAsync(user, token); if (result.Succeeded) - { - return StatusCode(StatusCodes.Status200OK, - new Response { Status = "Success", Message = "Email verificado satisfactoriamente" }); - } + return Ok(new Response { Status = "Success", Message = "Email verificado satisfactoriamente." }); } - return StatusCode(StatusCodes.Status409Conflict, - new Response { Status = "Error", Message = "Este usuario no existe" }); + return Conflict(new Response { Status = "Error", Message = "Este usuario no existe." }); } [HttpPost] @@ -141,9 +139,7 @@ namespace phronCare.API.Controllers await signInManager.SignOutAsync(); var user = await userManager.FindByNameAsync(loginModel.Username); if (user is null) - { - return Unauthorized("El nombre de usuario o contraseña son incorrectos"); - }; + return Unauthorized("El nombre de usuario o contraseña son incorrectos."); var canSignIn = await signInManager.CanSignInAsync(user); if (!canSignIn) @@ -151,26 +147,24 @@ namespace phronCare.API.Controllers string message = string.Empty; if (userManager.Options.SignIn.RequireConfirmedEmail && !(await userManager.IsEmailConfirmedAsync(user))) { - message = "El usuario {" + user.UserName + "} no puede iniciar sesion sin tener el email confirmado."; + message = $"El usuario {user.UserName} no puede iniciar sesión sin tener el email confirmado."; } if (userManager.Options.SignIn.RequireConfirmedPhoneNumber && !(await userManager.IsPhoneNumberConfirmedAsync(user))) { - message = "El usuario {" + user.UserName + "} no puede iniciar sesion sin tener confirmado el numero de telefono."; + message = $"El usuario {user.UserName} no puede iniciar sesión sin tener confirmado el número de teléfono."; } - return StatusCode(StatusCodes.Status401Unauthorized, message); + return Unauthorized(message); } - if (user != null && await userManager.CheckPasswordAsync(user, loginModel.Password)) + + if (await userManager.CheckPasswordAsync(user, loginModel.Password)) { if (!user.TwoFactorEnabled) - { return await GenerateAccess(user); - } - else - { - return StatusCode(StatusCodes.Status202Accepted, new Response { Status = "Success", Message = $"Debe ingresar el codigo desde la app Google Authenticator : {user.UserName}" }); - }; + + return Accepted(new Response { Status = "Success", Message = $"Debe ingresar el código desde la app Google Authenticator: {user.UserName}" }); } - return Unauthorized("El nombre de usuario o contraseña son incorrectos"); + + return Unauthorized("El nombre de usuario o contraseña son incorrectos."); } [HttpPost] @@ -180,21 +174,17 @@ namespace phronCare.API.Controllers var user = await userManager.FindByNameAsync(username); if (user is not null) { - string secretKey = user.Id; // Obtén la clave secreta del usuario desde tu base de datos - bool isBase32 = false; // Define si la clave secreta está en formato Base32 - bool validate = authenticator.ValidateTwoFactorPIN(secretKey, code, isBase32); + bool validate = authenticator.ValidateTwoFactorPIN(user.Id, code); if (validate) - { return await GenerateAccess(user); - } } - return StatusCode(StatusCodes.Status401Unauthorized, new Response { Status = "Error", Message = "Código de verificación incorrecto." }); + return Unauthorized(new Response { Status = "Error", Message = "Código de verificación incorrecto." }); } [HttpPost] [AllowAnonymous] [Route("forgot-password")] - public async Task ForgotPassword([Required]string email) + public async Task ForgotPassword([Required] string email) { var user = await userManager.FindByEmailAsync(email); if (user != null) @@ -202,27 +192,21 @@ namespace phronCare.API.Controllers var token = await userManager.GeneratePasswordResetTokenAsync(user); var forgotPasswordLink = Url.Action(nameof(ResetPassword), "Authentication", new { token, email = user.Email }, Request.Scheme); - var message = new Message([user.Email!], "phronCare - Enlace de recuperacion de contraseña.", forgotPasswordLink!); + var message = new Message(new[] { user.Email! }, "phronCare - Enlace de recuperación de contraseña", forgotPasswordLink!); emailService.SendEmail(message); - return StatusCode(StatusCodes.Status200OK, $"La solicitud de cambio de contraseña se envio a: {user.Email} con éxito. Por favor verifique el enlace enviado a la casilla de correo."); + return Ok($"La solicitud de cambio de contraseña se envió a: {user.Email} exitosamente."); } - return StatusCode(StatusCodes.Status400BadRequest, "No se pudo enviar el enlace al correo, por favor intente nuevamente."); + return BadRequest("No se pudo enviar el enlace al correo, por favor intente nuevamente."); } [HttpGet("reset-password")] - public async Task ResetPassword(string token, string email) + public IActionResult ResetPassword(string token, string email) { - //var model =new ResetPassword {Token=token, Email = email}; - var htmlContent = GetResetPasswordHtmlContent(email,token); + var htmlContent = GetResetPasswordHtmlContent(email, token); var bytes = Encoding.UTF8.GetBytes(htmlContent); var stream = new MemoryStream(bytes); return File(stream, "text/html"); } - private static string GetResetPasswordHtmlContent(string username, string token) - { - string htmlContent = @"phronCare - Notificación

phronCare XL: Notificación

" + username + "

Token

Tu solicitud de restablecimiento de contraseña ha sido confirmada

Para restablecer tu contraseña en phronCare, ingrese las credenciales proporcionadas.

"; - return htmlContent; - } [HttpPost] [AllowAnonymous] @@ -237,37 +221,100 @@ namespace phronCare.API.Controllers { return StatusCode(StatusCodes.Status500InternalServerError, resetPassResult.Errors.First().Description); } - return StatusCode(StatusCodes.Status200OK,$"La password ha sido cambiada correctamente."); + return Ok("La contraseña ha sido cambiada correctamente."); } - return StatusCode(StatusCodes.Status500InternalServerError,"No se pudo encontrar el usuario, por favor intente nuevamente."); + return StatusCode(StatusCodes.Status500InternalServerError, "No se pudo encontrar el usuario, por favor intente nuevamente."); + } + + private static string GetResetPasswordHtmlContent(string username, string token) + { + return @$" + + + + phronCare - Notificación +

Usuario: {username}

Token: {token}

+ "; } #endregion + #region GenerateAccess - private async Task GenerateAccess(IdentityUser? user) + //private async Task GenerateAccess(ApplicationUser user) + //{ + // try + // { + // var authClaims = new List + // { + // new(ClaimTypes.Name, user.UserName), + // new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + // }; + + // var userRoles = await userManager.GetRolesAsync(user); + // foreach (var role in userRoles) + // { + // authClaims.Add(new Claim(ClaimTypes.Role, role)); + // } + + // var jwtToken = GetToken(authClaims); + + // var userSession = new UserSession + // { + // UserName = user.UserName, + // Role = userRoles.First(), + // Token = new JwtSecurityTokenHandler().WriteToken(jwtToken), + // ExpiresIn = (int)jwtToken.ValidTo.Subtract(DateTime.Now).TotalSeconds, + // ExpiryTimeStamp = jwtToken.ValidTo + // }; + + // return Ok(userSession); + // } + // catch (Exception ex) + // { + // return BadRequest(ex.Message); + // } + //} + private async Task GenerateAccess(ApplicationUser user) { - var authClaims = new List + try { - new(ClaimTypes.Name, user.UserName), - new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), - }; - var userRoles = await userManager.GetRolesAsync(user); - foreach (var role in userRoles) - { - authClaims.Add(new Claim(ClaimTypes.Role, role)); + var authClaims = new List + { + new(ClaimTypes.Name, user.UserName), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + + var userRoles = await userManager.GetRolesAsync(user); + + if (!userRoles.Any()) + { + return BadRequest("El usuario no tiene ningún rol asignado. Verifica el proceso de registro."); + } + + foreach (var role in userRoles) + { + authClaims.Add(new Claim(ClaimTypes.Role, role)); + } + + var jwtToken = GetToken(authClaims); + + var userSession = new UserSession + { + UserName = user.UserName, + Role = userRoles.First(), // ya validado que hay al menos uno + Token = new JwtSecurityTokenHandler().WriteToken(jwtToken), + ExpiresIn = (int)jwtToken.ValidTo.Subtract(DateTime.Now).TotalSeconds, + ExpiryTimeStamp = jwtToken.ValidTo + }; + + return Ok(userSession); } - - var jwtToken = GetToken(authClaims); - - var userSession = new UserSession + catch (Exception ex) { - UserName = user.UserName, - Role = userRoles.First(), - Token = new JwtSecurityTokenHandler().WriteToken(jwtToken), - ExpiresIn = (int)jwtToken.ValidTo.Subtract(DateTime.Now).TotalSeconds, - ExpiryTimeStamp = jwtToken.ValidTo - }; - return Ok(userSession); + // O podés loguearlo con logger si querés + return StatusCode(StatusCodes.Status500InternalServerError, $"Error generando token: {ex.Message}"); + } } + public class UserSession { public string UserName { get; set; } @@ -277,31 +324,25 @@ namespace phronCare.API.Controllers public DateTime ExpiryTimeStamp { get; set; } } #endregion + #region GenerateToken private JwtSecurityToken GetToken(List authClaims) { var secret = configuration["JWT:Secret"]; - if (string.IsNullOrWhiteSpace(secret)) throw new InvalidOperationException("El Secret no está configurado."); - // Convertir explícitamente a bytes - var keyBytes = Encoding.UTF8.GetBytes(secret); - - var authSigningKey = new SymmetricSecurityKey(keyBytes); - + var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); var credentials = new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256); - var token = new JwtSecurityToken( + return new JwtSecurityToken( issuer: configuration["JWT:ValidIssuer"], audience: configuration["JWT:ValidAudience"], expires: DateTime.UtcNow.AddHours(JWT_TOKEN_VALIDITY_HOURS), claims: authClaims, signingCredentials: credentials ); - - return token; } #endregion } -} \ No newline at end of file +} diff --git a/phronCare.API/Migrations/20250429190347_ExtendApplicationUser.Designer.cs b/phronCare.API/Migrations/20250429190347_ExtendApplicationUser.Designer.cs new file mode 100644 index 0000000..0c6f362 --- /dev/null +++ b/phronCare.API/Migrations/20250429190347_ExtendApplicationUser.Designer.cs @@ -0,0 +1,302 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using phronCare.API.Models; + +#nullable disable + +namespace phronCare.API.Migrations +{ + [DbContext(typeof(phronCareDbContext))] + [Migration("20250429190347_ExtendApplicationUser")] + partial class ExtendApplicationUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("phronCare.API.Models.Security.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("BirthDate") + .HasColumnType("datetime2"); + + b.Property("CompanyName") + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Department") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("ProfileImageUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("phronCare.API.Models.Security.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("phronCare.API.Models.Security.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("phronCare.API.Models.Security.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("phronCare.API.Models.Security.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/phronCare.API/Migrations/20250429190347_ExtendApplicationUser.cs b/phronCare.API/Migrations/20250429190347_ExtendApplicationUser.cs new file mode 100644 index 0000000..1a20f88 --- /dev/null +++ b/phronCare.API/Migrations/20250429190347_ExtendApplicationUser.cs @@ -0,0 +1,112 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace phronCare.API.Migrations +{ + /// + public partial class ExtendApplicationUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: "a36da698-f5bc-4af3-bc25-81c675d52914"); + + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: "ee45c015-8a59-469d-b2ef-1019d9ab246b"); + + migrationBuilder.AddColumn( + name: "Address", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "BirthDate", + table: "AspNetUsers", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "CompanyName", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "Department", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "FirstName", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "LastName", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "ProfileImageUrl", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Address", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "BirthDate", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "CompanyName", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "Department", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "FirstName", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "LastName", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "ProfileImageUrl", + table: "AspNetUsers"); + + migrationBuilder.InsertData( + table: "AspNetRoles", + columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, + values: new object[,] + { + { "a36da698-f5bc-4af3-bc25-81c675d52914", "1/7/2023 00:00:00", "Administrator", "ADMINISTRATOR" }, + { "ee45c015-8a59-469d-b2ef-1019d9ab246b", "1/7/2023 00:00:00", "User", "User" } + }); + } + } +} diff --git a/phronCare.API/Migrations/phronCareDbContextModelSnapshot.cs b/phronCare.API/Migrations/phronCareDbContextModelSnapshot.cs index 03b151f..bafc77f 100644 --- a/phronCare.API/Migrations/phronCareDbContextModelSnapshot.cs +++ b/phronCare.API/Migrations/phronCareDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace phronCare.API.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.8") + .HasAnnotation("ProductVersion", "8.0.10") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -47,22 +47,6 @@ namespace phronCare.API.Migrations .HasFilter("[NormalizedName] IS NOT NULL"); b.ToTable("AspNetRoles", (string)null); - - b.HasData( - new - { - Id = "a36da698-f5bc-4af3-bc25-81c675d52914", - ConcurrencyStamp = "1/7/2023 00:00:00", - Name = "Administrator", - NormalizedName = "ADMINISTRATOR" - }, - new - { - Id = "ee45c015-8a59-469d-b2ef-1019d9ab246b", - ConcurrencyStamp = "1/7/2023 00:00:00", - Name = "User", - NormalizedName = "User" - }); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => @@ -90,71 +74,6 @@ namespace phronCare.API.Migrations b.ToTable("AspNetRoleClaims", (string)null); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => - { - b.Property("Id") - .HasColumnType("nvarchar(450)"); - - b.Property("AccessFailedCount") - .HasColumnType("int"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("nvarchar(max)"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); - - b.Property("EmailConfirmed") - .HasColumnType("bit"); - - b.Property("LockoutEnabled") - .HasColumnType("bit"); - - b.Property("LockoutEnd") - .HasColumnType("datetimeoffset"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); - - b.Property("PasswordHash") - .HasColumnType("nvarchar(max)"); - - b.Property("PhoneNumber") - .HasColumnType("nvarchar(max)"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("bit"); - - b.Property("SecurityStamp") - .HasColumnType("nvarchar(max)"); - - b.Property("TwoFactorEnabled") - .HasColumnType("bit"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex") - .HasFilter("[NormalizedUserName] IS NOT NULL"); - - b.ToTable("AspNetUsers", (string)null); - }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { b.Property("Id") @@ -236,6 +155,94 @@ namespace phronCare.API.Migrations b.ToTable("AspNetUserTokens", (string)null); }); + modelBuilder.Entity("phronCare.API.Models.Security.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("BirthDate") + .HasColumnType("datetime2"); + + b.Property("CompanyName") + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Department") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("ProfileImageUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) @@ -247,7 +254,7 @@ namespace phronCare.API.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + b.HasOne("phronCare.API.Models.Security.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -256,7 +263,7 @@ namespace phronCare.API.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + b.HasOne("phronCare.API.Models.Security.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -271,7 +278,7 @@ namespace phronCare.API.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + b.HasOne("phronCare.API.Models.Security.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -280,7 +287,7 @@ namespace phronCare.API.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + b.HasOne("phronCare.API.Models.Security.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) diff --git a/phronCare.API/Models/Authentication/SingUp/RegisterUser.cs b/phronCare.API/Models/Authentication/SingUp/RegisterUser.cs index 0e2f965..3dc6329 100644 --- a/phronCare.API/Models/Authentication/SingUp/RegisterUser.cs +++ b/phronCare.API/Models/Authentication/SingUp/RegisterUser.cs @@ -4,9 +4,33 @@ namespace phronCare.API.Models.Authentication.SingUp { public class RegisterUser { - [Required(ErrorMessage ="El Username es un dato obligatorio")] public string UserName { get; set; } = string.Empty; - [EmailAddress,Required(ErrorMessage = "El correo electronico es un dato obligatorio")] public string EmailAddress { get; set;} = string.Empty; - [Required(ErrorMessage = "La contraseña es un dato obligatorio")] public string Password { get; set; } = string.Empty; - [Required(ErrorMessage = "El rol es un dato obligatorio")] public string Role { get; set; } = string.Empty; + [Required(ErrorMessage = "El Username es un dato obligatorio")] + public string UserName { get; set; } = string.Empty; + + [Required(ErrorMessage = "El correo electrónico es un dato obligatorio")] + [EmailAddress] + public string EmailAddress { get; set; } = string.Empty; + + [Required(ErrorMessage = "La contraseña es un dato obligatorio")] + public string Password { get; set; } = string.Empty; + + [Required(ErrorMessage = "El rol es un dato obligatorio")] + public string Role { get; set; } = string.Empty; + + [Required(ErrorMessage = "El nombre es obligatorio")] + public string FirstName { get; set; } = string.Empty; + + [Required(ErrorMessage = "El apellido es obligatorio")] + public string LastName { get; set; } = string.Empty; + + public string? Address { get; set; } + public string? Department { get; set; } + public string? CompanyName { get; set; } + + [DataType(DataType.Date)] + public DateTime? BirthDate { get; set; } + + // Podés dejar esto para el futuro + public string? ProfileImageUrl { get; set; } } } diff --git a/phronCare.API/Models/Seurity/ApplicationUser.cs b/phronCare.API/Models/Seurity/ApplicationUser.cs new file mode 100644 index 0000000..7a7fe8a --- /dev/null +++ b/phronCare.API/Models/Seurity/ApplicationUser.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Identity; + +namespace phronCare.API.Models.Security +{ + public class ApplicationUser : IdentityUser + { + public string FirstName { get; set; } + public string LastName { get; set; } + public string? Address { get; set; } + public string? Department { get; set; } + public string? CompanyName { get; set; } + public DateTime? BirthDate { get; set; } + + // Nuevo campo para imagen + public string? ProfileImageUrl { get; set; } + + // Propiedad calculada + public string FullName => $"{FirstName} {LastName}"; + } +} diff --git a/phronCare.API/Models/phronCareDbContext.cs b/phronCare.API/Models/phronCareDbContext.cs index b37c509..cbc1b58 100644 --- a/phronCare.API/Models/phronCareDbContext.cs +++ b/phronCare.API/Models/phronCareDbContext.cs @@ -1,26 +1,59 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using phronCare.API.Models.Security; namespace phronCare.API.Models { - public class phronCareDbContext: IdentityDbContext + public class phronCareDbContext : IdentityDbContext { - public phronCareDbContext(DbContextOptions options):base(options) + public phronCareDbContext(DbContextOptions options) : base(options) { } + protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); //SeedRoles(builder); } + private static void SeedRoles(ModelBuilder modelBuilder) { modelBuilder.Entity().HasData - ( + ( new IdentityRole() { Name = "Administrator", ConcurrencyStamp = DateTime.Today.ToString(), NormalizedName = "administrator" }, new IdentityRole() { Name = "User", ConcurrencyStamp = DateTime.Today.ToString(), NormalizedName = "user" } - ); + ); } } } + + + + +//using Microsoft.AspNetCore.Identity; +//using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +//using Microsoft.EntityFrameworkCore; + +//namespace phronCare.API.Models +//{ +// public class phronCareDbContext: IdentityDbContext +// { +// public phronCareDbContext(DbContextOptions options):base(options) +// { +// } +// protected override void OnModelCreating(ModelBuilder builder) +// { +// base.OnModelCreating(builder); +// //SeedRoles(builder); +// } +// private static void SeedRoles(ModelBuilder modelBuilder) +// { +// modelBuilder.Entity().HasData +// ( +// new IdentityRole() { Name = "Administrator", ConcurrencyStamp = DateTime.Today.ToString(), NormalizedName = "administrator" }, +// new IdentityRole() { Name = "User", ConcurrencyStamp = DateTime.Today.ToString(), NormalizedName = "user" } +// ); +// } +// } +//} diff --git a/phronCare.API/Program.cs b/phronCare.API/Program.cs index 95bdcaa..347b483 100644 --- a/phronCare.API/Program.cs +++ b/phronCare.API/Program.cs @@ -1,22 +1,23 @@ -using Core.Interfaces; -using Core.Services; using Google.Authenticator; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; +using Models.Repositories; using Models.Interfaces; using Models.Models; -using Models.Repositories; -using phronCare.API.Models; -using Services.Models; -using Services.Services; using Services.Interfaces; +using Services.Services; +using Services.Models; using System.Text; using Infrastructure.Repositories.Patients; -using PhronCare.Core.Services.Sales; using PhronCare.Core.Data.Repositories.Sales; +using PhronCare.Core.Services.Sales; +using phronCare.API.Models.Security; +using phronCare.API.Models; +using Core.Interfaces; +using Core.Services; var builder = WebApplication.CreateBuilder(args); @@ -34,54 +35,7 @@ builder.Services.AddDbContext(options => #endregion #region Repositorios y Servicios -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +RepositorysAndServices(builder); #endregion @@ -90,49 +44,60 @@ builder.Services.Configure(opts => opts.SignIn.RequireConfirmed #endregion #region Security Identity EF Configuration -builder.Services.AddIdentity() +builder.Services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); -builder.Services.Configure( opts => opts.TokenLifespan=TimeSpan.FromHours(10)); + +builder.Services.Configure(opts => + opts.TokenLifespan = TimeSpan.FromHours(10)); + builder.Services.AddSingleton(); #endregion +//#region Security Identity EF Configuration +//builder.Services.AddIdentity() +// .AddEntityFrameworkStores() +// .AddDefaultTokenProviders(); +//builder.Services.Configure( opts => opts.TokenLifespan=TimeSpan.FromHours(10)); +//builder.Services.AddSingleton(); +//#endregion + #region Authentication Service builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; +}).AddJwtBearer(options => +{ + options.Audience = configuration["JWT:ValidAudience"]; + options.RequireHttpsMetadata = false; + options.SaveToken = true; + options.IncludeErrorDetails = true; + options.TokenValidationParameters = new TokenValidationParameters { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; - }).AddJwtBearer(options => - { - options.Audience = configuration["JWT:ValidAudience"]; - options.RequireHttpsMetadata = false; - options.SaveToken = true; - options.IncludeErrorDetails = true; - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidateAudience = true, - ValidateIssuerSigningKey = true, - ValidateLifetime = true, - ValidIssuer = configuration["JWT:ValidIssuer"], - ValidAudience = configuration["JWT:ValidAudience"], - IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration["JWT:Secret"])), - ClockSkew = TimeSpan.Zero - }; + ValidateIssuer = true, + ValidateAudience = true, + ValidateIssuerSigningKey = true, + ValidateLifetime = true, + ValidIssuer = configuration["JWT:ValidIssuer"], + ValidAudience = configuration["JWT:ValidAudience"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration["JWT:Secret"])), + ClockSkew = TimeSpan.Zero + }; - options.Events = new JwtBearerEvents + options.Events = new JwtBearerEvents + { + OnAuthenticationFailed = context => { - OnAuthenticationFailed = context => - { - var logger = context.HttpContext.RequestServices.GetRequiredService().CreateLogger("JWTDebug"); - logger.LogError(context.Exception, "Error de autenticación JWT"); - return Task.CompletedTask; - } - }; - }); + var logger = context.HttpContext.RequestServices.GetRequiredService().CreateLogger("JWTDebug"); + logger.LogError(context.Exception, "Error de autenticación JWT"); + return Task.CompletedTask; + } + }; +}); #endregion #region Email Configuration @@ -185,7 +150,7 @@ builder.Services.AddSwaggerGen(option => // })); builder.Services.AddCors(options => { - + options.AddPolicy("CORS", policy => { /* @@ -216,8 +181,8 @@ var app = builder.Build(); //if (app.Environment.IsDevelopment()) //{ - app.UseSwagger(); - app.UseSwaggerUI(); +app.UseSwagger(); +app.UseSwaggerUI(); //} app.UseCors("CORS"); @@ -226,4 +191,56 @@ app.UseAuthorization(); app.UseHttpsRedirection(); app.MapControllers(); -app.Run(); \ No newline at end of file +app.Run(); + +static void RepositorysAndServices(WebApplicationBuilder builder) +{ + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); +} \ No newline at end of file diff --git a/phronCare.API/phronCare.API.csproj b/phronCare.API/phronCare.API.csproj index f31b64c..5b7ec45 100644 --- a/phronCare.API/phronCare.API.csproj +++ b/phronCare.API/phronCare.API.csproj @@ -1,4 +1,4 @@ - + net8.0