chore(codebase): full cleanup pass
This commit is contained in:
@@ -5,7 +5,7 @@ namespace Hutopy.Modules.Contents.Data;
|
||||
|
||||
public class Album : Entity
|
||||
{
|
||||
public bool IsDeleted { get; private set; } // private set → EF updates it
|
||||
public bool IsDeleted { get; private set; } // private set → EF updates it
|
||||
[MaxLength(255)] public required string Title { get; set; }
|
||||
public IList<AlbumPhoto> Photos { get; set; } = new List<AlbumPhoto>();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public class ContentsDbContext(
|
||||
modelBuilder
|
||||
.Entity<Album>()
|
||||
.Property(c => c.IsDeleted)
|
||||
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", stored: true);
|
||||
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
|
||||
|
||||
modelBuilder
|
||||
.Entity<Album>()
|
||||
@@ -40,7 +40,7 @@ public class ContentsDbContext(
|
||||
modelBuilder
|
||||
.Entity<AlbumPhoto>()
|
||||
.Property(c => c.IsDeleted)
|
||||
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", stored: true);
|
||||
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
|
||||
|
||||
modelBuilder
|
||||
.Entity<AlbumPhoto>()
|
||||
|
||||
@@ -17,9 +17,9 @@ public static class DependencyInjection
|
||||
this IApplicationBuilder app,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
await using var context = scope.ServiceProvider.GetRequiredService<ContentsDbContext>();
|
||||
IServiceScopeFactory scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||
using IServiceScope scope = scopeFactory.CreateScope();
|
||||
await using ContentsDbContext context = scope.ServiceProvider.GetRequiredService<ContentsDbContext>();
|
||||
await context.Database.MigrateAsync(cancellationToken);
|
||||
|
||||
return app;
|
||||
|
||||
@@ -23,6 +23,7 @@ public record AddPhotoToAlbumResponse(
|
||||
public sealed class AddPhotoToAlbumRequestValidator : Validator<AddPhotoToAlbumRequest>
|
||||
{
|
||||
private const int MaxFileSizeBytes = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
private static readonly string[] AllowedImageTypes =
|
||||
[
|
||||
"image/jpeg",
|
||||
@@ -74,14 +75,14 @@ public class AddPhotoToAlbumHandler(
|
||||
AddPhotoToAlbumRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
Guid userId = User.GetUserId();
|
||||
|
||||
// Fetch the album we want to add photos to
|
||||
var album = await context
|
||||
Album? album = await context
|
||||
.Albums
|
||||
.SingleOrDefaultAsync(
|
||||
a => a.Id == request.AlbumId && a.CreatedBy == userId,
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
if (album is null)
|
||||
{
|
||||
@@ -90,7 +91,7 @@ public class AddPhotoToAlbumHandler(
|
||||
}
|
||||
|
||||
// Check if a photo with the same ID already exists
|
||||
var existingPhoto = await context
|
||||
bool existingPhoto = await context
|
||||
.AlbumPhotos
|
||||
.AnyAsync(p => p.Id == request.PhotoId, ct);
|
||||
|
||||
@@ -102,16 +103,16 @@ public class AddPhotoToAlbumHandler(
|
||||
|
||||
try
|
||||
{
|
||||
var (originalUrl, thumbnailUrl) = await ProcessAndUploadImage(request, ct);
|
||||
(string originalUrl, string thumbnailUrl) = await ProcessAndUploadImage(request, ct);
|
||||
|
||||
// Get the next order number
|
||||
var nextOrder = await context
|
||||
int nextOrder = await context
|
||||
.AlbumPhotos
|
||||
.Where(p => p.AlbumId == request.AlbumId)
|
||||
.MaxAsync(p => (int?)p.Order, ct) ?? 0;
|
||||
|
||||
// Create the album photo
|
||||
var photo = new AlbumPhoto
|
||||
AlbumPhoto photo = new()
|
||||
{
|
||||
Id = request.PhotoId,
|
||||
CreatedBy = userId,
|
||||
@@ -143,23 +144,23 @@ public class AddPhotoToAlbumHandler(
|
||||
AddPhotoToAlbumRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var originalFileName = Path.GetFileName(request.File.FileName);
|
||||
var nameWithoutExt = Path.GetFileNameWithoutExtension(originalFileName);
|
||||
var extension = Path.GetExtension(originalFileName);
|
||||
string originalFileName = Path.GetFileName(request.File.FileName);
|
||||
string nameWithoutExt = Path.GetFileNameWithoutExtension(originalFileName);
|
||||
string extension = Path.GetExtension(originalFileName);
|
||||
|
||||
var filenameOriginal = $"{nameWithoutExt}{extension}";
|
||||
var filenameThumbnail = $"{nameWithoutExt}.thumbnail{extension}";
|
||||
string filenameOriginal = $"{nameWithoutExt}{extension}";
|
||||
string filenameThumbnail = $"{nameWithoutExt}.thumbnail{extension}";
|
||||
|
||||
var blobOriginal = $"{SubDirectoryNames.Albums}/{request.AlbumId}/{filenameOriginal}";
|
||||
var blobThumbnail = $"{SubDirectoryNames.Albums}/{request.AlbumId}/{filenameThumbnail}";
|
||||
string blobOriginal = $"{SubDirectoryNames.Albums}/{request.AlbumId}/{filenameOriginal}";
|
||||
string blobThumbnail = $"{SubDirectoryNames.Albums}/{request.AlbumId}/{filenameThumbnail}";
|
||||
|
||||
// Process the original image
|
||||
await using var originalStream = request.File.OpenReadStream();
|
||||
using var image = await Image.LoadAsync(originalStream, ct);
|
||||
await using Stream originalStream = request.File.OpenReadStream();
|
||||
using Image image = await Image.LoadAsync(originalStream, ct);
|
||||
|
||||
// Calculate target size while preserving the original aspect ratio
|
||||
var originalWidth = image.Width;
|
||||
var originalHeight = image.Height;
|
||||
int originalWidth = image.Width;
|
||||
int originalHeight = image.Height;
|
||||
|
||||
double ratioX = (double)MaxThumbnailWidth / originalWidth;
|
||||
double ratioY = (double)MaxThumbnailHeight / originalHeight;
|
||||
@@ -169,20 +170,20 @@ public class AddPhotoToAlbumHandler(
|
||||
int newHeight = (int)(originalHeight * ratio);
|
||||
|
||||
// Create thumbnail
|
||||
using var thumbnailStream = new MemoryStream();
|
||||
using MemoryStream thumbnailStream = new();
|
||||
image.Mutate(x => x.Resize(newWidth, newHeight));
|
||||
await image.SaveAsync(thumbnailStream, image.Metadata.DecodedImageFormat!, ct);
|
||||
thumbnailStream.Position = 0;
|
||||
|
||||
// Upload both versions
|
||||
var originalUrl = await blobStorage.UploadFileAsync(
|
||||
string originalUrl = await blobStorage.UploadFileAsync(
|
||||
ContainerNames.Creators,
|
||||
blobOriginal,
|
||||
request.File.OpenReadStream(),
|
||||
request.File.ContentType,
|
||||
ct);
|
||||
|
||||
var thumbnailUrl = await blobStorage.UploadFileAsync(
|
||||
string thumbnailUrl = await blobStorage.UploadFileAsync(
|
||||
ContainerNames.Creators,
|
||||
blobThumbnail,
|
||||
thumbnailStream,
|
||||
|
||||
@@ -48,7 +48,7 @@ public class CreateAlbumHandler(
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Check if an album with the same ID already exists
|
||||
var existingAlbum = await context
|
||||
bool existingAlbum = await context
|
||||
.Albums
|
||||
.AnyAsync(a => a.Id == request.AlbumId, ct);
|
||||
|
||||
@@ -58,12 +58,7 @@ public class CreateAlbumHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
var album = new Album
|
||||
{
|
||||
Id = request.AlbumId,
|
||||
CreatedBy = User.GetUserId(),
|
||||
Title = request.Title
|
||||
};
|
||||
Album album = new() { Id = request.AlbumId, CreatedBy = User.GetUserId(), Title = request.Title };
|
||||
|
||||
context.Albums.Add(album);
|
||||
await context.SaveChangesAsync(ct);
|
||||
@@ -72,4 +67,4 @@ public class CreateAlbumHandler(
|
||||
new CreateAlbumResponse(album.Id),
|
||||
ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,12 +49,12 @@ public class GetAlbumHandler(
|
||||
GetAlbumRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var album = await context
|
||||
Album? album = await context
|
||||
.Albums
|
||||
.Include(a => a.Photos.OrderBy(p => p.Order))
|
||||
.SingleOrDefaultAsync(
|
||||
a => a.Id == request.AlbumId,
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
if (album is null)
|
||||
{
|
||||
@@ -62,7 +62,7 @@ public class GetAlbumHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
var photos = album.Photos
|
||||
List<AlbumPhotoDto> photos = album.Photos
|
||||
.Select(p => new AlbumPhotoDto(
|
||||
p.Id,
|
||||
p.OriginalUrl,
|
||||
|
||||
@@ -33,14 +33,14 @@ public class RemoveAlbumHandler(
|
||||
RemoveAlbumRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
Guid userId = User.GetUserId();
|
||||
|
||||
var album = await context
|
||||
Album? album = await context
|
||||
.Albums
|
||||
.Include(a => a.Photos)
|
||||
.SingleOrDefaultAsync(
|
||||
a => a.Id == request.AlbumId && a.CreatedBy == userId,
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
if (album is null)
|
||||
{
|
||||
@@ -53,7 +53,7 @@ public class RemoveAlbumHandler(
|
||||
album.DeletedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
// Soft delete all photos in the album
|
||||
foreach (var photo in album.Photos)
|
||||
foreach (AlbumPhoto photo in album.Photos)
|
||||
{
|
||||
photo.DeletedBy = userId;
|
||||
photo.DeletedAt = DateTimeOffset.UtcNow;
|
||||
@@ -63,4 +63,4 @@ public class RemoveAlbumHandler(
|
||||
|
||||
await SendNoContentAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,14 +38,14 @@ public class RemovePhotoFromAlbumHandler(
|
||||
RemovePhotoFromAlbumRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
Guid userId = User.GetUserId();
|
||||
|
||||
var album = await context
|
||||
Album? album = await context
|
||||
.Albums
|
||||
.Include(a => a.Photos)
|
||||
.SingleOrDefaultAsync(
|
||||
a => a.Id == request.AlbumId && a.CreatedBy == userId,
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
if (album is null)
|
||||
{
|
||||
@@ -53,7 +53,7 @@ public class RemovePhotoFromAlbumHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
var photo = album.Photos
|
||||
AlbumPhoto? photo = album.Photos
|
||||
.SingleOrDefault(p => p.Id == request.PhotoId);
|
||||
|
||||
if (photo is null)
|
||||
@@ -65,9 +65,9 @@ public class RemovePhotoFromAlbumHandler(
|
||||
// Soft delete the photo
|
||||
photo.DeletedBy = userId;
|
||||
photo.DeletedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
|
||||
await context.SaveChangesAsync(ct);
|
||||
|
||||
await SendNoContentAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ public class ContentModel
|
||||
public required string CreatedByName { get; init; }
|
||||
public required string? CreatedByPortraitUrl { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public Guid? DeletedBy { get; init; }
|
||||
public Guid? DeletedBy { get; init; }
|
||||
public DateTimeOffset? DeletedAt { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public required string Description { get; init; }
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
public class CreatorOptions
|
||||
{
|
||||
public const string ConfigurationSection = "Creators";
|
||||
|
||||
|
||||
public TimeSpan SlugReservationDuration { get; set; }
|
||||
}
|
||||
|
||||
@@ -5,16 +5,16 @@ namespace Hutopy.Modules.Creators.Data;
|
||||
public class Creator
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid CreatedBy { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
public Guid CreatedBy { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public Guid? DeletedBy { get; set; }
|
||||
public DateTimeOffset? DeletedAt { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Soft‑delete flag (false by default, true once DeletedAt is set)
|
||||
/// Soft‑delete flag (false by default, true once DeletedAt is set)
|
||||
/// </summary>
|
||||
public bool IsDeleted { get; private set; } // private set → EF updates it
|
||||
public bool IsDeleted { get; private set; } // private set → EF updates it
|
||||
|
||||
[MaxLength(2048)] public string? BannerUrl { get; set; }
|
||||
[MaxLength(2048)] public string? PortraitUrl { get; set; }
|
||||
|
||||
@@ -17,7 +17,7 @@ public class CreatorsDbContext(
|
||||
modelBuilder
|
||||
.Entity<Slugs>()
|
||||
.Property(x => x.NormalizedName)
|
||||
.HasComputedColumnSql("LOWER(\"Name\")", stored: true);
|
||||
.HasComputedColumnSql("LOWER(\"Name\")", true);
|
||||
|
||||
modelBuilder
|
||||
.Entity<Slugs>()
|
||||
@@ -27,7 +27,7 @@ public class CreatorsDbContext(
|
||||
modelBuilder
|
||||
.Entity<Creator>()
|
||||
.Property(c => c.IsDeleted)
|
||||
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", stored: true); // bool
|
||||
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true); // bool
|
||||
|
||||
modelBuilder
|
||||
.Entity<Creator>()
|
||||
@@ -38,7 +38,7 @@ public class CreatorsDbContext(
|
||||
.Entity<Creator>()
|
||||
.OwnsOne<Presentation>(x => x.Presentation)
|
||||
.ToTable(nameof(Presentation));
|
||||
|
||||
|
||||
modelBuilder
|
||||
.Entity<Creator>()
|
||||
.HasQueryFilter(c => !c.IsDeleted);
|
||||
|
||||
@@ -12,4 +12,4 @@ public class Socials
|
||||
[MaxLength(2048)] public string? YoutubeUrl { get; set; }
|
||||
[MaxLength(2048)] public string? RedditUrl { get; set; }
|
||||
[MaxLength(2048)] public string? WebsiteUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,21 +14,21 @@ public static class DependencyInjection
|
||||
builder.Services.Configure<CreatorOptions>(
|
||||
builder.Configuration.GetSection(CreatorOptions.ConfigurationSection));
|
||||
builder.Services.AddScoped<SlugPurger>();
|
||||
|
||||
|
||||
builder.Services.AddDbContext<CreatorsDbContext>(configureAction);
|
||||
builder.Services.AddTransient<ICreatorLookup, CreatorLookup>();
|
||||
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
public static async Task<IApplicationBuilder> UseCreatorModuleAsync(
|
||||
this IApplicationBuilder app,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
await using var context = scope.ServiceProvider.GetRequiredService<CreatorsDbContext>();
|
||||
await context.Database.MigrateAsync(cancellationToken: cancellationToken);
|
||||
IServiceScopeFactory scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||
using IServiceScope scope = scopeFactory.CreateScope();
|
||||
await using CreatorsDbContext context = scope.ServiceProvider.GetRequiredService<CreatorsDbContext>();
|
||||
await context.Database.MigrateAsync(cancellationToken);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ public static class ChangeBanner
|
||||
Request request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var creator = await context
|
||||
Creator? creator = await context
|
||||
.Creators
|
||||
.SingleOrDefaultAsync(
|
||||
c => c.Id == request.CreatorId,
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
if (creator is null)
|
||||
{
|
||||
@@ -41,7 +41,7 @@ public static class ChangeBanner
|
||||
return;
|
||||
}
|
||||
|
||||
var blobUrl = await blobStorage.UploadFileAsync(
|
||||
string blobUrl = await blobStorage.UploadFileAsync(
|
||||
ContainerNames.Creators,
|
||||
$"{request.CreatorId}/{SubDirectoryNames.Profile}/{CommonFileNames.BannerPicture}",
|
||||
request.File.OpenReadStream(),
|
||||
|
||||
@@ -38,12 +38,12 @@ public class ChangeEmailHandler(
|
||||
ChangeEmailRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var creator = await context
|
||||
Creator? creator = await context
|
||||
.Creators
|
||||
.Include(c => c.Presentation)
|
||||
.SingleOrDefaultAsync(
|
||||
c => c.Id == request.CreatorId,
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
if (creator is null)
|
||||
{
|
||||
@@ -59,9 +59,9 @@ public class ChangeEmailHandler(
|
||||
}
|
||||
|
||||
creator.Presentation.Email = request.Email?.Trim();
|
||||
|
||||
|
||||
await context.SaveChangesAsync(ct);
|
||||
|
||||
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +44,11 @@ public class ChangeLogoHandler(
|
||||
ChangeLogoRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var creator = await context
|
||||
Creator? creator = await context
|
||||
.Creators
|
||||
.SingleOrDefaultAsync(
|
||||
c => c.Id == request.CreatorId,
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
if (creator is null)
|
||||
{
|
||||
@@ -56,7 +56,7 @@ public class ChangeLogoHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
var blobUrl = await blobStorage.UploadFileAsync(
|
||||
string blobUrl = await blobStorage.UploadFileAsync(
|
||||
ContainerNames.Creators,
|
||||
$"{request.CreatorId}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}",
|
||||
request.File.OpenReadStream(),
|
||||
|
||||
@@ -34,11 +34,11 @@ public class ChangeNameHandler(
|
||||
ChangeNameRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var creator = await context
|
||||
Creator creator = await context
|
||||
.Creators
|
||||
.SingleAsync(
|
||||
c => c.Id == request.CreatorId,
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
creator.Name = request.Name;
|
||||
|
||||
|
||||
@@ -38,12 +38,12 @@ public class ChangePhoneNumberHandler(
|
||||
ChangePhoneNumberRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var creator = await context
|
||||
Creator? creator = await context
|
||||
.Creators
|
||||
.Include(c => c.Presentation)
|
||||
.SingleOrDefaultAsync(
|
||||
c => c.Id == request.CreatorId,
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
if (creator is null)
|
||||
{
|
||||
@@ -59,9 +59,9 @@ public class ChangePhoneNumberHandler(
|
||||
}
|
||||
|
||||
creator.Presentation.PhoneNumber = request.PhoneNumber?.Trim();
|
||||
|
||||
|
||||
await context.SaveChangesAsync(ct);
|
||||
|
||||
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +45,12 @@ public class ChangePresentationInfosHandler(
|
||||
ChangePresentationInfosRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var creator = await context
|
||||
Creator? creator = await context
|
||||
.Creators
|
||||
.Include(c => c.Presentation)
|
||||
.SingleOrDefaultAsync(
|
||||
c => c.Id == request.CreatorId,
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
if (creator is null)
|
||||
{
|
||||
@@ -60,12 +60,12 @@ public class ChangePresentationInfosHandler(
|
||||
|
||||
// Update the presentation info with the new values
|
||||
creator.Presentation.Description = request.Description.Trim();
|
||||
creator.Presentation.VideoUrl = request.VideoUrl != null
|
||||
creator.Presentation.VideoUrl = request.VideoUrl != null
|
||||
? YouTubeUrlHelper.ExtractVideoId(request.VideoUrl.Trim())
|
||||
: null;
|
||||
|
||||
|
||||
await context.SaveChangesAsync(ct);
|
||||
|
||||
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
namespace Hutopy.Modules.Creators.Features;
|
||||
|
||||
@@ -17,7 +18,7 @@ internal sealed class ChangeSlugRequestValidator
|
||||
RuleFor(r => r.CreatorId)
|
||||
.NotNull().WithMessage("You should specify the CreatorId")
|
||||
.NotEmpty().WithMessage("You should specify a valid/not empty CreatorId");
|
||||
|
||||
|
||||
RuleFor(r => r.SlugReservationId)
|
||||
.NotNull().WithMessage("You should specify the SlugReservationId")
|
||||
.NotEmpty().WithMessage("You should specify a valid/not empty SlugReservationId");
|
||||
@@ -39,15 +40,15 @@ public class ChangeSlugHandler(
|
||||
ChangeSlugRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await using var transaction = await context.Database.BeginTransactionAsync(ct);
|
||||
await using IDbContextTransaction transaction = await context.Database.BeginTransactionAsync(ct);
|
||||
|
||||
try
|
||||
{
|
||||
var creator = await context
|
||||
Creator creator = await context
|
||||
.Creators
|
||||
.SingleAsync(
|
||||
c => c.Id == request.CreatorId,
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
if (creator.CreatedBy != User.GetUserId())
|
||||
{
|
||||
@@ -55,7 +56,7 @@ public class ChangeSlugHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
var reservation = await context
|
||||
Slugs? reservation = await context
|
||||
.Slugs
|
||||
.FirstOrDefaultAsync(
|
||||
s => s.Id == request.SlugReservationId,
|
||||
@@ -67,7 +68,7 @@ public class ChangeSlugHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
var previousReservation = await context
|
||||
Slugs? previousReservation = await context
|
||||
.Slugs
|
||||
.FirstOrDefaultAsync(
|
||||
s => s.UsedBy == request.CreatorId,
|
||||
|
||||
@@ -27,12 +27,12 @@ public class ChangeSocialsHandler(
|
||||
|
||||
public override async Task HandleAsync(ChangeSocialsRequest request, CancellationToken ct)
|
||||
{
|
||||
var creator = await context
|
||||
Creator creator = await context
|
||||
.Creators
|
||||
.Include(c => c.Socials)
|
||||
.SingleAsync(
|
||||
c => c.Id == request.CreatorId,
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
creator.Socials.FacebookUrl = request.FacebookUrl;
|
||||
creator.Socials.InstagramUrl = request.InstagramUrl;
|
||||
|
||||
@@ -19,14 +19,14 @@ public class ChangeTitleHandler(
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
ChangeTitleRequest request,
|
||||
ChangeTitleRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var creator = await context
|
||||
Creator creator = await context
|
||||
.Creators
|
||||
.SingleAsync(
|
||||
c => c.Id == request.CreatorId,
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
creator.Title = request.Title;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
namespace Hutopy.Modules.Creators.Features;
|
||||
|
||||
@@ -17,7 +18,7 @@ public sealed class CreateCreatorRequestValidator : Validator<CreateCreatorReque
|
||||
.NotNull()
|
||||
.NotEmpty()
|
||||
.WithMessage("You should specify a valid SlugReservationId");
|
||||
|
||||
|
||||
RuleFor(r => r.CreatorId)
|
||||
.NotNull()
|
||||
.NotEmpty()
|
||||
@@ -40,11 +41,11 @@ public sealed class CreateCreatorHandler(
|
||||
CreateCreatorRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await using var transaction = await context.Database.BeginTransactionAsync(ct);
|
||||
|
||||
await using IDbContextTransaction transaction = await context.Database.BeginTransactionAsync(ct);
|
||||
|
||||
try
|
||||
{
|
||||
var slug = await context
|
||||
Slugs slug = await context
|
||||
.Slugs
|
||||
.SingleAsync(s => s.Id == req.SlugReservationId, ct);
|
||||
|
||||
@@ -55,23 +56,20 @@ public sealed class CreateCreatorHandler(
|
||||
await SendErrorsAsync(500, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
slug.UsedBy = req.CreatorId;
|
||||
|
||||
|
||||
await context.Creators.AddAsync(
|
||||
new Creator
|
||||
{
|
||||
Id = req.CreatorId,
|
||||
CreatedBy = User.GetUserId(),
|
||||
Name = slug.Name,
|
||||
Slug = slug.NormalizedName
|
||||
Id = req.CreatorId, CreatedBy = User.GetUserId(), Name = slug.Name, Slug = slug.NormalizedName
|
||||
},
|
||||
ct);
|
||||
|
||||
await context.SaveChangesAsync(ct);
|
||||
|
||||
await transaction.CommitAsync(ct);
|
||||
|
||||
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
catch (Exception)
|
||||
|
||||
@@ -28,7 +28,7 @@ public class GetCreatorByIdHandler(
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/creators/{CreatorId}");
|
||||
Options((o => o.WithTags("Creators")));
|
||||
Options(o => o.WithTags("Creators"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
@@ -36,13 +36,19 @@ public class GetCreatorByIdHandler(
|
||||
GetCreatorByIdRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var creator = await context
|
||||
Creator? creator = await context
|
||||
.Creators
|
||||
.FindAsync(
|
||||
[req.CreatorId],
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
if (creator is null) await SendNotFoundAsync(ct);
|
||||
else await SendAsync(creator, cancellation: ct);
|
||||
if (creator is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendAsync(creator, cancellation: ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public class GetCreatorBySlugHandler(
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/creators/@{Name}");
|
||||
Options((o => o.WithTags("Creators")));
|
||||
Options(o => o.WithTags("Creators"));
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@ public class GetCreatorBySlugHandler(
|
||||
GetCreatorBySlugRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var creatorName = req.Name.ToLower();
|
||||
string creatorName = req.Name.ToLower();
|
||||
|
||||
var response = await context
|
||||
GetCreatorBySlugResponse? response = await context
|
||||
.Creators
|
||||
.IgnoreQueryFilters()
|
||||
.Where(c => EF.Functions.ILike(c.Slug, creatorName))
|
||||
|
||||
@@ -34,12 +34,12 @@ public sealed class RemoveCreatorHandler(
|
||||
RemoveCreatorRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var creatorSlug = req.CreatorSlug.ToLower();
|
||||
string creatorSlug = req.CreatorSlug.ToLower();
|
||||
|
||||
var creator = await context
|
||||
Creator? creator = await context
|
||||
.Creators
|
||||
.Where(c => EF.Functions.ILike(c.Slug, creatorSlug))
|
||||
.SingleOrDefaultAsync(cancellationToken: ct);
|
||||
.SingleOrDefaultAsync(ct);
|
||||
|
||||
if (creator is null)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Configuration;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
using Hutopy.Modules.Creators.Services;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
|
||||
@@ -45,33 +46,31 @@ public sealed class ReserveSlug(
|
||||
ReserveSlugRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await using var transaction = await context.Database.BeginTransactionAsync(ct);
|
||||
await using IDbContextTransaction transaction = await context.Database.BeginTransactionAsync(ct);
|
||||
|
||||
try
|
||||
{
|
||||
// First, purge any expired slugs
|
||||
await slugPurger.PurgeExpiredSlugsAsync(ct);
|
||||
|
||||
var reservation = await context.Slugs.FirstOrDefaultAsync(
|
||||
s => s.Id == req.ReservationId && s.CreatedBy == User.GetUserId(),
|
||||
cancellationToken: ct);
|
||||
Slugs? reservation = await context.Slugs.FirstOrDefaultAsync(
|
||||
s => s.Id == req.ReservationId && s.CreatedBy == User.GetUserId(),
|
||||
ct);
|
||||
|
||||
if (reservation == null)
|
||||
{
|
||||
reservation = new Slugs
|
||||
{
|
||||
Id = req.ReservationId,
|
||||
CreatedBy = User.GetUserId(),
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Id = req.ReservationId, CreatedBy = User.GetUserId(), CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
|
||||
context.Slugs.Attach(reservation);
|
||||
context.Entry(reservation).State = EntityState.Added;
|
||||
}
|
||||
|
||||
reservation.Name = req.Slug;
|
||||
reservation.ReservedUntil = DateTimeOffset.UtcNow + opts.Value.SlugReservationDuration;
|
||||
|
||||
|
||||
await context.SaveChangesAsync(ct);
|
||||
|
||||
await transaction.CommitAsync(ct);
|
||||
@@ -81,7 +80,7 @@ public sealed class ReserveSlug(
|
||||
catch (Exception e)
|
||||
{
|
||||
await transaction.RollbackAsync(ct);
|
||||
|
||||
|
||||
Logger.LogError("Transaction failed: {Message}", e.Message);
|
||||
|
||||
if (e.InnerException is PostgresException innerException)
|
||||
|
||||
@@ -34,13 +34,13 @@ public sealed class RestoreCreatorHandler(
|
||||
RestoreCreatorRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var creatorSlug = req.CreatorSlug.ToLower();
|
||||
string creatorSlug = req.CreatorSlug.ToLower();
|
||||
|
||||
var creator = await context
|
||||
Creator? creator = await context
|
||||
.Creators
|
||||
.IgnoreQueryFilters()
|
||||
.Where(c => EF.Functions.ILike(c.Slug, creatorSlug))
|
||||
.SingleOrDefaultAsync(cancellationToken: ct);
|
||||
.SingleOrDefaultAsync(ct);
|
||||
|
||||
if (creator is null)
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ public class SlugPurger(CreatorsDbContext context)
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
if (now - s_lastPurgeTime < MinTimeBetweenPurges)
|
||||
{
|
||||
// Not enough time has passed since the last purge
|
||||
@@ -40,4 +40,4 @@ public class SlugPurger(CreatorsDbContext context)
|
||||
Semaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
public record JwtOptions
|
||||
{
|
||||
public const string SectionName = "Authentication:Jwt";
|
||||
|
||||
|
||||
public required TimeSpan Lifetime { get; init; }
|
||||
public required string Issuer { get; init; }
|
||||
public required string Audience { get; init; }
|
||||
public required string Key { get; init; }
|
||||
|
||||
|
||||
public TimeSpan RefreshTokenLifetime { get; init; }
|
||||
public bool RefreshTokenRequireRotation { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
|
||||
namespace Hutopy.Modules.Identity.Data
|
||||
{
|
||||
public class IdentityDbContext(
|
||||
DbContextOptions<IdentityDbContext> options)
|
||||
: IdentityDbContext<User, Role, Guid>(options)
|
||||
{
|
||||
public const string SchemaName = "Identity";
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder
|
||||
modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.HasDefaultSchema(SchemaName);
|
||||
}
|
||||
namespace Hutopy.Modules.Identity.Data;
|
||||
|
||||
public class IdentityDbContext(
|
||||
DbContextOptions<IdentityDbContext> options)
|
||||
: IdentityDbContext<User, Role, Guid>(options)
|
||||
{
|
||||
public const string SchemaName = "Identity";
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder
|
||||
modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.HasDefaultSchema(SchemaName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,19 +10,22 @@ public class IdentityService(
|
||||
{
|
||||
public async Task<UserModel?> GetCurrentUserAsync()
|
||||
{
|
||||
var currentUserId = contextAccessor.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
string? currentUserId = contextAccessor.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (string.IsNullOrEmpty(currentUserId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
UserModel? ret;
|
||||
var user = await userManager.FindByIdAsync(currentUserId);
|
||||
User? user = await userManager.FindByIdAsync(currentUserId);
|
||||
|
||||
if (user == null) ret = null;
|
||||
if (user == null)
|
||||
{
|
||||
ret = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var userModel = new UserModel
|
||||
UserModel userModel = new()
|
||||
{
|
||||
Id = user.Id,
|
||||
Username = user.UserName ?? string.Empty,
|
||||
@@ -44,17 +47,22 @@ public class IdentityService(
|
||||
|
||||
public async Task<IList<string>> GetCurrentUserRolesAsync()
|
||||
{
|
||||
var currentUserModel = await GetCurrentUserAsync();
|
||||
UserModel? currentUserModel = await GetCurrentUserAsync();
|
||||
|
||||
if (currentUserModel is null) return [];
|
||||
|
||||
var currentUser = await userManager.FindByIdAsync(currentUserModel.Id.ToString());
|
||||
if (currentUserModel is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (currentUser is null) return [];
|
||||
User? currentUser = await userManager.FindByIdAsync(currentUserModel.Id.ToString());
|
||||
|
||||
var userRoles = await userManager.GetRolesAsync(currentUser);
|
||||
if (currentUser is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
IList<string> userRoles = await userManager.GetRolesAsync(currentUser);
|
||||
|
||||
return userRoles;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,4 +17,3 @@ public class User : IdentityUser<Guid>
|
||||
public DateTime RefreshTokenExpiryTime { get; set; }
|
||||
public string Fullname => $"{Lastname}, {Firstname}";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
@@ -22,7 +23,7 @@ public class ChangeAddressHandler(
|
||||
ChangeAddressRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
@@ -32,11 +33,15 @@ public class ChangeAddressHandler(
|
||||
|
||||
user.Address = request.Address;
|
||||
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
IdentityResult result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendUnauthorizedAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
@@ -22,7 +23,7 @@ public class ChangeAliasHandler(
|
||||
ChangeAliasRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
@@ -32,11 +33,15 @@ public class ChangeAliasHandler(
|
||||
|
||||
user.Alias = request.Alias;
|
||||
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
IdentityResult result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendUnauthorizedAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
@@ -22,7 +23,7 @@ public class ChangeBirthDateHandler(
|
||||
ChangeBirthDateRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
@@ -32,11 +33,15 @@ public class ChangeBirthDateHandler(
|
||||
|
||||
user.BirthDate = request.BirthDate;
|
||||
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
IdentityResult result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendUnauthorizedAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
@@ -22,7 +23,7 @@ public class ChangeEmailHandler(
|
||||
ChangeEmailRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
@@ -33,11 +34,15 @@ public class ChangeEmailHandler(
|
||||
user.Email = request.Email;
|
||||
|
||||
// TODO: check to see if identity resets the `email confirmed` flag - @jonathan
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
IdentityResult result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendUnauthorizedAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
@@ -23,7 +24,7 @@ public class ChangeFullnameHandler(
|
||||
ChangeFullnameRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
@@ -34,11 +35,15 @@ public class ChangeFullnameHandler(
|
||||
user.Firstname = request.Firstname;
|
||||
user.Lastname = request.Lastname;
|
||||
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
IdentityResult result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendUnauthorizedAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
@@ -22,7 +23,7 @@ public class ChangePhoneHandler(
|
||||
ChangePhoneRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
@@ -33,11 +34,15 @@ public class ChangePhoneHandler(
|
||||
user.PhoneNumber = request.PhoneNumber;
|
||||
// TODO: check to see if identity resets the `phone confirmed` flag - @jonathan
|
||||
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
IdentityResult result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendUnauthorizedAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
@@ -40,7 +41,7 @@ public class ChangePortraitHandler(
|
||||
ChangePortraitRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
@@ -48,7 +49,7 @@ public class ChangePortraitHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
var blobUrl = await blobStorage.UploadFileAsync(
|
||||
string blobUrl = await blobStorage.UploadFileAsync(
|
||||
ContainerNames.Users,
|
||||
$"{user.Id}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}",
|
||||
request.File.OpenReadStream(),
|
||||
@@ -57,7 +58,7 @@ public class ChangePortraitHandler(
|
||||
|
||||
user.PortraitUrl = blobUrl;
|
||||
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
IdentityResult result = await userManager.UpdateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
|
||||
@@ -17,16 +17,16 @@ public class GetCurrentUserQueryHandler(
|
||||
public override async Task HandleAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userModel = await identityService.GetCurrentUserAsync();
|
||||
UserModel? userModel = await identityService.GetCurrentUserAsync();
|
||||
|
||||
if (userModel is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var roles = await identityService.GetCurrentUserRolesAsync();
|
||||
|
||||
|
||||
IList<string> roles = await identityService.GetCurrentUserRolesAsync();
|
||||
|
||||
await SendOkAsync(
|
||||
new UserDto
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
using Hutopy.Modules.Identity.Models;
|
||||
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
@@ -19,7 +20,7 @@ public class GetCurrentUserPortraitHandler(
|
||||
public override async Task HandleAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var identityUser = await identityService.GetCurrentUserAsync();
|
||||
UserModel? identityUser = await identityService.GetCurrentUserAsync();
|
||||
|
||||
if (identityUser is null)
|
||||
{
|
||||
@@ -27,7 +28,7 @@ public class GetCurrentUserPortraitHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
var stream = await blobStorage.DownloadFileAsync(
|
||||
MemoryStream stream = await blobStorage.DownloadFileAsync(
|
||||
ContainerNames.Users,
|
||||
$"{identityUser.Id.ToString()}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}",
|
||||
cancellationToken);
|
||||
|
||||
@@ -32,7 +32,7 @@ public class RefreshTokenHandler(
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Find the user using the refresh token
|
||||
var user = await userManager.Users
|
||||
User? user = await userManager.Users
|
||||
.FirstOrDefaultAsync(u => u.RefreshToken == request.RefreshToken, ct);
|
||||
|
||||
if (user == null || user.RefreshTokenExpiryTime <= DateTime.UtcNow)
|
||||
@@ -52,20 +52,20 @@ public class RefreshTokenHandler(
|
||||
await userManager.UpdateAsync(user);
|
||||
|
||||
// Generate a new access token
|
||||
var accessToken = JwtTokenHelper.GenerateJwtToken(
|
||||
expiresIn: jwtOptions.Value.Lifetime,
|
||||
issuer: jwtOptions.Value.Issuer,
|
||||
audience: jwtOptions.Value.Audience,
|
||||
key: jwtOptions.Value.Key,
|
||||
userId: user.Id.ToString(),
|
||||
email: user.Email ?? string.Empty,
|
||||
alias: user.Alias,
|
||||
firstname: user.Firstname ?? string.Empty,
|
||||
lastname: user.Lastname ?? string.Empty,
|
||||
portraitUrl: user.PortraitUrl);
|
||||
string accessToken = JwtTokenHelper.GenerateJwtToken(
|
||||
jwtOptions.Value.Lifetime,
|
||||
jwtOptions.Value.Issuer,
|
||||
jwtOptions.Value.Audience,
|
||||
jwtOptions.Value.Key,
|
||||
user.Id.ToString(),
|
||||
user.Email ?? string.Empty,
|
||||
user.Alias,
|
||||
user.Firstname ?? string.Empty,
|
||||
user.Lastname ?? string.Empty,
|
||||
user.PortraitUrl);
|
||||
|
||||
await SendOkAsync(
|
||||
new RefreshTokenResponse(accessToken, user.RefreshToken),
|
||||
cancellation: ct);
|
||||
ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
@@ -25,7 +26,7 @@ public class ResetPasswordHandler(
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Find user by email
|
||||
var user = await userManager.FindByEmailAsync(request.Email);
|
||||
User? user = await userManager.FindByEmailAsync(request.Email);
|
||||
if (user is null)
|
||||
{
|
||||
await SendStringAsync(
|
||||
@@ -36,7 +37,7 @@ public class ResetPasswordHandler(
|
||||
}
|
||||
|
||||
// Reset password with token
|
||||
var result = await userManager.ResetPasswordAsync(
|
||||
IdentityResult result = await userManager.ResetPasswordAsync(
|
||||
user,
|
||||
request.Token,
|
||||
request.NewPassword);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
@@ -23,19 +24,19 @@ public class SetPasswordHandler(
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Get current user id from claims
|
||||
var userId = User.GetUserId().ToString();
|
||||
|
||||
string userId = User.GetUserId().ToString();
|
||||
|
||||
// Get user from database
|
||||
var user = await userManager.FindByIdAsync(userId);
|
||||
User? user = await userManager.FindByIdAsync(userId);
|
||||
if (user is null)
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var result = await userManager.ResetPasswordAsync(user, resetToken, request.NewPassword);
|
||||
|
||||
string resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
|
||||
IdentityResult result = await userManager.ResetPasswordAsync(user, resetToken, request.NewPassword);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
await SendStringAsync(
|
||||
@@ -44,7 +45,7 @@ public class SetPasswordHandler(
|
||||
cancellation: ct);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
namespace Hutopy.Modules.Identity.Models;
|
||||
|
||||
public class Result(
|
||||
bool succeeded,
|
||||
bool succeeded,
|
||||
IEnumerable<string> errors)
|
||||
{
|
||||
public bool Succeeded { get; init; } = succeeded;
|
||||
public string[] Errors { get; init; } = errors.ToArray();
|
||||
|
||||
|
||||
public static Result Success()
|
||||
{
|
||||
return new Result(true, Array.Empty<string>());
|
||||
@@ -20,18 +20,18 @@ public class Result(
|
||||
|
||||
public class Result<T>(
|
||||
T? value,
|
||||
bool succeeded,
|
||||
bool succeeded,
|
||||
IEnumerable<string> errors)
|
||||
{
|
||||
public bool Succeeded { get; init; } = succeeded;
|
||||
public string[] Errors { get; init; } = errors.ToArray();
|
||||
public T? Value { get; set; } = value;
|
||||
|
||||
|
||||
public T GetValueOrDefault()
|
||||
{
|
||||
return Value ?? default(T)!;
|
||||
}
|
||||
|
||||
|
||||
public string GetErrorsAsString()
|
||||
{
|
||||
return Errors.Length == 0 ? string.Empty : string.Join(", ", Errors);
|
||||
|
||||
@@ -9,7 +9,7 @@ public sealed class UserLookup(
|
||||
{
|
||||
public async Task<UserReference?> GetUserAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId.ToString());
|
||||
User? user = await userManager.FindByIdAsync(userId.ToString());
|
||||
|
||||
return user is null
|
||||
? null
|
||||
|
||||
@@ -14,7 +14,7 @@ public class Membership
|
||||
public DateTimeOffset? StartDate { get; set; }
|
||||
public DateTimeOffset? EndDate { get; set; }
|
||||
public bool IsActive => EndDate == null || EndDate > DateTimeOffset.UtcNow;
|
||||
[MaxLength(256)]public string? StripeSubscriptionId { get; set; }
|
||||
|
||||
[MaxLength(256)] public string? StripeSubscriptionId { get; set; }
|
||||
|
||||
public ICollection<Payment> Payments { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ public sealed class MembershipsDbContext(
|
||||
public DbSet<MembershipTier> MembershipTiers => Set<MembershipTier>();
|
||||
public DbSet<Membership> Memberships => Set<Membership>();
|
||||
public DbSet<Payment> Payments => Set<Payment>();
|
||||
|
||||
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasDefaultSchema(SchemaName);
|
||||
@@ -20,7 +20,7 @@ public sealed class MembershipsDbContext(
|
||||
.Property(c => c.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
|
||||
modelBuilder
|
||||
.Entity<Membership>()
|
||||
.Property(c => c.CreatedAt)
|
||||
|
||||
@@ -9,5 +9,4 @@ public class Payment
|
||||
public decimal Amount { get; set; }
|
||||
[MaxLength(8)] public required string Currency { get; set; }
|
||||
[MaxLength(2048)] public required string InvoiceUrl { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -13,19 +13,19 @@ public static class DependencyInjection
|
||||
builder.Services.AddDbContext<MembershipsDbContext>(configureAction);
|
||||
|
||||
builder.Services.AddTransient<IMembershipNotifier, MembershipNotifier>();
|
||||
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static async Task<IApplicationBuilder> UseMembershipModuleAsync(
|
||||
this IApplicationBuilder app,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
await using var context = scope.ServiceProvider.GetRequiredService<MembershipsDbContext>();
|
||||
await context.Database.MigrateAsync(cancellationToken: cancellationToken);
|
||||
IServiceScopeFactory scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||
using IServiceScope scope = scopeFactory.CreateScope();
|
||||
await using MembershipsDbContext context = scope.ServiceProvider.GetRequiredService<MembershipsDbContext>();
|
||||
await context.Database.MigrateAsync(cancellationToken);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -19,18 +19,18 @@ public class CancelMembershipHandler(
|
||||
Delete("/api/memberships");
|
||||
Options(o => o.WithTags("Memberships"));
|
||||
}
|
||||
|
||||
|
||||
public override async Task HandleAsync(
|
||||
CancelMembershipRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var subscription = await dbContext
|
||||
Membership? subscription = await dbContext
|
||||
.Memberships
|
||||
.FindAsync(
|
||||
[req.SubscriptionId],
|
||||
cancellationToken: ct);
|
||||
ct);
|
||||
|
||||
if (subscription is not { EndDate: null }
|
||||
if (subscription is not { EndDate: null }
|
||||
|| subscription.StripeSubscriptionId is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
|
||||
@@ -27,9 +27,9 @@ public class CreateMembershipTierEndpoint(
|
||||
CreateMembershipTierRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tierId = Guid.CreateVersion7();
|
||||
Guid tierId = Guid.CreateVersion7();
|
||||
|
||||
var productId = await membershipTierProcessor.CreateAsync(
|
||||
string productId = await membershipTierProcessor.CreateAsync(
|
||||
req.CreatorId,
|
||||
tierId,
|
||||
req.Name,
|
||||
@@ -37,14 +37,14 @@ public class CreateMembershipTierEndpoint(
|
||||
req.Price);
|
||||
|
||||
// Record the new Tier
|
||||
var tier = new MembershipTier
|
||||
MembershipTier tier = new()
|
||||
{
|
||||
Id = tierId,
|
||||
CreatorId = req.CreatorId,
|
||||
Price = req.Price,
|
||||
Name = req.Name,
|
||||
Description = req.Description,
|
||||
StripeProductId = productId,
|
||||
StripeProductId = productId
|
||||
};
|
||||
|
||||
dbContext.MembershipTiers.Add(tier);
|
||||
|
||||
@@ -28,16 +28,16 @@ public class GetActiveMembershipsHandler(
|
||||
public override async Task HandleAsync(
|
||||
CancellationToken ct)
|
||||
{
|
||||
var subscriptions = await dbContext
|
||||
List<Membership> subscriptions = await dbContext
|
||||
.Memberships
|
||||
.Where(subscription => subscription.UserId == User.GetUserId())
|
||||
.Where(subscription => subscription.State == MembershipState.Active)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var result = await Task.WhenAll(
|
||||
GetActiveMembershipsResponse[] result = await Task.WhenAll(
|
||||
subscriptions.Select(async subscription =>
|
||||
{
|
||||
var creator = await creatorLookup.GetCreatorAsync(subscription.CreatorId, ct);
|
||||
CreatorReference? creator = await creatorLookup.GetCreatorAsync(subscription.CreatorId, ct);
|
||||
|
||||
return new GetActiveMembershipsResponse(
|
||||
subscription.Id,
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace Hutopy.Modules.Memberships.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record GetMembershipTiersRequest
|
||||
{
|
||||
{
|
||||
public Guid CreatorId { get; set; }
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class GetMembershipTiersEndpoint(
|
||||
GetMembershipTiersRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tiers = await dbContext
|
||||
List<TierModel> tiers = await dbContext
|
||||
.MembershipTiers
|
||||
.Where(tier => tier.CreatorId == req.CreatorId)
|
||||
.Select(tier => new TierModel(
|
||||
|
||||
@@ -44,7 +44,7 @@ public class SubscribeHandler(
|
||||
SubscribeRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tier = await dbContext
|
||||
MembershipTier? tier = await dbContext
|
||||
.MembershipTiers
|
||||
.Where(tier => tier.Id == req.MembershipTierId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
@@ -54,7 +54,7 @@ public class SubscribeHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
var creator = await creatorLookup.GetCreatorAsync(tier.CreatorId, ct);
|
||||
CreatorReference? creator = await creatorLookup.GetCreatorAsync(tier.CreatorId, ct);
|
||||
if (creator == null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
@@ -68,7 +68,7 @@ public class SubscribeHandler(
|
||||
}
|
||||
|
||||
// Process Stripe subscription
|
||||
var checkoutSession = await membershipPaymentProcessor.CreateCheckoutSessionAsync(
|
||||
MembershipCheckoutSession checkoutSession = await membershipPaymentProcessor.CreateCheckoutSessionAsync(
|
||||
User.GetUserId(),
|
||||
creator,
|
||||
tier.Id,
|
||||
@@ -78,6 +78,6 @@ public class SubscribeHandler(
|
||||
|
||||
await SendOkAsync(
|
||||
new SubscriptionResponse { StripeCheckoutUrl = checkoutSession.Url },
|
||||
cancellation: ct);
|
||||
ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public class MembershipNotifier(
|
||||
string tierId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var membership = new Membership
|
||||
Membership membership = new()
|
||||
{
|
||||
Id = Guid.CreateVersion7(),
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
@@ -40,17 +40,17 @@ public class MembershipNotifier(
|
||||
string currency,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var membership = await dbContext
|
||||
Membership? membership = await dbContext
|
||||
.Memberships
|
||||
.SingleOrDefaultAsync(
|
||||
m => m.StripeSubscriptionId == stripeSubscriptionId,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken);
|
||||
if (membership is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var payment = new Payment
|
||||
Payment payment = new()
|
||||
{
|
||||
Id = Guid.CreateVersion7(),
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
@@ -73,11 +73,11 @@ public class MembershipNotifier(
|
||||
DateTimeOffset? endDate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var membership = await dbContext
|
||||
Membership? membership = await dbContext
|
||||
.Memberships
|
||||
.SingleOrDefaultAsync(
|
||||
s => s.StripeSubscriptionId == subscriptionId,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken);
|
||||
if (membership == null)
|
||||
{
|
||||
return;
|
||||
@@ -92,11 +92,11 @@ public class MembershipNotifier(
|
||||
string subscriptionId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var membership = await dbContext
|
||||
Membership? membership = await dbContext
|
||||
.Memberships
|
||||
.SingleOrDefaultAsync(
|
||||
s => s.StripeSubscriptionId == subscriptionId,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken);
|
||||
if (membership == null)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -10,6 +10,8 @@ public class MessagingDbContext(
|
||||
{
|
||||
public const string SchemaName = "Messaging";
|
||||
|
||||
public DbSet<Message> Messages { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasDefaultSchema(SchemaName);
|
||||
@@ -21,8 +23,6 @@ public class MessagingDbContext(
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
}
|
||||
|
||||
public DbSet<Message> Messages { get; set; }
|
||||
|
||||
public async Task<IEnumerable<MessageDto>> GetMessagesAsync(
|
||||
Guid subjectId,
|
||||
Guid? parentId,
|
||||
@@ -30,7 +30,7 @@ public class MessagingDbContext(
|
||||
int pageSize,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var query = Messages
|
||||
IQueryable<Message> query = Messages
|
||||
.Where(c => c.SubjectId == subjectId)
|
||||
.Where(c => c.ParentId == parentId);
|
||||
|
||||
@@ -39,7 +39,7 @@ public class MessagingDbContext(
|
||||
var lastMessage = await Messages
|
||||
.Where(c => c.Id == lastId.Value)
|
||||
.Select(c => new { c.CreatedAt, c.Id })
|
||||
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (lastMessage != null)
|
||||
{
|
||||
@@ -49,17 +49,17 @@ public class MessagingDbContext(
|
||||
}
|
||||
}
|
||||
|
||||
var messages = await query
|
||||
List<Message> messages = await query
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.ThenByDescending(c => c.Id)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken: ct);
|
||||
.ToListAsync(ct);
|
||||
|
||||
|
||||
var result = await Task.WhenAll(
|
||||
MessageDto[] result = await Task.WhenAll(
|
||||
messages.Select(async message =>
|
||||
{
|
||||
var writer = await userLookup.GetUserAsync(message.CreatedBy, ct);
|
||||
UserReference? writer = await userLookup.GetUserAsync(message.CreatedBy, ct);
|
||||
return new MessageDto(
|
||||
message.Id,
|
||||
message.SubjectId,
|
||||
@@ -80,11 +80,11 @@ public class MessagingDbContext(
|
||||
int pageSize,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var query = Messages
|
||||
IQueryable<Message> query = Messages
|
||||
.Where(c => c.SubjectId == subjectId)
|
||||
.Where(c => c.ParentId == parentId);
|
||||
|
||||
var messageCount = await query
|
||||
int messageCount = await query
|
||||
.Take(pageSize)
|
||||
.CountAsync(ct);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ public static class DependencyInjection
|
||||
Action<DbContextOptionsBuilder>? configureAction = null)
|
||||
{
|
||||
builder.Services.AddDbContext<MessagingDbContext>(configureAction);
|
||||
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ public static class DependencyInjection
|
||||
this IApplicationBuilder app,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
await using var context = scope.ServiceProvider.GetRequiredService<MessagingDbContext>();
|
||||
await context.Database.MigrateAsync(cancellationToken: cancellationToken);
|
||||
IServiceScopeFactory scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||
using IServiceScope scope = scopeFactory.CreateScope();
|
||||
await using MessagingDbContext context = scope.ServiceProvider.GetRequiredService<MessagingDbContext>();
|
||||
await context.Database.MigrateAsync(cancellationToken);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -40,14 +40,14 @@ public class AddMessage(
|
||||
AddMessageRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var message = new Message
|
||||
Message message = new()
|
||||
{
|
||||
Id = req.Id ?? Guid.CreateVersion7(),
|
||||
SubjectId = req.SubjectId,
|
||||
CreatedBy = User.GetUserId(),
|
||||
Value = req.Message
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
await context.Messages.AddAsync(message, ct);
|
||||
|
||||
await context.SaveChangesAsync(ct);
|
||||
|
||||
@@ -20,7 +20,7 @@ internal sealed class AddReplyRequestValidator
|
||||
RuleFor(r => r.ParentId)
|
||||
.NotNull().WithMessage("You must specify a ParentId")
|
||||
.NotEmpty().WithMessage("You must specify a non-empty ParentId");
|
||||
|
||||
|
||||
RuleFor(r => r.SubjectId)
|
||||
.NotNull().WithMessage("You must specify a SubjectId")
|
||||
.NotEmpty().WithMessage("You must specify a non-empty SubjectId");
|
||||
@@ -45,10 +45,10 @@ internal sealed class AddReply(
|
||||
AddReplyRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var message = new Message
|
||||
Message message = new()
|
||||
{
|
||||
Id = Guid.CreateVersion7(),
|
||||
SubjectId = req.SubjectId,
|
||||
SubjectId = req.SubjectId,
|
||||
ParentId = req.ParentId,
|
||||
CreatedBy = User.GetUserId(),
|
||||
Value = req.Message
|
||||
|
||||
@@ -39,15 +39,15 @@ public class ChangeMessage(
|
||||
ChangeMessageRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var message = await context.Messages.FirstOrDefaultAsync(x => x.Id == req.Id, ct);
|
||||
|
||||
Message? message = await context.Messages.FirstOrDefaultAsync(x => x.Id == req.Id, ct);
|
||||
|
||||
if (message is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = HttpContext.User.GetUserId();
|
||||
|
||||
Guid userId = HttpContext.User.GetUserId();
|
||||
if (message.CreatedBy != userId)
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
@@ -58,7 +58,7 @@ public class ChangeMessage(
|
||||
message.Value = req.Message;
|
||||
|
||||
context.Update(message);
|
||||
|
||||
|
||||
await context.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,21 +30,21 @@ public class DeleteMessage(
|
||||
DeleteMessageRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var message = await context.Messages.FirstOrDefaultAsync(x => x.Id == req.MessageId, ct);
|
||||
Message? message = await context.Messages.FirstOrDefaultAsync(x => x.Id == req.MessageId, ct);
|
||||
|
||||
if (message is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = HttpContext.User.GetUserId();
|
||||
|
||||
Guid userId = HttpContext.User.GetUserId();
|
||||
if (message.CreatedBy != userId)
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
context.Messages.Remove(message);
|
||||
|
||||
await context.SaveChangesAsync(ct);
|
||||
|
||||
@@ -28,17 +28,14 @@ public class GetMessageCount(
|
||||
GetMessageCountRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var messageCount = await context.GetMessageCountAsync(
|
||||
int messageCount = await context.GetMessageCountAsync(
|
||||
req.SubjectId,
|
||||
null,
|
||||
req.PageSize,
|
||||
ct);
|
||||
|
||||
|
||||
await SendAsync(
|
||||
new()
|
||||
{
|
||||
Count = messageCount
|
||||
},
|
||||
new GetMessageCountResponse { Count = messageCount },
|
||||
cancellation: ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ public class GetMessages(
|
||||
GetMessagesRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var messages = await context.GetMessagesAsync(
|
||||
IEnumerable<MessageDto> messages = await context.GetMessagesAsync(
|
||||
req.SubjectId,
|
||||
null,
|
||||
req.LastId,
|
||||
req.PageSize,
|
||||
ct);
|
||||
|
||||
|
||||
await SendOkAsync(new GetMessagesResponse(messages), ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,16 +30,16 @@ public class GetMessagesByUser(
|
||||
GetMessagesByUserRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var messages = await context
|
||||
List<Message> messages = await context
|
||||
.Messages
|
||||
.Where(c => c.CreatedBy == req.UserId)
|
||||
.Where(c => c.ParentId == null)
|
||||
.ToListAsync(cancellationToken: ct);
|
||||
.ToListAsync(ct);
|
||||
|
||||
var result = await Task.WhenAll(
|
||||
MessageDto[] result = await Task.WhenAll(
|
||||
messages.Select(async message =>
|
||||
{
|
||||
var user = await userLookup.GetUserAsync(message.CreatedBy, ct);
|
||||
UserReference? user = await userLookup.GetUserAsync(message.CreatedBy, ct);
|
||||
|
||||
return new MessageDto
|
||||
{
|
||||
|
||||
@@ -31,7 +31,7 @@ public class GetReplies(
|
||||
GetRepliesRequest req,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var replies = await context.GetMessagesAsync(
|
||||
IEnumerable<MessageDto> replies = await context.GetMessagesAsync(
|
||||
req.SubjectId,
|
||||
req.ParentId,
|
||||
req.LastId,
|
||||
|
||||
@@ -17,5 +17,5 @@ public class Tip : Entity
|
||||
public enum TipStatus : short
|
||||
{
|
||||
Pending = 0,
|
||||
Paid = 1,
|
||||
Paid = 1
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ public sealed class TippingDbContext(
|
||||
public const string SchemaName = "Tipping";
|
||||
|
||||
public DbSet<Tip> Tips => Set<Tip>();
|
||||
|
||||
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasDefaultSchema(SchemaName);
|
||||
|
||||
@@ -13,7 +13,7 @@ public static class DependencyInjection
|
||||
{
|
||||
builder.Services.AddDbContext<TippingDbContext>(configureAction);
|
||||
builder.Services.AddTransient<ITipPaymentNotifier, TipPaymentNotifier>();
|
||||
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ public static class DependencyInjection
|
||||
this IApplicationBuilder app,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
await using var context = scope.ServiceProvider.GetRequiredService<MessagingDbContext>();
|
||||
await context.Database.MigrateAsync(cancellationToken: cancellationToken);
|
||||
IServiceScopeFactory scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||
using IServiceScope scope = scopeFactory.CreateScope();
|
||||
await using MessagingDbContext context = scope.ServiceProvider.GetRequiredService<MessagingDbContext>();
|
||||
await context.Database.MigrateAsync(cancellationToken);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -24,16 +24,16 @@ public class GetReceivedTipsHandler(
|
||||
public override async Task HandleAsync(
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tips = await dbContext
|
||||
List<Tip> tips = await dbContext
|
||||
.Tips
|
||||
.Where(tip => tip.CreatorId == User.GetUserId())
|
||||
.ToListAsync(ct);
|
||||
|
||||
var result = await Task.WhenAll(
|
||||
TipReceivedModel[] result = await Task.WhenAll(
|
||||
tips.Select(async tip =>
|
||||
{
|
||||
var tipper = await userLookup.GetUserAsync(tip.CreatorId, ct);
|
||||
|
||||
UserReference? tipper = await userLookup.GetUserAsync(tip.CreatorId, ct);
|
||||
|
||||
return new TipReceivedModel(
|
||||
tip.Id,
|
||||
tip.CreatedAt,
|
||||
|
||||
Reference in New Issue
Block a user