From 73b43c80cb6699ef972f43988d2dcb6cbcb7d46b Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 13:40:27 -0400 Subject: [PATCH 01/14] Cleanup of appsettings files --- src/Web/appsettings.Development.dist | 17 ----------------- src/Web/appsettings.json | 3 --- 2 files changed, 20 deletions(-) delete mode 100644 src/Web/appsettings.Development.dist diff --git a/src/Web/appsettings.Development.dist b/src/Web/appsettings.Development.dist deleted file mode 100644 index 25369fa..0000000 --- a/src/Web/appsettings.Development.dist +++ /dev/null @@ -1,17 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Information", - "Microsoft.AspNetCore.SpaProxy": "Information", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "Google-ClientId": "", - "Google-ClientSecret": "", - "Facebook-ClientId": "", - "Facebook-ClientSecret": "", - "Jwt-Audience": "", - "Jwt-Issuer": "", - "Jwt-Key": "", -} \ No newline at end of file diff --git a/src/Web/appsettings.json b/src/Web/appsettings.json index 76a510b..222224e 100644 --- a/src/Web/appsettings.json +++ b/src/Web/appsettings.json @@ -1,7 +1,4 @@ { - "ConnectionStrings": { - "DefaultConnection": "Server={DB_HOST},1433;Database=Hutopy;User Id=sa;Password={DB_PASSWORD};MultipleActiveResultSets=true;TrustServerCertificate=True" - }, "Logging": { "LogLevel": { "Default": "Information", From 9a360dc407bc4fdb3e5e1b6c4baa980c940b2676 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 13:40:55 -0400 Subject: [PATCH 02/14] Fix issue with missing configuration for Facebook or Google --- .../Identity/IdentityService.cs | 11 ++- src/Web/DependencyInjection.cs | 74 ++++++++++++------- 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 02b5ce0..9997316 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -218,12 +218,15 @@ public class IdentityService( { return null; } - + var user = await GetUserByUserNameAsync(userName); + + var jwtSection = configuration.GetRequiredSection("Authentication:Jwt"); + var token = JwtTokenHelper.GenerateJwtToken( - issuer: configuration["Jwt-Issuer"] ?? "", - audience: configuration["Jwt-Audience"] ?? "", - key: configuration["Jwt-Key"] ?? "", + issuer: jwtSection["Issuer"] ?? "", + audience: jwtSection["Audience"] ?? "", + key: jwtSection["Key"] ?? "", userId: user?.Id ?? ""); return token; diff --git a/src/Web/DependencyInjection.cs b/src/Web/DependencyInjection.cs index 3a7eb61..27f1ac1 100644 --- a/src/Web/DependencyInjection.cs +++ b/src/Web/DependencyInjection.cs @@ -30,7 +30,7 @@ public static class DependencyInjection services.AddExceptionHandler(); services.AddRazorPages(); - + services.AddHttpClient(); // Customise default API behaviour @@ -44,13 +44,16 @@ public static class DependencyInjection configure.Title = "Hutopy API"; // Add JWT - configure.AddSecurity("JWT", Enumerable.Empty(), new OpenApiSecurityScheme - { - Type = OpenApiSecuritySchemeType.ApiKey, - Name = "Authorization", - In = OpenApiSecurityApiKeyLocation.Header, - Description = "Type into the textbox: Bearer {your JWT token}." - }); + configure.AddSecurity( + "JWT", + [], + new OpenApiSecurityScheme + { + Type = OpenApiSecuritySchemeType.ApiKey, + Name = "Authorization", + In = OpenApiSecurityApiKeyLocation.Header, + Description = "Type into the textbox: Bearer {your JWT token}." + }); configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); }); @@ -58,7 +61,8 @@ public static class DependencyInjection return services; } - public static IServiceCollection AddKeyVaultIfConfigured(this IServiceCollection services, ConfigurationManager configuration) + public static IServiceCollection AddKeyVaultIfConfigured(this IServiceCollection services, + ConfigurationManager configuration) { var keyVaultUri = configuration["KeyVaultUri"]; if (!string.IsNullOrWhiteSpace(keyVaultUri)) @@ -70,10 +74,11 @@ public static class DependencyInjection return services; } - - public static IServiceCollection AddAuthorizationAndAuthentication(this IServiceCollection services, ConfigurationManager configuration) + + public static IServiceCollection AddAuthorizationAndAuthentication(this IServiceCollection services, + ConfigurationManager configuration) { - services.AddAuthentication(options => + var authenticationBuilder = services.AddAuthentication(options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; @@ -82,35 +87,50 @@ public static class DependencyInjection { options.LoginPath = "/api/Users/login"; }) - .AddCookie() - .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions => + .AddCookie(); + + var authJwt = configuration.GetSection("Authentication:Jwt"); + if (authJwt.Exists()) + { + authenticationBuilder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions => { jwtBearerOptions.Authority = "https://hutopy.com"; jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, - ValidIssuer = configuration["Jwt-Issuer"], + ValidIssuer = authJwt["Issuer"], ValidateAudience = true, - ValidAudience = configuration["Jwt-Audience"], + ValidAudience = authJwt["Audience"], ValidateLifetime = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt-Key"] ?? + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authJwt["Key"] ?? throw new ArgumentNullException("The Jwt Key is missing."))) }; - }) - .AddGoogle(GoogleDefaults.AuthenticationScheme, options => + }); + } + + var authGoogle = configuration.GetSection("Authentication:Google"); + if (authGoogle.Exists()) + { + authenticationBuilder.AddGoogle(GoogleDefaults.AuthenticationScheme, options => { - options.ClientId = configuration["Google-ClientId"] ?? - throw new ArgumentNullException("The Google ClientId is missing.");; - options.ClientSecret = configuration["Google-ClientSecret"] ?? - throw new ArgumentNullException("The Google ClientSecret is missing.");; - }) - .AddFacebook(FacebookDefaults.AuthenticationScheme, options => + options.ClientId = authGoogle["ClientId"] ?? + throw new ArgumentNullException("The Google ClientId is missing."); + options.ClientSecret = authGoogle["ClientSecret"] ?? + throw new ArgumentNullException("The Google ClientSecret is missing."); + }); + } + + var authFacebook = configuration.GetSection("Authentication:Facebook"); + if (authFacebook.Exists()) + { + authenticationBuilder.AddFacebook(FacebookDefaults.AuthenticationScheme, options => { - options.ClientId = configuration["Facebook-ClientId"] ?? + options.ClientId = authFacebook["ClientId"] ?? throw new ArgumentNullException("The Facebook ClientId is missing."); - options.ClientSecret = configuration["Facebook-ClientSecret"] ?? + options.ClientSecret = authFacebook["ClientSecret"] ?? throw new ArgumentNullException("The Facebook ClientSecret is missing."); }); + } return services; } From 6307ea45e58195f3d8a86cf5daa17a01130ac487 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 13:44:45 -0400 Subject: [PATCH 03/14] Just cleanup --- .../Queries/GetCurrentUser/GetCurrentUser.cs | 8 +-- src/Infrastructure/Utils/GenerateJwtToken.cs | 14 ++--- src/Web/Controllers/FacebookController.cs | 44 ++++++++------- src/Web/Controllers/GoogleController.cs | 56 ++++++++++--------- 4 files changed, 63 insertions(+), 59 deletions(-) diff --git a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs index 49f1152..7cbabd6 100644 --- a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs +++ b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs @@ -8,14 +8,14 @@ public class GetCurrentUserQueryHandler( IApplicationDbContext context, IMapper mapper, IIdentityService identityService - ) +) : IRequestHandler { public async Task Handle(GetCurrentUserQuery request, CancellationToken cancellationToken) { var identityUser = await identityService.GetCurrentUserAsync(); - var currentUserId = new Guid(identityUser?.Id ?? ""); - + var currentUserId = Guid.Parse(identityUser!.Id!); + var transactions = await context.UserTransactions .Where(x => x.ApplicationUserId == currentUserId.ToString()) .OrderBy(x => x.LastModified) @@ -30,7 +30,7 @@ public class GetCurrentUserQueryHandler( Id = currentUserId, FirstName = identityUser?.FirstName ?? "", LastName = identityUser?.LastName ?? "", - UserName =identityUser?.UserName ?? "", + UserName = identityUser?.UserName ?? "", UserTransactions = transactions, TotalBalance = transactions.Sum(x => x.Amount), UserRoles = roles diff --git a/src/Infrastructure/Utils/GenerateJwtToken.cs b/src/Infrastructure/Utils/GenerateJwtToken.cs index 9f01d6a..00423f9 100644 --- a/src/Infrastructure/Utils/GenerateJwtToken.cs +++ b/src/Infrastructure/Utils/GenerateJwtToken.cs @@ -9,20 +9,18 @@ public static class JwtTokenHelper { public static string GenerateJwtToken(string issuer, string audience, string key, string userId) { - var claims = new[] - { - new Claim(JwtRegisteredClaimNames.Sub, userId), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), - new Claim(ClaimTypes.NameIdentifier, userId) - }; - var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: issuer, audience: audience, - claims: claims, + claims: new[] + { + new Claim(JwtRegisteredClaimNames.Sub, userId), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(ClaimTypes.NameIdentifier, userId) + }, expires: DateTime.Now.AddMinutes(30), signingCredentials: credentials); diff --git a/src/Web/Controllers/FacebookController.cs b/src/Web/Controllers/FacebookController.cs index 679502e..3178b5f 100644 --- a/src/Web/Controllers/FacebookController.cs +++ b/src/Web/Controllers/FacebookController.cs @@ -13,41 +13,45 @@ public class FacebookController(IIdentityService identityService) : Controller [HttpGet("/api/facebook/sign-in")] public async Task SignIn() { - await HttpContext.ChallengeAsync(FacebookDefaults.AuthenticationScheme, new AuthenticationProperties - { - RedirectUri = Url.Action("Authorize") - }); + await HttpContext.ChallengeAsync(FacebookDefaults.AuthenticationScheme, + new AuthenticationProperties { RedirectUri = Url.Action("Authorize") }); } public async Task Authorize() { var authenticateResult = await HttpContext.AuthenticateAsync(FacebookDefaults.AuthenticationScheme); - + if (!authenticateResult.Succeeded) return BadRequest(); - + var claims = authenticateResult.Principal.Claims.ToList(); - + var name = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value ?? ""; var email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value ?? ""; var givenName = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value ?? ""; var familyName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value ?? ""; - - var claimsIdentity = new ClaimsIdentity(new List - { - new(ClaimTypes.Name, name), - new(ClaimTypes.Email, email), - new(ClaimTypes.GivenName, givenName), - new(ClaimTypes.Surname, familyName) - }, CookieAuthenticationDefaults.AuthenticationScheme); - + + var claimsIdentity = new ClaimsIdentity( + new List + { + new(ClaimTypes.Name, name), + new(ClaimTypes.Email, email), + new(ClaimTypes.GivenName, givenName), + new(ClaimTypes.Surname, familyName) + }, + CookieAuthenticationDefaults.AuthenticationScheme); + if (await identityService.FindUserByEmailAsync(email) != null) { - await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(claimsIdentity)); return Redirect("/"); } - - await identityService.CreateUserAsync(email, givenName, givenName, familyName, RandomGenerator.RandomString(24)); - await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); + + await identityService.CreateUserAsync(email, givenName, givenName, familyName, + RandomGenerator.RandomString(24)); + await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(claimsIdentity)); return Redirect("/"); } } diff --git a/src/Web/Controllers/GoogleController.cs b/src/Web/Controllers/GoogleController.cs index 6df233a..40e8adc 100644 --- a/src/Web/Controllers/GoogleController.cs +++ b/src/Web/Controllers/GoogleController.cs @@ -13,7 +13,8 @@ public class GoogleController(IIdentityService identityService, IHttpClientFacto [HttpPost("/api/google/sign-in")] public async Task SignIn([FromBody] GoogleSignInRequest request) { - var httpClient = httpClientFactory.CreateClient(); + using var httpClient = httpClientFactory.CreateClient(); + // Verify the token with Google var response = await httpClient.GetAsync($"https://www.googleapis.com/oauth2/v1/userinfo?access_token={request.AccessToken}"); if (!response.IsSuccessStatusCode) @@ -21,12 +22,11 @@ public class GoogleController(IIdentityService identityService, IHttpClientFacto return BadRequest("Invalid Google token."); } - var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); - - var email = payload["email"]?.ToString() ?? ""; - var name = payload["name"]?.ToString() ?? ""; - var givenName = payload["given_name"]?.ToString() ?? ""; - var familyName = payload["family_name"]?.ToString() ?? ""; + var userInfo = JObject.Parse(await response.Content.ReadAsStringAsync()); + var email = userInfo["email"]?.ToString() ?? ""; + var name = userInfo["name"]?.ToString() ?? ""; + var givenName = userInfo["given_name"]?.ToString() ?? ""; + var familyName = userInfo["family_name"]?.ToString() ?? ""; if (string.IsNullOrEmpty(email)) { @@ -47,27 +47,29 @@ public class GoogleController(IIdentityService identityService, IHttpClientFacto } // Sign in the user - var claims = new List - { - new(ClaimTypes.Name, name), - new(ClaimTypes.Email, email), - new(ClaimTypes.GivenName, givenName), - new(ClaimTypes.Surname, familyName) - }; - - var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); - await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); - - var issuer = configuration["Jwt-Issuer"] ?? - throw new ArgumentNullException("The Jwt issuer is missing."); - var audience = configuration["Jwt-Audience"] ?? - throw new ArgumentNullException("The Jwt audience is missing."); - var key = configuration["Jwt-Key"] ?? - throw new ArgumentNullException("The Jwt key is missing."); - - var jwtToken = JwtTokenHelper.GenerateJwtToken(issuer, audience, key, user.Id); + var claimsIdentity = new ClaimsIdentity( + new List + { + new(ClaimTypes.Name, name), + new(ClaimTypes.Email, email), + new(ClaimTypes.GivenName, givenName), + new(ClaimTypes.Surname, familyName) + }, + CookieAuthenticationDefaults.AuthenticationScheme); - return Ok(new { accessToken = jwtToken, email }); + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(claimsIdentity)); + + var jwtSection = configuration.GetRequiredSection("Authentication:Jwt"); + + var token = JwtTokenHelper.GenerateJwtToken( + issuer: jwtSection["Issuer"] ?? throw new ArgumentNullException("The Jwt issuer is missing."), + audience: jwtSection["Audience"] ?? throw new ArgumentNullException("The Jwt audience is missing."), + key: jwtSection["Key"] ?? throw new ArgumentNullException("The Jwt key is missing."), + userId: user.Id); + + return Ok(new { accessToken = token, email }); } public class GoogleSignInRequest From 310cfdfeb991f0cea83867d581dab06704389dfd Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 13:46:00 -0400 Subject: [PATCH 04/14] Fix some endpoints in web.http --- src/Web/Web.http | 114 ++++------------------------------- src/Web/http-client.env.json | 5 ++ 2 files changed, 17 insertions(+), 102 deletions(-) create mode 100644 src/Web/http-client.env.json diff --git a/src/Web/Web.http b/src/Web/Web.http index 105a254..c0ac5aa 100644 --- a/src/Web/Web.http +++ b/src/Web/Web.http @@ -1,12 +1,10 @@ # For more info on HTTP files go to https://aka.ms/vs/httpfile -@Web_HostAddress = https://localhost:5001 - @Email=administrator@localhost @Password=Administrator1! -@BearerToken= +@auth_token= # POST Users Register -POST {{Web_HostAddress}}/api/Users/Register +POST {{base_url}}/api/Users/Register Content-Type: application/json { @@ -17,7 +15,7 @@ Content-Type: application/json ### # POST Users Login -POST {{Web_HostAddress}}/api/Users/Login +POST {{base_url}}/api/Users/login Content-Type: application/json { @@ -25,11 +23,13 @@ Content-Type: application/json "password": "{{Password}}" } +> {% client.global.set("auth_token", response.body); %} + ### # POST Users Refresh -POST {{Web_HostAddress}}/api/Users/Refresh -Authorization: Bearer {{BearerToken}} +POST {{base_url}}/api/Users/Refresh +Authorization: Bearer {{auth_token}} Content-Type: application/json { @@ -39,101 +39,11 @@ Content-Type: application/json ### # GET WeatherForecast -GET {{Web_HostAddress}}/api/WeatherForecasts -Authorization: Bearer {{BearerToken}} +GET {{base_url}}/api/WeatherForecasts +Authorization: Bearer {{auth_token}} ### -# GET TodoLists -GET {{Web_HostAddress}}/api/TodoLists -Authorization: Bearer {{BearerToken}} - -### - -# POST TodoLists -POST {{Web_HostAddress}}/api/TodoLists -Authorization: Bearer {{BearerToken}} -Content-Type: application/json - -// CreateTodoListCommand -{ - "Title": "Backlog" -} - -### - -# PUT TodoLists -PUT {{Web_HostAddress}}/api/TodoLists/1 -Authorization: Bearer {{BearerToken}} -Content-Type: application/json - -// UpdateTodoListCommand -{ - "Id": 1, - "Title": "Product Backlog" -} - -### - -# DELETE TodoLists -DELETE {{Web_HostAddress}}/api/TodoLists/1 -Authorization: Bearer {{BearerToken}} - -### - -# GET TodoItems -@PageNumber = 1 -@PageSize = 10 -GET {{Web_HostAddress}}/api/TodoItems?ListId=1&PageNumber={{PageNumber}}&PageSize={{PageSize}} - -Authorization: Bearer {{BearerToken}} - -### - -# POST TodoItems -POST {{Web_HostAddress}}/api/TodoItems -Authorization: Bearer {{BearerToken}} -Content-Type: application/json - -// CreateTodoItemCommand -{ - "ListId": 1, - "Title": "Eat a burrito 🌯" -} - -### - -#PUT TodoItems UpdateItemDetails -PUT {{Web_HostAddress}}/api/TodoItems/UpdateItemDetails?Id=1 -Authorization: Bearer {{BearerToken}} -Content-Type: application/json - -// UpdateTodoItemDetailCommand -{ - "Id": 1, - "ListId": 1, - "Priority": 3, - "Note": "This is a good idea!" -} - -### - -# PUT TodoItems -PUT {{Web_HostAddress}}/api/TodoItems/1 -Authorization: Bearer {{BearerToken}} -Content-Type: application/json - -// UpdateTodoItemCommand -{ - "Id": 1, - "Title": "Eat a yummy burrito 🌯", - "Done": true -} - -### - -# DELETE TodoItem -DELETE {{Web_HostAddress}}/api/TodoItems/1 -Authorization: Bearer {{BearerToken}} - -### \ No newline at end of file +# GET GetMyUser +GET {{base_url}}/api/GetMyUser +Authorization: Bearer {{auth_token}} \ No newline at end of file diff --git a/src/Web/http-client.env.json b/src/Web/http-client.env.json new file mode 100644 index 0000000..4af4838 --- /dev/null +++ b/src/Web/http-client.env.json @@ -0,0 +1,5 @@ +{ + "dev": { + "base_url": "https://localhost:5001" + } +} \ No newline at end of file From 499a9450369342c9c31cb530974b0f3e9257ae8c Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 13:46:53 -0400 Subject: [PATCH 05/14] Fix login --- src/Application/Users/Commands/Login.cs | 36 ++++++++++++------------- src/Web/Endpoints/Users.cs | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Application/Users/Commands/Login.cs b/src/Application/Users/Commands/Login.cs index f61f810..8f232b5 100644 --- a/src/Application/Users/Commands/Login.cs +++ b/src/Application/Users/Commands/Login.cs @@ -1,27 +1,27 @@ using Hutopy.Application.Common.Interfaces; namespace Hutopy.Application.Users.Commands; -public record LoginCommand : IRequest + +public record LoginCommand( + string Email, + string Password) + : IRequest; + +public record LoginResponse( + string AccessToken, + string RefreshToken); + +public class LoginCommandHandler( + IApplicationDbContext Context, + IIdentityService identityService) + : IRequestHandler { - public required string EmailAddress { get; init; } - public required string Password { get; init; } -} - -public class LoginCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IIdentityService _identityService; - - public LoginCommandHandler(IApplicationDbContext context, IIdentityService identityService) + public async Task Handle(LoginCommand request, CancellationToken cancellationToken) { - _context = context; - _identityService = identityService; - } + var accessToken = await identityService.LoginAsync(request.Email, request.Password); - public async Task Handle(LoginCommand request, CancellationToken cancellationToken) - { - var jwt = await _identityService.LoginAsync(request.EmailAddress, request.Password); + if (string.IsNullOrWhiteSpace(accessToken)) throw new InvalidOperationException("Invalid login credentials"); - return jwt ?? "Invalid login credentials"; + return new LoginResponse(accessToken, string.Empty); } } diff --git a/src/Web/Endpoints/Users.cs b/src/Web/Endpoints/Users.cs index b2a15c5..c344509 100644 --- a/src/Web/Endpoints/Users.cs +++ b/src/Web/Endpoints/Users.cs @@ -24,7 +24,7 @@ public class Users : EndpointGroupBase return await sender.Send(query); } - private static async Task Login(ISender sender, LoginCommand command) + private static async Task Login(ISender sender, LoginCommand command) { return await sender.Send(command); } From 5231d44b132797c9bacadd53e7e4ec15b23806f5 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 13:55:24 -0400 Subject: [PATCH 06/14] Add ignored appsettings.Development.json --- src/Web/appsettings.Development.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/Web/appsettings.Development.json diff --git a/src/Web/appsettings.Development.json b/src/Web/appsettings.Development.json new file mode 100644 index 0000000..9dad30e --- /dev/null +++ b/src/Web/appsettings.Development.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Information", + "Microsoft.AspNetCore.SpaProxy": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ConnectionStrings": { + "DefaultConnection": "Server=localhost,1433;Database=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True" + }, + "Authentication": { + "Jwt": { + "Audience": "hutopy", + "Issuer": "https://auth.hutopy.com", + "Key": "b2df428b9929d3ace7c598bbf4e496b2f0b71ab3cd4f94540356cfc35b000000" + } + } +} \ No newline at end of file From ba35e3228eb7b42536c0bedbc646d1d52ae996dd Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 14:18:14 -0400 Subject: [PATCH 07/14] Add start-infrastructure script --- start-infrastructure.sh | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 start-infrastructure.sh diff --git a/start-infrastructure.sh b/start-infrastructure.sh new file mode 100644 index 0000000..d7a15c2 --- /dev/null +++ b/start-infrastructure.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +docker run \ + --cap-add SYS_PTRACE \ + -e 'ACCEPT_EULA=1' \ + -e 'MSSQL_SA_PASSWORD=P@ssword123!' \ + -p 1433:1433 \ + --name azuresqledge \ + -d mcr.microsoft.com/azure-sql-edge \ No newline at end of file From 81e92909890590f035ec5c4160c3cebd2916ef6b Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 16:27:44 -0400 Subject: [PATCH 08/14] Cleanup and more fixes --- Hutopy.sln | 1 + src/Infrastructure/DependencyInjection.cs | 29 +- src/Web/DependencyInjection.cs | 25 +- src/Web/Program.cs | 37 +- src/Web/Properties/launchSettings.json | 6 +- src/Web/Web.csproj | 46 +- src/Web/appsettings.Development.json | 2 +- src/Web/config.nswag | 63 -- src/Web/wwwroot/api/specification.json | 869 ------------------ .../TestcontainersTestDatabase.cs | 4 +- 10 files changed, 57 insertions(+), 1025 deletions(-) delete mode 100644 src/Web/config.nswag delete mode 100644 src/Web/wwwroot/api/specification.json diff --git a/Hutopy.sln b/Hutopy.sln index 5714f45..2fe5931 100644 --- a/Hutopy.sln +++ b/Hutopy.sln @@ -25,6 +25,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Packages.props = Directory.Packages.props global.json = global.json README.md = README.md + start-infrastructure.sh = start-infrastructure.sh EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web", "src\Web\Web.csproj", "{4E4EE20C-F06A-4A1B-851F-C5577796941C}" diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index e03ce4c..b8f43d6 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -15,22 +15,13 @@ namespace Hutopy.Infrastructure; public static class DependencyInjection { - public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, + IConfiguration configuration) { // Replace password in the connection string with env var in local environment. // Prod will use the connectionString stored in the vault with password in it directly. - var connectionString = configuration.GetConnectionString("DefaultConnection") ?? ""; - - var dbPassword = configuration["DB_PASSWORD"] ?? ""; - var dbHost = configuration["DB_HOST"] ?? "localhost"; - - if (dbPassword != string.Empty) - { - connectionString = connectionString.Replace("{DB_PASSWORD}", dbPassword); - connectionString = connectionString.Replace("{DB_HOST}", dbHost); - } - - Guard.Against.Null(connectionString, message: "Connection string 'DefaultConnection' not found."); + var connectionString = configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("Missing ConnectionString: DefaultConnection"); services.AddScoped(); services.AddScoped(); @@ -49,15 +40,9 @@ public static class DependencyInjection .AddBearerToken(IdentityConstants.BearerScheme); services.AddAuthorizationBuilder(); - - services.AddIdentityCore(options => - { - options.Password.RequireDigit = false; - options.Password.RequireLowercase = false; - options.Password.RequireUppercase = false; - options.Password.RequireNonAlphanumeric = false; - options.Password.RequiredLength = 8; - }) + + services + .AddIdentityCore() .AddRoles() .AddEntityFrameworkStores() .AddApiEndpoints() diff --git a/src/Web/DependencyInjection.cs b/src/Web/DependencyInjection.cs index 27f1ac1..2a76f33 100644 --- a/src/Web/DependencyInjection.cs +++ b/src/Web/DependencyInjection.cs @@ -39,25 +39,6 @@ public static class DependencyInjection services.AddEndpointsApiExplorer(); - services.AddOpenApiDocument((configure, sp) => - { - configure.Title = "Hutopy API"; - - // Add JWT - configure.AddSecurity( - "JWT", - [], - new OpenApiSecurityScheme - { - Type = OpenApiSecuritySchemeType.ApiKey, - Name = "Authorization", - In = OpenApiSecurityApiKeyLocation.Header, - Description = "Type into the textbox: Bearer {your JWT token}." - }); - - configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); - }); - return services; } @@ -78,7 +59,8 @@ public static class DependencyInjection public static IServiceCollection AddAuthorizationAndAuthentication(this IServiceCollection services, ConfigurationManager configuration) { - var authenticationBuilder = services.AddAuthentication(options => + var authenticationBuilder = services + .AddAuthentication(options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; @@ -86,8 +68,7 @@ public static class DependencyInjection .AddCookie("Identity.Application", options => { options.LoginPath = "/api/Users/login"; - }) - .AddCookie(); + }); var authJwt = configuration.GetSection("Authentication:Jwt"); if (authJwt.Exists()) diff --git a/src/Web/Program.cs b/src/Web/Program.cs index 781bdc2..ea5b072 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -4,6 +4,8 @@ using Hutopy.Infrastructure.Data; using Hutopy.Web; using Azure.Identity; using Microsoft.AspNetCore.HttpOverrides; +using NSwag; +using NSwag.Generation.Processors.Security; var builder = WebApplication.CreateBuilder(args); @@ -48,6 +50,25 @@ builder.Services.AddWebServices(); builder.Services.AddAuthorizationAndAuthentication(builder.Configuration); builder.Services.AddControllers(); +builder.Services.AddOpenApiDocument((configure, sp) => +{ + configure.Title = "Hutopy API"; + + // Add JWT + configure.AddSecurity( + "JWT", + [], + new OpenApiSecurityScheme + { + Type = OpenApiSecuritySchemeType.ApiKey, + Name = "Authorization", + In = OpenApiSecurityApiKeyLocation.Header, + Description = "Type into the textbox: Bearer {your JWT token}." + }); + + configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); + }); + var app = builder.Build(); app.UseForwardedHeaders( @@ -75,22 +96,18 @@ app.UseHealthChecks("/health"); app.UseHttpsRedirection(); app.UseStaticFiles(); -app.UseSwaggerUi(settings => +if (app.Environment.IsDevelopment()) { - settings.Path = "/api"; - settings.DocumentPath = "/api/specification.json"; -}); + app.UseOpenApi(); + app.UseSwaggerUi(options => options.Path = "/api"); +} app.MapControllerRoute( name: "default", pattern: "{controller}/{action=Index}/{id?}"); -app.MapFallbackToFile("index.html"); - -app.UseExceptionHandler(options => { }); - -app.Map("/", () => Results.Redirect("/api")); - +//TODO: validate the behavior +// app.UseExceptionHandler(); app.MapEndpoints(); app.Run(); diff --git a/src/Web/Properties/launchSettings.json b/src/Web/Properties/launchSettings.json index 6ef4f83..39a9650 100644 --- a/src/Web/Properties/launchSettings.json +++ b/src/Web/Properties/launchSettings.json @@ -8,9 +8,9 @@ } }, "profiles": { - "Hutopy.Web": { + "Hutopy.Web - DEV": { "commandName": "Project", - "launchBrowser": true, + "launchBrowser": false, "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -18,7 +18,7 @@ }, "IIS Express": { "commandName": "IISExpress", - "launchBrowser": true, + "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 9193242..80a1ea9 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -7,46 +7,26 @@ - - + + - - - - - - - - - - + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive - all - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - - OnBuildSuccess - - - - - - - - - - - - diff --git a/src/Web/appsettings.Development.json b/src/Web/appsettings.Development.json index 9dad30e..ec39dd6 100644 --- a/src/Web/appsettings.Development.json +++ b/src/Web/appsettings.Development.json @@ -8,7 +8,7 @@ } }, "ConnectionStrings": { - "DefaultConnection": "Server=localhost,1433;Database=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True" + "DefaultConnection": "Server=localhost,1433;Initial Catalog=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True" }, "Authentication": { "Jwt": { diff --git a/src/Web/config.nswag b/src/Web/config.nswag deleted file mode 100644 index 4af18f2..0000000 --- a/src/Web/config.nswag +++ /dev/null @@ -1,63 +0,0 @@ -{ - "runtime": "Net80", - "defaultVariables": null, - "documentGenerator": { - "aspNetCoreToOpenApi": { - "project": "Web.csproj", - "msBuildProjectExtensionsPath": null, - "configuration": null, - "runtime": null, - "targetFramework": null, - "noBuild": true, - "msBuildOutputPath": null, - "verbose": false, - "workingDirectory": null, - "requireParametersWithoutDefault": true, - "apiGroupNames": null, - "defaultPropertyNameHandling": "CamelCase", - "defaultReferenceTypeNullHandling": "Null", - "defaultDictionaryValueReferenceTypeNullHandling": "NotNull", - "defaultResponseReferenceTypeNullHandling": "NotNull", - "generateOriginalParameterNames": true, - "defaultEnumHandling": "Integer", - "flattenInheritanceHierarchy": false, - "generateKnownTypes": true, - "generateEnumMappingDescription": false, - "generateXmlObjects": false, - "generateAbstractProperties": false, - "generateAbstractSchemas": true, - "ignoreObsoleteProperties": false, - "allowReferencesWithProperties": false, - "useXmlDocumentation": true, - "resolveExternalXmlDocumentation": true, - "excludedTypeNames": [], - "serviceHost": null, - "serviceBasePath": null, - "serviceSchemes": [], - "infoTitle": "Hutopy API", - "infoDescription": null, - "infoVersion": "1.0.0", - "documentTemplate": null, - "documentProcessorTypes": [], - "operationProcessorTypes": [], - "typeNameGeneratorType": null, - "schemaNameGeneratorType": null, - "contractResolverType": null, - "serializerSettingsType": null, - "useDocumentProvider": true, - "documentName": "v1", - "aspNetCoreEnvironment": null, - "createWebHostBuilderMethod": null, - "startupType": null, - "allowNullableBodyParameters": true, - "useHttpAttributeNameAsOperationId": false, - "output": "wwwroot/api/specification.json", - "outputType": "OpenApi3", - "newLineBehavior": "Auto", - "assemblyPaths": [], - "assemblyConfig": null, - "referencePaths": [], - "useNuGetCache": false - } - } -} diff --git a/src/Web/wwwroot/api/specification.json b/src/Web/wwwroot/api/specification.json deleted file mode 100644 index bf1f109..0000000 --- a/src/Web/wwwroot/api/specification.json +++ /dev/null @@ -1,869 +0,0 @@ -{ - "x-generator": "NSwag v14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))", - "openapi": "3.0.0", - "info": { - "title": "Hutopy API", - "version": "1.0.0" - }, - "paths": { - "/api/GetMyUser": { - "get": { - "tags": [ - "GetMyUser" - ], - "operationId": "GetCurrentUser", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserDto" - } - } - } - } - }, - "security": [ - { - "JWT": [] - } - ] - } - }, - "/api/GetMyUser/profile-picture": { - "get": { - "tags": [ - "GetMyUser" - ], - "operationId": "GetCurrentUserProfilePicture", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Stream" - } - } - } - } - }, - "security": [ - { - "JWT": [] - } - ] - } - }, - "/api/GetMyUser/profile-picture-2": { - "patch": { - "tags": [ - "GetMyUser" - ], - "operationId": "PatchApiGetMyUserProfilePicture2", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Stream" - } - } - } - } - }, - "security": [ - { - "JWT": [] - } - ] - } - }, - "/api/JoinUs": { - "get": { - "tags": [ - "JoinUs" - ], - "operationId": "GetFutureCreators", - "parameters": [ - { - "name": "PageNumber", - "in": "query", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - }, - "x-position": 1 - }, - { - "name": "PageSize", - "in": "query", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - }, - "x-position": 2 - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaginatedListOfFutureCreatorListDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "JoinUs" - ], - "operationId": "CreateFutureCreator", - "requestBody": { - "x-name": "command", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateFutureCreatorCommand" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string", - "format": "guid" - } - } - } - } - } - } - }, - "/api/Stripe/confirmTransaction": { - "post": { - "tags": [ - "Stripe" - ], - "operationId": "ConfirmTransaction", - "requestBody": { - "x-name": "command", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConfirmStripeTransactionCommand" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/api/Stripe/getMyLastReceipt": { - "get": { - "tags": [ - "Stripe" - ], - "operationId": "GetMyLastReceipt", - "parameters": [ - { - "name": "Email", - "in": "query", - "required": true, - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 1 - }, - { - "name": "CreatorId", - "in": "query", - "required": true, - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 2 - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MyLastReceiptDto" - } - } - } - } - } - } - }, - "/api/Stripe": { - "post": { - "tags": [ - "Stripe" - ], - "operationId": "CreateSessionCheckout", - "requestBody": { - "x-name": "command", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSessionCheckoutCommand" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/api/Users": { - "post": { - "tags": [ - "Users" - ], - "operationId": "CreateUser", - "requestBody": { - "x-name": "command", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateUserCommand" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string", - "format": "guid" - } - } - } - } - } - }, - "get": { - "tags": [ - "Users" - ], - "operationId": "GetMinimalUser", - "parameters": [ - { - "name": "UserId", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 1 - }, - { - "name": "UserName", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 2 - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MinimalUserDto" - } - } - } - } - } - } - }, - "/api/Users/login": { - "post": { - "tags": [ - "Users" - ], - "operationId": "Login", - "requestBody": { - "x-name": "command", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LoginCommand" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/api/Users/upload-profile-picture": { - "post": { - "tags": [ - "Users" - ], - "operationId": "UploadProfilePicture", - "requestBody": { - "x-name": "stream", - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary", - "nullable": false - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/api/WeatherForecasts": { - "get": { - "tags": [ - "WeatherForecasts" - ], - "operationId": "GetWeatherForecasts", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WeatherForecast" - } - } - } - } - } - }, - "security": [ - { - "JWT": [] - } - ] - } - }, - "/api/facebook/sign-in": { - "get": { - "tags": [ - "Facebook" - ], - "operationId": "Facebook_SignIn", - "responses": { - "200": { - "description": "" - } - } - } - }, - "/api/google/sign-in": { - "post": { - "tags": [ - "Google" - ], - "operationId": "Google_SignIn", - "requestBody": { - "x-name": "request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GoogleSignInRequest" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "UserDto": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "format": "guid" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "userName": { - "type": "string" - }, - "userTransactions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UserTransactionDto" - } - }, - "userRoles": { - "type": "array", - "items": { - "type": "string" - } - }, - "totalBalance": { - "type": "number", - "format": "decimal" - } - } - }, - "UserTransactionDto": { - "type": "object", - "additionalProperties": false, - "properties": { - "amount": { - "type": "number", - "format": "decimal" - }, - "currency": { - "type": "string" - }, - "tipMessage": { - "type": "string" - }, - "created": { - "type": "string", - "format": "date-time" - }, - "isConfirmed": { - "type": "boolean" - } - } - }, - "Stream": { - "allOf": [ - { - "$ref": "#/components/schemas/MarshalByRefObject" - }, - { - "type": "object", - "x-abstract": true, - "additionalProperties": false, - "properties": { - "canRead": { - "type": "boolean" - }, - "canWrite": { - "type": "boolean" - }, - "canSeek": { - "type": "boolean" - }, - "canTimeout": { - "type": "boolean" - }, - "length": { - "type": "integer", - "format": "int64" - }, - "position": { - "type": "integer", - "format": "int64" - }, - "readTimeout": { - "type": "integer", - "format": "int32" - }, - "writeTimeout": { - "type": "integer", - "format": "int32" - } - } - } - ] - }, - "MarshalByRefObject": { - "type": "object", - "x-abstract": true, - "additionalProperties": false - }, - "PaginatedListOfFutureCreatorListDto": { - "type": "object", - "additionalProperties": false, - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FutureCreatorListDto" - } - }, - "pageNumber": { - "type": "integer", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "format": "int32" - }, - "hasPreviousPage": { - "type": "boolean" - }, - "hasNextPage": { - "type": "boolean" - } - } - }, - "FutureCreatorListDto": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "format": "guid" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - } - } - }, - "CreateFutureCreatorCommand": { - "type": "object", - "additionalProperties": false, - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "emailAddress": { - "type": "string" - }, - "phoneNumber": { - "type": "string" - }, - "socialNetworkAccount": { - "type": "string" - }, - "reasonToJoin": { - "type": "string" - } - } - }, - "ConfirmStripeTransactionCommand": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string" - }, - "object": { - "type": "string" - }, - "created": { - "type": "integer", - "format": "int32" - }, - "data": { - "$ref": "#/components/schemas/Data" - }, - "request": { - "$ref": "#/components/schemas/Request" - } - } - }, - "Data": { - "type": "object", - "additionalProperties": false, - "properties": { - "object": { - "$ref": "#/components/schemas/Object" - } - } - }, - "Object": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string" - }, - "amount": { - "type": "integer", - "format": "int32" - }, - "billing_details": { - "$ref": "#/components/schemas/BillingDetails" - }, - "calculated_statement_descriptor": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "paid": { - "type": "boolean" - }, - "payment_intent": { - "type": "string" - }, - "payment_method": { - "type": "string" - }, - "receipt_url": { - "type": "string" - }, - "status": { - "type": "string" - }, - "failure_message": { - "type": "string" - } - } - }, - "BillingDetails": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "phone": { - "type": "string" - } - } - }, - "Request": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string" - } - } - }, - "MyLastReceiptDto": { - "type": "object", - "additionalProperties": false, - "properties": { - "receiptUrl": { - "type": "string" - } - } - }, - "CreateSessionCheckoutCommand": { - "type": "object", - "additionalProperties": false, - "properties": { - "creatorId": { - "type": "string" - }, - "amount": { - "type": "integer", - "format": "int32" - }, - "currency": { - "type": "string" - }, - "tipMessage": { - "type": "string" - } - } - }, - "CreateUserCommand": { - "type": "object", - "additionalProperties": false, - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "emailAddress": { - "type": "string" - }, - "userName": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "LoginCommand": { - "type": "object", - "additionalProperties": false, - "properties": { - "emailAddress": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "MinimalUserDto": { - "type": "object", - "additionalProperties": false, - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "userName": { - "type": "string" - } - } - }, - "WeatherForecast": { - "type": "object", - "additionalProperties": false, - "properties": { - "date": { - "type": "string", - "format": "date-time" - }, - "temperatureC": { - "type": "integer", - "format": "int32" - }, - "temperatureF": { - "type": "integer", - "format": "int32" - }, - "summary": { - "type": "string", - "nullable": true - } - } - }, - "GoogleSignInRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "accessToken": { - "type": "string" - } - } - } - }, - "securitySchemes": { - "JWT": { - "type": "apiKey", - "description": "Type into the textbox: Bearer {your JWT token}.", - "name": "Authorization", - "in": "header" - } - } - }, - "security": [ - { - "JWT": [] - } - ] -} \ No newline at end of file diff --git a/tests/Application.FunctionalTests/TestcontainersTestDatabase.cs b/tests/Application.FunctionalTests/TestcontainersTestDatabase.cs index eaf57f8..342336f 100644 --- a/tests/Application.FunctionalTests/TestcontainersTestDatabase.cs +++ b/tests/Application.FunctionalTests/TestcontainersTestDatabase.cs @@ -35,11 +35,11 @@ public class TestcontainersTestDatabase : ITestDatabase var context = new ApplicationDbContext(options); - context.Database.Migrate(); + await context.Database.MigrateAsync(); _respawner = await Respawner.CreateAsync(_connectionString, new RespawnerOptions { - TablesToIgnore = new Respawn.Graph.Table[] { "__EFMigrationsHistory" } + TablesToIgnore = ["__EFMigrationsHistory"] }); } From 97a2c296f11706db320e85d64dea430be4ea4265 Mon Sep 17 00:00:00 2001 From: PascalMarchesseault <97350299+PascalMarchesseault@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:35:04 -0400 Subject: [PATCH 09/14] add MultiSubnetFailover=True --- src/Web/appsettings.Development.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Web/appsettings.Development.json b/src/Web/appsettings.Development.json index ec39dd6..43ba880 100644 --- a/src/Web/appsettings.Development.json +++ b/src/Web/appsettings.Development.json @@ -8,7 +8,7 @@ } }, "ConnectionStrings": { - "DefaultConnection": "Server=localhost,1433;Initial Catalog=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True" + "DefaultConnection": "Server=localhost,1433;Initial Catalog=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True;MultiSubnetFailover=True" }, "Authentication": { "Jwt": { From 623972bc36b855507cccb085d5ad3c15c372d909 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 27 Jun 2024 12:37:59 -0400 Subject: [PATCH 10/14] Adds messages api --- Directory.Packages.props | 1 + src/Application/Common/Models/UserModel.cs | 2 + .../Identity/IdentityService.cs | 11 ++- src/Infrastructure/Utils/GenerateJwtToken.cs | 25 +++++-- src/Web/Controllers/GoogleController.cs | 12 ++- src/Web/Endpoints/Users.cs | 10 +-- src/Web/Messages/Data/Message.cs | 13 ++++ src/Web/Messages/Data/MessagingDbContext.cs | 19 +++++ src/Web/Messages/Handlers/GetMessages.cs | 30 ++++++++ .../Messages/Handlers/GetMessagesByUser.cs | 33 ++++++++ src/Web/Messages/Handlers/PostMessage.cs | 34 +++++++++ src/Web/Messages/Handlers/PostReplyMessage.cs | 37 +++++++++ .../20240627081653_Initial.Designer.cs | 59 +++++++++++++++ .../Migrations/20240627081653_Initial.cs | 38 ++++++++++ .../MessagingDbContextModelSnapshot.cs | 56 ++++++++++++++ src/Web/Messages/Shared.cs | 42 +++++++++++ src/Web/Program.cs | 75 ++++++++++++------- src/Web/Web.csproj | 31 ++++---- src/Web/Web.http | 14 +++- src/Web/appsettings.Development.json | 3 +- .../CustomWebApplicationFactory.cs | 1 + 21 files changed, 485 insertions(+), 61 deletions(-) create mode 100644 src/Web/Messages/Data/Message.cs create mode 100644 src/Web/Messages/Data/MessagingDbContext.cs create mode 100644 src/Web/Messages/Handlers/GetMessages.cs create mode 100644 src/Web/Messages/Handlers/GetMessagesByUser.cs create mode 100644 src/Web/Messages/Handlers/PostMessage.cs create mode 100644 src/Web/Messages/Handlers/PostReplyMessage.cs create mode 100644 src/Web/Messages/Migrations/20240627081653_Initial.Designer.cs create mode 100644 src/Web/Messages/Migrations/20240627081653_Initial.cs create mode 100644 src/Web/Messages/Migrations/MessagingDbContextModelSnapshot.cs create mode 100644 src/Web/Messages/Shared.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index a2b4869..3c60acd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,7 @@ + diff --git a/src/Application/Common/Models/UserModel.cs b/src/Application/Common/Models/UserModel.cs index 5c4358b..71a2680 100644 --- a/src/Application/Common/Models/UserModel.cs +++ b/src/Application/Common/Models/UserModel.cs @@ -1,5 +1,6 @@ namespace Hutopy.Application.Common.Models; +// TODO: Review nullable affectation here public class UserModel { public string? Id { get; set; } @@ -7,4 +8,5 @@ public class UserModel public string? FirstName { get; set; } public string? LastName { get; set; } public string? Email { get; set; } + public string? PortraitUrl { get; set; } } diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 9997316..7cdf080 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Google.Apis.Oauth2.v2.Data; using System.Security.Claims; using Hutopy.Application.Common.Interfaces; @@ -220,14 +221,20 @@ public class IdentityService( } var user = await GetUserByUserNameAsync(userName); + + if (user is null) throw new InvalidOperationException(); var jwtSection = configuration.GetRequiredSection("Authentication:Jwt"); - + var token = JwtTokenHelper.GenerateJwtToken( issuer: jwtSection["Issuer"] ?? "", audience: jwtSection["Audience"] ?? "", key: jwtSection["Key"] ?? "", - userId: user?.Id ?? ""); + userId: user.Id, + email: user.Email, + firstname: user.FirstName, + lastname: user.LastName, + portraitUrl: user.PortraitUrl); return token; } diff --git a/src/Infrastructure/Utils/GenerateJwtToken.cs b/src/Infrastructure/Utils/GenerateJwtToken.cs index 00423f9..19e6cbd 100644 --- a/src/Infrastructure/Utils/GenerateJwtToken.cs +++ b/src/Infrastructure/Utils/GenerateJwtToken.cs @@ -7,20 +7,31 @@ namespace Hutopy.Infrastructure.Utils; public static class JwtTokenHelper { - public static string GenerateJwtToken(string issuer, string audience, string key, string userId) + public static string GenerateJwtToken(string issuer, string audience, string key, string? userId, string? email, + string? firstname, string? lastname, string? portraitUrl) { var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + var claims = new List(new[] + { + new Claim(JwtRegisteredClaimNames.Sub, userId), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(ClaimTypes.NameIdentifier, userId), + new Claim(ClaimTypes.Email, email), + new Claim(ClaimTypes.GivenName, firstname), + new Claim(ClaimTypes.Surname, lastname), + }); + + if (portraitUrl is not null) + { + claims.Add(new Claim("portrait-url", portraitUrl)); + } + var token = new JwtSecurityToken( issuer: issuer, audience: audience, - claims: new[] - { - new Claim(JwtRegisteredClaimNames.Sub, userId), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), - new Claim(ClaimTypes.NameIdentifier, userId) - }, + claims: claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: credentials); diff --git a/src/Web/Controllers/GoogleController.cs b/src/Web/Controllers/GoogleController.cs index 40e8adc..dfaa91e 100644 --- a/src/Web/Controllers/GoogleController.cs +++ b/src/Web/Controllers/GoogleController.cs @@ -64,10 +64,14 @@ public class GoogleController(IIdentityService identityService, IHttpClientFacto var jwtSection = configuration.GetRequiredSection("Authentication:Jwt"); var token = JwtTokenHelper.GenerateJwtToken( - issuer: jwtSection["Issuer"] ?? throw new ArgumentNullException("The Jwt issuer is missing."), - audience: jwtSection["Audience"] ?? throw new ArgumentNullException("The Jwt audience is missing."), - key: jwtSection["Key"] ?? throw new ArgumentNullException("The Jwt key is missing."), - userId: user.Id); + jwtSection["Issuer"] ?? throw new ArgumentNullException("The Jwt issuer is missing."), + jwtSection["Audience"] ?? throw new ArgumentNullException("The Jwt audience is missing."), + jwtSection["Key"] ?? throw new ArgumentNullException("The Jwt key is missing."), + user.Id, + user.Email, + user.FirstName, + user.LastName, + user.PortraitUrl); return Ok(new { accessToken = token, email }); } diff --git a/src/Web/Endpoints/Users.cs b/src/Web/Endpoints/Users.cs index c344509..bcfa964 100644 --- a/src/Web/Endpoints/Users.cs +++ b/src/Web/Endpoints/Users.cs @@ -23,18 +23,16 @@ public class Users : EndpointGroupBase { return await sender.Send(query); } - + private static async Task Login(ISender sender, LoginCommand command) { return await sender.Send(command); } - + private static async Task UploadProfilePicture(ISender sender, Stream stream) { - var command = new UploadProfilePictureCommand - { - ProfilePicture = stream - }; + var command = new UploadProfilePictureCommand { ProfilePicture = stream }; + return await sender.Send(command); } } diff --git a/src/Web/Messages/Data/Message.cs b/src/Web/Messages/Data/Message.cs new file mode 100644 index 0000000..df0fa50 --- /dev/null +++ b/src/Web/Messages/Data/Message.cs @@ -0,0 +1,13 @@ +namespace Hutopy.Web.Messages.Data; + +public class Message +{ + public Guid Id { get; init; } + public Guid ContentId { get; init; } // works for any - VideoId, ChatId, RoomId, xxxId, ForumId + public Guid CreatedBy { get; init; } + public DateTime CreatedAt { get; } + + public Guid ParentId { get; init; } + + public string Value { get; init; } = null!; +} diff --git a/src/Web/Messages/Data/MessagingDbContext.cs b/src/Web/Messages/Data/MessagingDbContext.cs new file mode 100644 index 0000000..28bb977 --- /dev/null +++ b/src/Web/Messages/Data/MessagingDbContext.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; + +namespace Hutopy.Web.Messages.Data; + +public class MessagingDbContext( + DbContextOptions options) + : DbContext(options) +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .Property(c => c.CreatedAt) + .ValueGeneratedOnAdd() + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + } + + public DbSet Messages { get; set; } +} diff --git a/src/Web/Messages/Handlers/GetMessages.cs b/src/Web/Messages/Handlers/GetMessages.cs new file mode 100644 index 0000000..d7afb3f --- /dev/null +++ b/src/Web/Messages/Handlers/GetMessages.cs @@ -0,0 +1,30 @@ +using FastEndpoints; +using Hutopy.Web.Messages.Data; +using Microsoft.EntityFrameworkCore; + +namespace Hutopy.Web.Messages.Handlers; + +public class GetMessages( + MessagingDbContext context) + : EndpointWithoutRequest> +{ + public override void Configure() + { + Tags("Messages"); + Get("/api/messages/{ContentId:guid}"); + AllowAnonymous(); + } + + public override async Task HandleAsync( + CancellationToken ct) + { + var contentId = Route("ContentId"); + + var comments = await context + .Messages + .Where(c => c.ContentId == contentId) + .ToListAsync(cancellationToken: ct); + + await SendAsync(comments, cancellation: ct); + } +} diff --git a/src/Web/Messages/Handlers/GetMessagesByUser.cs b/src/Web/Messages/Handlers/GetMessagesByUser.cs new file mode 100644 index 0000000..d70cd11 --- /dev/null +++ b/src/Web/Messages/Handlers/GetMessagesByUser.cs @@ -0,0 +1,33 @@ +using FastEndpoints; +using Hutopy.Web.Messages.Data; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Hutopy.Web.Messages.Handlers; + +public record GetMessagesByUserRequest( + [FromRoute] Guid UserId); + +public class GetMessagesByUser( + MessagingDbContext context) + : EndpointWithoutRequest> +{ + public override void Configure() + { + Tags("Messages"); + Get("/api/messages/by-user/{UserId:guid}"); + } + + public override async Task HandleAsync( + CancellationToken ct) + { + var userId = Route("UserId"); + + var posts = await context + .Messages + .Where(c => c.CreatedBy == userId) + .ToListAsync(cancellationToken: ct); + + await SendAsync(posts, cancellation: ct); + } +} diff --git a/src/Web/Messages/Handlers/PostMessage.cs b/src/Web/Messages/Handlers/PostMessage.cs new file mode 100644 index 0000000..7460728 --- /dev/null +++ b/src/Web/Messages/Handlers/PostMessage.cs @@ -0,0 +1,34 @@ +using FastEndpoints; +using Hutopy.Web.Messages.Data; + +namespace Hutopy.Web.Messages.Handlers; + +public record PostMessageRequest( + Guid ContentId, + string Message); + +public class PostMessage( + MessagingDbContext context) + : Endpoint +{ + public override void Configure() + { + // TODO: Find how to specify the name we see in Swagger + Tags("Messages"); + Post("/api/messages"); + } + + public override async Task HandleAsync( + PostMessageRequest req, + CancellationToken ct) + { + await context.Messages.AddAsync( + new Message { + ContentId = req.ContentId, + CreatedBy = User.GetUserId(), + Value = req.Message }, + ct); + + await context.SaveChangesAsync(ct); + } +} diff --git a/src/Web/Messages/Handlers/PostReplyMessage.cs b/src/Web/Messages/Handlers/PostReplyMessage.cs new file mode 100644 index 0000000..2385765 --- /dev/null +++ b/src/Web/Messages/Handlers/PostReplyMessage.cs @@ -0,0 +1,37 @@ +using FastEndpoints; +using Hutopy.Web.Messages.Data; + +namespace Hutopy.Web.Messages.Handlers; + +public record PostReplyMessageRequest( + Guid ContentId, + Guid ParentId, + string Message); + +public sealed class PostReplyMessage( + MessagingDbContext context) + : Endpoint +{ + public override void Configure() + { + Tags("Messages"); + Post("/api/messages/reply"); + } + + public override async Task HandleAsync( + PostReplyMessageRequest req, + CancellationToken ct) + { + await context.Messages.AddAsync( + new Message + { + ContentId = req.ContentId, + ParentId = req.ParentId, + CreatedBy = User.GetUserId(), + Value = req.Message + }, + ct); + + await context.SaveChangesAsync(ct); + } +} diff --git a/src/Web/Messages/Migrations/20240627081653_Initial.Designer.cs b/src/Web/Messages/Migrations/20240627081653_Initial.Designer.cs new file mode 100644 index 0000000..72d8e1c --- /dev/null +++ b/src/Web/Messages/Migrations/20240627081653_Initial.Designer.cs @@ -0,0 +1,59 @@ +// +using System; +using Hutopy.Web.Messages.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hutopy.Web.Messages.Migrations +{ + [DbContext(typeof(MessagingDbContext))] + [Migration("20240627081653_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Web.Messages.Data.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("ParentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Web/Messages/Migrations/20240627081653_Initial.cs b/src/Web/Messages/Migrations/20240627081653_Initial.cs new file mode 100644 index 0000000..bd5664e --- /dev/null +++ b/src/Web/Messages/Migrations/20240627081653_Initial.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Web.Messages.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Messages", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ContentId = table.Column(type: "uniqueidentifier", nullable: false), + CreatedBy = table.Column(type: "uniqueidentifier", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), + ParentId = table.Column(type: "uniqueidentifier", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Messages", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Messages"); + } + } +} diff --git a/src/Web/Messages/Migrations/MessagingDbContextModelSnapshot.cs b/src/Web/Messages/Migrations/MessagingDbContextModelSnapshot.cs new file mode 100644 index 0000000..be9a4b9 --- /dev/null +++ b/src/Web/Messages/Migrations/MessagingDbContextModelSnapshot.cs @@ -0,0 +1,56 @@ +// +using System; +using Hutopy.Web.Messages.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hutopy.Web.Messages.Migrations +{ + [DbContext(typeof(MessagingDbContext))] + partial class MessagingDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Web.Messages.Data.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("ParentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Web/Messages/Shared.cs b/src/Web/Messages/Shared.cs new file mode 100644 index 0000000..eb48399 --- /dev/null +++ b/src/Web/Messages/Shared.cs @@ -0,0 +1,42 @@ +using System.Security.Claims; + +namespace Hutopy.Web.Messages; + +public class Shared(string claimName) : Exception; + +public static class ClaimsPrincipalExtensions +{ + public static Guid GetUserId(this ClaimsPrincipal claims) + { + return (Guid)claims.GetFirstValue(ClaimTypes.NameIdentifier); + } + + public static string GetFirstName(this ClaimsPrincipal claims) + { + return (string)claims.GetFirstValue(ClaimTypes.GivenName); + } + + public static string GetLastName(this ClaimsPrincipal claims) + { + return (string)claims.GetFirstValue(ClaimTypes.Surname); + } + + public static string GetEmail(this ClaimsPrincipal claims) + { + return (string)claims.GetFirstValue(ClaimTypes.Email); + } + + public static object GetFirstValue(this ClaimsPrincipal claims, string key) + { + var claim = claims.FindFirst(key); + + if (claim is null) throw new Shared(key); + + if (typeof(TValue) == typeof(Guid)) + { + return Guid.Parse(claim.Value); + } + + return Convert.ChangeType(claim.Value, typeof(TValue)); + } +} diff --git a/src/Web/Program.cs b/src/Web/Program.cs index ea5b072..78700bc 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -1,10 +1,14 @@ +using Azure.Identity; +using FastEndpoints; using Hutopy.Application; using Hutopy.Infrastructure; using Hutopy.Infrastructure.Data; using Hutopy.Web; -using Azure.Identity; +using Hutopy.Web.Messages.Data; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.EntityFrameworkCore; using NSwag; +using NSwag.Generation.AspNetCore.Processors; using NSwag.Generation.Processors.Security; var builder = WebApplication.CreateBuilder(args); @@ -16,31 +20,31 @@ if (!builder.Environment.IsDevelopment()) } builder.Services.AddCors(options => +{ + options.AddPolicy("AllowAll", builder => { - options.AddPolicy("AllowAll", builder => - { - builder.AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader(); - }); - - options.AddPolicy("AllowHutopyUi", builder => - { - builder.WithOrigins("https://zealous-bay-08204590f.5.azurestaticapps.net") - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials(); - }); - - options.AddPolicy("AllowHutopyUiPreview", builder => - { - builder.WithOrigins("https://zealous-bay-08204590f-preview.eastus2.5.azurestaticapps.net") - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials(); - }); + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); }); + options.AddPolicy("AllowHutopyUi", builder => + { + builder.WithOrigins("https://zealous-bay-08204590f.5.azurestaticapps.net") + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + }); + + options.AddPolicy("AllowHutopyUiPreview", builder => + { + builder.WithOrigins("https://zealous-bay-08204590f-preview.eastus2.5.azurestaticapps.net") + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + }); +}); + // Add services to the container. builder.Services.AddKeyVaultIfConfigured(builder.Configuration); @@ -48,6 +52,8 @@ builder.Services.AddApplicationServices(); builder.Services.AddInfrastructureServices(builder.Configuration); builder.Services.AddWebServices(); builder.Services.AddAuthorizationAndAuthentication(builder.Configuration); + +// TODO: This old tech should be remove - need to move Facebook / Google controllers to FastEndpoints builder.Services.AddControllers(); builder.Services.AddOpenApiDocument((configure, sp) => @@ -63,11 +69,19 @@ builder.Services.AddOpenApiDocument((configure, sp) => Type = OpenApiSecuritySchemeType.ApiKey, Name = "Authorization", In = OpenApiSecurityApiKeyLocation.Header, - Description = "Type into the textbox: Bearer {your JWT token}." + Description = "Type into the textbox: Bearer {your JWT token}.", }); - + + configure.OperationProcessors.Add(new AspNetCoreOperationTagsProcessor()); configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); - }); +}); + +builder.Services.AddFastEndpoints(); + +builder.Services.AddDbContext((_, options) => +{ + options.UseSqlServer(builder.Configuration.GetConnectionString("CommentStore")); +}); var app = builder.Build(); @@ -110,6 +124,13 @@ app.MapControllerRoute( // app.UseExceptionHandler(); app.MapEndpoints(); +app.UseFastEndpoints(); + app.Run(); -public abstract partial class Program { } +namespace Hutopy.Web +{ + public abstract partial class Program + { + } +} diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 80a1ea9..053126a 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -7,26 +7,31 @@ - - + + - - - - - - - - - - + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + + + + + diff --git a/src/Web/Web.http b/src/Web/Web.http index c0ac5aa..054f4e4 100644 --- a/src/Web/Web.http +++ b/src/Web/Web.http @@ -23,7 +23,7 @@ Content-Type: application/json "password": "{{Password}}" } -> {% client.global.set("auth_token", response.body); %} +> {% client.global.set("auth_token", response.body.accessToken); %} ### @@ -46,4 +46,16 @@ Authorization: Bearer {{auth_token}} # GET GetMyUser GET {{base_url}}/api/GetMyUser +Authorization: Bearer {{auth_token}} + +### + +# GET /api/posts + +GET {{base_url}}/api/messages/00000001-0000-0000-0000-000000000001 + +### + +# GET /api/posts/by-user +GET {{base_url}}/api/messages/by-user/325C69E8-DBC4-4CEE-B56E-C3C90AEE7963 Authorization: Bearer {{auth_token}} \ No newline at end of file diff --git a/src/Web/appsettings.Development.json b/src/Web/appsettings.Development.json index 43ba880..9e2b285 100644 --- a/src/Web/appsettings.Development.json +++ b/src/Web/appsettings.Development.json @@ -8,7 +8,8 @@ } }, "ConnectionStrings": { - "DefaultConnection": "Server=localhost,1433;Initial Catalog=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True;MultiSubnetFailover=True" + "DefaultConnection": "Server=localhost,1433;Initial Catalog=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True;MultiSubnetFailover=True", + "CommentStore": "Server=localhost,1433;Initial Catalog=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True;MultiSubnetFailover=True" }, "Authentication": { "Jwt": { diff --git a/tests/Application.FunctionalTests/CustomWebApplicationFactory.cs b/tests/Application.FunctionalTests/CustomWebApplicationFactory.cs index d99727f..c1d8a80 100644 --- a/tests/Application.FunctionalTests/CustomWebApplicationFactory.cs +++ b/tests/Application.FunctionalTests/CustomWebApplicationFactory.cs @@ -1,6 +1,7 @@ using System.Data.Common; using Hutopy.Application.Common.Interfaces; using Hutopy.Infrastructure.Data; +using Hutopy.Web; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; From 0e318eae32c9a781847c00be3b5ac606a92d8ee8 Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Sat, 29 Jun 2024 22:45:17 -0400 Subject: [PATCH 11/14] update-current-user WIP --- .../Common/Interfaces/IIdentityService.cs | 20 +- src/Application/Common/Models/Result.cs | 27 +- src/Application/Common/Models/UserModel.cs | 13 +- .../Commands/UpdateCurrentUserCommand.cs | 40 ++ .../Users/Models/SocialNetworksModel.cs | 13 + .../Queries/GetCurrentUser/GetCurrentUser.cs | 18 +- .../Users/Queries/GetCurrentUser/UserDto.cs | 12 + .../Identity/ApplicationUser.cs | 7 + .../Identity/IdentityService.cs | 99 +++- ...6_AddMissingInformationsToUser.Designer.cs | 497 ++++++++++++++++++ ...0630001806_AddMissingInformationsToUser.cs | 95 ++++ .../ApplicationDbContextModelSnapshot.cs | 28 + src/Web/Endpoints/UpdateMyUser.cs | 20 + 13 files changed, 855 insertions(+), 34 deletions(-) create mode 100644 src/Application/Users/Commands/UpdateCurrentUserCommand.cs create mode 100644 src/Application/Users/Models/SocialNetworksModel.cs create mode 100644 src/Infrastructure/Migrations/20240630001806_AddMissingInformationsToUser.Designer.cs create mode 100644 src/Infrastructure/Migrations/20240630001806_AddMissingInformationsToUser.cs create mode 100644 src/Web/Endpoints/UpdateMyUser.cs diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index cd97117..d92663e 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -1,21 +1,27 @@ using Google.Apis.Oauth2.v2.Data; using Hutopy.Application.Common.Models; +using Hutopy.Application.Users.Models; namespace Hutopy.Application.Common.Interfaces; public interface IIdentityService { - Task GetUserNameAsync(string userId); - Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password); - Task FindUserByIdAsync(string id); + Task> CreateUserAsync(Userinfo userInfo); + Task> CreateUserAsync(string email, string userName, string firstName, string lastName, string password); Task GetCurrentUserAsync(); - Task FindUserByEmailAsync(string id); - Task LoginAsync(string email, string password); + Task> UpdateCurrentUserAsync(string id, string firstName, string lastName, string occupation, + string phoneNumber, string birthDate, string country, string city, string address, string about, + string description, + SocialNetworksModel socialNetworks); + Task> GetCurrentUserRolesAsync(); + Task FindUserByIdAsync(string id); + Task FindUserByEmailAsync(string email); Task GetUserByUserNameAsync(string userName); + Task LoginAsync(string email, string password); Task IsInRoleAsync(string userId, string role); Task AuthorizeAsync(string userId, string policyName); + Task GetUserNameAsync(string userId); + Task AddRoleAsync(string userId, string role); - Task> GetCurrentUserRolesAsync(); - Task<(Result Result, string UserId)> CreateUserAsync(Userinfo userInfo); Task DeleteUserAsync(string userId); } diff --git a/src/Application/Common/Models/Result.cs b/src/Application/Common/Models/Result.cs index f99bb27..9552531 100644 --- a/src/Application/Common/Models/Result.cs +++ b/src/Application/Common/Models/Result.cs @@ -5,9 +5,8 @@ public class Result( IEnumerable errors) { public bool Succeeded { get; init; } = succeeded; - public string[] Errors { get; init; } = errors.ToArray(); - + public static Result Success() { return new Result(true, Array.Empty()); @@ -18,3 +17,27 @@ public class Result( return new Result(false, errors); } } + +public class Result( + bool succeeded, + IEnumerable errors) +{ + public bool Succeeded { get; init; } = succeeded; + public string[] Errors { get; init; } = errors.ToArray(); + public T? Value { get; set; } + + public T GetValueOrDefault() + { + return Value ?? default(T)!; + } + + public static Result Success() + { + return new Result(true, Array.Empty()); + } + + public static Result Failure(IEnumerable errors) + { + return new Result(false, errors); + } +} diff --git a/src/Application/Common/Models/UserModel.cs b/src/Application/Common/Models/UserModel.cs index 71a2680..ac09db9 100644 --- a/src/Application/Common/Models/UserModel.cs +++ b/src/Application/Common/Models/UserModel.cs @@ -1,3 +1,5 @@ +using Hutopy.Application.Users.Models; + namespace Hutopy.Application.Common.Models; // TODO: Review nullable affectation here @@ -7,6 +9,15 @@ public class UserModel public string? UserName { get; set; } public string? FirstName { get; set; } public string? LastName { get; set; } - public string? Email { get; set; } + public string? Occupation { get; set; } + public string? Email { get; init; } = String.Empty; + public string? Phone { get; init; } = String.Empty; + public string? BirthDate { get; init; } = String.Empty; + public string? Country { get; init; } = String.Empty; + public string? City { get; init; } = String.Empty; + public string? Address { get; init; } = String.Empty; + public string? About { get; init; } = String.Empty; + public string? Description { get; init; } = String.Empty; + public SocialNetworksModel SocialNetworks { get; init; } = new(); public string? PortraitUrl { get; set; } } diff --git a/src/Application/Users/Commands/UpdateCurrentUserCommand.cs b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs new file mode 100644 index 0000000..3a23915 --- /dev/null +++ b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs @@ -0,0 +1,40 @@ +using Hutopy.Application.Common.Interfaces; +using Hutopy.Application.Users.Models; + +namespace Hutopy.Application.Users.Commands; + +public class UpdateCurrentUserCommand : IRequest +{ + public required string FirstName { get; init; } + public required string LastName { get; init; } + public required string Occupation { get; init; } + public required string PhoneNumber { get; init; } + public required string BirthDate { get; init; } + public required string Country { get; init; } + public required string City { get; init; } + public required string Address { get; init; } + public required string About { get; init; } + public required string Description { get; init; } + public required SocialNetworksModel SocialNetworks { get; init; } +} + +public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIdentityService identityService) : + IRequestHandler +{ + public async Task Handle(UpdateCurrentUserCommand request, CancellationToken cancellationToken) + { + var identityUser = await identityService.GetCurrentUserAsync(); + + if (identityUser?.Id is null) return string.Empty; + + var result = await identityService.UpdateCurrentUserAsync(identityUser.Id, request.FirstName, request.LastName, + request.Occupation, request.PhoneNumber, request.BirthDate, + request.Country, request.City, request.Address, request.About, + request.Description, request.SocialNetworks); + + await context.SaveChangesAsync(cancellationToken); + + return result.GetValueOrDefault(); + } +} + diff --git a/src/Application/Users/Models/SocialNetworksModel.cs b/src/Application/Users/Models/SocialNetworksModel.cs new file mode 100644 index 0000000..edd3cce --- /dev/null +++ b/src/Application/Users/Models/SocialNetworksModel.cs @@ -0,0 +1,13 @@ +namespace Hutopy.Application.Users.Models; + +public class SocialNetworksModel +{ + public string FacebookUrl { get; init; } = String.Empty; + public string InstagramUrl { get; init; } = String.Empty; + public string XUrl { get; init; } = String.Empty; + public string LinkedInUrl { get; init; } = String.Empty; + public string TikTokUrl { get; init; } = String.Empty; + public string YoutubeUrl { get; init; } = String.Empty; + public string RedditUrl { get; init; } = String.Empty; + public string YourWebsiteUrl { get; init; } = String.Empty; +} diff --git a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs index 7cbabd6..eeb9f7c 100644 --- a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs +++ b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs @@ -28,12 +28,22 @@ public class GetCurrentUserQueryHandler( var user = new UserDto { Id = currentUserId, - FirstName = identityUser?.FirstName ?? "", - LastName = identityUser?.LastName ?? "", - UserName = identityUser?.UserName ?? "", + FirstName = identityUser.FirstName ?? "", + LastName = identityUser.LastName ?? "", + UserName = identityUser.UserName ?? "", + Occupation = identityUser.Occupation ?? "", + Phone = identityUser.Phone ?? "", + Email = identityUser.Email ?? "", + BirthDate = identityUser.BirthDate ?? "", + Country = identityUser.Country ?? "", + City = identityUser.City ?? "", + Address = identityUser.Address ?? "", + About = identityUser.About ?? "", + Description = identityUser.Description ?? "", + SocialNetworks = identityUser.SocialNetworks, UserTransactions = transactions, TotalBalance = transactions.Sum(x => x.Amount), - UserRoles = roles + UserRoles = roles, }; return user; diff --git a/src/Application/Users/Queries/GetCurrentUser/UserDto.cs b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs index c7d121d..c0b34c1 100644 --- a/src/Application/Users/Queries/GetCurrentUser/UserDto.cs +++ b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs @@ -1,3 +1,5 @@ +using Hutopy.Application.Users.Models; + namespace Hutopy.Application.Users.Queries.GetCurrentUser; public class UserDto @@ -6,6 +8,16 @@ public class UserDto public required string FirstName { get; init; } public required string LastName { get; init; } public string UserName { get; init; } = String.Empty; + public string Occupation { get; init; } = String.Empty; + public string Email { get; init; } = String.Empty; + public string Phone { get; init; } = String.Empty; + public string BirthDate { get; init; } = String.Empty; + public string Country { get; init; } = String.Empty; + public string City { get; init; } = String.Empty; + public string Address { get; init; } = String.Empty; + public string About { get; init; } = String.Empty; + public string Description { get; init; } = String.Empty; + public SocialNetworksModel SocialNetworks { get; init; } = new(); public List UserTransactions { get; init; } = []; public IList UserRoles { get; init; } = []; public required decimal TotalBalance { get; init; } diff --git a/src/Infrastructure/Identity/ApplicationUser.cs b/src/Infrastructure/Identity/ApplicationUser.cs index 700a074..d956a9b 100644 --- a/src/Infrastructure/Identity/ApplicationUser.cs +++ b/src/Infrastructure/Identity/ApplicationUser.cs @@ -7,5 +7,12 @@ public class ApplicationUser : IdentityUser { public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; + public string Occupation { get; set; } = string.Empty; + public string BirthDate { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + public string City { get; set; } = string.Empty; + public string Address { get; set; } = string.Empty; + public string About { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; public SocialNetworks SocialNetworks { get; set; } = new(); } diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 7cdf080..d512ae5 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -3,6 +3,8 @@ using Google.Apis.Oauth2.v2.Data; using System.Security.Claims; using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Models; +using Hutopy.Application.Users.Models; +using Hutopy.Infrastructure.Identity.OwnedEntities; using Hutopy.Infrastructure.Utils; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -45,23 +47,10 @@ public class IdentityService( return userModel; } - - public async Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password) - { - var user = new ApplicationUser - { - UserName = userName, - Email = userName, - }; - - var result = await userManager.CreateAsync(user, password); - - return (result.ToApplicationResult(), user.Id); - } - public async Task<(Result Result, string UserId)> CreateUserAsync(Userinfo userInfo) + public async Task> CreateUserAsync(Userinfo userInfo) { - var user = new ApplicationUser + var applicationUser = new ApplicationUser { UserName = userInfo.Name, Email = userInfo.Email, @@ -71,12 +60,18 @@ public class IdentityService( var password = Guid.NewGuid().ToString("N")[..32]; - var result = await userManager.CreateAsync(user, password); + var identityResult = await userManager.CreateAsync(applicationUser, password); - return (result.ToApplicationResult(), user.Id); + var applicationResult = identityResult.ToApplicationResult(); + + var result = new Result(applicationResult.Succeeded, applicationResult.Errors); + + result.Value = applicationUser.Id; + + return result; } - public async Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password) + public async Task> CreateUserAsync(string email, string userName, string firstName, string lastName, string password) { var applicationUser = new ApplicationUser { @@ -88,7 +83,52 @@ public class IdentityService( var response = await userManager.CreateAsync(applicationUser, password); - return response.ToApplicationResult(); + var result = new Result(response.Succeeded, response.ToApplicationResult().Errors); + result.Value = applicationUser.Id; + + return result; + } + + public async Task> UpdateCurrentUserAsync(string id, string firstName, string lastName, string occupation, + string phoneNumber, string birthDate, string country, string city, string address, string about, string description, + SocialNetworksModel socialNetworks) + { + var applicationUser = await userManager.FindByIdAsync(id); + + if (applicationUser is null) return Result.Failure(new[] { "User not found." }); + + applicationUser.FirstName = firstName; + applicationUser.LastName = lastName; + applicationUser.Occupation = occupation; + applicationUser.PhoneNumber = phoneNumber; + applicationUser.BirthDate = birthDate; + applicationUser.Country = country; + applicationUser.City = city; + applicationUser.Address = address; + applicationUser.About = about; + applicationUser.Description = description; + applicationUser.SocialNetworks = new SocialNetworks() + { + FacebookUrl = socialNetworks.FacebookUrl, + InstagramUrl = socialNetworks.InstagramUrl, + XUrl = socialNetworks.XUrl, + LinkedInUrl = socialNetworks.LinkedInUrl, + TikTokUrl = socialNetworks.TikTokUrl, + YoutubeUrl = socialNetworks.YoutubeUrl, + RedditUrl = socialNetworks.RedditUrl, + YourWebsiteUrl = socialNetworks.YourWebsiteUrl + }; + + var response = await userManager.UpdateAsync(applicationUser); + + var applicationResult = response.ToApplicationResult(); + + var result = new Result(applicationResult.Succeeded, + applicationResult.Errors); + + result.Value = id; + + return result; } public async Task FindUserByIdAsync(string id) @@ -97,13 +137,32 @@ public class IdentityService( if (response == null) return null; - var userModel = new UserModel() + var userModel = new UserModel { Id = response.Id, UserName = response.UserName, FirstName = response.FirstName, LastName = response.LastName, Email = response.Email, + Occupation = response.Occupation, + Phone = response.PhoneNumber, + BirthDate = response.BirthDate, + Country = response.Country, + City = response.City, + Address = response.Address, + About = response.About, + Description = response.Description, + SocialNetworks = new SocialNetworksModel + { + FacebookUrl = response.SocialNetworks.FacebookUrl, + InstagramUrl = response.SocialNetworks.InstagramUrl, + XUrl = response.SocialNetworks.XUrl, + LinkedInUrl = response.SocialNetworks.LinkedInUrl, + TikTokUrl = response.SocialNetworks.TikTokUrl, + YoutubeUrl = response.SocialNetworks.YoutubeUrl, + RedditUrl = response.SocialNetworks.RedditUrl, + YourWebsiteUrl = response.SocialNetworks.YourWebsiteUrl, + } }; return userModel; diff --git a/src/Infrastructure/Migrations/20240630001806_AddMissingInformationsToUser.Designer.cs b/src/Infrastructure/Migrations/20240630001806_AddMissingInformationsToUser.Designer.cs new file mode 100644 index 0000000..2a9623e --- /dev/null +++ b/src/Infrastructure/Migrations/20240630001806_AddMissingInformationsToUser.Designer.cs @@ -0,0 +1,497 @@ +// +using System; +using Hutopy.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240630001806_AddMissingInformationsToUser")] + partial class AddMissingInformationsToUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Domain.Entities.FutureCreator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReasonToJoin") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SocialNetworkAccount") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("FutureCreators"); + }); + + modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsConfirmed") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Paid") + .HasColumnType("bit"); + + b.Property("StripeBillingDetailEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeBillingDetailName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeChargeId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeEventId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentIntent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeReceiptUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TipMessage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("UserTransactions"); + }); + + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("About") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BirthDate") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .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("Occupation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + 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.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("Hutopy.Domain.Entities.UserTransaction", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => + { + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.SocialNetworks", "SocialNetworks", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("FacebookUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("InstagramUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("LinkedInUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("RedditUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("TikTokUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("XUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("YourWebsiteUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("YoutubeUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_SocialNetworks", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.Navigation("SocialNetworks") + .IsRequired(); + }); + + 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("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.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("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Migrations/20240630001806_AddMissingInformationsToUser.cs b/src/Infrastructure/Migrations/20240630001806_AddMissingInformationsToUser.cs new file mode 100644 index 0000000..cd1a612 --- /dev/null +++ b/src/Infrastructure/Migrations/20240630001806_AddMissingInformationsToUser.cs @@ -0,0 +1,95 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + /// + public partial class AddMissingInformationsToUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "About", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Address", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "BirthDate", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "City", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Country", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Description", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Occupation", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "About", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "Address", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "BirthDate", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "City", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "Country", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "Description", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "Occupation", + table: "AspNetUsers"); + } + } +} diff --git a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index fcb6687..cd78f54 100644 --- a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -149,13 +149,37 @@ namespace Hutopy.Infrastructure.Migrations b.Property("Id") .HasColumnType("nvarchar(450)"); + b.Property("About") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("AccessFailedCount") .HasColumnType("int"); + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BirthDate") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("ConcurrencyStamp") .IsConcurrencyToken() .HasColumnType("nvarchar(max)"); + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("Email") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); @@ -185,6 +209,10 @@ namespace Hutopy.Infrastructure.Migrations .HasMaxLength(256) .HasColumnType("nvarchar(256)"); + b.Property("Occupation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("PasswordHash") .HasColumnType("nvarchar(max)"); diff --git a/src/Web/Endpoints/UpdateMyUser.cs b/src/Web/Endpoints/UpdateMyUser.cs new file mode 100644 index 0000000..60121be --- /dev/null +++ b/src/Web/Endpoints/UpdateMyUser.cs @@ -0,0 +1,20 @@ +using Hutopy.Application.Users.Commands; + +namespace Hutopy.Web.Endpoints; + +public class UpdateMyUser : EndpointGroupBase +{ + public override void Map(WebApplication app) + { + app.MapGroup(this) + .RequireAuthorization() + .MapPatch("/profile", UpdateCurrentUser); + } + + private static async Task UpdateCurrentUser(ISender sender, UpdateCurrentUserCommand command) + { + return await sender.Send(command); + } + + +} From 2603582286646994c0d093cc4fe9df5abbbf1143 Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Sun, 30 Jun 2024 12:35:14 -0400 Subject: [PATCH 12/14] Added profile ProfileColors and Profile images --- .../Constants/CommonFileNames.cs | 2 + .../Common/Interfaces/IIdentityService.cs | 8 +- src/Application/Common/Models/Result.cs | 11 +- src/Application/Common/Models/UserModel.cs | 30 +- .../Commands/ConfirmStripeTransaction.cs | 24 +- .../Commands/UpdateCurrentUserCommand.cs | 6 +- .../Users/Commands/UploadBannerPicture.cs | 27 + .../Users/Commands/UploadProfilePicture.cs | 2 + .../Users/Commands/UploadWebsiteIcon.cs | 27 + .../Users/Models/ProfileColorsModel.cs | 9 + .../Users/Models/SocialNetworksModel.cs | 16 +- .../Users/Queries/GetCurrentUser/UserDto.cs | 20 +- .../AzureBlob/AzureBlobStorageService.cs | 3 + .../Data/ApplicationDbContextInitializer.cs | 5 +- .../ApplicationUserConfiguration.cs | 10 + src/Infrastructure/DependencyInjection.cs | 3 +- .../Identity/ApplicationUser.cs | 2 + .../Identity/IdentityService.cs | 180 ++++-- .../Identity/OwnedEntities/ProfileColors.cs | 9 + .../Identity/OwnedEntities/SocialNetworks.cs | 16 +- .../Identity/OwnedEntities/StoredDataUrls.cs | 8 + ...3057_AddMoreInformationsToUser.Designer.cs | 557 ++++++++++++++++++ ...0240630163057_AddMoreInformationsToUser.cs | 77 +++ .../ApplicationDbContextModelSnapshot.cs | 60 ++ src/Infrastructure/Stripe/StripeService.cs | 3 + src/Web/Controllers/GoogleController.cs | 2 +- src/Web/Endpoints/UpdateMyUser.cs | 19 + 27 files changed, 1017 insertions(+), 119 deletions(-) create mode 100644 src/Application/Users/Commands/UploadBannerPicture.cs create mode 100644 src/Application/Users/Commands/UploadWebsiteIcon.cs create mode 100644 src/Application/Users/Models/ProfileColorsModel.cs create mode 100644 src/Infrastructure/Identity/OwnedEntities/ProfileColors.cs create mode 100644 src/Infrastructure/Identity/OwnedEntities/StoredDataUrls.cs create mode 100644 src/Infrastructure/Migrations/20240630163057_AddMoreInformationsToUser.Designer.cs create mode 100644 src/Infrastructure/Migrations/20240630163057_AddMoreInformationsToUser.cs diff --git a/src/Application/AzureBlobStorage/Constants/CommonFileNames.cs b/src/Application/AzureBlobStorage/Constants/CommonFileNames.cs index 7eabb9a..b6d44aa 100644 --- a/src/Application/AzureBlobStorage/Constants/CommonFileNames.cs +++ b/src/Application/AzureBlobStorage/Constants/CommonFileNames.cs @@ -3,4 +3,6 @@ public static class CommonFileNames { public static string ProfilePicture = "profilePicture"; + public static string BannerPicture = "bannerPicture"; + public static string WebsiteIcon = "websiteIcon"; } diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index d92663e..74e9240 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -9,10 +9,10 @@ public interface IIdentityService Task> CreateUserAsync(Userinfo userInfo); Task> CreateUserAsync(string email, string userName, string firstName, string lastName, string password); Task GetCurrentUserAsync(); - Task> UpdateCurrentUserAsync(string id, string firstName, string lastName, string occupation, - string phoneNumber, string birthDate, string country, string city, string address, string about, - string description, - SocialNetworksModel socialNetworks); + Task UpdateCurrentUserBannerPictureUrlAsync(string url); + Task UpdateCurrentUserProfilePictureUrlAsync(string url); + Task UpdateCurrentUserWebsiteIconUrlAsync(string url); + Task> UpdateCurrentUserAsync(UserModel userModel); Task> GetCurrentUserRolesAsync(); Task FindUserByIdAsync(string id); Task FindUserByEmailAsync(string email); diff --git a/src/Application/Common/Models/Result.cs b/src/Application/Common/Models/Result.cs index 9552531..294e2ed 100644 --- a/src/Application/Common/Models/Result.cs +++ b/src/Application/Common/Models/Result.cs @@ -19,25 +19,26 @@ public class Result( } public class Result( + T? value, bool succeeded, IEnumerable errors) { public bool Succeeded { get; init; } = succeeded; public string[] Errors { get; init; } = errors.ToArray(); - public T? Value { get; set; } + public T? Value { get; set; } = value; public T GetValueOrDefault() { return Value ?? default(T)!; } - public static Result Success() + public static Result Success(T value) { - return new Result(true, Array.Empty()); + return new Result(value, true, Array.Empty()); } - public static Result Failure(IEnumerable errors) + public static Result Failure(T value, IEnumerable errors) { - return new Result(false, errors); + return new Result(value, false, errors); } } diff --git a/src/Application/Common/Models/UserModel.cs b/src/Application/Common/Models/UserModel.cs index ac09db9..9fceab9 100644 --- a/src/Application/Common/Models/UserModel.cs +++ b/src/Application/Common/Models/UserModel.cs @@ -2,22 +2,22 @@ using Hutopy.Application.Users.Models; namespace Hutopy.Application.Common.Models; -// TODO: Review nullable affectation here public class UserModel { - public string? Id { get; set; } - public string? UserName { get; set; } - public string? FirstName { get; set; } - public string? LastName { get; set; } - public string? Occupation { get; set; } - public string? Email { get; init; } = String.Empty; - public string? Phone { get; init; } = String.Empty; - public string? BirthDate { get; init; } = String.Empty; - public string? Country { get; init; } = String.Empty; - public string? City { get; init; } = String.Empty; - public string? Address { get; init; } = String.Empty; - public string? About { get; init; } = String.Empty; - public string? Description { get; init; } = String.Empty; + public string Id { get; set; } = string.Empty; + public string UserName { get; set; } = string.Empty; + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string Occupation { get; set; } = string.Empty; + public string Email { get; init; } = string.Empty; + public string Phone { get; init; } = string.Empty; + public string BirthDate { get; init; } = string.Empty; + public string Country { get; init; } = string.Empty; + public string City { get; init; } = string.Empty; + public string Address { get; init; } = string.Empty; + public string About { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; public SocialNetworksModel SocialNetworks { get; init; } = new(); - public string? PortraitUrl { get; set; } + public ProfileColorsModel ProfileColors { get; init; } = new(); + public string ProfilePictureUrl { get; set; } = string.Empty; } diff --git a/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs b/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs index 0ddd17e..0c74658 100644 --- a/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs +++ b/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs @@ -17,29 +17,29 @@ public class Data public class Object { - public string Id { get; set; } = String.Empty; + public string Id { get; set; } = string.Empty; public int Amount { get; set; } public BillingDetails Billing_details { get; set; } = new(); - public string Calculated_statement_descriptor { get; set; } = String.Empty; - public string Currency { get; set; } = String.Empty; + public string Calculated_statement_descriptor { get; set; } = string.Empty; + public string Currency { get; set; } = string.Empty; public bool Paid { get; set; } - public string Payment_intent { get; set; } = String.Empty; - public string Payment_method { get; set; } = String.Empty; - public string Receipt_url { get; set; } = String.Empty; - public string Status { get; set; } = String.Empty; - public string Failure_message { get; set; } = String.Empty; + public string Payment_intent { get; set; } = string.Empty; + public string Payment_method { get; set; } = string.Empty; + public string Receipt_url { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public string Failure_message { get; set; } = string.Empty; } public class BillingDetails { - public string Email { get; set; } = String.Empty; - public string Name { get; set; } = String.Empty; - public string Phone { get; set; } = String.Empty; + public string Email { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; } public class Request { - public string Id { get; set; } = String.Empty; + public string Id { get; set; } = string.Empty; } public class ConfirmStripeTransactionCommandHandler( diff --git a/src/Application/Users/Commands/UpdateCurrentUserCommand.cs b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs index 3a23915..708ea6b 100644 --- a/src/Application/Users/Commands/UpdateCurrentUserCommand.cs +++ b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs @@ -16,6 +16,7 @@ public class UpdateCurrentUserCommand : IRequest public required string About { get; init; } public required string Description { get; init; } public required SocialNetworksModel SocialNetworks { get; init; } + public required ProfileColorsModel ProfileColors { get; init; } } public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIdentityService identityService) : @@ -27,10 +28,7 @@ public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIde if (identityUser?.Id is null) return string.Empty; - var result = await identityService.UpdateCurrentUserAsync(identityUser.Id, request.FirstName, request.LastName, - request.Occupation, request.PhoneNumber, request.BirthDate, - request.Country, request.City, request.Address, request.About, - request.Description, request.SocialNetworks); + var result = await identityService.UpdateCurrentUserAsync(identityUser); await context.SaveChangesAsync(cancellationToken); diff --git a/src/Application/Users/Commands/UploadBannerPicture.cs b/src/Application/Users/Commands/UploadBannerPicture.cs new file mode 100644 index 0000000..66922a6 --- /dev/null +++ b/src/Application/Users/Commands/UploadBannerPicture.cs @@ -0,0 +1,27 @@ +using Hutopy.Application.AzureBlobStorage.Constants; +using Hutopy.Application.Common.Interfaces; + +namespace Hutopy.Application.Users.Commands; + +public class UploadBannerPictureCommand : IRequest +{ + public required Stream BannerPicture { get; init; } +} + +public class UploadBannerPictureCommandHandler(IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler +{ + public async Task Handle(UploadBannerPictureCommand request, CancellationToken cancellationToken) + { + var identityUser = await identityService.GetCurrentUserAsync(); + var currentUserId = new Guid(identityUser?.Id ?? "").ToString(); + + var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.BannerPicture}"; + + var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.BannerPicture); + + await identityService.UpdateCurrentUserBannerPictureUrlAsync(url); + + return url; + } +} + diff --git a/src/Application/Users/Commands/UploadProfilePicture.cs b/src/Application/Users/Commands/UploadProfilePicture.cs index 08746d9..6fc4714 100644 --- a/src/Application/Users/Commands/UploadProfilePicture.cs +++ b/src/Application/Users/Commands/UploadProfilePicture.cs @@ -19,6 +19,8 @@ public class UploadProfilePictureCommandHandler(IIdentityService identityService var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.ProfilePicture); + await identityService.UpdateCurrentUserProfilePictureUrlAsync(url); + return url; } } diff --git a/src/Application/Users/Commands/UploadWebsiteIcon.cs b/src/Application/Users/Commands/UploadWebsiteIcon.cs new file mode 100644 index 0000000..78a83a2 --- /dev/null +++ b/src/Application/Users/Commands/UploadWebsiteIcon.cs @@ -0,0 +1,27 @@ +using Hutopy.Application.AzureBlobStorage.Constants; +using Hutopy.Application.Common.Interfaces; + +namespace Hutopy.Application.Users.Commands; + +public class UploadWebsiteIconCommand : IRequest +{ + public required Stream WebsiteIcon { get; init; } +} + +public class UploadWebsiteIconCommandHandler(IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler +{ + public async Task Handle(UploadWebsiteIconCommand request, CancellationToken cancellationToken) + { + var identityUser = await identityService.GetCurrentUserAsync(); + var currentUserId = new Guid(identityUser?.Id ?? "").ToString(); + + var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.WebsiteIcon}"; + + var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.WebsiteIcon); + + await identityService.UpdateCurrentUserWebsiteIconUrlAsync(url); + + return url; + } +} + diff --git a/src/Application/Users/Models/ProfileColorsModel.cs b/src/Application/Users/Models/ProfileColorsModel.cs new file mode 100644 index 0000000..04baadb --- /dev/null +++ b/src/Application/Users/Models/ProfileColorsModel.cs @@ -0,0 +1,9 @@ +namespace Hutopy.Application.Users.Models; + +public class ProfileColorsModel +{ + public string BannerTop { get; init; } = String.Empty; + public string BannerBottom { get; init; } = String.Empty; + public string Accent { get; init; } = String.Empty; + public string Menu { get; init; } = String.Empty; +} diff --git a/src/Application/Users/Models/SocialNetworksModel.cs b/src/Application/Users/Models/SocialNetworksModel.cs index edd3cce..19a2f02 100644 --- a/src/Application/Users/Models/SocialNetworksModel.cs +++ b/src/Application/Users/Models/SocialNetworksModel.cs @@ -2,12 +2,12 @@ public class SocialNetworksModel { - public string FacebookUrl { get; init; } = String.Empty; - public string InstagramUrl { get; init; } = String.Empty; - public string XUrl { get; init; } = String.Empty; - public string LinkedInUrl { get; init; } = String.Empty; - public string TikTokUrl { get; init; } = String.Empty; - public string YoutubeUrl { get; init; } = String.Empty; - public string RedditUrl { get; init; } = String.Empty; - public string YourWebsiteUrl { get; init; } = String.Empty; + public string FacebookUrl { get; init; } = string.Empty; + public string InstagramUrl { get; init; } = string.Empty; + public string XUrl { get; init; } = string.Empty; + public string LinkedInUrl { get; init; } = string.Empty; + public string TikTokUrl { get; init; } = string.Empty; + public string YoutubeUrl { get; init; } = string.Empty; + public string RedditUrl { get; init; } = string.Empty; + public string YourWebsiteUrl { get; init; } = string.Empty; } diff --git a/src/Application/Users/Queries/GetCurrentUser/UserDto.cs b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs index c0b34c1..ec2cc5d 100644 --- a/src/Application/Users/Queries/GetCurrentUser/UserDto.cs +++ b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs @@ -7,16 +7,16 @@ public class UserDto public Guid Id { get; init; } public required string FirstName { get; init; } public required string LastName { get; init; } - public string UserName { get; init; } = String.Empty; - public string Occupation { get; init; } = String.Empty; - public string Email { get; init; } = String.Empty; - public string Phone { get; init; } = String.Empty; - public string BirthDate { get; init; } = String.Empty; - public string Country { get; init; } = String.Empty; - public string City { get; init; } = String.Empty; - public string Address { get; init; } = String.Empty; - public string About { get; init; } = String.Empty; - public string Description { get; init; } = String.Empty; + public string UserName { get; init; } = string.Empty; + public string Occupation { get; init; } = string.Empty; + public string Email { get; init; } = string.Empty; + public string Phone { get; init; } = string.Empty; + public string BirthDate { get; init; } = string.Empty; + public string Country { get; init; } = string.Empty; + public string City { get; init; } = string.Empty; + public string Address { get; init; } = string.Empty; + public string About { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; public SocialNetworksModel SocialNetworks { get; init; } = new(); public List UserTransactions { get; init; } = []; public IList UserRoles { get; init; } = []; diff --git a/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs b/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs index f54f96b..b25521c 100644 --- a/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs +++ b/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs @@ -1,3 +1,6 @@ +using System; +using System.IO; +using System.Threading.Tasks; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Hutopy.Application.Common.Interfaces; diff --git a/src/Infrastructure/Data/ApplicationDbContextInitializer.cs b/src/Infrastructure/Data/ApplicationDbContextInitializer.cs index 6a985de..b771d57 100644 --- a/src/Infrastructure/Data/ApplicationDbContextInitializer.cs +++ b/src/Infrastructure/Data/ApplicationDbContextInitializer.cs @@ -1,4 +1,7 @@ -using Hutopy.Domain.Constants; +using System; +using System.Linq; +using System.Threading.Tasks; +using Hutopy.Domain.Constants; using Hutopy.Infrastructure.Identity; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity; diff --git a/src/Infrastructure/Data/Configurations/ApplicationUserConfiguration.cs b/src/Infrastructure/Data/Configurations/ApplicationUserConfiguration.cs index 0624b7a..b3a3735 100644 --- a/src/Infrastructure/Data/Configurations/ApplicationUserConfiguration.cs +++ b/src/Infrastructure/Data/Configurations/ApplicationUserConfiguration.cs @@ -12,5 +12,15 @@ public class ApplicationUserConfiguration : IEntityTypeConfiguration u.SocialNetworks) .ToTable($"{nameof(ApplicationUser)}_SocialNetworks"); + + // Relationship between ApplicationUser and ProfileColors + builder + .OwnsOne(u => u.ProfileColors) + .ToTable($"{nameof(ApplicationUser)}_ProfileColors"); + + // Relationship between ApplicationUser and StoredDataUrls + builder + .OwnsOne(u => u.StoredDataUrls) + .ToTable($"{nameof(ApplicationUser)}_StoredDataUrls"); } } diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index b8f43d6..2dfccb2 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -1,4 +1,5 @@ -using Hutopy.Application.Common.Interfaces; +using System; +using Hutopy.Application.Common.Interfaces; using Hutopy.Domain.Constants; using Hutopy.Infrastructure.AzureBlob; using Hutopy.Infrastructure.Data; diff --git a/src/Infrastructure/Identity/ApplicationUser.cs b/src/Infrastructure/Identity/ApplicationUser.cs index d956a9b..fbf49f7 100644 --- a/src/Infrastructure/Identity/ApplicationUser.cs +++ b/src/Infrastructure/Identity/ApplicationUser.cs @@ -15,4 +15,6 @@ public class ApplicationUser : IdentityUser public string About { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public SocialNetworks SocialNetworks { get; set; } = new(); + public ProfileColors ProfileColors { get; set; } = new(); + public StoredDataUrls StoredDataUrls { get; set; } = new(); } diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index d512ae5..c0f8a9b 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -1,6 +1,8 @@ -using System.Diagnostics; +using System; +using System.Collections.Generic; using Google.Apis.Oauth2.v2.Data; using System.Security.Claims; +using System.Threading.Tasks; using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Models; using Hutopy.Application.Users.Models; @@ -64,10 +66,8 @@ public class IdentityService( var applicationResult = identityResult.ToApplicationResult(); - var result = new Result(applicationResult.Succeeded, applicationResult.Errors); + var result = new Result(applicationUser.Id, applicationResult.Succeeded, applicationResult.Errors); - result.Value = applicationUser.Id; - return result; } @@ -83,51 +83,53 @@ public class IdentityService( var response = await userManager.CreateAsync(applicationUser, password); - var result = new Result(response.Succeeded, response.ToApplicationResult().Errors); - result.Value = applicationUser.Id; + var result = new Result(applicationUser.Id, response.Succeeded, response.ToApplicationResult().Errors); return result; } - public async Task> UpdateCurrentUserAsync(string id, string firstName, string lastName, string occupation, - string phoneNumber, string birthDate, string country, string city, string address, string about, string description, - SocialNetworksModel socialNetworks) + public async Task> UpdateCurrentUserAsync(UserModel userModel) { - var applicationUser = await userManager.FindByIdAsync(id); + var applicationUser = await userManager.FindByIdAsync(userModel.Id); - if (applicationUser is null) return Result.Failure(new[] { "User not found." }); + if (applicationUser is null) return Result.Failure("", new[] { "User not found." }); - applicationUser.FirstName = firstName; - applicationUser.LastName = lastName; - applicationUser.Occupation = occupation; - applicationUser.PhoneNumber = phoneNumber; - applicationUser.BirthDate = birthDate; - applicationUser.Country = country; - applicationUser.City = city; - applicationUser.Address = address; - applicationUser.About = about; - applicationUser.Description = description; - applicationUser.SocialNetworks = new SocialNetworks() + applicationUser.FirstName = userModel.FirstName; + applicationUser.LastName = userModel.LastName; + applicationUser.Occupation = userModel.Occupation; + applicationUser.PhoneNumber = userModel.Phone; + applicationUser.BirthDate = userModel.BirthDate; + applicationUser.Country = userModel.Country; + applicationUser.City = userModel.City; + applicationUser.Address = userModel.Address; + applicationUser.About = userModel.About; + applicationUser.Description = userModel.Description; + applicationUser.SocialNetworks = new SocialNetworks { - FacebookUrl = socialNetworks.FacebookUrl, - InstagramUrl = socialNetworks.InstagramUrl, - XUrl = socialNetworks.XUrl, - LinkedInUrl = socialNetworks.LinkedInUrl, - TikTokUrl = socialNetworks.TikTokUrl, - YoutubeUrl = socialNetworks.YoutubeUrl, - RedditUrl = socialNetworks.RedditUrl, - YourWebsiteUrl = socialNetworks.YourWebsiteUrl + FacebookUrl = userModel.SocialNetworks.FacebookUrl, + InstagramUrl = userModel.SocialNetworks.InstagramUrl, + XUrl = userModel.SocialNetworks.XUrl, + LinkedInUrl = userModel.SocialNetworks.LinkedInUrl, + TikTokUrl = userModel.SocialNetworks.TikTokUrl, + YoutubeUrl = userModel.SocialNetworks.YoutubeUrl, + RedditUrl = userModel.SocialNetworks.RedditUrl, + YourWebsiteUrl = userModel.SocialNetworks.YourWebsiteUrl + }; + applicationUser.ProfileColors = new ProfileColors + { + BannerTop = userModel.ProfileColors.BannerTop, + BannerBottom = userModel.ProfileColors.BannerBottom, + Accent = userModel.ProfileColors.Accent, + Menu = userModel.ProfileColors.Menu }; var response = await userManager.UpdateAsync(applicationUser); var applicationResult = response.ToApplicationResult(); - var result = new Result(applicationResult.Succeeded, + var result = new Result(userModel.Id, applicationResult.Succeeded, applicationResult.Errors); - - result.Value = id; - + return result; } @@ -140,12 +142,12 @@ public class IdentityService( var userModel = new UserModel { Id = response.Id, - UserName = response.UserName, + UserName = response.UserName ?? string.Empty, FirstName = response.FirstName, LastName = response.LastName, - Email = response.Email, + Email = response.Email ?? string.Empty, Occupation = response.Occupation, - Phone = response.PhoneNumber, + Phone = response.PhoneNumber ?? string.Empty, BirthDate = response.BirthDate, Country = response.Country, City = response.City, @@ -162,6 +164,57 @@ public class IdentityService( YoutubeUrl = response.SocialNetworks.YoutubeUrl, RedditUrl = response.SocialNetworks.RedditUrl, YourWebsiteUrl = response.SocialNetworks.YourWebsiteUrl, + }, + ProfileColors = new ProfileColorsModel + { + BannerTop = response.ProfileColors.BannerTop, + BannerBottom = response.ProfileColors.BannerBottom, + Accent = response.ProfileColors.Accent, + Menu = response.ProfileColors.Menu + } + }; + + return userModel; + } + + public async Task FindUserByEmailAsync(string email) + { + var response = await userManager.FindByEmailAsync(email); + + if (response == null) return null; + + var userModel = new UserModel + { + Id = response.Id, + UserName = response.UserName ?? string.Empty, + FirstName = response.FirstName, + LastName = response.LastName, + Email = response.Email ?? string.Empty, + Occupation = response.Occupation, + Phone = response.PhoneNumber ?? string.Empty, + BirthDate = response.BirthDate, + Country = response.Country, + City = response.City, + Address = response.Address, + About = response.About, + Description = response.Description, + SocialNetworks = new SocialNetworksModel + { + FacebookUrl = response.SocialNetworks.FacebookUrl, + InstagramUrl = response.SocialNetworks.InstagramUrl, + XUrl = response.SocialNetworks.XUrl, + LinkedInUrl = response.SocialNetworks.LinkedInUrl, + TikTokUrl = response.SocialNetworks.TikTokUrl, + YoutubeUrl = response.SocialNetworks.YoutubeUrl, + RedditUrl = response.SocialNetworks.RedditUrl, + YourWebsiteUrl = response.SocialNetworks.YourWebsiteUrl, + }, + ProfileColors = new ProfileColorsModel + { + BannerTop = response.ProfileColors.BannerTop, + BannerBottom = response.ProfileColors.BannerBottom, + Accent = response.ProfileColors.Accent, + Menu = response.ProfileColors.Menu } }; @@ -179,22 +232,49 @@ public class IdentityService( return await FindUserByIdAsync(currentUserId); } - public async Task FindUserByEmailAsync(string email) + public async Task UpdateCurrentUserBannerPictureUrlAsync(string url) { - var response = await userManager.FindByEmailAsync(email); + var userModel = await GetCurrentUserAsync(); + if (userModel is null) return Result.Failure(new[] { "User not found." }); - if (response == null) return null; + var applicationUser = await userManager.FindByIdAsync(userModel.Id); + if (applicationUser is null) return Result.Failure(new[] { "ApplicationUser not found." }); - var userModel = new UserModel - { - Id = response.Id, - UserName = response.UserName, - FirstName = response.FirstName, - LastName = response.LastName, - Email = response.Email - }; + applicationUser.StoredDataUrls.BannerPictureUrl = url; + + var response = await userManager.UpdateAsync(applicationUser); - return userModel; + return response.ToApplicationResult(); + } + + public async Task UpdateCurrentUserProfilePictureUrlAsync(string url) + { + var userModel = await GetCurrentUserAsync(); + if (userModel is null) return Result.Failure(new[] { "User not found." }); + + var applicationUser = await userManager.FindByIdAsync(userModel.Id); + if (applicationUser is null) return Result.Failure(new[] { "ApplicationUser not found." }); + + applicationUser.StoredDataUrls.ProfilePictureUrl = url; + + var response = await userManager.UpdateAsync(applicationUser); + + return response.ToApplicationResult(); + } + + public async Task UpdateCurrentUserWebsiteIconUrlAsync(string url) + { + var userModel = await GetCurrentUserAsync(); + if (userModel is null) return Result.Failure(new[] { "User not found." }); + + var applicationUser = await userManager.FindByIdAsync(userModel.Id); + if (applicationUser is null) return Result.Failure(new[] { "ApplicationUser not found." }); + + applicationUser.StoredDataUrls.WebsiteIconUrl = url; + + var response = await userManager.UpdateAsync(applicationUser); + + return response.ToApplicationResult(); } public async Task IsInRoleAsync(string userId, string role) @@ -293,7 +373,7 @@ public class IdentityService( email: user.Email, firstname: user.FirstName, lastname: user.LastName, - portraitUrl: user.PortraitUrl); + portraitUrl: user.ProfilePictureUrl); return token; } diff --git a/src/Infrastructure/Identity/OwnedEntities/ProfileColors.cs b/src/Infrastructure/Identity/OwnedEntities/ProfileColors.cs new file mode 100644 index 0000000..3783044 --- /dev/null +++ b/src/Infrastructure/Identity/OwnedEntities/ProfileColors.cs @@ -0,0 +1,9 @@ +namespace Hutopy.Infrastructure.Identity.OwnedEntities; + +public class ProfileColors +{ + public string BannerTop { get; init; } = string.Empty; + public string BannerBottom { get; init; } = string.Empty; + public string Accent { get; init; } = string.Empty; + public string Menu { get; init; } = string.Empty; +} diff --git a/src/Infrastructure/Identity/OwnedEntities/SocialNetworks.cs b/src/Infrastructure/Identity/OwnedEntities/SocialNetworks.cs index 1e0d30e..a03e139 100644 --- a/src/Infrastructure/Identity/OwnedEntities/SocialNetworks.cs +++ b/src/Infrastructure/Identity/OwnedEntities/SocialNetworks.cs @@ -2,12 +2,12 @@ namespace Hutopy.Infrastructure.Identity.OwnedEntities; public class SocialNetworks { - public string FacebookUrl { get; init; } = String.Empty; - public string InstagramUrl { get; init; } = String.Empty; - public string XUrl { get; init; } = String.Empty; - public string LinkedInUrl { get; init; } = String.Empty; - public string TikTokUrl { get; init; } = String.Empty; - public string YoutubeUrl { get; init; } = String.Empty; - public string RedditUrl { get; init; } = String.Empty; - public string YourWebsiteUrl { get; init; } = String.Empty; + public string FacebookUrl { get; init; } = string.Empty; + public string InstagramUrl { get; init; } = string.Empty; + public string XUrl { get; init; } = string.Empty; + public string LinkedInUrl { get; init; } = string.Empty; + public string TikTokUrl { get; init; } = string.Empty; + public string YoutubeUrl { get; init; } = string.Empty; + public string RedditUrl { get; init; } = string.Empty; + public string YourWebsiteUrl { get; init; } = string.Empty; } diff --git a/src/Infrastructure/Identity/OwnedEntities/StoredDataUrls.cs b/src/Infrastructure/Identity/OwnedEntities/StoredDataUrls.cs new file mode 100644 index 0000000..bea6099 --- /dev/null +++ b/src/Infrastructure/Identity/OwnedEntities/StoredDataUrls.cs @@ -0,0 +1,8 @@ +namespace Hutopy.Infrastructure.Identity.OwnedEntities; + +public class StoredDataUrls +{ + public string BannerPictureUrl { get; set; } = string.Empty; + public string ProfilePictureUrl { get; set; } = string.Empty; + public string WebsiteIconUrl { get; set; } = string.Empty; +} diff --git a/src/Infrastructure/Migrations/20240630163057_AddMoreInformationsToUser.Designer.cs b/src/Infrastructure/Migrations/20240630163057_AddMoreInformationsToUser.Designer.cs new file mode 100644 index 0000000..af10f04 --- /dev/null +++ b/src/Infrastructure/Migrations/20240630163057_AddMoreInformationsToUser.Designer.cs @@ -0,0 +1,557 @@ +// +using System; +using Hutopy.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240630163057_AddMoreInformationsToUser")] + partial class AddMoreInformationsToUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Domain.Entities.FutureCreator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReasonToJoin") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SocialNetworkAccount") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("FutureCreators"); + }); + + modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsConfirmed") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Paid") + .HasColumnType("bit"); + + b.Property("StripeBillingDetailEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeBillingDetailName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeChargeId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeEventId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentIntent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeReceiptUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TipMessage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("UserTransactions"); + }); + + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("About") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BirthDate") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .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("Occupation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + 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.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("Hutopy.Domain.Entities.UserTransaction", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => + { + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.ProfileColors", "ProfileColors", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("Accent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("BannerBottom") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("BannerTop") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("Menu") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_ProfileColors", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.SocialNetworks", "SocialNetworks", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("FacebookUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("InstagramUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("LinkedInUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("RedditUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("TikTokUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("XUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("YourWebsiteUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("YoutubeUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_SocialNetworks", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.StoredDataUrls", "StoredDataUrls", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("BannerPictureUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("ProfilePictureUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("WebsiteIconUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_StoredDataUrls", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.Navigation("ProfileColors") + .IsRequired(); + + b.Navigation("SocialNetworks") + .IsRequired(); + + b.Navigation("StoredDataUrls") + .IsRequired(); + }); + + 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("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.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("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Migrations/20240630163057_AddMoreInformationsToUser.cs b/src/Infrastructure/Migrations/20240630163057_AddMoreInformationsToUser.cs new file mode 100644 index 0000000..85e7b7e --- /dev/null +++ b/src/Infrastructure/Migrations/20240630163057_AddMoreInformationsToUser.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + /// + public partial class AddMoreInformationsToUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApplicationUser_ProfileColors", + columns: table => new + { + ApplicationUserId = table.Column(type: "nvarchar(450)", nullable: false), + BannerTop = table.Column(type: "nvarchar(max)", nullable: false), + BannerBottom = table.Column(type: "nvarchar(max)", nullable: false), + Accent = table.Column(type: "nvarchar(max)", nullable: false), + Menu = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApplicationUser_ProfileColors", x => x.ApplicationUserId); + table.ForeignKey( + name: "FK_ApplicationUser_ProfileColors_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql(@" + INSERT INTO ApplicationUser_ProfileColors (ApplicationUserId, BannerTop, BannerBottom, Accent, Menu) + SELECT Id, '', '', '', '' + FROM AspNetUsers + "); + + migrationBuilder.CreateTable( + name: "ApplicationUser_StoredDataUrls", + columns: table => new + { + ApplicationUserId = table.Column(type: "nvarchar(450)", nullable: false), + BannerPictureUrl = table.Column(type: "nvarchar(max)", nullable: false), + ProfilePictureUrl = table.Column(type: "nvarchar(max)", nullable: false), + WebsiteIconUrl = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApplicationUser_StoredDataUrls", x => x.ApplicationUserId); + table.ForeignKey( + name: "FK_ApplicationUser_StoredDataUrls_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql(@" + INSERT INTO ApplicationUser_StoredDataUrls (ApplicationUserId, BannerPictureUrl, ProfilePictureUrl, WebsiteIconUrl) + SELECT Id, '', '', '' + FROM AspNetUsers + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApplicationUser_ProfileColors"); + + migrationBuilder.DropTable( + name: "ApplicationUser_StoredDataUrls"); + } + } +} diff --git a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index cd78f54..12a98ec 100644 --- a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -389,6 +389,35 @@ namespace Hutopy.Infrastructure.Migrations modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => { + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.ProfileColors", "ProfileColors", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("Accent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("BannerBottom") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("BannerTop") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("Menu") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_ProfileColors", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.SocialNetworks", "SocialNetworks", b1 => { b1.Property("ApplicationUserId") @@ -434,8 +463,39 @@ namespace Hutopy.Infrastructure.Migrations .HasForeignKey("ApplicationUserId"); }); + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.StoredDataUrls", "StoredDataUrls", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("BannerPictureUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("ProfilePictureUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("WebsiteIconUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_StoredDataUrls", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.Navigation("ProfileColors") + .IsRequired(); + b.Navigation("SocialNetworks") .IsRequired(); + + b.Navigation("StoredDataUrls") + .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => diff --git a/src/Infrastructure/Stripe/StripeService.cs b/src/Infrastructure/Stripe/StripeService.cs index 7b78fff..3f4e366 100644 --- a/src/Infrastructure/Stripe/StripeService.cs +++ b/src/Infrastructure/Stripe/StripeService.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Stripe; using Stripe.Checkout; using Hutopy.Application.Common.Interfaces; diff --git a/src/Web/Controllers/GoogleController.cs b/src/Web/Controllers/GoogleController.cs index dfaa91e..8206069 100644 --- a/src/Web/Controllers/GoogleController.cs +++ b/src/Web/Controllers/GoogleController.cs @@ -71,7 +71,7 @@ public class GoogleController(IIdentityService identityService, IHttpClientFacto user.Email, user.FirstName, user.LastName, - user.PortraitUrl); + user.ProfilePictureUrl); return Ok(new { accessToken = token, email }); } diff --git a/src/Web/Endpoints/UpdateMyUser.cs b/src/Web/Endpoints/UpdateMyUser.cs index 60121be..9aca4dc 100644 --- a/src/Web/Endpoints/UpdateMyUser.cs +++ b/src/Web/Endpoints/UpdateMyUser.cs @@ -8,6 +8,9 @@ public class UpdateMyUser : EndpointGroupBase { app.MapGroup(this) .RequireAuthorization() + .MapPost(UpdateCurrentUserProfilePicture, "/profile-picture") + .MapPost(UpdateCurrentUserBannerPicture, "/banner-picture") + .MapPost(UpdateCurrentUserWebsiteIcon, "/website-icon") .MapPatch("/profile", UpdateCurrentUser); } @@ -16,5 +19,21 @@ public class UpdateMyUser : EndpointGroupBase return await sender.Send(command); } + private static async Task UpdateCurrentUserProfilePicture(ISender sender, Stream stream) + { + var command = new UploadProfilePictureCommand { ProfilePicture = stream }; + return await sender.Send(command); + } + private static async Task UpdateCurrentUserBannerPicture(ISender sender, Stream stream) + { + var command = new UploadBannerPictureCommand { BannerPicture = stream }; + return await sender.Send(command); + } + + private static async Task UpdateCurrentUserWebsiteIcon(ISender sender, Stream stream) + { + var command = new UploadWebsiteIconCommand { WebsiteIcon = stream }; + return await sender.Send(command); + } } From 90d76c32ce9813aa5bcd0a3ef1d194ad1e050755 Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Sun, 30 Jun 2024 12:39:24 -0400 Subject: [PATCH 13/14] Add storedDataUrls to GetCurrentUser --- src/Application/Common/Models/UserModel.cs | 1 + src/Application/Users/Models/StoredDataUrlsModel.cs | 8 ++++++++ .../Users/Queries/GetCurrentUser/GetCurrentUser.cs | 2 ++ .../Users/Queries/GetCurrentUser/UserDto.cs | 2 ++ src/Infrastructure/Identity/IdentityService.cs | 12 ++++++++++++ 5 files changed, 25 insertions(+) create mode 100644 src/Application/Users/Models/StoredDataUrlsModel.cs diff --git a/src/Application/Common/Models/UserModel.cs b/src/Application/Common/Models/UserModel.cs index 9fceab9..fb76755 100644 --- a/src/Application/Common/Models/UserModel.cs +++ b/src/Application/Common/Models/UserModel.cs @@ -19,5 +19,6 @@ public class UserModel public string Description { get; init; } = string.Empty; public SocialNetworksModel SocialNetworks { get; init; } = new(); public ProfileColorsModel ProfileColors { get; init; } = new(); + public StoredDataUrlsModel StoredDataUrls { get; init; } = new(); public string ProfilePictureUrl { get; set; } = string.Empty; } diff --git a/src/Application/Users/Models/StoredDataUrlsModel.cs b/src/Application/Users/Models/StoredDataUrlsModel.cs new file mode 100644 index 0000000..0539508 --- /dev/null +++ b/src/Application/Users/Models/StoredDataUrlsModel.cs @@ -0,0 +1,8 @@ +namespace Hutopy.Application.Users.Models; + +public class StoredDataUrlsModel +{ + public string BannerPictureUrl { get; set; } = string.Empty; + public string ProfilePictureUrl { get; set; } = string.Empty; + public string WebsiteIconUrl { get; set; } = string.Empty; +} diff --git a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs index eeb9f7c..2fef51c 100644 --- a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs +++ b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs @@ -41,6 +41,8 @@ public class GetCurrentUserQueryHandler( About = identityUser.About ?? "", Description = identityUser.Description ?? "", SocialNetworks = identityUser.SocialNetworks, + ProfileColors = identityUser.ProfileColors, + StoredDataUrls = identityUser.StoredDataUrls, UserTransactions = transactions, TotalBalance = transactions.Sum(x => x.Amount), UserRoles = roles, diff --git a/src/Application/Users/Queries/GetCurrentUser/UserDto.cs b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs index ec2cc5d..24cb7fd 100644 --- a/src/Application/Users/Queries/GetCurrentUser/UserDto.cs +++ b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs @@ -18,6 +18,8 @@ public class UserDto public string About { get; init; } = string.Empty; public string Description { get; init; } = string.Empty; public SocialNetworksModel SocialNetworks { get; init; } = new(); + public ProfileColorsModel ProfileColors { get; init; } = new(); + public StoredDataUrlsModel StoredDataUrls { get; init; } = new(); public List UserTransactions { get; init; } = []; public IList UserRoles { get; init; } = []; public required decimal TotalBalance { get; init; } diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index c0f8a9b..d159de4 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -171,6 +171,12 @@ public class IdentityService( BannerBottom = response.ProfileColors.BannerBottom, Accent = response.ProfileColors.Accent, Menu = response.ProfileColors.Menu + }, + StoredDataUrls = new StoredDataUrlsModel + { + ProfilePictureUrl = response.StoredDataUrls.ProfilePictureUrl, + BannerPictureUrl = response.StoredDataUrls.BannerPictureUrl, + WebsiteIconUrl = response.StoredDataUrls.WebsiteIconUrl, } }; @@ -215,6 +221,12 @@ public class IdentityService( BannerBottom = response.ProfileColors.BannerBottom, Accent = response.ProfileColors.Accent, Menu = response.ProfileColors.Menu + }, + StoredDataUrls = new StoredDataUrlsModel + { + ProfilePictureUrl = response.StoredDataUrls.ProfilePictureUrl, + BannerPictureUrl = response.StoredDataUrls.BannerPictureUrl, + WebsiteIconUrl = response.StoredDataUrls.WebsiteIconUrl, } }; From 041b8178acbe798cc06fff0505b48e3b7687c1a5 Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Sun, 30 Jun 2024 15:48:26 -0400 Subject: [PATCH 14/14] Mapping + changed Phone to PhoneNumber --- src/Application/Common/Models/UserModel.cs | 2 +- .../Commands/UpdateCurrentUserCommand.cs | 20 ++++++++++++++++--- .../Queries/GetCurrentUser/GetCurrentUser.cs | 2 +- .../Users/Queries/GetCurrentUser/UserDto.cs | 3 +-- .../Identity/IdentityService.cs | 6 +++--- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/Application/Common/Models/UserModel.cs b/src/Application/Common/Models/UserModel.cs index fb76755..ccff715 100644 --- a/src/Application/Common/Models/UserModel.cs +++ b/src/Application/Common/Models/UserModel.cs @@ -10,7 +10,7 @@ public class UserModel public string LastName { get; set; } = string.Empty; public string Occupation { get; set; } = string.Empty; public string Email { get; init; } = string.Empty; - public string Phone { get; init; } = string.Empty; + public string PhoneNumber { get; init; } = string.Empty; public string BirthDate { get; init; } = string.Empty; public string Country { get; init; } = string.Empty; public string City { get; init; } = string.Empty; diff --git a/src/Application/Users/Commands/UpdateCurrentUserCommand.cs b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs index 708ea6b..193d904 100644 --- a/src/Application/Users/Commands/UpdateCurrentUserCommand.cs +++ b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs @@ -1,4 +1,6 @@ -using Hutopy.Application.Common.Interfaces; +using System.ComponentModel.DataAnnotations.Schema; +using Hutopy.Application.Common.Interfaces; +using Hutopy.Application.Common.Models; using Hutopy.Application.Users.Models; namespace Hutopy.Application.Users.Commands; @@ -17,9 +19,18 @@ public class UpdateCurrentUserCommand : IRequest public required string Description { get; init; } public required SocialNetworksModel SocialNetworks { get; init; } public required ProfileColorsModel ProfileColors { get; init; } + + [NotMapped] + private class Mapping : Profile + { + public Mapping() + { + CreateMap(); + } + } } -public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIdentityService identityService) : +public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIdentityService identityService, IMapper mapper) : IRequestHandler { public async Task Handle(UpdateCurrentUserCommand request, CancellationToken cancellationToken) @@ -27,8 +38,11 @@ public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIde var identityUser = await identityService.GetCurrentUserAsync(); if (identityUser?.Id is null) return string.Empty; + + var userModel = mapper.Map(request); + userModel.Id = identityUser.Id; - var result = await identityService.UpdateCurrentUserAsync(identityUser); + var result = await identityService.UpdateCurrentUserAsync(userModel); await context.SaveChangesAsync(cancellationToken); diff --git a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs index 2fef51c..f8cc1ab 100644 --- a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs +++ b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs @@ -32,7 +32,7 @@ public class GetCurrentUserQueryHandler( LastName = identityUser.LastName ?? "", UserName = identityUser.UserName ?? "", Occupation = identityUser.Occupation ?? "", - Phone = identityUser.Phone ?? "", + PhoneNumber = identityUser.PhoneNumber ?? "", Email = identityUser.Email ?? "", BirthDate = identityUser.BirthDate ?? "", Country = identityUser.Country ?? "", diff --git a/src/Application/Users/Queries/GetCurrentUser/UserDto.cs b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs index 24cb7fd..23dfaa8 100644 --- a/src/Application/Users/Queries/GetCurrentUser/UserDto.cs +++ b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs @@ -10,7 +10,7 @@ public class UserDto public string UserName { get; init; } = string.Empty; public string Occupation { get; init; } = string.Empty; public string Email { get; init; } = string.Empty; - public string Phone { get; init; } = string.Empty; + public string PhoneNumber { get; init; } = string.Empty; public string BirthDate { get; init; } = string.Empty; public string Country { get; init; } = string.Empty; public string City { get; init; } = string.Empty; @@ -23,5 +23,4 @@ public class UserDto public List UserTransactions { get; init; } = []; public IList UserRoles { get; init; } = []; public required decimal TotalBalance { get; init; } - } diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index d159de4..b0be8f2 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -97,7 +97,7 @@ public class IdentityService( applicationUser.FirstName = userModel.FirstName; applicationUser.LastName = userModel.LastName; applicationUser.Occupation = userModel.Occupation; - applicationUser.PhoneNumber = userModel.Phone; + applicationUser.PhoneNumber = userModel.PhoneNumber; applicationUser.BirthDate = userModel.BirthDate; applicationUser.Country = userModel.Country; applicationUser.City = userModel.City; @@ -147,7 +147,7 @@ public class IdentityService( LastName = response.LastName, Email = response.Email ?? string.Empty, Occupation = response.Occupation, - Phone = response.PhoneNumber ?? string.Empty, + PhoneNumber = response.PhoneNumber ?? string.Empty, BirthDate = response.BirthDate, Country = response.Country, City = response.City, @@ -197,7 +197,7 @@ public class IdentityService( LastName = response.LastName, Email = response.Email ?? string.Empty, Occupation = response.Occupation, - Phone = response.PhoneNumber ?? string.Empty, + PhoneNumber = response.PhoneNumber ?? string.Empty, BirthDate = response.BirthDate, Country = response.Country, City = response.City,