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/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/Application/AzureBlobStorage/Constants/CommonFileNames.cs b/src/Application/AzureBlobStorage/Constants/CommonFileNames.cs index 7eabb9a..b6d44aa 100644 --- a/src/Application/AzureBlobStorage/Constants/CommonFileNames.cs +++ b/src/Application/AzureBlobStorage/Constants/CommonFileNames.cs @@ -3,4 +3,6 @@ public static class CommonFileNames { public static string ProfilePicture = "profilePicture"; + public static string BannerPicture = "bannerPicture"; + public static string WebsiteIcon = "websiteIcon"; } diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index cd97117..74e9240 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 UpdateCurrentUserBannerPictureUrlAsync(string url); + Task UpdateCurrentUserProfilePictureUrlAsync(string url); + Task UpdateCurrentUserWebsiteIconUrlAsync(string url); + Task> UpdateCurrentUserAsync(UserModel userModel); + Task> GetCurrentUserRolesAsync(); + Task FindUserByIdAsync(string id); + Task FindUserByEmailAsync(string email); 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..294e2ed 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,28 @@ public class Result( return new Result(false, errors); } } + +public class Result( + T? value, + bool succeeded, + IEnumerable errors) +{ + public bool Succeeded { get; init; } = succeeded; + public string[] Errors { get; init; } = errors.ToArray(); + public T? Value { get; set; } = value; + + public T GetValueOrDefault() + { + return Value ?? default(T)!; + } + + public static Result Success(T value) + { + return new Result(value, true, Array.Empty()); + } + + public static Result Failure(T value, IEnumerable errors) + { + return new Result(value, false, errors); + } +} diff --git a/src/Application/Common/Models/UserModel.cs b/src/Application/Common/Models/UserModel.cs index 5c4358b..ccff715 100644 --- a/src/Application/Common/Models/UserModel.cs +++ b/src/Application/Common/Models/UserModel.cs @@ -1,10 +1,24 @@ +using Hutopy.Application.Users.Models; + namespace Hutopy.Application.Common.Models; public class UserModel { - public string? Id { get; set; } - public string? UserName { get; set; } - public string? FirstName { get; set; } - public string? LastName { get; set; } - public string? Email { get; set; } + public string Id { get; set; } = string.Empty; + public string UserName { get; set; } = string.Empty; + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string Occupation { get; set; } = string.Empty; + public string Email { get; init; } = string.Empty; + public string PhoneNumber { get; init; } = string.Empty; + public string BirthDate { get; init; } = string.Empty; + public string Country { get; init; } = string.Empty; + public string City { get; init; } = string.Empty; + 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 ProfileColorsModel ProfileColors { get; init; } = new(); + public StoredDataUrlsModel StoredDataUrls { get; init; } = new(); + public string ProfilePictureUrl { get; set; } = string.Empty; } diff --git a/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs b/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs index 0ddd17e..0c74658 100644 --- a/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs +++ b/src/Application/Stripe/Commands/ConfirmStripeTransaction.cs @@ -17,29 +17,29 @@ public class Data public class Object { - public string Id { get; set; } = String.Empty; + public string Id { get; set; } = string.Empty; public int Amount { get; set; } public BillingDetails Billing_details { get; set; } = new(); - public string Calculated_statement_descriptor { get; set; } = String.Empty; - public string Currency { get; set; } = String.Empty; + public string Calculated_statement_descriptor { get; set; } = string.Empty; + public string Currency { get; set; } = string.Empty; public bool Paid { get; set; } - public string Payment_intent { get; set; } = String.Empty; - public string Payment_method { get; set; } = String.Empty; - public string Receipt_url { get; set; } = String.Empty; - public string Status { get; set; } = String.Empty; - public string Failure_message { get; set; } = String.Empty; + public string Payment_intent { get; set; } = string.Empty; + public string Payment_method { get; set; } = string.Empty; + public string Receipt_url { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public string Failure_message { get; set; } = string.Empty; } public class BillingDetails { - public string Email { get; set; } = String.Empty; - public string Name { get; set; } = String.Empty; - public string Phone { get; set; } = String.Empty; + public string Email { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; } public class Request { - public string Id { get; set; } = String.Empty; + public string Id { get; set; } = string.Empty; } public class ConfirmStripeTransactionCommandHandler( diff --git a/src/Application/Users/Commands/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/Application/Users/Commands/UpdateCurrentUserCommand.cs b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs new file mode 100644 index 0000000..193d904 --- /dev/null +++ b/src/Application/Users/Commands/UpdateCurrentUserCommand.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Hutopy.Application.Common.Interfaces; +using Hutopy.Application.Common.Models; +using Hutopy.Application.Users.Models; + +namespace Hutopy.Application.Users.Commands; + +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 required ProfileColorsModel ProfileColors { get; init; } + + [NotMapped] + private class Mapping : Profile + { + public Mapping() + { + CreateMap(); + } + } +} + +public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIdentityService identityService, IMapper mapper) : + IRequestHandler +{ + public async Task Handle(UpdateCurrentUserCommand request, CancellationToken cancellationToken) + { + var identityUser = await identityService.GetCurrentUserAsync(); + + if (identityUser?.Id is null) return string.Empty; + + var userModel = mapper.Map(request); + userModel.Id = identityUser.Id; + + var result = await identityService.UpdateCurrentUserAsync(userModel); + + await context.SaveChangesAsync(cancellationToken); + + return result.GetValueOrDefault(); + } +} + diff --git a/src/Application/Users/Commands/UploadBannerPicture.cs b/src/Application/Users/Commands/UploadBannerPicture.cs new file mode 100644 index 0000000..66922a6 --- /dev/null +++ b/src/Application/Users/Commands/UploadBannerPicture.cs @@ -0,0 +1,27 @@ +using Hutopy.Application.AzureBlobStorage.Constants; +using Hutopy.Application.Common.Interfaces; + +namespace Hutopy.Application.Users.Commands; + +public class UploadBannerPictureCommand : IRequest +{ + public required Stream BannerPicture { get; init; } +} + +public class UploadBannerPictureCommandHandler(IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler +{ + public async Task Handle(UploadBannerPictureCommand request, CancellationToken cancellationToken) + { + var identityUser = await identityService.GetCurrentUserAsync(); + var currentUserId = new Guid(identityUser?.Id ?? "").ToString(); + + var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.BannerPicture}"; + + var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.BannerPicture); + + await identityService.UpdateCurrentUserBannerPictureUrlAsync(url); + + return url; + } +} + diff --git a/src/Application/Users/Commands/UploadProfilePicture.cs b/src/Application/Users/Commands/UploadProfilePicture.cs index 08746d9..6fc4714 100644 --- a/src/Application/Users/Commands/UploadProfilePicture.cs +++ b/src/Application/Users/Commands/UploadProfilePicture.cs @@ -19,6 +19,8 @@ public class UploadProfilePictureCommandHandler(IIdentityService identityService var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.ProfilePicture); + await identityService.UpdateCurrentUserProfilePictureUrlAsync(url); + return url; } } diff --git a/src/Application/Users/Commands/UploadWebsiteIcon.cs b/src/Application/Users/Commands/UploadWebsiteIcon.cs new file mode 100644 index 0000000..78a83a2 --- /dev/null +++ b/src/Application/Users/Commands/UploadWebsiteIcon.cs @@ -0,0 +1,27 @@ +using Hutopy.Application.AzureBlobStorage.Constants; +using Hutopy.Application.Common.Interfaces; + +namespace Hutopy.Application.Users.Commands; + +public class UploadWebsiteIconCommand : IRequest +{ + public required Stream WebsiteIcon { get; init; } +} + +public class UploadWebsiteIconCommandHandler(IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler +{ + public async Task Handle(UploadWebsiteIconCommand request, CancellationToken cancellationToken) + { + var identityUser = await identityService.GetCurrentUserAsync(); + var currentUserId = new Guid(identityUser?.Id ?? "").ToString(); + + var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.WebsiteIcon}"; + + var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.WebsiteIcon); + + await identityService.UpdateCurrentUserWebsiteIconUrlAsync(url); + + return url; + } +} + diff --git a/src/Application/Users/Models/ProfileColorsModel.cs b/src/Application/Users/Models/ProfileColorsModel.cs new file mode 100644 index 0000000..04baadb --- /dev/null +++ b/src/Application/Users/Models/ProfileColorsModel.cs @@ -0,0 +1,9 @@ +namespace Hutopy.Application.Users.Models; + +public class ProfileColorsModel +{ + public string BannerTop { get; init; } = String.Empty; + public string BannerBottom { get; init; } = String.Empty; + public string Accent { get; init; } = String.Empty; + public string Menu { get; init; } = String.Empty; +} diff --git a/src/Application/Users/Models/SocialNetworksModel.cs b/src/Application/Users/Models/SocialNetworksModel.cs new file mode 100644 index 0000000..19a2f02 --- /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/Models/StoredDataUrlsModel.cs b/src/Application/Users/Models/StoredDataUrlsModel.cs new file mode 100644 index 0000000..0539508 --- /dev/null +++ b/src/Application/Users/Models/StoredDataUrlsModel.cs @@ -0,0 +1,8 @@ +namespace Hutopy.Application.Users.Models; + +public class StoredDataUrlsModel +{ + public string BannerPictureUrl { get; set; } = string.Empty; + public string ProfilePictureUrl { get; set; } = string.Empty; + public string WebsiteIconUrl { get; set; } = string.Empty; +} diff --git a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs index 49f1152..f8cc1ab 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) @@ -28,12 +28,24 @@ 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 ?? "", + PhoneNumber = identityUser.PhoneNumber ?? "", + Email = identityUser.Email ?? "", + BirthDate = identityUser.BirthDate ?? "", + Country = identityUser.Country ?? "", + City = identityUser.City ?? "", + Address = identityUser.Address ?? "", + About = identityUser.About ?? "", + Description = identityUser.Description ?? "", + SocialNetworks = identityUser.SocialNetworks, + ProfileColors = identityUser.ProfileColors, + StoredDataUrls = identityUser.StoredDataUrls, UserTransactions = transactions, TotalBalance = transactions.Sum(x => x.Amount), - UserRoles = roles + UserRoles = roles, }; return user; diff --git a/src/Application/Users/Queries/GetCurrentUser/UserDto.cs b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs index c7d121d..23dfaa8 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 @@ -5,9 +7,20 @@ public class UserDto public Guid Id { get; init; } public required string FirstName { get; init; } public required string LastName { get; init; } - public string UserName { get; init; } = String.Empty; + public string UserName { get; init; } = string.Empty; + public string Occupation { get; init; } = string.Empty; + public string Email { get; init; } = string.Empty; + public string PhoneNumber { get; init; } = string.Empty; + public string BirthDate { get; init; } = string.Empty; + public string Country { get; init; } = string.Empty; + public string City { get; init; } = string.Empty; + 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 ProfileColorsModel ProfileColors { get; init; } = new(); + public StoredDataUrlsModel StoredDataUrls { get; init; } = new(); public List UserTransactions { get; init; } = []; public IList UserRoles { get; init; } = []; public required decimal TotalBalance { get; init; } - } diff --git a/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs b/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs index f54f96b..b25521c 100644 --- a/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs +++ b/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs @@ -1,3 +1,6 @@ +using System; +using System.IO; +using System.Threading.Tasks; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Hutopy.Application.Common.Interfaces; diff --git a/src/Infrastructure/Data/ApplicationDbContextInitializer.cs b/src/Infrastructure/Data/ApplicationDbContextInitializer.cs index 6a985de..b771d57 100644 --- a/src/Infrastructure/Data/ApplicationDbContextInitializer.cs +++ b/src/Infrastructure/Data/ApplicationDbContextInitializer.cs @@ -1,4 +1,7 @@ -using Hutopy.Domain.Constants; +using System; +using System.Linq; +using System.Threading.Tasks; +using Hutopy.Domain.Constants; using Hutopy.Infrastructure.Identity; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity; diff --git a/src/Infrastructure/Data/Configurations/ApplicationUserConfiguration.cs b/src/Infrastructure/Data/Configurations/ApplicationUserConfiguration.cs index 0624b7a..b3a3735 100644 --- a/src/Infrastructure/Data/Configurations/ApplicationUserConfiguration.cs +++ b/src/Infrastructure/Data/Configurations/ApplicationUserConfiguration.cs @@ -12,5 +12,15 @@ public class ApplicationUserConfiguration : IEntityTypeConfiguration u.SocialNetworks) .ToTable($"{nameof(ApplicationUser)}_SocialNetworks"); + + // Relationship between ApplicationUser and ProfileColors + builder + .OwnsOne(u => u.ProfileColors) + .ToTable($"{nameof(ApplicationUser)}_ProfileColors"); + + // Relationship between ApplicationUser and StoredDataUrls + builder + .OwnsOne(u => u.StoredDataUrls) + .ToTable($"{nameof(ApplicationUser)}_StoredDataUrls"); } } diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index e03ce4c..2dfccb2 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -1,4 +1,5 @@ -using Hutopy.Application.Common.Interfaces; +using System; +using Hutopy.Application.Common.Interfaces; using Hutopy.Domain.Constants; using Hutopy.Infrastructure.AzureBlob; using Hutopy.Infrastructure.Data; @@ -15,22 +16,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 +41,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/Infrastructure/Identity/ApplicationUser.cs b/src/Infrastructure/Identity/ApplicationUser.cs index 700a074..fbf49f7 100644 --- a/src/Infrastructure/Identity/ApplicationUser.cs +++ b/src/Infrastructure/Identity/ApplicationUser.cs @@ -7,5 +7,14 @@ 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(); + public ProfileColors ProfileColors { get; set; } = new(); + public StoredDataUrls StoredDataUrls { get; set; } = new(); } diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 02b5ce0..b0be8f2 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -1,7 +1,12 @@ +using System; +using System.Collections.Generic; using Google.Apis.Oauth2.v2.Data; using System.Security.Claims; +using System.Threading.Tasks; using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Models; +using Hutopy.Application.Users.Models; +using Hutopy.Infrastructure.Identity.OwnedEntities; using Hutopy.Infrastructure.Utils; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -44,23 +49,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, @@ -70,12 +62,16 @@ 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(applicationUser.Id, applicationResult.Succeeded, applicationResult.Errors); + + 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 { @@ -87,7 +83,54 @@ public class IdentityService( var response = await userManager.CreateAsync(applicationUser, password); - return response.ToApplicationResult(); + var result = new Result(applicationUser.Id, response.Succeeded, response.ToApplicationResult().Errors); + + return result; + } + + public async Task> UpdateCurrentUserAsync(UserModel userModel) + { + var applicationUser = await userManager.FindByIdAsync(userModel.Id); + + if (applicationUser is null) return Result.Failure("", new[] { "User not found." }); + + applicationUser.FirstName = userModel.FirstName; + applicationUser.LastName = userModel.LastName; + applicationUser.Occupation = userModel.Occupation; + applicationUser.PhoneNumber = userModel.PhoneNumber; + applicationUser.BirthDate = userModel.BirthDate; + applicationUser.Country = userModel.Country; + applicationUser.City = userModel.City; + applicationUser.Address = userModel.Address; + applicationUser.About = userModel.About; + applicationUser.Description = userModel.Description; + applicationUser.SocialNetworks = new SocialNetworks + { + FacebookUrl = userModel.SocialNetworks.FacebookUrl, + InstagramUrl = userModel.SocialNetworks.InstagramUrl, + XUrl = userModel.SocialNetworks.XUrl, + LinkedInUrl = userModel.SocialNetworks.LinkedInUrl, + TikTokUrl = userModel.SocialNetworks.TikTokUrl, + YoutubeUrl = userModel.SocialNetworks.YoutubeUrl, + RedditUrl = userModel.SocialNetworks.RedditUrl, + YourWebsiteUrl = userModel.SocialNetworks.YourWebsiteUrl + }; + applicationUser.ProfileColors = new ProfileColors + { + BannerTop = userModel.ProfileColors.BannerTop, + BannerBottom = userModel.ProfileColors.BannerBottom, + Accent = userModel.ProfileColors.Accent, + Menu = userModel.ProfileColors.Menu + }; + + var response = await userManager.UpdateAsync(applicationUser); + + var applicationResult = response.ToApplicationResult(); + + var result = new Result(userModel.Id, applicationResult.Succeeded, + applicationResult.Errors); + + return result; } public async Task FindUserByIdAsync(string id) @@ -96,13 +139,95 @@ public class IdentityService( if (response == null) return null; - var userModel = new UserModel() + 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; + } + + public async Task FindUserByEmailAsync(string email) + { + var response = await userManager.FindByEmailAsync(email); + + if (response == null) return null; + + var userModel = new UserModel + { + Id = response.Id, + UserName = response.UserName ?? string.Empty, + FirstName = response.FirstName, + LastName = response.LastName, + Email = response.Email ?? string.Empty, + Occupation = response.Occupation, + 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; @@ -119,22 +244,49 @@ public class IdentityService( return await FindUserByIdAsync(currentUserId); } - public async Task FindUserByEmailAsync(string email) + public async Task UpdateCurrentUserBannerPictureUrlAsync(string url) { - var response = await userManager.FindByEmailAsync(email); + var userModel = await GetCurrentUserAsync(); + if (userModel is null) return Result.Failure(new[] { "User not found." }); - if (response == null) return null; + var applicationUser = await userManager.FindByIdAsync(userModel.Id); + if (applicationUser is null) return Result.Failure(new[] { "ApplicationUser not found." }); - var userModel = new UserModel - { - Id = response.Id, - UserName = response.UserName, - FirstName = response.FirstName, - LastName = response.LastName, - Email = response.Email - }; + applicationUser.StoredDataUrls.BannerPictureUrl = url; + + var response = await userManager.UpdateAsync(applicationUser); - return userModel; + return response.ToApplicationResult(); + } + + public async Task UpdateCurrentUserProfilePictureUrlAsync(string url) + { + var userModel = await GetCurrentUserAsync(); + if (userModel is null) return Result.Failure(new[] { "User not found." }); + + var applicationUser = await userManager.FindByIdAsync(userModel.Id); + if (applicationUser is null) return Result.Failure(new[] { "ApplicationUser not found." }); + + applicationUser.StoredDataUrls.ProfilePictureUrl = url; + + var response = await userManager.UpdateAsync(applicationUser); + + return response.ToApplicationResult(); + } + + public async Task UpdateCurrentUserWebsiteIconUrlAsync(string url) + { + var userModel = await GetCurrentUserAsync(); + if (userModel is null) return Result.Failure(new[] { "User not found." }); + + var applicationUser = await userManager.FindByIdAsync(userModel.Id); + if (applicationUser is null) return Result.Failure(new[] { "ApplicationUser not found." }); + + applicationUser.StoredDataUrls.WebsiteIconUrl = url; + + var response = await userManager.UpdateAsync(applicationUser); + + return response.ToApplicationResult(); } public async Task IsInRoleAsync(string userId, string role) @@ -218,13 +370,22 @@ public class IdentityService( { return null; } - + var user = await GetUserByUserNameAsync(userName); + + if (user is null) throw new InvalidOperationException(); + + var jwtSection = configuration.GetRequiredSection("Authentication:Jwt"); + var token = JwtTokenHelper.GenerateJwtToken( - issuer: configuration["Jwt-Issuer"] ?? "", - audience: configuration["Jwt-Audience"] ?? "", - key: configuration["Jwt-Key"] ?? "", - userId: user?.Id ?? ""); + issuer: jwtSection["Issuer"] ?? "", + audience: jwtSection["Audience"] ?? "", + key: jwtSection["Key"] ?? "", + userId: user.Id, + email: user.Email, + firstname: user.FirstName, + lastname: user.LastName, + portraitUrl: user.ProfilePictureUrl); return token; } diff --git a/src/Infrastructure/Identity/OwnedEntities/ProfileColors.cs b/src/Infrastructure/Identity/OwnedEntities/ProfileColors.cs new file mode 100644 index 0000000..3783044 --- /dev/null +++ b/src/Infrastructure/Identity/OwnedEntities/ProfileColors.cs @@ -0,0 +1,9 @@ +namespace Hutopy.Infrastructure.Identity.OwnedEntities; + +public class ProfileColors +{ + public string BannerTop { get; init; } = string.Empty; + public string BannerBottom { get; init; } = string.Empty; + public string Accent { get; init; } = string.Empty; + public string Menu { get; init; } = string.Empty; +} diff --git a/src/Infrastructure/Identity/OwnedEntities/SocialNetworks.cs b/src/Infrastructure/Identity/OwnedEntities/SocialNetworks.cs index 1e0d30e..a03e139 100644 --- a/src/Infrastructure/Identity/OwnedEntities/SocialNetworks.cs +++ b/src/Infrastructure/Identity/OwnedEntities/SocialNetworks.cs @@ -2,12 +2,12 @@ namespace Hutopy.Infrastructure.Identity.OwnedEntities; public class SocialNetworks { - public string FacebookUrl { get; init; } = String.Empty; - public string InstagramUrl { get; init; } = String.Empty; - public string XUrl { get; init; } = String.Empty; - public string LinkedInUrl { get; init; } = String.Empty; - public string TikTokUrl { get; init; } = String.Empty; - public string YoutubeUrl { get; init; } = String.Empty; - public string RedditUrl { get; init; } = String.Empty; - public string YourWebsiteUrl { get; init; } = String.Empty; + public string FacebookUrl { get; init; } = string.Empty; + public string InstagramUrl { get; init; } = string.Empty; + public string XUrl { get; init; } = string.Empty; + public string LinkedInUrl { get; init; } = string.Empty; + public string TikTokUrl { get; init; } = string.Empty; + public string YoutubeUrl { get; init; } = string.Empty; + public string RedditUrl { get; init; } = string.Empty; + public string YourWebsiteUrl { get; init; } = string.Empty; } diff --git a/src/Infrastructure/Identity/OwnedEntities/StoredDataUrls.cs b/src/Infrastructure/Identity/OwnedEntities/StoredDataUrls.cs new file mode 100644 index 0000000..bea6099 --- /dev/null +++ b/src/Infrastructure/Identity/OwnedEntities/StoredDataUrls.cs @@ -0,0 +1,8 @@ +namespace Hutopy.Infrastructure.Identity.OwnedEntities; + +public class StoredDataUrls +{ + public string BannerPictureUrl { get; set; } = string.Empty; + public string ProfilePictureUrl { get; set; } = string.Empty; + public string WebsiteIconUrl { get; set; } = string.Empty; +} diff --git a/src/Infrastructure/Migrations/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/20240630163057_AddMoreInformationsToUser.Designer.cs b/src/Infrastructure/Migrations/20240630163057_AddMoreInformationsToUser.Designer.cs new file mode 100644 index 0000000..af10f04 --- /dev/null +++ b/src/Infrastructure/Migrations/20240630163057_AddMoreInformationsToUser.Designer.cs @@ -0,0 +1,557 @@ +// +using System; +using Hutopy.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240630163057_AddMoreInformationsToUser")] + partial class AddMoreInformationsToUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Domain.Entities.FutureCreator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReasonToJoin") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SocialNetworkAccount") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("FutureCreators"); + }); + + modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsConfirmed") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Paid") + .HasColumnType("bit"); + + b.Property("StripeBillingDetailEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeBillingDetailName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeChargeId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeEventId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentIntent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeReceiptUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TipMessage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("UserTransactions"); + }); + + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("About") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BirthDate") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Occupation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => + { + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.ProfileColors", "ProfileColors", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("Accent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("BannerBottom") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("BannerTop") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("Menu") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_ProfileColors", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.SocialNetworks", "SocialNetworks", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("FacebookUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("InstagramUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("LinkedInUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("RedditUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("TikTokUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("XUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("YourWebsiteUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("YoutubeUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_SocialNetworks", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.StoredDataUrls", "StoredDataUrls", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("BannerPictureUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("ProfilePictureUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("WebsiteIconUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_StoredDataUrls", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.Navigation("ProfileColors") + .IsRequired(); + + b.Navigation("SocialNetworks") + .IsRequired(); + + b.Navigation("StoredDataUrls") + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Migrations/20240630163057_AddMoreInformationsToUser.cs b/src/Infrastructure/Migrations/20240630163057_AddMoreInformationsToUser.cs new file mode 100644 index 0000000..85e7b7e --- /dev/null +++ b/src/Infrastructure/Migrations/20240630163057_AddMoreInformationsToUser.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + /// + public partial class AddMoreInformationsToUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApplicationUser_ProfileColors", + columns: table => new + { + ApplicationUserId = table.Column(type: "nvarchar(450)", nullable: false), + BannerTop = table.Column(type: "nvarchar(max)", nullable: false), + BannerBottom = table.Column(type: "nvarchar(max)", nullable: false), + Accent = table.Column(type: "nvarchar(max)", nullable: false), + Menu = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApplicationUser_ProfileColors", x => x.ApplicationUserId); + table.ForeignKey( + name: "FK_ApplicationUser_ProfileColors_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql(@" + INSERT INTO ApplicationUser_ProfileColors (ApplicationUserId, BannerTop, BannerBottom, Accent, Menu) + SELECT Id, '', '', '', '' + FROM AspNetUsers + "); + + migrationBuilder.CreateTable( + name: "ApplicationUser_StoredDataUrls", + columns: table => new + { + ApplicationUserId = table.Column(type: "nvarchar(450)", nullable: false), + BannerPictureUrl = table.Column(type: "nvarchar(max)", nullable: false), + ProfilePictureUrl = table.Column(type: "nvarchar(max)", nullable: false), + WebsiteIconUrl = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApplicationUser_StoredDataUrls", x => x.ApplicationUserId); + table.ForeignKey( + name: "FK_ApplicationUser_StoredDataUrls_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql(@" + INSERT INTO ApplicationUser_StoredDataUrls (ApplicationUserId, BannerPictureUrl, ProfilePictureUrl, WebsiteIconUrl) + SELECT Id, '', '', '' + FROM AspNetUsers + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApplicationUser_ProfileColors"); + + migrationBuilder.DropTable( + name: "ApplicationUser_StoredDataUrls"); + } + } +} diff --git a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index fcb6687..12a98ec 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)"); @@ -361,6 +389,35 @@ namespace Hutopy.Infrastructure.Migrations modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => { + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.ProfileColors", "ProfileColors", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("Accent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("BannerBottom") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("BannerTop") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("Menu") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_ProfileColors", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.SocialNetworks", "SocialNetworks", b1 => { b1.Property("ApplicationUserId") @@ -406,8 +463,39 @@ namespace Hutopy.Infrastructure.Migrations .HasForeignKey("ApplicationUserId"); }); + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.StoredDataUrls", "StoredDataUrls", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("BannerPictureUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("ProfilePictureUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("WebsiteIconUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_StoredDataUrls", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.Navigation("ProfileColors") + .IsRequired(); + b.Navigation("SocialNetworks") .IsRequired(); + + b.Navigation("StoredDataUrls") + .IsRequired(); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => diff --git a/src/Infrastructure/Stripe/StripeService.cs b/src/Infrastructure/Stripe/StripeService.cs index 7b78fff..3f4e366 100644 --- a/src/Infrastructure/Stripe/StripeService.cs +++ b/src/Infrastructure/Stripe/StripeService.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Stripe; using Stripe.Checkout; using Hutopy.Application.Common.Interfaces; diff --git a/src/Infrastructure/Utils/GenerateJwtToken.cs b/src/Infrastructure/Utils/GenerateJwtToken.cs index 9f01d6a..19e6cbd 100644 --- a/src/Infrastructure/Utils/GenerateJwtToken.cs +++ b/src/Infrastructure/Utils/GenerateJwtToken.cs @@ -7,17 +7,26 @@ 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 claims = new[] + 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.NameIdentifier, userId), + new Claim(ClaimTypes.Email, email), + new Claim(ClaimTypes.GivenName, firstname), + new Claim(ClaimTypes.Surname, lastname), + }); - var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); - var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + if (portraitUrl is not null) + { + claims.Add(new Claim("portrait-url", portraitUrl)); + } var token = new JwtSecurityToken( issuer: issuer, 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..8206069 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,33 @@ 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( + 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.ProfilePictureUrl); + + return Ok(new { accessToken = token, email }); } public class GoogleSignInRequest diff --git a/src/Web/DependencyInjection.cs b/src/Web/DependencyInjection.cs index 3a7eb61..2a76f33 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 @@ -39,26 +39,11 @@ public static class DependencyInjection services.AddEndpointsApiExplorer(); - services.AddOpenApiDocument((configure, sp) => - { - 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.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); - }); - 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 +55,12 @@ 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; @@ -81,36 +68,50 @@ public static class DependencyInjection .AddCookie("Identity.Application", options => { options.LoginPath = "/api/Users/login"; - }) - .AddCookie() - .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions => + }); + + 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; } diff --git a/src/Web/Endpoints/UpdateMyUser.cs b/src/Web/Endpoints/UpdateMyUser.cs new file mode 100644 index 0000000..9aca4dc --- /dev/null +++ b/src/Web/Endpoints/UpdateMyUser.cs @@ -0,0 +1,39 @@ +using Hutopy.Application.Users.Commands; + +namespace Hutopy.Web.Endpoints; + +public class UpdateMyUser : EndpointGroupBase +{ + public override void Map(WebApplication app) + { + app.MapGroup(this) + .RequireAuthorization() + .MapPost(UpdateCurrentUserProfilePicture, "/profile-picture") + .MapPost(UpdateCurrentUserBannerPicture, "/banner-picture") + .MapPost(UpdateCurrentUserWebsiteIcon, "/website-icon") + .MapPatch("/profile", UpdateCurrentUser); + } + + private static async Task UpdateCurrentUser(ISender sender, UpdateCurrentUserCommand command) + { + return await sender.Send(command); + } + + private static async Task UpdateCurrentUserProfilePicture(ISender sender, Stream stream) + { + var command = new UploadProfilePictureCommand { ProfilePicture = stream }; + return await sender.Send(command); + } + + private static async Task UpdateCurrentUserBannerPicture(ISender sender, Stream stream) + { + var command = new UploadBannerPictureCommand { BannerPicture = stream }; + return await sender.Send(command); + } + + private static async Task UpdateCurrentUserWebsiteIcon(ISender sender, Stream stream) + { + var command = new UploadWebsiteIconCommand { WebsiteIcon = stream }; + return await sender.Send(command); + } +} diff --git a/src/Web/Endpoints/Users.cs b/src/Web/Endpoints/Users.cs index b2a15c5..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) + + 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 781bdc2..78700bc 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -1,9 +1,15 @@ +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); @@ -14,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); @@ -46,8 +52,37 @@ 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) => +{ + 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 AspNetCoreOperationTagsProcessor()); + configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); +}); + +builder.Services.AddFastEndpoints(); + +builder.Services.AddDbContext((_, options) => +{ + options.UseSqlServer(builder.Configuration.GetConnectionString("CommentStore")); +}); + var app = builder.Build(); app.UseForwardedHeaders( @@ -75,24 +110,27 @@ 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.UseFastEndpoints(); + app.Run(); -public abstract partial class Program { } +namespace Hutopy.Web +{ + public abstract partial class Program + { + } +} 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..053126a 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -14,6 +14,7 @@ + @@ -24,29 +25,13 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - all - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - OnBuildSuccess - - - - - - - - - - - + + + diff --git a/src/Web/Web.http b/src/Web/Web.http index 105a254..054f4e4 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.accessToken); %} + ### # 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,23 @@ 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}} +# GET GetMyUser +GET {{base_url}}/api/GetMyUser +Authorization: Bearer {{auth_token}} ### -# POST TodoLists -POST {{Web_HostAddress}}/api/TodoLists -Authorization: Bearer {{BearerToken}} -Content-Type: application/json +# GET /api/posts -// CreateTodoListCommand -{ - "Title": "Backlog" -} +GET {{base_url}}/api/messages/00000001-0000-0000-0000-000000000001 ### -# 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 /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.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.Development.json b/src/Web/appsettings.Development.json new file mode 100644 index 0000000..9e2b285 --- /dev/null +++ b/src/Web/appsettings.Development.json @@ -0,0 +1,21 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Information", + "Microsoft.AspNetCore.SpaProxy": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "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" + }, + "Authentication": { + "Jwt": { + "Audience": "hutopy", + "Issuer": "https://auth.hutopy.com", + "Key": "b2df428b9929d3ace7c598bbf4e496b2f0b71ab3cd4f94540356cfc35b000000" + } + } +} \ 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", 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/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 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/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 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; 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"] }); }