Merged PR 88: UpdateCurrentUser into Main

This commit is contained in:
Dominic Villemure
2024-06-30 19:53:02 +00:00
61 changed files with 2490 additions and 1326 deletions

View File

@@ -10,6 +10,7 @@
<PackageVersion Include="Azure.Identity" Version="1.11.0" /> <PackageVersion Include="Azure.Identity" Version="1.11.0" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.20.0" /> <PackageVersion Include="Azure.Storage.Blobs" Version="12.20.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" /> <PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="FastEndpoints" Version="5.26.0" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" /> <PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" /> <PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.8.1" /> <PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.8.1" />

View File

@@ -25,6 +25,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Directory.Packages.props = Directory.Packages.props Directory.Packages.props = Directory.Packages.props
global.json = global.json global.json = global.json
README.md = README.md README.md = README.md
start-infrastructure.sh = start-infrastructure.sh
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web", "src\Web\Web.csproj", "{4E4EE20C-F06A-4A1B-851F-C5577796941C}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web", "src\Web\Web.csproj", "{4E4EE20C-F06A-4A1B-851F-C5577796941C}"

View File

@@ -3,4 +3,6 @@
public static class CommonFileNames public static class CommonFileNames
{ {
public static string ProfilePicture = "profilePicture"; public static string ProfilePicture = "profilePicture";
public static string BannerPicture = "bannerPicture";
public static string WebsiteIcon = "websiteIcon";
} }

View File

@@ -1,21 +1,27 @@
using Google.Apis.Oauth2.v2.Data; using Google.Apis.Oauth2.v2.Data;
using Hutopy.Application.Common.Models; using Hutopy.Application.Common.Models;
using Hutopy.Application.Users.Models;
namespace Hutopy.Application.Common.Interfaces; namespace Hutopy.Application.Common.Interfaces;
public interface IIdentityService public interface IIdentityService
{ {
Task<string?> GetUserNameAsync(string userId); Task<Result<string>> CreateUserAsync(Userinfo userInfo);
Task<Result> CreateUserAsync(string email, string userName, string firstName, string lastName, string password); Task<Result<string>> CreateUserAsync(string email, string userName, string firstName, string lastName, string password);
Task<UserModel?> FindUserByIdAsync(string id);
Task<UserModel?> GetCurrentUserAsync(); Task<UserModel?> GetCurrentUserAsync();
Task<UserModel?> FindUserByEmailAsync(string id); Task<Result> UpdateCurrentUserBannerPictureUrlAsync(string url);
Task<string?> LoginAsync(string email, string password); Task<Result> UpdateCurrentUserProfilePictureUrlAsync(string url);
Task<Result> UpdateCurrentUserWebsiteIconUrlAsync(string url);
Task<Result<string>> UpdateCurrentUserAsync(UserModel userModel);
Task<IList<string>> GetCurrentUserRolesAsync();
Task<UserModel?> FindUserByIdAsync(string id);
Task<UserModel?> FindUserByEmailAsync(string email);
Task<UserModel?> GetUserByUserNameAsync(string userName); Task<UserModel?> GetUserByUserNameAsync(string userName);
Task<string?> LoginAsync(string email, string password);
Task<bool> IsInRoleAsync(string userId, string role); Task<bool> IsInRoleAsync(string userId, string role);
Task<bool> AuthorizeAsync(string userId, string policyName); Task<bool> AuthorizeAsync(string userId, string policyName);
Task<string?> GetUserNameAsync(string userId);
Task<Result> AddRoleAsync(string userId, string role); Task<Result> AddRoleAsync(string userId, string role);
Task<IList<string>> GetCurrentUserRolesAsync();
Task<(Result Result, string UserId)> CreateUserAsync(Userinfo userInfo);
Task<Result> DeleteUserAsync(string userId); Task<Result> DeleteUserAsync(string userId);
} }

View File

