Compare commits
2 Commits
6f01f51934
...
8756010173
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8756010173 | ||
|
|
4631f82ee4 |
13
.config/dotnet-tools.json
Normal file
13
.config/dotnet-tools.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "8.0.11",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -32,6 +32,8 @@ artifacts/
|
|||||||
# NuGet
|
# NuGet
|
||||||
*.nupkg
|
*.nupkg
|
||||||
*.snupkg
|
*.snupkg
|
||||||
|
# Local NuGet/global package caches in repo
|
||||||
|
.nuget/
|
||||||
# The packages folder can be ignored because of PackageReference
|
# The packages folder can be ignored because of PackageReference
|
||||||
# Uncomment if using packages.config
|
# Uncomment if using packages.config
|
||||||
#packages/
|
#packages/
|
||||||
@ -63,6 +65,9 @@ ScaffoldingReadMe.txt
|
|||||||
secrets.json
|
secrets.json
|
||||||
appsettings.*.json
|
appsettings.*.json
|
||||||
!appsettings.json
|
!appsettings.json
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@ -71,3 +76,6 @@ Thumbs.db
|
|||||||
# Others
|
# Others
|
||||||
*.swp
|
*.swp
|
||||||
*.tmp
|
*.tmp
|
||||||
|
*.pid
|
||||||
|
*.pid.lock
|
||||||
|
.dotnet/
|
||||||
|
|||||||
6
Directory.Build.props
Normal file
6
Directory.Build.props
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<RunAnalyzers>false</RunAnalyzers>
|
||||||
|
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
55
MemberCenter.sln
Normal file
55
MemberCenter.sln
Normal 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
|
||||||
9
NuGet.Config
Normal file
9
NuGet.Config
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<config>
|
||||||
|
<add key="globalPackagesFolder" value=".nuget/packages" />
|
||||||
|
</config>
|
||||||
|
<packageSources>
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
@ -21,22 +21,12 @@
|
|||||||
- 強制重設 superuser 密碼
|
- 強制重設 superuser 密碼
|
||||||
- 只執行 migration(差異安裝),不重建 schema
|
- 只執行 migration(差異安裝),不重建 schema
|
||||||
|
|
||||||
## 建議實作方式(.NET)
|
## CLI 指令規格(實作)
|
||||||
- 提供一個 CLI 或安裝頁面(類 nopCommerce / Wagtail)
|
|
||||||
- CLI 支援互動式輸入(DB 連線、admin 帳號、密碼)
|
|
||||||
- 內部流程一致:
|
|
||||||
1) 讀取/寫入連線資訊
|
|
||||||
2) 執行 migration(`dbContext.Database.Migrate()`)
|
|
||||||
3) 建立角色與 superuser(若不存在)
|
|
||||||
|
|
||||||
## CLI 指令規格(草案)
|
|
||||||
|
|
||||||
### 通用參數
|
### 通用參數
|
||||||
- `--connection-string <string>`: DB 連線字串
|
- `--connection-string <string>`: DB 連線字串
|
||||||
- `--db-provider <string>`: 預設 `postgres`
|
- `--appsettings <path>`: 設定檔路徑(讀/寫 `ConnectionStrings:Default`)
|
||||||
- `--appsettings <path>`: 設定檔路徑(預設 `appsettings.json`)
|
|
||||||
- `--no-prompt`: 不使用互動輸入(CI/CD)
|
- `--no-prompt`: 不使用互動輸入(CI/CD)
|
||||||
- `--dry-run`: 只檢查,不寫入
|
|
||||||
- `--verbose`: 詳細輸出
|
- `--verbose`: 詳細輸出
|
||||||
|
|
||||||
### 1) `installer init`
|
### 1) `installer init`
|
||||||
@ -49,11 +39,13 @@
|
|||||||
- `--force`: 若偵測已初始化,仍強制執行
|
- `--force`: 若偵測已初始化,仍強制執行
|
||||||
|
|
||||||
流程:
|
流程:
|
||||||
1) 讀取/寫入 DB 連線資訊
|
1) 解析連線字串(參數或 appsettings)
|
||||||
|
- 若提供 `--connection-string`,會寫入 appsettings
|
||||||
|
- 若 appsettings 中缺少連線字串,會互動式詢問並寫入
|
||||||
2) 執行 migrations(不 Drop)
|
2) 執行 migrations(不 Drop)
|
||||||
3) 建立 roles(admin, support)
|
3) 建立 roles(admin, support)
|
||||||
4) 建立 admin(不存在才建立)
|
4) 建立 admin(不存在才建立)並加入 admin 角色
|
||||||
5) 寫入安裝鎖定(install.lock 或 DB flag)
|
5) 寫入安裝鎖定(DB flag: `system_flags` / `installed=true`)
|
||||||
|
|
||||||
### 2) `installer add-admin`
|
### 2) `installer add-admin`
|
||||||
用途:新增 superuser
|
用途:新增 superuser
|
||||||
@ -64,7 +56,7 @@
|
|||||||
- `--admin-display-name <string>`
|
- `--admin-display-name <string>`
|
||||||
|
|
||||||
流程:
|
流程:
|
||||||
1) 檢查 DB 連線
|
1) 解析連線字串
|
||||||
2) 建立使用者並指派 admin 角色
|
2) 建立使用者並指派 admin 角色
|
||||||
|
|
||||||
### 3) `installer reset-admin-password`
|
### 3) `installer reset-admin-password`
|
||||||
@ -75,9 +67,8 @@
|
|||||||
- `--admin-password <string>`
|
- `--admin-password <string>`
|
||||||
|
|
||||||
流程:
|
流程:
|
||||||
1) 檢查帳號存在
|
1) 解析連線字串
|
||||||
2) 更新密碼(強制)
|
2) 更新密碼(強制)
|
||||||
3) 寫入 audit log
|
|
||||||
|
|
||||||
### 4) `installer migrate`
|
### 4) `installer migrate`
|
||||||
用途:只執行 migrations
|
用途:只執行 migrations
|
||||||
@ -86,9 +77,8 @@
|
|||||||
- `--target <migration>`: 指定遷移(可選)
|
- `--target <migration>`: 指定遷移(可選)
|
||||||
|
|
||||||
流程:
|
流程:
|
||||||
1) 連線檢查
|
1) 解析連線字串
|
||||||
2) 執行 migrations
|
2) 執行 migrations(可指定 target)
|
||||||
3) 輸出版本
|
|
||||||
|
|
||||||
## 安全注意
|
## 安全注意
|
||||||
- 密碼必須符合強度規則
|
- 密碼必須符合強度規則
|
||||||
|
|||||||
192
docs/SCHEMA.sql
192
docs/SCHEMA.sql
@ -1,4 +1,7 @@
|
|||||||
-- PostgreSQL schema draft (MVP)
|
-- PostgreSQL schema draft (MVP)
|
||||||
|
-- Note: OpenIddict/Identity tables are based on EF Core migrations.
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||||
|
|
||||||
CREATE TABLE tenants (
|
CREATE TABLE tenants (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
@ -10,62 +13,137 @@ CREATE TABLE tenants (
|
|||||||
|
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
email TEXT NOT NULL UNIQUE,
|
user_name TEXT,
|
||||||
password_hash TEXT NOT NULL,
|
normalized_user_name TEXT,
|
||||||
email_verified_at TIMESTAMPTZ,
|
email TEXT,
|
||||||
status TEXT NOT NULL DEFAULT 'active',
|
normalized_email TEXT,
|
||||||
|
email_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
password_hash TEXT,
|
||||||
|
security_stamp TEXT,
|
||||||
|
concurrency_stamp TEXT,
|
||||||
|
phone_number TEXT,
|
||||||
|
phone_number_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
two_factor_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
lockout_end TIMESTAMPTZ,
|
||||||
|
lockout_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
access_failed_count INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE oauth_clients (
|
CREATE TABLE roles (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
name TEXT,
|
||||||
|
normalized_name TEXT,
|
||||||
|
concurrency_stamp TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_roles (
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (user_id, role_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_claims (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
claim_type TEXT,
|
||||||
|
claim_value TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE role_claims (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||||
|
claim_type TEXT,
|
||||||
|
claim_value TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_logins (
|
||||||
|
login_provider TEXT NOT NULL,
|
||||||
|
provider_key TEXT NOT NULL,
|
||||||
|
provider_display_name TEXT,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (login_provider, provider_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_tokens (
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
login_provider TEXT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
redirect_uris TEXT[] NOT NULL DEFAULT '{}',
|
value TEXT,
|
||||||
client_type TEXT NOT NULL DEFAULT 'confidential',
|
PRIMARY KEY (user_id, login_provider, name)
|
||||||
client_secret_hash TEXT,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ALTER TABLE oauth_clients
|
CREATE UNIQUE INDEX idx_users_normalized_user_name ON users(normalized_user_name);
|
||||||
ADD CONSTRAINT chk_oauth_clients_secret
|
CREATE UNIQUE INDEX idx_users_normalized_email ON users(normalized_email);
|
||||||
CHECK (
|
CREATE UNIQUE INDEX idx_roles_normalized_name ON roles(normalized_name);
|
||||||
(client_type = 'confidential' AND client_secret_hash IS NOT NULL)
|
|
||||||
OR (client_type <> 'confidential')
|
-- OpenIddict (EF Core default tables)
|
||||||
|
CREATE TABLE "OpenIddictApplications" (
|
||||||
|
"Id" UUID PRIMARY KEY,
|
||||||
|
"ApplicationType" TEXT,
|
||||||
|
"ClientId" TEXT,
|
||||||
|
"ClientSecret" TEXT,
|
||||||
|
"ConsentType" TEXT,
|
||||||
|
"DisplayName" TEXT,
|
||||||
|
"DisplayNames" JSONB,
|
||||||
|
"Permissions" JSONB,
|
||||||
|
"PostLogoutRedirectUris" JSONB,
|
||||||
|
"Properties" JSONB,
|
||||||
|
"RedirectUris" JSONB,
|
||||||
|
"Requirements" JSONB,
|
||||||
|
"ConcurrencyToken" TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE oauth_codes (
|
CREATE TABLE "OpenIddictAuthorizations" (
|
||||||
id UUID PRIMARY KEY,
|
"Id" UUID PRIMARY KEY,
|
||||||
client_id UUID NOT NULL REFERENCES oauth_clients(id),
|
"ApplicationId" UUID REFERENCES "OpenIddictApplications"("Id") ON DELETE SET NULL,
|
||||||
user_id UUID NOT NULL REFERENCES users(id),
|
"CreationDate" TIMESTAMPTZ,
|
||||||
code_hash TEXT NOT NULL,
|
"Status" TEXT,
|
||||||
code_challenge TEXT NOT NULL,
|
"Subject" TEXT,
|
||||||
code_challenge_method TEXT NOT NULL DEFAULT 'S256',
|
"Type" TEXT,
|
||||||
expires_at TIMESTAMPTZ NOT NULL,
|
"Scopes" JSONB,
|
||||||
consumed_at TIMESTAMPTZ
|
"Properties" JSONB,
|
||||||
|
"ConcurrencyToken" TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE oauth_tokens (
|
CREATE TABLE "OpenIddictScopes" (
|
||||||
id UUID PRIMARY KEY,
|
"Id" UUID PRIMARY KEY,
|
||||||
user_id UUID NOT NULL REFERENCES users(id),
|
"Description" TEXT,
|
||||||
client_id UUID NOT NULL REFERENCES oauth_clients(id),
|
"Descriptions" JSONB,
|
||||||
access_token_hash TEXT NOT NULL,
|
"DisplayName" TEXT,
|
||||||
refresh_token_hash TEXT NOT NULL,
|
"DisplayNames" JSONB,
|
||||||
expires_at TIMESTAMPTZ NOT NULL,
|
"Name" TEXT,
|
||||||
revoked_at TIMESTAMPTZ
|
"Properties" JSONB,
|
||||||
|
"Resources" JSONB,
|
||||||
|
"ConcurrencyToken" TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE oidc_id_tokens (
|
CREATE TABLE "OpenIddictTokens" (
|
||||||
id UUID PRIMARY KEY,
|
"Id" UUID PRIMARY KEY,
|
||||||
user_id UUID NOT NULL REFERENCES users(id),
|
"ApplicationId" UUID REFERENCES "OpenIddictApplications"("Id") ON DELETE SET NULL,
|
||||||
client_id UUID NOT NULL REFERENCES oauth_clients(id),
|
"AuthorizationId" UUID REFERENCES "OpenIddictAuthorizations"("Id") ON DELETE SET NULL,
|
||||||
id_token_hash TEXT NOT NULL,
|
"CreationDate" TIMESTAMPTZ,
|
||||||
expires_at TIMESTAMPTZ NOT NULL
|
"ExpirationDate" TIMESTAMPTZ,
|
||||||
|
"RedemptionDate" TIMESTAMPTZ,
|
||||||
|
"Payload" TEXT,
|
||||||
|
"ReferenceId" TEXT,
|
||||||
|
"Status" TEXT,
|
||||||
|
"Subject" TEXT,
|
||||||
|
"Type" TEXT,
|
||||||
|
"Properties" JSONB,
|
||||||
|
"ConcurrencyToken" TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_openiddict_applications_client_id ON "OpenIddictApplications"("ClientId");
|
||||||
|
CREATE UNIQUE INDEX idx_openiddict_scopes_name ON "OpenIddictScopes"("Name");
|
||||||
|
CREATE UNIQUE INDEX idx_openiddict_tokens_reference_id ON "OpenIddictTokens"("ReferenceId");
|
||||||
|
CREATE INDEX idx_openiddict_tokens_subject ON "OpenIddictTokens"("Subject");
|
||||||
|
|
||||||
CREATE TABLE newsletter_lists (
|
CREATE TABLE newsletter_lists (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
status TEXT NOT NULL DEFAULT 'active',
|
status TEXT NOT NULL DEFAULT 'active',
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
@ -73,9 +151,9 @@ CREATE TABLE newsletter_lists (
|
|||||||
|
|
||||||
CREATE TABLE newsletter_subscriptions (
|
CREATE TABLE newsletter_subscriptions (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
list_id UUID NOT NULL REFERENCES newsletter_lists(id),
|
list_id UUID NOT NULL REFERENCES newsletter_lists(id) ON DELETE CASCADE,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
user_id UUID REFERENCES users(id),
|
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||||
status TEXT NOT NULL DEFAULT 'pending',
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
preferences JSONB NOT NULL DEFAULT '{}'::jsonb,
|
preferences JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
@ -85,7 +163,7 @@ CREATE TABLE newsletter_subscriptions (
|
|||||||
CREATE TABLE email_verifications (
|
CREATE TABLE email_verifications (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||||
token_hash TEXT NOT NULL,
|
token_hash TEXT NOT NULL,
|
||||||
purpose TEXT NOT NULL,
|
purpose TEXT NOT NULL,
|
||||||
expires_at TIMESTAMPTZ NOT NULL,
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
@ -94,7 +172,7 @@ CREATE TABLE email_verifications (
|
|||||||
|
|
||||||
CREATE TABLE unsubscribe_tokens (
|
CREATE TABLE unsubscribe_tokens (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
subscription_id UUID NOT NULL REFERENCES newsletter_subscriptions(id),
|
subscription_id UUID NOT NULL REFERENCES newsletter_subscriptions(id) ON DELETE CASCADE,
|
||||||
token_hash TEXT NOT NULL,
|
token_hash TEXT NOT NULL,
|
||||||
expires_at TIMESTAMPTZ NOT NULL,
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
consumed_at TIMESTAMPTZ
|
consumed_at TIMESTAMPTZ
|
||||||
@ -109,17 +187,11 @@ CREATE TABLE audit_logs (
|
|||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE roles (
|
CREATE TABLE system_flags (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
name TEXT NOT NULL UNIQUE,
|
key TEXT NOT NULL UNIQUE,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
value TEXT NOT NULL,
|
||||||
);
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
|
||||||
CREATE TABLE user_roles (
|
|
||||||
user_id UUID NOT NULL REFERENCES users(id),
|
|
||||||
role_id UUID NOT NULL REFERENCES roles(id),
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
||||||
PRIMARY KEY (user_id, role_id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_newsletter_subscriptions_email
|
CREATE INDEX idx_newsletter_subscriptions_email
|
||||||
@ -128,17 +200,5 @@ CREATE INDEX idx_newsletter_subscriptions_email
|
|||||||
CREATE INDEX idx_newsletter_subscriptions_list_id
|
CREATE INDEX idx_newsletter_subscriptions_list_id
|
||||||
ON newsletter_subscriptions(list_id);
|
ON newsletter_subscriptions(list_id);
|
||||||
|
|
||||||
CREATE INDEX idx_oauth_tokens_user_id
|
|
||||||
ON oauth_tokens(user_id);
|
|
||||||
|
|
||||||
CREATE INDEX idx_oauth_tokens_client_id
|
|
||||||
ON oauth_tokens(client_id);
|
|
||||||
|
|
||||||
CREATE INDEX idx_oauth_codes_client_id
|
|
||||||
ON oauth_codes(client_id);
|
|
||||||
|
|
||||||
CREATE INDEX idx_email_verifications_email
|
CREATE INDEX idx_email_verifications_email
|
||||||
ON email_verifications(email);
|
ON email_verifications(email);
|
||||||
|
|
||||||
CREATE INDEX idx_user_roles_role_id
|
|
||||||
ON user_roles(role_id);
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ paths:
|
|||||||
get:
|
get:
|
||||||
summary: OAuth2 Authorization Endpoint
|
summary: OAuth2 Authorization Endpoint
|
||||||
description: Authorization Code + PKCE flow
|
description: Authorization Code + PKCE flow
|
||||||
|
security: []
|
||||||
parameters:
|
parameters:
|
||||||
- in: query
|
- in: query
|
||||||
name: client_id
|
name: client_id
|
||||||
@ -49,20 +50,15 @@ paths:
|
|||||||
/oauth/token:
|
/oauth/token:
|
||||||
post:
|
post:
|
||||||
summary: OAuth2 Token Endpoint
|
summary: OAuth2 Token Endpoint
|
||||||
|
security: []
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/x-www-form-urlencoded:
|
application/x-www-form-urlencoded:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
oneOf:
|
||||||
required: [grant_type, code, redirect_uri, client_id, code_verifier]
|
- $ref: '#/components/schemas/AuthorizationCodeTokenRequest'
|
||||||
properties:
|
- $ref: '#/components/schemas/RefreshTokenRequest'
|
||||||
grant_type: { type: string, enum: [authorization_code, refresh_token] }
|
|
||||||
code: { type: string }
|
|
||||||
redirect_uri: { type: string }
|
|
||||||
client_id: { type: string }
|
|
||||||
code_verifier: { type: string }
|
|
||||||
refresh_token: { type: string }
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Token response
|
description: Token response
|
||||||
@ -88,6 +84,7 @@ paths:
|
|||||||
/auth/register:
|
/auth/register:
|
||||||
post:
|
post:
|
||||||
summary: Register user
|
summary: Register user
|
||||||
|
security: []
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@ -105,12 +102,13 @@ paths:
|
|||||||
/auth/login:
|
/auth/login:
|
||||||
post:
|
post:
|
||||||
summary: API login
|
summary: API login
|
||||||
|
security: []
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/x-www-form-urlencoded:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/LoginRequest'
|
$ref: '#/components/schemas/PasswordTokenRequest'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Token response
|
description: Token response
|
||||||
@ -122,12 +120,13 @@ paths:
|
|||||||
/auth/refresh:
|
/auth/refresh:
|
||||||
post:
|
post:
|
||||||
summary: Refresh token
|
summary: Refresh token
|
||||||
|
security: []
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/x-www-form-urlencoded:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/RefreshRequest'
|
$ref: '#/components/schemas/RefreshTokenRequest'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Token response
|
description: Token response
|
||||||
@ -157,6 +156,7 @@ paths:
|
|||||||
/auth/password/forgot:
|
/auth/password/forgot:
|
||||||
post:
|
post:
|
||||||
summary: Request password reset
|
summary: Request password reset
|
||||||
|
security: []
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@ -170,6 +170,7 @@ paths:
|
|||||||
/auth/password/reset:
|
/auth/password/reset:
|
||||||
post:
|
post:
|
||||||
summary: Reset password
|
summary: Reset password
|
||||||
|
security: []
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@ -183,11 +184,16 @@ paths:
|
|||||||
/auth/email/verify:
|
/auth/email/verify:
|
||||||
get:
|
get:
|
||||||
summary: Verify email
|
summary: Verify email
|
||||||
|
security: []
|
||||||
parameters:
|
parameters:
|
||||||
- in: query
|
- in: query
|
||||||
name: token
|
name: token
|
||||||
required: true
|
required: true
|
||||||
schema: { type: string }
|
schema: { type: string }
|
||||||
|
- in: query
|
||||||
|
name: email
|
||||||
|
required: true
|
||||||
|
schema: { type: string, format: email }
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Email verified
|
description: Email verified
|
||||||
@ -208,6 +214,7 @@ paths:
|
|||||||
/newsletter/subscribe:
|
/newsletter/subscribe:
|
||||||
post:
|
post:
|
||||||
summary: Subscribe (unauthenticated allowed)
|
summary: Subscribe (unauthenticated allowed)
|
||||||
|
security: []
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@ -220,11 +227,12 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/Subscription'
|
$ref: '#/components/schemas/PendingSubscriptionResponse'
|
||||||
|
|
||||||
/newsletter/confirm:
|
/newsletter/confirm:
|
||||||
get:
|
get:
|
||||||
summary: Confirm subscription (double opt-in)
|
summary: Confirm subscription (double opt-in)
|
||||||
|
security: []
|
||||||
parameters:
|
parameters:
|
||||||
- in: query
|
- in: query
|
||||||
name: token
|
name: token
|
||||||
@ -241,6 +249,7 @@ paths:
|
|||||||
/newsletter/unsubscribe:
|
/newsletter/unsubscribe:
|
||||||
post:
|
post:
|
||||||
summary: Unsubscribe single list
|
summary: Unsubscribe single list
|
||||||
|
security: []
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@ -266,10 +275,6 @@ paths:
|
|||||||
name: subscription_id
|
name: subscription_id
|
||||||
required: false
|
required: false
|
||||||
schema: { type: string }
|
schema: { type: string }
|
||||||
- in: query
|
|
||||||
name: tenant_id
|
|
||||||
required: false
|
|
||||||
schema: { type: string }
|
|
||||||
- in: query
|
- in: query
|
||||||
name: email
|
name: email
|
||||||
required: false
|
required: false
|
||||||
@ -519,14 +524,25 @@ components:
|
|||||||
email: { type: string, format: email }
|
email: { type: string, format: email }
|
||||||
password: { type: string }
|
password: { type: string }
|
||||||
|
|
||||||
LoginRequest:
|
PasswordTokenRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [email, password, client_id]
|
required: [grant_type, username, password]
|
||||||
properties:
|
properties:
|
||||||
email: { type: string, format: email }
|
grant_type: { type: string, enum: [password] }
|
||||||
|
username: { type: string, format: email }
|
||||||
password: { type: string }
|
password: { type: string }
|
||||||
client_id: { type: string }
|
|
||||||
scope: { type: string }
|
scope: { type: string }
|
||||||
|
client_id: { type: string }
|
||||||
|
|
||||||
|
AuthorizationCodeTokenRequest:
|
||||||
|
type: object
|
||||||
|
required: [grant_type, code, redirect_uri, code_verifier]
|
||||||
|
properties:
|
||||||
|
grant_type: { type: string, enum: [authorization_code] }
|
||||||
|
code: { type: string }
|
||||||
|
redirect_uri: { type: string }
|
||||||
|
code_verifier: { type: string }
|
||||||
|
client_id: { type: string }
|
||||||
|
|
||||||
RefreshRequest:
|
RefreshRequest:
|
||||||
type: object
|
type: object
|
||||||
@ -535,6 +551,14 @@ components:
|
|||||||
refresh_token: { type: string }
|
refresh_token: { type: string }
|
||||||
client_id: { type: string }
|
client_id: { type: string }
|
||||||
|
|
||||||
|
RefreshTokenRequest:
|
||||||
|
type: object
|
||||||
|
required: [grant_type, refresh_token]
|
||||||
|
properties:
|
||||||
|
grant_type: { type: string, enum: [refresh_token] }
|
||||||
|
refresh_token: { type: string }
|
||||||
|
client_id: { type: string }
|
||||||
|
|
||||||
ForgotPasswordRequest:
|
ForgotPasswordRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [email]
|
required: [email]
|
||||||
@ -543,8 +567,9 @@ components:
|
|||||||
|
|
||||||
ResetPasswordRequest:
|
ResetPasswordRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [token, new_password]
|
required: [email, token, new_password]
|
||||||
properties:
|
properties:
|
||||||
|
email: { type: string, format: email }
|
||||||
token: { type: string }
|
token: { type: string }
|
||||||
new_password: { type: string }
|
new_password: { type: string }
|
||||||
|
|
||||||
@ -558,9 +583,8 @@ components:
|
|||||||
|
|
||||||
SubscribeRequest:
|
SubscribeRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [tenant_id, list_id, email]
|
required: [list_id, email]
|
||||||
properties:
|
properties:
|
||||||
tenant_id: { type: string }
|
|
||||||
list_id: { type: string }
|
list_id: { type: string }
|
||||||
email: { type: string, format: email }
|
email: { type: string, format: email }
|
||||||
preferences: { type: object }
|
preferences: { type: object }
|
||||||
@ -570,13 +594,19 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
id: { type: string }
|
id: { type: string }
|
||||||
tenant_id: { type: string }
|
|
||||||
list_id: { type: string }
|
list_id: { type: string }
|
||||||
email: { type: string, format: email }
|
email: { type: string, format: email }
|
||||||
status: { type: string, enum: [pending, active, unsubscribed] }
|
status: { type: string, enum: [pending, active, unsubscribed] }
|
||||||
preferences: { type: object }
|
preferences: { type: object }
|
||||||
created_at: { type: string, format: date-time }
|
created_at: { type: string, format: date-time }
|
||||||
|
|
||||||
|
PendingSubscriptionResponse:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/Subscription'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
confirm_token: { type: string }
|
||||||
|
|
||||||
Tenant:
|
Tenant:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
7
src/MemberCenter.Api/Contracts/AdminRequests.cs
Normal file
7
src/MemberCenter.Api/Contracts/AdminRequests.cs
Normal 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);
|
||||||
13
src/MemberCenter.Api/Contracts/AuthRequests.cs
Normal file
13
src/MemberCenter.Api/Contracts/AuthRequests.cs
Normal 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);
|
||||||
7
src/MemberCenter.Api/Contracts/NewsletterRequests.cs
Normal file
7
src/MemberCenter.Api/Contracts/NewsletterRequests.cs
Normal 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);
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
144
src/MemberCenter.Api/Controllers/AdminOAuthClientsController.cs
Normal file
144
src/MemberCenter.Api/Controllers/AdminOAuthClientsController.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/MemberCenter.Api/Controllers/AdminTenantsController.cs
Normal file
112
src/MemberCenter.Api/Controllers/AdminTenantsController.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/MemberCenter.Api/Controllers/AuthController.cs
Normal file
104
src/MemberCenter.Api/Controllers/AuthController.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
211
src/MemberCenter.Api/Controllers/NewsletterController.cs
Normal file
211
src/MemberCenter.Api/Controllers/NewsletterController.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/MemberCenter.Api/Controllers/OAuthController.cs
Normal file
49
src/MemberCenter.Api/Controllers/OAuthController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/MemberCenter.Api/Controllers/TokenController.cs
Normal file
74
src/MemberCenter.Api/Controllers/TokenController.cs
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/MemberCenter.Api/Controllers/UserController.cs
Normal file
37
src/MemberCenter.Api/Controllers/UserController.cs
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/MemberCenter.Api/Extensions/ClaimsExtensions.cs
Normal file
31
src/MemberCenter.Api/Extensions/ClaimsExtensions.cs
Normal 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 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/MemberCenter.Api/MemberCenter.Api.csproj
Normal file
24
src/MemberCenter.Api/MemberCenter.Api.csproj
Normal 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>
|
||||||
93
src/MemberCenter.Api/Program.cs
Normal file
93
src/MemberCenter.Api/Program.cs
Normal 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();
|
||||||
38
src/MemberCenter.Api/Properties/launchSettings.json
Normal file
38
src/MemberCenter.Api/Properties/launchSettings.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/MemberCenter.Api/appsettings.json
Normal file
11
src/MemberCenter.Api/appsettings.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Default": "Host=localhost;Database=member_center;Username=postgres;Password=postgres"
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/MemberCenter.Application/MemberCenter.Application.csproj
Normal file
13
src/MemberCenter.Application/MemberCenter.Application.csproj
Normal 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>
|
||||||
8
src/MemberCenter.Domain/Constants/SubscriptionStatus.cs
Normal file
8
src/MemberCenter.Domain/Constants/SubscriptionStatus.cs
Normal 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";
|
||||||
|
}
|
||||||
11
src/MemberCenter.Domain/Entities/AuditLog.cs
Normal file
11
src/MemberCenter.Domain/Entities/AuditLog.cs
Normal 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;
|
||||||
|
}
|
||||||
12
src/MemberCenter.Domain/Entities/EmailVerification.cs
Normal file
12
src/MemberCenter.Domain/Entities/EmailVerification.cs
Normal 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; }
|
||||||
|
}
|
||||||
13
src/MemberCenter.Domain/Entities/NewsletterList.cs
Normal file
13
src/MemberCenter.Domain/Entities/NewsletterList.cs
Normal 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();
|
||||||
|
}
|
||||||
16
src/MemberCenter.Domain/Entities/NewsletterSubscription.cs
Normal file
16
src/MemberCenter.Domain/Entities/NewsletterSubscription.cs
Normal 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; }
|
||||||
|
}
|
||||||
9
src/MemberCenter.Domain/Entities/SystemFlag.cs
Normal file
9
src/MemberCenter.Domain/Entities/SystemFlag.cs
Normal 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;
|
||||||
|
}
|
||||||
12
src/MemberCenter.Domain/Entities/Tenant.cs
Normal file
12
src/MemberCenter.Domain/Entities/Tenant.cs
Normal 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();
|
||||||
|
}
|
||||||
12
src/MemberCenter.Domain/Entities/UnsubscribeToken.cs
Normal file
12
src/MemberCenter.Domain/Entities/UnsubscribeToken.cs
Normal 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; }
|
||||||
|
}
|
||||||
9
src/MemberCenter.Domain/MemberCenter.Domain.csproj
Normal file
9
src/MemberCenter.Domain/MemberCenter.Domain.csproj
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace MemberCenter.Infrastructure.Identity;
|
||||||
|
|
||||||
|
public class ApplicationRole : IdentityRole<Guid>
|
||||||
|
{
|
||||||
|
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace MemberCenter.Infrastructure.Identity;
|
||||||
|
|
||||||
|
public class ApplicationUser : IdentityUser<Guid>
|
||||||
|
{
|
||||||
|
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
817
src/MemberCenter.Infrastructure/Persistence/Migrations/20260203055037_InitialCreate.Designer.cs
generated
Normal file
817
src/MemberCenter.Infrastructure/Persistence/Migrations/20260203055037_InitialCreate.Designer.cs
generated
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/MemberCenter.Installer/MemberCenter.Installer.csproj
Normal file
25
src/MemberCenter.Installer/MemberCenter.Installer.csproj
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||||
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
436
src/MemberCenter.Installer/Program.cs
Normal file
436
src/MemberCenter.Installer/Program.cs
Normal file
@ -0,0 +1,436 @@
|
|||||||
|
using MemberCenter.Domain.Entities;
|
||||||
|
using MemberCenter.Infrastructure.Identity;
|
||||||
|
using MemberCenter.Infrastructure.Persistence;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.CommandLine;
|
||||||
|
|
||||||
|
var root = new RootCommand("Member Center installer");
|
||||||
|
|
||||||
|
var connectionStringOption = new Option<string>(
|
||||||
|
name: "--connection-string",
|
||||||
|
description: "Database connection string");
|
||||||
|
connectionStringOption.AddAlias("-c");
|
||||||
|
|
||||||
|
var appsettingsOption = new Option<string?>(
|
||||||
|
name: "--appsettings",
|
||||||
|
description: "Path to appsettings.json");
|
||||||
|
|
||||||
|
var noPromptOption = new Option<bool>(
|
||||||
|
name: "--no-prompt",
|
||||||
|
description: "Disable interactive prompts");
|
||||||
|
|
||||||
|
var verboseOption = new Option<bool>(
|
||||||
|
name: "--verbose",
|
||||||
|
description: "Enable verbose output");
|
||||||
|
|
||||||
|
var forceOption = new Option<bool>(
|
||||||
|
name: "--force",
|
||||||
|
description: "Force execution even if already installed");
|
||||||
|
|
||||||
|
var adminEmailOption = new Option<string?>(
|
||||||
|
name: "--admin-email",
|
||||||
|
description: "Admin email");
|
||||||
|
|
||||||
|
var adminPasswordOption = new Option<string?>(
|
||||||
|
name: "--admin-password",
|
||||||
|
description: "Admin password");
|
||||||
|
|
||||||
|
var adminDisplayNameOption = new Option<string?>(
|
||||||
|
name: "--admin-display-name",
|
||||||
|
description: "Admin display name (optional)");
|
||||||
|
|
||||||
|
var targetMigrationOption = new Option<string?>(
|
||||||
|
name: "--target",
|
||||||
|
description: "Target migration");
|
||||||
|
|
||||||
|
var initCommand = new Command("init", "Initialize database (migrate + seed + admin)");
|
||||||
|
initCommand.AddOption(connectionStringOption);
|
||||||
|
initCommand.AddOption(appsettingsOption);
|
||||||
|
initCommand.AddOption(noPromptOption);
|
||||||
|
initCommand.AddOption(verboseOption);
|
||||||
|
initCommand.AddOption(forceOption);
|
||||||
|
initCommand.AddOption(adminEmailOption);
|
||||||
|
initCommand.AddOption(adminPasswordOption);
|
||||||
|
initCommand.AddOption(adminDisplayNameOption);
|
||||||
|
|
||||||
|
initCommand.SetHandler(async (string? connectionString, string? appsettings, bool noPrompt, bool verbose, bool force, string? adminEmail, string? adminPassword) =>
|
||||||
|
{
|
||||||
|
var resolvedConnection = ResolveConnectionString(connectionString, appsettings, noPrompt);
|
||||||
|
if (string.IsNullOrWhiteSpace(resolvedConnection))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Connection string is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var services = BuildServices(resolvedConnection);
|
||||||
|
await using var scope = services.CreateAsyncScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<MemberCenterDbContext>();
|
||||||
|
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||||
|
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
|
||||||
|
|
||||||
|
if (!force && await IsInstalledAsync(db))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("System already installed. Use --force to override.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noPrompt)
|
||||||
|
{
|
||||||
|
adminEmail ??= Prompt("Admin email", "admin@example.com");
|
||||||
|
adminPassword ??= PromptSecret("Admin password");
|
||||||
|
}
|
||||||
|
|
||||||
|
adminEmail ??= "admin@example.com";
|
||||||
|
if (string.IsNullOrWhiteSpace(adminPassword))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Admin password is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.Database.MigrateAsync();
|
||||||
|
|
||||||
|
await EnsureRoleAsync(roleManager, "admin");
|
||||||
|
await EnsureRoleAsync(roleManager, "support");
|
||||||
|
|
||||||
|
var admin = await userManager.FindByEmailAsync(adminEmail);
|
||||||
|
if (admin is null)
|
||||||
|
{
|
||||||
|
admin = new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserName = adminEmail,
|
||||||
|
Email = adminEmail,
|
||||||
|
EmailConfirmed = true
|
||||||
|
};
|
||||||
|
var createResult = await userManager.CreateAsync(admin, adminPassword);
|
||||||
|
if (!createResult.Succeeded)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(string.Join(Environment.NewLine, createResult.Errors.Select(e => e.Description)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await userManager.IsInRoleAsync(admin, "admin"))
|
||||||
|
{
|
||||||
|
await userManager.AddToRoleAsync(admin, "admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
await SetInstalledFlagAsync(db);
|
||||||
|
|
||||||
|
if (verbose)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Init completed.");
|
||||||
|
}
|
||||||
|
}, connectionStringOption, appsettingsOption, noPromptOption, verboseOption, forceOption, adminEmailOption, adminPasswordOption);
|
||||||
|
|
||||||
|
var addAdminCommand = new Command("add-admin", "Add admin user");
|
||||||
|
addAdminCommand.AddOption(connectionStringOption);
|
||||||
|
addAdminCommand.AddOption(appsettingsOption);
|
||||||
|
addAdminCommand.AddOption(noPromptOption);
|
||||||
|
addAdminCommand.AddOption(adminEmailOption);
|
||||||
|
addAdminCommand.AddOption(adminPasswordOption);
|
||||||
|
addAdminCommand.SetHandler(async (string? connectionString, string? appsettings, bool noPrompt, string? adminEmail, string? adminPassword) =>
|
||||||
|
{
|
||||||
|
var resolvedConnection = ResolveConnectionString(connectionString, appsettings, noPrompt);
|
||||||
|
if (string.IsNullOrWhiteSpace(resolvedConnection))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Connection string is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noPrompt)
|
||||||
|
{
|
||||||
|
adminEmail ??= Prompt("Admin email", "admin@example.com");
|
||||||
|
adminPassword ??= PromptSecret("Admin password");
|
||||||
|
}
|
||||||
|
|
||||||
|
adminEmail ??= "admin@example.com";
|
||||||
|
if (string.IsNullOrWhiteSpace(adminPassword))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Admin password is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var services = BuildServices(resolvedConnection);
|
||||||
|
await using var scope = services.CreateAsyncScope();
|
||||||
|
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||||
|
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
|
||||||
|
|
||||||
|
await EnsureRoleAsync(roleManager, "admin");
|
||||||
|
|
||||||
|
var admin = await userManager.FindByEmailAsync(adminEmail);
|
||||||
|
if (admin is null)
|
||||||
|
{
|
||||||
|
admin = new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserName = adminEmail,
|
||||||
|
Email = adminEmail,
|
||||||
|
EmailConfirmed = true
|
||||||
|
};
|
||||||
|
var createResult = await userManager.CreateAsync(admin, adminPassword);
|
||||||
|
if (!createResult.Succeeded)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(string.Join(Environment.NewLine, createResult.Errors.Select(e => e.Description)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await userManager.IsInRoleAsync(admin, "admin"))
|
||||||
|
{
|
||||||
|
await userManager.AddToRoleAsync(admin, "admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Admin user ready.");
|
||||||
|
}, connectionStringOption, appsettingsOption, noPromptOption, adminEmailOption, adminPasswordOption);
|
||||||
|
|
||||||
|
var resetCommand = new Command("reset-admin-password", "Reset admin password");
|
||||||
|
resetCommand.AddOption(connectionStringOption);
|
||||||
|
resetCommand.AddOption(appsettingsOption);
|
||||||
|
resetCommand.AddOption(noPromptOption);
|
||||||
|
resetCommand.AddOption(adminEmailOption);
|
||||||
|
resetCommand.AddOption(adminPasswordOption);
|
||||||
|
resetCommand.SetHandler(async (string? connectionString, string? appsettings, bool noPrompt, string? adminEmail, string? adminPassword) =>
|
||||||
|
{
|
||||||
|
var resolvedConnection = ResolveConnectionString(connectionString, appsettings, noPrompt);
|
||||||
|
if (string.IsNullOrWhiteSpace(resolvedConnection))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Connection string is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noPrompt)
|
||||||
|
{
|
||||||
|
adminEmail ??= Prompt("Admin email", "admin@example.com");
|
||||||
|
adminPassword ??= PromptSecret("New password");
|
||||||
|
}
|
||||||
|
|
||||||
|
adminEmail ??= "admin@example.com";
|
||||||
|
if (string.IsNullOrWhiteSpace(adminPassword))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Admin password is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var services = BuildServices(resolvedConnection);
|
||||||
|
await using var scope = services.CreateAsyncScope();
|
||||||
|
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||||
|
|
||||||
|
var admin = await userManager.FindByEmailAsync(adminEmail);
|
||||||
|
if (admin is null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Admin not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resetToken = await userManager.GeneratePasswordResetTokenAsync(admin);
|
||||||
|
var result = await userManager.ResetPasswordAsync(admin, resetToken, adminPassword);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(string.Join(Environment.NewLine, result.Errors.Select(e => e.Description)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Password reset completed.");
|
||||||
|
}, connectionStringOption, appsettingsOption, noPromptOption, adminEmailOption, adminPasswordOption);
|
||||||
|
|
||||||
|
var migrateCommand = new Command("migrate", "Run migrations only");
|
||||||
|
migrateCommand.AddOption(connectionStringOption);
|
||||||
|
migrateCommand.AddOption(appsettingsOption);
|
||||||
|
migrateCommand.AddOption(targetMigrationOption);
|
||||||
|
migrateCommand.SetHandler(async (string? connectionString, string? appsettings, string? target) =>
|
||||||
|
{
|
||||||
|
var resolvedConnection = ResolveConnectionString(connectionString, appsettings, noPrompt: true);
|
||||||
|
if (string.IsNullOrWhiteSpace(resolvedConnection))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Connection string is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var services = BuildServices(resolvedConnection);
|
||||||
|
await using var scope = services.CreateAsyncScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<MemberCenterDbContext>();
|
||||||
|
if (string.IsNullOrWhiteSpace(target))
|
||||||
|
{
|
||||||
|
await db.Database.MigrateAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var migrator = scope.ServiceProvider.GetRequiredService<IMigrator>();
|
||||||
|
await migrator.MigrateAsync(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Migrations completed.");
|
||||||
|
}, connectionStringOption, appsettingsOption, targetMigrationOption);
|
||||||
|
|
||||||
|
root.AddCommand(initCommand);
|
||||||
|
root.AddCommand(addAdminCommand);
|
||||||
|
root.AddCommand(resetCommand);
|
||||||
|
root.AddCommand(migrateCommand);
|
||||||
|
|
||||||
|
return await root.InvokeAsync(args);
|
||||||
|
|
||||||
|
static IServiceProvider BuildServices(string connectionString)
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddDbContext<MemberCenterDbContext>(options =>
|
||||||
|
{
|
||||||
|
options.UseNpgsql(connectionString);
|
||||||
|
options.UseOpenIddict();
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
return services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string? ResolveConnectionString(string? connectionString, string? appsettingsPath, bool noPrompt)
|
||||||
|
{
|
||||||
|
var targetPath = string.IsNullOrWhiteSpace(appsettingsPath) ? "appsettings.json" : appsettingsPath;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(connectionString))
|
||||||
|
{
|
||||||
|
WriteConnectionString(targetPath, connectionString);
|
||||||
|
return connectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(targetPath))
|
||||||
|
{
|
||||||
|
var config = new ConfigurationBuilder()
|
||||||
|
.AddJsonFile(targetPath)
|
||||||
|
.Build();
|
||||||
|
var existing = config.GetConnectionString("Default");
|
||||||
|
if (!string.IsNullOrWhiteSpace(existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noPrompt)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var host = Prompt("DB host", "localhost");
|
||||||
|
var database = Prompt("DB name", "member_center");
|
||||||
|
var username = Prompt("DB user", "postgres");
|
||||||
|
var password = PromptSecret("DB password");
|
||||||
|
var generated = $"Host={host};Database={database};Username={username};Password={password}";
|
||||||
|
WriteConnectionString(targetPath, generated);
|
||||||
|
return generated;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WriteConnectionString(string path, string connectionString)
|
||||||
|
{
|
||||||
|
JsonNode root;
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
root = JsonNode.Parse(json) ?? new JsonObject();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
root = new JsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
var obj = root as JsonObject ?? new JsonObject();
|
||||||
|
var connectionStrings = obj["ConnectionStrings"] as JsonObject ?? new JsonObject();
|
||||||
|
connectionStrings["Default"] = connectionString;
|
||||||
|
obj["ConnectionStrings"] = connectionStrings;
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||||
|
File.WriteAllText(path, obj.ToJsonString(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task<bool> IsInstalledAsync(MemberCenterDbContext db)
|
||||||
|
{
|
||||||
|
var flag = await db.SystemFlags.FirstOrDefaultAsync(f => f.Key == "installed");
|
||||||
|
return flag is not null && string.Equals(flag.Value, "true", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task SetInstalledFlagAsync(MemberCenterDbContext db)
|
||||||
|
{
|
||||||
|
var flag = await db.SystemFlags.FirstOrDefaultAsync(f => f.Key == "installed");
|
||||||
|
if (flag is null)
|
||||||
|
{
|
||||||
|
db.SystemFlags.Add(new SystemFlag
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Key = "installed",
|
||||||
|
Value = "true",
|
||||||
|
UpdatedAt = DateTimeOffset.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
flag.Value = "true";
|
||||||
|
flag.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task EnsureRoleAsync(RoleManager<ApplicationRole> roleManager, string roleName)
|
||||||
|
{
|
||||||
|
if (!await roleManager.RoleExistsAsync(roleName))
|
||||||
|
{
|
||||||
|
var result = await roleManager.CreateAsync(new ApplicationRole
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = roleName,
|
||||||
|
NormalizedName = roleName.ToUpperInvariant()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(string.Join(Environment.NewLine, result.Errors.Select(e => e.Description)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Prompt(string label, string defaultValue)
|
||||||
|
{
|
||||||
|
Console.Write($"{label} [{defaultValue}]: ");
|
||||||
|
var input = Console.ReadLine();
|
||||||
|
return string.IsNullOrWhiteSpace(input) ? defaultValue : input.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string PromptSecret(string label)
|
||||||
|
{
|
||||||
|
Console.Write($"{label}: ");
|
||||||
|
var password = string.Empty;
|
||||||
|
ConsoleKeyInfo key;
|
||||||
|
while ((key = Console.ReadKey(true)).Key != ConsoleKey.Enter)
|
||||||
|
{
|
||||||
|
if (key.Key == ConsoleKey.Backspace && password.Length > 0)
|
||||||
|
{
|
||||||
|
password = password[..^1];
|
||||||
|
Console.Write("\b \b");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!char.IsControl(key.KeyChar))
|
||||||
|
{
|
||||||
|
password += key.KeyChar;
|
||||||
|
Console.Write("*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
|
return password;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user