Split creators out of identity

This commit is contained in:
Jonathan Bourdon
2024-07-31 23:29:26 -04:00
parent bbcc7a8a33
commit 2b30e1a03c
105 changed files with 1497 additions and 7490 deletions

View File

@@ -72,12 +72,12 @@ public class GoogleController(
jwtOptions.Value.Issuer,
jwtOptions.Value.Audience,
jwtOptions.Value.Key,
user.Id,
user.Id.ToString(),
user.Email,
user.Alias,
user.FirstName,
user.LastName,
user.StoredDataUrls.ProfilePictureUrl);
user.PortraitUrl);
return Ok(new { accessToken = token, email });
}

View File

@@ -9,8 +9,6 @@ using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using NSwag;
using NSwag.Generation.Processors.Security;
namespace Hutopy.Web;

View File

@@ -1,5 +1,4 @@
using Hutopy.Application.Users.Queries;
using Hutopy.Application.Users.Queries.GetCurrentUser;
using Hutopy.Application.Users.Queries.GetCurrentUser;
namespace Hutopy.Web.Endpoints;

View File

@@ -9,31 +9,19 @@ public class UpdateMyUser : EndpointGroupBase
app.MapGroup(this)
.RequireAuthorization()
.MapPost(UpdateCurrentUserProfilePicture, "/profile-picture")
.MapPost(UpdateCurrentUserBannerPicture, "/banner-picture")
.MapPost(UpdateCurrentUserWebsiteIcon, "/website-icon")
.MapPatch("/profile", UpdateCurrentUser);
}
private static async Task<IResult> UpdateCurrentUser(ISender sender, UpdateCurrentUserCommand command)
{
return await sender.Send(command);
}
private static async Task<IResult> UpdateCurrentUserProfilePicture(ISender sender, MemoryStream stream, string url = "")
private static async Task<IResult> UpdateCurrentUserProfilePicture(
ISender sender,
IFormFile formFile)
{
var command = new UploadProfilePictureCommand { ProfilePicture = stream, ProfilePictureUrl = url};
return await sender.Send(command);
}
private static async Task<IResult> UpdateCurrentUserBannerPicture(ISender sender, MemoryStream stream, string url = "")
{
var command = new UploadBannerPictureCommand { BannerPicture = stream, BannerPictureUrl = url};
return await sender.Send(command);
}
private static async Task<IResult> UpdateCurrentUserWebsiteIcon(ISender sender, MemoryStream stream, string url = "")
{
var command = new UploadWebsiteIconCommand { WebsiteIcon = stream, WebsitePictureUrl = url};
var command = new UploadProfilePictureCommand { File = formFile };
return await sender.Send(command);
}
}

View File

@@ -10,7 +10,8 @@ public class Users : EndpointGroupBase
app.MapGroup(this)
.MapPost(CreateUser)
.MapPost(Login, "/login")
.MapGet(GetUser);
.MapGet(GetUserById, "/id")
.MapGet(GetUserByUserName, "/user-name");
}
private static async Task<IResult> CreateUser(ISender sender, CreateUserCommand command)
@@ -18,7 +19,14 @@ public class Users : EndpointGroupBase
return await sender.Send(command);
}
private static async Task<UserDto> GetUser(ISender sender, [AsParameters] GetUserQuery query)
private static async Task<UserDto> GetUserById(ISender sender,
[AsParameters] GetUserByIdQuery query)
{
return await sender.Send(query);
}
private static async Task<UserDto> GetUserByUserName(ISender sender,
[AsParameters] GetUserByUserNameQuery query)
{
return await sender.Send(query);
}

View File