@@ -5,9 +5,8 @@ public class Result(
IEnumerable<string> errors) IEnumerable<string> errors)
{ {
public bool Succeeded { get; init; } = succeeded; public bool Succeeded { get; init; } = succeeded;
public string[] Errors { get; init; } = errors.ToArray(); public string[] Errors { get; init; } = errors.ToArray();
public static Result Success() public static Result Success()
{ {
return new Result(true, Array.Empty<string>()); return new Result(true, Array.Empty<string>());
@@ -18,3 +17,28 @@ public class Result(
return new Result(false, errors); return new Result(false, errors);
} }
} }
public class Result<T>(
T? value,
bool succeeded,
IEnumerable<string> 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<T> Success(T value)
{
return new Result<T>(value, true, Array.Empty<string>());
}
public static Result<T> Failure(T value, IEnumerable<string> errors)
{
return new Result<T>(value, false, errors);
}
}

View File

@@ -1,10 +1,24 @@
using Hutopy.Application.Users.Models;
namespace Hutopy.Application.Common.Models; namespace Hutopy.Application.Common.Models;
public class UserModel public class UserModel
{ {
public string? Id { get; set; } public string Id { get; set; } = string.Empty;
public string? UserName { get; set; } public string UserName { get; set; } = string.Empty;
public string? FirstName { get; set; } public string FirstName { get; set; } = string.Empty;
public string? LastName { get; set; } public string LastName { get; set; } = string.Empty;
public string? Email { get; set; } 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;
} }

View File

@@ -17,29 +17,29 @@ public class Data
public class Object public class Object
{ {
public string Id { get; set; } = String.Empty; public string Id { get; set; } = string.Empty;
public int Amount { get; set; } public int Amount { get; set; }
public BillingDetails Billing_details { get; set; } = new(); public BillingDetails Billing_details { get; set; } = new();
public string Calculated_statement_descriptor { get; set; } = String.Empty; public string Calculated_statement_descriptor { get; set; } = string.Empty;
public string Currency { get; set; } = String.Empty; public string Currency { get; set; } = string.Empty;
public bool Paid { get; set; } public bool Paid { get; set; }
public string Payment_intent { get; set; } = String.Empty; public string Payment_intent { get; set; } = string.Empty;
public string Payment_method { get; set; } = String.Empty; public string Payment_method { get; set; } = string.Empty;
public string Receipt_url { get; set; } = String.Empty; public string Receipt_url { get; set; } = string.Empty;
public string Status { get; set; } = String.Empty; public string Status { get; set; } = string.Empty;
public string Failure_message { get; set; } = String.Empty; public string Failure_message { get; set; } = string.Empty;
} }
public class BillingDetails public class BillingDetails
{ {
public string Email { get; set; } = String.Empty; public string Email { get; set; } = string.Empty;
public string Name { get; set; } = String.Empty; public string Name { get; set; } = string.Empty;
public string Phone { get; set; } = String.Empty; public string Phone { get; set; } = string.Empty;
} }
public class Request public class Request
{ {
public string Id { get; set; } = String.Empty; public string Id { get; set; } = string.Empty;
} }
public class ConfirmStripeTransactionCommandHandler( public class ConfirmStripeTransactionCommandHandler(

View File

@@ -1,27 +1,27 @@
using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Interfaces;
namespace Hutopy.Application.Users.Commands; namespace Hutopy.Application.Users.Commands;
public record LoginCommand : IRequest<string>
public record LoginCommand(
string Email,
string Password)
: IRequest<LoginResponse>;
public record LoginResponse(
string AccessToken,
string RefreshToken);
public class LoginCommandHandler(
IApplicationDbContext Context,
IIdentityService identityService)
: IRequestHandler<LoginCommand, LoginResponse>
{ {
public required string EmailAddress { get; init; } public async Task<LoginResponse> Handle(LoginCommand request, CancellationToken cancellationToken)
public required string Password { get; init; }
}
public class LoginCommandHandler : IRequestHandler<LoginCommand, string>
{
private readonly IApplicationDbContext _context;
private readonly IIdentityService _identityService;
public LoginCommandHandler(IApplicationDbContext context, IIdentityService identityService)
{ {
_context = context; var accessToken = await identityService.LoginAsync(request.Email, request.Password);
_identityService = identityService;
}
public async Task<string> Handle(LoginCommand request, CancellationToken cancellationToken) if (string.IsNullOrWhiteSpace(accessToken)) throw new InvalidOperationException("Invalid login credentials");
{
var jwt = await _identityService.LoginAsync(request.EmailAddress, request.Password);
return jwt ?? "Invalid login credentials"; return new LoginResponse(accessToken, string.Empty);
} }
} }

View File

@@ -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<string>
{
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<UpdateCurrentUserCommand, UserModel>();
}
}
}
public class UpdateCurrentUserCommandHandler(IApplicationDbContext context, IIdentityService identityService, IMapper mapper) :
IRequestHandler<UpdateCurrentUserCommand, string>
{
public async Task<string> Handle(UpdateCurrentUserCommand request, CancellationToken cancellationToken)
{
var identityUser = await identityService.GetCurrentUserAsync();
if (identityUser?.Id is null) return string.Empty;
var userModel = mapper.Map<UserModel>(request);
userModel.Id = identityUser.Id;
var result = await identityService.UpdateCurrentUserAsync(userModel);
await context.SaveChangesAsync(cancellationToken);
return result.GetValueOrDefault();
}
}

View File

@@ -0,0 +1,27 @@
using Hutopy.Application.AzureBlobStorage.Constants;
using Hutopy.Application.Common.Interfaces;
namespace Hutopy.Application.Users.Commands;
public class UploadBannerPictureCommand : IRequest<string>
{
public required Stream BannerPicture { get; init; }
}
public class UploadBannerPictureCommandHandler(IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler<UploadBannerPictureCommand, string>
{
public async Task<string> 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;
}
}

View File

@@ -19,6 +19,8 @@ public class UploadProfilePictureCommandHandler(IIdentityService identityService
var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.ProfilePicture); var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.ProfilePicture);
await identityService.UpdateCurrentUserProfilePictureUrlAsync(url);
return url; return url;
} }
} }

View File

