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.
This commit is contained in:
warrenchen 2026-02-03 15:04:18 +09:00
parent 6f01f51934
commit 4631f82ee4
41 changed files with 3722 additions and 7 deletions

13
.config/dotnet-tools.json Normal file
View File

@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.11",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

8
.gitignore vendored
View File

@ -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/

6
Directory.Build.props Normal file
View File

@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<RunAnalyzers>false</RunAnalyzers>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
</PropertyGroup>
</Project>

55
MemberCenter.sln Normal file
View File

@ -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

View File

@ -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] }

View File

@ -0,0 +1,7 @@
namespace MemberCenter.Api.Contracts;
public sealed record TenantRequest(string Name, List<string> Domains, string Status);
public sealed record NewsletterListRequest(Guid TenantId, string Name, string Status);
public sealed record OAuthClientRequest(Guid TenantId, string Name, List<string> RedirectUris, string ClientType);

View File

@ -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);

View File

@ -0,0 +1,7 @@
namespace MemberCenter.Api.Contracts;
public sealed record SubscribeRequest(Guid ListId, string Email, Dictionary<string, object>? Preferences, string? Source);
public sealed record UnsubscribeRequest(string Token);
public sealed record UpdatePreferencesRequest(Guid SubscriptionId, Dictionary<string, object> Preferences);

View File

@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}

View File

@ -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<IActionResult> List()
{
var results = new List<object>();
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> Delete(string id)
{
var app = await _applicationManager.FindByIdAsync(id);
if (app is null)
{
return NotFound();
}
await _applicationManager.DeleteAsync(app);
return NoContent();
}
}

View File

@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}

View File

@ -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<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
public AuthController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
[HttpPost("register")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> Logout([FromBody] LogoutRequest request)
{
await _signInManager.SignOutAsync();
return NoContent();
}
}

View File

@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<string, object>? value)
{
if (value is null)
{
return JsonDocument.Parse("{}");
}
return JsonDocument.Parse(JsonSerializer.Serialize(value));
}
}

View File

@ -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<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
public OAuthController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
[HttpGet("/oauth/authorize")]
[Authorize]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.Features.Get<OpenIddictServerAspNetCoreFeature>()?.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);
}
}

View File

@ -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<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
public TokenController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
[HttpPost("/oauth/token")]
[HttpPost("/auth/login")]
[HttpPost("/auth/refresh")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.Features.Get<OpenIddictServerAspNetCoreFeature>()?.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.");
}
}

View File

@ -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<ApplicationUser> _userManager;
public UserController(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
[Authorize]
[HttpGet("profile")]
public async Task<IActionResult> 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
});
}
}

View File

@ -0,0 +1,31 @@
using OpenIddict.Abstractions;
namespace MemberCenter.Api.Extensions;
public static class ClaimsExtensions
{
public static IEnumerable<string> 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<string> 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 }
};
}
}

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\MemberCenter.Application\MemberCenter.Application.csproj" />
<ProjectReference Include="..\MemberCenter.Infrastructure\MemberCenter.Infrastructure.csproj" />
<ProjectReference Include="..\MemberCenter.Domain\MemberCenter.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="OpenIddict.AspNetCore" Version="5.7.0" />
<PackageReference Include="OpenIddict.Server.AspNetCore" Version="5.7.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@ -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<MemberCenterDbContext>(options =>
{
var connectionString = builder.Configuration.GetConnectionString("Default")
?? "Host=localhost;Database=member_center;Username=postgres;Password=postgres";
options.UseNpgsql(connectionString);
options.UseOpenIddict();
});
builder.Services
.AddIdentity<ApplicationUser, ApplicationRole>(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<MemberCenterDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
});
builder.Services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<MemberCenterDbContext>()
.ReplaceDefaultEntities<Guid>();
})
.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();

View File

@ -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"
}
}
}
}

View File

@ -0,0 +1,11 @@
{
"ConnectionStrings": {
"Default": "Host=localhost;Database=member_center;Username=postgres;Password=postgres"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MemberCenter.Domain\MemberCenter.Domain.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -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";
}

View File

@ -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;
}

View File

@ -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; }
}

View File

@ -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<NewsletterSubscription> Subscriptions { get; set; } = new();
}

View File

@ -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; }
}

View File

@ -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;
}

View File

