From 006a566b93f47dc557cb8bfdd416ece48c19e701 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 13:40:27 -0400 Subject: [PATCH 01/16] 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 1725008211d8724c33b8460f0ef298c3fbd9b823 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 13:40:55 -0400 Subject: [PATCH 02/16] 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 128e68f63f9f160edd04cd5f46b30959a5fbe16b Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 13:44:45 -0400 Subject: [PATCH 03/16] 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 390bf0b72afdc527228342355ea7915408f3f7ce Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 13:46:00 -0400 Subject: [PATCH 04/16] 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 b17d2f2366eb3e9a09eb3d1a2f6e32cb78bf81f0 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 13:46:53 -0400 Subject: [PATCH 05/16] 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 c1c542dc1a5b3ae034a699b5df341b90a1437d0d Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 13:55:24 -0400 Subject: [PATCH 06/16] 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 d78753573259e3afc3cdca36d062c627751dcdb0 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 14:18:14 -0400 Subject: [PATCH 07/16] 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 f57b693cd98787c70c6cf1b6197db298cc080a01 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 20 Jun 2024 16:27:44 -0400 Subject: [PATCH 08/16] 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 891597fb08a4a7dbfdefc64214bb3ce46852893a 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/16] 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 72e243cf84203d8ac1969aa3ac190a79bc6a7ce8 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 27 Jun 2024 12:37:59 -0400 Subject: [PATCH 10/16] 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 5282fcfd49a2fbbab5242b7d5fbce0b1f2d21564 Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Sat, 29 Jun 2024 22:45:17 -0400 Subject: [PATCH 11/16] 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 ecaaaaad330051b2b67d8a6c2a983f2eea64586a Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Mon, 1 Jul 2024 03:55:13 -0400 Subject: [PATCH 12/16] Add PortraitUrl to User --- .../Common/Interfaces/IIdentityService.cs | 3 +- .../Commands/UpdateCurrentUserCommand.cs | 24 +- .../Data/ApplicationDbContextInitializer.cs | 7 +- .../Identity/ApplicationUser.cs | 1 + .../Identity/IdentityService.cs | 76 +-- ...701072549_AddPortraitUrlToUser.Designer.cs | 500 ++++++++++++++++++ .../20240701072549_AddPortraitUrlToUser.cs | 28 + .../ApplicationDbContextModelSnapshot.cs | 3 + src/Infrastructure/Utils/GenerateJwtToken.cs | 14 +- 9 files changed, 614 insertions(+), 42 deletions(-) create mode 100644 src/Infrastructure/Migrations/20240701072549_AddPortraitUrlToUser.Designer.cs create mode 100644 src/Infrastructure/Migrations/20240701072549_AddPortraitUrlToUser.cs diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index d92663e..b5b40e6 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -12,7 +12,8 @@ public interface IIdentityService 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); + SocialNetworksModel socialNetworks, + string? portraitUrl); Task> GetCurrentUserRolesAsync(); Task FindUserByIdAsync(string id); Task FindUserByEmailAsync(string email); diff --git a/src/Application/Users/Commands/UpdateCurrentUserCommand.cs b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs index 3a23915..50d576d 100644 --- a/src/Application/Users/Commands/UpdateCurrentUserCommand.cs +++ b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs @@ -16,9 +16,10 @@ public class UpdateCurrentUserCommand : IRequest public required string About { get; init; } public required string Description { get; init; } public required SocialNetworksModel SocialNetworks { get; init; } + public required string PortraitUrl { get; init; } } -public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIdentityService identityService) : +public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIdentityService identityService) : IRequestHandler { public async Task Handle(UpdateCurrentUserCommand request, CancellationToken cancellationToken) @@ -26,15 +27,24 @@ public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIde 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); + + 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, + request.PortraitUrl); await context.SaveChangesAsync(cancellationToken); return result.GetValueOrDefault(); } } - diff --git a/src/Infrastructure/Data/ApplicationDbContextInitializer.cs b/src/Infrastructure/Data/ApplicationDbContextInitializer.cs index 6a985de..79d5e54 100644 --- a/src/Infrastructure/Data/ApplicationDbContextInitializer.cs +++ b/src/Infrastructure/Data/ApplicationDbContextInitializer.cs @@ -65,7 +65,12 @@ public class ApplicationDbContextInitializer( } // Default users - var administrator = new ApplicationUser { UserName = "administrator@localhost", Email = "administrator@localhost" }; + var administrator = new ApplicationUser + { + UserName = "administrator@localhost", + Email = "administrator@localhost", + PortraitUrl = "images/usersmedia/anonyme/profilepictures/profilePascal.jpg" + }; if (userManager.Users.All(u => u.UserName != administrator.UserName)) { diff --git a/src/Infrastructure/Identity/ApplicationUser.cs b/src/Infrastructure/Identity/ApplicationUser.cs index d956a9b..ccf91b8 100644 --- a/src/Infrastructure/Identity/ApplicationUser.cs +++ b/src/Infrastructure/Identity/ApplicationUser.cs @@ -15,4 +15,5 @@ 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 string? PortraitUrl { get; set; } } diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index d512ae5..37a907e 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -20,7 +20,7 @@ public class IdentityService( IAuthorizationService authorizationService, IHttpContextAccessor contextAccessor, IConfiguration configuration - ) +) : IIdentityService { public async Task GetUserNameAsync(string userId) @@ -29,11 +29,11 @@ public class IdentityService( return user?.UserName; } - + public async Task GetUserByUserNameAsync(string userName) { var response = await userManager.FindByNameAsync(userName); - + if (response == null) return null; var userModel = new UserModel() @@ -42,12 +42,13 @@ public class IdentityService( UserName = response.UserName, FirstName = response.FirstName, LastName = response.LastName, - Email = response.Email, + Email = response.Email, + PortraitUrl = response.PortraitUrl, }; return userModel; } - + public async Task> CreateUserAsync(Userinfo userInfo) { var applicationUser = new ApplicationUser @@ -57,7 +58,7 @@ public class IdentityService( FirstName = userInfo.GivenName, LastName = userInfo.FamilyName }; - + var password = Guid.NewGuid().ToString("N")[..32]; var identityResult = await userManager.CreateAsync(applicationUser, password); @@ -65,13 +66,14 @@ public class IdentityService( 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 { @@ -82,16 +84,24 @@ public class IdentityService( }; var response = await userManager.CreateAsync(applicationUser, password); - + 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) + 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, + string? portraitUrl) { var applicationUser = await userManager.FindByIdAsync(id); @@ -107,6 +117,7 @@ public class IdentityService( applicationUser.Address = address; applicationUser.About = about; applicationUser.Description = description; + applicationUser.PortraitUrl = portraitUrl; applicationUser.SocialNetworks = new SocialNetworks() { FacebookUrl = socialNetworks.FacebookUrl, @@ -130,7 +141,7 @@ public class IdentityService( return result; } - + public async Task FindUserByIdAsync(string id) { var response = await userManager.FindByIdAsync(id); @@ -152,6 +163,7 @@ public class IdentityService( Address = response.Address, About = response.About, Description = response.Description, + PortraitUrl = response.PortraitUrl, SocialNetworks = new SocialNetworksModel { FacebookUrl = response.SocialNetworks.FacebookUrl, @@ -167,7 +179,7 @@ public class IdentityService( return userModel; } - + public async Task GetCurrentUserAsync() { var currentUserId = contextAccessor.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; @@ -203,7 +215,7 @@ public class IdentityService( return user != null && await userManager.IsInRoleAsync(user, role); } - + public async Task CurrentUserIsInRoleAsync(string role) { var currentUserModel = await GetCurrentUserAsync(); @@ -241,44 +253,45 @@ public class IdentityService( return result.ToApplicationResult(); } - + public async Task AddRoleAsync(string userId, string role) { var hasAdminAccess = await CurrentUserIsInRoleAsync("Administrator"); - - if (!hasAdminAccess) return Result.Failure(new []{"Only administrator can assign new roles to a user."}); + + if (!hasAdminAccess) return Result.Failure(new[] { "Only administrator can assign new roles to a user." }); var user = await userManager.FindByIdAsync(userId); - - if (user is null) return Result.Failure(new []{"User not found."}); - + + if (user is null) return Result.Failure(new[] { "User not found." }); + var result = await userManager.AddToRoleAsync(user, role); return result.ToApplicationResult(); } - + public async Task> GetCurrentUserRolesAsync() { var currentUserModel = await GetCurrentUserAsync(); - + var currentUser = await userManager.FindByIdAsync(currentUserModel?.Id ?? ""); - - if (currentUser is null) return []; + + if (currentUser is null) return []; var userRoles = await userManager.GetRolesAsync(currentUser); return userRoles; } - + public async Task LoginAsync(string userName, string password) { - var result = await signInManager.PasswordSignInAsync(userName, password, isPersistent: false, lockoutOnFailure: false); - + var result = + await signInManager.PasswordSignInAsync(userName, password, isPersistent: false, lockoutOnFailure: false); + if (!result.Succeeded) { return null; } - + var user = await GetUserByUserNameAsync(userName); if (user is null) throw new InvalidOperationException(); @@ -298,3 +311,6 @@ public class IdentityService( return token; } } + +public class ConfigurationMissingException(string ConfigurationKey) + : Exception; diff --git a/src/Infrastructure/Migrations/20240701072549_AddPortraitUrlToUser.Designer.cs b/src/Infrastructure/Migrations/20240701072549_AddPortraitUrlToUser.Designer.cs new file mode 100644 index 0000000..2ce61d4 --- /dev/null +++ b/src/Infrastructure/Migrations/20240701072549_AddPortraitUrlToUser.Designer.cs @@ -0,0 +1,500 @@ +// +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("20240701072549_AddPortraitUrlToUser")] + partial class AddPortraitUrlToUser + { + /// + 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("PortraitUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.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/20240701072549_AddPortraitUrlToUser.cs b/src/Infrastructure/Migrations/20240701072549_AddPortraitUrlToUser.cs new file mode 100644 index 0000000..a279865 --- /dev/null +++ b/src/Infrastructure/Migrations/20240701072549_AddPortraitUrlToUser.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + /// + public partial class AddPortraitUrlToUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PortraitUrl", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PortraitUrl", + table: "AspNetUsers"); + } + } +} diff --git a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index cd78f54..80e3d53 100644 --- a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -222,6 +222,9 @@ namespace Hutopy.Infrastructure.Migrations b.Property("PhoneNumberConfirmed") .HasColumnType("bit"); + b.Property("PortraitUrl") + .HasColumnType("nvarchar(max)"); + b.Property("SecurityStamp") .HasColumnType("nvarchar(max)"); diff --git a/src/Infrastructure/Utils/GenerateJwtToken.cs b/src/Infrastructure/Utils/GenerateJwtToken.cs index 19e6cbd..a4d74de 100644 --- a/src/Infrastructure/Utils/GenerateJwtToken.cs +++ b/src/Infrastructure/Utils/GenerateJwtToken.cs @@ -7,8 +7,15 @@ namespace Hutopy.Infrastructure.Utils; public static class JwtTokenHelper { - public static string GenerateJwtToken(string issuer, string audience, string key, string? userId, string? email, - string? firstname, string? lastname, string? portraitUrl) + 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); @@ -19,7 +26,8 @@ public static class JwtTokenHelper 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.Name, email), + new Claim(ClaimTypes.GivenName, firstname), new Claim(ClaimTypes.Surname, lastname), }); From 3558952634305f844b60d45f92add251878fb08e Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Thu, 4 Jul 2024 20:47:07 -0400 Subject: [PATCH 13/16] merged --- .../Common/Interfaces/IIdentityService.cs | 2 +- .../Commands/UpdateCurrentUserCommand.cs | 2 +- .../Queries/GetMinimalUser/GetMinimalUser.cs | 10 +++-- .../Queries/GetMinimalUser/MinimalUserDto.cs | 6 +++ .../Identity/IdentityResultExtensions.cs | 3 +- .../Identity/IdentityService.cs | 42 ++++++++++++++++--- src/Infrastructure/Identity/RoleService.cs | 4 +- src/Infrastructure/Utils/GenerateJwtToken.cs | 10 +++-- src/Web/Controllers/GoogleController.cs | 2 +- 9 files changed, 62 insertions(+), 19 deletions(-) diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index edcbb9c..74e9240 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -12,7 +12,7 @@ public interface IIdentityService Task UpdateCurrentUserBannerPictureUrlAsync(string url); Task UpdateCurrentUserProfilePictureUrlAsync(string url); Task UpdateCurrentUserWebsiteIconUrlAsync(string url); - Task> UpdateCurrentUserAsync(UserModel userModel, string? portraitUrl); + Task> UpdateCurrentUserAsync(UserModel userModel); Task> GetCurrentUserRolesAsync(); Task FindUserByIdAsync(string id); Task FindUserByEmailAsync(string email); diff --git a/src/Application/Users/Commands/UpdateCurrentUserCommand.cs b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs index d47985a..8c9766f 100644 --- a/src/Application/Users/Commands/UpdateCurrentUserCommand.cs +++ b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs @@ -44,7 +44,7 @@ public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIde var userModel = mapper.Map(request); userModel.Id = identityUser.Id; - var result = await identityService.UpdateCurrentUserAsync(userModel, request.PortraitUrl); + var result = await identityService.UpdateCurrentUserAsync(userModel); await context.SaveChangesAsync(cancellationToken); diff --git a/src/Application/Users/Queries/GetMinimalUser/GetMinimalUser.cs b/src/Application/Users/Queries/GetMinimalUser/GetMinimalUser.cs index e7e5940..124e9cf 100644 --- a/src/Application/Users/Queries/GetMinimalUser/GetMinimalUser.cs +++ b/src/Application/Users/Queries/GetMinimalUser/GetMinimalUser.cs @@ -28,15 +28,17 @@ public class GetMinimalUserQueryHandler( { identityUser = await identityService.GetUserByUserNameAsync(request.UserName); } - - + var user = new MinimalUserDto { FirstName = identityUser?.FirstName ?? string.Empty, LastName = identityUser?.LastName ?? string.Empty, - UserName = identityUser?.UserName ?? string.Empty + UserName = identityUser?.UserName ?? string.Empty, + SocialNetworks = identityUser?.SocialNetworks ?? new(), + ProfileColors = identityUser?.ProfileColors ?? new(), + StoredDataUrls = identityUser?.StoredDataUrls ?? new(), }; return user; } -} \ No newline at end of file +} diff --git a/src/Application/Users/Queries/GetMinimalUser/MinimalUserDto.cs b/src/Application/Users/Queries/GetMinimalUser/MinimalUserDto.cs index 97fa6a1..7648fa7 100644 --- a/src/Application/Users/Queries/GetMinimalUser/MinimalUserDto.cs +++ b/src/Application/Users/Queries/GetMinimalUser/MinimalUserDto.cs @@ -1,3 +1,5 @@ +using Hutopy.Application.Users.Models; + namespace Hutopy.Application.Users.Queries.GetMinimalUser; public class MinimalUserDto @@ -5,4 +7,8 @@ public class MinimalUserDto public required string FirstName { get; init; } public required string LastName { get; init; } public required string UserName { get; init; } = String.Empty; + + public SocialNetworksModel SocialNetworks { get; init; } = new(); + public ProfileColorsModel ProfileColors { get; init; } = new(); + public StoredDataUrlsModel StoredDataUrls { get; init; } = new(); } diff --git a/src/Infrastructure/Identity/IdentityResultExtensions.cs b/src/Infrastructure/Identity/IdentityResultExtensions.cs index 630c372..0902e4f 100644 --- a/src/Infrastructure/Identity/IdentityResultExtensions.cs +++ b/src/Infrastructure/Identity/IdentityResultExtensions.cs @@ -1,4 +1,5 @@ -using Hutopy.Application.Common.Models; +using System.Linq; +using Hutopy.Application.Common.Models; using Microsoft.AspNetCore.Identity; namespace Hutopy.Infrastructure.Identity; diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index fe0fc3d..2bc6ff4 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -41,10 +41,42 @@ 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, + PhoneNumber = 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 + }, + StoredDataUrls = new StoredDataUrlsModel + { + ProfilePictureUrl = response.StoredDataUrls.ProfilePictureUrl, + BannerPictureUrl = response.StoredDataUrls.BannerPictureUrl, + WebsiteIconUrl = response.StoredDataUrls.WebsiteIconUrl, + } }; return userModel; @@ -88,7 +120,7 @@ public class IdentityService( return result; } - public async Task> UpdateCurrentUserAsync(UserModel userModel, string? portraitUrl) + public async Task> UpdateCurrentUserAsync(UserModel userModel) { var applicationUser = await userManager.FindByIdAsync(userModel.Id); @@ -104,7 +136,6 @@ public class IdentityService( applicationUser.Address = userModel.Address; applicationUser.About = userModel.About; applicationUser.Description = userModel.Description; - applicationUser.PortraitUrl = portraitUrl; applicationUser.SocialNetworks = new SocialNetworks { FacebookUrl = userModel.SocialNetworks.FacebookUrl, @@ -155,7 +186,6 @@ public class IdentityService( Address = response.Address, About = response.About, Description = response.Description, - PortraitUrl = response.PortraitUrl, SocialNetworks = new SocialNetworksModel { FacebookUrl = response.SocialNetworks.FacebookUrl, @@ -387,7 +417,7 @@ public class IdentityService( email: user.Email, firstname: user.FirstName, lastname: user.LastName, - portraitUrl: user.PortraitUrl); + profilePictureUrl: user.StoredDataUrls.ProfilePictureUrl); return token; } diff --git a/src/Infrastructure/Identity/RoleService.cs b/src/Infrastructure/Identity/RoleService.cs index 71f0ed0..59fc22b 100644 --- a/src/Infrastructure/Identity/RoleService.cs +++ b/src/Infrastructure/Identity/RoleService.cs @@ -1,4 +1,6 @@ -using Hutopy.Application.Common.Interfaces; +using System; +using System.Threading.Tasks; +using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Models; using Microsoft.AspNetCore.Identity; diff --git a/src/Infrastructure/Utils/GenerateJwtToken.cs b/src/Infrastructure/Utils/GenerateJwtToken.cs index a4d74de..013da69 100644 --- a/src/Infrastructure/Utils/GenerateJwtToken.cs +++ b/src/Infrastructure/Utils/GenerateJwtToken.cs @@ -1,4 +1,6 @@ -using System.IdentityModel.Tokens.Jwt; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.IdentityModel.Tokens; @@ -15,7 +17,7 @@ public static class JwtTokenHelper string? email, string? firstname, string? lastname, - string? portraitUrl) + string? profilePictureUrl) { var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); @@ -31,9 +33,9 @@ public static class JwtTokenHelper new Claim(ClaimTypes.Surname, lastname), }); - if (portraitUrl is not null) + if (profilePictureUrl is not null) { - claims.Add(new Claim("portrait-url", portraitUrl)); + claims.Add(new Claim("portrait-url", profilePictureUrl)); } var token = new JwtSecurityToken( diff --git a/src/Web/Controllers/GoogleController.cs b/src/Web/Controllers/GoogleController.cs index dfaa91e..38e04e7 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.StoredDataUrls.ProfilePictureUrl); return Ok(new { accessToken = token, email }); } From 4a0502488e6a9af69b59d5cec7dbc4178cc557e4 Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Thu, 4 Jul 2024 20:50:20 -0400 Subject: [PATCH 14/16] Merged with master --- src/Infrastructure/Data/ApplicationDbContextInitializer.cs | 5 ++++- src/Infrastructure/Identity/ApplicationUserManager.cs | 6 +++++- src/Infrastructure/Identity/IdentityService.cs | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure/Data/ApplicationDbContextInitializer.cs b/src/Infrastructure/Data/ApplicationDbContextInitializer.cs index bc57199..71abda5 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 Hutopy.Infrastructure.Identity.OwnedEntities; using Microsoft.AspNetCore.Builder; diff --git a/src/Infrastructure/Identity/ApplicationUserManager.cs b/src/Infrastructure/Identity/ApplicationUserManager.cs index 987c8b6..8125aae 100644 --- a/src/Infrastructure/Identity/ApplicationUserManager.cs +++ b/src/Infrastructure/Identity/ApplicationUserManager.cs @@ -1,4 +1,8 @@ -using Microsoft.AspNetCore.Identity; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index b03c9e2..3d9bbc0 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Google.Apis.Oauth2.v2.Data; using System.Security.Claims; +using System.Threading; using System.Threading.Tasks; using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Models; From 4da28344f4634817df7a0ad10bd7e6e800f79b0d Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Thu, 4 Jul 2024 21:12:46 -0400 Subject: [PATCH 15/16] Remove portraitUrl -> replaced by ProfilePictureUrl Removed duplicate ContentStore connectionString --- .../Commands/UpdateCurrentUserCommand.cs | 1 - .../Identity/ApplicationUser.cs | 1 - ...240705010235_RemovePortraitUrl.Designer.cs | 560 ++++++++++++++++++ .../20240705010235_RemovePortraitUrl.cs | 28 + .../ApplicationDbContextModelSnapshot.cs | 3 - src/Web/appsettings.Development.json | 1 - 6 files changed, 588 insertions(+), 6 deletions(-) create mode 100644 src/Infrastructure/Migrations/20240705010235_RemovePortraitUrl.Designer.cs create mode 100644 src/Infrastructure/Migrations/20240705010235_RemovePortraitUrl.cs diff --git a/src/Application/Users/Commands/UpdateCurrentUserCommand.cs b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs index 8c9766f..7d55961 100644 --- a/src/Application/Users/Commands/UpdateCurrentUserCommand.cs +++ b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs @@ -19,7 +19,6 @@ public class UpdateCurrentUserCommand : IRequest public required string Description { get; init; } public required SocialNetworksModel SocialNetworks { get; init; } public required ProfileColorsModel ProfileColors { get; init; } - public string PortraitUrl { get; init; } = string.Empty; [NotMapped] diff --git a/src/Infrastructure/Identity/ApplicationUser.cs b/src/Infrastructure/Identity/ApplicationUser.cs index 29e74ec..74ad0cf 100644 --- a/src/Infrastructure/Identity/ApplicationUser.cs +++ b/src/Infrastructure/Identity/ApplicationUser.cs @@ -18,5 +18,4 @@ public class ApplicationUser : IdentityUser public SocialNetworks SocialNetworks { get; set; } = new(); public ProfileColors ProfileColors { get; set; } = new(); public StoredDataUrls StoredDataUrls { get; set; } = new(); - public string? PortraitUrl { get; set; } } diff --git a/src/Infrastructure/Migrations/20240705010235_RemovePortraitUrl.Designer.cs b/src/Infrastructure/Migrations/20240705010235_RemovePortraitUrl.Designer.cs new file mode 100644 index 0000000..1df55e6 --- /dev/null +++ b/src/Infrastructure/Migrations/20240705010235_RemovePortraitUrl.Designer.cs @@ -0,0 +1,560 @@ +// +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("20240705010235_RemovePortraitUrl")] + partial class RemovePortraitUrl + { + /// + 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("CreatorAlias") + .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/20240705010235_RemovePortraitUrl.cs b/src/Infrastructure/Migrations/20240705010235_RemovePortraitUrl.cs new file mode 100644 index 0000000..e504870 --- /dev/null +++ b/src/Infrastructure/Migrations/20240705010235_RemovePortraitUrl.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + /// + public partial class RemovePortraitUrl : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PortraitUrl", + table: "AspNetUsers"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PortraitUrl", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: true); + } + } +} diff --git a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 0c4a193..3d3f44a 100644 --- a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -225,9 +225,6 @@ namespace Hutopy.Infrastructure.Migrations b.Property("PhoneNumberConfirmed") .HasColumnType("bit"); - b.Property("PortraitUrl") - .HasColumnType("nvarchar(max)"); - b.Property("SecurityStamp") .HasColumnType("nvarchar(max)"); diff --git a/src/Web/appsettings.Development.json b/src/Web/appsettings.Development.json index decba8e..d118e3b 100644 --- a/src/Web/appsettings.Development.json +++ b/src/Web/appsettings.Development.json @@ -9,7 +9,6 @@ }, "ConnectionStrings": { "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", "ContentStore": "Server=localhost,1433;Initial Catalog=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True;MultiSubnetFailover=True" }, "Authentication": { From 44d86a1f5697331eef9b69b00756a1215ed1aadb Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Thu, 4 Jul 2024 21:13:19 -0400 Subject: [PATCH 16/16] Commentstore --- src/Web/appsettings.Development.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Web/appsettings.Development.json b/src/Web/appsettings.Development.json index d118e3b..decba8e 100644 --- a/src/Web/appsettings.Development.json +++ b/src/Web/appsettings.Development.json @@ -9,6 +9,7 @@ }, "ConnectionStrings": { "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", "ContentStore": "Server=localhost,1433;Initial Catalog=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True;MultiSubnetFailover=True" }, "Authentication": {