@@ -0,0 +1,27 @@
using Hutopy.Application.AzureBlobStorage.Constants;
using Hutopy.Application.Common.Interfaces;
namespace Hutopy.Application.Users.Commands;
public class UploadWebsiteIconCommand : IRequest<string>
{
public required Stream WebsiteIcon { get; init; }
}
public class UploadWebsiteIconCommandHandler(IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler<UploadWebsiteIconCommand, string>
{
public async Task<string> 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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -8,14 +8,14 @@ public class GetCurrentUserQueryHandler(
IApplicationDbContext context, IApplicationDbContext context,
IMapper mapper, IMapper mapper,
IIdentityService identityService IIdentityService identityService
) )
: IRequestHandler<GetCurrentUserQuery, UserDto> : IRequestHandler<GetCurrentUserQuery, UserDto>
{ {
public async Task<UserDto> Handle(GetCurrentUserQuery request, CancellationToken cancellationToken) public async Task<UserDto> Handle(GetCurrentUserQuery request, CancellationToken cancellationToken)
{ {
var identityUser = await identityService.GetCurrentUserAsync(); var identityUser = await identityService.GetCurrentUserAsync();
var currentUserId = new Guid(identityUser?.Id ?? ""); var currentUserId = Guid.Parse(identityUser!.Id!);
var transactions = await context.UserTransactions var transactions = await context.UserTransactions
.Where(x => x.ApplicationUserId == currentUserId.ToString()) .Where(x => x.ApplicationUserId == currentUserId.ToString())
.OrderBy(x => x.LastModified) .OrderBy(x => x.LastModified)
@@ -28,12 +28,24 @@ public class GetCurrentUserQueryHandler(
var user = new UserDto var user = new UserDto
{ {
Id = currentUserId, Id = currentUserId,
FirstName = identityUser?.FirstName ?? "", FirstName = identityUser.FirstName ?? "",
LastName = identityUser?.LastName ?? "", LastName = identityUser.LastName ?? "",
UserName =identityUser?.UserName ?? "", 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, UserTransactions = transactions,
TotalBalance = transactions.Sum(x => x.Amount), TotalBalance = transactions.Sum(x => x.Amount),
UserRoles = roles UserRoles = roles,
}; };
return user; return user;

View File

@@ -1,3 +1,5 @@
using Hutopy.Application.Users.Models;
namespace Hutopy.Application.Users.Queries.GetCurrentUser; namespace Hutopy.Application.Users.Queries.GetCurrentUser;
public class UserDto public class UserDto
@@ -5,9 +7,20 @@ public class UserDto
public Guid Id { get; init; } public Guid Id { get; init; }
public required string FirstName { get; init; } public required string FirstName { get; init; }
public required string LastName { 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<UserTransactionDto> UserTransactions { get; init; } = []; public List<UserTransactionDto> UserTransactions { get; init; } = [];
public IList<string> UserRoles { get; init; } = []; public IList<string> UserRoles { get; init; } = [];
public required decimal TotalBalance { get; init; } public required decimal TotalBalance { get; init; }
} }

View File

@@ -1,3 +1,6 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Azure.Storage.Blobs; using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models; using Azure.Storage.Blobs.Models;
using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Interfaces;

View File

@@ -1,4 +1,7 @@
using Hutopy.Domain.Constants; using System;
using System.Linq;
using System.Threading.Tasks;
using Hutopy.Domain.Constants;
using Hutopy.Infrastructure.Identity; using Hutopy.Infrastructure.Identity;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;

View File

@@ -12,5 +12,15 @@ public class ApplicationUserConfiguration : IEntityTypeConfiguration<Application
builder builder
.OwnsOne(u => u.SocialNetworks) .OwnsOne(u => u.SocialNetworks)
.ToTable($"{nameof(ApplicationUser)}_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");
} }
} }

View File

@@ -1,4 +1,5 @@
using Hutopy.Application.Common.Interfaces; using System;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Domain.Constants; using Hutopy.Domain.Constants;
using Hutopy.Infrastructure.AzureBlob; using Hutopy.Infrastructure.AzureBlob;
using Hutopy.Infrastructure.Data; using Hutopy.Infrastructure.Data;
@@ -15,22 +16,13 @@ namespace Hutopy.Infrastructure;
public static class DependencyInjection 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. // 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. // Prod will use the connectionString stored in the vault with password in it directly.
var connectionString = configuration.GetConnectionString("DefaultConnection") ?? ""; var connectionString = configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Missing ConnectionString: 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.");
services.AddScoped<ISaveChangesInterceptor, AuditableEntityInterceptor>(); services.AddScoped<ISaveChangesInterceptor, AuditableEntityInterceptor>();
services.AddScoped<ISaveChangesInterceptor, DispatchDomainEventsInterceptor>(); services.AddScoped<ISaveChangesInterceptor, DispatchDomainEventsInterceptor>();
@@ -49,15 +41,9 @@ public static class DependencyInjection
.AddBearerToken(IdentityConstants.BearerScheme); .AddBearerToken(IdentityConstants.BearerScheme);
services.AddAuthorizationBuilder(); services.AddAuthorizationBuilder();
services.AddIdentityCore<ApplicationUser>(options => services
{ .AddIdentityCore<ApplicationUser>()
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredLength = 8;
})
.AddRoles<IdentityRole>() .AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>() .AddEntityFrameworkStores<ApplicationDbContext>()
.AddApiEndpoints() .AddApiEndpoints()

View File

@@ -7,5 +7,14 @@ public class ApplicationUser : IdentityUser
{ {
public string FirstName { get; set; } = string.Empty; public string FirstName { get; set; } = string.Empty;
public string LastName { 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 SocialNetworks SocialNetworks { get; set; } = new();
public ProfileColors ProfileColors { get; set; } = new();
public StoredDataUrls StoredDataUrls { get; set; } = new();
} }

View File

@@ -1,7 +1,12 @@
using System;
using System.Collections.Generic;
using Google.Apis.Oauth2.v2.Data; using Google.Apis.Oauth2.v2.Data;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks;
using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Interfaces;
using Hutopy.Application.Common.Models; using Hutopy.Application.Common.Models;
using Hutopy.Application.Users.Models;
using Hutopy.Infrastructure.Identity.OwnedEntities;
using Hutopy.Infrastructure.Utils; using Hutopy.Infrastructure.Utils;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@@ -44,23 +49,10 @@ public class IdentityService(
return userModel; 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<Result<string>> CreateUserAsync(Userinfo userInfo)
{ {
var user = new ApplicationUser var applicationUser = new ApplicationUser
{ {
UserName = userInfo.Name, UserName = userInfo.Name,
Email = userInfo.Email, Email = userInfo.Email,
@@ -70,12 +62,16 @@ public class IdentityService(
var password = Guid.NewGuid().ToString("N")[..32]; 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<string>(applicationUser.Id, applicationResult.Succeeded, applicationResult.Errors);
return result;
} }
public async Task<Result> CreateUserAsync(string email, string userName, string firstName, string lastName, string password) public async Task<Result<string>> CreateUserAsync(string email, string userName, string firstName, string lastName, string password)
{ {
var applicationUser = new ApplicationUser var applicationUser = new ApplicationUser
{ {
@@ -87,7 +83,54 @@ public class IdentityService(
var response = await userManager.CreateAsync(applicationUser, password); var response = await userManager.CreateAsync(applicationUser, password);
return response.ToApplicationResult(); var result = new Result<string>(applicationUser.Id, response.Succeeded, response.ToApplicationResult().Errors);
return result;
}
public async Task<Result<string>> UpdateCurrentUserAsync(UserModel userModel)
{
var applicationUser = await userManager.FindByIdAsync(userModel.Id);
if (applicationUser is null) return Result<string>.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<string>(userModel.Id, applicationResult.Succeeded,
applicationResult.Errors);
return result;
} }
public async Task<UserModel?> FindUserByIdAsync(string id) public async Task<UserModel?> FindUserByIdAsync(string id)
@@ -96,13 +139,95 @@ public class IdentityService(
if (response == null) return null; if (response == null) return null;
var userModel = new UserModel() var userModel = new UserModel
{ {
Id = response.Id, Id = response.Id,
UserName = response.UserName, UserName = response.UserName ?? string.Empty,
FirstName = response.FirstName, FirstName = response.FirstName,
LastName = response.LastName, 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<UserModel?> 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; return userModel;
@@ -119,22 +244,49 @@ public class IdentityService(
return await FindUserByIdAsync(currentUserId); return await FindUserByIdAsync(currentUserId);
} }
public async Task<UserModel?> FindUserByEmailAsync(string email) public async Task<Result> 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 applicationUser.StoredDataUrls.BannerPictureUrl = url;
{
Id = response.Id, var response = await userManager.UpdateAsync(applicationUser);
UserName = response.UserName,
FirstName = response.FirstName,
LastName = response.LastName,
Email = response.Email
};
return userModel; return response.ToApplicationResult();
}
public async Task<Result> 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<Result> 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<bool> IsInRoleAsync(string userId, string role) public async Task<bool> IsInRoleAsync(string userId, string role)
@@ -218,13 +370,22 @@ public class IdentityService(
{ {
return null; return null;
} }
var user = await GetUserByUserNameAsync(userName); var user = await GetUserByUserNameAsync(userName);
if (user is null) throw new InvalidOperationException();
var jwtSection = configuration.GetRequiredSection("Authentication:Jwt");
var token = JwtTokenHelper.GenerateJwtToken( var token = JwtTokenHelper.GenerateJwtToken(
issuer: configuration["Jwt-Issuer"] ?? "", issuer: jwtSection["Issuer"] ?? "",
audience: configuration["Jwt-Audience"] ?? "", audience: jwtSection["Audience"] ?? "",
key: configuration["Jwt-Key"] ?? "", key: jwtSection["Key"] ?? "",
userId: user?.Id ?? ""); userId: user.Id,
email: user.Email,
firstname: user.FirstName,
lastname: user.LastName,
portraitUrl: user.ProfilePictureUrl);
return token; return token;
} }

View File

@@ -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;
}

View File

@@ -2,12 +2,12 @@ namespace Hutopy.Infrastructure.Identity.OwnedEntities;
public class SocialNetworks public class SocialNetworks
{ {
public string FacebookUrl { get; init; } = String.Empty; public string FacebookUrl { get; init; } = string.Empty;
public string InstagramUrl { get; init; } = String.Empty; public string InstagramUrl { get; init; } = string.Empty;
public string XUrl { get; init; } = String.Empty; public string XUrl { get; init; } = string.Empty;
public string LinkedInUrl { get; init; } = String.Empty; public string LinkedInUrl { get; init; } = string.Empty;
public string TikTokUrl { get; init; } = String.Empty; public string TikTokUrl { get; init; } = string.Empty;
public string YoutubeUrl { get; init; } = String.Empty; public string YoutubeUrl { get; init; } = string.Empty;
public string RedditUrl { get; init; } = String.Empty; public string RedditUrl { get; init; } = string.Empty;
public string YourWebsiteUrl { get; init; } = String.Empty; public string YourWebsiteUrl { get; init; } = string.Empty;
} }

View File

@@ -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;
}

View File

@@ -0,0 +1,497 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("Created")
.HasColumnType("datetimeoffset");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("EmailAddress")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("FirstName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("datetimeoffset");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ReasonToJoin")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("SocialNetworkAccount")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("FutureCreators");
});
modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("ApplicationUserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("datetimeoffset");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsConfirmed")
.HasColumnType("bit");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("datetimeoffset");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<bool>("Paid")
.HasColumnType("bit");
b.Property<string>("StripeBillingDetailEmail")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("StripeBillingDetailName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("StripeChargeId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("StripeEventId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("StripePaymentIntent")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("StripePaymentMethod")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("StripeReceiptUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("TipMessage")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("UserTransactions");
});
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("About")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("Address")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("BirthDate")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("City")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Country")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<string>("FirstName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Occupation")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("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<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("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<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("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<string>("ApplicationUserId")
.HasColumnType("nvarchar(450)");
b1.Property<string>("FacebookUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("InstagramUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("LinkedInUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("RedditUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("TikTokUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("XUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("YourWebsiteUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("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<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", b =>
{
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,95 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Hutopy.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddMissingInformationsToUser : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "About",
table: "AspNetUsers",
type: "nvarchar(max)",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "Address",
table: "AspNetUsers",
type: "nvarchar(max)",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "BirthDate",
table: "AspNetUsers",
type: "nvarchar(max)",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "City",
table: "AspNetUsers",
type: "nvarchar(max)",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "Country",
table: "AspNetUsers",
type: "nvarchar(max)",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "Description",
table: "AspNetUsers",
type: "nvarchar(max)",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "Occupation",
table: "AspNetUsers",
type: "nvarchar(max)",
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
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");
}
}
}

View File

@@ -0,0 +1,557 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("Created")
.HasColumnType("datetimeoffset");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("EmailAddress")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("FirstName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("datetimeoffset");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ReasonToJoin")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("SocialNetworkAccount")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("FutureCreators");
});
modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("ApplicationUserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<DateTimeOffset>("Created")
.HasColumnType("datetimeoffset");
b.Property<string>("CreatedBy")
.HasColumnType("nvarchar(max)");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsConfirmed")
.HasColumnType("bit");
b.Property<DateTimeOffset>("LastModified")
.HasColumnType("datetimeoffset");
b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)");
b.Property<bool>("Paid")
.HasColumnType("bit");
b.Property<string>("StripeBillingDetailEmail")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("StripeBillingDetailName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("StripeChargeId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("StripeEventId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("StripePaymentIntent")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("StripePaymentMethod")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("StripeReceiptUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("TipMessage")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("UserTransactions");
});
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("About")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("Address")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("BirthDate")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("City")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Country")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<string>("FirstName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Occupation")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("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<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("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<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("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<string>("ApplicationUserId")
.HasColumnType("nvarchar(450)");
b1.Property<string>("Accent")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("BannerBottom")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("BannerTop")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("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<string>("ApplicationUserId")
.HasColumnType("nvarchar(450)");
b1.Property<string>("FacebookUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("InstagramUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("LinkedInUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("RedditUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("TikTokUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("XUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("YourWebsiteUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("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<string>("ApplicationUserId")
.HasColumnType("nvarchar(450)");
b1.Property<string>("BannerPictureUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("ProfilePictureUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("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<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", b =>
{
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,77 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Hutopy.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddMoreInformationsToUser : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ApplicationUser_ProfileColors",
columns: table => new
{
ApplicationUserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
BannerTop = table.Column<string>(type: "nvarchar(max)", nullable: false),
BannerBottom = table.Column<string>(type: "nvarchar(max)", nullable: false),
Accent = table.Column<string>(type: "nvarchar(max)", nullable: false),
Menu = table.Column<string>(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<string>(type: "nvarchar(450)", nullable: false),
BannerPictureUrl = table.Column<string>(type: "nvarchar(max)", nullable: false),
ProfilePictureUrl = table.Column<string>(type: "nvarchar(max)", nullable: false),
WebsiteIconUrl = table.Column<string>(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
");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ApplicationUser_ProfileColors");
migrationBuilder.DropTable(
name: "ApplicationUser_StoredDataUrls");
}
}
}

View File

@@ -149,13 +149,37 @@ namespace Hutopy.Infrastructure.Migrations
b.Property<string>("Id") b.Property<string>("Id")
.HasColumnType("nvarchar(450)"); .HasColumnType("nvarchar(450)");
b.Property<string>("About")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("AccessFailedCount") b.Property<int>("AccessFailedCount")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("Address")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("BirthDate")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("City")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ConcurrencyStamp") b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken() .IsConcurrencyToken()
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<string>("Country")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email") b.Property<string>("Email")
.HasMaxLength(256) .HasMaxLength(256)
.HasColumnType("nvarchar(256)"); .HasColumnType("nvarchar(256)");
@@ -185,6 +209,10 @@ namespace Hutopy.Infrastructure.Migrations
.HasMaxLength(256) .HasMaxLength(256)
.HasColumnType("nvarchar(256)"); .HasColumnType("nvarchar(256)");
b.Property<string>("Occupation")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("PasswordHash") b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
@@ -361,6 +389,35 @@ namespace Hutopy.Infrastructure.Migrations
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b =>
{ {
b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.ProfileColors", "ProfileColors", b1 =>
{
b1.Property<string>("ApplicationUserId")
.HasColumnType("nvarchar(450)");
b1.Property<string>("Accent")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("BannerBottom")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("BannerTop")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("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 => b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.SocialNetworks", "SocialNetworks", b1 =>
{ {
b1.Property<string>("ApplicationUserId") b1.Property<string>("ApplicationUserId")
@@ -406,8 +463,39 @@ namespace Hutopy.Infrastructure.Migrations
.HasForeignKey("ApplicationUserId"); .HasForeignKey("ApplicationUserId");
}); });
b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.StoredDataUrls", "StoredDataUrls", b1 =>
{
b1.Property<string>("ApplicationUserId")
.HasColumnType("nvarchar(450)");
b1.Property<string>("BannerPictureUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("ProfilePictureUrl")
.IsRequired()
.HasColumnType("nvarchar(max)");
b1.Property<string>("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") b.Navigation("SocialNetworks")
.IsRequired(); .IsRequired();
b.Navigation("StoredDataUrls")
.IsRequired();
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>

View File

@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Stripe; using Stripe;
using Stripe.Checkout; using Stripe.Checkout;
using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Interfaces;

View File

@@ -7,17 +7,26 @@ namespace Hutopy.Infrastructure.Utils;
public static class JwtTokenHelper 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<Claim>(new[]
{ {
new Claim(JwtRegisteredClaimNames.Sub, userId), new Claim(JwtRegisteredClaimNames.Sub, userId),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), 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)); if (portraitUrl is not null)
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); {
claims.Add(new Claim("portrait-url", portraitUrl));
}
var token = new JwtSecurityToken( var token = new JwtSecurityToken(
issuer: issuer, issuer: issuer,

View File

@@ -13,41 +13,45 @@ public class FacebookController(IIdentityService identityService) : Controller
[HttpGet("/api/facebook/sign-in")] [HttpGet("/api/facebook/sign-in")]
public async Task SignIn() public async Task SignIn()
{ {
await HttpContext.ChallengeAsync(FacebookDefaults.AuthenticationScheme, new AuthenticationProperties await HttpContext.ChallengeAsync(FacebookDefaults.AuthenticationScheme,
{ new AuthenticationProperties { RedirectUri = Url.Action("Authorize") });
RedirectUri = Url.Action("Authorize")
});
} }
public async Task<IActionResult> Authorize() public async Task<IActionResult> Authorize()
{ {
var authenticateResult = await HttpContext.AuthenticateAsync(FacebookDefaults.AuthenticationScheme); var authenticateResult = await HttpContext.AuthenticateAsync(FacebookDefaults.AuthenticationScheme);
if (!authenticateResult.Succeeded) return BadRequest(); if (!authenticateResult.Succeeded) return BadRequest();
var claims = authenticateResult.Principal.Claims.ToList(); var claims = authenticateResult.Principal.Claims.ToList();
var name = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value ?? ""; var name = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value ?? "";
var email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value ?? ""; var email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value ?? "";
var givenName = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value ?? ""; var givenName = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value ?? "";
var familyName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value ?? ""; var familyName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value ?? "";
var claimsIdentity = new ClaimsIdentity(new List<Claim> var claimsIdentity = new ClaimsIdentity(
{ new List<Claim>
new(ClaimTypes.Name, name), {
new(ClaimTypes.Email, email), new(ClaimTypes.Name, name),
new(ClaimTypes.GivenName, givenName), new(ClaimTypes.Email, email),
new(ClaimTypes.Surname, familyName) new(ClaimTypes.GivenName, givenName),
}, CookieAuthenticationDefaults.AuthenticationScheme); new(ClaimTypes.Surname, familyName)
},
CookieAuthenticationDefaults.AuthenticationScheme);
if (await identityService.FindUserByEmailAsync(email) != null) if (await identityService.FindUserByEmailAsync(email) != null)
{ {
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity));
return Redirect("/"); return Redirect("/");
} }
await identityService.CreateUserAsync(email, givenName, givenName, familyName, RandomGenerator.RandomString(24)); await identityService.CreateUserAsync(email, givenName, givenName, familyName,
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); RandomGenerator.RandomString(24));
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity));
return Redirect("/"); return Redirect("/");
} }
} }

View File

@@ -13,7 +13,8 @@ public class GoogleController(IIdentityService identityService, IHttpClientFacto
[HttpPost("/api/google/sign-in")] [HttpPost("/api/google/sign-in")]
public async Task<IActionResult> SignIn([FromBody] GoogleSignInRequest request) public async Task<IActionResult> SignIn([FromBody] GoogleSignInRequest request)
{ {
var httpClient = httpClientFactory.CreateClient(); using var httpClient = httpClientFactory.CreateClient();
// Verify the token with Google // Verify the token with Google
var response = await httpClient.GetAsync($"https://www.googleapis.com/oauth2/v1/userinfo?access_token={request.AccessToken}"); var response = await httpClient.GetAsync($"https://www.googleapis.com/oauth2/v1/userinfo?access_token={request.AccessToken}");
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
@@ -21,12 +22,11 @@ public class GoogleController(IIdentityService identityService, IHttpClientFacto
return BadRequest("Invalid Google token."); return BadRequest("Invalid Google token.");
} }
var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); var userInfo = JObject.Parse(await response.Content.ReadAsStringAsync());
var email = userInfo["email"]?.ToString() ?? "";
var email = payload["email"]?.ToString() ?? ""; var name = userInfo["name"]?.ToString() ?? "";
var name = payload["name"]?.ToString() ?? ""; var givenName = userInfo["given_name"]?.ToString() ?? "";
var givenName = payload["given_name"]?.ToString() ?? ""; var familyName = userInfo["family_name"]?.ToString() ?? "";
var familyName = payload["family_name"]?.ToString() ?? "";
if (string.IsNullOrEmpty(email)) if (string.IsNullOrEmpty(email))
{ {
@@ -47,27 +47,33 @@ public class GoogleController(IIdentityService identityService, IHttpClientFacto
} }
// Sign in the user // Sign in the user
var claims = new List<Claim> var claimsIdentity = new ClaimsIdentity(
{ new List<Claim>
new(ClaimTypes.Name, name), {
new(ClaimTypes.Email, email), new(ClaimTypes.Name, name),
new(ClaimTypes.GivenName, givenName), new(ClaimTypes.Email, email),
new(ClaimTypes.Surname, familyName) new(ClaimTypes.GivenName, givenName),
}; new(ClaimTypes.Surname, familyName)
},
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); 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);
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 public class GoogleSignInRequest

View File

@@ -30,7 +30,7 @@ public static class DependencyInjection
services.AddExceptionHandler<CustomExceptionHandler>(); services.AddExceptionHandler<CustomExceptionHandler>();
services.AddRazorPages(); services.AddRazorPages();
services.AddHttpClient(); services.AddHttpClient();
// Customise default API behaviour // Customise default API behaviour
@@ -39,26 +39,11 @@ public static class DependencyInjection
services.AddEndpointsApiExplorer(); services.AddEndpointsApiExplorer();
services.AddOpenApiDocument((configure, sp) =>
{
configure.Title = "Hutopy API";
// Add JWT
configure.AddSecurity("JWT", Enumerable.Empty<string>(), 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; return services;
} }
public static IServiceCollection AddKeyVaultIfConfigured(this IServiceCollection services, ConfigurationManager configuration) public static IServiceCollection AddKeyVaultIfConfigured(this IServiceCollection services,
ConfigurationManager configuration)
{ {
var keyVaultUri = configuration["KeyVaultUri"]; var keyVaultUri = configuration["KeyVaultUri"];
if (!string.IsNullOrWhiteSpace(keyVaultUri)) if (!string.IsNullOrWhiteSpace(keyVaultUri))
@@ -70,10 +55,12 @@ public static class DependencyInjection
return services; 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.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
@@ -81,36 +68,50 @@ public static class DependencyInjection
.AddCookie("Identity.Application", options => .AddCookie("Identity.Application", options =>
{ {
options.LoginPath = "/api/Users/login"; 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.Authority = "https://hutopy.com";
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
{ {
ValidateIssuer = true, ValidateIssuer = true,
ValidIssuer = configuration["Jwt-Issuer"], ValidIssuer = authJwt["Issuer"],
ValidateAudience = true, ValidateAudience = true,
ValidAudience = configuration["Jwt-Audience"], ValidAudience = authJwt["Audience"],
ValidateLifetime = true, 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."))) 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"] ?? options.ClientId = authGoogle["ClientId"] ??
throw new ArgumentNullException("The Google ClientId is missing.");; throw new ArgumentNullException("The Google ClientId is missing.");
options.ClientSecret = configuration["Google-ClientSecret"] ?? options.ClientSecret = authGoogle["ClientSecret"] ??
throw new ArgumentNullException("The Google ClientSecret is missing.");; throw new ArgumentNullException("The Google ClientSecret is missing.");
}) });
.AddFacebook(FacebookDefaults.AuthenticationScheme, options => }
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."); 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."); throw new ArgumentNullException("The Facebook ClientSecret is missing.");
}); });
}
return services; return services;
} }

View File

@@ -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<string> UpdateCurrentUser(ISender sender, UpdateCurrentUserCommand command)
{
return await sender.Send(command);
}
private static async Task<string> UpdateCurrentUserProfilePicture(ISender sender, Stream stream)
{
var command = new UploadProfilePictureCommand { ProfilePicture = stream };
return await sender.Send(command);
}
private static async Task<string> UpdateCurrentUserBannerPicture(ISender sender, Stream stream)
{
var command = new UploadBannerPictureCommand { BannerPicture = stream };
return await sender.Send(command);
}
private static async Task<string> UpdateCurrentUserWebsiteIcon(ISender sender, Stream stream)
{
var command = new UploadWebsiteIconCommand { WebsiteIcon = stream };
return await sender.Send(command);
}
}

View File

@@ -23,18 +23,16 @@ public class Users : EndpointGroupBase
{ {
return await sender.Send(query); return await sender.Send(query);
} }
private static async Task<string> Login(ISender sender, LoginCommand command) private static async Task<LoginResponse> Login(ISender sender, LoginCommand command)
{ {
return await sender.Send(command); return await sender.Send(command);
} }
private static async Task<string> UploadProfilePicture(ISender sender, Stream stream) private static async Task<string> UploadProfilePicture(ISender sender, Stream stream)
{ {
var command = new UploadProfilePictureCommand var command = new UploadProfilePictureCommand { ProfilePicture = stream };
{
ProfilePicture = stream
};
return await sender.Send(command); return await sender.Send(command);
} }
} }

View File

@@ -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!;
}

View File

@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore;
namespace Hutopy.Web.Messages.Data;
public class MessagingDbContext(
DbContextOptions<MessagingDbContext> options)
: DbContext(options)
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Message>()
.Property(c => c.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
}
public DbSet<Message> Messages { get; set; }
}

View File

@@ -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<List<Message>>
{
public override void Configure()
{
Tags("Messages");
Get("/api/messages/{ContentId:guid}");
AllowAnonymous();
}
public override async Task HandleAsync(
CancellationToken ct)
{
var contentId = Route<Guid>("ContentId");
var comments = await context
.Messages
.Where(c => c.ContentId == contentId)
.ToListAsync(cancellationToken: ct);
await SendAsync(comments, cancellation: ct);
}
}

View File

@@ -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<List<Message>>
{
public override void Configure()
{
Tags("Messages");
Get("/api/messages/by-user/{UserId:guid}");
}
public override async Task HandleAsync(
CancellationToken ct)
{
var userId = Route<Guid>("UserId");
var posts = await context
.Messages
.Where(c => c.CreatedBy == userId)
.ToListAsync(cancellationToken: ct);
await SendAsync(posts, cancellation: ct);
}
}

View File

@@ -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<PostMessageRequest>
{
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);
}
}

View File

@@ -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<PostReplyMessageRequest>
{
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);
}
}