@ -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<string> Domains { get; set; } = new();
public string Status { get; set; } = "active";
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public List<NewsletterList> NewsletterLists { get; set; } = new();
}

View File

@ -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; }
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Identity;
namespace MemberCenter.Infrastructure.Identity;
public class ApplicationRole : IdentityRole<Guid>
{
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}

View File

@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Identity;
namespace MemberCenter.Infrastructure.Identity;
public class ApplicationUser : IdentityUser<Guid>
{
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MemberCenter.Application\MemberCenter.Application.csproj" />
<ProjectReference Include="..\MemberCenter.Domain\MemberCenter.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.8" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="5.7.0" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -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<ApplicationUser, ApplicationRole, Guid>
{
public MemberCenterDbContext(DbContextOptions<MemberCenterDbContext> options)
: base(options)
{
}
public DbSet<Tenant> Tenants => Set<Tenant>();
public DbSet<NewsletterList> NewsletterLists => Set<NewsletterList>();
public DbSet<NewsletterSubscription> NewsletterSubscriptions => Set<NewsletterSubscription>();
public DbSet<EmailVerification> EmailVerifications => Set<EmailVerification>();
public DbSet<UnsubscribeToken> UnsubscribeTokens => Set<UnsubscribeToken>();
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
public DbSet<SystemFlag> SystemFlags => Set<SystemFlag>();
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.UseOpenIddict();
builder.Entity<Tenant>(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<NewsletterList>(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<NewsletterSubscription>(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<ApplicationUser>()
.WithMany()
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.SetNull);
});
builder.Entity<EmailVerification>(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<Tenant>()
.WithMany()
.HasForeignKey(x => x.TenantId)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<UnsubscribeToken>(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<AuditLog>(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<SystemFlag>(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<ApplicationUser>(entity =>
{
entity.ToTable("users");
entity.Property(x => x.CreatedAt).HasDefaultValueSql("now()");
});
builder.Entity<ApplicationRole>(entity =>
{
entity.ToTable("roles");
entity.Property(x => x.CreatedAt).HasDefaultValueSql("now()");
});
builder.Entity<Microsoft.AspNetCore.Identity.IdentityUserRole<Guid>>(entity =>
{
entity.ToTable("user_roles");
});
builder.Entity<Microsoft.AspNetCore.Identity.IdentityUserClaim<Guid>>(entity =>
{
entity.ToTable("user_claims");
});
builder.Entity<Microsoft.AspNetCore.Identity.IdentityRoleClaim<Guid>>(entity =>
{
entity.ToTable("role_claims");
});
builder.Entity<Microsoft.AspNetCore.Identity.IdentityUserLogin<Guid>>(entity =>
{
entity.ToTable("user_logins");
});
builder.Entity<Microsoft.AspNetCore.Identity.IdentityUserToken<Guid>>(entity =>
{
entity.ToTable("user_tokens");
});
builder.Entity<Microsoft.AspNetCore.Identity.IdentityUserLogin<Guid>>().HasKey(l => new { l.LoginProvider, l.ProviderKey });
builder.Entity<Microsoft.AspNetCore.Identity.IdentityUserToken<Guid>>().HasKey(t => new { t.UserId, t.LoginProvider, t.Name });
}
}

View File

@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace MemberCenter.Infrastructure.Persistence;
public class MemberCenterDbContextFactory : IDesignTimeDbContextFactory<MemberCenterDbContext>
{
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<MemberCenterDbContext>();
optionsBuilder.UseNpgsql(connectionString);
return new MemberCenterDbContext(optionsBuilder.Options);
}
}

View File

@ -0,0 +1,817 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Action")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("ActorId")
.HasColumnType("uuid");
b.Property<string>("ActorType")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Payload")
.IsRequired()
.HasColumnType("jsonb");
b.HasKey("Id");
b.ToTable("audit_logs", (string)null);
});
modelBuilder.Entity("MemberCenter.Domain.Entities.EmailVerification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset?>("ConsumedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Purpose")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("active");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("TenantId");
b.ToTable("newsletter_lists", (string)null);
});
modelBuilder.Entity("MemberCenter.Domain.Entities.NewsletterSubscription", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("ListId")
.HasColumnType("uuid");
b.Property<string>("Preferences")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("pending");
b.Property<Guid?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<List<string>>("Domains")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("active");
b.HasKey("Id");
b.ToTable("tenants", (string)null);
});
modelBuilder.Entity("MemberCenter.Domain.Entities.UnsubscribeToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset?>("ConsumedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("SubscriptionId")
.HasColumnType("uuid");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("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<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("role_claims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("user_claims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("user_logins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("user_roles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("user_tokens", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ApplicationType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ClientId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ClientSecret")
.HasColumnType("text");
b.Property<string>("ClientType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ConsentType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("DisplayNames")
.HasColumnType("text");
b.Property<string>("JsonWebKeySet")
.HasColumnType("text");
b.Property<string>("Permissions")
.HasColumnType("text");
b.Property<string>("PostLogoutRedirectUris")
.HasColumnType("text");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<string>("RedirectUris")
.HasColumnType("text");
b.Property<string>("Requirements")
.HasColumnType("text");
b.Property<string>("Settings")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("OpenIddictApplications", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ApplicationId")
.HasColumnType("text");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<string>("Scopes")
.HasColumnType("text");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<string>("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<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Descriptions")
.HasColumnType("text");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("DisplayNames")
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<string>("Resources")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("OpenIddictScopes", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ApplicationId")
.HasColumnType("text");
b.Property<string>("AuthorizationId")
.HasColumnType("text");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpirationDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Payload")
.HasColumnType("text");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<DateTime?>("RedemptionDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("ReferenceId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<string>("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<System.Guid>", b =>
{
b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", 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<System.Guid>", 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
}
}
}

View File

@ -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
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "audit_logs",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
ActorType = table.Column<string>(type: "text", nullable: false),
ActorId = table.Column<Guid>(type: "uuid", nullable: true),
Action = table.Column<string>(type: "text", nullable: false),
Payload = table.Column<string>(type: "jsonb", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(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<string>(type: "text", nullable: false),
ApplicationType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
ClientId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
ClientSecret = table.Column<string>(type: "text", nullable: true),
ClientType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
ConcurrencyToken = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
ConsentType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
DisplayName = table.Column<string>(type: "text", nullable: true),
DisplayNames = table.Column<string>(type: "text", nullable: true),
JsonWebKeySet = table.Column<string>(type: "text", nullable: true),
Permissions = table.Column<string>(type: "text", nullable: true),
PostLogoutRedirectUris = table.Column<string>(type: "text", nullable: true),
Properties = table.Column<string>(type: "text", nullable: true),
RedirectUris = table.Column<string>(type: "text", nullable: true),
Requirements = table.Column<string>(type: "text", nullable: true),
Settings = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictApplications", x => x.Id);
});
migrationBuilder.CreateTable(
name: "OpenIddictScopes",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
ConcurrencyToken = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
Description = table.Column<string>(type: "text", nullable: true),
Descriptions = table.Column<string>(type: "text", nullable: true),
DisplayName = table.Column<string>(type: "text", nullable: true),
DisplayNames = table.Column<string>(type: "text", nullable: true),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Properties = table.Column<string>(type: "text", nullable: true),
Resources = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictScopes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "roles",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_roles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "system_flags",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Key = table.Column<string>(type: "text", nullable: false),
Value = table.Column<string>(type: "text", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(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<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Domains = table.Column<List<string>>(type: "text[]", nullable: false),
Status = table.Column<string>(type: "text", nullable: false, defaultValue: "active"),
CreatedAt = table.Column<DateTimeOffset>(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<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: true),
SecurityStamp = table.Column<string>(type: "text", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
PhoneNumber = table.Column<string>(type: "text", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_users", x => x.Id);
});
migrationBuilder.CreateTable(
name: "OpenIddictAuthorizations",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
ApplicationId = table.Column<string>(type: "text", nullable: true),
ConcurrencyToken = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Properties = table.Column<string>(type: "text", nullable: true),
Scopes = table.Column<string>(type: "text", nullable: true),
Status = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
Subject = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: true),
Type = table.Column<string>(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<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RoleId = table.Column<Guid>(type: "uuid", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(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<Guid>(type: "uuid", nullable: false),
Email = table.Column<string>(type: "text", nullable: false),
TenantId = table.Column<Guid>(type: "uuid", nullable: false),
TokenHash = table.Column<string>(type: "text", nullable: false),
Purpose = table.Column<string>(type: "text", nullable: false),
ExpiresAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
ConsumedAt = table.Column<DateTimeOffset>(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<Guid>(type: "uuid", nullable: false),
TenantId = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Status = table.Column<string>(type: "text", nullable: false, defaultValue: "active"),
CreatedAt = table.Column<DateTimeOffset>(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<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(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<string>(type: "text", nullable: false),
ProviderKey = table.Column<string>(type: "text", nullable: false),
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<Guid>(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<Guid>(type: "uuid", nullable: false),
RoleId = table.Column<Guid>(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<Guid>(type: "uuid", nullable: false),
LoginProvider = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Value = table.Column<string>(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<string>(type: "text", nullable: false),
ApplicationId = table.Column<string>(type: "text", nullable: true),
AuthorizationId = table.Column<string>(type: "text", nullable: true),
ConcurrencyToken = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ExpirationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Payload = table.Column<string>(type: "text", nullable: true),
Properties = table.Column<string>(type: "text", nullable: true),
RedemptionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ReferenceId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
Status = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
Subject = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: true),
Type = table.Column<string>(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<Guid>(type: "uuid", nullable: false),
ListId = table.Column<Guid>(type: "uuid", nullable: false),
Email = table.Column<string>(type: "text", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: true),
Status = table.Column<string>(type: "text", nullable: false, defaultValue: "pending"),
Preferences = table.Column<string>(type: "jsonb", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(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<Guid>(type: "uuid", nullable: false),
SubscriptionId = table.Column<Guid>(type: "uuid", nullable: false),
TokenHash = table.Column<string>(type: "text", nullable: false),
ExpiresAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
ConsumedAt = table.Column<DateTimeOffset>(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);
}
/// <inheritdoc />
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");
}
}
}

View File

@ -0,0 +1,814 @@
// <auto-generated />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Action")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("ActorId")
.HasColumnType("uuid");
b.Property<string>("ActorType")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Payload")
.IsRequired()
.HasColumnType("jsonb");
b.HasKey("Id");
b.ToTable("audit_logs", (string)null);
});
modelBuilder.Entity("MemberCenter.Domain.Entities.EmailVerification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset?>("ConsumedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Purpose")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("active");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("TenantId");
b.ToTable("newsletter_lists", (string)null);
});
modelBuilder.Entity("MemberCenter.Domain.Entities.NewsletterSubscription", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("ListId")
.HasColumnType("uuid");
b.Property<string>("Preferences")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("pending");
b.Property<Guid?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<List<string>>("Domains")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Status")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("text")
.HasDefaultValue("active");
b.HasKey("Id");
b.ToTable("tenants", (string)null);
});
modelBuilder.Entity("MemberCenter.Domain.Entities.UnsubscribeToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset?>("ConsumedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("SubscriptionId")
.HasColumnType("uuid");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("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<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("role_claims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("user_claims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("user_logins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<Guid>("RoleId")
.HasColumnType("uuid");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("user_roles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("user_tokens", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ApplicationType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ClientId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ClientSecret")
.HasColumnType("text");
b.Property<string>("ClientType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("ConsentType")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("DisplayNames")
.HasColumnType("text");
b.Property<string>("JsonWebKeySet")
.HasColumnType("text");
b.Property<string>("Permissions")
.HasColumnType("text");
b.Property<string>("PostLogoutRedirectUris")
.HasColumnType("text");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<string>("RedirectUris")
.HasColumnType("text");
b.Property<string>("Requirements")
.HasColumnType("text");
b.Property<string>("Settings")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("OpenIddictApplications", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ApplicationId")
.HasColumnType("text");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<string>("Scopes")
.HasColumnType("text");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<string>("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<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Descriptions")
.HasColumnType("text");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("DisplayNames")
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<string>("Resources")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("OpenIddictScopes", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("text");
b.Property<string>("ApplicationId")
.HasColumnType("text");
b.Property<string>("AuthorizationId")
.HasColumnType("text");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpirationDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Payload")
.HasColumnType("text");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<DateTime?>("RedemptionDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("ReferenceId")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<string>("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<System.Guid>", b =>
{
b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", 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<System.Guid>", 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
}
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MemberCenter.Infrastructure\MemberCenter.Infrastructure.csproj" />
<ProjectReference Include="..\MemberCenter.Application\MemberCenter.Application.csproj" />
<ProjectReference Include="..\MemberCenter.Domain\MemberCenter.Domain.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,2 @@
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");