From 4631f82ee41ad463093460d2db24deb4a8e6f53a Mon Sep 17 00:00:00 2001 From: warrenchen Date: Tue, 3 Feb 2026 15:04:18 +0900 Subject: [PATCH] Add initial installer project and setup for MemberCenter - Created MemberCenter.Installer project with references to Infrastructure, Application, and Domain projects. - Added Program.cs with a basic console output. - Generated MemberCenterDbContextModelSnapshot for database schema representation. --- .config/dotnet-tools.json | 13 + .gitignore | 8 + Directory.Build.props | 6 + MemberCenter.sln | 55 ++ docs/openapi.yaml | 8 +- .../Contracts/AdminRequests.cs | 7 + .../Contracts/AuthRequests.cs | 13 + .../Contracts/NewsletterRequests.cs | 7 + .../AdminNewsletterListsController.cs | 112 +++ .../AdminOAuthClientsController.cs | 144 +++ .../Controllers/AdminTenantsController.cs | 112 +++ .../Controllers/AuthController.cs | 104 +++ .../Controllers/NewsletterController.cs | 211 +++++ .../Controllers/OAuthController.cs | 49 ++ .../Controllers/TokenController.cs | 74 ++ .../Controllers/UserController.cs | 37 + .../Extensions/ClaimsExtensions.cs | 31 + src/MemberCenter.Api/MemberCenter.Api.csproj | 24 + src/MemberCenter.Api/Program.cs | 93 ++ .../Properties/launchSettings.json | 38 + src/MemberCenter.Api/appsettings.json | 11 + .../MemberCenter.Application.csproj | 13 + .../Constants/SubscriptionStatus.cs | 8 + src/MemberCenter.Domain/Entities/AuditLog.cs | 11 + .../Entities/EmailVerification.cs | 12 + .../Entities/NewsletterList.cs | 13 + .../Entities/NewsletterSubscription.cs | 16 + .../Entities/SystemFlag.cs | 9 + src/MemberCenter.Domain/Entities/Tenant.cs | 12 + .../Entities/UnsubscribeToken.cs | 12 + .../MemberCenter.Domain.csproj | 9 + .../Identity/ApplicationRole.cs | 8 + .../Identity/ApplicationUser.cs | 8 + .../MemberCenter.Infrastructure.csproj | 25 + .../Persistence/MemberCenterDbContext.cs | 167 ++++ .../MemberCenterDbContextFactory.cs | 20 + .../20260203055037_InitialCreate.Designer.cs | 817 ++++++++++++++++++ .../20260203055037_InitialCreate.cs | 580 +++++++++++++ .../MemberCenterDbContextModelSnapshot.cs | 814 +++++++++++++++++ .../MemberCenter.Installer.csproj | 16 + src/MemberCenter.Installer/Program.cs | 2 + 41 files changed, 3722 insertions(+), 7 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 Directory.Build.props create mode 100644 MemberCenter.sln create mode 100644 src/MemberCenter.Api/Contracts/AdminRequests.cs create mode 100644 src/MemberCenter.Api/Contracts/AuthRequests.cs create mode 100644 src/MemberCenter.Api/Contracts/NewsletterRequests.cs create mode 100644 src/MemberCenter.Api/Controllers/AdminNewsletterListsController.cs create mode 100644 src/MemberCenter.Api/Controllers/AdminOAuthClientsController.cs create mode 100644 src/MemberCenter.Api/Controllers/AdminTenantsController.cs create mode 100644 src/MemberCenter.Api/Controllers/AuthController.cs create mode 100644 src/MemberCenter.Api/Controllers/NewsletterController.cs create mode 100644 src/MemberCenter.Api/Controllers/OAuthController.cs create mode 100644 src/MemberCenter.Api/Controllers/TokenController.cs create mode 100644 src/MemberCenter.Api/Controllers/UserController.cs create mode 100644 src/MemberCenter.Api/Extensions/ClaimsExtensions.cs create mode 100644 src/MemberCenter.Api/MemberCenter.Api.csproj create mode 100644 src/MemberCenter.Api/Program.cs create mode 100644 src/MemberCenter.Api/Properties/launchSettings.json create mode 100644 src/MemberCenter.Api/appsettings.json create mode 100644 src/MemberCenter.Application/MemberCenter.Application.csproj create mode 100644 src/MemberCenter.Domain/Constants/SubscriptionStatus.cs create mode 100644 src/MemberCenter.Domain/Entities/AuditLog.cs create mode 100644 src/MemberCenter.Domain/Entities/EmailVerification.cs create mode 100644 src/MemberCenter.Domain/Entities/NewsletterList.cs create mode 100644 src/MemberCenter.Domain/Entities/NewsletterSubscription.cs create mode 100644 src/MemberCenter.Domain/Entities/SystemFlag.cs create mode 100644 src/MemberCenter.Domain/Entities/Tenant.cs create mode 100644 src/MemberCenter.Domain/Entities/UnsubscribeToken.cs create mode 100644 src/MemberCenter.Domain/MemberCenter.Domain.csproj create mode 100644 src/MemberCenter.Infrastructure/Identity/ApplicationRole.cs create mode 100644 src/MemberCenter.Infrastructure/Identity/ApplicationUser.cs create mode 100644 src/MemberCenter.Infrastructure/MemberCenter.Infrastructure.csproj create mode 100644 src/MemberCenter.Infrastructure/Persistence/MemberCenterDbContext.cs create mode 100644 src/MemberCenter.Infrastructure/Persistence/MemberCenterDbContextFactory.cs create mode 100644 src/MemberCenter.Infrastructure/Persistence/Migrations/20260203055037_InitialCreate.Designer.cs create mode 100644 src/MemberCenter.Infrastructure/Persistence/Migrations/20260203055037_InitialCreate.cs create mode 100644 src/MemberCenter.Infrastructure/Persistence/Migrations/MemberCenterDbContextModelSnapshot.cs create mode 100644 src/MemberCenter.Installer/MemberCenter.Installer.csproj create mode 100644 src/MemberCenter.Installer/Program.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..3dd2352 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "8.0.11", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ca73059..f276d92 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,8 @@ artifacts/ # NuGet *.nupkg *.snupkg +# Local NuGet/global package caches in repo +.nuget/ # The packages folder can be ignored because of PackageReference # Uncomment if using packages.config #packages/ @@ -63,6 +65,9 @@ ScaffoldingReadMe.txt secrets.json appsettings.*.json !appsettings.json +.env +.env.* +!.env.example # OS .DS_Store @@ -71,3 +76,6 @@ Thumbs.db # Others *.swp *.tmp +*.pid +*.pid.lock +.dotnet/ diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..55672f0 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,6 @@ + + + false + false + + diff --git a/MemberCenter.sln b/MemberCenter.sln new file mode 100644 index 0000000..0052256 --- /dev/null +++ b/MemberCenter.sln @@ -0,0 +1,55 @@ + +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", "{150D3A20-BF61-4012-BD40-05D408749112}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MemberCenter.Domain", "src\MemberCenter.Domain\MemberCenter.Domain.csproj", "{7733733D-22EB-431D-A8AA-833486C3E0E2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MemberCenter.Application", "src\MemberCenter.Application\MemberCenter.Application.csproj", "{90EC27FD-E72D-4506-A81A-BD81F4D555CF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MemberCenter.Infrastructure", "src\MemberCenter.Infrastructure\MemberCenter.Infrastructure.csproj", "{28015B2B-16F2-4DA0-9DA6-D79C94330A4D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MemberCenter.Api", "src\MemberCenter.Api\MemberCenter.Api.csproj", "{051ECE48-E49B-4E42-BE08-6E9AAB7262BC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MemberCenter.Installer", "src\MemberCenter.Installer\MemberCenter.Installer.csproj", "{5FAA2380-3354-4FC8-BDFE-2E31E8AD9EE2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7733733D-22EB-431D-A8AA-833486C3E0E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7733733D-22EB-431D-A8AA-833486C3E0E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7733733D-22EB-431D-A8AA-833486C3E0E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7733733D-22EB-431D-A8AA-833486C3E0E2}.Release|Any CPU.Build.0 = Release|Any CPU + {90EC27FD-E72D-4506-A81A-BD81F4D555CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90EC27FD-E72D-4506-A81A-BD81F4D555CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90EC27FD-E72D-4506-A81A-BD81F4D555CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90EC27FD-E72D-4506-A81A-BD81F4D555CF}.Release|Any CPU.Build.0 = Release|Any CPU + {28015B2B-16F2-4DA0-9DA6-D79C94330A4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28015B2B-16F2-4DA0-9DA6-D79C94330A4D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28015B2B-16F2-4DA0-9DA6-D79C94330A4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28015B2B-16F2-4DA0-9DA6-D79C94330A4D}.Release|Any CPU.Build.0 = Release|Any CPU + {051ECE48-E49B-4E42-BE08-6E9AAB7262BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {051ECE48-E49B-4E42-BE08-6E9AAB7262BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {051ECE48-E49B-4E42-BE08-6E9AAB7262BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {051ECE48-E49B-4E42-BE08-6E9AAB7262BC}.Release|Any CPU.Build.0 = Release|Any CPU + {5FAA2380-3354-4FC8-BDFE-2E31E8AD9EE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FAA2380-3354-4FC8-BDFE-2E31E8AD9EE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FAA2380-3354-4FC8-BDFE-2E31E8AD9EE2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FAA2380-3354-4FC8-BDFE-2E31E8AD9EE2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {7733733D-22EB-431D-A8AA-833486C3E0E2} = {150D3A20-BF61-4012-BD40-05D408749112} + {90EC27FD-E72D-4506-A81A-BD81F4D555CF} = {150D3A20-BF61-4012-BD40-05D408749112} + {28015B2B-16F2-4DA0-9DA6-D79C94330A4D} = {150D3A20-BF61-4012-BD40-05D408749112} + {051ECE48-E49B-4E42-BE08-6E9AAB7262BC} = {150D3A20-BF61-4012-BD40-05D408749112} + {5FAA2380-3354-4FC8-BDFE-2E31E8AD9EE2} = {150D3A20-BF61-4012-BD40-05D408749112} + EndGlobalSection +EndGlobal diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 64b51a1..da17300 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -266,10 +266,6 @@ paths: name: subscription_id required: false schema: { type: string } - - in: query - name: tenant_id - required: false - schema: { type: string } - in: query name: email required: false @@ -558,9 +554,8 @@ components: SubscribeRequest: type: object - required: [tenant_id, list_id, email] + required: [list_id, email] properties: - tenant_id: { type: string } list_id: { type: string } email: { type: string, format: email } preferences: { type: object } @@ -570,7 +565,6 @@ components: type: object properties: id: { type: string } - tenant_id: { type: string } list_id: { type: string } email: { type: string, format: email } status: { type: string, enum: [pending, active, unsubscribed] } diff --git a/src/MemberCenter.Api/Contracts/AdminRequests.cs b/src/MemberCenter.Api/Contracts/AdminRequests.cs new file mode 100644 index 0000000..29e54ed --- /dev/null +++ b/src/MemberCenter.Api/Contracts/AdminRequests.cs @@ -0,0 +1,7 @@ +namespace MemberCenter.Api.Contracts; + +public sealed record TenantRequest(string Name, List Domains, string Status); + +public sealed record NewsletterListRequest(Guid TenantId, string Name, string Status); + +public sealed record OAuthClientRequest(Guid TenantId, string Name, List RedirectUris, string ClientType); diff --git a/src/MemberCenter.Api/Contracts/AuthRequests.cs b/src/MemberCenter.Api/Contracts/AuthRequests.cs new file mode 100644 index 0000000..5daa459 --- /dev/null +++ b/src/MemberCenter.Api/Contracts/AuthRequests.cs @@ -0,0 +1,13 @@ +namespace MemberCenter.Api.Contracts; + +public sealed record RegisterRequest(string Email, string Password); + +public sealed record LoginRequest(string Email, string Password, string? Scope); + +public sealed record RefreshRequest(string RefreshToken); + +public sealed record ForgotPasswordRequest(string Email); + +public sealed record ResetPasswordRequest(string Email, string Token, string NewPassword); + +public sealed record LogoutRequest(string RefreshToken); diff --git a/src/MemberCenter.Api/Contracts/NewsletterRequests.cs b/src/MemberCenter.Api/Contracts/NewsletterRequests.cs new file mode 100644 index 0000000..63687b8 --- /dev/null +++ b/src/MemberCenter.Api/Contracts/NewsletterRequests.cs @@ -0,0 +1,7 @@ +namespace MemberCenter.Api.Contracts; + +public sealed record SubscribeRequest(Guid ListId, string Email, Dictionary? Preferences, string? Source); + +public sealed record UnsubscribeRequest(string Token); + +public sealed record UpdatePreferencesRequest(Guid SubscriptionId, Dictionary Preferences); diff --git a/src/MemberCenter.Api/Controllers/AdminNewsletterListsController.cs b/src/MemberCenter.Api/Controllers/AdminNewsletterListsController.cs new file mode 100644 index 0000000..86776c8 --- /dev/null +++ b/src/MemberCenter.Api/Controllers/AdminNewsletterListsController.cs @@ -0,0 +1,112 @@ +using MemberCenter.Api.Contracts; +using MemberCenter.Domain.Entities; +using MemberCenter.Infrastructure.Persistence; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace MemberCenter.Api.Controllers; + +[ApiController] +[Route("admin/newsletter-lists")] +[Authorize(Policy = "Admin")] +public class AdminNewsletterListsController : ControllerBase +{ + private readonly MemberCenterDbContext _dbContext; + + public AdminNewsletterListsController(MemberCenterDbContext dbContext) + { + _dbContext = dbContext; + } + + [HttpGet] + public async Task List() + { + var lists = await _dbContext.NewsletterLists.ToListAsync(); + return Ok(lists.Select(l => new + { + l.Id, + l.TenantId, + l.Name, + l.Status + })); + } + + [HttpPost] + public async Task Create([FromBody] NewsletterListRequest request) + { + var list = new NewsletterList + { + Id = Guid.NewGuid(), + TenantId = request.TenantId, + Name = request.Name, + Status = request.Status + }; + + _dbContext.NewsletterLists.Add(list); + await _dbContext.SaveChangesAsync(); + + return Created($"/admin/newsletter-lists/{list.Id}", new + { + list.Id, + list.TenantId, + list.Name, + list.Status + }); + } + + [HttpGet("{id:guid}")] + public async Task Get(Guid id) + { + var list = await _dbContext.NewsletterLists.FindAsync(id); + if (list is null) + { + return NotFound(); + } + + return Ok(new + { + list.Id, + list.TenantId, + list.Name, + list.Status + }); + } + + [HttpPut("{id:guid}")] + public async Task Update(Guid id, [FromBody] NewsletterListRequest request) + { + var list = await _dbContext.NewsletterLists.FindAsync(id); + if (list is null) + { + return NotFound(); + } + + list.TenantId = request.TenantId; + list.Name = request.Name; + list.Status = request.Status; + + await _dbContext.SaveChangesAsync(); + return Ok(new + { + list.Id, + list.TenantId, + list.Name, + list.Status + }); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var list = await _dbContext.NewsletterLists.FindAsync(id); + if (list is null) + { + return NotFound(); + } + + _dbContext.NewsletterLists.Remove(list); + await _dbContext.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/MemberCenter.Api/Controllers/AdminOAuthClientsController.cs b/src/MemberCenter.Api/Controllers/AdminOAuthClientsController.cs new file mode 100644 index 0000000..50e7653 --- /dev/null +++ b/src/MemberCenter.Api/Controllers/AdminOAuthClientsController.cs @@ -0,0 +1,144 @@ +using MemberCenter.Api.Contracts; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using OpenIddict.Abstractions; +using System.Text.Json; + +namespace MemberCenter.Api.Controllers; + +[ApiController] +[Route("admin/oauth-clients")] +[Authorize(Policy = "Admin")] +public class AdminOAuthClientsController : ControllerBase +{ + private readonly IOpenIddictApplicationManager _applicationManager; + + public AdminOAuthClientsController(IOpenIddictApplicationManager applicationManager) + { + _applicationManager = applicationManager; + } + + [HttpGet] + public async Task List() + { + var results = new List(); + await foreach (var application in _applicationManager.ListAsync()) + { + results.Add(new + { + id = await _applicationManager.GetIdAsync(application), + name = await _applicationManager.GetDisplayNameAsync(application), + client_id = await _applicationManager.GetClientIdAsync(application), + client_type = await _applicationManager.GetClientTypeAsync(application), + redirect_uris = await _applicationManager.GetRedirectUrisAsync(application), + properties = await _applicationManager.GetPropertiesAsync(application) + }); + } + + return Ok(results); + } + + [HttpPost] + public async Task Create([FromBody] OAuthClientRequest request) + { + var descriptor = new OpenIddictApplicationDescriptor + { + ClientId = Guid.NewGuid().ToString("N"), + DisplayName = request.Name, + ClientType = request.ClientType, + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, + OpenIddictConstants.Permissions.ResponseTypes.Code, + OpenIddictConstants.Permissions.Scopes.Email, + OpenIddictConstants.Permissions.Scopes.Profile, + "scp:openid" + } + }; + + foreach (var uri in request.RedirectUris) + { + descriptor.RedirectUris.Add(new Uri(uri)); + } + + descriptor.Properties["tenant_id"] = JsonSerializer.SerializeToElement(request.TenantId.ToString()); + + await _applicationManager.CreateAsync(descriptor); + + return Created("/admin/oauth-clients", new + { + descriptor.ClientId, + descriptor.DisplayName, + descriptor.ClientType, + redirect_uris = descriptor.RedirectUris.Select(u => u.ToString()) + }); + } + + [HttpGet("{id}")] + public async Task Get(string id) + { + var app = await _applicationManager.FindByIdAsync(id); + if (app is null) + { + return NotFound(); + } + + return Ok(new + { + id, + name = await _applicationManager.GetDisplayNameAsync(app), + client_id = await _applicationManager.GetClientIdAsync(app), + client_type = await _applicationManager.GetClientTypeAsync(app), + redirect_uris = await _applicationManager.GetRedirectUrisAsync(app), + properties = await _applicationManager.GetPropertiesAsync(app) + }); + } + + [HttpPut("{id}")] + public async Task Update(string id, [FromBody] OAuthClientRequest request) + { + var app = await _applicationManager.FindByIdAsync(id); + if (app is null) + { + return NotFound(); + } + + var descriptor = new OpenIddictApplicationDescriptor(); + await _applicationManager.PopulateAsync(descriptor, app); + + descriptor.DisplayName = request.Name; + descriptor.ClientType = request.ClientType; + descriptor.RedirectUris.Clear(); + foreach (var uri in request.RedirectUris) + { + descriptor.RedirectUris.Add(new Uri(uri)); + } + descriptor.Properties["tenant_id"] = JsonSerializer.SerializeToElement(request.TenantId.ToString()); + + await _applicationManager.UpdateAsync(app, descriptor); + + return Ok(new + { + id, + descriptor.DisplayName, + descriptor.ClientType, + redirect_uris = descriptor.RedirectUris.Select(u => u.ToString()) + }); + } + + [HttpDelete("{id}")] + public async Task Delete(string id) + { + var app = await _applicationManager.FindByIdAsync(id); + if (app is null) + { + return NotFound(); + } + + await _applicationManager.DeleteAsync(app); + return NoContent(); + } +} diff --git a/src/MemberCenter.Api/Controllers/AdminTenantsController.cs b/src/MemberCenter.Api/Controllers/AdminTenantsController.cs new file mode 100644 index 0000000..8d0b27d --- /dev/null +++ b/src/MemberCenter.Api/Controllers/AdminTenantsController.cs @@ -0,0 +1,112 @@ +using MemberCenter.Api.Contracts; +using MemberCenter.Domain.Entities; +using MemberCenter.Infrastructure.Persistence; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace MemberCenter.Api.Controllers; + +[ApiController] +[Route("admin/tenants")] +[Authorize(Policy = "Admin")] +public class AdminTenantsController : ControllerBase +{ + private readonly MemberCenterDbContext _dbContext; + + public AdminTenantsController(MemberCenterDbContext dbContext) + { + _dbContext = dbContext; + } + + [HttpGet] + public async Task List() + { + var tenants = await _dbContext.Tenants.ToListAsync(); + return Ok(tenants.Select(t => new + { + t.Id, + t.Name, + t.Domains, + t.Status + })); + } + + [HttpPost] + public async Task Create([FromBody] TenantRequest request) + { + var tenant = new Tenant + { + Id = Guid.NewGuid(), + Name = request.Name, + Domains = request.Domains, + Status = request.Status + }; + + _dbContext.Tenants.Add(tenant); + await _dbContext.SaveChangesAsync(); + + return Created($"/admin/tenants/{tenant.Id}", new + { + tenant.Id, + tenant.Name, + tenant.Domains, + tenant.Status + }); + } + + [HttpGet("{id:guid}")] + public async Task Get(Guid id) + { + var tenant = await _dbContext.Tenants.FindAsync(id); + if (tenant is null) + { + return NotFound(); + } + + return Ok(new + { + tenant.Id, + tenant.Name, + tenant.Domains, + tenant.Status + }); + } + + [HttpPut("{id:guid}")] + public async Task Update(Guid id, [FromBody] TenantRequest request) + { + var tenant = await _dbContext.Tenants.FindAsync(id); + if (tenant is null) + { + return NotFound(); + } + + tenant.Name = request.Name; + tenant.Domains = request.Domains; + tenant.Status = request.Status; + + await _dbContext.SaveChangesAsync(); + return Ok(new + { + tenant.Id, + tenant.Name, + tenant.Domains, + tenant.Status + }); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + var tenant = await _dbContext.Tenants.FindAsync(id); + if (tenant is null) + { + return NotFound(); + } + + _dbContext.Tenants.Remove(tenant); + await _dbContext.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/src/MemberCenter.Api/Controllers/AuthController.cs b/src/MemberCenter.Api/Controllers/AuthController.cs new file mode 100644 index 0000000..df6e9a4 --- /dev/null +++ b/src/MemberCenter.Api/Controllers/AuthController.cs @@ -0,0 +1,104 @@ +using MemberCenter.Api.Contracts; +using MemberCenter.Infrastructure.Identity; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace MemberCenter.Api.Controllers; + +[ApiController] +[Route("auth")] +public class AuthController : ControllerBase +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public AuthController(UserManager userManager, SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + [HttpPost("register")] + public async Task Register([FromBody] RegisterRequest request) + { + var user = new ApplicationUser + { + Id = Guid.NewGuid(), + UserName = request.Email, + Email = request.Email, + EmailConfirmed = false + }; + + var result = await _userManager.CreateAsync(user, request.Password); + if (!result.Succeeded) + { + return BadRequest(result.Errors.Select(e => e.Description)); + } + + return Ok(new + { + id = user.Id, + email = user.Email, + email_verified = user.EmailConfirmed, + created_at = user.CreatedAt + }); + } + + [HttpPost("password/forgot")] + public async Task ForgotPassword([FromBody] ForgotPasswordRequest request) + { + var user = await _userManager.FindByEmailAsync(request.Email); + if (user is null) + { + return NoContent(); + } + + var token = await _userManager.GeneratePasswordResetTokenAsync(user); + return Ok(new { token }); + } + + [HttpPost("password/reset")] + public async Task ResetPassword([FromBody] ResetPasswordRequest request) + { + var user = await _userManager.FindByEmailAsync(request.Email); + if (user is null) + { + return BadRequest("Invalid user."); + } + + var result = await _userManager.ResetPasswordAsync(user, request.Token, request.NewPassword); + if (!result.Succeeded) + { + return BadRequest(result.Errors.Select(e => e.Description)); + } + + return NoContent(); + } + + [HttpGet("email/verify")] + public async Task VerifyEmail([FromQuery] string token, [FromQuery] string email) + { + var user = await _userManager.FindByEmailAsync(email); + if (user is null) + { + return BadRequest("Invalid user."); + } + + var result = await _userManager.ConfirmEmailAsync(user, token); + if (!result.Succeeded) + { + return BadRequest(result.Errors.Select(e => e.Description)); + } + + return Ok(new { status = "verified" }); + } + + [Authorize] + [HttpPost("logout")] + public async Task Logout([FromBody] LogoutRequest request) + { + await _signInManager.SignOutAsync(); + return NoContent(); + } +} diff --git a/src/MemberCenter.Api/Controllers/NewsletterController.cs b/src/MemberCenter.Api/Controllers/NewsletterController.cs new file mode 100644 index 0000000..49070bd --- /dev/null +++ b/src/MemberCenter.Api/Controllers/NewsletterController.cs @@ -0,0 +1,211 @@ +using MemberCenter.Api.Contracts; +using MemberCenter.Domain.Constants; +using MemberCenter.Domain.Entities; +using MemberCenter.Infrastructure.Persistence; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; + +namespace MemberCenter.Api.Controllers; + +[ApiController] +[Route("newsletter")] +public class NewsletterController : ControllerBase +{ + private readonly MemberCenterDbContext _dbContext; + + public NewsletterController(MemberCenterDbContext dbContext) + { + _dbContext = dbContext; + } + + [HttpPost("subscribe")] + public async Task Subscribe([FromBody] SubscribeRequest request) + { + var list = await _dbContext.NewsletterLists.FirstOrDefaultAsync(l => l.Id == request.ListId); + if (list is null) + { + return NotFound("List not found."); + } + + var subscription = await _dbContext.NewsletterSubscriptions + .FirstOrDefaultAsync(s => s.ListId == request.ListId && s.Email == request.Email); + + if (subscription is null) + { + subscription = new NewsletterSubscription + { + Id = Guid.NewGuid(), + ListId = request.ListId, + Email = request.Email, + Status = SubscriptionStatus.Pending, + Preferences = ToJsonDocument(request.Preferences) + }; + _dbContext.NewsletterSubscriptions.Add(subscription); + } + else + { + subscription.Status = SubscriptionStatus.Pending; + subscription.Preferences = ToJsonDocument(request.Preferences); + } + + var confirmToken = CreateToken(); + _dbContext.UnsubscribeTokens.Add(new UnsubscribeToken + { + Id = Guid.NewGuid(), + SubscriptionId = subscription.Id, + TokenHash = HashToken(confirmToken), + ExpiresAt = DateTimeOffset.UtcNow.AddDays(7) + }); + + await _dbContext.SaveChangesAsync(); + + return Ok(new + { + id = subscription.Id, + list_id = subscription.ListId, + email = subscription.Email, + status = subscription.Status, + created_at = subscription.CreatedAt, + confirm_token = confirmToken + }); + } + + [HttpGet("confirm")] + public async Task Confirm([FromQuery] string token) + { + var tokenHash = HashToken(token); + var confirmToken = await _dbContext.UnsubscribeTokens + .Include(t => t.Subscription) + .FirstOrDefaultAsync(t => t.TokenHash == tokenHash && t.ConsumedAt == null); + + if (confirmToken?.Subscription is null) + { + return NotFound("Invalid token."); + } + + if (confirmToken.ExpiresAt < DateTimeOffset.UtcNow) + { + return BadRequest("Token expired."); + } + + confirmToken.Subscription.Status = SubscriptionStatus.Active; + confirmToken.ConsumedAt = DateTimeOffset.UtcNow; + await _dbContext.SaveChangesAsync(); + + return Ok(new + { + id = confirmToken.Subscription.Id, + list_id = confirmToken.Subscription.ListId, + email = confirmToken.Subscription.Email, + status = confirmToken.Subscription.Status, + created_at = confirmToken.Subscription.CreatedAt + }); + } + + [HttpPost("unsubscribe")] + public async Task Unsubscribe([FromBody] UnsubscribeRequest request) + { + var tokenHash = HashToken(request.Token); + var unsubscribeToken = await _dbContext.UnsubscribeTokens + .Include(t => t.Subscription) + .FirstOrDefaultAsync(t => t.TokenHash == tokenHash && t.ConsumedAt == null); + + if (unsubscribeToken?.Subscription is null) + { + return NotFound("Invalid token."); + } + + unsubscribeToken.Subscription.Status = SubscriptionStatus.Unsubscribed; + unsubscribeToken.ConsumedAt = DateTimeOffset.UtcNow; + await _dbContext.SaveChangesAsync(); + + return Ok(new + { + id = unsubscribeToken.Subscription.Id, + list_id = unsubscribeToken.Subscription.ListId, + email = unsubscribeToken.Subscription.Email, + status = unsubscribeToken.Subscription.Status, + created_at = unsubscribeToken.Subscription.CreatedAt + }); + } + + [HttpGet("preferences")] + public async Task Preferences([FromQuery] Guid? subscriptionId, [FromQuery] string? email) + { + NewsletterSubscription? subscription = null; + + if (subscriptionId.HasValue) + { + subscription = await _dbContext.NewsletterSubscriptions.FindAsync(subscriptionId.Value); + } + else if (!string.IsNullOrWhiteSpace(email)) + { + subscription = await _dbContext.NewsletterSubscriptions + .Where(s => s.Email == email) + .OrderByDescending(s => s.CreatedAt) + .FirstOrDefaultAsync(); + } + + if (subscription is null) + { + return NotFound("Subscription not found."); + } + + return Ok(new + { + id = subscription.Id, + list_id = subscription.ListId, + email = subscription.Email, + status = subscription.Status, + preferences = subscription.Preferences.RootElement + }); + } + + [HttpPost("preferences")] + public async Task UpdatePreferences([FromBody] UpdatePreferencesRequest request) + { + var subscription = await _dbContext.NewsletterSubscriptions.FindAsync(request.SubscriptionId); + if (subscription is null) + { + return NotFound("Subscription not found."); + } + + subscription.Preferences = ToJsonDocument(request.Preferences); + await _dbContext.SaveChangesAsync(); + + return Ok(new + { + id = subscription.Id, + list_id = subscription.ListId, + email = subscription.Email, + status = subscription.Status, + preferences = subscription.Preferences.RootElement + }); + } + + private static string CreateToken() + { + var bytes = RandomNumberGenerator.GetBytes(32); + return Convert.ToBase64String(bytes); + } + + private static string HashToken(string token) + { + using var sha = SHA256.Create(); + var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(token)); + return Convert.ToHexString(bytes).ToLowerInvariant(); + } + + private static JsonDocument ToJsonDocument(Dictionary? value) + { + if (value is null) + { + return JsonDocument.Parse("{}"); + } + + return JsonDocument.Parse(JsonSerializer.Serialize(value)); + } +} diff --git a/src/MemberCenter.Api/Controllers/OAuthController.cs b/src/MemberCenter.Api/Controllers/OAuthController.cs new file mode 100644 index 0000000..a182a2d --- /dev/null +++ b/src/MemberCenter.Api/Controllers/OAuthController.cs @@ -0,0 +1,49 @@ +using MemberCenter.Api.Extensions; +using MemberCenter.Infrastructure.Identity; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; +using System.Security.Claims; + +namespace MemberCenter.Api.Controllers; + +[ApiController] +public class OAuthController : ControllerBase +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public OAuthController(UserManager userManager, SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + [HttpGet("/oauth/authorize")] + [Authorize] + public async Task Authorize() + { + var request = HttpContext.Features.Get()?.Transaction?.Request; + if (request is null) + { + return BadRequest("Invalid OpenIddict request."); + } + + var user = await _userManager.GetUserAsync(User); + if (user is null) + { + return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + var principal = await _signInManager.CreateUserPrincipalAsync(user); + principal.SetScopes(request.GetScopes()); + foreach (var claim in principal.Claims) + { + claim.SetDestinations(ClaimsExtensions.GetDestinations(claim)); + } + + return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } +} diff --git a/src/MemberCenter.Api/Controllers/TokenController.cs b/src/MemberCenter.Api/Controllers/TokenController.cs new file mode 100644 index 0000000..717118b --- /dev/null +++ b/src/MemberCenter.Api/Controllers/TokenController.cs @@ -0,0 +1,74 @@ +using MemberCenter.Api.Extensions; +using MemberCenter.Infrastructure.Identity; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; + +namespace MemberCenter.Api.Controllers; + +[ApiController] +public class TokenController : ControllerBase +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public TokenController(UserManager userManager, SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + [HttpPost("/oauth/token")] + [HttpPost("/auth/login")] + [HttpPost("/auth/refresh")] + public async Task Exchange() + { + var request = HttpContext.Features.Get()?.Transaction?.Request; + if (request is null) + { + return BadRequest("Invalid OpenIddict request."); + } + + if (request.IsPasswordGrantType()) + { + var user = await _userManager.FindByEmailAsync(request.Username ?? string.Empty); + if (user is null) + { + return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + var valid = await _userManager.CheckPasswordAsync(user, request.Password ?? string.Empty); + if (!valid) + { + return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + var principal = await _signInManager.CreateUserPrincipalAsync(user); + var scopes = request.Scope.GetScopesOrDefault(); + principal.SetScopes(scopes); + + foreach (var claim in principal.Claims) + { + claim.SetDestinations(ClaimsExtensions.GetDestinations(claim)); + } + + return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + if (request.IsRefreshTokenGrantType()) + { + var authenticateResult = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + if (!authenticateResult.Succeeded || authenticateResult.Principal is null) + { + return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + var principal = authenticateResult.Principal; + return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + return BadRequest("Unsupported grant type."); + } +} diff --git a/src/MemberCenter.Api/Controllers/UserController.cs b/src/MemberCenter.Api/Controllers/UserController.cs new file mode 100644 index 0000000..dbba210 --- /dev/null +++ b/src/MemberCenter.Api/Controllers/UserController.cs @@ -0,0 +1,37 @@ +using MemberCenter.Infrastructure.Identity; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace MemberCenter.Api.Controllers; + +[ApiController] +[Route("user")] +public class UserController : ControllerBase +{ + private readonly UserManager _userManager; + + public UserController(UserManager userManager) + { + _userManager = userManager; + } + + [Authorize] + [HttpGet("profile")] + public async Task Profile() + { + var user = await _userManager.GetUserAsync(User); + if (user is null) + { + return Unauthorized(); + } + + return Ok(new + { + id = user.Id, + email = user.Email, + email_verified = user.EmailConfirmed, + created_at = user.CreatedAt + }); + } +} diff --git a/src/MemberCenter.Api/Extensions/ClaimsExtensions.cs b/src/MemberCenter.Api/Extensions/ClaimsExtensions.cs new file mode 100644 index 0000000..2a8fc62 --- /dev/null +++ b/src/MemberCenter.Api/Extensions/ClaimsExtensions.cs @@ -0,0 +1,31 @@ +using OpenIddict.Abstractions; + +namespace MemberCenter.Api.Extensions; + +public static class ClaimsExtensions +{ + public static IEnumerable GetScopesOrDefault(this string? scope) + { + if (string.IsNullOrWhiteSpace(scope)) + { + return new[] + { + OpenIddictConstants.Scopes.OpenId, + OpenIddictConstants.Scopes.Email, + OpenIddictConstants.Scopes.Profile + }; + } + + return scope.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + + public static IEnumerable GetDestinations(System.Security.Claims.Claim claim) + { + return claim.Type switch + { + OpenIddictConstants.Claims.Name or OpenIddictConstants.Claims.Email => + new[] { OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken }, + _ => new[] { OpenIddictConstants.Destinations.AccessToken } + }; + } +} diff --git a/src/MemberCenter.Api/MemberCenter.Api.csproj b/src/MemberCenter.Api/MemberCenter.Api.csproj new file mode 100644 index 0000000..3a68cca --- /dev/null +++ b/src/MemberCenter.Api/MemberCenter.Api.csproj @@ -0,0 +1,24 @@ + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + net8.0 + enable + enable + + + diff --git a/src/MemberCenter.Api/Program.cs b/src/MemberCenter.Api/Program.cs new file mode 100644 index 0000000..9e4555d --- /dev/null +++ b/src/MemberCenter.Api/Program.cs @@ -0,0 +1,93 @@ +using MemberCenter.Infrastructure.Identity; +using MemberCenter.Infrastructure.Persistence; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDbContext(options => +{ + var connectionString = builder.Configuration.GetConnectionString("Default") + ?? "Host=localhost;Database=member_center;Username=postgres;Password=postgres"; + + options.UseNpgsql(connectionString); + options.UseOpenIddict(); +}); + +builder.Services + .AddIdentity(options => + { + options.User.RequireUniqueEmail = true; + options.Password.RequireDigit = true; + options.Password.RequireLowercase = true; + options.Password.RequireUppercase = true; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequiredLength = 8; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme; + options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme; +}); + +builder.Services.AddOpenIddict() + .AddCore(options => + { + options.UseEntityFrameworkCore() + .UseDbContext() + .ReplaceDefaultEntities(); + }) + .AddServer(options => + { + options.SetAuthorizationEndpointUris("/oauth/authorize"); + options.SetTokenEndpointUris("/oauth/token", "/auth/login", "/auth/refresh"); + options.SetLogoutEndpointUris("/auth/logout"); + + options.AllowAuthorizationCodeFlow() + .RequireProofKeyForCodeExchange(); + options.AllowRefreshTokenFlow(); + options.AllowPasswordFlow(); + + options.AcceptAnonymousClients(); + + options.RegisterScopes( + OpenIddictConstants.Scopes.OpenId, + OpenIddictConstants.Scopes.Email, + OpenIddictConstants.Scopes.Profile); + + options.AddDevelopmentEncryptionCertificate(); + options.AddDevelopmentSigningCertificate(); + + options.UseAspNetCore() + .EnableAuthorizationEndpointPassthrough() + .EnableTokenEndpointPassthrough() + .EnableLogoutEndpointPassthrough() + .EnableStatusCodePagesIntegration(); + }) + .AddValidation(options => + { + options.UseLocalServer(); + options.UseAspNetCore(); + }); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("Admin", policy => policy.RequireRole("admin")); +}); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.UseRouting(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/src/MemberCenter.Api/Properties/launchSettings.json b/src/MemberCenter.Api/Properties/launchSettings.json new file mode 100644 index 0000000..07327ad --- /dev/null +++ b/src/MemberCenter.Api/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:0", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:0", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:0;http://localhost:0", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/MemberCenter.Api/appsettings.json b/src/MemberCenter.Api/appsettings.json new file mode 100644 index 0000000..067a9df --- /dev/null +++ b/src/MemberCenter.Api/appsettings.json @@ -0,0 +1,11 @@ +{ + "ConnectionStrings": { + "Default": "Host=localhost;Database=member_center;Username=postgres;Password=postgres" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/MemberCenter.Application/MemberCenter.Application.csproj b/src/MemberCenter.Application/MemberCenter.Application.csproj new file mode 100644 index 0000000..873401d --- /dev/null +++ b/src/MemberCenter.Application/MemberCenter.Application.csproj @@ -0,0 +1,13 @@ + + + + + + + + net8.0 + enable + enable + + + diff --git a/src/MemberCenter.Domain/Constants/SubscriptionStatus.cs b/src/MemberCenter.Domain/Constants/SubscriptionStatus.cs new file mode 100644 index 0000000..b3fb133 --- /dev/null +++ b/src/MemberCenter.Domain/Constants/SubscriptionStatus.cs @@ -0,0 +1,8 @@ +namespace MemberCenter.Domain.Constants; + +public static class SubscriptionStatus +{ + public const string Pending = "pending"; + public const string Active = "active"; + public const string Unsubscribed = "unsubscribed"; +} diff --git a/src/MemberCenter.Domain/Entities/AuditLog.cs b/src/MemberCenter.Domain/Entities/AuditLog.cs new file mode 100644 index 0000000..5414e0f --- /dev/null +++ b/src/MemberCenter.Domain/Entities/AuditLog.cs @@ -0,0 +1,11 @@ +namespace MemberCenter.Domain.Entities; + +public sealed class AuditLog +{ + public Guid Id { get; set; } + public string ActorType { get; set; } = string.Empty; + public Guid? ActorId { get; set; } + public string Action { get; set; } = string.Empty; + public System.Text.Json.JsonDocument Payload { get; set; } = System.Text.Json.JsonDocument.Parse("{}"); + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; +} diff --git a/src/MemberCenter.Domain/Entities/EmailVerification.cs b/src/MemberCenter.Domain/Entities/EmailVerification.cs new file mode 100644 index 0000000..56e6cde --- /dev/null +++ b/src/MemberCenter.Domain/Entities/EmailVerification.cs @@ -0,0 +1,12 @@ +namespace MemberCenter.Domain.Entities; + +public sealed class EmailVerification +{ + public Guid Id { get; set; } + public string Email { get; set; } = string.Empty; + public Guid TenantId { get; set; } + public string TokenHash { get; set; } = string.Empty; + public string Purpose { get; set; } = string.Empty; + public DateTimeOffset ExpiresAt { get; set; } + public DateTimeOffset? ConsumedAt { get; set; } +} diff --git a/src/MemberCenter.Domain/Entities/NewsletterList.cs b/src/MemberCenter.Domain/Entities/NewsletterList.cs new file mode 100644 index 0000000..b3a31f5 --- /dev/null +++ b/src/MemberCenter.Domain/Entities/NewsletterList.cs @@ -0,0 +1,13 @@ +namespace MemberCenter.Domain.Entities; + +public sealed class NewsletterList +{ + public Guid Id { get; set; } + public Guid TenantId { get; set; } + public string Name { get; set; } = string.Empty; + public string Status { get; set; } = "active"; + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + + public Tenant? Tenant { get; set; } + public List Subscriptions { get; set; } = new(); +} diff --git a/src/MemberCenter.Domain/Entities/NewsletterSubscription.cs b/src/MemberCenter.Domain/Entities/NewsletterSubscription.cs new file mode 100644 index 0000000..b7ae9bb --- /dev/null +++ b/src/MemberCenter.Domain/Entities/NewsletterSubscription.cs @@ -0,0 +1,16 @@ +using MemberCenter.Domain.Constants; + +namespace MemberCenter.Domain.Entities; + +public sealed class NewsletterSubscription +{ + public Guid Id { get; set; } + public Guid ListId { get; set; } + public string Email { get; set; } = string.Empty; + public Guid? UserId { get; set; } + public string Status { get; set; } = SubscriptionStatus.Pending; + public System.Text.Json.JsonDocument Preferences { get; set; } = System.Text.Json.JsonDocument.Parse("{}"); + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + + public NewsletterList? List { get; set; } +} diff --git a/src/MemberCenter.Domain/Entities/SystemFlag.cs b/src/MemberCenter.Domain/Entities/SystemFlag.cs new file mode 100644 index 0000000..555d71b --- /dev/null +++ b/src/MemberCenter.Domain/Entities/SystemFlag.cs @@ -0,0 +1,9 @@ +namespace MemberCenter.Domain.Entities; + +public sealed class SystemFlag +{ + public Guid Id { get; set; } + public string Key { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow; +} diff --git a/src/MemberCenter.Domain/Entities/Tenant.cs b/src/MemberCenter.Domain/Entities/Tenant.cs new file mode 100644 index 0000000..f46a99b --- /dev/null +++ b/src/MemberCenter.Domain/Entities/Tenant.cs @@ -0,0 +1,12 @@ +namespace MemberCenter.Domain.Entities; + +public sealed class Tenant +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public List Domains { get; set; } = new(); + public string Status { get; set; } = "active"; + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + + public List NewsletterLists { get; set; } = new(); +} diff --git a/src/MemberCenter.Domain/Entities/UnsubscribeToken.cs b/src/MemberCenter.Domain/Entities/UnsubscribeToken.cs new file mode 100644 index 0000000..e5e0408 --- /dev/null +++ b/src/MemberCenter.Domain/Entities/UnsubscribeToken.cs @@ -0,0 +1,12 @@ +namespace MemberCenter.Domain.Entities; + +public sealed class UnsubscribeToken +{ + public Guid Id { get; set; } + public Guid SubscriptionId { get; set; } + public string TokenHash { get; set; } = string.Empty; + public DateTimeOffset ExpiresAt { get; set; } + public DateTimeOffset? ConsumedAt { get; set; } + + public NewsletterSubscription? Subscription { get; set; } +} diff --git a/src/MemberCenter.Domain/MemberCenter.Domain.csproj b/src/MemberCenter.Domain/MemberCenter.Domain.csproj new file mode 100644 index 0000000..bb23fb7 --- /dev/null +++ b/src/MemberCenter.Domain/MemberCenter.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/MemberCenter.Infrastructure/Identity/ApplicationRole.cs b/src/MemberCenter.Infrastructure/Identity/ApplicationRole.cs new file mode 100644 index 0000000..8e245f9 --- /dev/null +++ b/src/MemberCenter.Infrastructure/Identity/ApplicationRole.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Identity; + +namespace MemberCenter.Infrastructure.Identity; + +public class ApplicationRole : IdentityRole +{ + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; +} diff --git a/src/MemberCenter.Infrastructure/Identity/ApplicationUser.cs b/src/MemberCenter.Infrastructure/Identity/ApplicationUser.cs new file mode 100644 index 0000000..4b6c00b --- /dev/null +++ b/src/MemberCenter.Infrastructure/Identity/ApplicationUser.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Identity; + +namespace MemberCenter.Infrastructure.Identity; + +public class ApplicationUser : IdentityUser +{ + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; +} diff --git a/src/MemberCenter.Infrastructure/MemberCenter.Infrastructure.csproj b/src/MemberCenter.Infrastructure/MemberCenter.Infrastructure.csproj new file mode 100644 index 0000000..04807e2 --- /dev/null +++ b/src/MemberCenter.Infrastructure/MemberCenter.Infrastructure.csproj @@ -0,0 +1,25 @@ + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + net8.0 + enable + enable + + + diff --git a/src/MemberCenter.Infrastructure/Persistence/MemberCenterDbContext.cs b/src/MemberCenter.Infrastructure/Persistence/MemberCenterDbContext.cs new file mode 100644 index 0000000..b4c40ec --- /dev/null +++ b/src/MemberCenter.Infrastructure/Persistence/MemberCenterDbContext.cs @@ -0,0 +1,167 @@ +using MemberCenter.Domain.Entities; +using MemberCenter.Infrastructure.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace MemberCenter.Infrastructure.Persistence; + +public class MemberCenterDbContext + : IdentityDbContext +{ + public MemberCenterDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Tenants => Set(); + public DbSet NewsletterLists => Set(); + public DbSet NewsletterSubscriptions => Set(); + public DbSet EmailVerifications => Set(); + public DbSet UnsubscribeTokens => Set(); + public DbSet AuditLogs => Set(); + public DbSet SystemFlags => Set(); + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.UseOpenIddict(); + + builder.Entity(entity => + { + entity.ToTable("tenants"); + entity.HasKey(x => x.Id); + entity.Property(x => x.Name).IsRequired(); + entity.Property(x => x.Status).IsRequired().HasDefaultValue("active"); + entity.Property(x => x.Domains).HasColumnType("text[]"); + entity.Property(x => x.CreatedAt).HasDefaultValueSql("now()"); + }); + + builder.Entity(entity => + { + entity.ToTable("newsletter_lists"); + entity.HasKey(x => x.Id); + entity.Property(x => x.Name).IsRequired(); + entity.Property(x => x.Status).IsRequired().HasDefaultValue("active"); + entity.Property(x => x.CreatedAt).HasDefaultValueSql("now()"); + entity.HasOne(x => x.Tenant) + .WithMany(t => t.NewsletterLists) + .HasForeignKey(x => x.TenantId) + .OnDelete(DeleteBehavior.Cascade); + }); + + builder.Entity(entity => + { + entity.ToTable("newsletter_subscriptions"); + entity.HasKey(x => x.Id); + entity.Property(x => x.Email).IsRequired(); + entity.Property(x => x.Status).IsRequired().HasDefaultValue("pending"); + entity.Property(x => x.CreatedAt).HasDefaultValueSql("now()"); + entity.Property(x => x.Preferences) + .HasColumnType("jsonb") + .HasConversion( + v => v.RootElement.GetRawText(), + v => System.Text.Json.JsonDocument.Parse(v, new System.Text.Json.JsonDocumentOptions())); + entity.HasIndex(x => x.Email).HasDatabaseName("idx_newsletter_subscriptions_email"); + entity.HasIndex(x => x.ListId).HasDatabaseName("idx_newsletter_subscriptions_list_id"); + entity.HasIndex(x => new { x.ListId, x.Email }).IsUnique(); + entity.HasOne(x => x.List) + .WithMany(l => l.Subscriptions) + .HasForeignKey(x => x.ListId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne() + .WithMany() + .HasForeignKey(x => x.UserId) + .OnDelete(DeleteBehavior.SetNull); + }); + + builder.Entity(entity => + { + entity.ToTable("email_verifications"); + entity.HasKey(x => x.Id); + entity.Property(x => x.Email).IsRequired(); + entity.Property(x => x.TokenHash).IsRequired(); + entity.Property(x => x.Purpose).IsRequired(); + entity.HasIndex(x => x.Email).HasDatabaseName("idx_email_verifications_email"); + entity.HasOne() + .WithMany() + .HasForeignKey(x => x.TenantId) + .OnDelete(DeleteBehavior.Cascade); + }); + + builder.Entity(entity => + { + entity.ToTable("unsubscribe_tokens"); + entity.HasKey(x => x.Id); + entity.Property(x => x.TokenHash).IsRequired(); + entity.HasOne(x => x.Subscription) + .WithMany() + .HasForeignKey(x => x.SubscriptionId) + .OnDelete(DeleteBehavior.Cascade); + }); + + builder.Entity(entity => + { + entity.ToTable("audit_logs"); + entity.HasKey(x => x.Id); + entity.Property(x => x.ActorType).IsRequired(); + entity.Property(x => x.Action).IsRequired(); + entity.Property(x => x.Payload) + .HasColumnType("jsonb") + .HasConversion( + v => v.RootElement.GetRawText(), + v => System.Text.Json.JsonDocument.Parse(v, new System.Text.Json.JsonDocumentOptions())); + entity.Property(x => x.CreatedAt).HasDefaultValueSql("now()"); + }); + + builder.Entity(entity => + { + entity.ToTable("system_flags"); + entity.HasKey(x => x.Id); + entity.Property(x => x.Key).IsRequired(); + entity.Property(x => x.Value).IsRequired(); + entity.Property(x => x.UpdatedAt).HasDefaultValueSql("now()"); + entity.HasIndex(x => x.Key).IsUnique(); + }); + + builder.Entity(entity => + { + entity.ToTable("users"); + entity.Property(x => x.CreatedAt).HasDefaultValueSql("now()"); + }); + + builder.Entity(entity => + { + entity.ToTable("roles"); + entity.Property(x => x.CreatedAt).HasDefaultValueSql("now()"); + }); + + builder.Entity>(entity => + { + entity.ToTable("user_roles"); + }); + + builder.Entity>(entity => + { + entity.ToTable("user_claims"); + }); + + builder.Entity>(entity => + { + entity.ToTable("role_claims"); + }); + + builder.Entity>(entity => + { + entity.ToTable("user_logins"); + }); + + builder.Entity>(entity => + { + entity.ToTable("user_tokens"); + }); + + builder.Entity>().HasKey(l => new { l.LoginProvider, l.ProviderKey }); + builder.Entity>().HasKey(t => new { t.UserId, t.LoginProvider, t.Name }); + } +} diff --git a/src/MemberCenter.Infrastructure/Persistence/MemberCenterDbContextFactory.cs b/src/MemberCenter.Infrastructure/Persistence/MemberCenterDbContextFactory.cs new file mode 100644 index 0000000..247f42b --- /dev/null +++ b/src/MemberCenter.Infrastructure/Persistence/MemberCenterDbContextFactory.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace MemberCenter.Infrastructure.Persistence; + +public class MemberCenterDbContextFactory : IDesignTimeDbContextFactory +{ + public MemberCenterDbContext CreateDbContext(string[] args) + { + var connectionString = + Environment.GetEnvironmentVariable("MEMBERCENTER_CONNECTION") + ?? Environment.GetEnvironmentVariable("ConnectionStrings__Default") + ?? "Host=localhost;Database=member_center;Username=postgres;Password=postgres"; + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(connectionString); + + return new MemberCenterDbContext(optionsBuilder.Options); + } +} diff --git a/src/MemberCenter.Infrastructure/Persistence/Migrations/20260203055037_InitialCreate.Designer.cs b/src/MemberCenter.Infrastructure/Persistence/Migrations/20260203055037_InitialCreate.Designer.cs new file mode 100644 index 0000000..4e93273 --- /dev/null +++ b/src/MemberCenter.Infrastructure/Persistence/Migrations/20260203055037_InitialCreate.Designer.cs @@ -0,0 +1,817 @@ +// +using System; +using System.Collections.Generic; +using MemberCenter.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MemberCenter.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(MemberCenterDbContext))] + [Migration("20260203055037_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MemberCenter.Domain.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActorId") + .HasColumnType("uuid"); + + b.Property("ActorType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.ToTable("audit_logs", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.EmailVerification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Purpose") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .HasDatabaseName("idx_email_verifications_email"); + + b.HasIndex("TenantId"); + + b.ToTable("email_verifications", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.NewsletterList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("active"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("newsletter_lists", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.NewsletterSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ListId") + .HasColumnType("uuid"); + + b.Property("Preferences") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("pending"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .HasDatabaseName("idx_newsletter_subscriptions_email"); + + b.HasIndex("ListId") + .HasDatabaseName("idx_newsletter_subscriptions_list_id"); + + b.HasIndex("UserId"); + + b.HasIndex("ListId", "Email") + .IsUnique(); + + b.ToTable("newsletter_subscriptions", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.SystemFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("system_flags", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property>("Domains") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("active"); + + b.HasKey("Id"); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.UnsubscribeToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriptionId") + .HasColumnType("uuid"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("unsubscribe_tokens", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Infrastructure.Identity.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("roles", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("role_claims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_claims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("user_logins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("user_roles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("user_tokens", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.EmailVerification", b => + { + b.HasOne("MemberCenter.Domain.Entities.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.NewsletterList", b => + { + b.HasOne("MemberCenter.Domain.Entities.Tenant", "Tenant") + .WithMany("NewsletterLists") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.NewsletterSubscription", b => + { + b.HasOne("MemberCenter.Domain.Entities.NewsletterList", "List") + .WithMany("Subscriptions") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("List"); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.UnsubscribeToken", b => + { + b.HasOne("MemberCenter.Domain.Entities.NewsletterSubscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.NewsletterList", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.Tenant", b => + { + b.Navigation("NewsletterLists"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/MemberCenter.Infrastructure/Persistence/Migrations/20260203055037_InitialCreate.cs b/src/MemberCenter.Infrastructure/Persistence/Migrations/20260203055037_InitialCreate.cs new file mode 100644 index 0000000..8b49d2b --- /dev/null +++ b/src/MemberCenter.Infrastructure/Persistence/Migrations/20260203055037_InitialCreate.cs @@ -0,0 +1,580 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MemberCenter.Infrastructure.Persistence.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "audit_logs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ActorType = table.Column(type: "text", nullable: false), + ActorId = table.Column(type: "uuid", nullable: true), + Action = table.Column(type: "text", nullable: false), + Payload = table.Column(type: "jsonb", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("PK_audit_logs", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictApplications", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ApplicationType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + ClientId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + ClientSecret = table.Column(type: "text", nullable: true), + ClientType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + ConsentType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + DisplayName = table.Column(type: "text", nullable: true), + DisplayNames = table.Column(type: "text", nullable: true), + JsonWebKeySet = table.Column(type: "text", nullable: true), + Permissions = table.Column(type: "text", nullable: true), + PostLogoutRedirectUris = table.Column(type: "text", nullable: true), + Properties = table.Column(type: "text", nullable: true), + RedirectUris = table.Column(type: "text", nullable: true), + Requirements = table.Column(type: "text", nullable: true), + Settings = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictApplications", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictScopes", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Description = table.Column(type: "text", nullable: true), + Descriptions = table.Column(type: "text", nullable: true), + DisplayName = table.Column(type: "text", nullable: true), + DisplayNames = table.Column(type: "text", nullable: true), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + Properties = table.Column(type: "text", nullable: true), + Resources = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictScopes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "roles", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_roles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "system_flags", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Key = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("PK_system_flags", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "tenants", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Domains = table.Column>(type: "text[]", nullable: false), + Status = table.Column(type: "text", nullable: false, defaultValue: "active"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("PK_tenants", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictAuthorizations", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ApplicationId = table.Column(type: "text", nullable: true), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + CreationDate = table.Column(type: "timestamp with time zone", nullable: true), + Properties = table.Column(type: "text", nullable: true), + Scopes = table.Column(type: "text", nullable: true), + Status = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Subject = table.Column(type: "character varying(400)", maxLength: 400, nullable: true), + Type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictAuthorizations_OpenIddictApplications_Application~", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "role_claims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RoleId = table.Column(type: "uuid", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_role_claims", x => x.Id); + table.ForeignKey( + name: "FK_role_claims_roles_RoleId", + column: x => x.RoleId, + principalTable: "roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "email_verifications", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Email = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "uuid", nullable: false), + TokenHash = table.Column(type: "text", nullable: false), + Purpose = table.Column(type: "text", nullable: false), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), + ConsumedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_email_verifications", x => x.Id); + table.ForeignKey( + name: "FK_email_verifications_tenants_TenantId", + column: x => x.TenantId, + principalTable: "tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "newsletter_lists", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TenantId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Status = table.Column(type: "text", nullable: false, defaultValue: "active"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("PK_newsletter_lists", x => x.Id); + table.ForeignKey( + name: "FK_newsletter_lists_tenants_TenantId", + column: x => x.TenantId, + principalTable: "tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_claims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "uuid", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_user_claims", x => x.Id); + table.ForeignKey( + name: "FK_user_claims_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_logins", + columns: table => new + { + LoginProvider = table.Column(type: "text", nullable: false), + ProviderKey = table.Column(type: "text", nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_user_logins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_user_logins_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_roles", + columns: table => new + { + UserId = table.Column(type: "uuid", nullable: false), + RoleId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_user_roles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_user_roles_roles_RoleId", + column: x => x.RoleId, + principalTable: "roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_user_roles_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_tokens", + columns: table => new + { + UserId = table.Column(type: "uuid", nullable: false), + LoginProvider = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_user_tokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_user_tokens_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictTokens", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ApplicationId = table.Column(type: "text", nullable: true), + AuthorizationId = table.Column(type: "text", nullable: true), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + CreationDate = table.Column(type: "timestamp with time zone", nullable: true), + ExpirationDate = table.Column(type: "timestamp with time zone", nullable: true), + Payload = table.Column(type: "text", nullable: true), + Properties = table.Column(type: "text", nullable: true), + RedemptionDate = table.Column(type: "timestamp with time zone", nullable: true), + ReferenceId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + Status = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Subject = table.Column(type: "character varying(400)", maxLength: 400, nullable: true), + Type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictTokens", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId", + column: x => x.AuthorizationId, + principalTable: "OpenIddictAuthorizations", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "newsletter_subscriptions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ListId = table.Column(type: "uuid", nullable: false), + Email = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: true), + Status = table.Column(type: "text", nullable: false, defaultValue: "pending"), + Preferences = table.Column(type: "jsonb", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("PK_newsletter_subscriptions", x => x.Id); + table.ForeignKey( + name: "FK_newsletter_subscriptions_newsletter_lists_ListId", + column: x => x.ListId, + principalTable: "newsletter_lists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_newsletter_subscriptions_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "unsubscribe_tokens", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + SubscriptionId = table.Column(type: "uuid", nullable: false), + TokenHash = table.Column(type: "text", nullable: false), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), + ConsumedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_unsubscribe_tokens", x => x.Id); + table.ForeignKey( + name: "FK_unsubscribe_tokens_newsletter_subscriptions_SubscriptionId", + column: x => x.SubscriptionId, + principalTable: "newsletter_subscriptions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "idx_email_verifications_email", + table: "email_verifications", + column: "Email"); + + migrationBuilder.CreateIndex( + name: "IX_email_verifications_TenantId", + table: "email_verifications", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_newsletter_lists_TenantId", + table: "newsletter_lists", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "idx_newsletter_subscriptions_email", + table: "newsletter_subscriptions", + column: "Email"); + + migrationBuilder.CreateIndex( + name: "idx_newsletter_subscriptions_list_id", + table: "newsletter_subscriptions", + column: "ListId"); + + migrationBuilder.CreateIndex( + name: "IX_newsletter_subscriptions_ListId_Email", + table: "newsletter_subscriptions", + columns: new[] { "ListId", "Email" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_newsletter_subscriptions_UserId", + table: "newsletter_subscriptions", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictApplications_ClientId", + table: "OpenIddictApplications", + column: "ClientId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictAuthorizations_ApplicationId_Status_Subject_Type", + table: "OpenIddictAuthorizations", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictScopes_Name", + table: "OpenIddictScopes", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ApplicationId_Status_Subject_Type", + table: "OpenIddictTokens", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_AuthorizationId", + table: "OpenIddictTokens", + column: "AuthorizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ReferenceId", + table: "OpenIddictTokens", + column: "ReferenceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_role_claims_RoleId", + table: "role_claims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "roles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_system_flags_Key", + table: "system_flags", + column: "Key", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_unsubscribe_tokens_SubscriptionId", + table: "unsubscribe_tokens", + column: "SubscriptionId"); + + migrationBuilder.CreateIndex( + name: "IX_user_claims_UserId", + table: "user_claims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_user_logins_UserId", + table: "user_logins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_user_roles_RoleId", + table: "user_roles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "users", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "users", + column: "NormalizedUserName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "audit_logs"); + + migrationBuilder.DropTable( + name: "email_verifications"); + + migrationBuilder.DropTable( + name: "OpenIddictScopes"); + + migrationBuilder.DropTable( + name: "OpenIddictTokens"); + + migrationBuilder.DropTable( + name: "role_claims"); + + migrationBuilder.DropTable( + name: "system_flags"); + + migrationBuilder.DropTable( + name: "unsubscribe_tokens"); + + migrationBuilder.DropTable( + name: "user_claims"); + + migrationBuilder.DropTable( + name: "user_logins"); + + migrationBuilder.DropTable( + name: "user_roles"); + + migrationBuilder.DropTable( + name: "user_tokens"); + + migrationBuilder.DropTable( + name: "OpenIddictAuthorizations"); + + migrationBuilder.DropTable( + name: "newsletter_subscriptions"); + + migrationBuilder.DropTable( + name: "roles"); + + migrationBuilder.DropTable( + name: "OpenIddictApplications"); + + migrationBuilder.DropTable( + name: "newsletter_lists"); + + migrationBuilder.DropTable( + name: "users"); + + migrationBuilder.DropTable( + name: "tenants"); + } + } +} diff --git a/src/MemberCenter.Infrastructure/Persistence/Migrations/MemberCenterDbContextModelSnapshot.cs b/src/MemberCenter.Infrastructure/Persistence/Migrations/MemberCenterDbContextModelSnapshot.cs new file mode 100644 index 0000000..e89b999 --- /dev/null +++ b/src/MemberCenter.Infrastructure/Persistence/Migrations/MemberCenterDbContextModelSnapshot.cs @@ -0,0 +1,814 @@ +// +using System; +using System.Collections.Generic; +using MemberCenter.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MemberCenter.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(MemberCenterDbContext))] + partial class MemberCenterDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MemberCenter.Domain.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActorId") + .HasColumnType("uuid"); + + b.Property("ActorType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.ToTable("audit_logs", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.EmailVerification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Purpose") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .HasDatabaseName("idx_email_verifications_email"); + + b.HasIndex("TenantId"); + + b.ToTable("email_verifications", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.NewsletterList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("active"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("newsletter_lists", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.NewsletterSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ListId") + .HasColumnType("uuid"); + + b.Property("Preferences") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("pending"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .HasDatabaseName("idx_newsletter_subscriptions_email"); + + b.HasIndex("ListId") + .HasDatabaseName("idx_newsletter_subscriptions_list_id"); + + b.HasIndex("UserId"); + + b.HasIndex("ListId", "Email") + .IsUnique(); + + b.ToTable("newsletter_subscriptions", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.SystemFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("system_flags", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property>("Domains") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("active"); + + b.HasKey("Id"); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.UnsubscribeToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriptionId") + .HasColumnType("uuid"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionId"); + + b.ToTable("unsubscribe_tokens", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Infrastructure.Identity.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("roles", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("role_claims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_claims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("user_logins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("user_roles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("user_tokens", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.EmailVerification", b => + { + b.HasOne("MemberCenter.Domain.Entities.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.NewsletterList", b => + { + b.HasOne("MemberCenter.Domain.Entities.Tenant", "Tenant") + .WithMany("NewsletterLists") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.NewsletterSubscription", b => + { + b.HasOne("MemberCenter.Domain.Entities.NewsletterList", "List") + .WithMany("Subscriptions") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("List"); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.UnsubscribeToken", b => + { + b.HasOne("MemberCenter.Domain.Entities.NewsletterSubscription", "Subscription") + .WithMany() + .HasForeignKey("SubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.NewsletterList", b => + { + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("MemberCenter.Domain.Entities.Tenant", b => + { + b.Navigation("NewsletterLists"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/MemberCenter.Installer/MemberCenter.Installer.csproj b/src/MemberCenter.Installer/MemberCenter.Installer.csproj new file mode 100644 index 0000000..3ada09f --- /dev/null +++ b/src/MemberCenter.Installer/MemberCenter.Installer.csproj @@ -0,0 +1,16 @@ + + + + + + + + + + Exe + net8.0 + enable + enable + + + diff --git a/src/MemberCenter.Installer/Program.cs b/src/MemberCenter.Installer/Program.cs new file mode 100644 index 0000000..83fa4f4 --- /dev/null +++ b/src/MemberCenter.Installer/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!");