many fixes and improvements - rework for modules/ and common/
feat(emailer): add Postmark and Resend providers
This commit is contained in:
2
.github/workflows/backend-ci.yml
vendored
2
.github/workflows/backend-ci.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- name: dotnet build and publish
|
- name: dotnet build and publish
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
cd backend
|
||||||
dotnet publish --configuration Release --artifacts-path ./publish/
|
dotnet publish --configuration Release --artifacts-path ./publish/ backend.sln
|
||||||
|
|
||||||
# Deploy to Azure WebApp
|
# Deploy to Azure WebApp
|
||||||
- name: Deploy to Azure WebApp
|
- name: Deploy to Azure WebApp
|
||||||
|
|||||||
30
Stripe.md
Normal file
30
Stripe.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Stripe
|
||||||
|
|
||||||
|
## Events Workflow
|
||||||
|
|
||||||
|
### Membership
|
||||||
|
|
||||||
|
1. checkout.session.completed
|
||||||
|
- Store StripeSubscriptionId, UserId, CreatorId, TierId
|
||||||
|
- Save a new Subscription entity with the status "Pending"
|
||||||
|
|
||||||
|
2. invoice.payment_succeeded
|
||||||
|
- Grant access (set Subscription.Active = true or similar)
|
||||||
|
- Record transaction or set StartDate
|
||||||
|
- Notify Creator (e.g., new member)
|
||||||
|
|
||||||
|
3. customer.subscription.updated
|
||||||
|
- Update `EndDate = CancelAt ?? CanceledAt`
|
||||||
|
|
||||||
|
4. customer.subscription.deleted
|
||||||
|
- Revoke access
|
||||||
|
- Mark Subscription as inactive/ended
|
||||||
|
|
||||||
|
### Tips
|
||||||
|
|
||||||
|
1. checkout.session.completed
|
||||||
|
- Store TipId, StripeSessionId, TipperId, CreatorId
|
||||||
|
- PaymentIntentStatus == "paid"
|
||||||
|
- Status = "Paid"
|
||||||
|
- Notify creator
|
||||||
|
- Record transaction
|
||||||
13
backend/.github/FUNDING.yml
vendored
13
backend/.github/FUNDING.yml
vendored
@@ -1,13 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: JasonTaylorDev # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
|
||||||
# patreon: # Replace with a single Patreon username
|
|
||||||
# open_collective: # Replace with a single Open Collective username
|
|
||||||
# ko_fi: # Replace with a single Ko-fi username
|
|
||||||
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
# liberapay: # Replace with a single Liberapay username
|
|
||||||
# issuehunt: # Replace with a single IssueHunt username
|
|
||||||
# otechie: # Replace with a single Otechie username
|
|
||||||
# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
||||||
# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
||||||
10
backend/Common/Domain/Entity.cs
Normal file
10
backend/Common/Domain/Entity.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Hutopy.Common.Domain;
|
||||||
|
|
||||||
|
public abstract class Entity
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
public Guid CreatedBy { get; init; }
|
||||||
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
public Guid? DeletedBy { get; set; }
|
||||||
|
public DateTimeOffset? DeletedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Azure.Identity;
|
using Hutopy.Modules.Identity.Data;
|
||||||
using Hutopy.Web.Features.Users.Data;
|
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
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;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
namespace Hutopy.Web;
|
namespace Hutopy;
|
||||||
|
|
||||||
public static class DependencyInjection
|
public static class DependencyInjection
|
||||||
{
|
{
|
||||||
@@ -19,7 +17,7 @@ public static class DependencyInjection
|
|||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
|
|
||||||
services.AddHealthChecks()
|
services.AddHealthChecks()
|
||||||
.AddDbContextCheck<ApplicationDbContext>();
|
.AddDbContextCheck<IdentityDbContext>();
|
||||||
|
|
||||||
services.AddHttpClient();
|
services.AddHttpClient();
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<!-- See https://aka.ms/dotnet/msbuild/customize for more details on customizing your build -->
|
|
||||||
<Project>
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<LangVersion>default</LangVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,33 +1,48 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<RootNamespace>Hutopy.Web</RootNamespace>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<AssemblyName>Hutopy.Web</AssemblyName>
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>default</LangVersion>
|
||||||
<UserSecretsId>de6d03c4-8b1c-49e2-a8ca-c38cd4dc7d85</UserSecretsId>
|
<UserSecretsId>de6d03c4-8b1c-49e2-a8ca-c38cd4dc7d85</UserSecretsId>
|
||||||
</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.3" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" Version="9.0.3"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="9.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="9.0.3"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.3"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.3"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.3"/>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore"
|
||||||
<PackageReference Include="NSwag.AspNetCore" Version="14.3.0" />
|
Version="9.0.3"/>
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
|
||||||
|
<PackageReference Include="NSwag.AspNetCore" Version="14.3.0"/>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||||
<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="SixLabors.ImageSharp" Version="3.1.8" />
|
<PackageReference Include="Postmark" Version="5.2.0"/>
|
||||||
<PackageReference Include="Stripe.net" Version="47.4.0" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8"/>
|
||||||
|
<PackageReference Include="Stripe.net" Version="47.4.0"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Modules\Contents\Migrations\"/>
|
||||||
|
<Folder Include="Modules\Creators\Migrations\"/>
|
||||||
|
<Folder Include="Modules\Identity\Migrations\"/>
|
||||||
|
<Folder Include="Modules\Memberships\Migrations\"/>
|
||||||
|
<Folder Include="Modules\Messaging\Migrations\"/>
|
||||||
|
<Folder Include="Modules\Tipping\Migrations\"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
|
||||||
VisualStudioVersion = 17.0.31903.59
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6ED356A7-8B47-4613-AD01-C85CF28491BD}"
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E2DA20AA-28D1-455C-BF50-C49A8F831633}"
|
|
||||||
ProjectSection(SolutionItems) = preProject
|
|
||||||
.editorconfig = .editorconfig
|
|
||||||
.gitignore = .gitignore
|
|
||||||
Directory.Build.props = Directory.Build.props
|
|
||||||
global.json = global.json
|
|
||||||
README.md = README.md
|
|
||||||
start-infrastructure.sh = start-infrastructure.sh
|
|
||||||
azure-pipelines.yml = azure-pipelines.yml
|
|
||||||
update-databases.sh = update-databases.sh
|
|
||||||
create-sql-scripts.sh = create-sql-scripts.sh
|
|
||||||
EndProjectSection
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web", "src\Web\Web.csproj", "{4E4EE20C-F06A-4A1B-851F-C5577796941C}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{4E4EE20C-F06A-4A1B-851F-C5577796941C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{4E4EE20C-F06A-4A1B-851F-C5577796941C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{4E4EE20C-F06A-4A1B-851F-C5577796941C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{4E4EE20C-F06A-4A1B-851F-C5577796941C}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(NestedProjects) = preSolution
|
|
||||||
{4E4EE20C-F06A-4A1B-851F-C5577796941C} = {6ED356A7-8B47-4613-AD01-C85CF28491BD}
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
|
||||||
SolutionGuid = {3CB609D9-5D54-4C11-A371-DAAC8B74E430}
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
|
public static class CommonFileNames
|
||||||
|
{
|
||||||
|
public const string ProfilePicture = "profilePicture";
|
||||||
|
public const string BannerPicture = "bannerPicture";
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Hutopy.Web.Common.BlobStorage;
|
namespace Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public static class ContainerNames
|
public static class ContainerNames
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Hutopy.Web.Common.BlobStorage;
|
namespace Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public static class ContentTypes
|
public static class ContentTypes
|
||||||
{
|
{
|
||||||
@@ -39,11 +39,6 @@ public static class ContentTypes
|
|||||||
|
|
||||||
// Check for HTML content by looking for "<!DOCTYPE html>" or "<html>" tags
|
// Check for HTML content by looking for "<!DOCTYPE html>" or "<html>" tags
|
||||||
string content = Encoding.UTF8.GetString(buffer);
|
string content = Encoding.UTF8.GetString(buffer);
|
||||||
if (content.Contains("<!DOCTYPE html>"))
|
return content.Contains("<!DOCTYPE html>");
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
32
backend/Infrastructure/BlobStorage/Contracts/IBlobStorage.cs
Normal file
32
backend/Infrastructure/BlobStorage/Contracts/IBlobStorage.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
namespace Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
|
public interface IBlobStorage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Upload a file to blob storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <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="stream"></param>
|
||||||
|
/// <param name="contentType">The content type.</param>
|
||||||
|
/// <param name="ct">The cancellation token</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<string> UploadFileAsync(
|
||||||
|
string containerName,
|
||||||
|
string blobName,
|
||||||
|
Stream stream,
|
||||||
|
string contentType,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Download a file to blob storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <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="ct">The cancellation token for the request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<MemoryStream> DownloadFileAsync(
|
||||||
|
string containerName,
|
||||||
|
string blobName,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
|
public static class SubDirectoryNames
|
||||||
|
{
|
||||||
|
public const string Profile = "profile";
|
||||||
|
public const string Contents = "contents";
|
||||||
|
public const string Albums = "albums";
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
using Azure;
|
using Azure;
|
||||||
using Azure.Storage.Blobs;
|
using Azure.Storage.Blobs;
|
||||||
using Azure.Storage.Blobs.Models;
|
using Azure.Storage.Blobs.Models;
|
||||||
|
using Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
namespace Hutopy.Web.Common.BlobStorage;
|
namespace Hutopy.Infrastructure.BlobStorage.Services;
|
||||||
|
|
||||||
public class AzureBlobStorage
|
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
|
||||||
|
|
||||||
@@ -27,8 +28,12 @@ public class AzureBlobStorage
|
|||||||
/// <param name="contentType">The content type.</param>
|
/// <param name="contentType">The content type.</param>
|
||||||
/// <param name="ct">The cancellation token</param>
|
/// <param name="ct">The cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<string> UploadFileAsync(string containerName, string blobName, Stream stream,
|
public async Task<string> UploadFileAsync(
|
||||||
string contentType, CancellationToken ct = default)
|
string containerName,
|
||||||
|
string blobName,
|
||||||
|
Stream stream,
|
||||||
|
string contentType,
|
||||||
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// Read the file stream into a memory stream to determine the length
|
// Read the file stream into a memory stream to determine the length
|
||||||
// WATCH FOR MEMORY USAGE USING THE MEMORY STREAM.
|
// WATCH FOR MEMORY USAGE USING THE MEMORY STREAM.
|
||||||
@@ -113,7 +118,9 @@ public class AzureBlobStorage
|
|||||||
/// <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>
|
||||||
/// <param name="ct">The cancellation token for the request</param>
|
/// <param name="ct">The cancellation token for the request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<MemoryStream> DownloadFileAsync(string containerName, string blobName,
|
public async Task<MemoryStream> DownloadFileAsync(
|
||||||
|
string containerName,
|
||||||
|
string blobName,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
namespace Hutopy.Infrastructure.Configuration;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Users;
|
|
||||||
|
|
||||||
public class WebsiteOptions
|
public class WebsiteOptions
|
||||||
{
|
{
|
||||||
40
backend/Infrastructure/DependencyInjection.cs
Normal file
40
backend/Infrastructure/DependencyInjection.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||||
|
using Hutopy.Infrastructure.BlobStorage.Services;
|
||||||
|
using Hutopy.Infrastructure.Configuration;
|
||||||
|
using Hutopy.Infrastructure.Emailer.Configuration;
|
||||||
|
using Hutopy.Infrastructure.Emailer.Contracts;
|
||||||
|
using Hutopy.Infrastructure.Emailer.Services;
|
||||||
|
using Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
||||||
|
using Hutopy.Infrastructure.Payments.Stripe.Services;
|
||||||
|
using Hutopy.Modules.Memberships.Contracts;
|
||||||
|
using Hutopy.Modules.Tipping.Contracts;
|
||||||
|
|
||||||
|
namespace Hutopy.Infrastructure;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static WebApplicationBuilder AddInfrastructureModule(
|
||||||
|
this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Services.Configure<WebsiteOptions>(
|
||||||
|
builder.Configuration.GetRequiredSection(WebsiteOptions.SectionName));
|
||||||
|
|
||||||
|
builder.Services.AddTransient<IBlobStorage, AzureBlobStorage>();
|
||||||
|
|
||||||
|
builder.Services.AddTransient<ITipProcessor, StripeTipProcessor>();
|
||||||
|
builder.Services.AddTransient<IMembershipPaymentProcessor, MembershipPaymentProcessor>();
|
||||||
|
builder.Services.AddTransient<IMembershipCancellationProcessor, MembershipCancellationProcessor>();
|
||||||
|
builder.Services.AddTransient<IMembershipTierProcessor, MembershipTierProcessor>();
|
||||||
|
builder.Services.Configure<StripeOptions>(
|
||||||
|
builder.Configuration.GetSection(StripeOptions.ConfigurationSection));
|
||||||
|
|
||||||
|
builder.Services.Configure<EmailerOptions>(
|
||||||
|
builder.Configuration.GetSection(EmailerOptions.ConfigurationSection));
|
||||||
|
builder.Services.AddTransient<IEmailSender, ResendEmailSender>();
|
||||||
|
//builder.Services.AddTransient<IEmailSender, EmailSender>();
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Hutopy.Infrastructure.Emailer.Configuration;
|
||||||
|
|
||||||
|
public class EmailerOptions
|
||||||
|
{
|
||||||
|
public const string ConfigurationSection = "Emailer";
|
||||||
|
|
||||||
|
public string ApiKey { get; set; } = default!;
|
||||||
|
public string FromEmail { get; set; } = default!;
|
||||||
|
}
|
||||||
6
backend/Infrastructure/Emailer/Contracts/IEmailSender.cs
Normal file
6
backend/Infrastructure/Emailer/Contracts/IEmailSender.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Hutopy.Infrastructure.Emailer.Contracts;
|
||||||
|
|
||||||
|
public interface IEmailSender
|
||||||
|
{
|
||||||
|
Task SendEmailAsync(string email, string subject, string message);
|
||||||
|
}
|
||||||
@@ -1,22 +1,15 @@
|
|||||||
using System.Threading.Tasks;
|
using Hutopy.Infrastructure.Emailer.Contracts;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Users.Services;
|
namespace Hutopy.Infrastructure.Emailer.Services;
|
||||||
|
|
||||||
public interface IEmailSender
|
public class LoggerEmailSender(ILogger<IEmailSender> logger)
|
||||||
{
|
|
||||||
Task SendEmailAsync(string email, string subject, string message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EmailSender(ILogger<IEmailSender> logger)
|
|
||||||
: IEmailSender
|
: IEmailSender
|
||||||
{
|
{
|
||||||
|
|
||||||
public async Task SendEmailAsync(string email, string subject, string message)
|
public async Task SendEmailAsync(string email, string subject, string message)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
logger.LogInformation("Sending email to {Email} with subject: {Subject}", email, subject);
|
logger.LogInformation("Sending email to {Email} with subject: {Subject}", email, subject);
|
||||||
// TODO: Implement actual email sending logic
|
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
logger.LogInformation("Email sent successfully to {Email}", email);
|
logger.LogInformation("Email sent successfully to {Email}", email);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Hutopy.Infrastructure.Emailer.Configuration;
|
||||||
|
using Hutopy.Infrastructure.Emailer.Contracts;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using PostmarkDotNet;
|
||||||
|
|
||||||
|
namespace Hutopy.Infrastructure.Emailer.Services;
|
||||||
|
|
||||||
|
public class PostmarkEmailSender : IEmailSender
|
||||||
|
{
|
||||||
|
private readonly PostmarkClient _client;
|
||||||
|
private readonly EmailerOptions _options;
|
||||||
|
|
||||||
|
public PostmarkEmailSender(IOptions<EmailerOptions> options)
|
||||||
|
{
|
||||||
|
_options = options.Value;
|
||||||
|
_client = new PostmarkClient(_options.ApiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendEmailAsync(string email, string subject, string message)
|
||||||
|
{
|
||||||
|
PostmarkResponse? sendResult = await _client.SendMessageAsync(new PostmarkMessage
|
||||||
|
{
|
||||||
|
From = _options.FromEmail,
|
||||||
|
To = email,
|
||||||
|
Subject = subject,
|
||||||
|
HtmlBody = message,
|
||||||
|
TrackOpens = true,
|
||||||
|
MessageStream = "outbound" // Optional: use "broadcast" for bulk
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sendResult.Status != PostmarkStatus.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Postmark failed to send email: {sendResult.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
backend/Infrastructure/Emailer/Services/ResendEmailSender.cs
Normal file
46
backend/Infrastructure/Emailer/Services/ResendEmailSender.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Hutopy.Infrastructure.Emailer.Configuration;
|
||||||
|
using Hutopy.Infrastructure.Emailer.Contracts;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Hutopy.Infrastructure.Emailer.Services;
|
||||||
|
|
||||||
|
public class ResendEmailSender : IEmailSender
|
||||||
|
{
|
||||||
|
private static readonly Uri EndpointUri = new("https://api.resend.com/emails");
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly EmailerOptions _options;
|
||||||
|
|
||||||
|
public ResendEmailSender(
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
IOptions<EmailerOptions> options)
|
||||||
|
{
|
||||||
|
_httpClient = httpClientFactory.CreateClient();
|
||||||
|
_options = options.Value;
|
||||||
|
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization =
|
||||||
|
new AuthenticationHeaderValue("Bearer", _options.ApiKey);
|
||||||
|
|
||||||
|
_httpClient.DefaultRequestHeaders.Accept.Add(
|
||||||
|
new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendEmailAsync(string toEmail, string subject, string htmlMessage)
|
||||||
|
{
|
||||||
|
var payload = new { from = _options.FromEmail, to = toEmail, subject, html = htmlMessage };
|
||||||
|
|
||||||
|
string json = JsonSerializer.Serialize(payload);
|
||||||
|
StringContent content = new(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
HttpResponseMessage response = await _httpClient.PostAsync(EndpointUri, content);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
string body = await response.Content.ReadAsStringAsync();
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Resend email failed: {response.StatusCode} - {body}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
||||||
|
|
||||||
|
public class StripeOptions
|
||||||
|
{
|
||||||
|
public const string ConfigurationSection = "Stripe";
|
||||||
|
|
||||||
|
[Required] public required string SecretKey { get; init; }
|
||||||
|
|
||||||
|
[Required] public required string WebhookSecret { get; init; }
|
||||||
|
|
||||||
|
[Required] [Range(0, 1)] public required decimal HutopyRate { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Hutopy.Modules.Memberships.Contracts;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Hutopy.Infrastructure.Payments.Stripe.Services;
|
||||||
|
|
||||||
|
public sealed class MembershipCancellationProcessor
|
||||||
|
: IMembershipCancellationProcessor
|
||||||
|
{
|
||||||
|
public async Task<DateTimeOffset?> CancelAsync(
|
||||||
|
string subscriptionId,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
SubscriptionService subscriptionService = new();
|
||||||
|
|
||||||
|
// Stripe - Cancel Subscription immediately
|
||||||
|
// var subscription = await subscriptionService.CancelAsync(
|
||||||
|
// subscriptionId,
|
||||||
|
// cancellationToken: ct);
|
||||||
|
|
||||||
|
// Stripe - Cancel Subscription AtPeriodEnd
|
||||||
|
Subscription? subscription = await subscriptionService.UpdateAsync(
|
||||||
|
subscriptionId,
|
||||||
|
new SubscriptionUpdateOptions { CancelAtPeriodEnd = true },
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
return subscription.CancelAt ?? subscription.CanceledAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
||||||
|
using Hutopy.Modules.Creators.Contracts;
|
||||||
|
using Hutopy.Modules.Memberships.Contracts;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Stripe;
|
||||||
|
using Stripe.Checkout;
|
||||||
|
|
||||||
|
namespace Hutopy.Infrastructure.Payments.Stripe.Services;
|
||||||
|
|
||||||
|
public class MembershipPaymentProcessor(
|
||||||
|
IOptions<StripeOptions> stripeOptions)
|
||||||
|
: IMembershipPaymentProcessor
|
||||||
|
{
|
||||||
|
public async Task<MembershipCheckoutSession> CreateCheckoutSessionAsync(
|
||||||
|
Guid userId,
|
||||||
|
CreatorReference creatorReference,
|
||||||
|
Guid tierId,
|
||||||
|
string priceId,
|
||||||
|
string successUrl,
|
||||||
|
string cancelUrl)
|
||||||
|
{
|
||||||
|
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
|
||||||
|
|
||||||
|
// Create Stripe customer for the user if not already created
|
||||||
|
var customerService = new CustomerService();
|
||||||
|
var customer = await customerService.CreateAsync(
|
||||||
|
new CustomerCreateOptions
|
||||||
|
{
|
||||||
|
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create Checkout Session for the subscription
|
||||||
|
var sessionService = new SessionService();
|
||||||
|
var session = await sessionService.CreateAsync(
|
||||||
|
new SessionCreateOptions
|
||||||
|
{
|
||||||
|
Customer = customer.Id,
|
||||||
|
PaymentMethodTypes = ["card"],
|
||||||
|
LineItems =
|
||||||
|
[
|
||||||
|
new SessionLineItemOptions { Price = priceId, Quantity = 1 }
|
||||||
|
],
|
||||||
|
Mode = "subscription",
|
||||||
|
SubscriptionData = new SessionSubscriptionDataOptions
|
||||||
|
{
|
||||||
|
ApplicationFeePercent = stripeOptions.Value.HutopyRate,
|
||||||
|
TransferData = new SessionSubscriptionDataTransferDataOptions { Destination = creatorReference.StripeAccountId }
|
||||||
|
},
|
||||||
|
SuccessUrl = successUrl, // Redirect after successful payment
|
||||||
|
CancelUrl = cancelUrl, // Redirect after canceled payment
|
||||||
|
Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "userId", userId.ToString() },
|
||||||
|
{ "creatorId", creatorReference.Id.ToString() },
|
||||||
|
{ "creatorName", creatorReference.Name },
|
||||||
|
{ "tierId", tierId.ToString() }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new MembershipCheckoutSession(
|
||||||
|
session.Id,
|
||||||
|
session.Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
||||||
|
using Hutopy.Modules.Memberships.Contracts;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Hutopy.Infrastructure.Payments.Stripe.Services;
|
||||||
|
|
||||||
|
public sealed class MembershipTierProcessor(
|
||||||
|
IOptions<StripeOptions> stripeOptions)
|
||||||
|
: IMembershipTierProcessor
|
||||||
|
{
|
||||||
|
public async Task<string> CreateAsync(
|
||||||
|
Guid creatorId,
|
||||||
|
Guid tierId,
|
||||||
|
string productName,
|
||||||
|
string currencyCode,
|
||||||
|
decimal amount)
|
||||||
|
{
|
||||||
|
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
|
||||||
|
|
||||||
|
// Create the product
|
||||||
|
var productService = new ProductService();
|
||||||
|
var product = await productService.CreateAsync(
|
||||||
|
new ProductCreateOptions
|
||||||
|
{
|
||||||
|
Name = productName,
|
||||||
|
Metadata = { { "creatorId", creatorId.ToString() }, { "tierId", tierId.ToString() } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the price for the product
|
||||||
|
var priceService = new PriceService();
|
||||||
|
await priceService.CreateAsync(
|
||||||
|
new PriceCreateOptions
|
||||||
|
{
|
||||||
|
Product = product.Id,
|
||||||
|
UnitAmountDecimal = amount * 100, // Convert amount to cents
|
||||||
|
Currency = currencyCode,
|
||||||
|
Recurring = new PriceRecurringOptions { Interval = "month" }
|
||||||
|
});
|
||||||
|
|
||||||
|
return product.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
||||||
|
using Hutopy.Modules.Creators.Contracts;
|
||||||
|
using Hutopy.Modules.Tipping.Contracts;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Stripe;
|
||||||
|
using Stripe.Checkout;
|
||||||
|
|
||||||
|
namespace Hutopy.Infrastructure.Payments.Stripe.Services;
|
||||||
|
|
||||||
|
public class StripeTipProcessor(
|
||||||
|
IOptions<StripeOptions> stripeOptions)
|
||||||
|
: ITipProcessor
|
||||||
|
{
|
||||||
|
public async Task<TipCheckoutSession> CreateCheckoutSessionAsync(
|
||||||
|
Guid tipId,
|
||||||
|
CreatorReference creator,
|
||||||
|
decimal amount,
|
||||||
|
string currency,
|
||||||
|
string message,
|
||||||
|
string successUrl,
|
||||||
|
string cancelUrl,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
|
||||||
|
|
||||||
|
// Create Stripe customer for the user if not already created
|
||||||
|
var customerService = new CustomerService();
|
||||||
|
var customer = await customerService.CreateAsync(
|
||||||
|
new CustomerCreateOptions(),
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
// Create paymentIntent for the user
|
||||||
|
var sessionService = new SessionService();
|
||||||
|
var session = await sessionService.CreateAsync(
|
||||||
|
new SessionCreateOptions
|
||||||
|
{
|
||||||
|
ClientReferenceId = tipId.ToString(),
|
||||||
|
Customer = customer.Id,
|
||||||
|
PaymentMethodTypes = ["card"],
|
||||||
|
LineItems =
|
||||||
|
[
|
||||||
|
new SessionLineItemOptions
|
||||||
|
{
|
||||||
|
PriceData = new SessionLineItemPriceDataOptions
|
||||||
|
{
|
||||||
|
Currency = currency,
|
||||||
|
UnitAmountDecimal = amount, // Amount in cents
|
||||||
|
ProductData = new SessionLineItemPriceDataProductDataOptions
|
||||||
|
{
|
||||||
|
Name = $"Tip for {creator.Name}", // or any descriptive name for the tip
|
||||||
|
Metadata = new Dictionary<string, string> { { "creatorId", creator.Id.ToString() } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Quantity = 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Mode = "payment",
|
||||||
|
PaymentIntentData = new SessionPaymentIntentDataOptions
|
||||||
|
{
|
||||||
|
ApplicationFeeAmount =
|
||||||
|
Convert.ToInt64(amount * 100 * stripeOptions.Value.HutopyRate), // Platform fee
|
||||||
|
TransferData = new SessionPaymentIntentDataTransferDataOptions
|
||||||
|
{
|
||||||
|
Destination = creator.StripeAccountId // Creator's Stripe account ID
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SuccessUrl = successUrl, // Redirect after successful payment
|
||||||
|
CancelUrl = cancelUrl, // Redirect after canceled payment
|
||||||
|
Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "creatorId", creator.Id.ToString() }, { "creatorName", creator.Name }, { "message", message },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
return new TipCheckoutSession(session.Id, session.Url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace Hutopy.Web.Common.Security;
|
namespace Hutopy.Infrastructure.Security;
|
||||||
|
|
||||||
public static class ClaimsPrincipalExtensions
|
public static class ClaimsPrincipalExtensions
|
||||||
{
|
{
|
||||||
@@ -43,8 +43,7 @@ public static class ClaimsPrincipalExtensions
|
|||||||
{
|
{
|
||||||
var claim = claims.FindFirst(key);
|
var claim = claims.FindFirst(key);
|
||||||
|
|
||||||
if (claim is null) return default;
|
return claim is null ? null : claims.GetRequiredClaim<TValue>(key);
|
||||||
return claims.GetRequiredClaim<TValue>(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object GetRequiredClaim<TValue>(this ClaimsPrincipal claims, string key)
|
private static object GetRequiredClaim<TValue>(this ClaimsPrincipal claims, string key)
|
||||||
@@ -53,11 +52,6 @@ public static class ClaimsPrincipalExtensions
|
|||||||
|
|
||||||
if (claim is null) throw new MissingClaimException(key);
|
if (claim is null) throw new MissingClaimException(key);
|
||||||
|
|
||||||
if (typeof(TValue) == typeof(Guid))
|
return typeof(TValue) == typeof(Guid) ? Guid.Parse(claim.Value) : Convert.ChangeType(claim.Value, typeof(TValue));
|
||||||
{
|
|
||||||
return Guid.Parse(claim.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Convert.ChangeType(claim.Value, typeof(TValue));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ using System.Security.Claims;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
namespace Hutopy.Web.Common.Security;
|
namespace Hutopy.Infrastructure.Security;
|
||||||
|
|
||||||
public static class JwtTokenHelper
|
public static class JwtTokenHelper
|
||||||
{
|
{
|
||||||
@@ -22,23 +22,22 @@ public static class JwtTokenHelper
|
|||||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
|
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
|
||||||
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
var claims = new List<Claim>(new[]
|
var claims = new List<Claim>([
|
||||||
{
|
|
||||||
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),
|
||||||
new Claim(ClaimTypes.Name, email), new Claim(ClaimTypes.GivenName, firstname),
|
new Claim(ClaimTypes.Name, email), new Claim(ClaimTypes.GivenName, firstname),
|
||||||
new Claim(ClaimTypes.Surname, lastname)
|
new Claim(ClaimTypes.Surname, lastname)
|
||||||
});
|
]);
|
||||||
|
|
||||||
if (alias is not null)
|
if (alias is not null)
|
||||||
{
|
{
|
||||||
claims.Add(new(KnownClaims.Alias, alias));
|
claims.Add(new Claim(KnownClaims.Alias, alias));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (portraitUrl is not null)
|
if (portraitUrl is not null)
|
||||||
{
|
{
|
||||||
claims.Add(new(KnownClaims.PortraitUrl, portraitUrl));
|
claims.Add(new Claim(KnownClaims.PortraitUrl, portraitUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
var token = new JwtSecurityToken(
|
var token = new JwtSecurityToken(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Hutopy.Web.Common.Security;
|
namespace Hutopy.Infrastructure.Security;
|
||||||
|
|
||||||
public static class KnownClaims
|
public static class KnownClaims
|
||||||
{
|
{
|
||||||
5
backend/Infrastructure/Security/MissingClaimException.cs
Normal file
5
backend/Infrastructure/Security/MissingClaimException.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
namespace Hutopy.Infrastructure.Security;
|
||||||
|
|
||||||
|
public class MissingClaimException(
|
||||||
|
string claimName)
|
||||||
|
: Exception($"Claim '{claimName}' is missing.");
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Hutopy.Web.Common.Security;
|
namespace Hutopy.Infrastructure.Security;
|
||||||
|
|
||||||
// If we need to add special characters we can alternate between 2 pools.
|
// If we need to add special characters we can alternate between 2 pools.
|
||||||
public static class PasswordGenerator
|
public static class PasswordGenerator
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace Hutopy.Web.Common.Security;
|
namespace Hutopy.Infrastructure.Security;
|
||||||
|
|
||||||
public static class RefreshTokenGenerator
|
public static class RefreshTokenGenerator
|
||||||
{
|
{
|
||||||
public static string Next()
|
public static string Next()
|
||||||
{
|
{
|
||||||
var randomNumber = new byte[32];
|
var randomNumber = new byte[32];
|
||||||
using var rng = RandomNumberGenerator.Create();
|
RandomNumberGenerator.Fill(randomNumber);
|
||||||
rng.GetBytes(randomNumber);
|
|
||||||
return Convert.ToBase64String(randomNumber);
|
return Convert.ToBase64String(randomNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Hutopy.Web.Common.YouTube;
|
namespace Hutopy.Infrastructure.YouTube;
|
||||||
|
|
||||||
public static class YouTubeUrlHelper
|
public static class YouTubeUrlHelper
|
||||||
{
|
{
|
||||||
11
backend/Modules/Contents/Data/Album.cs
Normal file
11
backend/Modules/Contents/Data/Album.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Hutopy.Common.Domain;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Contents.Data;
|
||||||
|
|
||||||
|
public class Album : Entity
|
||||||
|
{
|
||||||
|
public bool IsDeleted { get; private set; } // private set → EF updates it
|
||||||
|
[MaxLength(255)] public required string Title { get; set; }
|
||||||
|
public IList<AlbumPhoto> Photos { get; set; } = new List<AlbumPhoto>();
|
||||||
|
}
|
||||||
15
backend/Modules/Contents/Data/AlbumPhoto.cs
Normal file
15
backend/Modules/Contents/Data/AlbumPhoto.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Hutopy.Common.Domain;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Contents.Data;
|
||||||
|
|
||||||
|
public class AlbumPhoto : Entity
|
||||||
|
{
|
||||||
|
public bool IsDeleted { get; private set; } // private set → EF updates it
|
||||||
|
public Guid AlbumId { get; set; }
|
||||||
|
public Album Album { get; init; } = null!;
|
||||||
|
[MaxLength(2048)] public required string OriginalUrl { get; set; }
|
||||||
|
[MaxLength(2048)] public required string ThumbnailUrl { get; set; }
|
||||||
|
[MaxLength(256)] public string? Caption { get; set; }
|
||||||
|
public int Order { get; set; }
|
||||||
|
}
|
||||||
56
backend/Modules/Contents/Data/ContentsDbContext.cs
Normal file
56
backend/Modules/Contents/Data/ContentsDbContext.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
namespace Hutopy.Modules.Contents.Data;
|
||||||
|
|
||||||
|
public class ContentsDbContext(
|
||||||
|
DbContextOptions<ContentsDbContext> options)
|
||||||
|
: DbContext(options)
|
||||||
|
{
|
||||||
|
public const string SchemaName = "Content";
|
||||||
|
|
||||||
|
public DbSet<Album> Albums => Set<Album>();
|
||||||
|
public DbSet<AlbumPhoto> AlbumPhotos => Set<AlbumPhoto>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(
|
||||||
|
ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.HasDefaultSchema(SchemaName);
|
||||||
|
|
||||||
|
// Album configuration
|
||||||
|
modelBuilder
|
||||||
|
.Entity<Album>()
|
||||||
|
.Property(c => c.CreatedAt)
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
modelBuilder
|
||||||
|
.Entity<Album>()
|
||||||
|
.Property(c => c.IsDeleted)
|
||||||
|
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", stored: true);
|
||||||
|
|
||||||
|
modelBuilder
|
||||||
|
.Entity<Album>()
|
||||||
|
.HasQueryFilter(a => !a.IsDeleted);
|
||||||
|
|
||||||
|
// AlbumPhoto configuration
|
||||||
|
modelBuilder
|
||||||
|
.Entity<AlbumPhoto>()
|
||||||
|
.Property(c => c.CreatedAt)
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
modelBuilder
|
||||||
|
.Entity<AlbumPhoto>()
|
||||||
|
.Property(c => c.IsDeleted)
|
||||||
|
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", stored: true);
|
||||||
|
|
||||||
|
modelBuilder
|
||||||
|
.Entity<AlbumPhoto>()
|
||||||
|
.HasOne(ap => ap.Album)
|
||||||
|
.WithMany(a => a.Photos)
|
||||||
|
.HasForeignKey(ap => ap.AlbumId)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
modelBuilder
|
||||||
|
.Entity<AlbumPhoto>()
|
||||||
|
.HasQueryFilter(ap => !ap.IsDeleted);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
backend/Modules/Contents/DependencyInjection.cs
Normal file
27
backend/Modules/Contents/DependencyInjection.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Hutopy.Modules.Contents.Data;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Contents;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static WebApplicationBuilder AddContentModule(
|
||||||
|
this WebApplicationBuilder builder,
|
||||||
|
Action<DbContextOptionsBuilder>? configureAction = null)
|
||||||
|
{
|
||||||
|
builder.Services.AddDbContext<ContentsDbContext>(configureAction);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<IApplicationBuilder> UseContentModuleAsync(
|
||||||
|
this IApplicationBuilder app,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||||
|
using var scope = scopeFactory.CreateScope();
|
||||||
|
await using var context = scope.ServiceProvider.GetRequiredService<ContentsDbContext>();
|
||||||
|
await context.Database.MigrateAsync(cancellationToken);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Infrastructure.Security;
|
||||||
using Hutopy.Web.Common.BlobStorage;
|
using Hutopy.Modules.Contents.Data;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Contents.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record AddPhotoToAlbumRequest(
|
public record AddPhotoToAlbumRequest(
|
||||||
@@ -24,12 +24,12 @@ public sealed class AddPhotoToAlbumRequestValidator : Validator<AddPhotoToAlbumR
|
|||||||
{
|
{
|
||||||
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",
|
||||||
"image/png",
|
"image/png",
|
||||||
"image/gif",
|
"image/gif",
|
||||||
"image/webp"
|
"image/webp"
|
||||||
};
|
];
|
||||||
|
|
||||||
public AddPhotoToAlbumRequestValidator()
|
public AddPhotoToAlbumRequestValidator()
|
||||||
{
|
{
|
||||||
@@ -56,8 +56,8 @@ public sealed class AddPhotoToAlbumRequestValidator : Validator<AddPhotoToAlbumR
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class AddPhotoToAlbumHandler(
|
public class AddPhotoToAlbumHandler(
|
||||||
ContentDbContext context,
|
ContentsDbContext context,
|
||||||
AzureBlobStorage blobStorage)
|
IBlobStorage blobStorage)
|
||||||
: Endpoint<AddPhotoToAlbumRequest, AddPhotoToAlbumResponse>
|
: Endpoint<AddPhotoToAlbumRequest, AddPhotoToAlbumResponse>
|
||||||
{
|
{
|
||||||
private const int MaxThumbnailWidth = 500;
|
private const int MaxThumbnailWidth = 500;
|
||||||
@@ -133,7 +133,7 @@ public class AddPhotoToAlbumHandler(
|
|||||||
{
|
{
|
||||||
await SendStringAsync("Invalid image format", 400, cancellation: ct);
|
await SendStringAsync("Invalid image format", 400, cancellation: ct);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
await SendStringAsync("Error processing image", 500, cancellation: ct);
|
await SendStringAsync("Error processing image", 500, cancellation: ct);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Infrastructure.Security;
|
||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Modules.Contents.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Contents.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record CreateAlbumRequest(
|
public record CreateAlbumRequest(
|
||||||
@@ -34,7 +34,7 @@ public sealed class CreateAlbumRequestValidator : Validator<CreateAlbumRequest>
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class CreateAlbumHandler(
|
public class CreateAlbumHandler(
|
||||||
ContentDbContext context)
|
ContentsDbContext context)
|
||||||
: Endpoint<CreateAlbumRequest, CreateAlbumResponse>
|
: Endpoint<CreateAlbumRequest, CreateAlbumResponse>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
using FastEndpoints;
|
using Hutopy.Modules.Contents.Data;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
|
||||||
using Hutopy.Web.Common.Security;
|
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Contents.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record GetAlbumRequest(
|
public record GetAlbumRequest(
|
||||||
@@ -39,7 +35,7 @@ public sealed class GetAlbumRequestValidator : Validator<GetAlbumRequest>
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class GetAlbumHandler(
|
public class GetAlbumHandler(
|
||||||
ContentDbContext context)
|
ContentsDbContext context)
|
||||||
: Endpoint<GetAlbumRequest, GetAlbumResponse>
|
: Endpoint<GetAlbumRequest, GetAlbumResponse>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
using FastEndpoints;
|
using Hutopy.Infrastructure.Security;
|
||||||
using JetBrains.Annotations;
|
using Hutopy.Modules.Contents.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
|
||||||
using Hutopy.Web.Common.Security;
|
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Contents.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record RemoveAlbumRequest(
|
public record RemoveAlbumRequest(
|
||||||
@@ -23,7 +20,7 @@ public sealed class RemoveAlbumRequestValidator : Validator<RemoveAlbumRequest>
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class RemoveAlbumHandler(
|
public class RemoveAlbumHandler(
|
||||||
ContentDbContext context)
|
ContentsDbContext context)
|
||||||
: Endpoint<RemoveAlbumRequest>
|
: Endpoint<RemoveAlbumRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
using FastEndpoints;
|
using Hutopy.Infrastructure.Security;
|
||||||
using JetBrains.Annotations;
|
using Hutopy.Modules.Contents.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
|
||||||
using Hutopy.Web.Common.Security;
|
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Contents.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record RemovePhotoFromAlbumRequest(
|
public record RemovePhotoFromAlbumRequest(
|
||||||
@@ -28,7 +25,7 @@ public sealed class RemovePhotoFromAlbumRequestValidator : Validator<RemovePhoto
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class RemovePhotoFromAlbumHandler(
|
public class RemovePhotoFromAlbumHandler(
|
||||||
ContentDbContext context)
|
ContentsDbContext context)
|
||||||
: Endpoint<RemovePhotoFromAlbumRequest>
|
: Endpoint<RemovePhotoFromAlbumRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
134
backend/Modules/Contents/Migrations/20250609212411_Initial.Designer.cs
generated
Normal file
134
backend/Modules/Contents/Migrations/20250609212411_Initial.Designer.cs
generated
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Hutopy.Modules.Contents.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Contents.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ContentsDbContext))]
|
||||||
|
[Migration("20250609212411_Initial")]
|
||||||
|
partial class Initial
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("Content")
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.3")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Contents.Data.Album", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DeletedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Albums", "Content");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Contents.Data.AlbumPhoto", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("AlbumId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Caption")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DeletedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
|
||||||
|
|
||||||
|
b.Property<int>("Order")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("ThumbnailUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AlbumId");
|
||||||
|
|
||||||
|
b.ToTable("AlbumPhotos", "Content");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Contents.Data.AlbumPhoto", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Modules.Contents.Data.Album", "Album")
|
||||||
|
.WithMany("Photos")
|
||||||
|
.HasForeignKey("AlbumId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Album");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Contents.Data.Album", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Photos");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,28 +3,29 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Data.Migrations
|
namespace Hutopy.Modules.Contents.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class AddAlbumAndPhotos : Migration
|
public partial class Initial : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
migrationBuilder.EnsureSchema(
|
||||||
|
name: "Content");
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "Albums",
|
name: "Albums",
|
||||||
schema: "Content",
|
schema: "Content",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
IsDeleted = table.Column<bool>(type: "boolean", nullable: false, computedColumnSql: "\"DeletedAt\" IS NOT NULL", stored: true),
|
||||||
|
Title = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
DeletedBy = table.Column<Guid>(type: "uuid", nullable: true),
|
DeletedBy = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
DeletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
DeletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false, computedColumnSql: "\"DeletedAt\" IS NOT NULL", stored: true),
|
|
||||||
Title = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
|
||||||
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
|
||||||
CoverPhotoUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true)
|
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
@@ -37,15 +38,16 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
|||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
IsDeleted = table.Column<bool>(type: "boolean", nullable: false, computedColumnSql: "\"DeletedAt\" IS NOT NULL", stored: true),
|
||||||
|
AlbumId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
OriginalUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||||
|
ThumbnailUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||||
|
Caption = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
Order = table.Column<int>(type: "integer", nullable: false),
|
||||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
DeletedBy = table.Column<Guid>(type: "uuid", nullable: true),
|
DeletedBy = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
DeletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
DeletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false, computedColumnSql: "\"DeletedAt\" IS NOT NULL", stored: true),
|
|
||||||
AlbumId = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
PhotoUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
|
||||||
Caption = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
|
||||||
Order = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Hutopy.Modules.Contents.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Contents.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ContentsDbContext))]
|
||||||
|
partial class ContentsDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("Content")
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.3")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Contents.Data.Album", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DeletedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Albums", "Content");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Contents.Data.AlbumPhoto", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("AlbumId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Caption")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DeletedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
|
||||||
|
|
||||||
|
b.Property<int>("Order")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("ThumbnailUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AlbumId");
|
||||||
|
|
||||||
|
b.ToTable("AlbumPhotos", "Content");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Contents.Data.AlbumPhoto", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hutopy.Modules.Contents.Data.Album", "Album")
|
||||||
|
.WithMany("Photos")
|
||||||
|
.HasForeignKey("AlbumId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Album");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Contents.Data.Album", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Photos");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Hutopy.Web.Features.Contents.Handlers.Models;
|
namespace Hutopy.Modules.Contents.Models;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ContentModel
|
public class ContentModel
|
||||||
@@ -15,5 +15,4 @@ public class ContentModel
|
|||||||
public string HtmlFileUrl { get; init; } = "";
|
public string HtmlFileUrl { get; init; } = "";
|
||||||
public required string[]? Urls { get; init; }
|
public required string[]? Urls { get; init; }
|
||||||
public string? ThumbnailUrl { get; init; }
|
public string? ThumbnailUrl { get; init; }
|
||||||
public IList<ReactionModel>? Reactions { get; set; } = new List<ReactionModel>();
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Hutopy.Web.Features.Contents.Handlers.Models;
|
namespace Hutopy.Modules.Contents.Models;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record FollowModel(
|
public record FollowModel(
|
||||||
8
backend/Modules/Creators/Configuration/CreatorOptions.cs
Normal file
8
backend/Modules/Creators/Configuration/CreatorOptions.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Hutopy.Modules.Creators.Configuration;
|
||||||
|
|
||||||
|
public class CreatorOptions
|
||||||
|
{
|
||||||
|
public const string ConfigurationSection = "Creators";
|
||||||
|
|
||||||
|
public TimeSpan SlugReservationDuration { get; set; }
|
||||||
|
}
|
||||||
9
backend/Modules/Creators/Contracts/CreatorReference.cs
Normal file
9
backend/Modules/Creators/Contracts/CreatorReference.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Hutopy.Modules.Creators.Contracts;
|
||||||
|
|
||||||
|
public record CreatorReference(
|
||||||
|
Guid Id,
|
||||||
|
string Name,
|
||||||
|
string? PortraitUrl,
|
||||||
|
bool OnboardingComplete,
|
||||||
|
bool AcceptCharges,
|
||||||
|
string? StripeAccountId);
|
||||||
6
backend/Modules/Creators/Contracts/ICreatorLookup.cs
Normal file
6
backend/Modules/Creators/Contracts/ICreatorLookup.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Hutopy.Modules.Creators.Contracts;
|
||||||
|
|
||||||
|
public interface ICreatorLookup
|
||||||
|
{
|
||||||
|
Task<CreatorReference?> GetCreatorAsync(Guid creatorId, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
32
backend/Modules/Creators/Data/Creator.cs
Normal file
32
backend/Modules/Creators/Data/Creator.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
|
public class Creator
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public Guid CreatedBy { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
public Guid? DeletedBy { get; set; }
|
||||||
|
public DateTimeOffset? DeletedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Soft‑delete flag (false by default, true once DeletedAt is set)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDeleted { get; private set; } // private set → EF updates it
|
||||||
|
|
||||||
|
[MaxLength(2048)] public string? BannerUrl { get; set; }
|
||||||
|
[MaxLength(2048)] public string? PortraitUrl { get; set; }
|
||||||
|
public bool Verified { get; set; }
|
||||||
|
[MaxLength(256)] public required string Name { get; set; }
|
||||||
|
[MaxLength(128)] public required string Slug { get; set; }
|
||||||
|
[MaxLength(256)] public string? Title { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(21)] public string? StripeAccountId { get; set; }
|
||||||
|
public bool IsStripeDetailsSubmitted { get; set; }
|
||||||
|
public bool IsStripePayoutReady { get; set; }
|
||||||
|
public bool IsStripeChargesEnabled { get; set; }
|
||||||
|
public Socials Socials { get; set; } = new();
|
||||||
|
public Presentation Presentation { get; set; } = new() { Description = "Welcome to my profile!" };
|
||||||
|
}
|
||||||
46
backend/Modules/Creators/Data/CreatorsDbContext.cs
Normal file
46
backend/Modules/Creators/Data/CreatorsDbContext.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
namespace Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
|
public class CreatorsDbContext(
|
||||||
|
DbContextOptions<CreatorsDbContext> options)
|
||||||
|
: DbContext(options)
|
||||||
|
{
|
||||||
|
public const string SchemaName = "Creators";
|
||||||
|
|
||||||
|
public DbSet<Creator> Creators => Set<Creator>();
|
||||||
|
public DbSet<Slugs> Slugs => Set<Slugs>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(
|
||||||
|
ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.HasDefaultSchema(SchemaName);
|
||||||
|
|
||||||
|
modelBuilder
|
||||||
|
.Entity<Slugs>()
|
||||||
|
.Property(x => x.NormalizedName)
|
||||||
|
.HasComputedColumnSql("LOWER(\"Name\")", stored: true);
|
||||||
|
|
||||||
|
modelBuilder
|
||||||
|
.Entity<Slugs>()
|
||||||
|
.HasIndex(x => x.NormalizedName)
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
modelBuilder
|
||||||
|
.Entity<Creator>()
|
||||||
|
.Property(c => c.IsDeleted)
|
||||||
|
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", stored: true); // bool
|
||||||
|
|
||||||
|
modelBuilder
|
||||||
|
.Entity<Creator>()
|
||||||
|
.OwnsOne<Socials>(x => x.Socials)
|
||||||
|
.ToTable(nameof(Socials));
|
||||||
|
|
||||||
|
modelBuilder
|
||||||
|
.Entity<Creator>()
|
||||||
|
.OwnsOne<Presentation>(x => x.Presentation)
|
||||||
|
.ToTable(nameof(Presentation));
|
||||||
|
|
||||||
|
modelBuilder
|
||||||
|
.Entity<Creator>()
|
||||||
|
.HasQueryFilter(c => !c.IsDeleted);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
backend/Modules/Creators/Data/Presentation.cs
Normal file
11
backend/Modules/Creators/Data/Presentation.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
|
public class Presentation
|
||||||
|
{
|
||||||
|
public string Description { get; set; } = null!;
|
||||||
|
[MaxLength(2048)] public string? VideoUrl { get; set; }
|
||||||
|
[MaxLength(256)] public string? PhoneNumber { get; set; }
|
||||||
|
[MaxLength(256)] public string? Email { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Data;
|
namespace Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
public class Slugs
|
public class Slugs
|
||||||
{
|
{
|
||||||
15
backend/Modules/Creators/Data/Socials.cs
Normal file
15
backend/Modules/Creators/Data/Socials.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
|
public class Socials
|
||||||
|
{
|
||||||
|
[MaxLength(2048)] public string? FacebookUrl { get; set; }
|
||||||
|
[MaxLength(2048)] public string? InstagramUrl { get; set; }
|
||||||
|
[MaxLength(2048)] public string? XUrl { get; set; }
|
||||||
|
[MaxLength(2048)] public string? LinkedInUrl { get; set; }
|
||||||
|
[MaxLength(2048)] public string? TikTokUrl { get; set; }
|
||||||
|
[MaxLength(2048)] public string? YoutubeUrl { get; set; }
|
||||||
|
[MaxLength(2048)] public string? RedditUrl { get; set; }
|
||||||
|
[MaxLength(2048)] public string? WebsiteUrl { get; set; }
|
||||||
|
}
|
||||||
35
backend/Modules/Creators/DependencyInjection.cs
Normal file
35
backend/Modules/Creators/DependencyInjection.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using Hutopy.Modules.Creators.Configuration;
|
||||||
|
using Hutopy.Modules.Creators.Contracts;
|
||||||
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
using Hutopy.Modules.Creators.Services;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Creators;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static WebApplicationBuilder AddCreatorModule(
|
||||||
|
this WebApplicationBuilder builder,
|
||||||
|
Action<DbContextOptionsBuilder>? configureAction = null)
|
||||||
|
{
|
||||||
|
builder.Services.Configure<CreatorOptions>(
|
||||||
|
builder.Configuration.GetSection(CreatorOptions.ConfigurationSection));
|
||||||
|
builder.Services.AddScoped<SlugPurger>();
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<CreatorsDbContext>(configureAction);
|
||||||
|
builder.Services.AddTransient<ICreatorLookup, CreatorLookup>();
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<IApplicationBuilder> UseCreatorModuleAsync(
|
||||||
|
this IApplicationBuilder app,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||||
|
using var scope = scopeFactory.CreateScope();
|
||||||
|
await using var context = scope.ServiceProvider.GetRequiredService<CreatorsDbContext>();
|
||||||
|
await context.Database.MigrateAsync(cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
backend/Modules/Creators/Features/ChangeBanner.cs
Normal file
60
backend/Modules/Creators/Features/ChangeBanner.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||||
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public static class ChangeBanner
|
||||||
|
{
|
||||||
|
public record Request(
|
||||||
|
Guid CreatorId,
|
||||||
|
IFormFile File);
|
||||||
|
|
||||||
|
public record Response(
|
||||||
|
string BlobUrl);
|
||||||
|
|
||||||
|
public class Handler(
|
||||||
|
CreatorsDbContext context,
|
||||||
|
IBlobStorage blobStorage)
|
||||||
|
: Endpoint<Request, Response>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/creators/{CreatorId}/banner");
|
||||||
|
Options(o => o.WithTags("Creators"));
|
||||||
|
AllowFileUploads();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(
|
||||||
|
Request request,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var creator = await context
|
||||||
|
.Creators
|
||||||
|
.SingleOrDefaultAsync(
|
||||||
|
c => c.Id == request.CreatorId,
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
if (creator is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blobUrl = await blobStorage.UploadFileAsync(
|
||||||
|
ContainerNames.Creators,
|
||||||
|
$"{request.CreatorId}/{SubDirectoryNames.Profile}/{CommonFileNames.BannerPicture}",
|
||||||
|
request.File.OpenReadStream(),
|
||||||
|
request.File.ContentType,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
creator.BannerUrl = $"{blobUrl}?t={DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}";
|
||||||
|
|
||||||
|
await context.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
await SendOkAsync(
|
||||||
|
new Response(blobUrl),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
using FluentValidation;
|
using Hutopy.Infrastructure.Security;
|
||||||
using JetBrains.Annotations;
|
using Hutopy.Modules.Creators.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
|
||||||
using Hutopy.Web.Common.Security;
|
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record ChangeEmailRequest(
|
public record ChangeEmailRequest(
|
||||||
@@ -28,7 +25,7 @@ public sealed class ChangeEmailRequestValidator : Validator<ChangeEmailRequest>
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ChangeEmailHandler(
|
public class ChangeEmailHandler(
|
||||||
ContentDbContext context)
|
CreatorsDbContext context)
|
||||||
: Endpoint<ChangeEmailRequest>
|
: Endpoint<ChangeEmailRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Hutopy.Web.Common.BlobStorage;
|
using Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record ChangeLogoRequest(
|
public record ChangeLogoRequest(
|
||||||
@@ -29,8 +29,8 @@ public sealed class ChangeLogoRequestValidator : Validator<ChangeLogoRequest>
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ChangeLogoHandler(
|
public class ChangeLogoHandler(
|
||||||
ContentDbContext context,
|
CreatorsDbContext context,
|
||||||
AzureBlobStorage blobStorage)
|
IBlobStorage blobStorage)
|
||||||
: Endpoint<ChangeLogoRequest>
|
: Endpoint<ChangeLogoRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record ChangeNameRequest(
|
public record ChangeNameRequest(
|
||||||
@@ -21,7 +21,7 @@ internal sealed class ChangeNameRequestValidator
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ChangeNameHandler(
|
public class ChangeNameHandler(
|
||||||
ContentDbContext context)
|
CreatorsDbContext context)
|
||||||
: Endpoint<ChangeNameRequest>
|
: Endpoint<ChangeNameRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
using FluentValidation;
|
using Hutopy.Infrastructure.Security;
|
||||||
using JetBrains.Annotations;
|
using Hutopy.Modules.Creators.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
|
||||||
using Hutopy.Web.Common.Security;
|
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record ChangePhoneNumberRequest(
|
public record ChangePhoneNumberRequest(
|
||||||
@@ -28,7 +25,7 @@ public sealed class ChangePhoneNumberRequestValidator : Validator<ChangePhoneNum
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ChangePhoneNumberHandler(
|
public class ChangePhoneNumberHandler(
|
||||||
ContentDbContext context)
|
CreatorsDbContext context)
|
||||||
: Endpoint<ChangePhoneNumberRequest>
|
: Endpoint<ChangePhoneNumberRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Hutopy.Web.Common.YouTube;
|
using Hutopy.Infrastructure.YouTube;
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record ChangePresentationInfosRequest(
|
public record ChangePresentationInfosRequest(
|
||||||
@@ -32,7 +32,7 @@ public sealed class ChangePresentationInfosRequestValidator : Validator<ChangePr
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ChangePresentationInfosHandler(
|
public class ChangePresentationInfosHandler(
|
||||||
ContentDbContext context)
|
CreatorsDbContext context)
|
||||||
: Endpoint<ChangePresentationInfosRequest>
|
: Endpoint<ChangePresentationInfosRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Infrastructure.Security;
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record ChangeSlugRequest(
|
public record ChangeSlugRequest(
|
||||||
@@ -26,7 +26,7 @@ internal sealed class ChangeSlugRequestValidator
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ChangeSlugHandler(
|
public class ChangeSlugHandler(
|
||||||
ContentDbContext context)
|
CreatorsDbContext context)
|
||||||
: Endpoint<ChangeSlugRequest>
|
: Endpoint<ChangeSlugRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record ChangeSocialsRequest(
|
public record ChangeSocialsRequest(
|
||||||
@@ -16,7 +16,7 @@ public record ChangeSocialsRequest(
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ChangeSocialsHandler(
|
public class ChangeSocialsHandler(
|
||||||
ContentDbContext context)
|
CreatorsDbContext context)
|
||||||
: Endpoint<ChangeSocialsRequest>
|
: Endpoint<ChangeSocialsRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record ChangeTitleRequest(
|
public record ChangeTitleRequest(
|
||||||
@@ -9,7 +9,7 @@ public record ChangeTitleRequest(
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ChangeTitleHandler(
|
public class ChangeTitleHandler(
|
||||||
ContentDbContext context)
|
CreatorsDbContext context)
|
||||||
: Endpoint<ChangeTitleRequest>
|
: Endpoint<ChangeTitleRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
68
backend/Modules/Creators/Features/CheckStatusStripe.cs
Normal file
68
backend/Modules/Creators/Features/CheckStatusStripe.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
||||||
|
using Hutopy.Infrastructure.Security;
|
||||||
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public record CheckStatusStripeResponse(
|
||||||
|
bool IsStripeAccountPresent,
|
||||||
|
bool IsStripeOnboardingComplete,
|
||||||
|
bool IsStripeChargesEnabled,
|
||||||
|
bool IsStripePayoutReady
|
||||||
|
);
|
||||||
|
|
||||||
|
public class CheckStatusStripeIdHandler(
|
||||||
|
IOptionsSnapshot<StripeOptions> stripeOptions,
|
||||||
|
CreatorsDbContext dbContext)
|
||||||
|
: EndpointWithoutRequest<CheckStatusStripeResponse>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/stripe/check-status");
|
||||||
|
Options(o => o.WithTags("Creators"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
// 1. Get the creator's information
|
||||||
|
Guid creatorId = HttpContext.User.GetUserId();
|
||||||
|
|
||||||
|
// 2. Get or create the creator
|
||||||
|
Creator? creator = await dbContext.Creators.SingleOrDefaultAsync(c => c.Id == creatorId, ct);
|
||||||
|
if (creator is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. The Creator is not being onboarded
|
||||||
|
if (string.IsNullOrWhiteSpace(creator.StripeAccountId))
|
||||||
|
{
|
||||||
|
await SendErrorsAsync(cancellation: ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Update Creator's stripe account information
|
||||||
|
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
|
||||||
|
AccountService accountService = new();
|
||||||
|
Account? account = await accountService.GetAsync(creator.StripeAccountId, cancellationToken: ct);
|
||||||
|
creator.IsStripePayoutReady = account.PayoutsEnabled;
|
||||||
|
creator.IsStripeChargesEnabled = account.ChargesEnabled;
|
||||||
|
creator.IsStripeDetailsSubmitted = account.DetailsSubmitted;
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
// 6. Return the account link URL to the client
|
||||||
|
await SendOkAsync(
|
||||||
|
new CheckStatusStripeResponse(
|
||||||
|
creator.StripeAccountId != null,
|
||||||
|
creator.IsStripeDetailsSubmitted,
|
||||||
|
creator.IsStripeChargesEnabled,
|
||||||
|
creator.IsStripePayoutReady
|
||||||
|
),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
backend/Modules/Creators/Features/ConnectStripe.cs
Normal file
90
backend/Modules/Creators/Features/ConnectStripe.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
using Hutopy.Infrastructure.Configuration;
|
||||||
|
using Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
||||||
|
using Hutopy.Infrastructure.Security;
|
||||||
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public record ConnectStripeResponse(
|
||||||
|
string Url);
|
||||||
|
|
||||||
|
public class ConnectStripeIdHandler(
|
||||||
|
IOptionsSnapshot<WebsiteOptions> websiteOptions,
|
||||||
|
IOptionsSnapshot<StripeOptions> stripeOptions,
|
||||||
|
CreatorsDbContext dbContext)
|
||||||
|
: EndpointWithoutRequest<ConnectStripeResponse>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/stripe/connect");
|
||||||
|
Options(o => o.WithTags("Creators"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
// 1. Get the creator's information
|
||||||
|
Guid creatorId = HttpContext.User.GetUserId();
|
||||||
|
string email = HttpContext.User.GetEmail();
|
||||||
|
|
||||||
|
// 2. Get or create the creator
|
||||||
|
Creator? creator = await dbContext
|
||||||
|
.Creators
|
||||||
|
.SingleOrDefaultAsync(
|
||||||
|
c => c.Id == creatorId,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
if (creator is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Create a Stripe account
|
||||||
|
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
|
||||||
|
AccountService accountService = new();
|
||||||
|
if (string.IsNullOrWhiteSpace(creator.StripeAccountId))
|
||||||
|
{
|
||||||
|
Account? account = await accountService.CreateAsync(
|
||||||
|
new AccountCreateOptions
|
||||||
|
{
|
||||||
|
Type = "express",
|
||||||
|
Capabilities = new AccountCapabilitiesOptions
|
||||||
|
{
|
||||||
|
Transfers = new AccountCapabilitiesTransfersOptions { Requested = true }
|
||||||
|
},
|
||||||
|
Email = email
|
||||||
|
},
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
// 5. Update the creator's Stripe account ID
|
||||||
|
creator.StripeAccountId = account.Id;
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check if the creator already has a Stripe account
|
||||||
|
if (creator is { IsStripeDetailsSubmitted: true, IsStripeChargesEnabled: true, IsStripePayoutReady: true })
|
||||||
|
{
|
||||||
|
await SendErrorsAsync(cancellation: ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Create an account link
|
||||||
|
AccountLinkService accountLinkService = new();
|
||||||
|
AccountLink? accountLink = await accountLinkService.CreateAsync(
|
||||||
|
new AccountLinkCreateOptions
|
||||||
|
{
|
||||||
|
Account = creator.StripeAccountId,
|
||||||
|
RefreshUrl = $"{websiteOptions.Value.FrontendBaseUrl}/profile?stripe=retry",
|
||||||
|
ReturnUrl = $"{websiteOptions.Value.FrontendBaseUrl}/profile?stripe=complete",
|
||||||
|
Type = "account_onboarding"
|
||||||
|
},
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
// 6. Return the account link URL to the client
|
||||||
|
await SendOkAsync(new ConnectStripeResponse(accountLink.Url), ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Infrastructure.Security;
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record CreateCreatorRequest(
|
public record CreateCreatorRequest(
|
||||||
@@ -27,7 +27,7 @@ public sealed class CreateCreatorRequestValidator : Validator<CreateCreatorReque
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public sealed class CreateCreatorHandler(
|
public sealed class CreateCreatorHandler(
|
||||||
ContentDbContext context)
|
CreatorsDbContext context)
|
||||||
: Endpoint<CreateCreatorRequest>
|
: Endpoint<CreateCreatorRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -74,7 +74,7 @@ public sealed class CreateCreatorHandler(
|
|||||||
|
|
||||||
await SendOkAsync(ct);
|
await SendOkAsync(ct);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
await transaction.RollbackAsync(ct);
|
await transaction.RollbackAsync(ct);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public sealed class GetCreatorByIdRequest
|
public sealed class GetCreatorByIdRequest
|
||||||
@@ -22,7 +22,7 @@ public sealed class GetCreatorByIdRequestValidator
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class GetCreatorByIdHandler(
|
public class GetCreatorByIdHandler(
|
||||||
ContentDbContext context)
|
CreatorsDbContext context)
|
||||||
: Endpoint<GetCreatorByIdRequest, Creator>
|
: Endpoint<GetCreatorByIdRequest, Creator>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Infrastructure.Security;
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public sealed class GetCreatorBySlugRequest
|
public sealed class GetCreatorBySlugRequest
|
||||||
@@ -22,11 +22,11 @@ public record GetCreatorBySlugResponse
|
|||||||
public bool AcceptDonation { get; init; }
|
public bool AcceptDonation { get; init; }
|
||||||
public string? BannerUrl { get; init; }
|
public string? BannerUrl { get; init; }
|
||||||
public string? PortraitUrl { get; init; }
|
public string? PortraitUrl { get; init; }
|
||||||
public string Slug { get; init; }
|
public required string Slug { get; init; }
|
||||||
public string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
public string? Title { get; init; }
|
public string? Title { get; init; }
|
||||||
public Socials Socials { get; init; }
|
public Socials? Socials { get; init; }
|
||||||
public Presentation Presentation { get; init; }
|
public Presentation? Presentation { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
@@ -43,7 +43,7 @@ public sealed class GetCreatorBySlugRequestValidator
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class GetCreatorBySlugHandler(
|
public class GetCreatorBySlugHandler(
|
||||||
ContentDbContext context)
|
CreatorsDbContext context)
|
||||||
: Endpoint<GetCreatorBySlugRequest, GetCreatorBySlugResponse>
|
: Endpoint<GetCreatorBySlugRequest, GetCreatorBySlugResponse>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -73,12 +73,12 @@ public class GetCreatorBySlugHandler(
|
|||||||
DeletedAt = c.DeletedAt,
|
DeletedAt = c.DeletedAt,
|
||||||
IsDeleted = c.IsDeleted,
|
IsDeleted = c.IsDeleted,
|
||||||
Verified = c.Verified,
|
Verified = c.Verified,
|
||||||
AcceptDonation = c.AcceptDonation,
|
|
||||||
BannerUrl = c.BannerUrl,
|
BannerUrl = c.BannerUrl,
|
||||||
PortraitUrl = c.PortraitUrl,
|
PortraitUrl = c.PortraitUrl,
|
||||||
Slug = c.Slug,
|
Slug = c.Slug,
|
||||||
Name = c.Name,
|
Name = c.Name,
|
||||||
Title = c.Title,
|
Title = c.Title,
|
||||||
|
AcceptDonation = c.IsStripeChargesEnabled && c.IsStripePayoutReady,
|
||||||
Socials = c.Socials,
|
Socials = c.Socials,
|
||||||
Presentation = c.Presentation
|
Presentation = c.Presentation
|
||||||
})
|
})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Infrastructure.Security;
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public sealed class GetCreatorProfileResponse
|
public sealed class GetCreatorProfileResponse
|
||||||
@@ -16,26 +16,29 @@ public sealed class GetCreatorProfileResponse
|
|||||||
public required string Slug { get; set; }
|
public required string Slug { get; set; }
|
||||||
public string? Title { get; set; }
|
public string? Title { get; set; }
|
||||||
public bool Verified { get; set; }
|
public bool Verified { get; set; }
|
||||||
public bool AcceptDonation { get; set; }
|
public bool IsStripeAccountPresent { get; set; }
|
||||||
|
public bool IsStripeDetailsSubmitted { get; set; }
|
||||||
|
public bool IsStripePayoutReady { get; set; }
|
||||||
|
public bool IsStripeChargesEnabled { get; set; }
|
||||||
public required Presentation Presentation { get; set; }
|
public required Presentation Presentation { get; set; }
|
||||||
public required Socials Socials { get; set; }
|
public required Socials Socials { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class GetCreatorProfileHandler(
|
public class GetCreatorProfileHandler(
|
||||||
ContentDbContext context)
|
CreatorsDbContext context)
|
||||||
: EndpointWithoutRequest<GetCreatorProfileResponse>
|
: EndpointWithoutRequest<GetCreatorProfileResponse>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
{
|
{
|
||||||
Get("/api/creators/profile");
|
Get("/api/creators/profile");
|
||||||
Options((o => o.WithTags("Creators")));
|
Options(o => o.WithTags("Creators"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task HandleAsync(
|
public override async Task HandleAsync(
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var creator = await context
|
GetCreatorProfileResponse? creator = await context
|
||||||
.Creators
|
.Creators
|
||||||
.IgnoreQueryFilters()
|
.IgnoreQueryFilters()
|
||||||
.Where(c => c.Id == HttpContext.User.GetUserId())
|
.Where(c => c.Id == HttpContext.User.GetUserId())
|
||||||
@@ -52,13 +55,22 @@ public class GetCreatorProfileHandler(
|
|||||||
Slug = c.Slug,
|
Slug = c.Slug,
|
||||||
Title = c.Title,
|
Title = c.Title,
|
||||||
Verified = c.Verified,
|
Verified = c.Verified,
|
||||||
AcceptDonation = c.AcceptDonation,
|
IsStripeAccountPresent = !string.IsNullOrWhiteSpace(c.StripeAccountId),
|
||||||
|
IsStripeDetailsSubmitted = c.IsStripeDetailsSubmitted,
|
||||||
|
IsStripeChargesEnabled = c.IsStripeChargesEnabled,
|
||||||
|
IsStripePayoutReady = c.IsStripePayoutReady,
|
||||||
Presentation = c.Presentation,
|
Presentation = c.Presentation,
|
||||||
Socials = c.Socials,
|
Socials = c.Socials
|
||||||
})
|
})
|
||||||
.SingleOrDefaultAsync(ct);
|
.SingleOrDefaultAsync(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Infrastructure.Security;
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record RemoveCreatorRequest(
|
public record RemoveCreatorRequest(
|
||||||
@@ -21,7 +21,7 @@ public sealed class RemoveCreatorRequestValidator : Validator<RemoveCreatorReque
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public sealed class RemoveCreatorHandler(
|
public sealed class RemoveCreatorHandler(
|
||||||
ContentDbContext context)
|
CreatorsDbContext context)
|
||||||
: Endpoint<RemoveCreatorRequest>
|
: Endpoint<RemoveCreatorRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Infrastructure.Security;
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Configuration;
|
||||||
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
using Hutopy.Modules.Creators.Services;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record ReserveSlugRequest
|
public record ReserveSlugRequest
|
||||||
@@ -28,15 +30,15 @@ public sealed class ReserveSlugRequestValidator : Validator<ReserveSlugRequest>
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public sealed class ReserveSlug(
|
public sealed class ReserveSlug(
|
||||||
ContentDbContext context,
|
CreatorsDbContext context,
|
||||||
IOptions<ContentOptions> opts,
|
IOptions<CreatorOptions> opts,
|
||||||
SlugPurger slugPurger)
|
SlugPurger slugPurger)
|
||||||
: Endpoint<ReserveSlugRequest>
|
: Endpoint<ReserveSlugRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
{
|
{
|
||||||
Post("/api/creators/@{Slug}/reserve");
|
Post("/api/creators/@{Slug}/reserve");
|
||||||
Options(o => o.WithTags("Contents"));
|
Options(o => o.WithTags("Creators"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task HandleAsync(
|
public override async Task HandleAsync(
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Infrastructure.Security;
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record RestoreCreatorRequest(
|
public record RestoreCreatorRequest(
|
||||||
@@ -21,7 +21,7 @@ public sealed class RestoreCreatorRequestValidator : Validator<RestoreCreatorReq
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public sealed class RestoreCreatorHandler(
|
public sealed class RestoreCreatorHandler(
|
||||||
ContentDbContext context)
|
CreatorsDbContext context)
|
||||||
: Endpoint<RestoreCreatorRequest>
|
: Endpoint<RestoreCreatorRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
48
backend/Modules/Creators/Features/RevokeStripe.cs
Normal file
48
backend/Modules/Creators/Features/RevokeStripe.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using Hutopy.Infrastructure.Security;
|
||||||
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Creators.Features;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public class RemoveStripeHandler(
|
||||||
|
CreatorsDbContext dbContext)
|
||||||
|
: EndpointWithoutRequest
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Delete("/api/stripe");
|
||||||
|
Options(o => o.WithTags("Creators"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
// 1. Get the creator's ID from the authenticated user
|
||||||
|
Guid creatorId = HttpContext.User.GetUserId();
|
||||||
|
|
||||||
|
// 2. Retrieve the creator from the database
|
||||||
|
Creator? creator = await dbContext
|
||||||
|
.Creators
|
||||||
|
.SingleOrDefaultAsync(
|
||||||
|
c => c.Id == creatorId,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
// 3. If the creator doesn't exist or has no Stripe account linked, return 404
|
||||||
|
if (creator is null || string.IsNullOrWhiteSpace(creator.StripeAccountId))
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Remove Stripe configuration
|
||||||
|
creator.StripeAccountId = null;
|
||||||
|
creator.IsStripeDetailsSubmitted = false;
|
||||||
|
creator.IsStripeChargesEnabled = false;
|
||||||
|
creator.IsStripePayoutReady = false;
|
||||||
|
|
||||||
|
// 5. Persist changes
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
// 6. Respond with success
|
||||||
|
await SendOkAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
@@ -9,75 +9,24 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Data.Migrations
|
namespace Hutopy.Modules.Creators.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ContentDbContext))]
|
[DbContext(typeof(CreatorsDbContext))]
|
||||||
[Migration("20250423153323_AddPresentation")]
|
[Migration("20250609203815_Initial")]
|
||||||
partial class AddPresentation
|
partial class Initial
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasDefaultSchema("Content")
|
.HasDefaultSchema("Creators")
|
||||||
.HasAnnotation("ProductVersion", "9.0.3")
|
.HasAnnotation("ProductVersion", "9.0.3")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
|
modelBuilder.Entity("Hutopy.Modules.Creators.Data.Creator", b =>
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
|
||||||
|
|
||||||
b.Property<Guid>("CreatedBy")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<Guid>("CreatorId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<Guid?>("DeletedBy")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.Property<string>("HtmlFileUrl")
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.Property<string>("ThumbnailUrl")
|
|
||||||
.HasMaxLength(2048)
|
|
||||||
.HasColumnType("character varying(2048)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("Urls")
|
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatorId");
|
|
||||||
|
|
||||||
b.ToTable("Contents", "Content");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>
|
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -107,10 +56,19 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
|||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
|
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
|
||||||
|
|
||||||
|
b.Property<bool>("IsStripeChargesEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStripeOnboardingComplete")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStripePayoutReady")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(255)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("character varying(255)");
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
b.Property<string>("PortraitUrl")
|
b.Property<string>("PortraitUrl")
|
||||||
.HasMaxLength(2048)
|
.HasMaxLength(2048)
|
||||||
@@ -121,19 +79,23 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
|||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
.HasColumnType("character varying(128)");
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("StripeAccountId")
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("character varying(21)");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.HasMaxLength(255)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("character varying(255)");
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
b.Property<bool>("Verified")
|
b.Property<bool>("Verified")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("Creators", "Content");
|
b.ToTable("Creators", "Creators");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Slugs", b =>
|
modelBuilder.Entity("Hutopy.Modules.Creators.Data.Slugs", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -155,7 +117,7 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
|||||||
.ValueGeneratedOnAddOrUpdate()
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
.HasColumnType("character varying(128)")
|
.HasColumnType("character varying(128)")
|
||||||
.HasComputedColumnSql("LOWER( \"Content\".\"Slugs\".\"Name\")", true);
|
.HasComputedColumnSql("LOWER(\"Name\")", true);
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("ReservedUntil")
|
b.Property<DateTimeOffset>("ReservedUntil")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
@@ -168,69 +130,27 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
|||||||
b.HasIndex("NormalizedName")
|
b.HasIndex("NormalizedName")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Slugs", "Content");
|
b.ToTable("Slugs", "Creators");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
|
modelBuilder.Entity("Hutopy.Modules.Creators.Data.Creator", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Hutopy.Web.Features.Contents.Data.Creator", "Creator")
|
b.OwnsOne("Hutopy.Modules.Creators.Data.Presentation", "Presentation", b1 =>
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("CreatorId");
|
|
||||||
|
|
||||||
b.OwnsMany("Hutopy.Web.Features.Contents.Data.ContentReaction", "Reactions", b1 =>
|
|
||||||
{
|
|
||||||
b1.Property<Guid>("ContentId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b1.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
|
|
||||||
|
|
||||||
b1.Property<int>("Reaction")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b1.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b1.Property<string>("UserName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b1.HasKey("ContentId", "Id");
|
|
||||||
|
|
||||||
b1.ToTable("Reactions", "Content");
|
|
||||||
|
|
||||||
b1.WithOwner()
|
|
||||||
.HasForeignKey("ContentId");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.Navigation("Creator");
|
|
||||||
|
|
||||||
b.Navigation("Reactions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>
|
|
||||||
{
|
|
||||||
b.OwnsOne("Hutopy.Web.Features.Contents.Data.Presentation", "Presentation", b1 =>
|
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("CreatorId")
|
b1.Property<Guid>("CreatorId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b1.Property<string>("Description")
|
b1.Property<string>("Description")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(2000)
|
.HasColumnType("text");
|
||||||
.HasColumnType("character varying(2000)");
|
|
||||||
|
|
||||||
b1.Property<string>("Email")
|
b1.Property<string>("Email")
|
||||||
.HasMaxLength(255)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("character varying(255)");
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
b1.Property<string>("PhoneNumber")
|
b1.Property<string>("PhoneNumber")
|
||||||
.HasMaxLength(255)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("character varying(255)");
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
b1.Property<string>("VideoUrl")
|
b1.Property<string>("VideoUrl")
|
||||||
.HasMaxLength(2048)
|
.HasMaxLength(2048)
|
||||||
@@ -238,13 +158,13 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
|||||||
|
|
||||||
b1.HasKey("CreatorId");
|
b1.HasKey("CreatorId");
|
||||||
|
|
||||||
b1.ToTable("Presentation", "Content");
|
b1.ToTable("Presentation", "Creators");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("CreatorId");
|
.HasForeignKey("CreatorId");
|
||||||
});
|
});
|
||||||
|
|
||||||
b.OwnsOne("Hutopy.Web.Features.Contents.Data.Socials", "Socials", b1 =>
|
b.OwnsOne("Hutopy.Modules.Creators.Data.Socials", "Socials", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("CreatorId")
|
b1.Property<Guid>("CreatorId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
@@ -283,7 +203,7 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
|||||||
|
|
||||||
b1.HasKey("CreatorId");
|
b1.HasKey("CreatorId");
|
||||||
|
|
||||||
b1.ToTable("Socials", "Content");
|
b1.ToTable("Socials", "Creators");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("CreatorId");
|
.HasForeignKey("CreatorId");
|
||||||
141
backend/Modules/Creators/Migrations/20250609203815_Initial.cs
Normal file
141
backend/Modules/Creators/Migrations/20250609203815_Initial.cs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Creators.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Initial : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.EnsureSchema(
|
||||||
|
name: "Creators");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Creators",
|
||||||
|
schema: "Creators",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
DeletedBy = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
DeletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "boolean", nullable: false, computedColumnSql: "\"DeletedAt\" IS NOT NULL", stored: true),
|
||||||
|
BannerUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
PortraitUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
Verified = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||||
|
Slug = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
StripeAccountId = table.Column<string>(type: "character varying(21)", maxLength: 21, nullable: true),
|
||||||
|
IsStripeOnboardingComplete = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
IsStripePayoutReady = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
IsStripeChargesEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
AcceptDonation = table.Column<bool>(type: "boolean", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Creators", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Slugs",
|
||||||
|
schema: "Creators",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UsedBy = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
NormalizedName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false, computedColumnSql: "LOWER(\"Name\")", stored: true),
|
||||||
|
ReservedUntil = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Slugs", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Presentation",
|
||||||
|
schema: "Creators",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
CreatorId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "text", nullable: false),
|
||||||
|
VideoUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
PhoneNumber = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Presentation", x => x.CreatorId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Presentation_Creators_CreatorId",
|
||||||
|
column: x => x.CreatorId,
|
||||||
|
principalSchema: "Creators",
|
||||||
|
principalTable: "Creators",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Socials",
|
||||||
|
schema: "Creators",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
CreatorId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
FacebookUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
InstagramUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
XUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
LinkedInUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
TikTokUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
YoutubeUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
RedditUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
WebsiteUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Socials", x => x.CreatorId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Socials_Creators_CreatorId",
|
||||||
|
column: x => x.CreatorId,
|
||||||
|
principalSchema: "Creators",
|
||||||
|
principalTable: "Creators",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Slugs_NormalizedName",
|
||||||
|
schema: "Creators",
|
||||||
|
table: "Slugs",
|
||||||
|
column: "NormalizedName",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Presentation",
|
||||||
|
schema: "Creators");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Slugs",
|
||||||
|
schema: "Creators");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Socials",
|
||||||
|
schema: "Creators");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Creators",
|
||||||
|
schema: "Creators");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
218
backend/Modules/Creators/Migrations/20250610200446_AddStripe.Designer.cs
generated
Normal file
218
backend/Modules/Creators/Migrations/20250610200446_AddStripe.Designer.cs
generated
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Creators.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(CreatorsDbContext))]
|
||||||
|
[Migration("20250610200446_AddStripe")]
|
||||||
|
partial class AddStripe
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("Creators")
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.3")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Creators.Data.Creator", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("BannerUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DeletedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
|
||||||
|
|
||||||
|
b.Property<bool>("IsStripeChargesEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStripeDetailsSubmitted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStripePayoutReady")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PortraitUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("StripeAccountId")
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("character varying(21)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("Verified")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Creators", "Creators");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Creators.Data.Slugs", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasComputedColumnSql("LOWER(\"Name\")", true);
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ReservedUntil")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UsedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Slugs", "Creators");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Creators.Data.Creator", b =>
|
||||||
|
{
|
||||||
|
b.OwnsOne("Hutopy.Modules.Creators.Data.Presentation", "Presentation", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("CreatorId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b1.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b1.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b1.Property<string>("VideoUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.HasKey("CreatorId");
|
||||||
|
|
||||||
|
b1.ToTable("Presentation", "Creators");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("CreatorId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("Hutopy.Modules.Creators.Data.Socials", "Socials", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("CreatorId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("FacebookUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("InstagramUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("LinkedInUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("RedditUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("TikTokUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("WebsiteUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("XUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("YoutubeUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.HasKey("CreatorId");
|
||||||
|
|
||||||
|
b1.ToTable("Socials", "Creators");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("CreatorId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Presentation")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Socials")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,30 +2,42 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Data.Migrations
|
namespace Hutopy.Modules.Creators.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class Adds_AcceptDonation_Creator : Migration
|
public partial class AddStripe : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<bool>(
|
migrationBuilder.DropColumn(
|
||||||
name: "AcceptDonation",
|
name: "AcceptDonation",
|
||||||
schema: "Content",
|
schema: "Creators",
|
||||||
|
table: "Creators");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "IsStripeOnboardingComplete",
|
||||||
|
schema: "Creators",
|
||||||
table: "Creators",
|
table: "Creators",
|
||||||
type: "boolean",
|
newName: "IsStripeDetailsSubmitted");
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "IsStripeDetailsSubmitted",
|
||||||
|
schema: "Creators",
|
||||||
|
table: "Creators",
|
||||||
|
newName: "IsStripeOnboardingComplete");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
name: "AcceptDonation",
|
name: "AcceptDonation",
|
||||||
schema: "Content",
|
schema: "Creators",
|
||||||
table: "Creators");
|
table: "Creators",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Creators.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(CreatorsDbContext))]
|
||||||
|
partial class CreatorsDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("Creators")
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.3")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Creators.Data.Creator", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("BannerUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DeletedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
|
||||||
|
|
||||||
|
b.Property<bool>("IsStripeChargesEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStripeDetailsSubmitted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStripePayoutReady")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PortraitUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("StripeAccountId")
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("character varying(21)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("Verified")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Creators", "Creators");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Creators.Data.Slugs", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasComputedColumnSql("LOWER(\"Name\")", true);
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ReservedUntil")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UsedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Slugs", "Creators");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Modules.Creators.Data.Creator", b =>
|
||||||
|
{
|
||||||
|
b.OwnsOne("Hutopy.Modules.Creators.Data.Presentation", "Presentation", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("CreatorId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b1.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b1.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b1.Property<string>("VideoUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.HasKey("CreatorId");
|
||||||
|
|
||||||
|
b1.ToTable("Presentation", "Creators");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("CreatorId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("Hutopy.Modules.Creators.Data.Socials", "Socials", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("CreatorId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("FacebookUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("InstagramUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("LinkedInUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("RedditUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("TikTokUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("WebsiteUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("XUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.Property<string>("YoutubeUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b1.HasKey("CreatorId");
|
||||||
|
|
||||||
|
b1.ToTable("Socials", "Creators");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("CreatorId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Presentation")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Socials")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
backend/Modules/Creators/Services/CreatorLookup.cs
Normal file
26
backend/Modules/Creators/Services/CreatorLookup.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Hutopy.Modules.Creators.Contracts;
|
||||||
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Creators.Services;
|
||||||
|
|
||||||
|
public sealed class CreatorLookup(
|
||||||
|
CreatorsDbContext context)
|
||||||
|
: ICreatorLookup
|
||||||
|
{
|
||||||
|
public async Task<CreatorReference?> GetCreatorAsync(Guid creatorId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Creator? creator = await context
|
||||||
|
.Creators
|
||||||
|
.FirstOrDefaultAsync(c => c.Id == creatorId, cancellationToken);
|
||||||
|
|
||||||
|
return creator is null
|
||||||
|
? null
|
||||||
|
: new CreatorReference(
|
||||||
|
creator.Id,
|
||||||
|
creator.Name,
|
||||||
|
creator.PortraitUrl,
|
||||||
|
creator.IsStripeDetailsSubmitted,
|
||||||
|
creator.IsStripeChargesEnabled,
|
||||||
|
creator.StripeAccountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
namespace Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Modules.Creators.Data;
|
||||||
|
|
||||||
public class SlugPurger(ContentDbContext context)
|
namespace Hutopy.Modules.Creators.Services;
|
||||||
|
|
||||||
|
public class SlugPurger(CreatorsDbContext context)
|
||||||
{
|
{
|
||||||
private static readonly SemaphoreSlim _semaphore = new(1, 1);
|
private static readonly SemaphoreSlim Semaphore = new(1, 1);
|
||||||
private static DateTimeOffset _lastPurgeTime = DateTimeOffset.MinValue;
|
private static DateTimeOffset s_lastPurgeTime = DateTimeOffset.MinValue;
|
||||||
private static readonly TimeSpan _minTimeBetweenPurges = TimeSpan.FromSeconds(10);
|
private static readonly TimeSpan MinTimeBetweenPurges = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
public async Task PurgeExpiredSlugsAsync(CancellationToken ct)
|
public async Task PurgeExpiredSlugsAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
// Try to acquire the semaphore
|
// Try to acquire the semaphore
|
||||||
if (!await _semaphore.WaitAsync(0, ct))
|
if (!await Semaphore.WaitAsync(0, ct))
|
||||||
{
|
{
|
||||||
// Another purge operation is in progress, skip this one
|
// Another purge operation is in progress, skip this one
|
||||||
return;
|
return;
|
||||||
@@ -18,7 +20,7 @@ public class SlugPurger(ContentDbContext context)
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
if (now - _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
|
||||||
return;
|
return;
|
||||||
@@ -31,11 +33,11 @@ public class SlugPurger(ContentDbContext context)
|
|||||||
.ExecuteDeleteAsync(ct);
|
.ExecuteDeleteAsync(ct);
|
||||||
|
|
||||||
// Update the last purge time regardless of whether we found expired slugs or not
|
// Update the last purge time regardless of whether we found expired slugs or not
|
||||||
_lastPurgeTime = now;
|
s_lastPurgeTime = now;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_semaphore.Release();
|
Semaphore.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Hutopy.Web.Features.Users;
|
namespace Hutopy.Modules.Identity.Configuration;
|
||||||
|
|
||||||
public record JwtOptions
|
public record JwtOptions
|
||||||
{
|
{
|
||||||
6
backend/Modules/Identity/Contracts/IUserLookup.cs
Normal file
6
backend/Modules/Identity/Contracts/IUserLookup.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Hutopy.Modules.Identity.Contracts;
|
||||||
|
|
||||||
|
public interface IUserLookup
|
||||||
|
{
|
||||||
|
Task<UserReference?> GetUserAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Hutopy.Web.Features.Users;
|
namespace Hutopy.Modules.Identity.Contracts;
|
||||||
|
|
||||||
public static class KnownRoles
|
public static class KnownRoles
|
||||||
{
|
{
|
||||||
6
backend/Modules/Identity/Contracts/UserReference.cs
Normal file
6
backend/Modules/Identity/Contracts/UserReference.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Hutopy.Modules.Identity.Contracts;
|
||||||
|
|
||||||
|
public record UserReference(
|
||||||
|
Guid Id,
|
||||||
|
string Fullname,
|
||||||
|
string? PortraitUrl);
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Users.Data
|
namespace Hutopy.Modules.Identity.Data
|
||||||
{
|
{
|
||||||
public class ApplicationDbContext(
|
public class IdentityDbContext(
|
||||||
DbContextOptions<ApplicationDbContext> options)
|
DbContextOptions<IdentityDbContext> options)
|
||||||
: IdentityDbContext<IdentityUser, IdentityRole, Guid>(options)
|
: IdentityDbContext<User, Role, Guid>(options)
|
||||||
{
|
{
|
||||||
public const string SchemaName = "Identity";
|
public const string SchemaName = "Identity";
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder
|
protected override void OnModelCreating(ModelBuilder
|
||||||
modelBuilder)
|
modelBuilder)
|
||||||
{
|
{
|
||||||
60
backend/Modules/Identity/Data/IdentityService.cs
Normal file
60
backend/Modules/Identity/Data/IdentityService.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Hutopy.Modules.Identity.Models;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Identity.Data;
|
||||||
|
|
||||||
|
public class IdentityService(
|
||||||
|
UserManager userManager,
|
||||||
|
IHttpContextAccessor contextAccessor
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public async Task<UserModel?> GetCurrentUserAsync()
|
||||||
|
{
|
||||||
|
var currentUserId = contextAccessor.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (string.IsNullOrEmpty(currentUserId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserModel? ret;
|
||||||
|
var user = await userManager.FindByIdAsync(currentUserId);
|
||||||
|
|
||||||
|
if (user == null) ret = null;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var userModel = new UserModel
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
Username = user.UserName ?? string.Empty,
|
||||||
|
PhoneNumber = user.PhoneNumber ?? string.Empty,
|
||||||
|
Email = user.Email ?? string.Empty,
|
||||||
|
PortraitUrl = user.PortraitUrl,
|
||||||
|
Alias = user.Alias,
|
||||||
|
Firstname = user.Firstname,
|
||||||
|
Lastname = user.Lastname,
|
||||||
|
BirthDate = user.BirthDate,
|
||||||
|
Address = user.Address
|
||||||
|
};
|
||||||
|
|
||||||
|
ret = userModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IList<string>> GetCurrentUserRolesAsync()
|
||||||
|
{
|
||||||
|
var currentUserModel = await GetCurrentUserAsync();
|
||||||
|
|
||||||
|
if (currentUserModel is null) return [];
|
||||||
|
|
||||||
|
var currentUser = await userManager.FindByIdAsync(currentUserModel.Id.ToString());
|
||||||
|
|
||||||
|
if (currentUser is null) return [];
|
||||||
|
|
||||||
|
var userRoles = await userManager.GetRolesAsync(currentUser);
|
||||||
|
|
||||||
|
return userRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
9
backend/Modules/Identity/Data/Role.cs
Normal file
9
backend/Modules/Identity/Data/Role.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Identity.Data;
|
||||||
|
|
||||||
|
public class Role : IdentityRole<Guid>
|
||||||
|
{
|
||||||
|
public Role() { }
|
||||||
|
public Role(string roleName) : base(roleName) { }
|
||||||
|
}
|
||||||
20
backend/Modules/Identity/Data/User.cs
Normal file
20
backend/Modules/Identity/Data/User.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Identity.Data;
|
||||||
|
|
||||||
|
public class User : IdentityUser<Guid>
|
||||||
|
{
|
||||||
|
[MaxLength(256)] public string? Alias { get; set; }
|
||||||
|
[MaxLength(256)] public string? Firstname { get; set; }
|
||||||
|
[MaxLength(256)] public string? Lastname { get; set; }
|
||||||
|
public DateTime? BirthDate { get; set; }
|
||||||
|
[MaxLength(256)] public string? Address { get; set; }
|
||||||
|
[MaxLength(2048)] public string? PortraitUrl { get; set; }
|
||||||
|
[MaxLength(256)] public string? GoogleId { get; set; }
|
||||||
|
[MaxLength(256)] public string? FacebookId { get; set; }
|
||||||
|
[MaxLength(44)] public string? RefreshToken { get; set; }
|
||||||
|
public DateTime RefreshTokenExpiryTime { get; set; }
|
||||||
|
public string Fullname => $"{Lastname}, {Firstname}";
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Users.Data;
|
namespace Hutopy.Modules.Identity.Data;
|
||||||
|
|
||||||
public sealed class IdentityUserManager(
|
public sealed class UserManager(
|
||||||
IUserStore<IdentityUser> store,
|
IUserStore<User> store,
|
||||||
IOptions<IdentityOptions> optionsAccessor,
|
IOptions<IdentityOptions> optionsAccessor,
|
||||||
IPasswordHasher<IdentityUser> passwordHasher,
|
IPasswordHasher<User> passwordHasher,
|
||||||
IEnumerable<IUserValidator<IdentityUser>> userValidators,
|
IEnumerable<IUserValidator<User>> userValidators,
|
||||||
IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators,
|
IEnumerable<IPasswordValidator<User>> passwordValidators,
|
||||||
ILookupNormalizer keyNormalizer,
|
ILookupNormalizer keyNormalizer,
|
||||||
IdentityErrorDescriber errors,
|
IdentityErrorDescriber errors,
|
||||||
IServiceProvider services,
|
IServiceProvider services,
|
||||||
ILogger<UserManager<IdentityUser>> logger)
|
ILogger<UserManager<User>> logger)
|
||||||
: UserManager<IdentityUser>(
|
: UserManager<User>(
|
||||||
store,
|
store,
|
||||||
optionsAccessor,
|
optionsAccessor,
|
||||||
passwordHasher,
|
passwordHasher,
|
||||||
72
backend/Modules/Identity/DependencyInjection.cs
Normal file
72
backend/Modules/Identity/DependencyInjection.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using Hutopy.Modules.Identity.Configuration;
|
||||||
|
using Hutopy.Modules.Identity.Contracts;
|
||||||
|
using Hutopy.Modules.Identity.Data;
|
||||||
|
using Hutopy.Modules.Identity.Services;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace Hutopy.Modules.Identity;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static WebApplicationBuilder AddIdentityModule(
|
||||||
|
this WebApplicationBuilder builder,
|
||||||
|
Action<DbContextOptionsBuilder>? configureAction = null)
|
||||||
|
{
|
||||||
|
builder.Services.AddDbContext<IdentityDbContext>(configureAction);
|
||||||
|
|
||||||
|
builder.Services.Configure<JwtOptions>(
|
||||||
|
builder.Configuration.GetRequiredSection(JwtOptions.SectionName));
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication()
|
||||||
|
.AddBearerToken(IdentityConstants.BearerScheme);
|
||||||
|
|
||||||
|
builder.Services.AddAuthorizationBuilder();
|
||||||
|
|
||||||
|
builder.Services
|
||||||
|
.AddIdentityCore<User>()
|
||||||
|
.AddUserManager<UserManager>()
|
||||||
|
.AddRoles<Role>()
|
||||||
|
.AddEntityFrameworkStores<IdentityDbContext>()
|
||||||
|
.AddApiEndpoints()
|
||||||
|
.AddDefaultTokenProviders();
|
||||||
|
|
||||||
|
// Singleton services
|
||||||
|
builder.Services.AddSingleton(TimeProvider.System);
|
||||||
|
|
||||||
|
// Scoped services
|
||||||
|
builder.Services.AddScoped<IdentityService>();
|
||||||
|
builder.Services.AddTransient<IUserLookup, UserLookup>();
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<IApplicationBuilder> UseIdentityModuleAsync(
|
||||||
|
this IApplicationBuilder app,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
|
||||||
|
using var scope = scopeFactory.CreateScope();
|
||||||
|
await using var context = scope.ServiceProvider.GetRequiredService<IdentityDbContext>();
|
||||||
|
await context.Database.MigrateAsync(cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<Role>>();
|
||||||
|
await TrySeedAsync(roleManager);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task TrySeedAsync(RoleManager<Role> roleManager)
|
||||||
|
{
|
||||||
|
var administratorRole = new Role(KnownRoles.Administrator);
|
||||||
|
if (roleManager.Roles.All(r => r.Name != administratorRole.Name))
|
||||||
|
{
|
||||||
|
await roleManager.CreateAsync(administratorRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
var roleCreator = new Role(KnownRoles.Creator);
|
||||||
|
if (roleManager.Roles.All(r => r.Name != roleCreator.Name))
|
||||||
|
{
|
||||||
|
await roleManager.CreateAsync(roleCreator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Infrastructure.Security;
|
||||||
using Hutopy.Web.Features.Users.Data;
|
using Hutopy.Modules.Identity.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Users.Handlers;
|
namespace Hutopy.Modules.Identity.Handlers;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record ChangeAddressRequest(
|
public record ChangeAddressRequest(
|
||||||
@@ -9,7 +9,7 @@ public record ChangeAddressRequest(
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ChangeAddressHandler(
|
public class ChangeAddressHandler(
|
||||||
IdentityUserManager userManager)
|
UserManager userManager)
|
||||||
: Endpoint<ChangeAddressRequest>
|
: Endpoint<ChangeAddressRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Infrastructure.Security;
|
||||||
using Hutopy.Web.Features.Users.Data;
|
using Hutopy.Modules.Identity.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Users.Handlers;
|
namespace Hutopy.Modules.Identity.Handlers;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record ChangeAliasRequest(
|
public record ChangeAliasRequest(
|
||||||
@@ -9,7 +9,7 @@ public record ChangeAliasRequest(
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ChangeAliasHandler(
|
public class ChangeAliasHandler(
|
||||||
IdentityUserManager userManager)
|
UserManager userManager)
|
||||||
: Endpoint<ChangeAliasRequest>
|
: Endpoint<ChangeAliasRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Infrastructure.Security;
|
||||||
using Hutopy.Web.Features.Users.Data;
|
using Hutopy.Modules.Identity.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Users.Handlers;
|
namespace Hutopy.Modules.Identity.Handlers;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record ChangeBirthDateRequest(
|
public record ChangeBirthDateRequest(
|
||||||
@@ -9,7 +9,7 @@ public record ChangeBirthDateRequest(
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ChangeBirthDateHandler(
|
public class ChangeBirthDateHandler(
|
||||||
IdentityUserManager userManager)
|
UserManager userManager)
|
||||||
: Endpoint<ChangeBirthDateRequest>
|
: Endpoint<ChangeBirthDateRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Hutopy.Web.Common.Security;
|
using Hutopy.Infrastructure.Security;
|
||||||
using Hutopy.Web.Features.Users.Data;
|
using Hutopy.Modules.Identity.Data;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Users.Handlers;
|
namespace Hutopy.Modules.Identity.Handlers;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record ChangeEmailRequest(
|
public record ChangeEmailRequest(
|
||||||
@@ -9,7 +9,7 @@ public record ChangeEmailRequest(
|
|||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class ChangeEmailHandler(
|
public class ChangeEmailHandler(
|
||||||
IdentityUserManager userManager)
|
UserManager userManager)
|
||||||
: Endpoint<ChangeEmailRequest>
|
: Endpoint<ChangeEmailRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user