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
|
||||
run: |
|
||||
cd backend
|
||||
dotnet publish --configuration Release --artifacts-path ./publish/
|
||||
dotnet publish --configuration Release --artifacts-path ./publish/ backend.sln
|
||||
|
||||
# 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 Azure.Identity;
|
||||
using Hutopy.Web.Features.Users.Data;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
using Microsoft.AspNetCore.Authentication.Facebook;
|
||||
using Microsoft.AspNetCore.Authentication.Google;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Hutopy.Web;
|
||||
namespace Hutopy;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
@@ -19,7 +17,7 @@ public static class DependencyInjection
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
services.AddHealthChecks()
|
||||
.AddDbContextCheck<ApplicationDbContext>();
|
||||
.AddDbContextCheck<IdentityDbContext>();
|
||||
|
||||
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">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Hutopy.Web</RootNamespace>
|
||||
<AssemblyName>Hutopy.Web</AssemblyName>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>default</LangVersion>
|
||||
<UserSecretsId>de6d03c4-8b1c-49e2-a8ca-c38cd4dc7d85</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.24.0" />
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.4.0" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.13.2" />
|
||||
<PackageReference Include="FastEndpoints" Version="5.35.0" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
|
||||
<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.JwtBearer" 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.OpenApi" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="NSwag.AspNetCore" Version="14.3.0" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.24.0"/>
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.4.0"/>
|
||||
<PackageReference Include="Azure.Identity" Version="1.13.2"/>
|
||||
<PackageReference Include="FastEndpoints" Version="5.35.0"/>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0"/>
|
||||
<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.JwtBearer" 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.OpenApi" Version="9.0.3"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.3"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore"
|
||||
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">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
|
||||
<PackageReference Include="Stripe.net" Version="47.4.0" />
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0"/>
|
||||
<PackageReference Include="Postmark" Version="5.2.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>
|
||||
|
||||
</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
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Hutopy.Web.Common.BlobStorage;
|
||||
namespace Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
public static class ContentTypes
|
||||
{
|
||||
@@ -39,11 +39,6 @@ public static class ContentTypes
|
||||
|
||||
// Check for HTML content by looking for "<!DOCTYPE html>" or "<html>" tags
|
||||
string content = Encoding.UTF8.GetString(buffer);
|
||||
if (content.Contains("<!DOCTYPE html>"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return content.Contains("<!DOCTYPE html>");
|
||||
}
|
||||
}
|
||||
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.Storage.Blobs;
|
||||
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
|
||||
|
||||
@@ -27,8 +28,12 @@ public class AzureBlobStorage
|
||||
/// <param name="contentType">The content type.</param>
|
||||
/// <param name="ct">The cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> UploadFileAsync(string containerName, string blobName, Stream stream,
|
||||
string contentType, CancellationToken ct = default)
|
||||
public async Task<string> UploadFileAsync(
|
||||
string containerName,
|
||||
string blobName,
|
||||
Stream stream,
|
||||
string contentType,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Read the file stream into a memory stream to determine the length
|
||||
// WATCH FOR MEMORY USAGE USING THE MEMORY STREAM.
|
||||
@@ -113,7 +118,9 @@ public class AzureBlobStorage
|
||||
/// <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>
|
||||
public async Task<MemoryStream> DownloadFileAsync(string containerName, string blobName,
|
||||
public async Task<MemoryStream> DownloadFileAsync(
|
||||
string containerName,
|
||||
string blobName,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
|
||||
namespace Hutopy.Web.Features.Users;
|
||||
namespace Hutopy.Infrastructure.Configuration;
|
||||
|
||||
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
|
||||
{
|
||||
Task SendEmailAsync(string email, string subject, string message);
|
||||
}
|
||||
|
||||
public class EmailSender(ILogger<IEmailSender> logger)
|
||||
public class LoggerEmailSender(ILogger<IEmailSender> logger)
|
||||
: IEmailSender
|
||||
{
|
||||
|
||||
public async Task SendEmailAsync(string email, string subject, string message)
|
||||
{
|
||||
try
|
||||
try
|
||||
{
|
||||
logger.LogInformation("Sending email to {Email} with subject: {Subject}", email, subject);
|
||||
// TODO: Implement actual email sending logic
|
||||
await Task.Delay(1000);
|
||||
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;
|
||||
|
||||
namespace Hutopy.Web.Common.Security;
|
||||
namespace Hutopy.Infrastructure.Security;
|
||||
|
||||
public static class ClaimsPrincipalExtensions
|
||||
{
|
||||
@@ -43,8 +43,7 @@ public static class ClaimsPrincipalExtensions
|
||||
{
|
||||
var claim = claims.FindFirst(key);
|
||||
|
||||
if (claim is null) return default;
|
||||
return claims.GetRequiredClaim<TValue>(key);
|
||||
return claim is null ? null : claims.GetRequiredClaim<TValue>(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 (typeof(TValue) == typeof(Guid))
|
||||
{
|
||||
return Guid.Parse(claim.Value);
|
||||
}
|
||||
|
||||
return Convert.ChangeType(claim.Value, typeof(TValue));
|
||||
return typeof(TValue) == typeof(Guid) ? Guid.Parse(claim.Value) : Convert.ChangeType(claim.Value, typeof(TValue));
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Hutopy.Web.Common.Security;
|
||||
namespace Hutopy.Infrastructure.Security;
|
||||
|
||||
public static class JwtTokenHelper
|
||||
{
|
||||
@@ -22,23 +22,22 @@ public static class JwtTokenHelper
|
||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
|
||||
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.Jti, Guid.NewGuid().ToString()),
|
||||
new Claim(ClaimTypes.NameIdentifier, userId), new Claim(ClaimTypes.Email, email),
|
||||
new Claim(ClaimTypes.Name, email), new Claim(ClaimTypes.GivenName, firstname),
|
||||
new Claim(ClaimTypes.Surname, lastname)
|
||||
});
|
||||
]);
|
||||
|
||||
if (alias is not null)
|
||||
{
|
||||
claims.Add(new(KnownClaims.Alias, alias));
|
||||
claims.Add(new Claim(KnownClaims.Alias, alias));
|
||||
}
|
||||
|
||||
if (portraitUrl is not null)
|
||||
{
|
||||
claims.Add(new(KnownClaims.PortraitUrl, portraitUrl));
|
||||
claims.Add(new Claim(KnownClaims.PortraitUrl, portraitUrl));
|
||||
}
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hutopy.Web.Common.Security;
|
||||
namespace Hutopy.Infrastructure.Security;
|
||||
|
||||
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.Text;
|
||||
|
||||
namespace Hutopy.Web.Common.Security;
|
||||
namespace Hutopy.Infrastructure.Security;
|
||||
|
||||
// If we need to add special characters we can alternate between 2 pools.
|
||||
public static class PasswordGenerator
|
||||
@@ -1,14 +1,13 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Hutopy.Web.Common.Security;
|
||||
namespace Hutopy.Infrastructure.Security;
|
||||
|
||||
public static class RefreshTokenGenerator
|
||||
{
|
||||
public static string Next()
|
||||
{
|
||||
var randomNumber = new byte[32];
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
rng.GetBytes(randomNumber);
|
||||
RandomNumberGenerator.Fill(randomNumber);
|
||||
return Convert.ToBase64String(randomNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Hutopy.Web.Common.YouTube;
|
||||
namespace Hutopy.Infrastructure.YouTube;
|
||||
|
||||
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.Web.Common.Security;
|
||||
using Hutopy.Web.Common.BlobStorage;
|
||||
using Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Contents.Data;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Contents.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record AddPhotoToAlbumRequest(
|
||||
@@ -23,13 +23,13 @@ public record AddPhotoToAlbumResponse(
|
||||
public sealed class AddPhotoToAlbumRequestValidator : Validator<AddPhotoToAlbumRequest>
|
||||
{
|
||||
private const int MaxFileSizeBytes = 10 * 1024 * 1024; // 10MB
|
||||
private static readonly string[] AllowedImageTypes =
|
||||
{
|
||||
private static readonly string[] AllowedImageTypes =
|
||||
[
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp"
|
||||
};
|
||||
];
|
||||
|
||||
public AddPhotoToAlbumRequestValidator()
|
||||
{
|
||||
@@ -56,8 +56,8 @@ public sealed class AddPhotoToAlbumRequestValidator : Validator<AddPhotoToAlbumR
|
||||
|
||||
[PublicAPI]
|
||||
public class AddPhotoToAlbumHandler(
|
||||
ContentDbContext context,
|
||||
AzureBlobStorage blobStorage)
|
||||
ContentsDbContext context,
|
||||
IBlobStorage blobStorage)
|
||||
: Endpoint<AddPhotoToAlbumRequest, AddPhotoToAlbumResponse>
|
||||
{
|
||||
private const int MaxThumbnailWidth = 500;
|
||||
@@ -133,7 +133,7 @@ public class AddPhotoToAlbumHandler(
|
||||
{
|
||||
await SendStringAsync("Invalid image format", 400, cancellation: ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
await SendStringAsync("Error processing image", 500, cancellation: ct);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Contents.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Contents.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record CreateAlbumRequest(
|
||||
@@ -34,7 +34,7 @@ public sealed class CreateAlbumRequestValidator : Validator<CreateAlbumRequest>
|
||||
|
||||
[PublicAPI]
|
||||
public class CreateAlbumHandler(
|
||||
ContentDbContext context)
|
||||
ContentsDbContext context)
|
||||
: Endpoint<CreateAlbumRequest, CreateAlbumResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -1,10 +1,6 @@
|
||||
using FastEndpoints;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Modules.Contents.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Contents.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record GetAlbumRequest(
|
||||
@@ -39,7 +35,7 @@ public sealed class GetAlbumRequestValidator : Validator<GetAlbumRequest>
|
||||
|
||||
[PublicAPI]
|
||||
public class GetAlbumHandler(
|
||||
ContentDbContext context)
|
||||
ContentsDbContext context)
|
||||
: Endpoint<GetAlbumRequest, GetAlbumResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -1,10 +1,7 @@
|
||||
using FastEndpoints;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Contents.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Contents.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record RemoveAlbumRequest(
|
||||
@@ -23,7 +20,7 @@ public sealed class RemoveAlbumRequestValidator : Validator<RemoveAlbumRequest>
|
||||
|
||||
[PublicAPI]
|
||||
public class RemoveAlbumHandler(
|
||||
ContentDbContext context)
|
||||
ContentsDbContext context)
|
||||
: Endpoint<RemoveAlbumRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -66,4 +63,4 @@ public class RemoveAlbumHandler(
|
||||
|
||||
await SendNoContentAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
using FastEndpoints;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Contents.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Contents.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record RemovePhotoFromAlbumRequest(
|
||||
@@ -28,7 +25,7 @@ public sealed class RemovePhotoFromAlbumRequestValidator : Validator<RemovePhoto
|
||||
|
||||
[PublicAPI]
|
||||
public class RemovePhotoFromAlbumHandler(
|
||||
ContentDbContext context)
|
||||
ContentsDbContext context)
|
||||
: Endpoint<RemovePhotoFromAlbumRequest>
|
||||
{
|
||||
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
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Data.Migrations
|
||||
namespace Hutopy.Modules.Contents.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAlbumAndPhotos : Migration
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "Content");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Albums",
|
||||
schema: "Content",
|
||||
columns: table => new
|
||||
{
|
||||
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),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||
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),
|
||||
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)
|
||||
DeletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -37,15 +38,16 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
||||
columns: table => new
|
||||
{
|
||||
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),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||
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),
|
||||
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)
|
||||
DeletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
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]
|
||||
public class ContentModel
|
||||
@@ -15,5 +15,4 @@ public class ContentModel
|
||||
public string HtmlFileUrl { get; init; } = "";
|
||||
public required string[]? Urls { 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]
|
||||
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;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Data;
|
||||
namespace Hutopy.Modules.Creators.Data;
|
||||
|
||||
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 JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Creators.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangeEmailRequest(
|
||||
@@ -28,7 +25,7 @@ public sealed class ChangeEmailRequestValidator : Validator<ChangeEmailRequest>
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeEmailHandler(
|
||||
ContentDbContext context)
|
||||
CreatorsDbContext context)
|
||||
: Endpoint<ChangeEmailRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -67,4 +64,4 @@ public class ChangeEmailHandler(
|
||||
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Hutopy.Web.Common.BlobStorage;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Creators.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangeLogoRequest(
|
||||
@@ -29,8 +29,8 @@ public sealed class ChangeLogoRequestValidator : Validator<ChangeLogoRequest>
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeLogoHandler(
|
||||
ContentDbContext context,
|
||||
AzureBlobStorage blobStorage)
|
||||
CreatorsDbContext context,
|
||||
IBlobStorage blobStorage)
|
||||
: Endpoint<ChangeLogoRequest>
|
||||
{
|
||||
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]
|
||||
public record ChangeNameRequest(
|
||||
@@ -21,7 +21,7 @@ internal sealed class ChangeNameRequestValidator
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeNameHandler(
|
||||
ContentDbContext context)
|
||||
CreatorsDbContext context)
|
||||
: Endpoint<ChangeNameRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -1,10 +1,7 @@
|
||||
using FluentValidation;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Creators.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangePhoneNumberRequest(
|
||||
@@ -28,7 +25,7 @@ public sealed class ChangePhoneNumberRequestValidator : Validator<ChangePhoneNum
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangePhoneNumberHandler(
|
||||
ContentDbContext context)
|
||||
CreatorsDbContext context)
|
||||
: Endpoint<ChangePhoneNumberRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -67,4 +64,4 @@ public class ChangePhoneNumberHandler(
|
||||
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Hutopy.Web.Common.YouTube;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Infrastructure.YouTube;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Creators.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangePresentationInfosRequest(
|
||||
@@ -32,7 +32,7 @@ public sealed class ChangePresentationInfosRequestValidator : Validator<ChangePr
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangePresentationInfosHandler(
|
||||
ContentDbContext context)
|
||||
CreatorsDbContext context)
|
||||
: Endpoint<ChangePresentationInfosRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -1,7 +1,7 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Creators.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangeSlugRequest(
|
||||
@@ -26,7 +26,7 @@ internal sealed class ChangeSlugRequestValidator
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeSlugHandler(
|
||||
ContentDbContext context)
|
||||
CreatorsDbContext context)
|
||||
: Endpoint<ChangeSlugRequest>
|
||||
{
|
||||
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]
|
||||
public record ChangeSocialsRequest(
|
||||
@@ -16,7 +16,7 @@ public record ChangeSocialsRequest(
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeSocialsHandler(
|
||||
ContentDbContext context)
|
||||
CreatorsDbContext context)
|
||||
: Endpoint<ChangeSocialsRequest>
|
||||
{
|
||||
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]
|
||||
public record ChangeTitleRequest(
|
||||
@@ -9,7 +9,7 @@ public record ChangeTitleRequest(
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeTitleHandler(
|
||||
ContentDbContext context)
|
||||
CreatorsDbContext context)
|
||||
: Endpoint<ChangeTitleRequest>
|
||||
{
|
||||
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.Web.Features.Contents.Data;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Creators.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record CreateCreatorRequest(
|
||||
@@ -27,7 +27,7 @@ public sealed class CreateCreatorRequestValidator : Validator<CreateCreatorReque
|
||||
|
||||
[PublicAPI]
|
||||
public sealed class CreateCreatorHandler(
|
||||
ContentDbContext context)
|
||||
CreatorsDbContext context)
|
||||
: Endpoint<CreateCreatorRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -74,7 +74,7 @@ public sealed class CreateCreatorHandler(
|
||||
|
||||
await SendOkAsync(ct);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
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]
|
||||
public sealed class GetCreatorByIdRequest
|
||||
@@ -22,7 +22,7 @@ public sealed class GetCreatorByIdRequestValidator
|
||||
|
||||
[PublicAPI]
|
||||
public class GetCreatorByIdHandler(
|
||||
ContentDbContext context)
|
||||
CreatorsDbContext context)
|
||||
: Endpoint<GetCreatorByIdRequest, Creator>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -1,7 +1,7 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Creators.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public sealed class GetCreatorBySlugRequest
|
||||
@@ -22,11 +22,11 @@ public record GetCreatorBySlugResponse
|
||||
public bool AcceptDonation { get; init; }
|
||||
public string? BannerUrl { get; init; }
|
||||
public string? PortraitUrl { get; init; }
|
||||
public string Slug { get; init; }
|
||||
public string Name { get; init; }
|
||||
public required string Slug { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public string? Title { get; init; }
|
||||
public Socials Socials { get; init; }
|
||||
public Presentation Presentation { get; init; }
|
||||
public Socials? Socials { get; init; }
|
||||
public Presentation? Presentation { get; init; }
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
@@ -43,7 +43,7 @@ public sealed class GetCreatorBySlugRequestValidator
|
||||
|
||||
[PublicAPI]
|
||||
public class GetCreatorBySlugHandler(
|
||||
ContentDbContext context)
|
||||
CreatorsDbContext context)
|
||||
: Endpoint<GetCreatorBySlugRequest, GetCreatorBySlugResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -73,12 +73,12 @@ public class GetCreatorBySlugHandler(
|
||||
DeletedAt = c.DeletedAt,
|
||||
IsDeleted = c.IsDeleted,
|
||||
Verified = c.Verified,
|
||||
AcceptDonation = c.AcceptDonation,
|
||||
BannerUrl = c.BannerUrl,
|
||||
PortraitUrl = c.PortraitUrl,
|
||||
Slug = c.Slug,
|
||||
Name = c.Name,
|
||||
Title = c.Title,
|
||||
AcceptDonation = c.IsStripeChargesEnabled && c.IsStripePayoutReady,
|
||||
Socials = c.Socials,
|
||||
Presentation = c.Presentation
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Creators.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public sealed class GetCreatorProfileResponse
|
||||
@@ -16,26 +16,29 @@ public sealed class GetCreatorProfileResponse
|
||||
public required string Slug { get; set; }
|
||||
public string? Title { 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 Socials Socials { get; set; }
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public class GetCreatorProfileHandler(
|
||||
ContentDbContext context)
|
||||
CreatorsDbContext context)
|
||||
: EndpointWithoutRequest<GetCreatorProfileResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/creators/profile");
|
||||
Options((o => o.WithTags("Creators")));
|
||||
Options(o => o.WithTags("Creators"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
CancellationToken ct)
|
||||
{
|
||||
var creator = await context
|
||||
GetCreatorProfileResponse? creator = await context
|
||||
.Creators
|
||||
.IgnoreQueryFilters()
|
||||
.Where(c => c.Id == HttpContext.User.GetUserId())
|
||||
@@ -52,13 +55,22 @@ public class GetCreatorProfileHandler(
|
||||
Slug = c.Slug,
|
||||
Title = c.Title,
|
||||
Verified = c.Verified,
|
||||
AcceptDonation = c.AcceptDonation,
|
||||
IsStripeAccountPresent = !string.IsNullOrWhiteSpace(c.StripeAccountId),
|
||||
IsStripeDetailsSubmitted = c.IsStripeDetailsSubmitted,
|
||||
IsStripeChargesEnabled = c.IsStripeChargesEnabled,
|
||||
IsStripePayoutReady = c.IsStripePayoutReady,
|
||||
Presentation = c.Presentation,
|
||||
Socials = c.Socials,
|
||||
Socials = c.Socials
|
||||
})
|
||||
.SingleOrDefaultAsync(ct);
|
||||
|
||||
if (creator is null) await SendNotFoundAsync(ct);
|
||||
else await SendAsync(creator, cancellation: ct);
|
||||
if (creator is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendAsync(creator, cancellation: ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Creators.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record RemoveCreatorRequest(
|
||||
@@ -21,7 +21,7 @@ public sealed class RemoveCreatorRequestValidator : Validator<RemoveCreatorReque
|
||||
|
||||
[PublicAPI]
|
||||
public sealed class RemoveCreatorHandler(
|
||||
ContentDbContext context)
|
||||
CreatorsDbContext context)
|
||||
: Endpoint<RemoveCreatorRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Configuration;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
using Hutopy.Modules.Creators.Services;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Creators.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record ReserveSlugRequest
|
||||
@@ -28,15 +30,15 @@ public sealed class ReserveSlugRequestValidator : Validator<ReserveSlugRequest>
|
||||
|
||||
[PublicAPI]
|
||||
public sealed class ReserveSlug(
|
||||
ContentDbContext context,
|
||||
IOptions<ContentOptions> opts,
|
||||
CreatorsDbContext context,
|
||||
IOptions<CreatorOptions> opts,
|
||||
SlugPurger slugPurger)
|
||||
: Endpoint<ReserveSlugRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/creators/@{Slug}/reserve");
|
||||
Options(o => o.WithTags("Contents"));
|
||||
Options(o => o.WithTags("Creators"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(
|
||||
@@ -1,7 +1,7 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||
namespace Hutopy.Modules.Creators.Features;
|
||||
|
||||
[PublicAPI]
|
||||
public record RestoreCreatorRequest(
|
||||
@@ -21,7 +21,7 @@ public sealed class RestoreCreatorRequestValidator : Validator<RestoreCreatorReq
|
||||
|
||||
[PublicAPI]
|
||||
public sealed class RestoreCreatorHandler(
|
||||
ContentDbContext context)
|
||||
CreatorsDbContext context)
|
||||
: Endpoint<RestoreCreatorRequest>
|
||||
{
|
||||
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 />
|
||||
using System;
|
||||
using Hutopy.Web.Features.Contents.Data;
|
||||
using Hutopy.Modules.Creators.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
@@ -9,75 +9,24 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Data.Migrations
|
||||
namespace Hutopy.Modules.Creators.Migrations
|
||||
{
|
||||
[DbContext(typeof(ContentDbContext))]
|
||||
[Migration("20250423153323_AddPresentation")]
|
||||
partial class AddPresentation
|
||||
[DbContext(typeof(CreatorsDbContext))]
|
||||
[Migration("20250609203815_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("Content")
|
||||
.HasDefaultSchema("Creators")
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", 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 =>
|
||||
modelBuilder.Entity("Hutopy.Modules.Creators.Data.Creator", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -107,10 +56,19 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
||||
.HasColumnType("boolean")
|
||||
.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")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PortraitUrl")
|
||||
.HasMaxLength(2048)
|
||||
@@ -121,19 +79,23 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("StripeAccountId")
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("character varying(21)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("Verified")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
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")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -155,7 +117,7 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasComputedColumnSql("LOWER( \"Content\".\"Slugs\".\"Name\")", true);
|
||||
.HasComputedColumnSql("LOWER(\"Name\")", true);
|
||||
|
||||
b.Property<DateTimeOffset>("ReservedUntil")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
@@ -168,69 +130,27 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
||||
b.HasIndex("NormalizedName")
|
||||
.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")
|
||||
.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 =>
|
||||
b.OwnsOne("Hutopy.Modules.Creators.Data.Presentation", "Presentation", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("CreatorId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<string>("Email")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.Property<string>("PhoneNumber")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.Property<string>("VideoUrl")
|
||||
.HasMaxLength(2048)
|
||||
@@ -238,13 +158,13 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
||||
|
||||
b1.HasKey("CreatorId");
|
||||
|
||||
b1.ToTable("Presentation", "Content");
|
||||
b1.ToTable("Presentation", "Creators");
|
||||
|
||||
b1.WithOwner()
|
||||
.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")
|
||||
.HasColumnType("uuid");
|
||||
@@ -283,7 +203,7 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
||||
|
||||
b1.HasKey("CreatorId");
|
||||
|
||||
b1.ToTable("Socials", "Content");
|
||||
b1.ToTable("Socials", "Creators");
|
||||
|
||||
b1.WithOwner()
|
||||
.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
|
||||
|
||||
namespace Hutopy.Web.Features.Contents.Data.Migrations
|
||||
namespace Hutopy.Modules.Creators.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Adds_AcceptDonation_Creator : Migration
|
||||
public partial class AddStripe : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AcceptDonation",
|
||||
schema: "Content",
|
||||
schema: "Creators",
|
||||
table: "Creators");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "IsStripeOnboardingComplete",
|
||||
schema: "Creators",
|
||||
table: "Creators",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
newName: "IsStripeDetailsSubmitted");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "IsStripeDetailsSubmitted",
|
||||
schema: "Creators",
|
||||
table: "Creators",
|
||||
newName: "IsStripeOnboardingComplete");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AcceptDonation",
|
||||
schema: "Content",
|
||||
table: "Creators");
|
||||
schema: "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 DateTimeOffset _lastPurgeTime = DateTimeOffset.MinValue;
|
||||
private static readonly TimeSpan _minTimeBetweenPurges = TimeSpan.FromSeconds(10);
|
||||
private static readonly SemaphoreSlim Semaphore = new(1, 1);
|
||||
private static DateTimeOffset s_lastPurgeTime = DateTimeOffset.MinValue;
|
||||
private static readonly TimeSpan MinTimeBetweenPurges = TimeSpan.FromSeconds(10);
|
||||
|
||||
public async Task PurgeExpiredSlugsAsync(CancellationToken ct)
|
||||
{
|
||||
// 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
|
||||
return;
|
||||
@@ -18,7 +20,7 @@ public class SlugPurger(ContentDbContext context)
|
||||
try
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (now - _lastPurgeTime < _minTimeBetweenPurges)
|
||||
if (now - s_lastPurgeTime < MinTimeBetweenPurges)
|
||||
{
|
||||
// Not enough time has passed since the last purge
|
||||
return;
|
||||
@@ -31,11 +33,11 @@ public class SlugPurger(ContentDbContext context)
|
||||
.ExecuteDeleteAsync(ct);
|
||||
|
||||
// Update the last purge time regardless of whether we found expired slugs or not
|
||||
_lastPurgeTime = now;
|
||||
s_lastPurgeTime = now;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
Semaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hutopy.Web.Features.Users;
|
||||
namespace Hutopy.Modules.Identity.Configuration;
|
||||
|
||||
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
|
||||
{
|
||||
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;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Data
|
||||
namespace Hutopy.Modules.Identity.Data
|
||||
{
|
||||
public class ApplicationDbContext(
|
||||
DbContextOptions<ApplicationDbContext> options)
|
||||
: IdentityDbContext<IdentityUser, IdentityRole, Guid>(options)
|
||||
public class IdentityDbContext(
|
||||
DbContextOptions<IdentityDbContext> options)
|
||||
: IdentityDbContext<User, Role, Guid>(options)
|
||||
{
|
||||
public const string SchemaName = "Identity";
|
||||
|
||||
protected override void OnModelCreating(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.Extensions.Options;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Data;
|
||||
namespace Hutopy.Modules.Identity.Data;
|
||||
|
||||
public sealed class IdentityUserManager(
|
||||
IUserStore<IdentityUser> store,
|
||||
public sealed class UserManager(
|
||||
IUserStore<User> store,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IPasswordHasher<IdentityUser> passwordHasher,
|
||||
IEnumerable<IUserValidator<IdentityUser>> userValidators,
|
||||
IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators,
|
||||
IPasswordHasher<User> passwordHasher,
|
||||
IEnumerable<IUserValidator<User>> userValidators,
|
||||
IEnumerable<IPasswordValidator<User>> passwordValidators,
|
||||
ILookupNormalizer keyNormalizer,
|
||||
IdentityErrorDescriber errors,
|
||||
IServiceProvider services,
|
||||
ILogger<UserManager<IdentityUser>> logger)
|
||||
: UserManager<IdentityUser>(
|
||||
ILogger<UserManager<User>> logger)
|
||||
: UserManager<User>(
|
||||
store,
|
||||
optionsAccessor,
|
||||
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.Web.Features.Users.Data;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangeAddressRequest(
|
||||
@@ -9,7 +9,7 @@ public record ChangeAddressRequest(
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeAddressHandler(
|
||||
IdentityUserManager userManager)
|
||||
UserManager userManager)
|
||||
: Endpoint<ChangeAddressRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -1,7 +1,7 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Features.Users.Data;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangeAliasRequest(
|
||||
@@ -9,7 +9,7 @@ public record ChangeAliasRequest(
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeAliasHandler(
|
||||
IdentityUserManager userManager)
|
||||
UserManager userManager)
|
||||
: Endpoint<ChangeAliasRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -1,7 +1,7 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Features.Users.Data;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangeBirthDateRequest(
|
||||
@@ -9,7 +9,7 @@ public record ChangeBirthDateRequest(
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeBirthDateHandler(
|
||||
IdentityUserManager userManager)
|
||||
UserManager userManager)
|
||||
: Endpoint<ChangeBirthDateRequest>
|
||||
{
|
||||
public override void Configure()
|
||||
@@ -1,7 +1,7 @@
|
||||
using Hutopy.Web.Common.Security;
|
||||
using Hutopy.Web.Features.Users.Data;
|
||||
using Hutopy.Infrastructure.Security;
|
||||
using Hutopy.Modules.Identity.Data;
|
||||
|
||||
namespace Hutopy.Web.Features.Users.Handlers;
|
||||
namespace Hutopy.Modules.Identity.Handlers;
|
||||
|
||||
[PublicAPI]
|
||||
public record ChangeEmailRequest(
|
||||
@@ -9,7 +9,7 @@ public record ChangeEmailRequest(
|
||||
|
||||
[PublicAPI]
|
||||
public class ChangeEmailHandler(
|
||||
IdentityUserManager userManager)
|
||||
UserManager userManager)
|
||||
: Endpoint<ChangeEmailRequest>
|
||||
{
|
||||
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