diff --git a/src/Application/Common/Models/UserModel.cs b/src/Application/Common/Models/UserModel.cs index b38e4b4..2a28ec7 100644 --- a/src/Application/Common/Models/UserModel.cs +++ b/src/Application/Common/Models/UserModel.cs @@ -3,14 +3,13 @@ namespace Hutopy.Application.Common.Models; public class UserModel { public Guid Id { get; set; } - public string UserName { get; init; } = null!; + public string Username { get; init; } = null!; public string? Alias { get; init; } public string? PortraitUrl { get; init; } - public string? FirstName { get; init; } - public string? LastName { get; init; } - public string? Occupation { get; init; } + public string? Firstname { get; init; } + public string? Lastname { get; init; } public string? Email { get; init; } public string? PhoneNumber { get; init; } - public string? BirthDate { get; init; } + public DateTime? BirthDate { get; init; } public string? Address { get; init; } } diff --git a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs deleted file mode 100644 index 18e3f8e..0000000 --- a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUser.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Hutopy.Application.Common.Interfaces; - -namespace Hutopy.Application.Users.Queries.GetCurrentUser; - -public record GetCurrentUserQuery : IRequest; - -public class GetCurrentUserQueryHandler( - IApplicationDbContext context, - IMapper mapper, - IIdentityService identityService -) - : IRequestHandler -{ - public async Task Handle(GetCurrentUserQuery request, CancellationToken cancellationToken) - { - var userModel = await identityService.GetCurrentUserAsync(); - - if (userModel is null) return null; - - var transactions = await context - .UserTransactions - .Where(x => x.ApplicationUserId == userModel.Id) - .OrderBy(x => x.LastModifiedAt) - .ProjectTo(mapper.ConfigurationProvider) - .Where(x => x.IsConfirmed == true) - .ToListAsync(cancellationToken); - - var roles = await identityService.GetCurrentUserRolesAsync(); - - var user = new UserDto - { - Id = userModel.Id, - Alias = userModel.Alias, - PortraitUrl = userModel.PortraitUrl, - FirstName = userModel.FirstName, - LastName = userModel.LastName, - UserName = userModel.UserName, - Occupation = userModel.Occupation, - PhoneNumber = userModel.PhoneNumber, - Email = userModel.Email, - BirthDate = userModel.BirthDate, - Address = userModel.Address, - UserTransactions = transactions, - TotalBalance = transactions.Sum(x => x.Amount), - UserRoles = roles, - }; - - return user; - } -} diff --git a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUserProfilePicture.cs b/src/Application/Users/Queries/GetCurrentUser/GetCurrentUserProfilePicture.cs deleted file mode 100644 index 26865b7..0000000 --- a/src/Application/Users/Queries/GetCurrentUser/GetCurrentUserProfilePicture.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Hutopy.Application.AzureBlobStorage.Constants; -using Hutopy.Application.Common.Interfaces; - -namespace Hutopy.Application.Users.Queries.GetCurrentUser; - -public record GetCurrentUserProfilePictureQuery : IRequest; - -public class GetCurrentUserProfilePictureQueryHandler( - IIdentityService identityService, - IBlobStorage blobStorage -) - : IRequestHandler -{ - public async Task Handle(GetCurrentUserProfilePictureQuery request, CancellationToken cancellationToken) - { - var identityUser = await identityService.GetCurrentUserAsync(); - - return await blobStorage.DownloadFileAsync( - ContainerNames.Users, - $"{identityUser.Id.ToString()}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}", - cancellationToken); - } -} diff --git a/src/Application/Users/Queries/GetCurrentUser/UserDto.cs b/src/Application/Users/Queries/GetCurrentUser/UserDto.cs deleted file mode 100644 index 689c411..0000000 --- a/src/Application/Users/Queries/GetCurrentUser/UserDto.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Hutopy.Application.Users.Queries.GetCurrentUser; - -public class UserDto -{ - public Guid Id { get; init; } - public IList UserRoles { get; init; } = []; - public string UserName { get; init; } = null!; - public string? Alias { get; init; } - public string? PortraitUrl { get; init; } - public string? FirstName { get; init; } - public string? LastName { get; init; } - public string? Occupation { get; init; } - public string? Email { get; init; } - public string? PhoneNumber { get; init; } - public string? BirthDate { get; init; } - public string? Address { get; init; } - public List UserTransactions { get; init; } = []; - public required decimal TotalBalance { get; init; } -} diff --git a/src/Application/Users/Queries/GetUser/UserDto.cs b/src/Application/Users/Queries/GetUser/UserDto.cs index 86ce3d4..c64bb21 100644 --- a/src/Application/Users/Queries/GetUser/UserDto.cs +++ b/src/Application/Users/Queries/GetUser/UserDto.cs @@ -17,9 +17,8 @@ public static class UserDtoExtensions new() { Id = model.Id, - FirstName = model.FirstName, - LastName = model.LastName, - UserName = model.UserName, - Occupation = model.Occupation + FirstName = model.Firstname, + LastName = model.Lastname, + UserName = model.Username }; } diff --git a/src/Infrastructure/Identity/ApplicationUser.cs b/src/Infrastructure/Identity/ApplicationUser.cs index 7d52e10..8a1ed8e 100644 --- a/src/Infrastructure/Identity/ApplicationUser.cs +++ b/src/Infrastructure/Identity/ApplicationUser.cs @@ -6,10 +6,9 @@ namespace Hutopy.Infrastructure.Identity; public class ApplicationUser : IdentityUser { [MaxLength(255)] public string? Alias { get; set; } - [MaxLength(255)] public string? FirstName { get; set; } - [MaxLength(255)] public string? LastName { get; set; } - [MaxLength(255)] public string? Occupation { get; set; } - [MaxLength(255)] public string? BirthDate { get; set; } + [MaxLength(255)] public string? Firstname { get; set; } + [MaxLength(255)] public string? Lastname { get; set; } + public DateTime? BirthDate { get; set; } [MaxLength(255)] public string? Address { get; set; } [MaxLength(255)] public string? PortraitUrl { get; set; } } diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index f853d1f..6cf9a0f 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -36,13 +36,12 @@ public class IdentityService( return new() { Id = user.Id, - UserName = user.UserName!, + Username = user.UserName!, PhoneNumber = user.PhoneNumber, Email = user.Email, Alias = user.Alias, - FirstName = user.FirstName, - LastName = user.LastName, - Occupation = user.Occupation, + Firstname = user.Firstname, + Lastname = user.Lastname, BirthDate = user.BirthDate, Address = user.Address, PortraitUrl = user.PortraitUrl @@ -55,8 +54,8 @@ public class IdentityService( { UserName = userInfo.Name, Email = userInfo.Email, - FirstName = userInfo.GivenName, - LastName = userInfo.FamilyName + Firstname = userInfo.GivenName, + Lastname = userInfo.FamilyName }; var password = Guid.NewGuid().ToString("N")[..32]; @@ -75,7 +74,7 @@ public class IdentityService( { var applicationUser = new ApplicationUser { - UserName = userName, Email = email, FirstName = firstName, LastName = lastName + UserName = userName, Email = email, Firstname = firstName, Lastname = lastName }; var response = await userManager.CreateAsync(applicationUser, password); @@ -95,9 +94,8 @@ public class IdentityService( applicationUser.Email = userModel.Email; applicationUser.PhoneNumber = userModel.PhoneNumber; applicationUser.Alias = userModel.Alias; - applicationUser.FirstName = userModel.FirstName; - applicationUser.LastName = userModel.LastName; - applicationUser.Occupation = userModel.Occupation; + applicationUser.Firstname = userModel.Firstname; + applicationUser.Lastname = userModel.Lastname; applicationUser.BirthDate = userModel.BirthDate; applicationUser.Address = userModel.Address; applicationUser.PortraitUrl = userModel.PortraitUrl; @@ -120,14 +118,13 @@ public class IdentityService( var userModel = new UserModel { Id = response.Id, - UserName = response.UserName ?? string.Empty, + Username = response.UserName ?? string.Empty, PhoneNumber = response.PhoneNumber ?? string.Empty, Email = response.Email ?? string.Empty, PortraitUrl = response.PortraitUrl, Alias = response.Alias, - FirstName = response.FirstName, - LastName = response.LastName, - Occupation = response.Occupation, + Firstname = response.Firstname, + Lastname = response.Lastname, BirthDate = response.BirthDate, Address = response.Address, }; @@ -275,8 +272,8 @@ public class IdentityService( userId: user.Id.ToString(), email: user.Email, alias: user.Alias, - firstname: user.FirstName, - lastname: user.LastName, + firstname: user.Firstname, + lastname: user.Lastname, portraitUrl: user.PortraitUrl); return token; diff --git a/src/Infrastructure/Migrations/20240903230821_ChangeBirthDateToDateTime.Designer.cs b/src/Infrastructure/Migrations/20240903230821_ChangeBirthDateToDateTime.Designer.cs new file mode 100644 index 0000000..fbdfc47 --- /dev/null +++ b/src/Infrastructure/Migrations/20240903230821_ChangeBirthDateToDateTime.Designer.cs @@ -0,0 +1,436 @@ +// +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("20240903230821_ChangeBirthDateToDateTime")] + partial class ChangeBirthDateToDateTime + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Domain.Entities.FutureCreator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReasonToJoin") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SocialNetworkAccount") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("FutureCreators"); + }); + + modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsConfirmed") + .HasColumnType("bit"); + + b.Property("LastModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Paid") + .HasColumnType("bit"); + + b.Property("StripeBillingDetailEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeBillingDetailName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeChargeId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeEventId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentIntent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeReceiptUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TipMessage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("UserTransactions"); + }); + + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Alias") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("BirthDate") + .HasMaxLength(255) + .HasColumnType("datetime2"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("Firstname") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Lastname") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Occupation") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("PortraitUrl") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Migrations/20240903230821_ChangeBirthDateToDateTime.cs b/src/Infrastructure/Migrations/20240903230821_ChangeBirthDateToDateTime.cs new file mode 100644 index 0000000..6e94cdb --- /dev/null +++ b/src/Infrastructure/Migrations/20240903230821_ChangeBirthDateToDateTime.cs @@ -0,0 +1,83 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + /// + public partial class ChangeBirthDateToDateTime : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "City", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "Country", + table: "AspNetUsers"); + + migrationBuilder.RenameColumn( + name: "LastName", + table: "AspNetUsers", + newName: "Lastname"); + + migrationBuilder.RenameColumn( + name: "FirstName", + table: "AspNetUsers", + newName: "Firstname"); + + migrationBuilder.AlterColumn( + name: "BirthDate", + table: "AspNetUsers", + type: "datetime2", + maxLength: 255, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(255)", + oldMaxLength: 255, + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Lastname", + table: "AspNetUsers", + newName: "LastName"); + + migrationBuilder.RenameColumn( + name: "Firstname", + table: "AspNetUsers", + newName: "FirstName"); + + migrationBuilder.AlterColumn( + name: "BirthDate", + table: "AspNetUsers", + type: "nvarchar(255)", + maxLength: 255, + nullable: true, + oldClrType: typeof(DateTime), + oldType: "datetime2", + oldMaxLength: 255, + oldNullable: true); + + migrationBuilder.AddColumn( + name: "City", + table: "AspNetUsers", + type: "nvarchar(255)", + maxLength: 255, + nullable: true); + + migrationBuilder.AddColumn( + name: "Country", + table: "AspNetUsers", + type: "nvarchar(255)", + maxLength: 255, + nullable: true); + } + } +} diff --git a/src/Infrastructure/Migrations/20240903231429_RemoveOccupation.Designer.cs b/src/Infrastructure/Migrations/20240903231429_RemoveOccupation.Designer.cs new file mode 100644 index 0000000..dd3ab98 --- /dev/null +++ b/src/Infrastructure/Migrations/20240903231429_RemoveOccupation.Designer.cs @@ -0,0 +1,432 @@ +// +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("20240903231429_RemoveOccupation")] + partial class RemoveOccupation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Domain.Entities.FutureCreator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReasonToJoin") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SocialNetworkAccount") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("FutureCreators"); + }); + + modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsConfirmed") + .HasColumnType("bit"); + + b.Property("LastModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Paid") + .HasColumnType("bit"); + + b.Property("StripeBillingDetailEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeBillingDetailName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeChargeId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeEventId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentIntent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeReceiptUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TipMessage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("UserTransactions"); + }); + + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Alias") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("BirthDate") + .HasMaxLength(255) + .HasColumnType("datetime2"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("Firstname") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Lastname") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("PortraitUrl") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Migrations/20240903231429_RemoveOccupation.cs b/src/Infrastructure/Migrations/20240903231429_RemoveOccupation.cs new file mode 100644 index 0000000..c1cb1b0 --- /dev/null +++ b/src/Infrastructure/Migrations/20240903231429_RemoveOccupation.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + /// + public partial class RemoveOccupation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Occupation", + table: "AspNetUsers"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Occupation", + table: "AspNetUsers", + type: "nvarchar(255)", + maxLength: 255, + nullable: true); + } + } +} diff --git a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 7e0e9ed..b3d38bc 100644 --- a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -188,22 +188,14 @@ namespace Hutopy.Infrastructure.Migrations .HasMaxLength(255) .HasColumnType("nvarchar(255)"); - b.Property("BirthDate") + b.Property("BirthDate") .HasMaxLength(255) - .HasColumnType("nvarchar(255)"); - - b.Property("City") - .HasMaxLength(255) - .HasColumnType("nvarchar(255)"); + .HasColumnType("datetime2"); b.Property("ConcurrencyStamp") .IsConcurrencyToken() .HasColumnType("nvarchar(max)"); - b.Property("Country") - .HasMaxLength(255) - .HasColumnType("nvarchar(255)"); - b.Property("Email") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); @@ -211,11 +203,11 @@ namespace Hutopy.Infrastructure.Migrations b.Property("EmailConfirmed") .HasColumnType("bit"); - b.Property("FirstName") + b.Property("Firstname") .HasMaxLength(255) .HasColumnType("nvarchar(255)"); - b.Property("LastName") + b.Property("Lastname") .HasMaxLength(255) .HasColumnType("nvarchar(255)"); @@ -233,10 +225,6 @@ namespace Hutopy.Infrastructure.Migrations .HasMaxLength(256) .HasColumnType("nvarchar(256)"); - b.Property("Occupation") - .HasMaxLength(255) - .HasColumnType("nvarchar(255)"); - b.Property("PasswordHash") .HasColumnType("nvarchar(max)"); diff --git a/src/Web/Controllers/GoogleController.cs b/src/Web/Controllers/GoogleController.cs index 34609ab..fd48001 100644 --- a/src/Web/Controllers/GoogleController.cs +++ b/src/Web/Controllers/GoogleController.cs @@ -75,8 +75,8 @@ public class GoogleController( user.Id.ToString(), user.Email, user.Alias, - user.FirstName, - user.LastName, + user.Firstname, + user.Lastname, user.PortraitUrl); return Ok(new { accessToken = token, email }); diff --git a/src/Web/Endpoints/GetMyUser.cs b/src/Web/Endpoints/GetMyUser.cs deleted file mode 100644 index e891a2e..0000000 --- a/src/Web/Endpoints/GetMyUser.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Hutopy.Application.Users.Queries.GetCurrentUser; -using Hutopy.Web.Infrastructure; - -namespace Hutopy.Web.Endpoints; - -public class GetMyUser : EndpointGroupBase -{ - public override void Map(WebApplication app) - { - app.MapGroup(this) - .RequireAuthorization() - .MapGet(GetCurrentUser) - .MapGet(GetCurrentUserProfilePicture, "profile-picture"); - } - - private static async Task GetCurrentUser(ISender sender, [AsParameters] GetCurrentUserQuery query) - { - return await sender.Send(query); - } - - private static async Task GetCurrentUserProfilePicture(ISender sender, [AsParameters] GetCurrentUserProfilePictureQuery query) - { - return await sender.Send(query); - } -} diff --git a/src/Web/Features/Contents/Handlers/GetCreatorProfile.cs b/src/Web/Features/Contents/Handlers/GetCreatorProfile.cs new file mode 100644 index 0000000..67dd5cb --- /dev/null +++ b/src/Web/Features/Contents/Handlers/GetCreatorProfile.cs @@ -0,0 +1,30 @@ +using Hutopy.Web.Common; +using Hutopy.Web.Features.Contents.Data; + +namespace Hutopy.Web.Features.Contents.Handlers; + +[PublicAPI] +public class GetCreatorProfileHandler( + ContentDbContext context) + : EndpointWithoutRequest +{ + public override void Configure() + { + Get("/api/creators/profile"); + Options((o => o.WithTags("Creators"))); + AllowAnonymous(); + } + + public override async Task HandleAsync( + CancellationToken ct) + { + var creator = await context + .Creators + .FindAsync( + [HttpContext.User.GetUserId()], + cancellationToken: ct); + + if (creator is null) await SendNotFoundAsync(ct); + else await SendAsync(creator, cancellation: ct); + } +} diff --git a/src/Web/Features/Contents/Handlers/Models/ContentModel.cs b/src/Web/Features/Contents/Handlers/Models/ContentModel.cs index c110b0b..d317529 100644 --- a/src/Web/Features/Contents/Handlers/Models/ContentModel.cs +++ b/src/Web/Features/Contents/Handlers/Models/ContentModel.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations.Schema; - -namespace Hutopy.Web.Features.Contents.Handlers.Models; +namespace Hutopy.Web.Features.Contents.Handlers.Models; [PublicAPI] public class ContentModel diff --git a/src/Web/Features/Users/Handlers/ChangeAddress.cs b/src/Web/Features/Users/Handlers/ChangeAddress.cs new file mode 100644 index 0000000..b139f44 --- /dev/null +++ b/src/Web/Features/Users/Handlers/ChangeAddress.cs @@ -0,0 +1,43 @@ +using Hutopy.Infrastructure.Identity; +using Hutopy.Web.Common; + +namespace Hutopy.Web.Features.Users.Handlers; + +[PublicAPI] +public record ChangeAddressRequest( + string? Address); + +[PublicAPI] +public class ChangeAddressHandler( + ApplicationUserManager userManager) + : Endpoint +{ + public override void Configure() + { + Post("/api/users/address"); + Options(o => o.WithTags("Users")); + } + + public override async Task HandleAsync( + ChangeAddressRequest request, + CancellationToken ct) + { + var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString()); + + if (user is null) + { + await SendNotFoundAsync(ct); + return; + } + + user.Address = request.Address; + // TODO: check to see if identity resets the email-validated flag - @jonathan + + var result = await userManager.UpdateAsync(user); + + if (result.Succeeded) + await SendOkAsync(ct); + else + await SendUnauthorizedAsync(ct); + } +} diff --git a/src/Web/Features/Users/Handlers/ChangeAlias.cs b/src/Web/Features/Users/Handlers/ChangeAlias.cs new file mode 100644 index 0000000..7272075 --- /dev/null +++ b/src/Web/Features/Users/Handlers/ChangeAlias.cs @@ -0,0 +1,42 @@ +using Hutopy.Infrastructure.Identity; +using Hutopy.Web.Common; + +namespace Hutopy.Web.Features.Users.Handlers; + +[PublicAPI] +public record ChangeAliasRequest( + string? Alias); + +[PublicAPI] +public class ChangeAliasHandler( + ApplicationUserManager userManager) + : Endpoint +{ + public override void Configure() + { + Post("/api/users/alias"); + Options(o => o.WithTags("Users")); + } + + public override async Task HandleAsync( + ChangeAliasRequest request, + CancellationToken ct) + { + var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString()); + + if (user is null) + { + await SendNotFoundAsync(ct); + return; + } + + user.Alias = request.Alias; + + var result = await userManager.UpdateAsync(user); + + if (result.Succeeded) + await SendOkAsync(ct); + else + await SendUnauthorizedAsync(ct); + } +} diff --git a/src/Web/Features/Users/Handlers/ChangeBirthDate.cs b/src/Web/Features/Users/Handlers/ChangeBirthDate.cs new file mode 100644 index 0000000..661bd8a --- /dev/null +++ b/src/Web/Features/Users/Handlers/ChangeBirthDate.cs @@ -0,0 +1,42 @@ +using Hutopy.Infrastructure.Identity; +using Hutopy.Web.Common; + +namespace Hutopy.Web.Features.Users.Handlers; + +[PublicAPI] +public record ChangeBirthDateRequest( + DateTime BirthDate); + +[PublicAPI] +public class ChangeBirthDateHandler( + ApplicationUserManager userManager) + : Endpoint +{ + public override void Configure() + { + Post("/api/users/birthdate"); + Options(o => o.WithTags("Users")); + } + + public override async Task HandleAsync( + ChangeBirthDateRequest request, + CancellationToken ct) + { + var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString()); + + if (user is null) + { + await SendNotFoundAsync(ct); + return; + } + + user.BirthDate = request.BirthDate; + + var result = await userManager.UpdateAsync(user); + + if (result.Succeeded) + await SendOkAsync(ct); + else + await SendUnauthorizedAsync(ct); + } +} diff --git a/src/Web/Features/Users/Handlers/ChangeEmail.cs b/src/Web/Features/Users/Handlers/ChangeEmail.cs new file mode 100644 index 0000000..8f7c844 --- /dev/null +++ b/src/Web/Features/Users/Handlers/ChangeEmail.cs @@ -0,0 +1,43 @@ +using Hutopy.Infrastructure.Identity; +using Hutopy.Web.Common; + +namespace Hutopy.Web.Features.Users.Handlers; + +[PublicAPI] +public record ChangeEmailRequest( + string? Email); + +[PublicAPI] +public class ChangeEmailHandler( + ApplicationUserManager userManager) + : Endpoint +{ + public override void Configure() + { + Post("/api/users/email"); + Options(o => o.WithTags("Users")); + } + + public override async Task HandleAsync( + ChangeEmailRequest request, + CancellationToken ct) + { + var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString()); + + if (user is null) + { + await SendNotFoundAsync(ct); + return; + } + + user.Email = request.Email; + // TODO: check to see if identity resets the email-validated flag - @jonathan + + var result = await userManager.UpdateAsync(user); + + if (result.Succeeded) + await SendOkAsync(ct); + else + await SendUnauthorizedAsync(ct); + } +} diff --git a/src/Web/Features/Users/Handlers/ChangeFullname.cs b/src/Web/Features/Users/Handlers/ChangeFullname.cs new file mode 100644 index 0000000..b71762e --- /dev/null +++ b/src/Web/Features/Users/Handlers/ChangeFullname.cs @@ -0,0 +1,44 @@ +using Hutopy.Infrastructure.Identity; +using Hutopy.Web.Common; + +namespace Hutopy.Web.Features.Users.Handlers; + +[PublicAPI] +public record ChangeFullnameRequest( + string? Firstname, + string? Lastname); + +[PublicAPI] +public class ChangeFullnameHandler( + ApplicationUserManager userManager) + : Endpoint +{ + public override void Configure() + { + Post("/api/users/fullname"); + Options(o => o.WithTags("Users")); + } + + public override async Task HandleAsync( + ChangeFullnameRequest request, + CancellationToken ct) + { + var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString()); + + if (user is null) + { + await SendNotFoundAsync(ct); + return; + } + + user.Firstname = request.Firstname; + user.Lastname = request.Lastname; + + var result = await userManager.UpdateAsync(user); + + if (result.Succeeded) + await SendOkAsync(ct); + else + await SendUnauthorizedAsync(ct); + } +} diff --git a/src/Web/Features/Users/Handlers/ChangePhone.cs b/src/Web/Features/Users/Handlers/ChangePhone.cs new file mode 100644 index 0000000..8e7a91f --- /dev/null +++ b/src/Web/Features/Users/Handlers/ChangePhone.cs @@ -0,0 +1,43 @@ +using Hutopy.Infrastructure.Identity; +using Hutopy.Web.Common; + +namespace Hutopy.Web.Features.Users.Handlers; + +[PublicAPI] +public record ChangePhoneRequest( + string? PhoneNumber); + +[PublicAPI] +public class ChangePhoneHandler( + ApplicationUserManager userManager) + : Endpoint +{ + public override void Configure() + { + Post("/api/users/phone"); + Options(o => o.WithTags("Users")); + } + + public override async Task HandleAsync( + ChangePhoneRequest request, + CancellationToken ct) + { + var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString()); + + if (user is null) + { + await SendNotFoundAsync(ct); + return; + } + + user.PhoneNumber = request.PhoneNumber; + // TODO: check to see if identity resets the email-validated flag - @jonathan + + var result = await userManager.UpdateAsync(user); + + if (result.Succeeded) + await SendOkAsync(ct); + else + await SendUnauthorizedAsync(ct); + } +} diff --git a/src/Web/Features/Users/Handlers/GetCurrentUser.cs b/src/Web/Features/Users/Handlers/GetCurrentUser.cs new file mode 100644 index 0000000..30981ac --- /dev/null +++ b/src/Web/Features/Users/Handlers/GetCurrentUser.cs @@ -0,0 +1,48 @@ +using Hutopy.Application.Common.Interfaces; +using Hutopy.Web.Features.Users.Handlers.Models; + +namespace Hutopy.Web.Features.Users.Handlers; + +[PublicAPI] +public class GetCurrentUserQueryHandler( + IIdentityService identityService +) + : EndpointWithoutRequest +{ + public override void Configure() + { + Get("/api/users/profile"); + Options(o => o.WithTags("Users")); + } + + public override async Task HandleAsync( + CancellationToken cancellationToken) + { + var userModel = await identityService.GetCurrentUserAsync(); + + if (userModel is null) + { + await SendNotFoundAsync(cancellationToken); + return; + } + + var roles = await identityService.GetCurrentUserRolesAsync(); + + await SendOkAsync( + new UserDto + { + Id = userModel.Id, + Alias = userModel.Alias, + PortraitUrl = userModel.PortraitUrl, + Firstname = userModel.Firstname, + Lastname = userModel.Lastname, + Username = userModel.Username, + PhoneNumber = userModel.PhoneNumber, + Email = userModel.Email, + BirthDate = userModel.BirthDate, + Address = userModel.Address, + UserRoles = roles, + }, + cancellationToken); + } +} diff --git a/src/Web/Features/Users/Handlers/GetCurrentUserProfilePicture.cs b/src/Web/Features/Users/Handlers/GetCurrentUserProfilePicture.cs new file mode 100644 index 0000000..806fef9 --- /dev/null +++ b/src/Web/Features/Users/Handlers/GetCurrentUserProfilePicture.cs @@ -0,0 +1,31 @@ +using Hutopy.Application.AzureBlobStorage.Constants; +using Hutopy.Application.Common.Interfaces; + +namespace Hutopy.Web.Features.Users.Handlers; + +[PublicAPI] +public class GetCurrentUserPortraitHandler( + IIdentityService identityService, + IBlobStorage blobStorage +) + : EndpointWithoutRequest +{ + public override void Configure() + { + Get("/api/users/portrait"); + Options(o => o.WithTags("Users")); + } + + public override async Task HandleAsync( + CancellationToken cancellationToken) + { + var identityUser = await identityService.GetCurrentUserAsync(); + + var stream = await blobStorage.DownloadFileAsync( + ContainerNames.Users, + $"{identityUser.Id.ToString()}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}", + cancellationToken); + + await SendOkAsync(stream, cancellationToken); + } +} diff --git a/src/Web/Features/Users/Handlers/Models/UserDto.cs b/src/Web/Features/Users/Handlers/Models/UserDto.cs new file mode 100644 index 0000000..b546f29 --- /dev/null +++ b/src/Web/Features/Users/Handlers/Models/UserDto.cs @@ -0,0 +1,16 @@ +namespace Hutopy.Web.Features.Users.Handlers.Models; + +public class UserDto +{ + public Guid Id { get; init; } + public IList UserRoles { get; init; } = []; + public string Username { get; init; } = null!; + public string? Alias { get; init; } + public string? PortraitUrl { get; init; } + public string? Firstname { get; init; } + public string? Lastname { get; init; } + public string? Email { get; init; } + public string? PhoneNumber { get; init; } + public DateTime? BirthDate { get; init; } + public string? Address { get; init; } +} diff --git a/src/Application/Users/Queries/GetCurrentUser/UserTransactionDto.cs b/src/Web/Features/Wallets/UserTransactionDto.cs similarity index 55% rename from src/Application/Users/Queries/GetCurrentUser/UserTransactionDto.cs rename to src/Web/Features/Wallets/UserTransactionDto.cs index f682df4..361dcff 100644 --- a/src/Application/Users/Queries/GetCurrentUser/UserTransactionDto.cs +++ b/src/Web/Features/Wallets/UserTransactionDto.cs @@ -1,6 +1,4 @@ -using Hutopy.Domain.Entities; - -namespace Hutopy.Application.Users.Queries.GetCurrentUser; +namespace Hutopy.Web.Features.Wallets; public class UserTransactionDto { @@ -13,11 +11,4 @@ public class UserTransactionDto public DateTimeOffset Created { get; init; } public bool IsConfirmed { get; init; } - private class Mapping : Profile - { - public Mapping() - { - CreateMap(); - } - } } diff --git a/src/Web/TestDataSeeder.cs b/src/Web/TestDataSeeder.cs index 68bc5e5..08c7bbf 100644 --- a/src/Web/TestDataSeeder.cs +++ b/src/Web/TestDataSeeder.cs @@ -113,7 +113,7 @@ internal class TestDataSeeder( SubjectId = content.Id, CreatedAt = currentDate, CreatedBy = author.Id, - CreatedByName = author.Alias ?? $"{author.FirstName} {author.LastName}", + CreatedByName = author.Alias ?? $"{author.Firstname} {author.Lastname}", CreatedByPortraitUrl = author.PortraitUrl, Value = $"Message #{m} on {content.Title}" }; @@ -143,7 +143,7 @@ internal class TestDataSeeder( SubjectId = content.Id, ParentId = parent.Id, CreatedBy = author.Id, - CreatedByName = author.Alias ?? $"{author.FirstName} {author.LastName}", + CreatedByName = author.Alias ?? $"{author.Firstname} {author.Lastname}", CreatedByPortraitUrl = author.PortraitUrl, CreatedAt = currentDate, Value = $"Reply {r} to {parent.Value} on {content.Title}" @@ -165,8 +165,8 @@ internal class TestDataSeeder( Email = $"{name}@test", EmailConfirmed = true, Alias = name, - FirstName = $"FirstName of {name}", - LastName = $"LastName of {name}", + Firstname = $"FirstName of {name}", + Lastname = $"LastName of {name}", PortraitUrl = portraitUrl }; diff --git a/tests/Application.FunctionalTests/Testing.cs b/tests/Application.FunctionalTests/Testing.cs index 74cd4d0..dc44dc4 100644 --- a/tests/Application.FunctionalTests/Testing.cs +++ b/tests/Application.FunctionalTests/Testing.cs @@ -73,7 +73,7 @@ public partial class Testing var user = new ApplicationUser { - UserName = userName, Email = userName, FirstName = "FirstName", LastName = "LastName" + UserName = userName, Email = userName, Firstname = "FirstName", Lastname = "LastName" }; var result = await userManager.CreateAsync(user, password);