chore(codebase): full cleanup pass

This commit is contained in:
2025-06-21 01:58:48 -04:00
parent 8323477cd0
commit 81b5db34ef
92 changed files with 529 additions and 452 deletions

View File

@@ -112,7 +112,7 @@ csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences # Modifier preferences
csharp_prefer_static_local_function = true:warning csharp_prefer_static_local_function = true:warning
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:silent
# Code-block preferences # Code-block preferences
csharp_prefer_braces = true:silent csharp_prefer_braces = true:silent
@@ -273,31 +273,31 @@ dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
dotnet_naming_symbols.interfaces.applicable_kinds = interface dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interfaces.required_modifiers = dotnet_naming_symbols.interfaces.required_modifiers =
dotnet_naming_symbols.enums.applicable_kinds = enum dotnet_naming_symbols.enums.applicable_kinds = enum
dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.enums.required_modifiers = dotnet_naming_symbols.enums.required_modifiers =
dotnet_naming_symbols.events.applicable_kinds = event dotnet_naming_symbols.events.applicable_kinds = event
dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.events.required_modifiers = dotnet_naming_symbols.events.required_modifiers =
dotnet_naming_symbols.methods.applicable_kinds = method dotnet_naming_symbols.methods.applicable_kinds = method
dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.methods.required_modifiers = dotnet_naming_symbols.methods.required_modifiers =
dotnet_naming_symbols.properties.applicable_kinds = property dotnet_naming_symbols.properties.applicable_kinds = property
dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.properties.required_modifiers = dotnet_naming_symbols.properties.required_modifiers =
dotnet_naming_symbols.public_fields.applicable_kinds = field dotnet_naming_symbols.public_fields.applicable_kinds = field
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_fields.required_modifiers = dotnet_naming_symbols.public_fields.required_modifiers =
dotnet_naming_symbols.private_fields.applicable_kinds = field dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_fields.required_modifiers = dotnet_naming_symbols.private_fields.required_modifiers =
dotnet_naming_symbols.private_static_fields.applicable_kinds = field dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
@@ -305,15 +305,15 @@ dotnet_naming_symbols.private_static_fields.required_modifiers = static
dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types_and_namespaces.required_modifiers = dotnet_naming_symbols.types_and_namespaces.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers = dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.type_parameters.applicable_kinds = namespace dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
dotnet_naming_symbols.type_parameters.applicable_accessibilities = * dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
dotnet_naming_symbols.type_parameters.required_modifiers = dotnet_naming_symbols.type_parameters.required_modifiers =
dotnet_naming_symbols.private_constant_fields.applicable_kinds = field dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
@@ -321,7 +321,7 @@ dotnet_naming_symbols.private_constant_fields.required_modifiers = const
dotnet_naming_symbols.local_variables.applicable_kinds = local dotnet_naming_symbols.local_variables.applicable_kinds = local
dotnet_naming_symbols.local_variables.applicable_accessibilities = local dotnet_naming_symbols.local_variables.applicable_accessibilities = local
dotnet_naming_symbols.local_variables.required_modifiers = dotnet_naming_symbols.local_variables.required_modifiers =
dotnet_naming_symbols.local_constants.applicable_kinds = local dotnet_naming_symbols.local_constants.applicable_kinds = local
dotnet_naming_symbols.local_constants.applicable_accessibilities = local dotnet_naming_symbols.local_constants.applicable_accessibilities = local
@@ -329,7 +329,7 @@ dotnet_naming_symbols.local_constants.required_modifiers = const
dotnet_naming_symbols.parameters.applicable_kinds = parameter dotnet_naming_symbols.parameters.applicable_kinds = parameter
dotnet_naming_symbols.parameters.applicable_accessibilities = * dotnet_naming_symbols.parameters.applicable_accessibilities = *
dotnet_naming_symbols.parameters.required_modifiers = dotnet_naming_symbols.parameters.required_modifiers =
dotnet_naming_symbols.public_constant_fields.applicable_kinds = field dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
@@ -345,38 +345,38 @@ dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readon
dotnet_naming_symbols.local_functions.applicable_kinds = local_function dotnet_naming_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_symbols.local_functions.applicable_accessibilities = * dotnet_naming_symbols.local_functions.applicable_accessibilities = *
dotnet_naming_symbols.local_functions.required_modifiers = dotnet_naming_symbols.local_functions.required_modifiers =
# Naming styles # Naming styles
dotnet_naming_style.pascalcase.required_prefix = dotnet_naming_style.pascalcase.required_prefix =
dotnet_naming_style.pascalcase.required_suffix = dotnet_naming_style.pascalcase.required_suffix =
dotnet_naming_style.pascalcase.word_separator = dotnet_naming_style.pascalcase.word_separator =
dotnet_naming_style.pascalcase.capitalization = pascal_case dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_style.ipascalcase.required_prefix = I dotnet_naming_style.ipascalcase.required_prefix = I
dotnet_naming_style.ipascalcase.required_suffix = dotnet_naming_style.ipascalcase.required_suffix =
dotnet_naming_style.ipascalcase.word_separator = dotnet_naming_style.ipascalcase.word_separator =
dotnet_naming_style.ipascalcase.capitalization = pascal_case dotnet_naming_style.ipascalcase.capitalization = pascal_case
dotnet_naming_style.tpascalcase.required_prefix = T dotnet_naming_style.tpascalcase.required_prefix = T
dotnet_naming_style.tpascalcase.required_suffix = dotnet_naming_style.tpascalcase.required_suffix =
dotnet_naming_style.tpascalcase.word_separator = dotnet_naming_style.tpascalcase.word_separator =
dotnet_naming_style.tpascalcase.capitalization = pascal_case dotnet_naming_style.tpascalcase.capitalization = pascal_case
dotnet_naming_style._camelcase.required_prefix = _ dotnet_naming_style._camelcase.required_prefix = _
dotnet_naming_style._camelcase.required_suffix = dotnet_naming_style._camelcase.required_suffix =
dotnet_naming_style._camelcase.word_separator = dotnet_naming_style._camelcase.word_separator =
dotnet_naming_style._camelcase.capitalization = camel_case dotnet_naming_style._camelcase.capitalization = camel_case
dotnet_naming_style.camelcase.required_prefix = dotnet_naming_style.camelcase.required_prefix =
dotnet_naming_style.camelcase.required_suffix = dotnet_naming_style.camelcase.required_suffix =
dotnet_naming_style.camelcase.word_separator = dotnet_naming_style.camelcase.word_separator =
dotnet_naming_style.camelcase.capitalization = camel_case dotnet_naming_style.camelcase.capitalization = camel_case
dotnet_naming_style.s_camelcase.required_prefix = s_ dotnet_naming_style.s_camelcase.required_prefix = s_
dotnet_naming_style.s_camelcase.required_suffix = dotnet_naming_style.s_camelcase.required_suffix =
dotnet_naming_style.s_camelcase.word_separator = dotnet_naming_style.s_camelcase.word_separator =
dotnet_naming_style.s_camelcase.capitalization = camel_case dotnet_naming_style.s_camelcase.capitalization = camel_case
dotnet_style_namespace_match_folder = true:suggestion dotnet_style_namespace_match_folder = true:suggestion

View File

