chore(codebase): full cleanup pass
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 =>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
/// Soft‑delete flag (false by default, true once DeletedAt is set)
|
/// Soft‑delete 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; }
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -17,5 +17,5 @@ public class Tip : Entity
|
|||||||
public enum TipStatus : short
|
public enum TipStatus : short
|
||||||
{
|
{
|
||||||
Pending = 0,
|
Pending = 0,
|
||||||
Paid = 1,
|
Paid = 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user