@@ -7,17 +7,50 @@ public class ContentDbContext(
: DbContext(options)
{
public const string SchemaName = "Content";
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("Content");
modelBuilder
.Entity<Content>()
.Property(c => c.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
modelBuilder
.Entity<Creator>()
.OwnsOne<About>(x => x.About);
modelBuilder
.Entity<Creator>()
.OwnsOne<SocialNetworks>(x => x.SocialNetworks);
modelBuilder
.Entity<Creator>()
.OwnsOne<ProfileColors>(x => x.ProfileColors);
modelBuilder
.Entity<Creator>()
.OwnsOne<StoredDataUrls>(x => x.StoredDataUrls);
}
public DbSet<Content> Contents { get; set; }
public DbSet<Content> Contents { get; init; } = null!;
public DbSet<Creator> Creators { get; init; } = null!;
public async Task<Creator?> FindByCreatorAliasAsync(
string creatorAlias,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(creatorAlias);
var user = await Creators.SingleOrDefaultAsync(creator =>
EF.Functions.Like(
creatorAlias,
creator.Name),
cancellationToken: cancellationToken);
return user;
}
}

View File

@@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
namespace Hutopy.Web.Features.Contents.Data;
public class Creator
{
public Guid Id { get; set; }
public Guid CreatedBy { get; init; }
public DateTimeOffset CreatedAt { get; init; }
[MaxLength(255)] public string Name { get; set; } = null!;
public About About { get; set; } = new();
public SocialNetworks SocialNetworks { get; set; } = new();
public ProfileColors ProfileColors { get; set; } = new();
public StoredDataUrls StoredDataUrls { get; set; } = new();
}
public class About
{
[MaxLength(255)] public string? Title { get; set; }
[MaxLength(255)] public string? Description { get; set; }
}
public class ProfileColors
{
[MaxLength(9)] public string? BannerTop { get; set; }
[MaxLength(9)] public string? BannerBottom { get; set; }
[MaxLength(9)] public string? Accent { get; set; }
[MaxLength(9)] public string? Menu { get; set; }
}
public class SocialNetworks
{
[MaxLength(255)] public string? FacebookUrl { get; set; }
[MaxLength(255)] public string? InstagramUrl { get; set; }
[MaxLength(255)] public string? XUrl { get; set; }
[MaxLength(255)] public string? LinkedInUrl { get; set; }
[MaxLength(255)] public string? TikTokUrl { get; set; }
[MaxLength(255)] public string? YoutubeUrl { get; set; }
[MaxLength(255)] public string? RedditUrl { get; set; }
[MaxLength(255)] public string? WebsiteUrl { get; set; }
}
public class StoredDataUrls
{
[MaxLength(255)] public string? BannerPictureUrl { get; set; }
[MaxLength(255)] public string? ProfilePictureUrl { get; set; }
}

View File

@@ -0,0 +1,49 @@
using FastEndpoints;
using Hutopy.Application.AzureBlobStorage.Constants;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Application.Utils;
using Hutopy.Web.Features.Contents.Data;
using Microsoft.EntityFrameworkCore;
namespace Hutopy.Web.Features.Contents.Handlers;
public record ChangeBannerRequest(
Guid CreatorId,
IFormFile File);
public class ChangeBannerHandler(
IHttpContextAccessor contextAccessor,
ContentDbContext context,
IAzureBlobStorageService azureBlobStorageService)
: Endpoint<ChangeBannerRequest>
{
public override void Configure()
{
Post("/api/creators/{CreatorId}/banner");
Options(o => o.WithTags("Contents"));
AllowFileUploads();
}
public override async Task HandleAsync(ChangeBannerRequest request, CancellationToken ct)
{
var creator = await context
.Creators
.Include(c => c.StoredDataUrls)
.SingleAsync(
c => c.Id == request.CreatorId,
cancellationToken: ct);
var contentType = contextAccessor.EnsureContentType();
var blobUrl = await azureBlobStorageService.UploadFileAsync(
ContainerNames.Users,
$"{request.CreatorId}/{SubDirectoryNames.Profile}/{CommonFileNames.BannerPicture}",
request.File.OpenReadStream(),
contentType,
ct);
await context.SaveChangesAsync(ct);
await SendOkAsync(blobUrl, ct);
}
}

View File

@@ -0,0 +1,71 @@
using FastEndpoints;
using FluentValidation;
using Hutopy.Web.Features.Contents.Data;
using Microsoft.EntityFrameworkCore;
namespace Hutopy.Web.Features.Contents.Handlers;
public record ChangeColorsRequest(
Guid CreatorId,
string? BannerTop,
string? BannerBottom,
string? Accent,
string? Menu);
public sealed class ChangeColorsRequestValidator
: Validator<ChangeColorsRequest>
{
public ChangeColorsRequestValidator()
{
RuleFor(x => x.BannerTop)
.MinimumLength(4).WithMessage("The minimum value should be in the format #444")
.MinimumLength(9).WithMessage("The maximum value should be in the format #11223344")
.Must(x => x.StartsWith('#')).WithMessage("The format should be a valid html color and start with #");
RuleFor(x => x.BannerBottom)
.MinimumLength(4).WithMessage("The minimum value should be in the format #444")
.MinimumLength(9).WithMessage("The maximum value should be in the format #11223344")
.Must(x => x.StartsWith('#')).WithMessage("The format should be a valid html color and start with #");
RuleFor(x => x.Accent)
.MinimumLength(4).WithMessage("The minimum value should be in the format #444")
.MinimumLength(9).WithMessage("The maximum value should be in the format #11223344")
.Must(x => x.StartsWith('#')).WithMessage("The format should be a valid html color and start with #");
RuleFor(x => x.Menu)
.MinimumLength(4).WithMessage("The minimum value should be in the format #444")
.MinimumLength(9).WithMessage("The maximum value should be in the format #11223344")
.Must(x => x.StartsWith('#')).WithMessage("The format should be a valid html color and start with #");
}
}
public class ChangeColorsHandler(
ContentDbContext context)
: Endpoint<ChangeColorsRequest>
{
public override void Configure()
{
Post("/api/creators/{CreatorId}/colors");
Options(o => o.WithTags("Contents"));
AllowFileUploads();
}
public override async Task HandleAsync(ChangeColorsRequest request, CancellationToken ct)
{
var creator = await context
.Creators
.Include(c => c.ProfileColors)
.SingleAsync(
c => c.Id == request.CreatorId,
cancellationToken: ct);
creator.ProfileColors.BannerTop = request.BannerTop;
creator.ProfileColors.BannerBottom = request.BannerBottom;
creator.ProfileColors.Accent = request.Accent;
creator.ProfileColors.Menu = request.Menu;
await context.SaveChangesAsync(ct);
await SendOkAsync(ct);
}
}

View File

@@ -53,6 +53,7 @@ public sealed class PostContent(
PostContentRequest req,
CancellationToken ct)
{
var urls = new ConcurrentBag<string>();
await Parallel.ForEachAsync(
@@ -98,14 +99,11 @@ public sealed class PostContent(
IFormFile file,
CancellationToken ct = default)
{
var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream, ct);
// TODO: I would like us to use ContainerNames.Creators but it seems we are missing configurations @jbourdon
var url = await blobStorage.UploadFileAsync(
ContainerNames.Users,
$"{creatorId}/{SubDirectoryNames.Contents}/{contentId}/{file.FileName}",
memoryStream,
file.OpenReadStream(),
file.ContentType,
ct: ct);

View File

@@ -0,0 +1,53 @@
using FastEndpoints;
using FluentValidation;
using Hutopy.Web.Common;
using Hutopy.Web.Features.Contents.Data;
namespace Hutopy.Web.Features.Contents.Handlers;
public record CreateCreatorRequest(
Guid CreatorId,
string Name);
public sealed class CreateCreatorRequestValidator : Validator<CreateCreatorRequest>
{
public CreateCreatorRequestValidator()
{
RuleFor(r => r.CreatorId)
.NotNull().WithMessage("You should specify the CreatorId")
.NotEmpty().WithMessage("You should specify a valid/not empty CreatorId");
RuleFor(r => r.Name)
.NotNull().WithMessage("You should specify the Name")
.NotEmpty().WithMessage("You should specify a valid/not empty Name");
}
}
public sealed class CreateCreatorHandler(
ContentDbContext context)
: Endpoint<CreateCreatorRequest>
{
public override void Configure()
{
Post("/api/creators");
Options(o => o.WithTags("Contents"));
}
public override async Task HandleAsync(
CreateCreatorRequest req,
CancellationToken ct)
{
await context.Creators.AddAsync(
new()
{
Id = req.CreatorId,
CreatedBy = User.GetUserId(),
Name = req.Name
},
ct);
await context.SaveChangesAsync(ct);
await SendOkAsync(ct);
}
}

View File

@@ -1,13 +1,13 @@
using FastEndpoints;
using FluentValidation;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Application.Common.Models;
using Hutopy.Web.Features.Contents.Data;
using Microsoft.EntityFrameworkCore;
namespace Hutopy.Web.Features.Contents.Handlers;
public sealed class GetCreatorByAliasRequest
{
public string CreatorAlias { get; init; }
public required string Name { get; set; }
}
public sealed class GetCreatorByAliasRequestValidator
@@ -15,20 +15,20 @@ public sealed class GetCreatorByAliasRequestValidator
{
public GetCreatorByAliasRequestValidator()
{
RuleFor(r => r.CreatorAlias)
.NotNull().WithMessage("You should specify the CreatorAlias")
.NotEmpty().WithMessage("You should specify a valid/not empty CreatorAlias");
RuleFor(r => r.Name)
.NotNull().WithMessage("You should specify the Name")
.NotEmpty().WithMessage("You should specify a valid/not empty Name");
}
}
public class GetCreatorByAlias(
IIdentityService identityService)
: Endpoint<GetCreatorByAliasRequest, UserModel?>
public class GetCreatorByAliasHandler(
ContentDbContext context)
: Endpoint<GetCreatorByAliasRequest, Creator>
{
public override void Configure()
{
Get("/api/creators/@{Name}");
Options((o => o.WithTags("Creators")));
Get("/api/creators/@{CreatorAlias}");
AllowAnonymous();
}
@@ -36,10 +36,13 @@ public class GetCreatorByAlias(
GetCreatorByAliasRequest req,
CancellationToken ct)
{
var user = await identityService.FindUserByCreatorAliasAsync(
req.CreatorAlias,
ct);
var creator = await context
.Creators
.SingleOrDefaultAsync(
c => EF.Functions.Like(c.Name, req.Name),
cancellationToken: ct);
await SendAsync(user, cancellation: ct);
if (creator is null) await SendNotFoundAsync(ct);
else await SendAsync(creator, cancellation: ct);
}
}

View File

@@ -0,0 +1,47 @@
using FastEndpoints;
using FluentValidation;
using Hutopy.Web.Features.Contents.Data;
namespace Hutopy.Web.Features.Contents.Handlers;
public sealed class GetCreatorByIdRequest
{
public required Guid CreatorId { get; set; }
}
public sealed class GetCreatorByIdRequestValidator
: Validator<GetCreatorByIdRequest>
{
public GetCreatorByIdRequestValidator()
{
RuleFor(r => r.CreatorId)
.NotNull().WithMessage("You should specify the CreatorId")
.NotEmpty().WithMessage("You should specify a valid/not empty CreatorId");
}
}
public class GetCreatorByIdHandler(
ContentDbContext context)
: Endpoint<GetCreatorByIdRequest, Creator>
{
public override void Configure()
{
Get("/api/creators/{CreatorId}");
Options((o => o.WithTags("Creators")));
AllowAnonymous();
}
public override async Task HandleAsync(
GetCreatorByIdRequest req,
CancellationToken ct)
{
var creator = await context
.Creators
.FindAsync(
[req.CreatorId],
cancellationToken: ct);
if (creator is null) await SendNotFoundAsync(ct);
else await SendAsync(creator, cancellation: ct);
}
}

View File

@@ -1,59 +0,0 @@
// <auto-generated />
using System;
using Hutopy.Web.Features.Contents.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Hutopy.Web.Contents.Migrations
{
[DbContext(typeof(ContentDbContext))]
[Migration("20240718034516_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Content")
.HasAnnotation("ProductVersion", "8.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hutopy.Web.Contents.Data.Content", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.Property<string>("Uri")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Contents", "Content");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,43 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Hutopy.Web.Contents.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "Content");
migrationBuilder.CreateTable(
name: "Contents",
schema: "Content",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
Title = table.Column<string>(type: "text", nullable: true),
Description = table.Column<string>(type: "text", nullable: true),
Uri = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Contents", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Contents",
schema: "Content");
}
}
}

View File

@@ -1,59 +0,0 @@
// <auto-generated />
using System;
using Hutopy.Web.Features.Contents.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Hutopy.Web.Features.Contents.Migrations
{
[DbContext(typeof(ContentDbContext))]
[Migration("20240725022229_AddMultipleMediaUrlsToContent")]
partial class AddMultipleMediaUrlsToContent
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Content")
.HasAnnotation("ProductVersion", "8.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.Property<string[]>("Urls")
.HasColumnType("text[]");
b.HasKey("Id");
b.ToTable("Contents", "Content");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,42 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Hutopy.Web.Features.Contents.Migrations
{
/// <inheritdoc />
public partial class AddMultipleMediaUrlsToContent : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Uri",
schema: "Content",
table: "Contents");
migrationBuilder.AddColumn<string[]>(
name: "Urls",
schema: "Content",
table: "Contents",
type: "text[]",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Urls",
schema: "Content",
table: "Contents");
migrationBuilder.AddColumn<string>(
name: "Uri",
schema: "Content",
table: "Contents",
type: "text",
nullable: true);
}
}
}

View File

@@ -0,0 +1,214 @@
// <auto-generated />
using System;
using Hutopy.Web.Features.Contents.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Hutopy.Web.Features.Contents.Migrations
{
[DbContext(typeof(ContentDbContext))]
[Migration("20240802044656_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Content")
.HasAnnotation("ProductVersion", "8.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.Property<string[]>("Urls")
.HasColumnType("text[]");
b.HasKey("Id");
b.ToTable("Contents", "Content");
});
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.ToTable("Creators", "Content");
});
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>
{
b.OwnsOne("Hutopy.Web.Features.Contents.Data.About", "About", b1 =>
{
b1.Property<Guid>("CreatorId")
.HasColumnType("uuid");
b1.Property<string>("Description")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("Title")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.HasKey("CreatorId");
b1.ToTable("Creators", "Content");
b1.WithOwner()
.HasForeignKey("CreatorId");
});
b.OwnsOne("Hutopy.Web.Features.Contents.Data.ProfileColors", "ProfileColors", b1 =>
{
b1.Property<Guid>("CreatorId")
.HasColumnType("uuid");
b1.Property<string>("Accent")
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("BannerBottom")
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("BannerTop")
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("Menu")
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.HasKey("CreatorId");
b1.ToTable("Creators", "Content");
b1.WithOwner()
.HasForeignKey("CreatorId");
});
b.OwnsOne("Hutopy.Web.Features.Contents.Data.SocialNetworks", "SocialNetworks", b1 =>
{
b1.Property<Guid>("CreatorId")
.HasColumnType("uuid");
b1.Property<string>("FacebookUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("InstagramUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("LinkedInUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("RedditUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("TikTokUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("WebsiteUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("XUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("YoutubeUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.HasKey("CreatorId");
b1.ToTable("Creators", "Content");
b1.WithOwner()
.HasForeignKey("CreatorId");
});
b.OwnsOne("Hutopy.Web.Features.Contents.Data.StoredDataUrls", "StoredDataUrls", b1 =>
{
b1.Property<Guid>("CreatorId")
.HasColumnType("uuid");
b1.Property<string>("BannerPictureUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("ProfilePictureUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.HasKey("CreatorId");
b1.ToTable("Creators", "Content");
b1.WithOwner()
.HasForeignKey("CreatorId");
});
b.Navigation("About")
.IsRequired();
b.Navigation("ProfileColors")
.IsRequired();
b.Navigation("SocialNetworks")
.IsRequired();
b.Navigation("StoredDataUrls")
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,78 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Hutopy.Web.Features.Contents.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "Content");
migrationBuilder.CreateTable(
name: "Contents",
schema: "Content",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
Title = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "text", nullable: false),
Urls = table.Column<string[]>(type: "text[]", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Contents", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Creators",
schema: "Content",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
Name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
About_Title = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
About_Description = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
SocialNetworks_FacebookUrl = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
SocialNetworks_InstagramUrl = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
SocialNetworks_XUrl = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
SocialNetworks_LinkedInUrl = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
SocialNetworks_TikTokUrl = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
SocialNetworks_YoutubeUrl = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
SocialNetworks_RedditUrl = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
SocialNetworks_WebsiteUrl = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
ProfileColors_BannerTop = table.Column<string>(type: "character varying(9)", maxLength: 9, nullable: true),
ProfileColors_BannerBottom = table.Column<string>(type: "character varying(9)", maxLength: 9, nullable: true),
ProfileColors_Accent = table.Column<string>(type: "character varying(9)", maxLength: 9, nullable: true),
ProfileColors_Menu = table.Column<string>(type: "character varying(9)", maxLength: 9, nullable: true),
StoredDataUrls_BannerPictureUrl = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
StoredDataUrls_ProfilePictureUrl = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Creators", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Contents",
schema: "Content");
migrationBuilder.DropTable(
name: "Creators",
schema: "Content");
}
}
}

View File

@@ -8,7 +8,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Hutopy.Web.Contents.Migrations
namespace Hutopy.Web.Features.Contents.Migrations
{
[DbContext(typeof(ContentDbContext))]
partial class ContentDbContextModelSnapshot : ModelSnapshot
@@ -38,9 +38,11 @@ namespace Hutopy.Web.Contents.Migrations
.HasColumnType("uuid");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.Property<string[]>("Urls")
@@ -50,6 +52,159 @@ namespace Hutopy.Web.Contents.Migrations
b.ToTable("Contents", "Content");
});
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.ToTable("Creators", "Content");
});
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>
{
b.OwnsOne("Hutopy.Web.Features.Contents.Data.About", "About", b1 =>
{
b1.Property<Guid>("CreatorId")
.HasColumnType("uuid");
b1.Property<string>("Description")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("Title")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.HasKey("CreatorId");
b1.ToTable("Creators", "Content");
b1.WithOwner()
.HasForeignKey("CreatorId");
});
b.OwnsOne("Hutopy.Web.Features.Contents.Data.ProfileColors", "ProfileColors", b1 =>
{
b1.Property<Guid>("CreatorId")
.HasColumnType("uuid");
b1.Property<string>("Accent")
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("BannerBottom")
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("BannerTop")
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.Property<string>("Menu")
.HasMaxLength(9)
.HasColumnType("character varying(9)");
b1.HasKey("CreatorId");
b1.ToTable("Creators", "Content");
b1.WithOwner()
.HasForeignKey("CreatorId");
});
b.OwnsOne("Hutopy.Web.Features.Contents.Data.SocialNetworks", "SocialNetworks", b1 =>
{
b1.Property<Guid>("CreatorId")
.HasColumnType("uuid");
b1.Property<string>("FacebookUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("InstagramUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("LinkedInUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("RedditUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("TikTokUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("WebsiteUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("XUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("YoutubeUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.HasKey("CreatorId");
b1.ToTable("Creators", "Content");
b1.WithOwner()
.HasForeignKey("CreatorId");
});
b.OwnsOne("Hutopy.Web.Features.Contents.Data.StoredDataUrls", "StoredDataUrls", b1 =>
{
b1.Property<Guid>("CreatorId")
.HasColumnType("uuid");
b1.Property<string>("BannerPictureUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.Property<string>("ProfilePictureUrl")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b1.HasKey("CreatorId");
b1.ToTable("Creators", "Content");
b1.WithOwner()
.HasForeignKey("CreatorId");
});
b.Navigation("About")
.IsRequired();
b.Navigation("ProfileColors")
.IsRequired();
b.Navigation("SocialNetworks")
.IsRequired();
b.Navigation("StoredDataUrls")
.IsRequired();
});
#pragma warning restore 612, 618
}
}

View File

@@ -49,8 +49,8 @@ public class AddMessage(
CreatedByName = User.GetAlias() ?? $"{User.GetFirstName()} {User.GetLastName()}",
CreatedByPortraitUrl = User.GetPortraitUrl(),
Value = req.Message
};
};
await context.Messages.AddAsync(message, ct);
await context.SaveChangesAsync(ct);

View File

@@ -1,67 +0,0 @@
// <auto-generated />
using System;
using Hutopy.Web.Features.Messages.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Hutopy.Web.Features.Messages.Migrations
{
[DbContext(typeof(MessagingDbContext))]
[Migration("20240721041322_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Messaging")
.HasAnnotation("ProductVersion", "8.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Hutopy.Web.Features.Messages.Data.Message", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");
b.Property<string>("CreatedByName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("CreatedByPortraitUrl")
.HasColumnType("text");
b.Property<Guid?>("ParentId")
.HasColumnType("uuid");
b.Property<Guid>("SubjectId")
.HasColumnType("uuid");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Messages", "Messaging");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,60 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Hutopy.Web.Features.Messages.Migrations
{
/// <inheritdoc />
public partial class ChangedAuthorDefinition : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "CreatedByPortraitUrl",
schema: "Messaging",
table: "Messages",
type: "character varying(256)",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "CreatedByName",
schema: "Messaging",
table: "Messages",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "CreatedByPortraitUrl",
schema: "Messaging",
table: "Messages",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "CreatedByName",
schema: "Messaging",
table: "Messages",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
}
}
}

View File

@@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Hutopy.Web.Features.Messages.Migrations
{
[DbContext(typeof(MessagingDbContext))]
[Migration("20240721064224_ChangedAuthorDefinition")]
partial class ChangedAuthorDefinition
[Migration("20240802044717_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)

View File

@@ -22,8 +22,8 @@ namespace Hutopy.Web.Features.Messages.Migrations
Id = table.Column<Guid>(type: "uuid", nullable: false),
SubjectId = table.Column<Guid>(type: "uuid", nullable: false),
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
CreatedByName = table.Column<string>(type: "text", nullable: false),
CreatedByPortraitUrl = table.Column<string>(type: "text", nullable: true),
CreatedByName = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
CreatedByPortraitUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
ParentId = table.Column<Guid>(type: "uuid", nullable: true),
Value = table.Column<string>(type: "text", nullable: false)

View File

@@ -1,6 +1,5 @@
using System.Security.Claims;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Web.Common;
namespace Hutopy.Web.Services;
@@ -8,5 +7,5 @@ public class CurrentUser(
IHttpContextAccessor httpContextAccessor)
: IUser
{
public string? Id => httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
public Guid? Id => httpContextAccessor.HttpContext?.User.GetUserId();
}

View File

@@ -1,6 +1,5 @@
using Hutopy.Domain.Constants;
using Hutopy.Infrastructure.Identity;
using Hutopy.Infrastructure.Identity.OwnedEntities;
using Hutopy.Web.Common;
using Hutopy.Web.Features.Contents.Data;
using Hutopy.Web.Features.Messages.Data;
@@ -38,10 +37,14 @@ internal class TestDataSeeder(
_users.Add(await CreateUserAsync("admin", Roles.Administrator));
_users.Add(await CreateUserAsync("userA"));
_users.Add(await CreateUserAsync("userB"));
foreach (var creator in _creators)
{
_users.Add(await CreateCreatorAsync(creator));
var creatorUser = await CreateUserAsync(creator.Name, Roles.Creator);
creator.Id = creatorUser.Id;
await contentContext.Creators.AddAsync(creator);
var contents = GenerateContent(creator, 100);
foreach (var content in contents)
@@ -59,7 +62,7 @@ internal class TestDataSeeder(
}
}
private List<Content> GenerateContent(ApplicationUser user, int contentCount)
private List<Content> GenerateContent(Creator creator, int contentCount)
{
var currentDate = DateTimeOffset.UtcNow;
@@ -70,10 +73,10 @@ internal class TestDataSeeder(
var content = new Content
{
Id = GuidHelper.GenerateUuidV7(),
CreatedBy = Guid.Parse(user.Id),
CreatedBy = creator.Id,
CreatedAt = currentDate,
Title = $"Title {user.UserName}-{c}",
Description = $"Description {user.UserName}-{c}"
Title = $"Title {creator.Name}-{c}",
Description = $"Description {creator.Name}-{c}"
};
contentContext.Contents.Add(content);
@@ -103,9 +106,9 @@ internal class TestDataSeeder(
Id = GuidHelper.GenerateUuidV7(),
SubjectId = content.Id,
CreatedAt = currentDate,
CreatedBy = Guid.Parse(author.Id),
CreatedBy = author.Id,
CreatedByName = author.Alias ?? $"{author.FirstName} {author.LastName}",
CreatedByPortraitUrl = author.StoredDataUrls.ProfilePictureUrl,
CreatedByPortraitUrl = author.PortraitUrl,
Value = $"Message #{m} on {content.Title}"
};
@@ -133,9 +136,9 @@ internal class TestDataSeeder(
Id = GuidHelper.GenerateUuidV7(),
SubjectId = content.Id,
ParentId = parent.Id,
CreatedBy = Guid.Parse(author.Id),
CreatedBy = author.Id,
CreatedByName = author.Alias ?? $"{author.FirstName} {author.LastName}",
CreatedByPortraitUrl = author.StoredDataUrls.ProfilePictureUrl,
CreatedByPortraitUrl = author.PortraitUrl,
CreatedAt = currentDate,
Value = $"Reply {r} to {parent.Value} on {content.Title}"
};
@@ -167,49 +170,38 @@ internal class TestDataSeeder(
return user;
}
private async Task<ApplicationUser> CreateCreatorAsync(ApplicationUser creator)
{
await userManager.CreateAsync(creator, DefaultPassword);
await userManager.AddToRolesAsync(creator, new[] { Roles.Creator });
return creator;
}
private readonly List<ApplicationUser> _users =
[
];
private readonly static ApplicationUser Hutopy = new()
private readonly static Creator HutopyCreator = new()
{
UserName = "hutopy@test",
Email = "hutopy@test",
EmailConfirmed = true,
CreatorAlias = "hutopy",
FirstName = "FirstName of a Brand/Creator",
LastName = "LastName of a Brand/Creator",
About = "Page officielle",
Description = "Site officiel pour Hutopy. Venez-nous-y retrouver avec tous vos fans!",
ProfileColors = new ProfileColors
Name = "hutopy",
About = new()
{
BannerTop = "A30E79", BannerBottom = "6B0065", Accent = "23393B", Menu = "53B93B",
Title = "Page officielle",
Description = "Site officiel pour Hutopy. Venez-nous-y retrouver avec tous vos fans!",
},
ProfileColors = new()
{
BannerTop = "#A30E79", BannerBottom = "#6B0065", Accent = "#23393B", Menu = "#53B93B",
},
SocialNetworks =
new SocialNetworks
new()
{
XUrl = "https://twitter.com/Hutopyinc",
FacebookUrl = "https://www.facebook.com/Hutopy",
InstagramUrl = "https://www.instagram.com/hutopy.inc/"
},
StoredDataUrls = new StoredDataUrls
StoredDataUrls = new()
{
BannerPictureUrl = "/images/usersmedia/HutopyProfile/banners/banner01.png",
ProfilePictureUrl = "/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png"
}
};
private readonly ApplicationUser[] _creators =
private readonly Creator[] _creators =
[
Hutopy
HutopyCreator
];
}

View File

@@ -32,4 +32,9 @@
<PackageReference Include="FluentValidation.AspNetCore" />
</ItemGroup>
<ItemGroup>
<Folder Include="Features\Contents\Migrations\" />
<Folder Include="Features\Messages\Migrations\" />
</ItemGroup>
</Project>