diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index 74e9240..e780b94 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -1,13 +1,15 @@ using Google.Apis.Oauth2.v2.Data; using Hutopy.Application.Common.Models; -using Hutopy.Application.Users.Models; namespace Hutopy.Application.Common.Interfaces; public interface IIdentityService { Task> CreateUserAsync(Userinfo userInfo); - Task> CreateUserAsync(string email, string userName, string firstName, string lastName, string password); + + Task> CreateUserAsync(string email, string userName, string firstName, string lastName, + string password); + Task GetCurrentUserAsync(); Task UpdateCurrentUserBannerPictureUrlAsync(string url); Task UpdateCurrentUserProfilePictureUrlAsync(string url); @@ -15,6 +17,7 @@ public interface IIdentityService Task> UpdateCurrentUserAsync(UserModel userModel); Task> GetCurrentUserRolesAsync(); Task FindUserByIdAsync(string id); + Task FindUserByCreatorAliasAsync(string creatorAlias, CancellationToken cancellationToken); Task FindUserByEmailAsync(string email); Task GetUserByUserNameAsync(string userName); Task LoginAsync(string email, string password); diff --git a/src/Domain/Constants/Roles.cs b/src/Domain/Constants/Roles.cs index 38d9bf3..b285a6e 100644 --- a/src/Domain/Constants/Roles.cs +++ b/src/Domain/Constants/Roles.cs @@ -3,4 +3,5 @@ public abstract class Roles { public const string Administrator = nameof(Administrator); -} \ No newline at end of file + public const string Creator = nameof(Creator); +} diff --git a/src/Infrastructure/Data/ApplicationDbContextInitializer.cs b/src/Infrastructure/Data/ApplicationDbContextInitializer.cs index 4a1e041..bc57199 100644 --- a/src/Infrastructure/Data/ApplicationDbContextInitializer.cs +++ b/src/Infrastructure/Data/ApplicationDbContextInitializer.cs @@ -1,8 +1,6 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Hutopy.Domain.Constants; +using Hutopy.Domain.Constants; using Hutopy.Infrastructure.Identity; +using Hutopy.Infrastructure.Identity.OwnedEntities; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -28,7 +26,7 @@ public static class InitializerExtensions public class ApplicationDbContextInitializer( ILogger logger, ApplicationDbContext context, - UserManager userManager, + ApplicationUserManager userManager, RoleManager roleManager) { public async Task InitialiseAsync() @@ -61,27 +59,57 @@ public class ApplicationDbContextInitializer( { // Default roles var administratorRole = new IdentityRole(Roles.Administrator); - if (roleManager.Roles.All(r => r.Name != administratorRole.Name)) { await roleManager.CreateAsync(administratorRole); } - // Default users - var administrator = new ApplicationUser + var roleCreator = new IdentityRole(Roles.Creator); + if (roleManager.Roles.All(r => r.Name != roleCreator.Name)) { - UserName = "administrator@localhost", - Email = "administrator@localhost", - PortraitUrl = "images/usersmedia/anonyme/profilepictures/profilePascal.jpg" - }; + await roleManager.CreateAsync(roleCreator); + } + // Default users + var administrator = + new ApplicationUser { UserName = "administrator@localhost", Email = "administrator@localhost" }; if (userManager.Users.All(u => u.UserName != administrator.UserName)) { await userManager.CreateAsync(administrator, "Administrator1!"); - if (!string.IsNullOrWhiteSpace(administratorRole.Name)) + await userManager.AddToRolesAsync(administrator, new[] { Roles.Administrator }); + } + + // ADD CREATORS + await AddDefaultCreator(new ApplicationUser + { + UserName = "hutopy@localhost", + Email = "hutopy@localhost", + CreatorAlias = "hutopy", + About = "Page officielle", + Description = "Site officiel pour Hutopy. Venez-nous-y retrouver avec tous vos fans!", + EmailConfirmed = true, + ProfileColors = new ProfileColors { BannerTop = "A30E79", BannerBottom = "6B0065", Accent = "23393B", Menu = "53B93B", }, + SocialNetworks = + new SocialNetworks + { + XUrl = "https://twitter.com/Hutopyinc", + FacebookUrl = "https://www.facebook.com/Hutopy", + InstagramUrl = "https://www.instagram.com/hutopy.inc/" + }, + StoredDataUrls = new StoredDataUrls { - await userManager.AddToRolesAsync(administrator, new[] { administratorRole.Name }); + BannerPictureUrl = "/images/usersmedia/HutopyProfile/banners/banner01.png", + ProfilePictureUrl = "/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png" } + }); + } + + private async Task AddDefaultCreator(ApplicationUser hutopy) + { + if (userManager.Users.All(u => u.UserName != hutopy.UserName)) + { + await userManager.CreateAsync(hutopy, "Test123!"); + await userManager.AddToRolesAsync(hutopy, new[] { Roles.Creator }); } } } diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index 2dfccb2..4aae695 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -44,6 +44,7 @@ public static class DependencyInjection services .AddIdentityCore() + .AddUserManager() .AddRoles() .AddEntityFrameworkStores() .AddApiEndpoints() diff --git a/src/Infrastructure/Identity/ApplicationUser.cs b/src/Infrastructure/Identity/ApplicationUser.cs index 0988174..29e74ec 100644 --- a/src/Infrastructure/Identity/ApplicationUser.cs +++ b/src/Infrastructure/Identity/ApplicationUser.cs @@ -7,6 +7,7 @@ public class ApplicationUser : IdentityUser { public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; + public string? CreatorAlias { get; set; } public string Occupation { get; set; } = string.Empty; public string BirthDate { get; set; } = string.Empty; public string Country { get; set; } = string.Empty; diff --git a/src/Infrastructure/Identity/ApplicationUserManager.cs b/src/Infrastructure/Identity/ApplicationUserManager.cs new file mode 100644 index 0000000..987c8b6 --- /dev/null +++ b/src/Infrastructure/Identity/ApplicationUserManager.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Hutopy.Infrastructure.Identity; + +public sealed class ApplicationUserManager( + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger> logger) + : UserManager( + store, + optionsAccessor, + passwordHasher, + userValidators, + passwordValidators, + keyNormalizer, + errors, + services, + logger) +{ + public async Task FindByCreatorAliasAsync(string creatorAlias, + CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(creatorAlias); + ThrowIfDisposed(); + + var user = await Users.SingleOrDefaultAsync(u => EF.Functions.Like( + creatorAlias, + u.CreatorAlias), + cancellationToken: cancellationToken); + + return user; + } +} diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 2bc6ff4..b03c9e2 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -16,7 +16,7 @@ using Microsoft.Extensions.Configuration; namespace Hutopy.Infrastructure.Identity; public class IdentityService( - UserManager userManager, + ApplicationUserManager userManager, SignInManager signInManager, IUserClaimsPrincipalFactory userClaimsPrincipalFactory, IAuthorizationService authorizationService, @@ -167,10 +167,28 @@ public class IdentityService( public async Task FindUserByIdAsync(string id) { - var response = await userManager.FindByIdAsync(id); + var user = await userManager.FindByIdAsync(id); - if (response == null) return null; + if (user == null) return null; + var userModel = BuildModelFrom(user); + + return userModel; + } + + public async Task FindUserByCreatorAliasAsync(string creatorAlias, CancellationToken cancellationToken = default) + { + var user = await userManager.FindByCreatorAliasAsync(creatorAlias, cancellationToken); + + if (user == null) return null; + + var userModel = BuildModelFrom(user); + + return userModel; + } + + private static UserModel BuildModelFrom(ApplicationUser response) + { var userModel = new UserModel { Id = response.Id, @@ -211,10 +229,9 @@ public class IdentityService( WebsiteIconUrl = response.StoredDataUrls.WebsiteIconUrl, } }; - return userModel; } - + public async Task FindUserByEmailAsync(string email) { var response = await userManager.FindByEmailAsync(email); diff --git a/src/Infrastructure/Migrations/20240702055858_AddCreatorAliasTouser.Designer.cs b/src/Infrastructure/Migrations/20240702055858_AddCreatorAliasTouser.Designer.cs new file mode 100644 index 0000000..47946f4 --- /dev/null +++ b/src/Infrastructure/Migrations/20240702055858_AddCreatorAliasTouser.Designer.cs @@ -0,0 +1,560 @@ +// +using System; +using Hutopy.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240702055858_AddCreatorAliasTouser")] + partial class AddCreatorAliasTouser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Domain.Entities.FutureCreator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReasonToJoin") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SocialNetworkAccount") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("FutureCreators"); + }); + + modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsConfirmed") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetimeoffset"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Paid") + .HasColumnType("bit"); + + b.Property("StripeBillingDetailEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeBillingDetailName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeChargeId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeEventId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentIntent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentMethod") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeReceiptUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TipMessage") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("UserTransactions"); + }); + + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("About") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BirthDate") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatorAlias") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Occupation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b => + { + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.ProfileColors", "ProfileColors", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("Accent") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("BannerBottom") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("BannerTop") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("Menu") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_ProfileColors", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.SocialNetworks", "SocialNetworks", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("FacebookUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("InstagramUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("LinkedInUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("RedditUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("TikTokUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("XUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("YourWebsiteUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("YoutubeUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_SocialNetworks", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.StoredDataUrls", "StoredDataUrls", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("nvarchar(450)"); + + b1.Property("BannerPictureUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("ProfilePictureUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("WebsiteIconUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("ApplicationUserId"); + + b1.ToTable("ApplicationUser_StoredDataUrls", (string)null); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.Navigation("ProfileColors") + .IsRequired(); + + b.Navigation("SocialNetworks") + .IsRequired(); + + b.Navigation("StoredDataUrls") + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Migrations/20240702055858_AddCreatorAliasTouser.cs b/src/Infrastructure/Migrations/20240702055858_AddCreatorAliasTouser.cs new file mode 100644 index 0000000..456b236 --- /dev/null +++ b/src/Infrastructure/Migrations/20240702055858_AddCreatorAliasTouser.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Infrastructure.Migrations +{ + /// + public partial class AddCreatorAliasTouser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CreatorAlias", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CreatorAlias", + table: "AspNetUsers"); + } + } +} diff --git a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 4fb4c4e..0c4a193 100644 --- a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -176,6 +176,9 @@ namespace Hutopy.Infrastructure.Migrations .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("CreatorAlias") + .HasColumnType("nvarchar(max)"); + b.Property("Description") .IsRequired() .HasColumnType("nvarchar(max)"); diff --git a/src/Web/Messages/Shared.cs b/src/Web/Common/Shared.cs similarity index 97% rename from src/Web/Messages/Shared.cs rename to src/Web/Common/Shared.cs index eb48399..e36a0ec 100644 --- a/src/Web/Messages/Shared.cs +++ b/src/Web/Common/Shared.cs @@ -1,6 +1,6 @@ using System.Security.Claims; -namespace Hutopy.Web.Messages; +namespace Hutopy.Web.Common; public class Shared(string claimName) : Exception; diff --git a/src/Web/Contents/Data/Content.cs b/src/Web/Contents/Data/Content.cs new file mode 100644 index 0000000..c13bd0b --- /dev/null +++ b/src/Web/Contents/Data/Content.cs @@ -0,0 +1,13 @@ +namespace Hutopy.Web.Contents.Data; + +public class Content +{ + public Guid Id { get; init; } + public Guid CreatedBy { get; init; } + public DateTime CreatedAt { get; } + + public string? Title { get; init; } = null!; + public string? Description { get; init; } = null!; + public string? Uri { get; init; } = null!; +} + diff --git a/src/Web/Contents/Data/ContentDbContext.cs b/src/Web/Contents/Data/ContentDbContext.cs new file mode 100644 index 0000000..f525cbf --- /dev/null +++ b/src/Web/Contents/Data/ContentDbContext.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; + +namespace Hutopy.Web.Contents.Data; + +public class ContentDbContext( + DbContextOptions options) + : DbContext(options) +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .Property(c => c.CreatedAt) + .ValueGeneratedOnAdd() + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + } + + public DbSet Contents { get; set; } +} diff --git a/src/Web/Contents/Handlers/GetContents.cs b/src/Web/Contents/Handlers/GetContents.cs new file mode 100644 index 0000000..c9f3258 --- /dev/null +++ b/src/Web/Contents/Handlers/GetContents.cs @@ -0,0 +1,30 @@ +using FastEndpoints; +using Hutopy.Web.Contents.Data; +using Microsoft.EntityFrameworkCore; + +namespace Hutopy.Web.Contents.Handlers; + +public class GetContents( + ContentDbContext context) + : EndpointWithoutRequest +{ + public override void Configure() + { + Tags("Contents"); + Get("/api/contents/{ContentId:guid}"); + AllowAnonymous(); + } + + public override async Task HandleAsync( + CancellationToken ct) + { + var contentId = Route("ContentId"); + + var comments = await context + .Contents + .Where(c => c.Id == contentId) + .ToListAsync(cancellationToken: ct); + + await SendAsync(comments.First(), cancellation: ct); + } +} diff --git a/src/Web/Contents/Handlers/GetContentsByUser.cs b/src/Web/Contents/Handlers/GetContentsByUser.cs new file mode 100644 index 0000000..a3e87ff --- /dev/null +++ b/src/Web/Contents/Handlers/GetContentsByUser.cs @@ -0,0 +1,30 @@ +using FastEndpoints; +using Hutopy.Web.Contents.Data; +using Microsoft.EntityFrameworkCore; + +namespace Hutopy.Web.Contents.Handlers; + +public class GetContentsByUser( + ContentDbContext context) + : EndpointWithoutRequest> +{ + public override void Configure() + { + Tags("Contents"); + Get("/api/contents/user/{UserId:guid}"); + AllowAnonymous(); + } + + public override async Task HandleAsync( + CancellationToken ct) + { + var userId = Route("UserId"); + + var posts = await context + .Contents + .Where(c => c.CreatedBy == userId) + .ToListAsync(cancellationToken: ct); + + await SendAsync(posts, cancellation: ct); + } +} diff --git a/src/Web/Contents/Handlers/PostMessage.cs b/src/Web/Contents/Handlers/PostMessage.cs new file mode 100644 index 0000000..850cfba --- /dev/null +++ b/src/Web/Contents/Handlers/PostMessage.cs @@ -0,0 +1,39 @@ +using FastEndpoints; +using Hutopy.Web.Common; +using Hutopy.Web.Contents.Data; + +namespace Hutopy.Web.Contents.Handlers; + +public record struct PostContentRequest( + string? Title, + string? Description, + string? Uri); + +public class PostMessage( + ContentDbContext context) + : Endpoint +{ + public override void Configure() + { + // TODO: Find how to specify the name we see in Swagger + Tags("Contents"); + Post("/api/contents"); + } + + public override async Task HandleAsync( + PostContentRequest req, + CancellationToken ct) + { + await context.Contents.AddAsync( + new Content + { + CreatedBy = User.GetUserId(), + Title = req.Title, + Description = req.Description, + Uri = req.Uri + }, + ct); + + await context.SaveChangesAsync(ct); + } +} diff --git a/src/Web/Contents/Migrations/20240702034957_Initial.Designer.cs b/src/Web/Contents/Migrations/20240702034957_Initial.Designer.cs new file mode 100644 index 0000000..c63f6b4 --- /dev/null +++ b/src/Web/Contents/Migrations/20240702034957_Initial.Designer.cs @@ -0,0 +1,58 @@ +// +using System; +using Hutopy.Web.Contents.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.Contents.Migrations +{ + [DbContext(typeof(ContentDbContext))] + [Migration("20240702034957_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Web.Contents.Data.Content", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("Uri") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Contents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Web/Contents/Migrations/20240702034957_Initial.cs b/src/Web/Contents/Migrations/20240702034957_Initial.cs new file mode 100644 index 0000000..593cc49 --- /dev/null +++ b/src/Web/Contents/Migrations/20240702034957_Initial.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Web.Contents.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Contents", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + CreatedBy = table.Column(type: "uniqueidentifier", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), + Title = table.Column(type: "nvarchar(max)", nullable: true), + Description = table.Column(type: "nvarchar(max)", nullable: true), + Uri = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Contents", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Contents"); + } + } +} diff --git a/src/Web/Contents/Migrations/ContentDbContextModelSnapshot.cs b/src/Web/Contents/Migrations/ContentDbContextModelSnapshot.cs new file mode 100644 index 0000000..1892c37 --- /dev/null +++ b/src/Web/Contents/Migrations/ContentDbContextModelSnapshot.cs @@ -0,0 +1,55 @@ +// +using System; +using Hutopy.Web.Contents.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Hutopy.Web.Contents.Migrations +{ + [DbContext(typeof(ContentDbContext))] + partial class ContentDbContextModelSnapshot : 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.Contents.Data.Content", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("Uri") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Contents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Web/Creators/Handlers/GetCreatorByAlias.cs b/src/Web/Creators/Handlers/GetCreatorByAlias.cs new file mode 100644 index 0000000..a46201f --- /dev/null +++ b/src/Web/Creators/Handlers/GetCreatorByAlias.cs @@ -0,0 +1,29 @@ +using FastEndpoints; +using Hutopy.Application.Common.Interfaces; +using Hutopy.Application.Common.Models; + +namespace Hutopy.Web.Creators.Handlers; + +public class GetCreatorByAlias( + IIdentityService identityService) + : EndpointWithoutRequest +{ + public override void Configure() + { + Tags("Creators"); + Get("/api/creators/@{CreatorAlias}"); + AllowAnonymous(); + } + + public override async Task HandleAsync( + CancellationToken ct) + { + var creatorAlias = Route("CreatorAlias"); + + ArgumentException.ThrowIfNullOrEmpty(creatorAlias); + + var user = await identityService.FindUserByCreatorAliasAsync(creatorAlias, ct); + + await SendAsync(user, cancellation: ct); + } +} diff --git a/src/Web/Messages/Handlers/GetMessagesByUser.cs b/src/Web/Messages/Handlers/GetMessagesByUser.cs index d70cd11..b5b25e0 100644 --- a/src/Web/Messages/Handlers/GetMessagesByUser.cs +++ b/src/Web/Messages/Handlers/GetMessagesByUser.cs @@ -5,9 +5,6 @@ using Microsoft.EntityFrameworkCore; namespace Hutopy.Web.Messages.Handlers; -public record GetMessagesByUserRequest( - [FromRoute] Guid UserId); - public class GetMessagesByUser( MessagingDbContext context) : EndpointWithoutRequest> @@ -15,7 +12,7 @@ public class GetMessagesByUser( public override void Configure() { Tags("Messages"); - Get("/api/messages/by-user/{UserId:guid}"); + Get("/api/messages/user/{UserId:guid}"); } public override async Task HandleAsync( diff --git a/src/Web/Messages/Handlers/PostMessage.cs b/src/Web/Messages/Handlers/PostMessage.cs index 7460728..46a316c 100644 --- a/src/Web/Messages/Handlers/PostMessage.cs +++ b/src/Web/Messages/Handlers/PostMessage.cs @@ -1,4 +1,5 @@ using FastEndpoints; +using Hutopy.Web.Common; using Hutopy.Web.Messages.Data; namespace Hutopy.Web.Messages.Handlers; diff --git a/src/Web/Messages/Handlers/PostReplyMessage.cs b/src/Web/Messages/Handlers/PostReplyMessage.cs index 2385765..5a9bef9 100644 --- a/src/Web/Messages/Handlers/PostReplyMessage.cs +++ b/src/Web/Messages/Handlers/PostReplyMessage.cs @@ -1,4 +1,5 @@ using FastEndpoints; +using Hutopy.Web.Common; using Hutopy.Web.Messages.Data; namespace Hutopy.Web.Messages.Handlers; diff --git a/src/Web/Pages/Shared/_LoginPartial.cshtml b/src/Web/Pages/Shared/_LoginPartial.cshtml index c16ebd7..b8197de 100644 --- a/src/Web/Pages/Shared/_LoginPartial.cshtml +++ b/src/Web/Pages/Shared/_LoginPartial.cshtml @@ -1,7 +1,6 @@ @using Hutopy.Infrastructure.Identity @using Microsoft.AspNetCore.Identity @inject SignInManager SignInManager -@inject UserManager UserManager @{ string? returnUrl = null; diff --git a/src/Web/Program.cs b/src/Web/Program.cs index 78700bc..097dac9 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -4,6 +4,7 @@ using Hutopy.Application; using Hutopy.Infrastructure; using Hutopy.Infrastructure.Data; using Hutopy.Web; +using Hutopy.Web.Contents.Data; using Hutopy.Web.Messages.Data; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.EntityFrameworkCore; @@ -21,24 +22,24 @@ if (!builder.Environment.IsDevelopment()) builder.Services.AddCors(options => { - options.AddPolicy("AllowAll", builder => + options.AddPolicy("AllowAll", policy => { - builder.AllowAnyOrigin() + policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); - options.AddPolicy("AllowHutopyUi", builder => + options.AddPolicy("AllowHutopyUi", policy => { - builder.WithOrigins("https://zealous-bay-08204590f.5.azurestaticapps.net") + policy.WithOrigins("https://zealous-bay-08204590f.5.azurestaticapps.net") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); }); - - options.AddPolicy("AllowHutopyUiPreview", builder => + + options.AddPolicy("AllowHutopyUiPreview", policy => { - builder.WithOrigins("https://zealous-bay-08204590f-preview.eastus2.5.azurestaticapps.net") + policy.WithOrigins("https://zealous-bay-08204590f-preview.eastus2.5.azurestaticapps.net") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); @@ -83,6 +84,11 @@ builder.Services.AddDbContext((_, options) => options.UseSqlServer(builder.Configuration.GetConnectionString("CommentStore")); }); +builder.Services.AddDbContext((_, options) => +{ + options.UseSqlServer(builder.Configuration.GetConnectionString("ContentStore")); +}); + var app = builder.Build(); app.UseForwardedHeaders( diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 053126a..817e221 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -30,8 +30,4 @@ - - - - diff --git a/src/Web/appsettings.Development.json b/src/Web/appsettings.Development.json index 9e2b285..decba8e 100644 --- a/src/Web/appsettings.Development.json +++ b/src/Web/appsettings.Development.json @@ -9,7 +9,8 @@ }, "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" + "CommentStore": "Server=localhost,1433;Initial Catalog=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True;MultiSubnetFailover=True", + "ContentStore": "Server=localhost,1433;Initial Catalog=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True;MultiSubnetFailover=True" }, "Authentication": { "Jwt": { diff --git a/tests/Application.FunctionalTests/Testing.cs b/tests/Application.FunctionalTests/Testing.cs index 49d8384..8270718 100644 --- a/tests/Application.FunctionalTests/Testing.cs +++ b/tests/Application.FunctionalTests/Testing.cs @@ -63,7 +63,7 @@ public partial class Testing { using var scope = _scopeFactory.CreateScope(); - var userManager = scope.ServiceProvider.GetRequiredService>(); + var userManager = scope.ServiceProvider.GetRequiredService(); var user = new ApplicationUser { UserName = userName, Email = userName };