diff --git a/Directory.Packages.props b/Directory.Packages.props
index cad324b..42adba5 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -8,7 +8,7 @@
-
+
diff --git a/src/Application/AzureBlobStorage/Constants/ContainerNames.cs b/src/Application/AzureBlobStorage/Constants/ContainerNames.cs
index 5bab776..1f49c7e 100644
--- a/src/Application/AzureBlobStorage/Constants/ContainerNames.cs
+++ b/src/Application/AzureBlobStorage/Constants/ContainerNames.cs
@@ -3,4 +3,5 @@
public static class ContainerNames
{
public static string Users = "users";
+ public static string Creators = "creators";
}
diff --git a/src/Application/AzureBlobStorage/Constants/SubDirectoryNames.cs b/src/Application/AzureBlobStorage/Constants/SubDirectoryNames.cs
index 6ee43ea..7590b74 100644
--- a/src/Application/AzureBlobStorage/Constants/SubDirectoryNames.cs
+++ b/src/Application/AzureBlobStorage/Constants/SubDirectoryNames.cs
@@ -3,5 +3,5 @@
public static class SubDirectoryNames
{
public static string Profile = "profile";
- public static string Posts = "posts";
+ public static string Contents = "contents";
}
diff --git a/src/Application/Common/Interfaces/IAzureBlobStorageService.cs b/src/Application/Common/Interfaces/IAzureBlobStorageService.cs
index a397ce0..e0aa991 100644
--- a/src/Application/Common/Interfaces/IAzureBlobStorageService.cs
+++ b/src/Application/Common/Interfaces/IAzureBlobStorageService.cs
@@ -2,6 +2,6 @@
public interface IAzureBlobStorageService
{
- Task UploadFileAsync(string containerName, string blobName, Stream fileStream, string contentType);
- Task DownloadFileAsync(string containerName, string blobName);
+ Task UploadFileAsync(string containerName, string blobName, MemoryStream memoryStream, string contentType, CancellationToken ct = default);
+ Task DownloadFileAsync(string containerName, string blobName, CancellationToken ct = default);
}
diff --git a/src/Application/Users/Commands/UploadBannerPicture.cs b/src/Application/Users/Commands/UploadBannerPicture.cs
index be7590f..63573fb 100644
--- a/src/Application/Users/Commands/UploadBannerPicture.cs
+++ b/src/Application/Users/Commands/UploadBannerPicture.cs
@@ -10,7 +10,7 @@ namespace Hutopy.Application.Users.Commands;
///
public class UploadBannerPictureCommand : IRequest
{
- public required Stream BannerPicture { get; init; }
+ public required MemoryStream BannerPicture { get; init; }
public string BannerPictureUrl { get; init; } = string.Empty;
}
@@ -32,7 +32,12 @@ public class UploadBannerPictureCommandHandler(IHttpContextAccessor contextAcces
var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.BannerPicture}";
- var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.BannerPicture, contentType);
+ var url = await azureBlobStorageService.UploadFileAsync(
+ ContainerNames.Users,
+ blobName,
+ request.BannerPicture,
+ contentType,
+ cancellationToken);
await identityService.UpdateCurrentUserBannerPictureUrlAsync(url);
diff --git a/src/Application/Users/Commands/UploadProfilePicture.cs b/src/Application/Users/Commands/UploadProfilePicture.cs
index 2e017d2..431e014 100644
--- a/src/Application/Users/Commands/UploadProfilePicture.cs
+++ b/src/Application/Users/Commands/UploadProfilePicture.cs
@@ -10,13 +10,13 @@ namespace Hutopy.Application.Users.Commands;
///
public class UploadProfilePictureCommand : IRequest
{
- public required Stream ProfilePicture { get; init; }
+ public required MemoryStream ProfilePicture { get; init; }
public string ProfilePictureUrl { get; init; } = string.Empty;
}
public class UploadProfilePictureCommandHandler(IHttpContextAccessor contextAccessor, IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler
{
- public async Task Handle(UploadProfilePictureCommand request, CancellationToken cancellationToken)
+ public async Task Handle(UploadProfilePictureCommand request, CancellationToken ct)
{
// If an url to the picture is provided, use it right away and don't upload anything.
if (!string.IsNullOrEmpty(request.ProfilePictureUrl))
@@ -32,7 +32,12 @@ public class UploadProfilePictureCommandHandler(IHttpContextAccessor contextAcce
var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}";
- var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.ProfilePicture, contentType);
+ var url = await azureBlobStorageService.UploadFileAsync(
+ ContainerNames.Users,
+ blobName,
+ request.ProfilePicture,
+ contentType,
+ ct);
await identityService.UpdateCurrentUserProfilePictureUrlAsync(url);
diff --git a/src/Application/Users/Commands/UploadWebsiteIcon.cs b/src/Application/Users/Commands/UploadWebsiteIcon.cs
index eaa8cd6..2e74eda 100644
--- a/src/Application/Users/Commands/UploadWebsiteIcon.cs
+++ b/src/Application/Users/Commands/UploadWebsiteIcon.cs
@@ -10,14 +10,17 @@ namespace Hutopy.Application.Users.Commands;
///
public class UploadWebsiteIconCommand : IRequest
{
- public required Stream WebsiteIcon { get; init; }
-
+ public required MemoryStream WebsiteIcon { get; init; }
+
public string WebsitePictureUrl { get; init; } = string.Empty;
}
-public class UploadWebsiteIconCommandHandler(IHttpContextAccessor contextAccessor, IIdentityService identityService, IAzureBlobStorageService azureBlobStorageService) : IRequestHandler
+public class UploadWebsiteIconCommandHandler(
+ IHttpContextAccessor contextAccessor,
+ IIdentityService identityService,
+ IAzureBlobStorageService azureBlobStorageService) : IRequestHandler
{
- public async Task Handle(UploadWebsiteIconCommand request, CancellationToken cancellationToken)
+ public async Task Handle(UploadWebsiteIconCommand request, CancellationToken ct)
{
// If an url to the picture is provided, use it right away and don't upload anything.
if (!string.IsNullOrEmpty(request.WebsitePictureUrl))
@@ -25,19 +28,23 @@ public class UploadWebsiteIconCommandHandler(IHttpContextAccessor contextAccesso
await identityService.UpdateCurrentUserWebsiteIconUrlAsync(request.WebsitePictureUrl);
return Results.Ok(request.WebsitePictureUrl);
}
-
+
var contentType = contextAccessor.EnsureContentType();
-
+
var identityUser = await identityService.GetCurrentUserAsync();
var currentUserId = new Guid(identityUser?.Id ?? "").ToString();
-
+
var blobName = $"{currentUserId}/{SubDirectoryNames.Profile}/{CommonFileNames.WebsiteIcon}";
-
- var url = await azureBlobStorageService.UploadFileAsync(ContainerNames.Users, blobName, request.WebsiteIcon, contentType);
+
+ var url = await azureBlobStorageService.UploadFileAsync(
+ ContainerNames.Users,
+ blobName,
+ request.WebsiteIcon,
+ contentType,
+ ct);
await identityService.UpdateCurrentUserWebsiteIconUrlAsync(url);
-
+
return Results.Ok(request.WebsitePictureUrl);
}
}
-
diff --git a/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs b/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs
index 8f12630..7887cef 100644
--- a/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs
+++ b/src/Infrastructure/AzureBlob/AzureBlobStorageService.cs
@@ -11,7 +11,7 @@ public class AzureBlobStorageService : IAzureBlobStorageService
{
private readonly BlobServiceClient _blobServiceClient;
private readonly ILogger _logger;
- private readonly long _maxUploadSize = 10 * 1024 * 1024; // 10 MB in bytes
+ private readonly long _maxUploadSize = 10 * 1024 * 1024; // 10 MB in bytes
public AzureBlobStorageService(IConfiguration configuration, ILogger logger)
{
@@ -25,63 +25,73 @@ public class AzureBlobStorageService : IAzureBlobStorageService
///
/// The blob name (path within the container, include the file name).
/// The name of the container where the file is stored.
- /// The stream.
+ /// The memory stream containing the image.
/// The content type.
+ /// The cancellation token
///
- public async Task UploadFileAsync(string containerName, string blobName, Stream fileStream, string contentType)
+ public async Task UploadFileAsync(string containerName, string blobName, MemoryStream memoryStream,
+ string contentType, CancellationToken ct = default)
{
// Read the file stream into a memory stream to determine the length
// WATCH FOR MEMORY USAGE USING THE MEMORY STREAM.
- var memoryStream = new MemoryStream();
- await fileStream.CopyToAsync(memoryStream);
memoryStream.Position = 0;
// Check if the file size exceeds the maximum upload size
if (memoryStream.Length > _maxUploadSize)
{
- _logger.LogInformation($"Blob storage: File size exceeds the maximum allowed size of {_maxUploadSize} bytes.");
- throw new InvalidOperationException($"Blob storage: File size exceeds the maximum allowed size of {_maxUploadSize} bytes.");
+ _logger.LogError(
+ $"Blob storage: File size exceeds the maximum allowed size of {_maxUploadSize} bytes.");
+ throw new InvalidOperationException(
+ $"Blob storage: File size exceeds the maximum allowed size of {_maxUploadSize} bytes.");
}
-
+
// Validate content type
if (!ContentTypes.IsAllowed(contentType, memoryStream))
{
- _logger.LogInformation($"Blob storage: Unsupported file type {contentType}. Only PNG and JPEG are allowed.");
+ _logger.LogError(
+ $"Blob storage: Unsupported file type {contentType}. Only PNG and JPEG are allowed.");
throw new InvalidOperationException("Unsupported file type. Only PNG and JPEG are allowed.");
}
-
+
try
{
// Get a reference to a container
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
// Create the container if it does not exist
- await containerClient.CreateIfNotExistsAsync();
+ await containerClient.CreateIfNotExistsAsync(
+ PublicAccessType.Blob,
+ cancellationToken: ct);
// Get a reference to a blob
var blobClient = containerClient.GetBlobClient(blobName);
// Define the BlobHttpHeaders to include the content type
- var blobHttpHeaders = new BlobHttpHeaders
- {
- ContentType = contentType
- };
+ var blobHttpHeaders = new BlobHttpHeaders { ContentType = contentType };
// Upload the file
- var response = await blobClient.UploadAsync(memoryStream, new BlobUploadOptions
- {
- HttpHeaders = blobHttpHeaders
- });
+ var response = await blobClient.UploadAsync(
+ memoryStream,
+ new BlobUploadOptions { HttpHeaders = blobHttpHeaders },
+ ct);
var fileUri = blobClient.Uri.ToString();
-
+
_logger.LogInformation(
- $"Blob storage: Status [ {response.GetRawResponse().Status.ToString()} ] " +
- $"Uploaded [ {blobName} ] to the container [ {containerName} ] " +
- $"with contentType [ {contentType} ] " +
- $"with a length of [ {memoryStream.Length} bytes ]" +
- $"with the uri [ {fileUri} ]"
- );
+ """
+ Blob storage: Status [ {ResponseStatus} ]
+ Uploaded [ {BlobName} ] to the container [ {ContainerName} ]
+ with contentType [ {ContentType} ]
+ with a length of [ {StreamLength} bytes ]
+ with the uri [ {FileUri} ]
+ """,
+ response.GetRawResponse().Status.ToString(),
+ blobName,
+ containerName,
+ contentType,
+ memoryStream.Length,
+ fileUri
+ );
// Return the URI of the uploaded blob
return fileUri;
@@ -89,7 +99,7 @@ public class AzureBlobStorageService : IAzureBlobStorageService
catch (RequestFailedException ex)
{
_logger.LogError($"Blob storage: Azure Storage request failed: {ex.Message}");
- throw new Exception("Error uploading file to Azure Blob Storage", ex);
+ throw;
}
catch (Exception ex)
{
@@ -99,25 +109,27 @@ public class AzureBlobStorageService : IAzureBlobStorageService
}
///
- /// Download a file to microsoft azure blob storage.
+ /// Download a file to microsoft's azure blob storage.
///
/// The blob name (path within the container).
/// The name of the container where the file is stored. (users)
+ /// The cancellation token for the request
///
- public async Task DownloadFileAsync(string containerName, string blobName)
+ public async Task DownloadFileAsync(string containerName, string blobName,
+ CancellationToken ct = default)
{
try
{
// Get a reference to a container
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
-
+
// Get a reference to a blob
var blobClient = containerClient.GetBlobClient(blobName);
// Download the blob to a stream
- BlobDownloadInfo download = await blobClient.DownloadAsync();
+ BlobDownloadInfo download = await blobClient.DownloadAsync(ct);
MemoryStream memoryStream = new();
- await download.Content.CopyToAsync(memoryStream);
+ await download.Content.CopyToAsync(memoryStream, ct);
memoryStream.Position = 0; // Ensure the stream is at the beginning
return memoryStream;
@@ -125,12 +137,12 @@ public class AzureBlobStorageService : IAzureBlobStorageService
catch (RequestFailedException ex)
{
_logger.LogError($"Azure Storage request failed: {ex.Message}");
- throw new Exception("Error downloading file from Azure Blob Storage", ex);
+ throw;
}
catch (Exception ex)
{
_logger.LogError($"An error occurred: {ex.Message}");
- throw new ApplicationException("Error downloading file from Azure Blob Storage.", ex);
+ throw;
}
}
}
diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj
index dd3cb64..36b3844 100644
--- a/src/Infrastructure/Infrastructure.csproj
+++ b/src/Infrastructure/Infrastructure.csproj
@@ -4,7 +4,7 @@
Hutopy.Infrastructure
-
+
diff --git a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
index be95cf4..8b4b9ec 100644
--- a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
+++ b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -66,7 +66,7 @@ namespace Hutopy.Infrastructure.Migrations
b.HasKey("Id");
- b.ToTable("FutureCreators");
+ b.ToTable("FutureCreators", (string)null);
});
modelBuilder.Entity("Hutopy.Domain.Entities.UserTransaction", b =>
@@ -141,7 +141,7 @@ namespace Hutopy.Infrastructure.Migrations
b.HasIndex("ApplicationUserId");
- b.ToTable("UserTransactions");
+ b.ToTable("UserTransactions", (string)null);
});
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b =>
@@ -395,7 +395,7 @@ namespace Hutopy.Infrastructure.Migrations
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b =>
{
- b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.ProfileColors", "ProfileColors", b1 =>
+ b.OwnsOne("Hutopy.Infrastructure.Identity.ApplicationUser.ProfileColors#Hutopy.Infrastructure.Identity.OwnedEntities.ProfileColors", "ProfileColors", b1 =>
{
b1.Property("ApplicationUserId")
.HasColumnType("nvarchar(450)");
@@ -424,7 +424,7 @@ namespace Hutopy.Infrastructure.Migrations
.HasForeignKey("ApplicationUserId");
});
- b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.SocialNetworks", "SocialNetworks", b1 =>
+ b.OwnsOne("Hutopy.Infrastructure.Identity.ApplicationUser.SocialNetworks#Hutopy.Infrastructure.Identity.OwnedEntities.SocialNetworks", "SocialNetworks", b1 =>
{
b1.Property("ApplicationUserId")
.HasColumnType("nvarchar(450)");
@@ -469,7 +469,7 @@ namespace Hutopy.Infrastructure.Migrations
.HasForeignKey("ApplicationUserId");
});
- b.OwnsOne("Hutopy.Infrastructure.Identity.OwnedEntities.StoredDataUrls", "StoredDataUrls", b1 =>
+ b.OwnsOne("Hutopy.Infrastructure.Identity.ApplicationUser.StoredDataUrls#Hutopy.Infrastructure.Identity.OwnedEntities.StoredDataUrls", "StoredDataUrls", b1 =>
{
b1.Property("ApplicationUserId")
.HasColumnType("nvarchar(450)");
diff --git a/src/Web/Endpoints/UpdateMyUser.cs b/src/Web/Endpoints/UpdateMyUser.cs
index b26303a..7399116 100644
--- a/src/Web/Endpoints/UpdateMyUser.cs
+++ b/src/Web/Endpoints/UpdateMyUser.cs
@@ -19,19 +19,19 @@ public class UpdateMyUser : EndpointGroupBase
return await sender.Send(command);
}
- private static async Task UpdateCurrentUserProfilePicture(ISender sender, Stream stream, string url = "")
+ private static async Task UpdateCurrentUserProfilePicture(ISender sender, MemoryStream stream, string url = "")
{
var command = new UploadProfilePictureCommand { ProfilePicture = stream, ProfilePictureUrl = url};
return await sender.Send(command);
}
- private static async Task UpdateCurrentUserBannerPicture(ISender sender, Stream stream, string url = "")
+ private static async Task UpdateCurrentUserBannerPicture(ISender sender, MemoryStream stream, string url = "")
{
var command = new UploadBannerPictureCommand { BannerPicture = stream, BannerPictureUrl = url};
return await sender.Send(command);
}
- private static async Task UpdateCurrentUserWebsiteIcon(ISender sender, Stream stream, string url = "")
+ private static async Task UpdateCurrentUserWebsiteIcon(ISender sender, MemoryStream stream, string url = "")
{
var command = new UploadWebsiteIconCommand { WebsiteIcon = stream, WebsitePictureUrl = url};
return await sender.Send(command);
diff --git a/src/Web/Features/Contents/Data/Content.cs b/src/Web/Features/Contents/Data/Content.cs
index c5c3dec..08660bb 100644
--- a/src/Web/Features/Contents/Data/Content.cs
+++ b/src/Web/Features/Contents/Data/Content.cs
@@ -6,7 +6,7 @@ public class Content
public Guid CreatedBy { get; init; }
public DateTimeOffset CreatedAt { get; init; }
- public string? Title { get; init; }
- public string? Description { get; init; }
- public string? Uri { get; init; }
+ public string Title { get; set; }
+ public string Description { get; set; }
+ public string[]? Urls { get; init; }
}
diff --git a/src/Web/Features/Contents/Handlers/CreateContent.cs b/src/Web/Features/Contents/Handlers/CreateContent.cs
new file mode 100644
index 0000000..9f7c94a
--- /dev/null
+++ b/src/Web/Features/Contents/Handlers/CreateContent.cs
@@ -0,0 +1,114 @@
+using System.Collections.Concurrent;
+using FastEndpoints;
+using FluentValidation;
+using Hutopy.Application.AzureBlobStorage.Constants;
+using Hutopy.Application.Common.Interfaces;
+using Hutopy.Web.Common;
+using Hutopy.Web.Features.Contents.Data;
+
+namespace Hutopy.Web.Features.Contents.Handlers;
+
+public record PostContentRequest(
+ Guid Id,
+ Guid CreatorId,
+ string Title,
+ string Description,
+ IFormFileCollection Files);
+
+public sealed class PostContentRequestValidator : Validator
+{
+ public PostContentRequestValidator()
+ {
+ RuleFor(r => r.Id)
+ .NotNull().WithMessage("You should specify the Id")
+ .NotEmpty().WithMessage("You should specify a valid/not empty Id");
+
+ RuleFor(r => r.CreatorId)
+ .NotNull().WithMessage("You should specify the CreatorId")
+ .NotEmpty().WithMessage("You should specify a valid/not empty CreatorId");
+
+ RuleFor(r => r.Title)
+ .NotNull().WithMessage("You should specify the Title")
+ .NotEmpty().WithMessage("You should specify a valid/not empty Title");
+
+ RuleFor(r => r.Description)
+ .NotNull().WithMessage("You should specify the Description")
+ .NotEmpty().WithMessage("You should specify a valid/not empty Description");
+ }
+}
+
+public sealed class PostContent(
+ IAzureBlobStorageService blobStorage,
+ ContentDbContext context)
+ : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/api/contents");
+ Options(o => o.WithTags("Contents"));
+ AllowFileUploads();
+ }
+
+ public override async Task HandleAsync(
+ PostContentRequest req,
+ CancellationToken ct)
+ {
+ var urls = new ConcurrentBag();
+
+ await Parallel.ForEachAsync(
+ req.Files,
+ ct,
+ async (file, ict) =>
+ {
+ try
+ {
+ var contentUrl = await SaveFileAsync(
+ req.CreatorId,
+ req.Id,
+ file,
+ ict);
+
+ urls.Add(contentUrl);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("{ErrorMessage}", ex.Message);
+ }
+ });
+
+ await context.Contents.AddAsync(
+ new()
+ {
+ Id = req.Id,
+ CreatedBy = User.GetUserId(),
+ Title = req.Title,
+ Description = req.Description,
+ Urls = urls.ToArray()
+ },
+ ct);
+
+ await context.SaveChangesAsync(ct);
+
+ await SendOkAsync(ct);
+ }
+
+ private async Task SaveFileAsync(
+ Guid creatorId,
+ Guid contentId,
+ 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.ContentType,
+ ct: ct);
+
+ return url;
+ }
+}
diff --git a/src/Web/Features/Creators/Handlers/GetCreatorByAlias.cs b/src/Web/Features/Contents/Handlers/GetCreatorByAlias.cs
similarity index 78%
rename from src/Web/Features/Creators/Handlers/GetCreatorByAlias.cs
rename to src/Web/Features/Contents/Handlers/GetCreatorByAlias.cs
index 0c798e9..d5277e2 100644
--- a/src/Web/Features/Creators/Handlers/GetCreatorByAlias.cs
+++ b/src/Web/Features/Contents/Handlers/GetCreatorByAlias.cs
@@ -3,11 +3,11 @@ using FluentValidation;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Application.Common.Models;
-namespace Hutopy.Web.Features.Creators.Handlers;
+namespace Hutopy.Web.Features.Contents.Handlers;
public sealed class GetCreatorByAliasRequest
{
- public string CreatorAlias { get; set; }
+ public string CreatorAlias { get; init; }
}
public sealed class GetCreatorByAliasRequestValidator
@@ -16,7 +16,8 @@ public sealed class GetCreatorByAliasRequestValidator
public GetCreatorByAliasRequestValidator()
{
RuleFor(r => r.CreatorAlias)
- .NotNull().WithMessage("You must specify a creator-alias");
+ .NotNull().WithMessage("You should specify the CreatorAlias")
+ .NotEmpty().WithMessage("You should specify a valid/not empty CreatorAlias");
}
}
diff --git a/src/Web/Features/Contents/Handlers/PostContent.cs b/src/Web/Features/Contents/Handlers/PostContent.cs
deleted file mode 100644
index 71bebd5..0000000
--- a/src/Web/Features/Contents/Handlers/PostContent.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using FastEndpoints;
-using Hutopy.Web.Common;
-using Hutopy.Web.Features.Contents.Data;
-
-namespace Hutopy.Web.Features.Contents.Handlers;
-
-public record struct PostContentRequest(
- string? Title,
- string? Description,
- string? Uri);
-
-public class PostContent(
- ContentDbContext context)
- : Endpoint
-{
- public override void Configure()
- {
- Post("/api/contents");
- Options( o => o.WithTags("Contents"));
- }
-
- public override async Task HandleAsync(
- PostContentRequest req,
- CancellationToken ct)
- {
- await context.Contents.AddAsync(
- new Content
- {
- Id = GuidHelper.GenerateUuidV7(),
- CreatedBy = User.GetUserId(),
- Title = req.Title,
- Description = req.Description,
- Uri = req.Uri
- },
- ct);
-
- await context.SaveChangesAsync(ct);
- }
-}
diff --git a/src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.Designer.cs b/src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.Designer.cs
new file mode 100644
index 0000000..6971bc3
--- /dev/null
+++ b/src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.Designer.cs
@@ -0,0 +1,59 @@
+//
+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
+ {
+ ///
+ 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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Title")
+ .HasColumnType("text");
+
+ b.Property("Urls")
+ .HasColumnType("text[]");
+
+ b.HasKey("Id");
+
+ b.ToTable("Contents", "Content");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.cs b/src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.cs
new file mode 100644
index 0000000..74051db
--- /dev/null
+++ b/src/Web/Features/Contents/Migrations/20240725022229_AddMultipleMediaUrlsToContent.cs
@@ -0,0 +1,42 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Hutopy.Web.Features.Contents.Migrations
+{
+ ///
+ public partial class AddMultipleMediaUrlsToContent : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Uri",
+ schema: "Content",
+ table: "Contents");
+
+ migrationBuilder.AddColumn(
+ name: "Urls",
+ schema: "Content",
+ table: "Contents",
+ type: "text[]",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Urls",
+ schema: "Content",
+ table: "Contents");
+
+ migrationBuilder.AddColumn(
+ name: "Uri",
+ schema: "Content",
+ table: "Contents",
+ type: "text",
+ nullable: true);
+ }
+ }
+}
diff --git a/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs b/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs
index e01f3a9..0bff6dd 100644
--- a/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs
+++ b/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs
@@ -23,7 +23,7 @@ namespace Hutopy.Web.Contents.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
- modelBuilder.Entity("Hutopy.Web.Contents.Data.Content", b =>
+ modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
{
b.Property("Id")
.ValueGeneratedOnAdd()
@@ -43,8 +43,8 @@ namespace Hutopy.Web.Contents.Migrations
b.Property("Title")
.HasColumnType("text");
- b.Property("Uri")
- .HasColumnType("text");
+ b.Property("Urls")
+ .HasColumnType("text[]");
b.HasKey("Id");
diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj
index ead4845..ba72aba 100644
--- a/src/Web/Web.csproj
+++ b/src/Web/Web.csproj
@@ -12,6 +12,7 @@
+