View File

@@ -0,0 +1,59 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ContentId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<Guid>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ParentId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Messages");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Hutopy.Web.Messages.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Messages",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ContentId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
ParentId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Value = table.Column<string>(type: "nvarchar(max)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Messages", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Messages");
}
}
}

View File

@@ -0,0 +1,56 @@
// <auto-generated />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ContentId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<Guid>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ParentId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Messages");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -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<Guid>(ClaimTypes.NameIdentifier);
}
public static string GetFirstName(this ClaimsPrincipal claims)
{
return (string)claims.GetFirstValue<string>(ClaimTypes.GivenName);
}
public static string GetLastName(this ClaimsPrincipal claims)
{
return (string)claims.GetFirstValue<string>(ClaimTypes.Surname);
}
public static string GetEmail(this ClaimsPrincipal claims)
{
return (string)claims.GetFirstValue<string>(ClaimTypes.Email);
}
public static object GetFirstValue<TValue>(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));
}
}

View File

@@ -1,9 +1,15 @@
using Azure.Identity;
using FastEndpoints;
using Hutopy.Application; using Hutopy.Application;
using Hutopy.Infrastructure; using Hutopy.Infrastructure;
using Hutopy.Infrastructure.Data; using Hutopy.Infrastructure.Data;
using Hutopy.Web; using Hutopy.Web;
using Azure.Identity; using Hutopy.Web.Messages.Data;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.EntityFrameworkCore;
using NSwag;
using NSwag.Generation.AspNetCore.Processors;
using NSwag.Generation.Processors.Security;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -14,31 +20,31 @@ if (!builder.Environment.IsDevelopment())
} }
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", builder =>
{ {
options.AddPolicy("AllowAll", builder => builder.AllowAnyOrigin()
{ .AllowAnyMethod()
builder.AllowAnyOrigin() .AllowAnyHeader();
.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();
});
}); });
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. // Add services to the container.
builder.Services.AddKeyVaultIfConfigured(builder.Configuration); builder.Services.AddKeyVaultIfConfigured(builder.Configuration);
@@ -46,8 +52,37 @@ builder.Services.AddApplicationServices();
builder.Services.AddInfrastructureServices(builder.Configuration); builder.Services.AddInfrastructureServices(builder.Configuration);
builder.Services.AddWebServices(); builder.Services.AddWebServices();
builder.Services.AddAuthorizationAndAuthentication(builder.Configuration); 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.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<MessagingDbContext>((_, options) =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("CommentStore"));
});
var app = builder.Build(); var app = builder.Build();
app.UseForwardedHeaders( app.UseForwardedHeaders(
@@ -75,24 +110,27 @@ app.UseHealthChecks("/health");
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseStaticFiles(); app.UseStaticFiles();
app.UseSwaggerUi(settings => if (app.Environment.IsDevelopment())
{ {
settings.Path = "/api"; app.UseOpenApi();
settings.DocumentPath = "/api/specification.json"; app.UseSwaggerUi(options => options.Path = "/api");
}); }
app.MapControllerRoute( app.MapControllerRoute(
name: "default", name: "default",
pattern: "{controller}/{action=Index}/{id?}"); pattern: "{controller}/{action=Index}/{id?}");
app.MapFallbackToFile("index.html"); //TODO: validate the behavior
// app.UseExceptionHandler();
app.UseExceptionHandler(options => { });
app.Map("/", () => Results.Redirect("/api"));
app.MapEndpoints(); app.MapEndpoints();
app.UseFastEndpoints();
app.Run(); app.Run();
public abstract partial class Program { } namespace Hutopy.Web
{
public abstract partial class Program
{
}
}

View File

@@ -8,9 +8,9 @@
} }
}, },
"profiles": { "profiles": {
"Hutopy.Web": { "Hutopy.Web - DEV": {
"commandName": "Project", "commandName": "Project",
"launchBrowser": true, "launchBrowser": false,
"applicationUrl": "https://localhost:5001;http://localhost:5000", "applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
@@ -18,7 +18,7 @@
}, },
"IIS Express": { "IIS Express": {
"commandName": "IISExpress", "commandName": "IISExpress",
"launchBrowser": true, "launchBrowser": false,
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@@ -14,6 +14,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" /> <PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
<PackageReference Include="Azure.Identity" /> <PackageReference Include="Azure.Identity" />
<PackageReference Include="FastEndpoints" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Google" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
@@ -24,29 +25,13 @@
<PackageReference Include="NSwag.AspNetCore" /> <PackageReference Include="NSwag.AspNetCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NSwag.MSBuild">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="FluentValidation.AspNetCore" /> <PackageReference Include="FluentValidation.AspNetCore" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<!-- Auto-generated Open API specification and Angular TypeScript clients --> <Folder Include="Messages\Migrations\" />
<PropertyGroup> </ItemGroup>
<RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent>
</PropertyGroup>
<Target Name="NSwag" AfterTargets="PostBuildEvent" Condition=" '$(Configuration)' == 'Debug' And '$(SkipNSwag)' != 'True' ">
<Exec ConsoleToMSBuild="true" ContinueOnError="true" WorkingDirectory="$(ProjectDir)" EnvironmentVariables="ASPNETCORE_ENVIRONMENT=Development" Command="$(NSwagExe_Net80) run config.nswag /variables:Configuration=$(Configuration)">
<Output TaskParameter="ExitCode" PropertyName="NSwagExitCode" />
<Output TaskParameter="ConsoleOutput" PropertyName="NSwagOutput" />
</Exec>
<Message Text="$(NSwagOutput)" Condition="'$(NSwagExitCode)' == '0'" Importance="low" />
<Error Text="$(NSwagOutput)" Condition="'$(NSwagExitCode)' != '0'" />
</Target>
</Project> </Project>

View File

@@ -1,12 +1,10 @@
# For more info on HTTP files go to https://aka.ms/vs/httpfile # For more info on HTTP files go to https://aka.ms/vs/httpfile
@Web_HostAddress = https://localhost:5001
@Email=administrator@localhost @Email=administrator@localhost
@Password=Administrator1! @Password=Administrator1!
@BearerToken=<YourToken> @auth_token=<Your Token>
# POST Users Register # POST Users Register
POST {{Web_HostAddress}}/api/Users/Register POST {{base_url}}/api/Users/Register
Content-Type: application/json Content-Type: application/json
{ {
@@ -17,7 +15,7 @@ Content-Type: application/json
### ###
# POST Users Login # POST Users Login
POST {{Web_HostAddress}}/api/Users/Login POST {{base_url}}/api/Users/login
Content-Type: application/json Content-Type: application/json
{ {
@@ -25,11 +23,13 @@ Content-Type: application/json
"password": "{{Password}}" "password": "{{Password}}"
} }
> {% client.global.set("auth_token", response.body.accessToken); %}
### ###
# POST Users Refresh # POST Users Refresh
POST {{Web_HostAddress}}/api/Users/Refresh POST {{base_url}}/api/Users/Refresh
Authorization: Bearer {{BearerToken}} Authorization: Bearer {{auth_token}}
Content-Type: application/json Content-Type: application/json
{ {
@@ -39,101 +39,23 @@ Content-Type: application/json
### ###
# GET WeatherForecast # GET WeatherForecast
GET {{Web_HostAddress}}/api/WeatherForecasts GET {{base_url}}/api/WeatherForecasts
Authorization: Bearer {{BearerToken}} Authorization: Bearer {{auth_token}}
### ###
# GET TodoLists # GET GetMyUser
GET {{Web_HostAddress}}/api/TodoLists GET {{base_url}}/api/GetMyUser
Authorization: Bearer {{BearerToken}} Authorization: Bearer {{auth_token}}
### ###
# POST TodoLists # GET /api/posts
POST {{Web_HostAddress}}/api/TodoLists
Authorization: Bearer {{BearerToken}}
Content-Type: application/json
// CreateTodoListCommand GET {{base_url}}/api/messages/00000001-0000-0000-0000-000000000001
{
"Title": "Backlog"
}
### ###
# PUT TodoLists # GET /api/posts/by-user
PUT {{Web_HostAddress}}/api/TodoLists/1 GET {{base_url}}/api/messages/by-user/325C69E8-DBC4-4CEE-B56E-C3C90AEE7963
Authorization: Bearer {{BearerToken}} Authorization: Bearer {{auth_token}}
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}}
###

View File

@@ -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": "",
}

View File

@@ -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"
}
}
}

View File

@@ -1,7 +1,4 @@
{ {
"ConnectionStrings": {
"DefaultConnection": "Server={DB_HOST},1433;Database=Hutopy;User Id=sa;Password={DB_PASSWORD};MultipleActiveResultSets=true;TrustServerCertificate=True"
},
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,5 @@
{
"dev": {
"base_url": "https://localhost:5001"
}
}

View File

@@ -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": []
}
]
}

9
start-infrastructure.sh Normal file
View File

@@ -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

View File

@@ -1,6 +1,7 @@
using System.Data.Common; using System.Data.Common;
using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Interfaces;
using Hutopy.Infrastructure.Data; using Hutopy.Infrastructure.Data;
using Hutopy.Web;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.TestHost;

View File

@@ -35,11 +35,11 @@ public class TestcontainersTestDatabase : ITestDatabase
var context = new ApplicationDbContext(options); var context = new ApplicationDbContext(options);
context.Database.Migrate(); await context.Database.MigrateAsync();
_respawner = await Respawner.CreateAsync(_connectionString, new RespawnerOptions _respawner = await Respawner.CreateAsync(_connectionString, new RespawnerOptions
{ {
TablesToIgnore = new Respawn.Graph.Table[] { "__EFMigrationsHistory" } TablesToIgnore = ["__EFMigrationsHistory"]
}); });
} }