@@ -1,5 +1,6 @@
using System.Text; using System.Text;
using Hutopy.Modules.Identity.Data; using Hutopy.Modules.Identity.Data;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Facebook; using Microsoft.AspNetCore.Authentication.Facebook;
using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -29,18 +30,18 @@ public static class DependencyInjection
return services; return services;
} }
public static IServiceCollection AddAuthorizationAndAuthentication(this IServiceCollection services, public static IServiceCollection AddAuthorizationAndAuthentication(this IServiceCollection services,
ConfigurationManager configuration) ConfigurationManager configuration)
{ {
var authenticationBuilder = services AuthenticationBuilder authenticationBuilder = services
.AddAuthentication(options => .AddAuthentication(options =>
{ {
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}); });
var authJwt = configuration.GetSection("Authentication:Jwt"); IConfigurationSection authJwt = configuration.GetSection("Authentication:Jwt");
if (authJwt.Exists()) if (authJwt.Exists())
{ {
authenticationBuilder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions => authenticationBuilder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions =>
@@ -59,7 +60,7 @@ public static class DependencyInjection
}); });
} }
var authGoogle = configuration.GetSection("Authentication:Google"); IConfigurationSection authGoogle = configuration.GetSection("Authentication:Google");
if (authGoogle.Exists()) if (authGoogle.Exists())
{ {
authenticationBuilder.AddGoogle(GoogleDefaults.AuthenticationScheme, options => authenticationBuilder.AddGoogle(GoogleDefaults.AuthenticationScheme, options =>
@@ -71,7 +72,7 @@ public static class DependencyInjection
}); });
} }
var authFacebook = configuration.GetSection("Authentication:Facebook"); IConfigurationSection authFacebook = configuration.GetSection("Authentication:Facebook");
if (authFacebook.Exists()) if (authFacebook.Exists())
{ {
authenticationBuilder.AddFacebook(FacebookDefaults.AuthenticationScheme, options => authenticationBuilder.AddFacebook(FacebookDefaults.AuthenticationScheme, options =>

View File

@@ -1,2 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xml:space="preserve">
<s:Boolean x:Key="/Default/UserDictionary/Words/=hutopy/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=hutopy/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -10,38 +10,38 @@
<EnableNETAnalyzers>true</EnableNETAnalyzers> <EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisMode>AllEnabledByDefault</AnalysisMode> <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors> <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<WarningsAsErrors /> <WarningsAsErrors/>
<CodeAnalysisRuleSet>analyzers.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>analyzers.ruleset</CodeAnalysisRuleSet>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" Version="12.24.0" /> <PackageReference Include="Azure.Storage.Blobs" Version="12.24.0"/>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.4.0" /> <PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.4.0"/>
<PackageReference Include="Azure.Identity" Version="1.13.2" /> <PackageReference Include="Azure.Identity" Version="1.13.2"/>
<PackageReference Include="FastEndpoints" Version="5.35.0" /> <PackageReference Include="FastEndpoints" Version="5.35.0"/>
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" /> <PackageReference Include="JetBrains.Annotations" Version="2024.3.0"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" Version="9.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" Version="9.0.6"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="9.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="9.0.6"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6"/>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.6"/>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.6"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.6"/>
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.6"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
<PackageReference Include="NSwag.AspNetCore" Version="14.3.0" /> <PackageReference Include="NSwag.AspNetCore" Version="14.3.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" /> <PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0"/>
<PackageReference Include="Postmark" Version="5.2.0" /> <PackageReference Include="Postmark" Version="5.2.0"/>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.8"/>
<PackageReference Include="Stripe.net" Version="47.4.0" /> <PackageReference Include="Stripe.net" Version="47.4.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0" PrivateAssets="all"/>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" PrivateAssets="all"/>
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.12.0.118525" PrivateAssets="all" /> <PackageReference Include="SonarAnalyzer.CSharp" Version="10.12.0.118525" PrivateAssets="all"/>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507" PrivateAssets="all"/>
</ItemGroup> </ItemGroup>

View File

@@ -1,6 +1,6 @@
namespace Hutopy.Infrastructure.BlobStorage.Contracts; namespace Hutopy.Infrastructure.BlobStorage.Contracts;
public static class ContainerNames internal static class ContainerNames
{ {
public const string Users = "users"; public const string Users = "users";
public const string Creators = "creators"; public const string Creators = "creators";

View File

@@ -3,7 +3,7 @@ namespace Hutopy.Infrastructure.BlobStorage.Contracts;
public interface IBlobStorage public interface IBlobStorage
{ {
/// <summary> /// <summary>
/// Upload a file to blob storage. /// Upload a file to blob storage.
/// </summary> /// </summary>
/// <param name="containerName">The name of the container where the file is stored.</param> /// <param name="containerName">The name of the container where the file is stored.</param>
/// <param name="blobName">The blob name (path within the container, include the file name).</param> /// <param name="blobName">The blob name (path within the container, include the file name).</param>
@@ -12,14 +12,14 @@ public interface IBlobStorage
/// <param name="ct">The cancellation token</param> /// <param name="ct">The cancellation token</param>
/// <returns></returns> /// <returns></returns>
Task<string> UploadFileAsync( Task<string> UploadFileAsync(
string containerName, string containerName,
string blobName, string blobName,
Stream stream, Stream stream,
string contentType, string contentType,
CancellationToken ct = default); CancellationToken ct = default);
/// <summary> /// <summary>
/// Download a file to blob storage. /// Download a file to blob storage.
/// </summary> /// </summary>
/// <param name="blobName">The blob name (path within the container).</param> /// <param name="blobName">The blob name (path within the container).</param>
/// <param name="containerName">The name of the container where the file is stored. (users)</param> /// <param name="containerName">The name of the container where the file is stored. (users)</param>

View File

@@ -8,19 +8,19 @@ namespace Hutopy.Infrastructure.BlobStorage.Services;
public class AzureBlobStorage : IBlobStorage public class AzureBlobStorage : IBlobStorage
{ {
private const long MaxUploadSize = 10 * 1024 * 1024; // 10 MB in bytes private const long MaxUploadSize = 10 * 1024 * 1024; // 10 MB in bytes
private readonly BlobServiceClient _blobServiceClient; private readonly BlobServiceClient _blobServiceClient;
private readonly ILogger<AzureBlobStorage> _logger; private readonly ILogger<AzureBlobStorage> _logger;
public AzureBlobStorage(IConfiguration configuration, ILogger<AzureBlobStorage> logger) public AzureBlobStorage(IConfiguration configuration, ILogger<AzureBlobStorage> logger)
{ {
_logger = logger; _logger = logger;
var connectionString = configuration.GetConnectionString("AzureBlob"); string? connectionString = configuration.GetConnectionString("AzureBlob");
_blobServiceClient = new BlobServiceClient(connectionString); _blobServiceClient = new BlobServiceClient(connectionString);
} }
/// <summary> /// <summary>
/// Upload a file to microsoft azure blob storage. /// Upload a file to microsoft azure blob storage.
/// </summary> /// </summary>
/// <param name="containerName">The name of the container where the file is stored.</param> /// <param name="containerName">The name of the container where the file is stored.</param>
/// <param name="blobName">The blob name (path within the container, include the file name).</param> /// <param name="blobName">The blob name (path within the container, include the file name).</param>
@@ -29,8 +29,8 @@ public class AzureBlobStorage : IBlobStorage
/// <param name="ct">The cancellation token</param> /// <param name="ct">The cancellation token</param>
/// <returns></returns> /// <returns></returns>
public async Task<string> UploadFileAsync( public async Task<string> UploadFileAsync(
string containerName, string containerName,
string blobName, string blobName,
Stream stream, Stream stream,
string contentType, string contentType,
CancellationToken ct = default) CancellationToken ct = default)
@@ -59,7 +59,7 @@ public class AzureBlobStorage : IBlobStorage
try try
{ {
// Get a reference to a container // Get a reference to a container
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName); BlobContainerClient? containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
// Create the container if it does not exist // Create the container if it does not exist
await containerClient.CreateIfNotExistsAsync( await containerClient.CreateIfNotExistsAsync(
@@ -67,18 +67,18 @@ public class AzureBlobStorage : IBlobStorage
cancellationToken: ct); cancellationToken: ct);
// Get a reference to a blob // Get a reference to a blob
var blobClient = containerClient.GetBlobClient(blobName); BlobClient? blobClient = containerClient.GetBlobClient(blobName);
// Define the BlobHttpHeaders to include the content type // Define the BlobHttpHeaders to include the content type
var blobHttpHeaders = new BlobHttpHeaders { ContentType = contentType }; BlobHttpHeaders blobHttpHeaders = new() { ContentType = contentType };
// Upload the file // Upload the file
var response = await blobClient.UploadAsync( Response<BlobContentInfo>? response = await blobClient.UploadAsync(
stream, stream,
new BlobUploadOptions { HttpHeaders = blobHttpHeaders }, new BlobUploadOptions { HttpHeaders = blobHttpHeaders },
ct); ct);
var fileUri = blobClient.Uri.ToString(); string fileUri = blobClient.Uri.ToString();
_logger.LogInformation( _logger.LogInformation(
""" """
@@ -112,7 +112,7 @@ public class AzureBlobStorage : IBlobStorage
} }
/// <summary> /// <summary>
/// Download a file to microsoft's azure blob storage. /// Download a file to microsoft's azure blob storage.
/// </summary> /// </summary>
/// <param name="blobName">The blob name (path within the container).</param> /// <param name="blobName">The blob name (path within the container).</param>
/// <param name="containerName">The name of the container where the file is stored. (users)</param> /// <param name="containerName">The name of the container where the file is stored. (users)</param>
@@ -126,14 +126,14 @@ public class AzureBlobStorage : IBlobStorage
try try
{ {
// Get a reference to a container // Get a reference to a container
var containerClient = _blobServiceClient.GetBlobContainerClient(containerName); BlobContainerClient? containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
// Get a reference to a blob // Get a reference to a blob
var blobClient = containerClient.GetBlobClient(blobName); BlobClient? blobClient = containerClient.GetBlobClient(blobName);
// Download the blob to a stream // Download the blob to a stream
BlobDownloadInfo download = await blobClient.DownloadAsync(ct); BlobDownloadInfo download = await blobClient.DownloadAsync(ct);
MemoryStream memoryStream = new(); MemoryStream memoryStream = new();
await download.Content.CopyToAsync(memoryStream, ct); await download.Content.CopyToAsync(memoryStream, ct);
memoryStream.Position = 0; // Ensure the stream is at the beginning memoryStream.Position = 0; // Ensure the stream is at the beginning

View File

@@ -3,6 +3,6 @@ namespace Hutopy.Infrastructure.Configuration;
public class WebsiteOptions public class WebsiteOptions
{ {
public const string SectionName = "Website"; public const string SectionName = "Website";
public string FrontendBaseUrl { get; set; } = "https://localhost:5173"; public string FrontendBaseUrl { get; set; } = "https://localhost:5173";
} }

View File

@@ -5,7 +5,7 @@ namespace Hutopy.Infrastructure.Payments.Stripe.Configuration;
public class StripeOptions public class StripeOptions
{ {
public const string ConfigurationSection = "Stripe"; public const string ConfigurationSection = "Stripe";
[Required] public required string SecretKey { get; init; } [Required] public required string SecretKey { get; init; }
[Required] public required string WebhookSecret { get; init; } [Required] public required string WebhookSecret { get; init; }

View File

@@ -9,7 +9,7 @@ namespace Hutopy.Infrastructure.Payments.Stripe.Services;
public class MembershipPaymentProcessor( public class MembershipPaymentProcessor(
IOptions<StripeOptions> stripeOptions) IOptions<StripeOptions> stripeOptions)
: IMembershipPaymentProcessor : IMembershipPaymentProcessor
{ {
public async Task<MembershipCheckoutSession> CreateCheckoutSessionAsync( public async Task<MembershipCheckoutSession> CreateCheckoutSessionAsync(
Guid userId, Guid userId,
@@ -22,16 +22,16 @@ public class MembershipPaymentProcessor(
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey; StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
// Create Stripe customer for the user if not already created // Create Stripe customer for the user if not already created
var customerService = new CustomerService(); CustomerService customerService = new();
var customer = await customerService.CreateAsync( Customer? customer = await customerService.CreateAsync(
new CustomerCreateOptions new CustomerCreateOptions
{ {
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } } Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } }
}); });
// Create Checkout Session for the subscription // Create Checkout Session for the subscription
var sessionService = new SessionService(); SessionService sessionService = new();
var session = await sessionService.CreateAsync( Session? session = await sessionService.CreateAsync(
new SessionCreateOptions new SessionCreateOptions
{ {
Customer = customer.Id, Customer = customer.Id,
@@ -44,7 +44,11 @@ public class MembershipPaymentProcessor(
SubscriptionData = new SessionSubscriptionDataOptions SubscriptionData = new SessionSubscriptionDataOptions
{ {
ApplicationFeePercent = stripeOptions.Value.HutopyRate, ApplicationFeePercent = stripeOptions.Value.HutopyRate,
TransferData = new SessionSubscriptionDataTransferDataOptions { Destination = creatorReference.StripeAccountId } TransferData =
new SessionSubscriptionDataTransferDataOptions
{
Destination = creatorReference.StripeAccountId
}
}, },
SuccessUrl = successUrl, // Redirect after successful payment SuccessUrl = successUrl, // Redirect after successful payment
CancelUrl = cancelUrl, // Redirect after canceled payment CancelUrl = cancelUrl, // Redirect after canceled payment
@@ -61,5 +65,4 @@ public class MembershipPaymentProcessor(
session.Id, session.Id,
session.Url); session.Url);
} }
} }

View File

@@ -6,7 +6,7 @@ using Stripe;
namespace Hutopy.Infrastructure.Payments.Stripe.Services; namespace Hutopy.Infrastructure.Payments.Stripe.Services;
public sealed class MembershipTierProcessor( public sealed class MembershipTierProcessor(
IOptions<StripeOptions> stripeOptions) IOptions<StripeOptions> stripeOptions)
: IMembershipTierProcessor : IMembershipTierProcessor
{ {
public async Task<string> CreateAsync( public async Task<string> CreateAsync(
@@ -19,8 +19,8 @@ public sealed class MembershipTierProcessor(
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey; StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
// Create the product // Create the product
var productService = new ProductService(); ProductService productService = new();
var product = await productService.CreateAsync( Product? product = await productService.CreateAsync(
new ProductCreateOptions new ProductCreateOptions
{ {
Name = productName, Name = productName,
@@ -28,7 +28,7 @@ public sealed class MembershipTierProcessor(
}); });
// Create the price for the product // Create the price for the product
var priceService = new PriceService(); PriceService priceService = new();
await priceService.CreateAsync( await priceService.CreateAsync(
new PriceCreateOptions new PriceCreateOptions
{ {

View File

@@ -24,14 +24,14 @@ public class StripeTipProcessor(
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey; StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
// Create Stripe customer for the user if not already created // Create Stripe customer for the user if not already created
var customerService = new CustomerService(); CustomerService customerService = new();
var customer = await customerService.CreateAsync( Customer? customer = await customerService.CreateAsync(
new CustomerCreateOptions(), new CustomerCreateOptions(),
cancellationToken: ct); cancellationToken: ct);
// Create paymentIntent for the user // Create paymentIntent for the user
var sessionService = new SessionService(); SessionService sessionService = new();
var session = await sessionService.CreateAsync( Session? session = await sessionService.CreateAsync(
new SessionCreateOptions new SessionCreateOptions
{ {
ClientReferenceId = tipId.ToString(), ClientReferenceId = tipId.ToString(),
@@ -68,7 +68,7 @@ public class StripeTipProcessor(
CancelUrl = cancelUrl, // Redirect after canceled payment CancelUrl = cancelUrl, // Redirect after canceled payment
Metadata = new Dictionary<string, string> Metadata = new Dictionary<string, string>
{ {
{ "creatorId", creator.Id.ToString() }, { "creatorName", creator.Name }, { "message", message }, { "creatorId", creator.Id.ToString() }, { "creatorName", creator.Name }, { "message", message }
} }
}, },
cancellationToken: ct); cancellationToken: ct);

View File

@@ -8,22 +8,22 @@ public static class ClaimsPrincipalExtensions
{ {
return (Guid)claims.GetRequiredClaim<Guid>(ClaimTypes.NameIdentifier); return (Guid)claims.GetRequiredClaim<Guid>(ClaimTypes.NameIdentifier);
} }
public static string GetName(this ClaimsPrincipal claims) public static string GetName(this ClaimsPrincipal claims)
{ {
return (string)claims.GetRequiredClaim<string>(ClaimTypes.Name); return (string)claims.GetRequiredClaim<string>(ClaimTypes.Name);
} }
public static string? GetAlias(this ClaimsPrincipal claims) public static string? GetAlias(this ClaimsPrincipal claims)
{ {
return (string?)claims.GetClaim<string?>(KnownClaims.Alias); return (string?)claims.GetClaim<string?>(KnownClaims.Alias);
} }
public static string? GetPortraitUrl(this ClaimsPrincipal claims) public static string? GetPortraitUrl(this ClaimsPrincipal claims)
{ {
return (string?)claims.GetClaim<string?>(KnownClaims.PortraitUrl); return (string?)claims.GetClaim<string?>(KnownClaims.PortraitUrl);
} }
public static string GetFirstName(this ClaimsPrincipal claims) public static string GetFirstName(this ClaimsPrincipal claims)
{ {
return (string)claims.GetRequiredClaim<string>(ClaimTypes.GivenName); return (string)claims.GetRequiredClaim<string>(ClaimTypes.GivenName);
@@ -41,17 +41,22 @@ public static class ClaimsPrincipalExtensions
private static object? GetClaim<TValue>(this ClaimsPrincipal claims, string key) private static object? GetClaim<TValue>(this ClaimsPrincipal claims, string key)
{ {
var claim = claims.FindFirst(key); Claim? claim = claims.FindFirst(key);
return claim is null ? null : claims.GetRequiredClaim<TValue>(key); return claim is null ? null : claims.GetRequiredClaim<TValue>(key);
} }
private static object GetRequiredClaim<TValue>(this ClaimsPrincipal claims, string key) private static object GetRequiredClaim<TValue>(this ClaimsPrincipal claims, string key)
{ {
var claim = claims.FindFirst(key); Claim? claim = claims.FindFirst(key);
if (claim is null) throw new MissingClaimException(key); if (claim is null)
{
throw new MissingClaimException(key);
}
return typeof(TValue) == typeof(Guid) ? Guid.Parse(claim.Value) : Convert.ChangeType(claim.Value, typeof(TValue)); return typeof(TValue) == typeof(Guid)
? Guid.Parse(claim.Value)
: Convert.ChangeType(claim.Value, typeof(TValue));
} }
} }

View File

@@ -19,10 +19,10 @@ public static class JwtTokenHelper
string lastname, string lastname,
string? portraitUrl) string? portraitUrl)
{ {
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); SymmetricSecurityKey securityKey = new(Encoding.UTF8.GetBytes(key));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); SigningCredentials credentials = new(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>([ List<Claim> claims = new([
new Claim(JwtRegisteredClaimNames.Sub, userId), new Claim(JwtRegisteredClaimNames.Sub, userId),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, userId), new Claim(ClaimTypes.Email, email), new Claim(ClaimTypes.NameIdentifier, userId), new Claim(ClaimTypes.Email, email),
@@ -40,10 +40,10 @@ public static class JwtTokenHelper
claims.Add(new Claim(KnownClaims.PortraitUrl, portraitUrl)); claims.Add(new Claim(KnownClaims.PortraitUrl, portraitUrl));
} }
var token = new JwtSecurityToken( JwtSecurityToken token = new(
issuer: issuer, issuer,
audience: audience, audience,
claims: claims, claims,
expires: DateTime.Now.Add(expiresIn), expires: DateTime.Now.Add(expiresIn),
signingCredentials: credentials); signingCredentials: credentials);

View File

@@ -21,38 +21,54 @@ public static class PasswordGenerator
bool requireSpecialCharacter = true) bool requireSpecialCharacter = true)
{ {
// Create pools based on the requirements // Create pools based on the requirements
var characterPool = new StringBuilder(); StringBuilder characterPool = new();
if (requireNumber) if (requireNumber)
{
characterPool.Append(LowerLetters); characterPool.Append(LowerLetters);
}
if (requireCapital) if (requireCapital)
{
characterPool.Append(UpperLetters); characterPool.Append(UpperLetters);
}
if (requireNumber) if (requireNumber)
{
characterPool.Append(Numbers); characterPool.Append(Numbers);
}
if (requireSpecialCharacter) if (requireSpecialCharacter)
{
characterPool.Append(SpecialCharacters); characterPool.Append(SpecialCharacters);
}
// Ensure that the length is within the specified bounds // Ensure that the length is within the specified bounds
var password = new char[length]; char[] password = new char[length];
// Ensure at least one character from each required category is included // Ensure at least one character from each required category is included
int index = 0; int index = 0;
if (requireLowercase) if (requireLowercase)
{
password[index++] = LowerLetters[Random.Next(LowerLetters.Length)]; password[index++] = LowerLetters[Random.Next(LowerLetters.Length)];
}
if (requireCapital) if (requireCapital)
{
password[index++] = UpperLetters[Random.Next(UpperLetters.Length)]; password[index++] = UpperLetters[Random.Next(UpperLetters.Length)];
}
if (requireNumber) if (requireNumber)
{
password[index++] = Numbers[Random.Next(Numbers.Length)]; password[index++] = Numbers[Random.Next(Numbers.Length)];
}
if (requireSpecialCharacter) if (requireSpecialCharacter)
{
password[index++] = SpecialCharacters[Random.Next(SpecialCharacters.Length)]; password[index++] = SpecialCharacters[Random.Next(SpecialCharacters.Length)];
}
// Fill the rest with the password // Fill the rest with the password
for (int i = index; i < length; i++) for (int i = index; i < length; i++)
{ {

View File

@@ -6,7 +6,7 @@ public static class RefreshTokenGenerator
{ {
public static string Next() public static string Next()
{ {
var randomNumber = new byte[32]; byte[] randomNumber = new byte[32];
RandomNumberGenerator.Fill(randomNumber); RandomNumberGenerator.Fill(randomNumber);
return Convert.ToBase64String(randomNumber); return Convert.ToBase64String(randomNumber);
} }

View File

@@ -13,52 +13,62 @@ public static class YouTubeUrlHelper
RegexOptions.Compiled); RegexOptions.Compiled);
/// <summary> /// <summary>
/// Extracts the video ID from a YouTube URL or returns the input if it's already a video ID. /// Extracts the video ID from a YouTube URL or returns the input if it's already a video ID.
/// </summary> /// </summary>
/// <param name="input">The YouTube URL or video ID</param> /// <param name="input">The YouTube URL or video ID</param>
/// <returns>The extracted video ID or null if invalid</returns> /// <returns>The extracted video ID or null if invalid</returns>
public static string? ExtractVideoId(string? input) public static string? ExtractVideoId(string? input)
{ {
if (string.IsNullOrWhiteSpace(input)) if (string.IsNullOrWhiteSpace(input))
{
return null; return null;
}
// If it's already a valid video ID, return it // If it's already a valid video ID, return it
if (IsValidVideoId(input)) if (IsValidVideoId(input))
{
return input; return input;
}
// Try to extract video ID from URL // Try to extract video ID from URL
var match = VideoIdRegex.Match(input); Match match = VideoIdRegex.Match(input);
return match.Success ? match.Groups[1].Value : null; return match.Success ? match.Groups[1].Value : null;
} }
/// <summary> /// <summary>
/// Validates if the input is a valid YouTube video ID. /// Validates if the input is a valid YouTube video ID.
/// </summary> /// </summary>
/// <param name="input">The video ID to validate</param> /// <param name="input">The video ID to validate</param>
/// <returns>True if the input is a valid video ID</returns> /// <returns>True if the input is a valid video ID</returns>
public static bool IsValidVideoId(string? input) public static bool IsValidVideoId(string? input)
{ {
if (string.IsNullOrWhiteSpace(input)) if (string.IsNullOrWhiteSpace(input))
{
return false; return false;
}
return ShortUrlRegex.IsMatch(input); return ShortUrlRegex.IsMatch(input);
} }
/// <summary> /// <summary>
/// Validates if the input is a valid YouTube URL or video ID. /// Validates if the input is a valid YouTube URL or video ID.
/// </summary> /// </summary>
/// <param name="input">The URL or video ID to validate</param> /// <param name="input">The URL or video ID to validate</param>
/// <returns>True if the input is a valid YouTube URL or video ID</returns> /// <returns>True if the input is a valid YouTube URL or video ID</returns>
public static bool IsValidYouTubeUrlOrId(string? input) public static bool IsValidYouTubeUrlOrId(string? input)
{ {
if (string.IsNullOrWhiteSpace(input)) if (string.IsNullOrWhiteSpace(input))
{
return false; return false;
}
// Check if it's a valid video ID // Check if it's a valid video ID
if (IsValidVideoId(input)) if (IsValidVideoId(input))
{
return true; return true;
}
// Check if it's a valid YouTube URL // Check if it's a valid YouTube URL
return VideoIdRegex.IsMatch(input); return VideoIdRegex.IsMatch(input);
} }
} }

View File

@@ -5,7 +5,7 @@ namespace Hutopy.Modules.Contents.Data;
public class Album : Entity 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; } [MaxLength(255)] public required string Title { get; set; }
public IList<AlbumPhoto> Photos { get; set; } = new List<AlbumPhoto>(); public IList<AlbumPhoto> Photos { get; set; } = new List<AlbumPhoto>();
} }

View File

@@ -24,7 +24,7 @@ public class ContentsDbContext(
modelBuilder modelBuilder
.Entity<Album>() .Entity<Album>()
.Property(c => c.IsDeleted) .Property(c => c.IsDeleted)
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", stored: true); .HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
modelBuilder modelBuilder
.Entity<Album>() .Entity<Album>()
@@ -40,7 +40,7 @@ public class ContentsDbContext(
modelBuilder modelBuilder
.Entity<AlbumPhoto>() .Entity<AlbumPhoto>()
.Property(c => c.IsDeleted) .Property(c => c.IsDeleted)
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", stored: true); .HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
modelBuilder modelBuilder
.Entity<AlbumPhoto>() .Entity<AlbumPhoto>()

View File

@@ -17,9 +17,9 @@ public static class DependencyInjection
this IApplicationBuilder app, this IApplicationBuilder app,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); IServiceScopeFactory scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
using var scope = scopeFactory.CreateScope(); using IServiceScope scope = scopeFactory.CreateScope();
await using var context = scope.ServiceProvider.GetRequiredService<ContentsDbContext>(); await using ContentsDbContext context = scope.ServiceProvider.GetRequiredService<ContentsDbContext>();
await context.Database.MigrateAsync(cancellationToken); await context.Database.MigrateAsync(cancellationToken);
return app; return app;

View File

@@ -23,6 +23,7 @@ public record AddPhotoToAlbumResponse(
public sealed class AddPhotoToAlbumRequestValidator : Validator<AddPhotoToAlbumRequest> public sealed class AddPhotoToAlbumRequestValidator : Validator<AddPhotoToAlbumRequest>
{ {
private const int MaxFileSizeBytes = 10 * 1024 * 1024; // 10MB private const int MaxFileSizeBytes = 10 * 1024 * 1024; // 10MB
private static readonly string[] AllowedImageTypes = private static readonly string[] AllowedImageTypes =
[ [
"image/jpeg", "image/jpeg",
@@ -74,14 +75,14 @@ public class AddPhotoToAlbumHandler(
AddPhotoToAlbumRequest request, AddPhotoToAlbumRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var userId = User.GetUserId(); Guid userId = User.GetUserId();
// Fetch the album we want to add photos to // Fetch the album we want to add photos to
var album = await context Album? album = await context
.Albums .Albums
.SingleOrDefaultAsync( .SingleOrDefaultAsync(
a => a.Id == request.AlbumId && a.CreatedBy == userId, a => a.Id == request.AlbumId && a.CreatedBy == userId,
cancellationToken: ct); ct);
if (album is null) if (album is null)
{ {
@@ -90,7 +91,7 @@ public class AddPhotoToAlbumHandler(
} }
// Check if a photo with the same ID already exists // Check if a photo with the same ID already exists
var existingPhoto = await context bool existingPhoto = await context
.AlbumPhotos .AlbumPhotos
.AnyAsync(p => p.Id == request.PhotoId, ct); .AnyAsync(p => p.Id == request.PhotoId, ct);
@@ -102,16 +103,16 @@ public class AddPhotoToAlbumHandler(
try try
{ {
var (originalUrl, thumbnailUrl) = await ProcessAndUploadImage(request, ct); (string originalUrl, string thumbnailUrl) = await ProcessAndUploadImage(request, ct);
// Get the next order number // Get the next order number
var nextOrder = await context int nextOrder = await context
.AlbumPhotos .AlbumPhotos
.Where(p => p.AlbumId == request.AlbumId) .Where(p => p.AlbumId == request.AlbumId)
.MaxAsync(p => (int?)p.Order, ct) ?? 0; .MaxAsync(p => (int?)p.Order, ct) ?? 0;
// Create the album photo // Create the album photo
var photo = new AlbumPhoto AlbumPhoto photo = new()
{ {
Id = request.PhotoId, Id = request.PhotoId,
CreatedBy = userId, CreatedBy = userId,
@@ -143,23 +144,23 @@ public class AddPhotoToAlbumHandler(
AddPhotoToAlbumRequest request, AddPhotoToAlbumRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var originalFileName = Path.GetFileName(request.File.FileName); string originalFileName = Path.GetFileName(request.File.FileName);
var nameWithoutExt = Path.GetFileNameWithoutExtension(originalFileName); string nameWithoutExt = Path.GetFileNameWithoutExtension(originalFileName);
var extension = Path.GetExtension(originalFileName); string extension = Path.GetExtension(originalFileName);
var filenameOriginal = $"{nameWithoutExt}{extension}"; string filenameOriginal = $"{nameWithoutExt}{extension}";
var filenameThumbnail = $"{nameWithoutExt}.thumbnail{extension}"; string filenameThumbnail = $"{nameWithoutExt}.thumbnail{extension}";
var blobOriginal = $"{SubDirectoryNames.Albums}/{request.AlbumId}/{filenameOriginal}"; string blobOriginal = $"{SubDirectoryNames.Albums}/{request.AlbumId}/{filenameOriginal}";
var blobThumbnail = $"{SubDirectoryNames.Albums}/{request.AlbumId}/{filenameThumbnail}"; string blobThumbnail = $"{SubDirectoryNames.Albums}/{request.AlbumId}/{filenameThumbnail}";
// Process the original image // Process the original image
await using var originalStream = request.File.OpenReadStream(); await using Stream originalStream = request.File.OpenReadStream();
using var image = await Image.LoadAsync(originalStream, ct); using Image image = await Image.LoadAsync(originalStream, ct);
// Calculate target size while preserving the original aspect ratio // Calculate target size while preserving the original aspect ratio
var originalWidth = image.Width; int originalWidth = image.Width;
var originalHeight = image.Height; int originalHeight = image.Height;
double ratioX = (double)MaxThumbnailWidth / originalWidth; double ratioX = (double)MaxThumbnailWidth / originalWidth;
double ratioY = (double)MaxThumbnailHeight / originalHeight; double ratioY = (double)MaxThumbnailHeight / originalHeight;
@@ -169,20 +170,20 @@ public class AddPhotoToAlbumHandler(
int newHeight = (int)(originalHeight * ratio); int newHeight = (int)(originalHeight * ratio);
// Create thumbnail // Create thumbnail
using var thumbnailStream = new MemoryStream(); using MemoryStream thumbnailStream = new();
image.Mutate(x => x.Resize(newWidth, newHeight)); image.Mutate(x => x.Resize(newWidth, newHeight));
await image.SaveAsync(thumbnailStream, image.Metadata.DecodedImageFormat!, ct); await image.SaveAsync(thumbnailStream, image.Metadata.DecodedImageFormat!, ct);
thumbnailStream.Position = 0; thumbnailStream.Position = 0;
// Upload both versions // Upload both versions
var originalUrl = await blobStorage.UploadFileAsync( string originalUrl = await blobStorage.UploadFileAsync(
ContainerNames.Creators, ContainerNames.Creators,
blobOriginal, blobOriginal,
request.File.OpenReadStream(), request.File.OpenReadStream(),
request.File.ContentType, request.File.ContentType,
ct); ct);
var thumbnailUrl = await blobStorage.UploadFileAsync( string thumbnailUrl = await blobStorage.UploadFileAsync(
ContainerNames.Creators, ContainerNames.Creators,
blobThumbnail, blobThumbnail,
thumbnailStream, thumbnailStream,

View File

@@ -48,7 +48,7 @@ public class CreateAlbumHandler(
CancellationToken ct) CancellationToken ct)
{ {
// Check if an album with the same ID already exists // Check if an album with the same ID already exists
var existingAlbum = await context bool existingAlbum = await context
.Albums .Albums
.AnyAsync(a => a.Id == request.AlbumId, ct); .AnyAsync(a => a.Id == request.AlbumId, ct);
@@ -58,12 +58,7 @@ public class CreateAlbumHandler(
return; return;
} }
var album = new Album Album album = new() { Id = request.AlbumId, CreatedBy = User.GetUserId(), Title = request.Title };
{
Id = request.AlbumId,
CreatedBy = User.GetUserId(),
Title = request.Title
};
context.Albums.Add(album); context.Albums.Add(album);
await context.SaveChangesAsync(ct); await context.SaveChangesAsync(ct);
@@ -72,4 +67,4 @@ public class CreateAlbumHandler(
new CreateAlbumResponse(album.Id), new CreateAlbumResponse(album.Id),
ct); ct);
} }
} }

View File

@@ -49,12 +49,12 @@ public class GetAlbumHandler(
GetAlbumRequest request, GetAlbumRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var album = await context Album? album = await context
.Albums .Albums
.Include(a => a.Photos.OrderBy(p => p.Order)) .Include(a => a.Photos.OrderBy(p => p.Order))
.SingleOrDefaultAsync( .SingleOrDefaultAsync(
a => a.Id == request.AlbumId, a => a.Id == request.AlbumId,
cancellationToken: ct); ct);
if (album is null) if (album is null)
{ {
@@ -62,7 +62,7 @@ public class GetAlbumHandler(
return; return;
} }
var photos = album.Photos List<AlbumPhotoDto> photos = album.Photos
.Select(p => new AlbumPhotoDto( .Select(p => new AlbumPhotoDto(
p.Id, p.Id,
p.OriginalUrl, p.OriginalUrl,

View File

@@ -33,14 +33,14 @@ public class RemoveAlbumHandler(
RemoveAlbumRequest request, RemoveAlbumRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var userId = User.GetUserId(); Guid userId = User.GetUserId();
var album = await context Album? album = await context
.Albums .Albums
.Include(a => a.Photos) .Include(a => a.Photos)
.SingleOrDefaultAsync( .SingleOrDefaultAsync(
a => a.Id == request.AlbumId && a.CreatedBy == userId, a => a.Id == request.AlbumId && a.CreatedBy == userId,
cancellationToken: ct); ct);
if (album is null) if (album is null)
{ {
@@ -53,7 +53,7 @@ public class RemoveAlbumHandler(
album.DeletedAt = DateTimeOffset.UtcNow; album.DeletedAt = DateTimeOffset.UtcNow;
// Soft delete all photos in the album // Soft delete all photos in the album
foreach (var photo in album.Photos) foreach (AlbumPhoto photo in album.Photos)
{ {
photo.DeletedBy = userId; photo.DeletedBy = userId;
photo.DeletedAt = DateTimeOffset.UtcNow; photo.DeletedAt = DateTimeOffset.UtcNow;
@@ -63,4 +63,4 @@ public class RemoveAlbumHandler(
await SendNoContentAsync(ct); await SendNoContentAsync(ct);
} }
} }

View File

@@ -38,14 +38,14 @@ public class RemovePhotoFromAlbumHandler(
RemovePhotoFromAlbumRequest request, RemovePhotoFromAlbumRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var userId = User.GetUserId(); Guid userId = User.GetUserId();
var album = await context Album? album = await context
.Albums .Albums
.Include(a => a.Photos) .Include(a => a.Photos)
.SingleOrDefaultAsync( .SingleOrDefaultAsync(
a => a.Id == request.AlbumId && a.CreatedBy == userId, a => a.Id == request.AlbumId && a.CreatedBy == userId,
cancellationToken: ct); ct);
if (album is null) if (album is null)
{ {
@@ -53,7 +53,7 @@ public class RemovePhotoFromAlbumHandler(
return; return;
} }
var photo = album.Photos AlbumPhoto? photo = album.Photos
.SingleOrDefault(p => p.Id == request.PhotoId); .SingleOrDefault(p => p.Id == request.PhotoId);
if (photo is null) if (photo is null)
@@ -65,9 +65,9 @@ public class RemovePhotoFromAlbumHandler(
// Soft delete the photo // Soft delete the photo
photo.DeletedBy = userId; photo.DeletedBy = userId;
photo.DeletedAt = DateTimeOffset.UtcNow; photo.DeletedAt = DateTimeOffset.UtcNow;
await context.SaveChangesAsync(ct); await context.SaveChangesAsync(ct);
await SendNoContentAsync(ct); await SendNoContentAsync(ct);
} }
} }

View File

@@ -8,7 +8,7 @@ public class ContentModel
public required string CreatedByName { get; init; } public required string CreatedByName { get; init; }
public required string? CreatedByPortraitUrl { get; init; } public required string? CreatedByPortraitUrl { get; init; }
public required DateTimeOffset CreatedAt { get; init; } public required DateTimeOffset CreatedAt { get; init; }
public Guid? DeletedBy { get; init; } public Guid? DeletedBy { get; init; }
public DateTimeOffset? DeletedAt { get; init; } public DateTimeOffset? DeletedAt { get; init; }
public required string Title { get; init; } public required string Title { get; init; }
public required string Description { get; init; } public required string Description { get; init; }

View File

@@ -3,6 +3,6 @@
public class CreatorOptions public class CreatorOptions
{ {
public const string ConfigurationSection = "Creators"; public const string ConfigurationSection = "Creators";
public TimeSpan SlugReservationDuration { get; set; } public TimeSpan SlugReservationDuration { get; set; }
} }

View File

@@ -5,16 +5,16 @@ namespace Hutopy.Modules.Creators.Data;
public class Creator public class Creator
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid CreatedBy { get; set; } public Guid CreatedBy { get; set; }
public DateTimeOffset CreatedAt { get; init; } public DateTimeOffset CreatedAt { get; init; }
public Guid? DeletedBy { get; set; } public Guid? DeletedBy { get; set; }
public DateTimeOffset? DeletedAt { get; set; } public DateTimeOffset? DeletedAt { get; set; }
/// <summary> /// <summary>
/// Softdelete flag (false by default, true once DeletedAt is set) /// Softdelete flag (false by default, true once DeletedAt is set)
/// </summary> /// </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? BannerUrl { get; set; }
[MaxLength(2048)] public string? PortraitUrl { get; set; } [MaxLength(2048)] public string? PortraitUrl { get; set; }

View File

@@ -17,7 +17,7 @@ public class CreatorsDbContext(
modelBuilder modelBuilder
.Entity<Slugs>() .Entity<Slugs>()
.Property(x => x.NormalizedName) .Property(x => x.NormalizedName)
.HasComputedColumnSql("LOWER(\"Name\")", stored: true); .HasComputedColumnSql("LOWER(\"Name\")", true);
modelBuilder modelBuilder
.Entity<Slugs>() .Entity<Slugs>()
@@ -27,7 +27,7 @@ public class CreatorsDbContext(
modelBuilder modelBuilder
.Entity<Creator>() .Entity<Creator>()
.Property(c => c.IsDeleted) .Property(c => c.IsDeleted)
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", stored: true); // bool .HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true); // bool
modelBuilder modelBuilder
.Entity<Creator>() .Entity<Creator>()
@@ -38,7 +38,7 @@ public class CreatorsDbContext(
.Entity<Creator>() .Entity<Creator>()
.OwnsOne<Presentation>(x => x.Presentation) .OwnsOne<Presentation>(x => x.Presentation)
.ToTable(nameof(Presentation)); .ToTable(nameof(Presentation));
modelBuilder modelBuilder
.Entity<Creator>() .Entity<Creator>()
.HasQueryFilter(c => !c.IsDeleted); .HasQueryFilter(c => !c.IsDeleted);

View File

@@ -12,4 +12,4 @@ public class Socials
[MaxLength(2048)] public string? YoutubeUrl { get; set; } [MaxLength(2048)] public string? YoutubeUrl { get; set; }
[MaxLength(2048)] public string? RedditUrl { get; set; } [MaxLength(2048)] public string? RedditUrl { get; set; }
[MaxLength(2048)] public string? WebsiteUrl { get; set; } [MaxLength(2048)] public string? WebsiteUrl { get; set; }
} }

View File

@@ -14,21 +14,21 @@ public static class DependencyInjection
builder.Services.Configure<CreatorOptions>( builder.Services.Configure<CreatorOptions>(
builder.Configuration.GetSection(CreatorOptions.ConfigurationSection)); builder.Configuration.GetSection(CreatorOptions.ConfigurationSection));
builder.Services.AddScoped<SlugPurger>(); builder.Services.AddScoped<SlugPurger>();
builder.Services.AddDbContext<CreatorsDbContext>(configureAction); builder.Services.AddDbContext<CreatorsDbContext>(configureAction);
builder.Services.AddTransient<ICreatorLookup, CreatorLookup>(); builder.Services.AddTransient<ICreatorLookup, CreatorLookup>();
return builder; return builder;
} }
public static async Task<IApplicationBuilder> UseCreatorModuleAsync( public static async Task<IApplicationBuilder> UseCreatorModuleAsync(
this IApplicationBuilder app, this IApplicationBuilder app,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); IServiceScopeFactory scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
using var scope = scopeFactory.CreateScope(); using IServiceScope scope = scopeFactory.CreateScope();
await using var context = scope.ServiceProvider.GetRequiredService<CreatorsDbContext>(); await using CreatorsDbContext context = scope.ServiceProvider.GetRequiredService<CreatorsDbContext>();
await context.Database.MigrateAsync(cancellationToken: cancellationToken); await context.Database.MigrateAsync(cancellationToken);
return app; return app;
} }

View File

@@ -29,11 +29,11 @@ public static class ChangeBanner
Request request, Request request,
CancellationToken ct) CancellationToken ct)
{ {
var creator = await context Creator? creator = await context
.Creators .Creators
.SingleOrDefaultAsync( .SingleOrDefaultAsync(
c => c.Id == request.CreatorId, c => c.Id == request.CreatorId,
cancellationToken: ct); ct);
if (creator is null) if (creator is null)
{ {
@@ -41,7 +41,7 @@ public static class ChangeBanner
return; return;
} }
var blobUrl = await blobStorage.UploadFileAsync( string blobUrl = await blobStorage.UploadFileAsync(
ContainerNames.Creators, ContainerNames.Creators,
$"{request.CreatorId}/{SubDirectoryNames.Profile}/{CommonFileNames.BannerPicture}", $"{request.CreatorId}/{SubDirectoryNames.Profile}/{CommonFileNames.BannerPicture}",
request.File.OpenReadStream(), request.File.OpenReadStream(),

View File

@@ -38,12 +38,12 @@ public class ChangeEmailHandler(
ChangeEmailRequest request, ChangeEmailRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var creator = await context Creator? creator = await context
.Creators .Creators
.Include(c => c.Presentation) .Include(c => c.Presentation)
.SingleOrDefaultAsync( .SingleOrDefaultAsync(
c => c.Id == request.CreatorId, c => c.Id == request.CreatorId,
cancellationToken: ct); ct);
if (creator is null) if (creator is null)
{ {
@@ -59,9 +59,9 @@ public class ChangeEmailHandler(
} }
creator.Presentation.Email = request.Email?.Trim(); creator.Presentation.Email = request.Email?.Trim();
await context.SaveChangesAsync(ct); await context.SaveChangesAsync(ct);
await SendOkAsync(ct); await SendOkAsync(ct);
} }
} }

View File

@@ -44,11 +44,11 @@ public class ChangeLogoHandler(
ChangeLogoRequest request, ChangeLogoRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var creator = await context Creator? creator = await context
.Creators .Creators
.SingleOrDefaultAsync( .SingleOrDefaultAsync(
c => c.Id == request.CreatorId, c => c.Id == request.CreatorId,
cancellationToken: ct); ct);
if (creator is null) if (creator is null)
{ {
@@ -56,7 +56,7 @@ public class ChangeLogoHandler(
return; return;
} }
var blobUrl = await blobStorage.UploadFileAsync( string blobUrl = await blobStorage.UploadFileAsync(
ContainerNames.Creators, ContainerNames.Creators,
$"{request.CreatorId}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}", $"{request.CreatorId}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}",
request.File.OpenReadStream(), request.File.OpenReadStream(),

View File

@@ -34,11 +34,11 @@ public class ChangeNameHandler(
ChangeNameRequest request, ChangeNameRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var creator = await context Creator creator = await context
.Creators .Creators
.SingleAsync( .SingleAsync(
c => c.Id == request.CreatorId, c => c.Id == request.CreatorId,
cancellationToken: ct); ct);
creator.Name = request.Name; creator.Name = request.Name;

View File

@@ -38,12 +38,12 @@ public class ChangePhoneNumberHandler(
ChangePhoneNumberRequest request, ChangePhoneNumberRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var creator = await context Creator? creator = await context
.Creators .Creators
.Include(c => c.Presentation) .Include(c => c.Presentation)
.SingleOrDefaultAsync( .SingleOrDefaultAsync(
c => c.Id == request.CreatorId, c => c.Id == request.CreatorId,
cancellationToken: ct); ct);
if (creator is null) if (creator is null)
{ {
@@ -59,9 +59,9 @@ public class ChangePhoneNumberHandler(
} }
creator.Presentation.PhoneNumber = request.PhoneNumber?.Trim(); creator.Presentation.PhoneNumber = request.PhoneNumber?.Trim();
await context.SaveChangesAsync(ct); await context.SaveChangesAsync(ct);
await SendOkAsync(ct); await SendOkAsync(ct);
} }
} }

View File

@@ -45,12 +45,12 @@ public class ChangePresentationInfosHandler(
ChangePresentationInfosRequest request, ChangePresentationInfosRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var creator = await context Creator? creator = await context
.Creators .Creators
.Include(c => c.Presentation) .Include(c => c.Presentation)
.SingleOrDefaultAsync( .SingleOrDefaultAsync(
c => c.Id == request.CreatorId, c => c.Id == request.CreatorId,
cancellationToken: ct); ct);
if (creator is null) if (creator is null)
{ {
@@ -60,12 +60,12 @@ public class ChangePresentationInfosHandler(
// Update the presentation info with the new values // Update the presentation info with the new values
creator.Presentation.Description = request.Description.Trim(); creator.Presentation.Description = request.Description.Trim();
creator.Presentation.VideoUrl = request.VideoUrl != null creator.Presentation.VideoUrl = request.VideoUrl != null
? YouTubeUrlHelper.ExtractVideoId(request.VideoUrl.Trim()) ? YouTubeUrlHelper.ExtractVideoId(request.VideoUrl.Trim())
: null; : null;
await context.SaveChangesAsync(ct); await context.SaveChangesAsync(ct);
await SendOkAsync(ct); await SendOkAsync(ct);
} }
} }

View File

@@ -1,5 +1,6 @@
using Hutopy.Infrastructure.Security; using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Creators.Data; using Hutopy.Modules.Creators.Data;
using Microsoft.EntityFrameworkCore.Storage;
namespace Hutopy.Modules.Creators.Features; namespace Hutopy.Modules.Creators.Features;
@@ -17,7 +18,7 @@ internal sealed class ChangeSlugRequestValidator
RuleFor(r => r.CreatorId) RuleFor(r => r.CreatorId)
.NotNull().WithMessage("You should specify the CreatorId") .NotNull().WithMessage("You should specify the CreatorId")
.NotEmpty().WithMessage("You should specify a valid/not empty CreatorId"); .NotEmpty().WithMessage("You should specify a valid/not empty CreatorId");
RuleFor(r => r.SlugReservationId) RuleFor(r => r.SlugReservationId)
.NotNull().WithMessage("You should specify the SlugReservationId") .NotNull().WithMessage("You should specify the SlugReservationId")
.NotEmpty().WithMessage("You should specify a valid/not empty SlugReservationId"); .NotEmpty().WithMessage("You should specify a valid/not empty SlugReservationId");
@@ -39,15 +40,15 @@ public class ChangeSlugHandler(
ChangeSlugRequest request, ChangeSlugRequest request,
CancellationToken ct) CancellationToken ct)
{ {
await using var transaction = await context.Database.BeginTransactionAsync(ct); await using IDbContextTransaction transaction = await context.Database.BeginTransactionAsync(ct);
try try
{ {
var creator = await context Creator creator = await context
.Creators .Creators
.SingleAsync( .SingleAsync(
c => c.Id == request.CreatorId, c => c.Id == request.CreatorId,
cancellationToken: ct); ct);
if (creator.CreatedBy != User.GetUserId()) if (creator.CreatedBy != User.GetUserId())
{ {
@@ -55,7 +56,7 @@ public class ChangeSlugHandler(
return; return;
} }
var reservation = await context Slugs? reservation = await context
.Slugs .Slugs
.FirstOrDefaultAsync( .FirstOrDefaultAsync(
s => s.Id == request.SlugReservationId, s => s.Id == request.SlugReservationId,
@@ -67,7 +68,7 @@ public class ChangeSlugHandler(
return; return;
} }
var previousReservation = await context Slugs? previousReservation = await context
.Slugs .Slugs
.FirstOrDefaultAsync( .FirstOrDefaultAsync(
s => s.UsedBy == request.CreatorId, s => s.UsedBy == request.CreatorId,

View File

@@ -27,12 +27,12 @@ public class ChangeSocialsHandler(
public override async Task HandleAsync(ChangeSocialsRequest request, CancellationToken ct) public override async Task HandleAsync(ChangeSocialsRequest request, CancellationToken ct)
{ {
var creator = await context Creator creator = await context
.Creators .Creators
.Include(c => c.Socials) .Include(c => c.Socials)
.SingleAsync( .SingleAsync(
c => c.Id == request.CreatorId, c => c.Id == request.CreatorId,
cancellationToken: ct); ct);
creator.Socials.FacebookUrl = request.FacebookUrl; creator.Socials.FacebookUrl = request.FacebookUrl;
creator.Socials.InstagramUrl = request.InstagramUrl; creator.Socials.InstagramUrl = request.InstagramUrl;

View File

@@ -19,14 +19,14 @@ public class ChangeTitleHandler(
} }
public override async Task HandleAsync( public override async Task HandleAsync(
ChangeTitleRequest request, ChangeTitleRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var creator = await context Creator creator = await context
.Creators .Creators
.SingleAsync( .SingleAsync(
c => c.Id == request.CreatorId, c => c.Id == request.CreatorId,
cancellationToken: ct); ct);
creator.Title = request.Title; creator.Title = request.Title;

View File

@@ -1,5 +1,6 @@
using Hutopy.Infrastructure.Security; using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Creators.Data; using Hutopy.Modules.Creators.Data;
using Microsoft.EntityFrameworkCore.Storage;
namespace Hutopy.Modules.Creators.Features; namespace Hutopy.Modules.Creators.Features;
@@ -17,7 +18,7 @@ public sealed class CreateCreatorRequestValidator : Validator<CreateCreatorReque
.NotNull() .NotNull()
.NotEmpty() .NotEmpty()
.WithMessage("You should specify a valid SlugReservationId"); .WithMessage("You should specify a valid SlugReservationId");
RuleFor(r => r.CreatorId) RuleFor(r => r.CreatorId)
.NotNull() .NotNull()
.NotEmpty() .NotEmpty()
@@ -40,11 +41,11 @@ public sealed class CreateCreatorHandler(
CreateCreatorRequest req, CreateCreatorRequest req,
CancellationToken ct) CancellationToken ct)
{ {
await using var transaction = await context.Database.BeginTransactionAsync(ct); await using IDbContextTransaction transaction = await context.Database.BeginTransactionAsync(ct);
try try
{ {
var slug = await context Slugs slug = await context
.Slugs .Slugs
.SingleAsync(s => s.Id == req.SlugReservationId, ct); .SingleAsync(s => s.Id == req.SlugReservationId, ct);
@@ -55,23 +56,20 @@ public sealed class CreateCreatorHandler(
await SendErrorsAsync(500, ct); await SendErrorsAsync(500, ct);
return; return;
} }
slug.UsedBy = req.CreatorId; slug.UsedBy = req.CreatorId;
await context.Creators.AddAsync( await context.Creators.AddAsync(
new Creator new Creator
{ {
Id = req.CreatorId, Id = req.CreatorId, CreatedBy = User.GetUserId(), Name = slug.Name, Slug = slug.NormalizedName
CreatedBy = User.GetUserId(),
Name = slug.Name,
Slug = slug.NormalizedName
}, },
ct); ct);
await context.SaveChangesAsync(ct); await context.SaveChangesAsync(ct);
await transaction.CommitAsync(ct); await transaction.CommitAsync(ct);
await SendOkAsync(ct); await SendOkAsync(ct);
} }
catch (Exception) catch (Exception)

View File

@@ -28,7 +28,7 @@ public class GetCreatorByIdHandler(
public override void Configure() public override void Configure()
{ {
Get("/api/creators/{CreatorId}"); Get("/api/creators/{CreatorId}");
Options((o => o.WithTags("Creators"))); Options(o => o.WithTags("Creators"));
AllowAnonymous(); AllowAnonymous();
} }
@@ -36,13 +36,19 @@ public class GetCreatorByIdHandler(
GetCreatorByIdRequest req, GetCreatorByIdRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var creator = await context Creator? creator = await context
.Creators .Creators
.FindAsync( .FindAsync(
[req.CreatorId], [req.CreatorId],
cancellationToken: ct); ct);
if (creator is null) await SendNotFoundAsync(ct); if (creator is null)
else await SendAsync(creator, cancellation: ct); {
await SendNotFoundAsync(ct);
}
else
{
await SendAsync(creator, cancellation: ct);
}
} }
} }

View File

@@ -49,7 +49,7 @@ public class GetCreatorBySlugHandler(
public override void Configure() public override void Configure()
{ {
Get("/api/creators/@{Name}"); Get("/api/creators/@{Name}");
Options((o => o.WithTags("Creators"))); Options(o => o.WithTags("Creators"));
AllowAnonymous(); AllowAnonymous();
} }
@@ -57,9 +57,9 @@ public class GetCreatorBySlugHandler(
GetCreatorBySlugRequest req, GetCreatorBySlugRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var creatorName = req.Name.ToLower(); string creatorName = req.Name.ToLower();
var response = await context GetCreatorBySlugResponse? response = await context
.Creators .Creators
.IgnoreQueryFilters() .IgnoreQueryFilters()
.Where(c => EF.Functions.ILike(c.Slug, creatorName)) .Where(c => EF.Functions.ILike(c.Slug, creatorName))

View File

@@ -34,12 +34,12 @@ public sealed class RemoveCreatorHandler(
RemoveCreatorRequest req, RemoveCreatorRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var creatorSlug = req.CreatorSlug.ToLower(); string creatorSlug = req.CreatorSlug.ToLower();
var creator = await context Creator? creator = await context
.Creators .Creators
.Where(c => EF.Functions.ILike(c.Slug, creatorSlug)) .Where(c => EF.Functions.ILike(c.Slug, creatorSlug))
.SingleOrDefaultAsync(cancellationToken: ct); .SingleOrDefaultAsync(ct);
if (creator is null) if (creator is null)
{ {

View File

@@ -4,6 +4,7 @@ using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Creators.Configuration; using Hutopy.Modules.Creators.Configuration;
using Hutopy.Modules.Creators.Data; using Hutopy.Modules.Creators.Data;
using Hutopy.Modules.Creators.Services; using Hutopy.Modules.Creators.Services;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Npgsql; using Npgsql;
@@ -45,33 +46,31 @@ public sealed class ReserveSlug(
ReserveSlugRequest req, ReserveSlugRequest req,
CancellationToken ct) CancellationToken ct)
{ {
await using var transaction = await context.Database.BeginTransactionAsync(ct); await using IDbContextTransaction transaction = await context.Database.BeginTransactionAsync(ct);
try try
{ {
// First, purge any expired slugs // First, purge any expired slugs
await slugPurger.PurgeExpiredSlugsAsync(ct); await slugPurger.PurgeExpiredSlugsAsync(ct);
var reservation = await context.Slugs.FirstOrDefaultAsync( Slugs? reservation = await context.Slugs.FirstOrDefaultAsync(
s => s.Id == req.ReservationId && s.CreatedBy == User.GetUserId(), s => s.Id == req.ReservationId && s.CreatedBy == User.GetUserId(),
cancellationToken: ct); ct);
if (reservation == null) if (reservation == null)
{ {
reservation = new Slugs reservation = new Slugs
{ {
Id = req.ReservationId, Id = req.ReservationId, CreatedBy = User.GetUserId(), CreatedAt = DateTimeOffset.UtcNow
CreatedBy = User.GetUserId(),
CreatedAt = DateTimeOffset.UtcNow,
}; };
context.Slugs.Attach(reservation); context.Slugs.Attach(reservation);
context.Entry(reservation).State = EntityState.Added; context.Entry(reservation).State = EntityState.Added;
} }
reservation.Name = req.Slug; reservation.Name = req.Slug;
reservation.ReservedUntil = DateTimeOffset.UtcNow + opts.Value.SlugReservationDuration; reservation.ReservedUntil = DateTimeOffset.UtcNow + opts.Value.SlugReservationDuration;
await context.SaveChangesAsync(ct); await context.SaveChangesAsync(ct);
await transaction.CommitAsync(ct); await transaction.CommitAsync(ct);
@@ -81,7 +80,7 @@ public sealed class ReserveSlug(
catch (Exception e) catch (Exception e)
{ {
await transaction.RollbackAsync(ct); await transaction.RollbackAsync(ct);
Logger.LogError("Transaction failed: {Message}", e.Message); Logger.LogError("Transaction failed: {Message}", e.Message);
if (e.InnerException is PostgresException innerException) if (e.InnerException is PostgresException innerException)

View File

@@ -34,13 +34,13 @@ public sealed class RestoreCreatorHandler(
RestoreCreatorRequest req, RestoreCreatorRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var creatorSlug = req.CreatorSlug.ToLower(); string creatorSlug = req.CreatorSlug.ToLower();
var creator = await context Creator? creator = await context
.Creators .Creators
.IgnoreQueryFilters() .IgnoreQueryFilters()
.Where(c => EF.Functions.ILike(c.Slug, creatorSlug)) .Where(c => EF.Functions.ILike(c.Slug, creatorSlug))
.SingleOrDefaultAsync(cancellationToken: ct); .SingleOrDefaultAsync(ct);
if (creator is null) if (creator is null)
{ {

View File

@@ -19,7 +19,7 @@ public class SlugPurger(CreatorsDbContext context)
try try
{ {
var now = DateTimeOffset.UtcNow; DateTimeOffset now = DateTimeOffset.UtcNow;
if (now - s_lastPurgeTime < MinTimeBetweenPurges) if (now - s_lastPurgeTime < MinTimeBetweenPurges)
{ {
// Not enough time has passed since the last purge // Not enough time has passed since the last purge
@@ -40,4 +40,4 @@ public class SlugPurger(CreatorsDbContext context)
Semaphore.Release(); Semaphore.Release();
} }
} }
} }

View File

@@ -3,12 +3,12 @@
public record JwtOptions public record JwtOptions
{ {
public const string SectionName = "Authentication:Jwt"; public const string SectionName = "Authentication:Jwt";
public required TimeSpan Lifetime { get; init; } public required TimeSpan Lifetime { get; init; }
public required string Issuer { get; init; } public required string Issuer { get; init; }
public required string Audience { get; init; } public required string Audience { get; init; }
public required string Key { get; init; } public required string Key { get; init; }
public TimeSpan RefreshTokenLifetime { get; init; } public TimeSpan RefreshTokenLifetime { get; init; }
public bool RefreshTokenRequireRotation { get; init; } public bool RefreshTokenRequireRotation { get; init; }
} }

View File

@@ -1,20 +1,18 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
namespace Hutopy.Modules.Identity.Data 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);
}
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);
} }
} }

View File

@@ -10,19 +10,22 @@ public class IdentityService(
{ {
public async Task<UserModel?> GetCurrentUserAsync() 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)) if (string.IsNullOrEmpty(currentUserId))
{ {
return null; return null;
} }
UserModel? ret; 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 else
{ {
var userModel = new UserModel UserModel userModel = new()
{ {
Id = user.Id, Id = user.Id,
Username = user.UserName ?? string.Empty, Username = user.UserName ?? string.Empty,
@@ -44,17 +47,22 @@ public class IdentityService(
public async Task<IList<string>> GetCurrentUserRolesAsync() public async Task<IList<string>> GetCurrentUserRolesAsync()
{ {
var currentUserModel = await GetCurrentUserAsync(); UserModel? currentUserModel = await GetCurrentUserAsync();
if (currentUserModel is null) return []; if (currentUserModel is null)
{
var currentUser = await userManager.FindByIdAsync(currentUserModel.Id.ToString()); 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; return userRoles;
} }
} }

View File

@@ -17,4 +17,3 @@ public class User : IdentityUser<Guid>
public DateTime RefreshTokenExpiryTime { get; set; } public DateTime RefreshTokenExpiryTime { get; set; }
public string Fullname => $"{Lastname}, {Firstname}"; public string Fullname => $"{Lastname}, {Firstname}";
} }

View File

@@ -1,5 +1,6 @@
using Hutopy.Infrastructure.Security; using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Identity.Data; using Hutopy.Modules.Identity.Data;
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Modules.Identity.Handlers; namespace Hutopy.Modules.Identity.Handlers;
@@ -22,7 +23,7 @@ public class ChangeAddressHandler(
ChangeAddressRequest request, ChangeAddressRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString()); User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null) if (user is null)
{ {
@@ -32,11 +33,15 @@ public class ChangeAddressHandler(
user.Address = request.Address; user.Address = request.Address;
var result = await userManager.UpdateAsync(user); IdentityResult result = await userManager.UpdateAsync(user);
if (result.Succeeded) if (result.Succeeded)
{
await SendOkAsync(ct); await SendOkAsync(ct);
}
else else
{
await SendUnauthorizedAsync(ct); await SendUnauthorizedAsync(ct);
}
} }
} }

View File

@@ -1,5 +1,6 @@
using Hutopy.Infrastructure.Security; using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Identity.Data; using Hutopy.Modules.Identity.Data;
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Modules.Identity.Handlers; namespace Hutopy.Modules.Identity.Handlers;
@@ -22,7 +23,7 @@ public class ChangeAliasHandler(
ChangeAliasRequest request, ChangeAliasRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString()); User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null) if (user is null)
{ {
@@ -32,11 +33,15 @@ public class ChangeAliasHandler(
user.Alias = request.Alias; user.Alias = request.Alias;
var result = await userManager.UpdateAsync(user); IdentityResult result = await userManager.UpdateAsync(user);
if (result.Succeeded) if (result.Succeeded)
{
await SendOkAsync(ct); await SendOkAsync(ct);
}
else else
{
await SendUnauthorizedAsync(ct); await SendUnauthorizedAsync(ct);
}
} }
} }

View File

@@ -1,5 +1,6 @@
using Hutopy.Infrastructure.Security; using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Identity.Data; using Hutopy.Modules.Identity.Data;
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Modules.Identity.Handlers; namespace Hutopy.Modules.Identity.Handlers;
@@ -22,7 +23,7 @@ public class ChangeBirthDateHandler(
ChangeBirthDateRequest request, ChangeBirthDateRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString()); User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null) if (user is null)
{ {
@@ -32,11 +33,15 @@ public class ChangeBirthDateHandler(
user.BirthDate = request.BirthDate; user.BirthDate = request.BirthDate;
var result = await userManager.UpdateAsync(user); IdentityResult result = await userManager.UpdateAsync(user);
if (result.Succeeded) if (result.Succeeded)
{
await SendOkAsync(ct); await SendOkAsync(ct);
}
else else
{
await SendUnauthorizedAsync(ct); await SendUnauthorizedAsync(ct);
}
} }
} }

View File

@@ -1,5 +1,6 @@
using Hutopy.Infrastructure.Security; using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Identity.Data; using Hutopy.Modules.Identity.Data;
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Modules.Identity.Handlers; namespace Hutopy.Modules.Identity.Handlers;
@@ -22,7 +23,7 @@ public class ChangeEmailHandler(
ChangeEmailRequest request, ChangeEmailRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString()); User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null) if (user is null)
{ {
@@ -33,11 +34,15 @@ public class ChangeEmailHandler(
user.Email = request.Email; user.Email = request.Email;
// TODO: check to see if identity resets the `email confirmed` flag - @jonathan // 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) if (result.Succeeded)
{
await SendOkAsync(ct); await SendOkAsync(ct);
}
else else
{
await SendUnauthorizedAsync(ct); await SendUnauthorizedAsync(ct);
}
} }
} }

View File

@@ -1,5 +1,6 @@
using Hutopy.Infrastructure.Security; using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Identity.Data; using Hutopy.Modules.Identity.Data;
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Modules.Identity.Handlers; namespace Hutopy.Modules.Identity.Handlers;
@@ -23,7 +24,7 @@ public class ChangeFullnameHandler(
ChangeFullnameRequest request, ChangeFullnameRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString()); User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null) if (user is null)
{ {
@@ -34,11 +35,15 @@ public class ChangeFullnameHandler(
user.Firstname = request.Firstname; user.Firstname = request.Firstname;
user.Lastname = request.Lastname; user.Lastname = request.Lastname;
var result = await userManager.UpdateAsync(user); IdentityResult result = await userManager.UpdateAsync(user);
if (result.Succeeded) if (result.Succeeded)
{
await SendOkAsync(ct); await SendOkAsync(ct);
}
else else
{
await SendUnauthorizedAsync(ct); await SendUnauthorizedAsync(ct);
}
} }
} }

View File

@@ -1,5 +1,6 @@
using Hutopy.Infrastructure.Security; using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Identity.Data; using Hutopy.Modules.Identity.Data;
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Modules.Identity.Handlers; namespace Hutopy.Modules.Identity.Handlers;
@@ -22,7 +23,7 @@ public class ChangePhoneHandler(
ChangePhoneRequest request, ChangePhoneRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString()); User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null) if (user is null)
{ {
@@ -33,11 +34,15 @@ public class ChangePhoneHandler(
user.PhoneNumber = request.PhoneNumber; user.PhoneNumber = request.PhoneNumber;
// TODO: check to see if identity resets the `phone confirmed` flag - @jonathan // 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) if (result.Succeeded)
{
await SendOkAsync(ct); await SendOkAsync(ct);
}
else else
{
await SendUnauthorizedAsync(ct); await SendUnauthorizedAsync(ct);
}
} }
} }

View File

@@ -1,6 +1,7 @@
using Hutopy.Infrastructure.BlobStorage.Contracts; using Hutopy.Infrastructure.BlobStorage.Contracts;
using Hutopy.Infrastructure.Security; using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Identity.Data; using Hutopy.Modules.Identity.Data;
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Modules.Identity.Handlers; namespace Hutopy.Modules.Identity.Handlers;
@@ -40,7 +41,7 @@ public class ChangePortraitHandler(
ChangePortraitRequest request, ChangePortraitRequest request,
CancellationToken ct) CancellationToken ct)
{ {
var user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString()); User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
if (user is null) if (user is null)
{ {
@@ -48,7 +49,7 @@ public class ChangePortraitHandler(
return; return;
} }
var blobUrl = await blobStorage.UploadFileAsync( string blobUrl = await blobStorage.UploadFileAsync(
ContainerNames.Users, ContainerNames.Users,
$"{user.Id}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}", $"{user.Id}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}",
request.File.OpenReadStream(), request.File.OpenReadStream(),
@@ -57,7 +58,7 @@ public class ChangePortraitHandler(
user.PortraitUrl = blobUrl; user.PortraitUrl = blobUrl;
var result = await userManager.UpdateAsync(user); IdentityResult result = await userManager.UpdateAsync(user);
if (result.Succeeded) if (result.Succeeded)
{ {

View File

@@ -17,16 +17,16 @@ public class GetCurrentUserQueryHandler(
public override async Task HandleAsync( public override async Task HandleAsync(
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var userModel = await identityService.GetCurrentUserAsync(); UserModel? userModel = await identityService.GetCurrentUserAsync();
if (userModel is null) if (userModel is null)
{ {
await SendNotFoundAsync(cancellationToken); await SendNotFoundAsync(cancellationToken);
return; return;
} }
var roles = await identityService.GetCurrentUserRolesAsync(); IList<string> roles = await identityService.GetCurrentUserRolesAsync();
await SendOkAsync( await SendOkAsync(
new UserDto new UserDto
{ {

View File

@@ -1,5 +1,6 @@
using Hutopy.Infrastructure.BlobStorage.Contracts; using Hutopy.Infrastructure.BlobStorage.Contracts;
using Hutopy.Modules.Identity.Data; using Hutopy.Modules.Identity.Data;
using Hutopy.Modules.Identity.Models;
namespace Hutopy.Modules.Identity.Handlers; namespace Hutopy.Modules.Identity.Handlers;
@@ -19,7 +20,7 @@ public class GetCurrentUserPortraitHandler(
public override async Task HandleAsync( public override async Task HandleAsync(
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var identityUser = await identityService.GetCurrentUserAsync(); UserModel? identityUser = await identityService.GetCurrentUserAsync();
if (identityUser is null) if (identityUser is null)
{ {
@@ -27,7 +28,7 @@ public class GetCurrentUserPortraitHandler(
return; return;
} }
var stream = await blobStorage.DownloadFileAsync( MemoryStream stream = await blobStorage.DownloadFileAsync(
ContainerNames.Users, ContainerNames.Users,
$"{identityUser.Id.ToString()}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}", $"{identityUser.Id.ToString()}/{SubDirectoryNames.Profile}/{CommonFileNames.ProfilePicture}",
cancellationToken); cancellationToken);

View File

@@ -32,7 +32,7 @@ public class RefreshTokenHandler(
CancellationToken ct) CancellationToken ct)
{ {
// Find the user using the refresh token // Find the user using the refresh token
var user = await userManager.Users User? user = await userManager.Users
.FirstOrDefaultAsync(u => u.RefreshToken == request.RefreshToken, ct); .FirstOrDefaultAsync(u => u.RefreshToken == request.RefreshToken, ct);
if (user == null || user.RefreshTokenExpiryTime <= DateTime.UtcNow) if (user == null || user.RefreshTokenExpiryTime <= DateTime.UtcNow)
@@ -52,20 +52,20 @@ public class RefreshTokenHandler(
await userManager.UpdateAsync(user); await userManager.UpdateAsync(user);
// Generate a new access token // Generate a new access token
var accessToken = JwtTokenHelper.GenerateJwtToken( string accessToken = JwtTokenHelper.GenerateJwtToken(
expiresIn: jwtOptions.Value.Lifetime, jwtOptions.Value.Lifetime,
issuer: jwtOptions.Value.Issuer, jwtOptions.Value.Issuer,
audience: jwtOptions.Value.Audience, jwtOptions.Value.Audience,
key: jwtOptions.Value.Key, jwtOptions.Value.Key,
userId: user.Id.ToString(), user.Id.ToString(),
email: user.Email ?? string.Empty, user.Email ?? string.Empty,
alias: user.Alias, user.Alias,
firstname: user.Firstname ?? string.Empty, user.Firstname ?? string.Empty,
lastname: user.Lastname ?? string.Empty, user.Lastname ?? string.Empty,
portraitUrl: user.PortraitUrl); user.PortraitUrl);
await SendOkAsync( await SendOkAsync(
new RefreshTokenResponse(accessToken, user.RefreshToken), new RefreshTokenResponse(accessToken, user.RefreshToken),
cancellation: ct); ct);
} }
} }

View File

@@ -1,4 +1,5 @@
using Hutopy.Modules.Identity.Data; using Hutopy.Modules.Identity.Data;
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Modules.Identity.Handlers; namespace Hutopy.Modules.Identity.Handlers;
@@ -25,7 +26,7 @@ public class ResetPasswordHandler(
CancellationToken ct) CancellationToken ct)
{ {
// Find user by email // Find user by email
var user = await userManager.FindByEmailAsync(request.Email); User? user = await userManager.FindByEmailAsync(request.Email);
if (user is null) if (user is null)
{ {
await SendStringAsync( await SendStringAsync(
@@ -36,7 +37,7 @@ public class ResetPasswordHandler(
} }
// Reset password with token // Reset password with token
var result = await userManager.ResetPasswordAsync( IdentityResult result = await userManager.ResetPasswordAsync(
user, user,
request.Token, request.Token,
request.NewPassword); request.NewPassword);

View File

@@ -1,5 +1,6 @@
using Hutopy.Infrastructure.Security; using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Identity.Data; using Hutopy.Modules.Identity.Data;
using Microsoft.AspNetCore.Identity;
namespace Hutopy.Modules.Identity.Handlers; namespace Hutopy.Modules.Identity.Handlers;
@@ -23,19 +24,19 @@ public class SetPasswordHandler(
CancellationToken ct) CancellationToken ct)
{ {
// Get current user id from claims // Get current user id from claims
var userId = User.GetUserId().ToString(); string userId = User.GetUserId().ToString();
// Get user from database // Get user from database
var user = await userManager.FindByIdAsync(userId); User? user = await userManager.FindByIdAsync(userId);
if (user is null) if (user is null)
{ {
await SendForbiddenAsync(ct); await SendForbiddenAsync(ct);
return; return;
} }
var resetToken = await userManager.GeneratePasswordResetTokenAsync(user); string resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
var result = await userManager.ResetPasswordAsync(user, resetToken, request.NewPassword); IdentityResult result = await userManager.ResetPasswordAsync(user, resetToken, request.NewPassword);
if (!result.Succeeded) if (!result.Succeeded)
{ {
await SendStringAsync( await SendStringAsync(
@@ -44,7 +45,7 @@ public class SetPasswordHandler(
cancellation: ct); cancellation: ct);
return; return;
} }
await SendOkAsync(ct); await SendOkAsync(ct);
} }
} }

View File

@@ -1,12 +1,12 @@
namespace Hutopy.Modules.Identity.Models; namespace Hutopy.Modules.Identity.Models;
public class Result( public class Result(
bool succeeded, bool succeeded,
IEnumerable<string> errors) IEnumerable<string> errors)
{ {
public bool Succeeded { get; init; } = succeeded; public bool Succeeded { get; init; } = succeeded;
public string[] Errors { get; init; } = errors.ToArray(); public string[] Errors { get; init; } = errors.ToArray();
public static Result Success() public static Result Success()
{ {
return new Result(true, Array.Empty<string>()); return new Result(true, Array.Empty<string>());
@@ -20,18 +20,18 @@ public class Result(
public class Result<T>( public class Result<T>(
T? value, T? value,
bool succeeded, bool succeeded,
IEnumerable<string> errors) IEnumerable<string> errors)
{ {
public bool Succeeded { get; init; } = succeeded; public bool Succeeded { get; init; } = succeeded;
public string[] Errors { get; init; } = errors.ToArray(); public string[] Errors { get; init; } = errors.ToArray();
public T? Value { get; set; } = value; public T? Value { get; set; } = value;
public T GetValueOrDefault() public T GetValueOrDefault()
{ {
return Value ?? default(T)!; return Value ?? default(T)!;
} }
public string GetErrorsAsString() public string GetErrorsAsString()
{ {
return Errors.Length == 0 ? string.Empty : string.Join(", ", Errors); return Errors.Length == 0 ? string.Empty : string.Join(", ", Errors);

View File

@@ -9,7 +9,7 @@ public sealed class UserLookup(
{ {
public async Task<UserReference?> GetUserAsync(Guid userId, CancellationToken cancellationToken = default) 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 return user is null
? null ? null

View File

@@ -14,7 +14,7 @@ public class Membership
public DateTimeOffset? StartDate { get; set; } public DateTimeOffset? StartDate { get; set; }
public DateTimeOffset? EndDate { get; set; } public DateTimeOffset? EndDate { get; set; }
public bool IsActive => EndDate == null || EndDate > DateTimeOffset.UtcNow; 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; } = []; public ICollection<Payment> Payments { get; set; } = [];
} }

View File

@@ -9,8 +9,8 @@ public sealed class MembershipsDbContext(
public DbSet<MembershipTier> MembershipTiers => Set<MembershipTier>(); public DbSet<MembershipTier> MembershipTiers => Set<MembershipTier>();
public DbSet<Membership> Memberships => Set<Membership>(); public DbSet<Membership> Memberships => Set<Membership>();
public DbSet<Payment> Payments => Set<Payment>(); public DbSet<Payment> Payments => Set<Payment>();
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.HasDefaultSchema(SchemaName); modelBuilder.HasDefaultSchema(SchemaName);
@@ -20,7 +20,7 @@ public sealed class MembershipsDbContext(
.Property(c => c.CreatedAt) .Property(c => c.CreatedAt)
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP"); .HasDefaultValueSql("CURRENT_TIMESTAMP");
modelBuilder modelBuilder
.Entity<Membership>() .Entity<Membership>()
.Property(c => c.CreatedAt) .Property(c => c.CreatedAt)

View File

@@ -9,5 +9,4 @@ public class Payment
public decimal Amount { get; set; } public decimal Amount { get; set; }
[MaxLength(8)] public required string Currency { get; set; } [MaxLength(8)] public required string Currency { get; set; }
[MaxLength(2048)] public required string InvoiceUrl { get; set; } [MaxLength(2048)] public required string InvoiceUrl { get; set; }
} }

View File

@@ -13,19 +13,19 @@ public static class DependencyInjection
builder.Services.AddDbContext<MembershipsDbContext>(configureAction); builder.Services.AddDbContext<MembershipsDbContext>(configureAction);
builder.Services.AddTransient<IMembershipNotifier, MembershipNotifier>(); builder.Services.AddTransient<IMembershipNotifier, MembershipNotifier>();
return builder; return builder;
} }
public static async Task<IApplicationBuilder> UseMembershipModuleAsync( public static async Task<IApplicationBuilder> UseMembershipModuleAsync(
this IApplicationBuilder app, this IApplicationBuilder app,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); IServiceScopeFactory scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
using var scope = scopeFactory.CreateScope(); using IServiceScope scope = scopeFactory.CreateScope();
await using var context = scope.ServiceProvider.GetRequiredService<MembershipsDbContext>(); await using MembershipsDbContext context = scope.ServiceProvider.GetRequiredService<MembershipsDbContext>();
await context.Database.MigrateAsync(cancellationToken: cancellationToken); await context.Database.MigrateAsync(cancellationToken);
return app; return app;
} }

View File

@@ -19,18 +19,18 @@ public class CancelMembershipHandler(
Delete("/api/memberships"); Delete("/api/memberships");
Options(o => o.WithTags("Memberships")); Options(o => o.WithTags("Memberships"));
} }
public override async Task HandleAsync( public override async Task HandleAsync(
CancelMembershipRequest req, CancelMembershipRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var subscription = await dbContext Membership? subscription = await dbContext
.Memberships .Memberships
.FindAsync( .FindAsync(
[req.SubscriptionId], [req.SubscriptionId],
cancellationToken: ct); ct);
if (subscription is not { EndDate: null } if (subscription is not { EndDate: null }
|| subscription.StripeSubscriptionId is null) || subscription.StripeSubscriptionId is null)
{ {
await SendNotFoundAsync(ct); await SendNotFoundAsync(ct);

View File

@@ -27,9 +27,9 @@ public class CreateMembershipTierEndpoint(
CreateMembershipTierRequest req, CreateMembershipTierRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var tierId = Guid.CreateVersion7(); Guid tierId = Guid.CreateVersion7();
var productId = await membershipTierProcessor.CreateAsync( string productId = await membershipTierProcessor.CreateAsync(
req.CreatorId, req.CreatorId,
tierId, tierId,
req.Name, req.Name,
@@ -37,14 +37,14 @@ public class CreateMembershipTierEndpoint(
req.Price); req.Price);
// Record the new Tier // Record the new Tier
var tier = new MembershipTier MembershipTier tier = new()
{ {
Id = tierId, Id = tierId,
CreatorId = req.CreatorId, CreatorId = req.CreatorId,
Price = req.Price, Price = req.Price,
Name = req.Name, Name = req.Name,
Description = req.Description, Description = req.Description,
StripeProductId = productId, StripeProductId = productId
}; };
dbContext.MembershipTiers.Add(tier); dbContext.MembershipTiers.Add(tier);

View File

@@ -28,16 +28,16 @@ public class GetActiveMembershipsHandler(
public override async Task HandleAsync( public override async Task HandleAsync(
CancellationToken ct) CancellationToken ct)
{ {
var subscriptions = await dbContext List<Membership> subscriptions = await dbContext
.Memberships .Memberships
.Where(subscription => subscription.UserId == User.GetUserId()) .Where(subscription => subscription.UserId == User.GetUserId())
.Where(subscription => subscription.State == MembershipState.Active) .Where(subscription => subscription.State == MembershipState.Active)
.ToListAsync(ct); .ToListAsync(ct);
var result = await Task.WhenAll( GetActiveMembershipsResponse[] result = await Task.WhenAll(
subscriptions.Select(async subscription => subscriptions.Select(async subscription =>
{ {
var creator = await creatorLookup.GetCreatorAsync(subscription.CreatorId, ct); CreatorReference? creator = await creatorLookup.GetCreatorAsync(subscription.CreatorId, ct);
return new GetActiveMembershipsResponse( return new GetActiveMembershipsResponse(
subscription.Id, subscription.Id,

View File

@@ -4,7 +4,7 @@ namespace Hutopy.Modules.Memberships.Handlers;
[PublicAPI] [PublicAPI]
public record GetMembershipTiersRequest public record GetMembershipTiersRequest
{ {
public Guid CreatorId { get; set; } public Guid CreatorId { get; set; }
} }
@@ -34,7 +34,7 @@ public class GetMembershipTiersEndpoint(
GetMembershipTiersRequest req, GetMembershipTiersRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var tiers = await dbContext List<TierModel> tiers = await dbContext
.MembershipTiers .MembershipTiers
.Where(tier => tier.CreatorId == req.CreatorId) .Where(tier => tier.CreatorId == req.CreatorId)
.Select(tier => new TierModel( .Select(tier => new TierModel(

View File

@@ -44,7 +44,7 @@ public class SubscribeHandler(
SubscribeRequest req, SubscribeRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var tier = await dbContext MembershipTier? tier = await dbContext
.MembershipTiers .MembershipTiers
.Where(tier => tier.Id == req.MembershipTierId) .Where(tier => tier.Id == req.MembershipTierId)
.FirstOrDefaultAsync(ct); .FirstOrDefaultAsync(ct);
@@ -54,7 +54,7 @@ public class SubscribeHandler(
return; return;
} }
var creator = await creatorLookup.GetCreatorAsync(tier.CreatorId, ct); CreatorReference? creator = await creatorLookup.GetCreatorAsync(tier.CreatorId, ct);
if (creator == null) if (creator == null)
{ {
await SendNotFoundAsync(ct); await SendNotFoundAsync(ct);
@@ -68,7 +68,7 @@ public class SubscribeHandler(
} }
// Process Stripe subscription // Process Stripe subscription
var checkoutSession = await membershipPaymentProcessor.CreateCheckoutSessionAsync( MembershipCheckoutSession checkoutSession = await membershipPaymentProcessor.CreateCheckoutSessionAsync(
User.GetUserId(), User.GetUserId(),
creator, creator,
tier.Id, tier.Id,
@@ -78,6 +78,6 @@ public class SubscribeHandler(
await SendOkAsync( await SendOkAsync(
new SubscriptionResponse { StripeCheckoutUrl = checkoutSession.Url }, new SubscriptionResponse { StripeCheckoutUrl = checkoutSession.Url },
cancellation: ct); ct);
} }
} }

View File

@@ -15,7 +15,7 @@ public class MembershipNotifier(
string tierId, string tierId,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var membership = new Membership Membership membership = new()
{ {
Id = Guid.CreateVersion7(), Id = Guid.CreateVersion7(),
CreatedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow,
@@ -40,17 +40,17 @@ public class MembershipNotifier(
string currency, string currency,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var membership = await dbContext Membership? membership = await dbContext
.Memberships .Memberships
.SingleOrDefaultAsync( .SingleOrDefaultAsync(
m => m.StripeSubscriptionId == stripeSubscriptionId, m => m.StripeSubscriptionId == stripeSubscriptionId,
cancellationToken: cancellationToken); cancellationToken);
if (membership is null) if (membership is null)
{ {
return; return;
} }
var payment = new Payment Payment payment = new()
{ {
Id = Guid.CreateVersion7(), Id = Guid.CreateVersion7(),
CreatedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow,
@@ -73,11 +73,11 @@ public class MembershipNotifier(
DateTimeOffset? endDate, DateTimeOffset? endDate,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var membership = await dbContext Membership? membership = await dbContext
.Memberships .Memberships
.SingleOrDefaultAsync( .SingleOrDefaultAsync(
s => s.StripeSubscriptionId == subscriptionId, s => s.StripeSubscriptionId == subscriptionId,
cancellationToken: cancellationToken); cancellationToken);
if (membership == null) if (membership == null)
{ {
return; return;
@@ -92,11 +92,11 @@ public class MembershipNotifier(
string subscriptionId, string subscriptionId,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var membership = await dbContext Membership? membership = await dbContext
.Memberships .Memberships
.SingleOrDefaultAsync( .SingleOrDefaultAsync(
s => s.StripeSubscriptionId == subscriptionId, s => s.StripeSubscriptionId == subscriptionId,
cancellationToken: cancellationToken); cancellationToken);
if (membership == null) if (membership == null)
{ {
return; return;

View File

@@ -10,6 +10,8 @@ public class MessagingDbContext(
{ {
public const string SchemaName = "Messaging"; public const string SchemaName = "Messaging";
public DbSet<Message> Messages { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.HasDefaultSchema(SchemaName); modelBuilder.HasDefaultSchema(SchemaName);
@@ -21,8 +23,6 @@ public class MessagingDbContext(
.HasDefaultValueSql("CURRENT_TIMESTAMP"); .HasDefaultValueSql("CURRENT_TIMESTAMP");
} }
public DbSet<Message> Messages { get; set; }
public async Task<IEnumerable<MessageDto>> GetMessagesAsync( public async Task<IEnumerable<MessageDto>> GetMessagesAsync(
Guid subjectId, Guid subjectId,
Guid? parentId, Guid? parentId,
@@ -30,7 +30,7 @@ public class MessagingDbContext(
int pageSize, int pageSize,
CancellationToken ct = default) CancellationToken ct = default)
{ {
var query = Messages IQueryable<Message> query = Messages
.Where(c => c.SubjectId == subjectId) .Where(c => c.SubjectId == subjectId)
.Where(c => c.ParentId == parentId); .Where(c => c.ParentId == parentId);
@@ -39,7 +39,7 @@ public class MessagingDbContext(
var lastMessage = await Messages var lastMessage = await Messages
.Where(c => c.Id == lastId.Value) .Where(c => c.Id == lastId.Value)
.Select(c => new { c.CreatedAt, c.Id }) .Select(c => new { c.CreatedAt, c.Id })
.FirstOrDefaultAsync(cancellationToken: ct); .FirstOrDefaultAsync(ct);
if (lastMessage != null) if (lastMessage != null)
{ {
@@ -49,17 +49,17 @@ public class MessagingDbContext(
} }
} }
var messages = await query List<Message> messages = await query
.OrderByDescending(c => c.CreatedAt) .OrderByDescending(c => c.CreatedAt)
.ThenByDescending(c => c.Id) .ThenByDescending(c => c.Id)
.Take(pageSize) .Take(pageSize)
.ToListAsync(cancellationToken: ct); .ToListAsync(ct);
var result = await Task.WhenAll( MessageDto[] result = await Task.WhenAll(
messages.Select(async message => messages.Select(async message =>
{ {
var writer = await userLookup.GetUserAsync(message.CreatedBy, ct); UserReference? writer = await userLookup.GetUserAsync(message.CreatedBy, ct);
return new MessageDto( return new MessageDto(
message.Id, message.Id,
message.SubjectId, message.SubjectId,
@@ -80,11 +80,11 @@ public class MessagingDbContext(
int pageSize, int pageSize,
CancellationToken ct = default) CancellationToken ct = default)
{ {
var query = Messages IQueryable<Message> query = Messages
.Where(c => c.SubjectId == subjectId) .Where(c => c.SubjectId == subjectId)
.Where(c => c.ParentId == parentId); .Where(c => c.ParentId == parentId);
var messageCount = await query int messageCount = await query
.Take(pageSize) .Take(pageSize)
.CountAsync(ct); .CountAsync(ct);

View File

@@ -9,7 +9,7 @@ public static class DependencyInjection
Action<DbContextOptionsBuilder>? configureAction = null) Action<DbContextOptionsBuilder>? configureAction = null)
{ {
builder.Services.AddDbContext<MessagingDbContext>(configureAction); builder.Services.AddDbContext<MessagingDbContext>(configureAction);
return builder; return builder;
} }
@@ -17,10 +17,10 @@ public static class DependencyInjection
this IApplicationBuilder app, this IApplicationBuilder app,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); IServiceScopeFactory scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
using var scope = scopeFactory.CreateScope(); using IServiceScope scope = scopeFactory.CreateScope();
await using var context = scope.ServiceProvider.GetRequiredService<MessagingDbContext>(); await using MessagingDbContext context = scope.ServiceProvider.GetRequiredService<MessagingDbContext>();
await context.Database.MigrateAsync(cancellationToken: cancellationToken); await context.Database.MigrateAsync(cancellationToken);
return app; return app;
} }

View File

@@ -40,14 +40,14 @@ public class AddMessage(
AddMessageRequest req, AddMessageRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var message = new Message Message message = new()
{ {
Id = req.Id ?? Guid.CreateVersion7(), Id = req.Id ?? Guid.CreateVersion7(),
SubjectId = req.SubjectId, SubjectId = req.SubjectId,
CreatedBy = User.GetUserId(), CreatedBy = User.GetUserId(),
Value = req.Message Value = req.Message
}; };
await context.Messages.AddAsync(message, ct); await context.Messages.AddAsync(message, ct);
await context.SaveChangesAsync(ct); await context.SaveChangesAsync(ct);

View File

@@ -20,7 +20,7 @@ internal sealed class AddReplyRequestValidator
RuleFor(r => r.ParentId) RuleFor(r => r.ParentId)
.NotNull().WithMessage("You must specify a ParentId") .NotNull().WithMessage("You must specify a ParentId")
.NotEmpty().WithMessage("You must specify a non-empty ParentId"); .NotEmpty().WithMessage("You must specify a non-empty ParentId");
RuleFor(r => r.SubjectId) RuleFor(r => r.SubjectId)
.NotNull().WithMessage("You must specify a SubjectId") .NotNull().WithMessage("You must specify a SubjectId")
.NotEmpty().WithMessage("You must specify a non-empty SubjectId"); .NotEmpty().WithMessage("You must specify a non-empty SubjectId");
@@ -45,10 +45,10 @@ internal sealed class AddReply(
AddReplyRequest req, AddReplyRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var message = new Message Message message = new()
{ {
Id = Guid.CreateVersion7(), Id = Guid.CreateVersion7(),
SubjectId = req.SubjectId, SubjectId = req.SubjectId,
ParentId = req.ParentId, ParentId = req.ParentId,
CreatedBy = User.GetUserId(), CreatedBy = User.GetUserId(),
Value = req.Message Value = req.Message

View File

@@ -39,15 +39,15 @@ public class ChangeMessage(
ChangeMessageRequest req, ChangeMessageRequest req,
CancellationToken ct) 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) if (message is null)
{ {
await SendNotFoundAsync(ct); await SendNotFoundAsync(ct);
return; return;
} }
var userId = HttpContext.User.GetUserId(); Guid userId = HttpContext.User.GetUserId();
if (message.CreatedBy != userId) if (message.CreatedBy != userId)
{ {
await SendForbiddenAsync(ct); await SendForbiddenAsync(ct);
@@ -58,7 +58,7 @@ public class ChangeMessage(
message.Value = req.Message; message.Value = req.Message;
context.Update(message); context.Update(message);
await context.SaveChangesAsync(ct); await context.SaveChangesAsync(ct);
} }
} }

View File

@@ -30,21 +30,21 @@ public class DeleteMessage(
DeleteMessageRequest req, DeleteMessageRequest req,
CancellationToken ct) 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) if (message is null)
{ {
await SendNotFoundAsync(ct); await SendNotFoundAsync(ct);
return; return;
} }
var userId = HttpContext.User.GetUserId(); Guid userId = HttpContext.User.GetUserId();
if (message.CreatedBy != userId) if (message.CreatedBy != userId)
{ {
await SendForbiddenAsync(ct); await SendForbiddenAsync(ct);
return; return;
} }
context.Messages.Remove(message); context.Messages.Remove(message);
await context.SaveChangesAsync(ct); await context.SaveChangesAsync(ct);

View File

@@ -28,17 +28,14 @@ public class GetMessageCount(
GetMessageCountRequest req, GetMessageCountRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var messageCount = await context.GetMessageCountAsync( int messageCount = await context.GetMessageCountAsync(
req.SubjectId, req.SubjectId,
null, null,
req.PageSize, req.PageSize,
ct); ct);
await SendAsync( await SendAsync(
new() new GetMessageCountResponse { Count = messageCount },
{
Count = messageCount
},
cancellation: ct); cancellation: ct);
} }
} }

View File

@@ -30,13 +30,13 @@ public class GetMessages(
GetMessagesRequest req, GetMessagesRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var messages = await context.GetMessagesAsync( IEnumerable<MessageDto> messages = await context.GetMessagesAsync(
req.SubjectId, req.SubjectId,
null, null,
req.LastId, req.LastId,
req.PageSize, req.PageSize,
ct); ct);
await SendOkAsync(new GetMessagesResponse(messages), ct); await SendOkAsync(new GetMessagesResponse(messages), ct);
} }
} }

View File

@@ -30,16 +30,16 @@ public class GetMessagesByUser(
GetMessagesByUserRequest req, GetMessagesByUserRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var messages = await context List<Message> messages = await context
.Messages .Messages
.Where(c => c.CreatedBy == req.UserId) .Where(c => c.CreatedBy == req.UserId)
.Where(c => c.ParentId == null) .Where(c => c.ParentId == null)
.ToListAsync(cancellationToken: ct); .ToListAsync(ct);
var result = await Task.WhenAll( MessageDto[] result = await Task.WhenAll(
messages.Select(async message => messages.Select(async message =>
{ {
var user = await userLookup.GetUserAsync(message.CreatedBy, ct); UserReference? user = await userLookup.GetUserAsync(message.CreatedBy, ct);
return new MessageDto return new MessageDto
{ {

View File

@@ -31,7 +31,7 @@ public class GetReplies(
GetRepliesRequest req, GetRepliesRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var replies = await context.GetMessagesAsync( IEnumerable<MessageDto> replies = await context.GetMessagesAsync(
req.SubjectId, req.SubjectId,
req.ParentId, req.ParentId,
req.LastId, req.LastId,

View File

@@ -17,5 +17,5 @@ public class Tip : Entity
public enum TipStatus : short public enum TipStatus : short
{ {
Pending = 0, Pending = 0,
Paid = 1, Paid = 1
} }

View File

@@ -7,8 +7,8 @@ public sealed class TippingDbContext(
public const string SchemaName = "Tipping"; public const string SchemaName = "Tipping";
public DbSet<Tip> Tips => Set<Tip>(); public DbSet<Tip> Tips => Set<Tip>();
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.HasDefaultSchema(SchemaName); modelBuilder.HasDefaultSchema(SchemaName);

View File

@@ -13,7 +13,7 @@ public static class DependencyInjection
{ {
builder.Services.AddDbContext<TippingDbContext>(configureAction); builder.Services.AddDbContext<TippingDbContext>(configureAction);
builder.Services.AddTransient<ITipPaymentNotifier, TipPaymentNotifier>(); builder.Services.AddTransient<ITipPaymentNotifier, TipPaymentNotifier>();
return builder; return builder;
} }
@@ -21,10 +21,10 @@ public static class DependencyInjection
this IApplicationBuilder app, this IApplicationBuilder app,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); IServiceScopeFactory scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
using var scope = scopeFactory.CreateScope(); using IServiceScope scope = scopeFactory.CreateScope();
await using var context = scope.ServiceProvider.GetRequiredService<MessagingDbContext>(); await using MessagingDbContext context = scope.ServiceProvider.GetRequiredService<MessagingDbContext>();
await context.Database.MigrateAsync(cancellationToken: cancellationToken); await context.Database.MigrateAsync(cancellationToken);
return app; return app;
} }

View File

@@ -24,16 +24,16 @@ public class GetReceivedTipsHandler(
public override async Task HandleAsync( public override async Task HandleAsync(
CancellationToken ct) CancellationToken ct)
{ {
var tips = await dbContext List<Tip> tips = await dbContext
.Tips .Tips
.Where(tip => tip.CreatorId == User.GetUserId()) .Where(tip => tip.CreatorId == User.GetUserId())
.ToListAsync(ct); .ToListAsync(ct);
var result = await Task.WhenAll( TipReceivedModel[] result = await Task.WhenAll(
tips.Select(async tip => tips.Select(async tip =>
{ {
var tipper = await userLookup.GetUserAsync(tip.CreatorId, ct); UserReference? tipper = await userLookup.GetUserAsync(tip.CreatorId, ct);
return new TipReceivedModel( return new TipReceivedModel(
tip.Id, tip.Id,
tip.CreatedAt, tip.CreatedAt,

View File

@@ -18,14 +18,17 @@ using NSwag;
using NSwag.Generation.AspNetCore.Processors; using NSwag.Generation.AspNetCore.Processors;
using NSwag.Generation.Processors.Security; using NSwag.Generation.Processors.Security;
var builder = WebApplication.CreateBuilder(args); WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
if (!builder.Environment.IsDevelopment()) if (!builder.Environment.IsDevelopment())
{ {
var vaultUri = Environment.GetEnvironmentVariable("VaultUri"); string? vaultUri = Environment.GetEnvironmentVariable("VaultUri");
if (vaultUri is null) throw new InvalidOperationException("Missing VaultUri configuration setting"); if (vaultUri is null)
{
throw new InvalidOperationException("Missing VaultUri configuration setting");
}
builder.Configuration.AddAzureKeyVault(new Uri(vaultUri), new DefaultAzureCredential()); builder.Configuration.AddAzureKeyVault(new Uri(vaultUri), new DefaultAzureCredential());
} }
@@ -60,15 +63,16 @@ builder.Services.AddOpenApiDocument((
Type = OpenApiSecuritySchemeType.ApiKey, Type = OpenApiSecuritySchemeType.ApiKey,
Name = "Authorization", Name = "Authorization",
In = OpenApiSecurityApiKeyLocation.Header, In = OpenApiSecurityApiKeyLocation.Header,
Description = "Type into the textbox: Bearer {your JWT token}.", Description = "Type into the textbox: Bearer {your JWT token}."
}); });
configure.OperationProcessors.Add(new AspNetCoreOperationTagsProcessor()); configure.OperationProcessors.Add(new AspNetCoreOperationTagsProcessor());
configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT"));
}); });
var postgresConnectionString = builder.Configuration.GetConnectionString("PostgresConnection") string postgresConnectionString = builder.Configuration.GetConnectionString("PostgresConnection")
?? throw new InvalidOperationException("Missing ConnectionStrings:PostgresConnection"); ?? throw new InvalidOperationException(
"Missing ConnectionStrings:PostgresConnection");
builder.Services.AddFastEndpoints(); builder.Services.AddFastEndpoints();
builder.AddInfrastructureModule(); builder.AddInfrastructureModule();
@@ -84,10 +88,9 @@ builder.AddContentModule(options =>
options.UseNpgsql( options.UseNpgsql(
postgresConnectionString, postgresConnectionString,
o => o.MigrationsHistoryTable("__EFMigrationsHistory", ContentsDbContext.SchemaName))); o => o.MigrationsHistoryTable("__EFMigrationsHistory", ContentsDbContext.SchemaName)));
builder.AddMembershipModule( builder.AddMembershipModule(options => options.UseNpgsql(
options => options.UseNpgsql( postgresConnectionString,
postgresConnectionString, o => o.MigrationsHistoryTable("__EFMigrationsHistory", MembershipsDbContext.SchemaName)));
o => o.MigrationsHistoryTable("__EFMigrationsHistory", MembershipsDbContext.SchemaName)));
builder.AddTippingModule(options => builder.AddTippingModule(options =>
options.UseNpgsql( options.UseNpgsql(
postgresConnectionString, postgresConnectionString,
@@ -97,7 +100,7 @@ builder.AddMessagingModule(options =>
postgresConnectionString, postgresConnectionString,
o => o.MigrationsHistoryTable("__EFMigrationsHistory", MessagingDbContext.SchemaName))); o => o.MigrationsHistoryTable("__EFMigrationsHistory", MessagingDbContext.SchemaName)));
var app = builder.Build(); WebApplication app = builder.Build();
app.UseForwardedHeaders( app.UseForwardedHeaders(
new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedProto } new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedProto }

View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Custom Rules" ToolsVersion="16.0"> <RuleSet Name="Custom Rules" ToolsVersion="16.0">
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp" RuleNamespace="Microsoft.CodeAnalysis.CSharp"> <Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp" RuleNamespace="Microsoft.CodeAnalysis.CSharp">
<Rule Id="CS1591" Action="None" /> <!-- Missing XML comment --> <Rule Id="SA1600" Action="None"/> <!-- Elements should be documented -->
<Rule Id="SA1633" Action="None"/> <!-- Missing File header comment -->
</Rules> </Rules>
<Rules AnalyzerId="SonarAnalyzer.CSharp" RuleNamespace="SonarAnalyzer.CSharp"> <Rules AnalyzerId="SonarAnalyzer.CSharp" RuleNamespace="SonarAnalyzer.CSharp">
<!-- <Rule Id="S1118" Action="Warning" /> Utility classes should not have public constructors --> <!-- <Rule Id="S1118" Action="Warning" /> Utility classes should not have public constructors -->

View File

@@ -1,6 +1,6 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"PostgresConnection": "Server=localhost,5432;Database=Hutopy;Uid=sa;Pwd=P@ssword123!;" "PostgresConnection": "Server=localhost,5432;Database=Hutopy;Uid=sa;Pwd=P@ssword123!;"
}, },
"Stripe": { "Stripe": {
"SecretKey": "sk_test_51OoveVDrRyqXtNdBaOs1DFFja0XhrQtJoAo83uSySMuqw4Wyt9NsuugrIHRqet9a50cr5GvolpTP8EZuTSttcgYx00gOUPNDoI", "SecretKey": "sk_test_51OoveVDrRyqXtNdBaOs1DFFja0XhrQtJoAo83uSySMuqw4Wyt9NsuugrIHRqet9a50cr5GvolpTP8EZuTSttcgYx00gOUPNDoI",