Add admin permission checks to admin controllers
This commit is contained in:
parent
fedb011154
commit
2eff12d903
@ -45,6 +45,7 @@
|
|||||||
- `docs/TECH_STACK.md`:技術棧與選型
|
- `docs/TECH_STACK.md`:技術棧與選型
|
||||||
- `docs/INSTALL.md`:安裝、初始化與維運指令
|
- `docs/INSTALL.md`:安裝、初始化與維運指令
|
||||||
- `docs/MEMBER_UPGRADE_PLAN.md`:會員中心下一階段升級規劃(設定畫面、SMTP、Email 驗證、忘記密碼、角色分級)
|
- `docs/MEMBER_UPGRADE_PLAN.md`:會員中心下一階段升級規劃(設定畫面、SMTP、Email 驗證、忘記密碼、角色分級)
|
||||||
|
- `docs/ADMIN_AUTHORIZATION.md`:後台 Role / Permission 權限模型與管理原則
|
||||||
- `docs/TEST_SITE.md`:手動整合測試站說明(API login、redirect login、會員 API happy path)
|
- `docs/TEST_SITE.md`:手動整合測試站說明(API login、redirect login、會員 API happy path)
|
||||||
|
|
||||||
## 專案結構
|
## 專案結構
|
||||||
|
|||||||
90
docs/ADMIN_AUTHORIZATION.md
Normal file
90
docs/ADMIN_AUTHORIZATION.md
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# Admin Authorization
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The admin authorization model separates a user's organizational role from the
|
||||||
|
individual admin capabilities granted to that role. The schema supports multiple
|
||||||
|
roles even though the initial non-superuser role is only `admin`.
|
||||||
|
|
||||||
|
## Core Rules
|
||||||
|
|
||||||
|
1. `superuser` is a hard-coded emergency and governance role.
|
||||||
|
- It does not depend on database permission mappings.
|
||||||
|
- It bypasses all admin permission checks.
|
||||||
|
- Creating a replacement superuser or resetting its password remains an
|
||||||
|
installer responsibility.
|
||||||
|
- Superuser-only account governance actions continue to require the
|
||||||
|
`Superuser` authorization policy.
|
||||||
|
|
||||||
|
2. Normal admin access uses Role-Based Access Control.
|
||||||
|
- A user may have one or more Identity roles through `user_roles`.
|
||||||
|
- A role may have one or more admin permissions through
|
||||||
|
`admin_role_permissions`.
|
||||||
|
- A user's effective permissions are the union of permissions assigned to all
|
||||||
|
of the user's roles.
|
||||||
|
|
||||||
|
3. Roles and role-permission mappings are database data.
|
||||||
|
- Existing ASP.NET Core Identity `roles` and `user_roles` tables remain the
|
||||||
|
source of role membership.
|
||||||
|
- `admin_permissions` stores the known permission catalog.
|
||||||
|
- `admin_role_permissions` maps any Identity role to any admin permission.
|
||||||
|
- Adding roles such as `support`, `auditor`, or `security_admin` does not
|
||||||
|
require another schema change.
|
||||||
|
|
||||||
|
4. Permission definitions are owned by application code.
|
||||||
|
- Permission keys are declared in `AdminPermissions`.
|
||||||
|
- Startup and installer seeding synchronize those known definitions into the
|
||||||
|
database.
|
||||||
|
- The database decides which roles receive known permissions; it must not be
|
||||||
|
used to invent capabilities that have no application implementation.
|
||||||
|
|
||||||
|
5. Permissions are action-oriented.
|
||||||
|
- Controllers require a module permission such as `admin.tenants`.
|
||||||
|
- Actions also require a capability permission such as
|
||||||
|
`admin.tenants.create` or `admin.tenants.delete`.
|
||||||
|
- Operations with materially different authorization boundaries must be
|
||||||
|
separate actions and separate permission keys.
|
||||||
|
|
||||||
|
6. Server-side checks are authoritative.
|
||||||
|
- Admin controllers retain `[Authorize(Policy = "Admin")]` as the outer admin
|
||||||
|
boundary.
|
||||||
|
- Controllers and actions use `AdminPermissionAttribute` for database-backed
|
||||||
|
permission checks.
|
||||||
|
- Non-admin access to `/admin/*` continues to return HTTP 404.
|
||||||
|
- Missing action permissions also return HTTP 404 so inaccessible admin
|
||||||
|
capabilities are not exposed.
|
||||||
|
|
||||||
|
7. Navigation and operation links use the same permission catalog.
|
||||||
|
- Admin menus reference the same `AdminPermissions` constants used by the
|
||||||
|
corresponding controller actions.
|
||||||
|
- Buttons and links are hidden when the current user lacks the target action
|
||||||
|
permission.
|
||||||
|
- UI visibility is only a usability measure; direct requests are always
|
||||||
|
protected by controller/action checks.
|
||||||
|
|
||||||
|
## Initial Mapping
|
||||||
|
|
||||||
|
The initial `admin` role receives every known permission except permissions
|
||||||
|
marked as superuser-only. This preserves current behavior while allowing the
|
||||||
|
mapping to be divided among more roles later.
|
||||||
|
|
||||||
|
The initial superuser-only permissions are:
|
||||||
|
|
||||||
|
- `admin.accounts.set_admin`
|
||||||
|
- `admin.accounts.set_disabled`
|
||||||
|
- `admin.accounts.reset_password`
|
||||||
|
|
||||||
|
No role or permission maintenance UI is included yet. Until one is introduced,
|
||||||
|
the permission catalog is updated in code and role-permission mappings may be
|
||||||
|
managed through controlled database changes or future installer commands.
|
||||||
|
|
||||||
|
## Adding an Admin Capability
|
||||||
|
|
||||||
|
1. Add a constant and definition to `AdminPermissions`.
|
||||||
|
2. Apply the module permission to the controller.
|
||||||
|
3. Apply the capability permission to every relevant action.
|
||||||
|
4. Use the same action permission for related menu items, links, and buttons.
|
||||||
|
5. Add or update role-permission mappings through the approved seed or
|
||||||
|
administrative workflow.
|
||||||
|
6. Verify direct access, navigation visibility, `admin`, and `superuser`
|
||||||
|
behavior.
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace MemberCenter.Api.Authorization;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||||
|
public sealed class AdminPermissionAttribute : TypeFilterAttribute
|
||||||
|
{
|
||||||
|
public AdminPermissionAttribute(string permissionKey)
|
||||||
|
: base(typeof(AdminPermissionFilter))
|
||||||
|
{
|
||||||
|
Arguments = [permissionKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AdminPermissionFilter : IAsyncAuthorizationFilter
|
||||||
|
{
|
||||||
|
private readonly string _permissionKey;
|
||||||
|
private readonly IAdminPermissionChecker _permissionChecker;
|
||||||
|
|
||||||
|
public AdminPermissionFilter(string permissionKey, IAdminPermissionChecker permissionChecker)
|
||||||
|
{
|
||||||
|
_permissionKey = permissionKey;
|
||||||
|
_permissionChecker = permissionChecker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||||
|
{
|
||||||
|
var hasPermission = await _permissionChecker.HasPermissionAsync(
|
||||||
|
context.HttpContext.User,
|
||||||
|
_permissionKey,
|
||||||
|
context.HttpContext.RequestAborted);
|
||||||
|
|
||||||
|
if (!hasPermission)
|
||||||
|
{
|
||||||
|
context.Result = new NotFoundResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
|
using MemberCenter.Api.Authorization;
|
||||||
using MemberCenter.Api.Contracts;
|
using MemberCenter.Api.Contracts;
|
||||||
using MemberCenter.Application.Abstractions;
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using MemberCenter.Application.Constants;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -8,6 +10,7 @@ namespace MemberCenter.Api.Controllers;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("admin/newsletter-lists")]
|
[Route("admin/newsletter-lists")]
|
||||||
[Authorize(Policy = "Admin")]
|
[Authorize(Policy = "Admin")]
|
||||||
|
[AdminPermission(AdminPermissions.NewsletterLists)]
|
||||||
public class AdminNewsletterListsController : ControllerBase
|
public class AdminNewsletterListsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly INewsletterListService _listService;
|
private readonly INewsletterListService _listService;
|
||||||
@ -18,6 +21,7 @@ public class AdminNewsletterListsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
[AdminPermission(AdminPermissions.NewsletterListsIndex)]
|
||||||
public async Task<IActionResult> List()
|
public async Task<IActionResult> List()
|
||||||
{
|
{
|
||||||
var lists = await _listService.ListAsync();
|
var lists = await _listService.ListAsync();
|
||||||
@ -25,6 +29,7 @@ public class AdminNewsletterListsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[AdminPermission(AdminPermissions.NewsletterListsCreate)]
|
||||||
public async Task<IActionResult> Create([FromBody] NewsletterListRequest request)
|
public async Task<IActionResult> Create([FromBody] NewsletterListRequest request)
|
||||||
{
|
{
|
||||||
var list = await _listService.CreateAsync(request.TenantId, request.Name, request.Status);
|
var list = await _listService.CreateAsync(request.TenantId, request.Name, request.Status);
|
||||||
@ -32,6 +37,7 @@ public class AdminNewsletterListsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
|
[AdminPermission(AdminPermissions.NewsletterListsIndex)]
|
||||||
public async Task<IActionResult> Get(Guid id)
|
public async Task<IActionResult> Get(Guid id)
|
||||||
{
|
{
|
||||||
var list = await _listService.GetAsync(id);
|
var list = await _listService.GetAsync(id);
|
||||||
@ -44,6 +50,7 @@ public class AdminNewsletterListsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
[HttpPut("{id:guid}")]
|
||||||
|
[AdminPermission(AdminPermissions.NewsletterListsEdit)]
|
||||||
public async Task<IActionResult> Update(Guid id, [FromBody] NewsletterListRequest request)
|
public async Task<IActionResult> Update(Guid id, [FromBody] NewsletterListRequest request)
|
||||||
{
|
{
|
||||||
var list = await _listService.UpdateAsync(id, request.TenantId, request.Name, request.Status);
|
var list = await _listService.UpdateAsync(id, request.TenantId, request.Name, request.Status);
|
||||||
@ -55,6 +62,7 @@ public class AdminNewsletterListsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
|
[AdminPermission(AdminPermissions.NewsletterListsDelete)]
|
||||||
public async Task<IActionResult> Delete(Guid id)
|
public async Task<IActionResult> Delete(Guid id)
|
||||||
{
|
{
|
||||||
var deleted = await _listService.DeleteAsync(id);
|
var deleted = await _listService.DeleteAsync(id);
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
using MemberCenter.Api.Authorization;
|
||||||
using MemberCenter.Api.Contracts;
|
using MemberCenter.Api.Contracts;
|
||||||
using MemberCenter.Application.Abstractions;
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using MemberCenter.Application.Constants;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using OpenIddict.Abstractions;
|
using OpenIddict.Abstractions;
|
||||||
@ -10,6 +12,7 @@ namespace MemberCenter.Api.Controllers;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("admin/oauth-clients")]
|
[Route("admin/oauth-clients")]
|
||||||
[Authorize(Policy = "Admin")]
|
[Authorize(Policy = "Admin")]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClients)]
|
||||||
public class AdminOAuthClientsController : ControllerBase
|
public class AdminOAuthClientsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IOpenIddictApplicationManager _applicationManager;
|
private readonly IOpenIddictApplicationManager _applicationManager;
|
||||||
@ -24,6 +27,7 @@ public class AdminOAuthClientsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClientsIndex)]
|
||||||
public async Task<IActionResult> List()
|
public async Task<IActionResult> List()
|
||||||
{
|
{
|
||||||
var results = new List<object>();
|
var results = new List<object>();
|
||||||
@ -44,6 +48,7 @@ public class AdminOAuthClientsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClientsCreate)]
|
||||||
public async Task<IActionResult> Create([FromBody] OAuthClientRequest request)
|
public async Task<IActionResult> Create([FromBody] OAuthClientRequest request)
|
||||||
{
|
{
|
||||||
if (!IsValidUsage(request.Usage))
|
if (!IsValidUsage(request.Usage))
|
||||||
@ -103,6 +108,7 @@ public class AdminOAuthClientsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClientsIndex)]
|
||||||
public async Task<IActionResult> Get(string id)
|
public async Task<IActionResult> Get(string id)
|
||||||
{
|
{
|
||||||
var app = await _applicationManager.FindByIdAsync(id);
|
var app = await _applicationManager.FindByIdAsync(id);
|
||||||
@ -123,6 +129,7 @@ public class AdminOAuthClientsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
[HttpPut("{id}")]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClientsEdit)]
|
||||||
public async Task<IActionResult> Update(string id, [FromBody] OAuthClientRequest request)
|
public async Task<IActionResult> Update(string id, [FromBody] OAuthClientRequest request)
|
||||||
{
|
{
|
||||||
if (!IsValidUsage(request.Usage))
|
if (!IsValidUsage(request.Usage))
|
||||||
@ -195,6 +202,7 @@ public class AdminOAuthClientsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClientsDelete)]
|
||||||
public async Task<IActionResult> Delete(string id)
|
public async Task<IActionResult> Delete(string id)
|
||||||
{
|
{
|
||||||
var app = await _applicationManager.FindByIdAsync(id);
|
var app = await _applicationManager.FindByIdAsync(id);
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
using MemberCenter.Api.Authorization;
|
||||||
using MemberCenter.Api.Contracts;
|
using MemberCenter.Api.Contracts;
|
||||||
using MemberCenter.Application.Abstractions;
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using MemberCenter.Application.Constants;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -8,6 +10,7 @@ namespace MemberCenter.Api.Controllers;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("admin/tenants")]
|
[Route("admin/tenants")]
|
||||||
[Authorize(Policy = "Admin")]
|
[Authorize(Policy = "Admin")]
|
||||||
|
[AdminPermission(AdminPermissions.Tenants)]
|
||||||
public class AdminTenantsController : ControllerBase
|
public class AdminTenantsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ITenantService _tenantService;
|
private readonly ITenantService _tenantService;
|
||||||
@ -18,6 +21,7 @@ public class AdminTenantsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
[AdminPermission(AdminPermissions.TenantsIndex)]
|
||||||
public async Task<IActionResult> List()
|
public async Task<IActionResult> List()
|
||||||
{
|
{
|
||||||
var tenants = await _tenantService.ListAsync();
|
var tenants = await _tenantService.ListAsync();
|
||||||
@ -25,6 +29,7 @@ public class AdminTenantsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[AdminPermission(AdminPermissions.TenantsCreate)]
|
||||||
public async Task<IActionResult> Create([FromBody] TenantRequest request)
|
public async Task<IActionResult> Create([FromBody] TenantRequest request)
|
||||||
{
|
{
|
||||||
var tenant = await _tenantService.CreateAsync(request.Name, request.Domains, request.Status, request.SendEngineWebhookClientId);
|
var tenant = await _tenantService.CreateAsync(request.Name, request.Domains, request.Status, request.SendEngineWebhookClientId);
|
||||||
@ -32,6 +37,7 @@ public class AdminTenantsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
|
[AdminPermission(AdminPermissions.TenantsIndex)]
|
||||||
public async Task<IActionResult> Get(Guid id)
|
public async Task<IActionResult> Get(Guid id)
|
||||||
{
|
{
|
||||||
var tenant = await _tenantService.GetAsync(id);
|
var tenant = await _tenantService.GetAsync(id);
|
||||||
@ -44,6 +50,7 @@ public class AdminTenantsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
[HttpPut("{id:guid}")]
|
||||||
|
[AdminPermission(AdminPermissions.TenantsEdit)]
|
||||||
public async Task<IActionResult> Update(Guid id, [FromBody] TenantRequest request)
|
public async Task<IActionResult> Update(Guid id, [FromBody] TenantRequest request)
|
||||||
{
|
{
|
||||||
var tenant = await _tenantService.UpdateAsync(id, request.Name, request.Domains, request.Status, request.SendEngineWebhookClientId);
|
var tenant = await _tenantService.UpdateAsync(id, request.Name, request.Domains, request.Status, request.SendEngineWebhookClientId);
|
||||||
@ -55,6 +62,7 @@ public class AdminTenantsController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
|
[AdminPermission(AdminPermissions.TenantsDelete)]
|
||||||
public async Task<IActionResult> Delete(Guid id)
|
public async Task<IActionResult> Delete(Guid id)
|
||||||
{
|
{
|
||||||
var deleted = await _tenantService.DeleteAsync(id);
|
var deleted = await _tenantService.DeleteAsync(id);
|
||||||
|
|||||||
@ -199,6 +199,9 @@ builder.Services.AddScoped<INewsletterListService, NewsletterListService>();
|
|||||||
builder.Services.AddScoped<IAccountProvisioningService, AccountProvisioningService>();
|
builder.Services.AddScoped<IAccountProvisioningService, AccountProvisioningService>();
|
||||||
builder.Services.AddScoped<IProfileService, ProfileService>();
|
builder.Services.AddScoped<IProfileService, ProfileService>();
|
||||||
builder.Services.AddScoped<IAuthResourceRegistryService, AuthResourceRegistryService>();
|
builder.Services.AddScoped<IAuthResourceRegistryService, AuthResourceRegistryService>();
|
||||||
|
builder.Services.AddScoped<AdminPermissionService>();
|
||||||
|
builder.Services.AddScoped<IAdminPermissionChecker>(services => services.GetRequiredService<AdminPermissionService>());
|
||||||
|
builder.Services.AddScoped<IAdminPermissionSeeder>(services => services.GetRequiredService<AdminPermissionService>());
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.Configure<SendEngineWebhookOptions>(builder.Configuration.GetSection("SendEngine"));
|
builder.Services.Configure<SendEngineWebhookOptions>(builder.Configuration.GetSection("SendEngine"));
|
||||||
builder.Services.AddHttpClient<SendEngineWebhookPublisher>();
|
builder.Services.AddHttpClient<SendEngineWebhookPublisher>();
|
||||||
@ -207,6 +210,7 @@ builder.Services.AddScoped<ISendEngineWebhookPublisher, SendEngineWebhookPublish
|
|||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
await EnsureAuthRegistryDefaultsAsync(app.Services);
|
await EnsureAuthRegistryDefaultsAsync(app.Services);
|
||||||
|
await EnsureAdminPermissionDefaultsAsync(app.Services);
|
||||||
|
|
||||||
app.UseForwardedHeaders();
|
app.UseForwardedHeaders();
|
||||||
if (!string.IsNullOrWhiteSpace(pathBase))
|
if (!string.IsNullOrWhiteSpace(pathBase))
|
||||||
@ -342,3 +346,10 @@ static async Task EnsureAuthRegistryDefaultsAsync(IServiceProvider services)
|
|||||||
var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
|
var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
|
||||||
await registry.EnsureDefaultsAsync();
|
await registry.EnsureDefaultsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async Task EnsureAdminPermissionDefaultsAsync(IServiceProvider services)
|
||||||
|
{
|
||||||
|
await using var scope = services.CreateAsyncScope();
|
||||||
|
var seeder = scope.ServiceProvider.GetRequiredService<IAdminPermissionSeeder>();
|
||||||
|
await seeder.EnsureDefaultsAsync();
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace MemberCenter.Application.Abstractions;
|
||||||
|
|
||||||
|
public interface IAdminPermissionChecker
|
||||||
|
{
|
||||||
|
Task<bool> HasPermissionAsync(ClaimsPrincipal user, string permissionKey, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
namespace MemberCenter.Application.Abstractions;
|
||||||
|
|
||||||
|
public interface IAdminPermissionSeeder
|
||||||
|
{
|
||||||
|
Task EnsureDefaultsAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
94
src/MemberCenter.Application/Constants/AdminPermissions.cs
Normal file
94
src/MemberCenter.Application/Constants/AdminPermissions.cs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
namespace MemberCenter.Application.Constants;
|
||||||
|
|
||||||
|
public static class AdminPermissions
|
||||||
|
{
|
||||||
|
public const string AdminRole = "admin";
|
||||||
|
public const string SuperuserRole = "superuser";
|
||||||
|
|
||||||
|
public const string Home = "admin.home";
|
||||||
|
public const string Accounts = "admin.accounts";
|
||||||
|
public const string AccountsIndex = "admin.accounts.index";
|
||||||
|
public const string AccountsSetAdmin = "admin.accounts.set_admin";
|
||||||
|
public const string AccountsSetDisabled = "admin.accounts.set_disabled";
|
||||||
|
public const string AccountsResetPassword = "admin.accounts.reset_password";
|
||||||
|
public const string Tenants = "admin.tenants";
|
||||||
|
public const string TenantsIndex = "admin.tenants.index";
|
||||||
|
public const string TenantsCreate = "admin.tenants.create";
|
||||||
|
public const string TenantsEdit = "admin.tenants.edit";
|
||||||
|
public const string TenantsDelete = "admin.tenants.delete";
|
||||||
|
public const string NewsletterLists = "admin.newsletter_lists";
|
||||||
|
public const string NewsletterListsIndex = "admin.newsletter_lists.index";
|
||||||
|
public const string NewsletterListsCreate = "admin.newsletter_lists.create";
|
||||||
|
public const string NewsletterListsEdit = "admin.newsletter_lists.edit";
|
||||||
|
public const string NewsletterListsDelete = "admin.newsletter_lists.delete";
|
||||||
|
public const string Subscriptions = "admin.subscriptions";
|
||||||
|
public const string SubscriptionsIndex = "admin.subscriptions.index";
|
||||||
|
public const string SubscriptionsExport = "admin.subscriptions.export";
|
||||||
|
public const string OAuthClients = "admin.oauth_clients";
|
||||||
|
public const string OAuthClientsIndex = "admin.oauth_clients.index";
|
||||||
|
public const string OAuthClientsCreate = "admin.oauth_clients.create";
|
||||||
|
public const string OAuthClientsEdit = "admin.oauth_clients.edit";
|
||||||
|
public const string OAuthClientsDelete = "admin.oauth_clients.delete";
|
||||||
|
public const string OAuthClientsRotateSecret = "admin.oauth_clients.rotate_secret";
|
||||||
|
public const string AuditLogs = "admin.audit_logs";
|
||||||
|
public const string AuditLogsIndex = "admin.audit_logs.index";
|
||||||
|
public const string Security = "admin.security";
|
||||||
|
public const string SecurityIndex = "admin.security.index";
|
||||||
|
public const string SecuritySave = "admin.security.save";
|
||||||
|
public const string SecurityTestEmail = "admin.security.test_email";
|
||||||
|
public const string Blacklist = "admin.blacklist";
|
||||||
|
public const string BlacklistIndex = "admin.blacklist.index";
|
||||||
|
public const string BlacklistCreate = "admin.blacklist.create";
|
||||||
|
|
||||||
|
public static readonly IReadOnlyList<AdminPermissionDefinition> All =
|
||||||
|
[
|
||||||
|
new(Home, "Overview", "Open the admin overview."),
|
||||||
|
|
||||||
|
new(Accounts, "Accounts", "Open account governance."),
|
||||||
|
new(AccountsIndex, "Accounts / View", "View account governance."),
|
||||||
|
new(AccountsSetAdmin, "Accounts / Grant admin", "Grant or remove admin role.", SuperuserOnly: true),
|
||||||
|
new(AccountsSetDisabled, "Accounts / Disable account", "Disable or enable member accounts.", SuperuserOnly: true),
|
||||||
|
new(AccountsResetPassword, "Accounts / Reset password", "Reset member passwords.", SuperuserOnly: true),
|
||||||
|
|
||||||
|
new(Tenants, "Tenants", "Open tenant management."),
|
||||||
|
new(TenantsIndex, "Tenants / View", "View tenants."),
|
||||||
|
new(TenantsCreate, "Tenants / Create", "Create tenants."),
|
||||||
|
new(TenantsEdit, "Tenants / Edit", "Edit tenants."),
|
||||||
|
new(TenantsDelete, "Tenants / Delete", "Delete tenants."),
|
||||||
|
|
||||||
|
new(NewsletterLists, "Newsletter lists", "Open newsletter list management."),
|
||||||
|
new(NewsletterListsIndex, "Newsletter lists / View", "View newsletter lists."),
|
||||||
|
new(NewsletterListsCreate, "Newsletter lists / Create", "Create newsletter lists."),
|
||||||
|
new(NewsletterListsEdit, "Newsletter lists / Edit", "Edit newsletter lists."),
|
||||||
|
new(NewsletterListsDelete, "Newsletter lists / Delete", "Delete newsletter lists."),
|
||||||
|
|
||||||
|
new(Subscriptions, "Subscriptions", "Open subscription management."),
|
||||||
|
new(SubscriptionsIndex, "Subscriptions / View", "View subscriptions."),
|
||||||
|
new(SubscriptionsExport, "Subscriptions / Export", "Export subscriptions."),
|
||||||
|
|
||||||
|
new(OAuthClients, "OAuth clients", "Open OAuth client management."),
|
||||||
|
new(OAuthClientsIndex, "OAuth clients / View", "View OAuth clients."),
|
||||||
|
new(OAuthClientsCreate, "OAuth clients / Create", "Create OAuth clients."),
|
||||||
|
new(OAuthClientsEdit, "OAuth clients / Edit", "Edit OAuth clients."),
|
||||||
|
new(OAuthClientsDelete, "OAuth clients / Delete", "Delete OAuth clients."),
|
||||||
|
new(OAuthClientsRotateSecret, "OAuth clients / Rotate secret", "Rotate OAuth client secrets."),
|
||||||
|
|
||||||
|
new(AuditLogs, "Audit logs", "Open audit logs."),
|
||||||
|
new(AuditLogsIndex, "Audit logs / View", "View audit logs."),
|
||||||
|
|
||||||
|
new(Security, "Security", "Open security settings."),
|
||||||
|
new(SecurityIndex, "Security / View", "View security settings."),
|
||||||
|
new(SecuritySave, "Security / Save", "Save security settings."),
|
||||||
|
new(SecurityTestEmail, "Security / Test email", "Send security settings test email."),
|
||||||
|
|
||||||
|
new(Blacklist, "Blacklist", "Open email blacklist."),
|
||||||
|
new(BlacklistIndex, "Blacklist / View", "View email blacklist."),
|
||||||
|
new(BlacklistCreate, "Blacklist / Create", "Add or update email blacklist entries.")
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record AdminPermissionDefinition(
|
||||||
|
string Key,
|
||||||
|
string Name,
|
||||||
|
string Description,
|
||||||
|
bool SuperuserOnly = false);
|
||||||
14
src/MemberCenter.Domain/Entities/AdminPermission.cs
Normal file
14
src/MemberCenter.Domain/Entities/AdminPermission.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace MemberCenter.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class AdminPermission
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Key { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public bool IsEnabled { get; set; } = true;
|
||||||
|
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
public List<AdminRolePermission> RolePermissions { get; set; } = new();
|
||||||
|
}
|
||||||
10
src/MemberCenter.Domain/Entities/AdminRolePermission.cs
Normal file
10
src/MemberCenter.Domain/Entities/AdminRolePermission.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace MemberCenter.Domain.Entities;
|
||||||
|
|
||||||
|
public sealed class AdminRolePermission
|
||||||
|
{
|
||||||
|
public Guid RoleId { get; set; }
|
||||||
|
public Guid PermissionId { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
public AdminPermission? Permission { get; set; }
|
||||||
|
}
|
||||||
@ -27,6 +27,8 @@ public class MemberCenterDbContext
|
|||||||
public DbSet<AuthResource> AuthResources => Set<AuthResource>();
|
public DbSet<AuthResource> AuthResources => Set<AuthResource>();
|
||||||
public DbSet<AuthResourceScope> AuthResourceScopes => Set<AuthResourceScope>();
|
public DbSet<AuthResourceScope> AuthResourceScopes => Set<AuthResourceScope>();
|
||||||
public DbSet<AuthClientUsagePermission> AuthClientUsagePermissions => Set<AuthClientUsagePermission>();
|
public DbSet<AuthClientUsagePermission> AuthClientUsagePermissions => Set<AuthClientUsagePermission>();
|
||||||
|
public DbSet<AdminPermission> AdminPermissions => Set<AdminPermission>();
|
||||||
|
public DbSet<AdminRolePermission> AdminRolePermissions => Set<AdminRolePermission>();
|
||||||
public DbSet<FileAccessDownloadToken> FileAccessDownloadTokens => Set<FileAccessDownloadToken>();
|
public DbSet<FileAccessDownloadToken> FileAccessDownloadTokens => Set<FileAccessDownloadToken>();
|
||||||
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; } = null!;
|
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; } = null!;
|
||||||
|
|
||||||
@ -248,6 +250,34 @@ public class MemberCenterDbContext
|
|||||||
entity.HasIndex(x => new { x.Usage, x.Scope }).IsUnique();
|
entity.HasIndex(x => new { x.Usage, x.Scope }).IsUnique();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Entity<AdminPermission>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("admin_permissions");
|
||||||
|
entity.HasKey(x => x.Id);
|
||||||
|
entity.Property(x => x.Key).IsRequired().HasMaxLength(200);
|
||||||
|
entity.Property(x => x.Name).IsRequired().HasMaxLength(200);
|
||||||
|
entity.Property(x => x.Description).HasMaxLength(1000);
|
||||||
|
entity.Property(x => x.IsEnabled).HasDefaultValue(true);
|
||||||
|
entity.Property(x => x.CreatedAt).HasDefaultValueSql("now()");
|
||||||
|
entity.Property(x => x.UpdatedAt).HasDefaultValueSql("now()");
|
||||||
|
entity.HasIndex(x => x.Key).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Entity<AdminRolePermission>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("admin_role_permissions");
|
||||||
|
entity.HasKey(x => new { x.RoleId, x.PermissionId });
|
||||||
|
entity.Property(x => x.CreatedAt).HasDefaultValueSql("now()");
|
||||||
|
entity.HasOne<ApplicationRole>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.RoleId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
entity.HasOne(x => x.Permission)
|
||||||
|
.WithMany(x => x.RolePermissions)
|
||||||
|
.HasForeignKey(x => x.PermissionId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
builder.Entity<FileAccessDownloadToken>(entity =>
|
builder.Entity<FileAccessDownloadToken>(entity =>
|
||||||
{
|
{
|
||||||
entity.ToTable("file_access_download_tokens");
|
entity.ToTable("file_access_download_tokens");
|
||||||
|
|||||||
1406
src/MemberCenter.Infrastructure/Persistence/Migrations/20260703055656_AddAdminPermissions.Designer.cs
generated
Normal file
1406
src/MemberCenter.Infrastructure/Persistence/Migrations/20260703055656_AddAdminPermissions.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,78 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MemberCenter.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAdminPermissions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "admin_permissions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Key = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||||
|
IsEnabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_admin_permissions", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "admin_role_permissions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
RoleId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
PermissionId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_admin_role_permissions", x => new { x.RoleId, x.PermissionId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_admin_role_permissions_admin_permissions_PermissionId",
|
||||||
|
column: x => x.PermissionId,
|
||||||
|
principalTable: "admin_permissions",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_admin_role_permissions_roles_RoleId",
|
||||||
|
column: x => x.RoleId,
|
||||||
|
principalTable: "roles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_admin_permissions_Key",
|
||||||
|
table: "admin_permissions",
|
||||||
|
column: "Key",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_admin_role_permissions_PermissionId",
|
||||||
|
table: "admin_role_permissions",
|
||||||
|
column: "PermissionId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "admin_role_permissions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "admin_permissions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,6 +23,69 @@ namespace MemberCenter.Infrastructure.Persistence.Migrations
|
|||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("MemberCenter.Domain.Entities.AdminPermission", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("character varying(1000)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("admin_permissions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MemberCenter.Domain.Entities.AdminRolePermission", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("PermissionId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.HasKey("RoleId", "PermissionId");
|
||||||
|
|
||||||
|
b.HasIndex("PermissionId");
|
||||||
|
|
||||||
|
b.ToTable("admin_role_permissions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("MemberCenter.Domain.Entities.AuditLog", b =>
|
modelBuilder.Entity("MemberCenter.Domain.Entities.AuditLog", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -1113,6 +1176,23 @@ namespace MemberCenter.Infrastructure.Persistence.Migrations
|
|||||||
b.ToTable("OpenIddictTokens", (string)null);
|
b.ToTable("OpenIddictTokens", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MemberCenter.Domain.Entities.AdminRolePermission", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MemberCenter.Domain.Entities.AdminPermission", "Permission")
|
||||||
|
.WithMany("RolePermissions")
|
||||||
|
.HasForeignKey("PermissionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("MemberCenter.Infrastructure.Identity.ApplicationRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Permission");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("MemberCenter.Domain.Entities.AuthResourceScope", b =>
|
modelBuilder.Entity("MemberCenter.Domain.Entities.AuthResourceScope", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("MemberCenter.Domain.Entities.AuthResource", "Resource")
|
b.HasOne("MemberCenter.Domain.Entities.AuthResource", "Resource")
|
||||||
@ -1279,6 +1359,11 @@ namespace MemberCenter.Infrastructure.Persistence.Migrations
|
|||||||
b.Navigation("Authorization");
|
b.Navigation("Authorization");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MemberCenter.Domain.Entities.AdminPermission", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("RolePermissions");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("MemberCenter.Domain.Entities.AuthResource", b =>
|
modelBuilder.Entity("MemberCenter.Domain.Entities.AuthResource", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Scopes");
|
b.Navigation("Scopes");
|
||||||
|
|||||||
@ -0,0 +1,151 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using MemberCenter.Application.Constants;
|
||||||
|
using MemberCenter.Domain.Entities;
|
||||||
|
using MemberCenter.Infrastructure.Identity;
|
||||||
|
using MemberCenter.Infrastructure.Persistence;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace MemberCenter.Infrastructure.Services;
|
||||||
|
|
||||||
|
public sealed class AdminPermissionService : IAdminPermissionChecker, IAdminPermissionSeeder
|
||||||
|
{
|
||||||
|
private readonly MemberCenterDbContext _dbContext;
|
||||||
|
private readonly RoleManager<ApplicationRole> _roleManager;
|
||||||
|
private Task<HashSet<string>>? _effectivePermissions;
|
||||||
|
|
||||||
|
public AdminPermissionService(MemberCenterDbContext dbContext, RoleManager<ApplicationRole> roleManager)
|
||||||
|
{
|
||||||
|
_dbContext = dbContext;
|
||||||
|
_roleManager = roleManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HasPermissionAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
string permissionKey,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (user.Identity?.IsAuthenticated != true)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.IsInRole(AdminPermissions.SuperuserRole))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var roleNames = user.FindAll(ClaimTypes.Role)
|
||||||
|
.Select(claim => claim.Value)
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||||
|
.Select(value => value.ToUpperInvariant())
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (roleNames.Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_effectivePermissions ??= LoadEffectivePermissionsAsync(roleNames, cancellationToken);
|
||||||
|
var effectivePermissions = await _effectivePermissions;
|
||||||
|
return effectivePermissions.Contains(permissionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HashSet<string>> LoadEffectivePermissionsAsync(
|
||||||
|
IReadOnlyCollection<string> normalizedRoleNames,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var permissions = await _dbContext.AdminRolePermissions
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(rolePermission => rolePermission.Permission != null && rolePermission.Permission.IsEnabled)
|
||||||
|
.Join(
|
||||||
|
_dbContext.Roles.AsNoTracking(),
|
||||||
|
rolePermission => rolePermission.RoleId,
|
||||||
|
role => role.Id,
|
||||||
|
(rolePermission, role) => new { rolePermission.Permission!.Key, role.NormalizedName })
|
||||||
|
.Where(item => item.NormalizedName != null && normalizedRoleNames.Contains(item.NormalizedName))
|
||||||
|
.Select(item => item.Key)
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return permissions.ToHashSet(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnsureDefaultsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var permissionsByKey = await _dbContext.AdminPermissions
|
||||||
|
.ToDictionaryAsync(permission => permission.Key, StringComparer.Ordinal, cancellationToken);
|
||||||
|
|
||||||
|
foreach (var definition in AdminPermissions.All)
|
||||||
|
{
|
||||||
|
if (!permissionsByKey.TryGetValue(definition.Key, out var permission))
|
||||||
|
{
|
||||||
|
permission = new AdminPermission
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Key = definition.Key
|
||||||
|
};
|
||||||
|
_dbContext.AdminPermissions.Add(permission);
|
||||||
|
permissionsByKey[definition.Key] = permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
permission.Name = definition.Name;
|
||||||
|
permission.Description = definition.Description;
|
||||||
|
permission.IsEnabled = true;
|
||||||
|
permission.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
await EnsureRoleExistsAsync(AdminPermissions.AdminRole);
|
||||||
|
await EnsureRoleExistsAsync(AdminPermissions.SuperuserRole);
|
||||||
|
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
var adminRole = await _roleManager.FindByNameAsync(AdminPermissions.AdminRole)
|
||||||
|
?? throw new InvalidOperationException("Admin role was not created.");
|
||||||
|
|
||||||
|
var currentPermissionIds = (await _dbContext.AdminRolePermissions
|
||||||
|
.Where(rolePermission => rolePermission.RoleId == adminRole.Id)
|
||||||
|
.Select(rolePermission => rolePermission.PermissionId)
|
||||||
|
.ToListAsync(cancellationToken))
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
foreach (var definition in AdminPermissions.All.Where(permission => !permission.SuperuserOnly))
|
||||||
|
{
|
||||||
|
var permission = permissionsByKey[definition.Key];
|
||||||
|
if (currentPermissionIds.Contains(permission.Id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dbContext.AdminRolePermissions.Add(new AdminRolePermission
|
||||||
|
{
|
||||||
|
RoleId = adminRole.Id,
|
||||||
|
PermissionId = permission.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureRoleExistsAsync(string roleName)
|
||||||
|
{
|
||||||
|
if (await _roleManager.RoleExistsAsync(roleName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _roleManager.CreateAsync(new ApplicationRole
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = roleName,
|
||||||
|
NormalizedName = roleName.ToUpperInvariant()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(string.Join("; ", result.Errors.Select(error => error.Description)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -104,6 +104,8 @@ initCommand.SetHandler(async (string? connectionString, string? appsettings, boo
|
|||||||
await db.Database.MigrateAsync();
|
await db.Database.MigrateAsync();
|
||||||
var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
|
var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
|
||||||
await registry.EnsureDefaultsAsync();
|
await registry.EnsureDefaultsAsync();
|
||||||
|
var adminPermissionSeeder = scope.ServiceProvider.GetRequiredService<IAdminPermissionSeeder>();
|
||||||
|
await adminPermissionSeeder.EnsureDefaultsAsync();
|
||||||
|
|
||||||
await EnsureRoleAsync(roleManager, "superuser");
|
await EnsureRoleAsync(roleManager, "superuser");
|
||||||
await EnsureRoleAsync(roleManager, "admin");
|
await EnsureRoleAsync(roleManager, "admin");
|
||||||
@ -274,6 +276,8 @@ migrateCommand.SetHandler(async (string? connectionString, string? appsettings,
|
|||||||
await db.Database.MigrateAsync();
|
await db.Database.MigrateAsync();
|
||||||
var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
|
var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
|
||||||
await registry.EnsureDefaultsAsync();
|
await registry.EnsureDefaultsAsync();
|
||||||
|
var adminPermissionSeeder = scope.ServiceProvider.GetRequiredService<IAdminPermissionSeeder>();
|
||||||
|
await adminPermissionSeeder.EnsureDefaultsAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -300,6 +304,8 @@ syncOAuthClientsCommand.SetHandler(async (string? connectionString, string? apps
|
|||||||
await using var scope = services.CreateAsyncScope();
|
await using var scope = services.CreateAsyncScope();
|
||||||
var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
|
var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
|
||||||
await registry.EnsureDefaultsAsync();
|
await registry.EnsureDefaultsAsync();
|
||||||
|
var adminPermissionSeeder = scope.ServiceProvider.GetRequiredService<IAdminPermissionSeeder>();
|
||||||
|
await adminPermissionSeeder.EnsureDefaultsAsync();
|
||||||
|
|
||||||
var applicationManager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
|
var applicationManager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
|
||||||
var applications = new List<object>();
|
var applications = new List<object>();
|
||||||
@ -374,6 +380,9 @@ static IServiceProvider BuildServices(string connectionString)
|
|||||||
.AddDefaultTokenProviders();
|
.AddDefaultTokenProviders();
|
||||||
|
|
||||||
services.AddScoped<IAuthResourceRegistryService, AuthResourceRegistryService>();
|
services.AddScoped<IAuthResourceRegistryService, AuthResourceRegistryService>();
|
||||||
|
services.AddScoped<AdminPermissionService>();
|
||||||
|
services.AddScoped<IAdminPermissionChecker>(provider => provider.GetRequiredService<AdminPermissionService>());
|
||||||
|
services.AddScoped<IAdminPermissionSeeder>(provider => provider.GetRequiredService<AdminPermissionService>());
|
||||||
|
|
||||||
return services.BuildServiceProvider();
|
return services.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
using MemberCenter.Application.Abstractions;
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using MemberCenter.Application.Constants;
|
||||||
using MemberCenter.Infrastructure.Identity;
|
using MemberCenter.Infrastructure.Identity;
|
||||||
using MemberCenter.Web.Areas.Admin.Models;
|
using MemberCenter.Web.Areas.Admin.Models;
|
||||||
|
using MemberCenter.Web.Authorization;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -9,6 +11,7 @@ namespace MemberCenter.Web.Areas.Admin.Controllers;
|
|||||||
|
|
||||||
[Area("Admin")]
|
[Area("Admin")]
|
||||||
[Authorize(Policy = "Admin")]
|
[Authorize(Policy = "Admin")]
|
||||||
|
[AdminPermission(AdminPermissions.Accounts)]
|
||||||
[Route("admin/accounts")]
|
[Route("admin/accounts")]
|
||||||
public class AccountsController : Controller
|
public class AccountsController : Controller
|
||||||
{
|
{
|
||||||
@ -24,6 +27,7 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
|
[AdminPermission(AdminPermissions.AccountsIndex)]
|
||||||
public async Task<IActionResult> Index(string? search = null, string? role = null, string? status = null, string? verified = null)
|
public async Task<IActionResult> Index(string? search = null, string? role = null, string? status = null, string? verified = null)
|
||||||
{
|
{
|
||||||
var items = await _accountGovernanceService.ListUsersAsync(search);
|
var items = await _accountGovernanceService.ListUsersAsync(search);
|
||||||
@ -42,6 +46,7 @@ public class AccountsController : Controller
|
|||||||
|
|
||||||
[HttpPost("{id:guid}/admin")]
|
[HttpPost("{id:guid}/admin")]
|
||||||
[Authorize(Policy = "Superuser")]
|
[Authorize(Policy = "Superuser")]
|
||||||
|
[AdminPermission(AdminPermissions.AccountsSetAdmin)]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> SetAdmin(Guid id, bool enabled, string? search = null, string? role = null, string? status = null, string? verified = null)
|
public async Task<IActionResult> SetAdmin(Guid id, bool enabled, string? search = null, string? role = null, string? status = null, string? verified = null)
|
||||||
{
|
{
|
||||||
@ -66,6 +71,7 @@ public class AccountsController : Controller
|
|||||||
|
|
||||||
[HttpPost("{id:guid}/disabled")]
|
[HttpPost("{id:guid}/disabled")]
|
||||||
[Authorize(Policy = "Superuser")]
|
[Authorize(Policy = "Superuser")]
|
||||||
|
[AdminPermission(AdminPermissions.AccountsSetDisabled)]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> SetDisabled(Guid id, bool disabled, string? search = null, string? role = null, string? status = null, string? verified = null)
|
public async Task<IActionResult> SetDisabled(Guid id, bool disabled, string? search = null, string? role = null, string? status = null, string? verified = null)
|
||||||
{
|
{
|
||||||
@ -90,6 +96,7 @@ public class AccountsController : Controller
|
|||||||
|
|
||||||
[HttpPost("{id:guid}/password-reset")]
|
[HttpPost("{id:guid}/password-reset")]
|
||||||
[Authorize(Policy = "Superuser")]
|
[Authorize(Policy = "Superuser")]
|
||||||
|
[AdminPermission(AdminPermissions.AccountsResetPassword)]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> ResetPassword(Guid id, string newPassword, string? search = null, string? role = null, string? status = null, string? verified = null)
|
public async Task<IActionResult> ResetPassword(Guid id, string newPassword, string? search = null, string? role = null, string? status = null, string? verified = null)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
using MemberCenter.Application.Abstractions;
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using MemberCenter.Application.Constants;
|
||||||
|
using MemberCenter.Web.Authorization;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -6,6 +8,7 @@ namespace MemberCenter.Web.Areas.Admin.Controllers;
|
|||||||
|
|
||||||
[Area("Admin")]
|
[Area("Admin")]
|
||||||
[Authorize(Policy = "Admin")]
|
[Authorize(Policy = "Admin")]
|
||||||
|
[AdminPermission(AdminPermissions.AuditLogs)]
|
||||||
[Route("admin/audit-logs")]
|
[Route("admin/audit-logs")]
|
||||||
public class AuditLogsController : Controller
|
public class AuditLogsController : Controller
|
||||||
{
|
{
|
||||||
@ -17,6 +20,7 @@ public class AuditLogsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
|
[AdminPermission(AdminPermissions.AuditLogsIndex)]
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
var logs = await _auditLogService.ListAsync();
|
var logs = await _auditLogService.ListAsync();
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
using MemberCenter.Application.Abstractions;
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using MemberCenter.Application.Constants;
|
||||||
|
using MemberCenter.Web.Authorization;
|
||||||
using MemberCenter.Web.Models.Admin;
|
using MemberCenter.Web.Models.Admin;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -7,6 +9,7 @@ namespace MemberCenter.Web.Areas.Admin.Controllers;
|
|||||||
|
|
||||||
[Area("Admin")]
|
[Area("Admin")]
|
||||||
[Authorize(Policy = "Admin")]
|
[Authorize(Policy = "Admin")]
|
||||||
|
[AdminPermission(AdminPermissions.Blacklist)]
|
||||||
[Route("admin/blacklist")]
|
[Route("admin/blacklist")]
|
||||||
public class BlacklistController : Controller
|
public class BlacklistController : Controller
|
||||||
{
|
{
|
||||||
@ -18,6 +21,7 @@ public class BlacklistController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
|
[AdminPermission(AdminPermissions.BlacklistIndex)]
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
var items = await _blacklistService.ListAsync();
|
var items = await _blacklistService.ListAsync();
|
||||||
@ -25,12 +29,14 @@ public class BlacklistController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("create")]
|
[HttpGet("create")]
|
||||||
|
[AdminPermission(AdminPermissions.BlacklistCreate)]
|
||||||
public IActionResult Create()
|
public IActionResult Create()
|
||||||
{
|
{
|
||||||
return View(new EmailBlacklistFormViewModel());
|
return View(new EmailBlacklistFormViewModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("create")]
|
[HttpPost("create")]
|
||||||
|
[AdminPermission(AdminPermissions.BlacklistCreate)]
|
||||||
public async Task<IActionResult> Create(EmailBlacklistFormViewModel model)
|
public async Task<IActionResult> Create(EmailBlacklistFormViewModel model)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
using MemberCenter.Application.Constants;
|
||||||
|
using MemberCenter.Web.Authorization;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -5,10 +7,12 @@ namespace MemberCenter.Web.Areas.Admin.Controllers;
|
|||||||
|
|
||||||
[Area("Admin")]
|
[Area("Admin")]
|
||||||
[Authorize(Policy = "Admin")]
|
[Authorize(Policy = "Admin")]
|
||||||
|
[AdminPermission(AdminPermissions.Home)]
|
||||||
[Route("admin")]
|
[Route("admin")]
|
||||||
public sealed class HomeController : Controller
|
public sealed class HomeController : Controller
|
||||||
{
|
{
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
|
[AdminPermission(AdminPermissions.Home)]
|
||||||
public IActionResult Index()
|
public IActionResult Index()
|
||||||
{
|
{
|
||||||
return View();
|
return View();
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
using MemberCenter.Application.Abstractions;
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using MemberCenter.Application.Constants;
|
||||||
|
using MemberCenter.Web.Authorization;
|
||||||
using MemberCenter.Web.Models.Admin;
|
using MemberCenter.Web.Models.Admin;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -7,6 +9,7 @@ namespace MemberCenter.Web.Areas.Admin.Controllers;
|
|||||||
|
|
||||||
[Area("Admin")]
|
[Area("Admin")]
|
||||||
[Authorize(Policy = "Admin")]
|
[Authorize(Policy = "Admin")]
|
||||||
|
[AdminPermission(AdminPermissions.NewsletterLists)]
|
||||||
[Route("admin/newsletter-lists")]
|
[Route("admin/newsletter-lists")]
|
||||||
public class NewsletterListsController : Controller
|
public class NewsletterListsController : Controller
|
||||||
{
|
{
|
||||||
@ -20,6 +23,7 @@ public class NewsletterListsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
|
[AdminPermission(AdminPermissions.NewsletterListsIndex)]
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
var lists = await _listService.ListAsync();
|
var lists = await _listService.ListAsync();
|
||||||
@ -27,6 +31,7 @@ public class NewsletterListsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("create")]
|
[HttpGet("create")]
|
||||||
|
[AdminPermission(AdminPermissions.NewsletterListsCreate)]
|
||||||
public async Task<IActionResult> Create()
|
public async Task<IActionResult> Create()
|
||||||
{
|
{
|
||||||
var tenants = await _tenantService.ListAsync();
|
var tenants = await _tenantService.ListAsync();
|
||||||
@ -37,6 +42,7 @@ public class NewsletterListsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("create")]
|
[HttpPost("create")]
|
||||||
|
[AdminPermission(AdminPermissions.NewsletterListsCreate)]
|
||||||
public async Task<IActionResult> Create(NewsletterListFormViewModel model)
|
public async Task<IActionResult> Create(NewsletterListFormViewModel model)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
@ -50,6 +56,7 @@ public class NewsletterListsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("edit/{id:guid}")]
|
[HttpGet("edit/{id:guid}")]
|
||||||
|
[AdminPermission(AdminPermissions.NewsletterListsEdit)]
|
||||||
public async Task<IActionResult> Edit(Guid id)
|
public async Task<IActionResult> Edit(Guid id)
|
||||||
{
|
{
|
||||||
var list = await _listService.GetAsync(id);
|
var list = await _listService.GetAsync(id);
|
||||||
@ -70,6 +77,7 @@ public class NewsletterListsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("edit/{id:guid}")]
|
[HttpPost("edit/{id:guid}")]
|
||||||
|
[AdminPermission(AdminPermissions.NewsletterListsEdit)]
|
||||||
public async Task<IActionResult> Edit(Guid id, NewsletterListFormViewModel model)
|
public async Task<IActionResult> Edit(Guid id, NewsletterListFormViewModel model)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
@ -88,6 +96,7 @@ public class NewsletterListsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("delete/{id:guid}")]
|
[HttpPost("delete/{id:guid}")]
|
||||||
|
[AdminPermission(AdminPermissions.NewsletterListsDelete)]
|
||||||
public async Task<IActionResult> Delete(Guid id)
|
public async Task<IActionResult> Delete(Guid id)
|
||||||
{
|
{
|
||||||
await _listService.DeleteAsync(id);
|
await _listService.DeleteAsync(id);
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
using MemberCenter.Application.Abstractions;
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using MemberCenter.Application.Constants;
|
||||||
|
using MemberCenter.Web.Authorization;
|
||||||
using MemberCenter.Web.Models.Admin;
|
using MemberCenter.Web.Models.Admin;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -8,6 +10,7 @@ namespace MemberCenter.Web.Areas.Admin.Controllers;
|
|||||||
|
|
||||||
[Area("Admin")]
|
[Area("Admin")]
|
||||||
[Authorize(Policy = "Admin")]
|
[Authorize(Policy = "Admin")]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClients)]
|
||||||
[Route("admin/oauth-clients")]
|
[Route("admin/oauth-clients")]
|
||||||
public class OAuthClientsController : Controller
|
public class OAuthClientsController : Controller
|
||||||
{
|
{
|
||||||
@ -26,6 +29,7 @@ public class OAuthClientsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClientsIndex)]
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
var results = new List<object>();
|
var results = new List<object>();
|
||||||
@ -51,6 +55,7 @@ public class OAuthClientsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("create")]
|
[HttpGet("create")]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClientsCreate)]
|
||||||
public async Task<IActionResult> Create()
|
public async Task<IActionResult> Create()
|
||||||
{
|
{
|
||||||
var tenants = await _tenantService.ListAsync();
|
var tenants = await _tenantService.ListAsync();
|
||||||
@ -61,6 +66,7 @@ public class OAuthClientsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("create")]
|
[HttpPost("create")]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClientsCreate)]
|
||||||
public async Task<IActionResult> Create(OAuthClientFormViewModel model)
|
public async Task<IActionResult> Create(OAuthClientFormViewModel model)
|
||||||
{
|
{
|
||||||
if (!IsValidUsage(model.Usage))
|
if (!IsValidUsage(model.Usage))
|
||||||
@ -131,6 +137,7 @@ public class OAuthClientsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("edit/{id}")]
|
[HttpGet("edit/{id}")]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClientsEdit)]
|
||||||
public async Task<IActionResult> Edit(string id)
|
public async Task<IActionResult> Edit(string id)
|
||||||
{
|
{
|
||||||
var app = await _applicationManager.FindByIdAsync(id);
|
var app = await _applicationManager.FindByIdAsync(id);
|
||||||
@ -158,6 +165,7 @@ public class OAuthClientsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("edit/{id}")]
|
[HttpPost("edit/{id}")]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClientsEdit)]
|
||||||
public async Task<IActionResult> Edit(string id, OAuthClientFormViewModel model)
|
public async Task<IActionResult> Edit(string id, OAuthClientFormViewModel model)
|
||||||
{
|
{
|
||||||
if (!IsValidUsage(model.Usage))
|
if (!IsValidUsage(model.Usage))
|
||||||
@ -232,6 +240,7 @@ public class OAuthClientsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("delete/{id}")]
|
[HttpPost("delete/{id}")]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClientsDelete)]
|
||||||
public async Task<IActionResult> Delete(string id)
|
public async Task<IActionResult> Delete(string id)
|
||||||
{
|
{
|
||||||
var app = await _applicationManager.FindByIdAsync(id);
|
var app = await _applicationManager.FindByIdAsync(id);
|
||||||
@ -245,6 +254,7 @@ public class OAuthClientsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("rotate-secret/{id}")]
|
[HttpPost("rotate-secret/{id}")]
|
||||||
|
[AdminPermission(AdminPermissions.OAuthClientsRotateSecret)]
|
||||||
public async Task<IActionResult> RotateSecret(string id)
|
public async Task<IActionResult> RotateSecret(string id)
|
||||||
{
|
{
|
||||||
var app = await _applicationManager.FindByIdAsync(id);
|
var app = await _applicationManager.FindByIdAsync(id);
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
using MemberCenter.Application.Abstractions;
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using MemberCenter.Application.Constants;
|
||||||
using MemberCenter.Application.Models.Admin;
|
using MemberCenter.Application.Models.Admin;
|
||||||
using MemberCenter.Infrastructure.Identity;
|
using MemberCenter.Infrastructure.Identity;
|
||||||
|
using MemberCenter.Web.Authorization;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -10,6 +12,7 @@ namespace MemberCenter.Web.Areas.Admin.Controllers;
|
|||||||
|
|
||||||
[Area("Admin")]
|
[Area("Admin")]
|
||||||
[Authorize(Policy = "Admin")]
|
[Authorize(Policy = "Admin")]
|
||||||
|
[AdminPermission(AdminPermissions.Security)]
|
||||||
[Route("admin/security")]
|
[Route("admin/security")]
|
||||||
public class SecurityController : Controller
|
public class SecurityController : Controller
|
||||||
{
|
{
|
||||||
@ -23,6 +26,7 @@ public class SecurityController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
|
[AdminPermission(AdminPermissions.SecurityIndex)]
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
var settings = await _settingsService.GetAsync();
|
var settings = await _settingsService.GetAsync();
|
||||||
@ -30,6 +34,7 @@ public class SecurityController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("")]
|
[HttpPost("")]
|
||||||
|
[AdminPermission(AdminPermissions.SecuritySave)]
|
||||||
public async Task<IActionResult> Save(SecuritySettingsDto model)
|
public async Task<IActionResult> Save(SecuritySettingsDto model)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
@ -52,6 +57,7 @@ public class SecurityController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("test-email")]
|
[HttpPost("test-email")]
|
||||||
|
[AdminPermission(AdminPermissions.SecurityTestEmail)]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> TestEmail(SecuritySettingsDto model)
|
public async Task<IActionResult> TestEmail(SecuritySettingsDto model)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
using MemberCenter.Application.Abstractions;
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using MemberCenter.Application.Constants;
|
||||||
|
using MemberCenter.Web.Authorization;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@ -7,6 +9,7 @@ namespace MemberCenter.Web.Areas.Admin.Controllers;
|
|||||||
|
|
||||||
[Area("Admin")]
|
[Area("Admin")]
|
||||||
[Authorize(Policy = "Admin")]
|
[Authorize(Policy = "Admin")]
|
||||||
|
[AdminPermission(AdminPermissions.Subscriptions)]
|
||||||
[Route("admin/subscriptions")]
|
[Route("admin/subscriptions")]
|
||||||
public class SubscriptionsController : Controller
|
public class SubscriptionsController : Controller
|
||||||
{
|
{
|
||||||
@ -18,6 +21,7 @@ public class SubscriptionsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
|
[AdminPermission(AdminPermissions.SubscriptionsIndex)]
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
var subscriptions = await _subscriptionService.ListAsync();
|
var subscriptions = await _subscriptionService.ListAsync();
|
||||||
@ -25,6 +29,7 @@ public class SubscriptionsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("export")]
|
[HttpGet("export")]
|
||||||
|
[AdminPermission(AdminPermissions.SubscriptionsExport)]
|
||||||
public async Task<IActionResult> Export()
|
public async Task<IActionResult> Export()
|
||||||
{
|
{
|
||||||
var subscriptions = await _subscriptionService.ListAsync();
|
var subscriptions = await _subscriptionService.ListAsync();
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
using MemberCenter.Application.Abstractions;
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using MemberCenter.Application.Constants;
|
||||||
|
using MemberCenter.Web.Authorization;
|
||||||
using MemberCenter.Web.Models.Admin;
|
using MemberCenter.Web.Models.Admin;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -7,6 +9,7 @@ namespace MemberCenter.Web.Areas.Admin.Controllers;
|
|||||||
|
|
||||||
[Area("Admin")]
|
[Area("Admin")]
|
||||||
[Authorize(Policy = "Admin")]
|
[Authorize(Policy = "Admin")]
|
||||||
|
[AdminPermission(AdminPermissions.Tenants)]
|
||||||
[Route("admin/tenants")]
|
[Route("admin/tenants")]
|
||||||
public class TenantsController : Controller
|
public class TenantsController : Controller
|
||||||
{
|
{
|
||||||
@ -18,6 +21,7 @@ public class TenantsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
|
[AdminPermission(AdminPermissions.TenantsIndex)]
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
var tenants = await _tenantService.ListAsync();
|
var tenants = await _tenantService.ListAsync();
|
||||||
@ -25,12 +29,14 @@ public class TenantsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("create")]
|
[HttpGet("create")]
|
||||||
|
[AdminPermission(AdminPermissions.TenantsCreate)]
|
||||||
public IActionResult Create()
|
public IActionResult Create()
|
||||||
{
|
{
|
||||||
return View(new TenantFormViewModel());
|
return View(new TenantFormViewModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("create")]
|
[HttpPost("create")]
|
||||||
|
[AdminPermission(AdminPermissions.TenantsCreate)]
|
||||||
public async Task<IActionResult> Create(TenantFormViewModel model)
|
public async Task<IActionResult> Create(TenantFormViewModel model)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
@ -50,6 +56,7 @@ public class TenantsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("edit/{id:guid}")]
|
[HttpGet("edit/{id:guid}")]
|
||||||
|
[AdminPermission(AdminPermissions.TenantsEdit)]
|
||||||
public async Task<IActionResult> Edit(Guid id)
|
public async Task<IActionResult> Edit(Guid id)
|
||||||
{
|
{
|
||||||
var tenant = await _tenantService.GetAsync(id);
|
var tenant = await _tenantService.GetAsync(id);
|
||||||
@ -69,6 +76,7 @@ public class TenantsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("edit/{id:guid}")]
|
[HttpPost("edit/{id:guid}")]
|
||||||
|
[AdminPermission(AdminPermissions.TenantsEdit)]
|
||||||
public async Task<IActionResult> Edit(Guid id, TenantFormViewModel model)
|
public async Task<IActionResult> Edit(Guid id, TenantFormViewModel model)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
@ -93,6 +101,7 @@ public class TenantsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("delete/{id:guid}")]
|
[HttpPost("delete/{id:guid}")]
|
||||||
|
[AdminPermission(AdminPermissions.TenantsDelete)]
|
||||||
public async Task<IActionResult> Delete(Guid id)
|
public async Task<IActionResult> Delete(Guid id)
|
||||||
{
|
{
|
||||||
await _tenantService.DeleteAsync(id);
|
await _tenantService.DeleteAsync(id);
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
@model IReadOnlyList<MemberCenter.Application.Models.Admin.EmailBlacklistDto>
|
@model IReadOnlyList<MemberCenter.Application.Models.Admin.EmailBlacklistDto>
|
||||||
|
|
||||||
<h1>Email Blacklist</h1>
|
<h1>Email Blacklist</h1>
|
||||||
<p><a href="/admin/blacklist/create">Add</a></p>
|
@if (await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.BlacklistCreate))
|
||||||
|
{
|
||||||
|
<p><a href="/admin/blacklist/create">Add</a></p>
|
||||||
|
}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Email</th><th>Reason</th><th>By</th><th>At</th></tr>
|
<tr><th>Email</th><th>Reason</th><th>By</th><th>At</th></tr>
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
@model IReadOnlyList<MemberCenter.Application.Models.Admin.NewsletterListDto>
|
@model IReadOnlyList<MemberCenter.Application.Models.Admin.NewsletterListDto>
|
||||||
|
|
||||||
<h1>Newsletter Lists</h1>
|
<h1>Newsletter Lists</h1>
|
||||||
<p><a href="/admin/newsletter-lists/create">Create</a></p>
|
@if (await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.NewsletterListsCreate))
|
||||||
|
{
|
||||||
|
<p><a href="/admin/newsletter-lists/create">Create</a></p>
|
||||||
|
}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>List ID</th><th>Name</th><th>Tenant</th><th>Status</th><th></th></tr>
|
<tr><th>List ID</th><th>Name</th><th>Tenant</th><th>Status</th><th></th></tr>
|
||||||
@ -15,10 +18,16 @@
|
|||||||
<td>@(string.IsNullOrWhiteSpace(list.TenantName) ? list.TenantId.ToString() : list.TenantName)</td>
|
<td>@(string.IsNullOrWhiteSpace(list.TenantName) ? list.TenantId.ToString() : list.TenantName)</td>
|
||||||
<td>@list.Status</td>
|
<td>@list.Status</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/admin/newsletter-lists/edit/@list.Id">Edit</a>
|
@if (await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.NewsletterListsEdit))
|
||||||
<form method="post" action="/admin/newsletter-lists/delete/@list.Id" style="display:inline">
|
{
|
||||||
<button type="submit">Delete</button>
|
<a href="/admin/newsletter-lists/edit/@list.Id">Edit</a>
|
||||||
</form>
|
}
|
||||||
|
@if (await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.NewsletterListsDelete))
|
||||||
|
{
|
||||||
|
<form method="post" action="/admin/newsletter-lists/delete/@list.Id" style="display:inline">
|
||||||
|
<button type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
@model IReadOnlyList<object>
|
@model IReadOnlyList<object>
|
||||||
|
|
||||||
<h1>OAuth Clients</h1>
|
<h1>OAuth Clients</h1>
|
||||||
<p><a href="/admin/oauth-clients/create">Create</a></p>
|
@if (await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.OAuthClientsCreate))
|
||||||
|
{
|
||||||
|
<p><a href="/admin/oauth-clients/create">Create</a></p>
|
||||||
|
}
|
||||||
@if (TempData["CreatedClientId"] is string createdId)
|
@if (TempData["CreatedClientId"] is string createdId)
|
||||||
{
|
{
|
||||||
<div>
|
<div>
|
||||||
@ -42,16 +45,23 @@
|
|||||||
<td>@clientType</td>
|
<td>@clientType</td>
|
||||||
<td>@usage</td>
|
<td>@usage</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/admin/oauth-clients/edit/@id">Edit</a>
|
@if (await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.OAuthClientsEdit))
|
||||||
@if (string.Equals(clientType, "confidential", StringComparison.OrdinalIgnoreCase))
|
{
|
||||||
{
|
<a href="/admin/oauth-clients/edit/@id">Edit</a>
|
||||||
<form method="post" action="/admin/oauth-clients/rotate-secret/@id" style="display:inline">
|
}
|
||||||
<button type="submit">Rotate Secret</button>
|
@if (string.Equals(clientType, "confidential", StringComparison.OrdinalIgnoreCase)
|
||||||
</form>
|
&& await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.OAuthClientsRotateSecret))
|
||||||
}
|
{
|
||||||
<form method="post" action="/admin/oauth-clients/delete/@id" style="display:inline">
|
<form method="post" action="/admin/oauth-clients/rotate-secret/@id" style="display:inline">
|
||||||
<button type="submit">Delete</button>
|
<button type="submit">Rotate Secret</button>
|
||||||
</form>
|
</form>
|
||||||
|
}
|
||||||
|
@if (await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.OAuthClientsDelete))
|
||||||
|
{
|
||||||
|
<form method="post" action="/admin/oauth-clients/delete/@id" style="display:inline">
|
||||||
|
<button type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,9 +49,14 @@
|
|||||||
<label asp-for="SenderEmail">Sender email</label>
|
<label asp-for="SenderEmail">Sender email</label>
|
||||||
<input asp-for="SenderEmail" />
|
<input asp-for="SenderEmail" />
|
||||||
|
|
||||||
<button type="submit">Save</button>
|
@if (await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.SecuritySave))
|
||||||
|
{
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@if (await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.SecurityTestEmail))
|
||||||
|
{
|
||||||
<h2>Test Email</h2>
|
<h2>Test Email</h2>
|
||||||
<form asp-action="TestEmail" method="post">
|
<form asp-action="TestEmail" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
@ -72,3 +77,4 @@
|
|||||||
<input asp-for="TestToEmail" />
|
<input asp-for="TestToEmail" />
|
||||||
<button type="submit">Send Test Email</button>
|
<button type="submit">Send Test Email</button>
|
||||||
</form>
|
</form>
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
@using MemberCenter.Application.Constants
|
||||||
|
@using MemberCenter.Application.Abstractions
|
||||||
|
@inject IAdminPermissionChecker AdminPermissionChecker
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@ -36,15 +39,24 @@
|
|||||||
<p class="admin-sidebar-copy">Lightweight structure for operations screens. Visual design can be replaced later.</p>
|
<p class="admin-sidebar-copy">Lightweight structure for operations screens. Visual design can be replaced later.</p>
|
||||||
</div>
|
</div>
|
||||||
<nav class="admin-nav" aria-label="Admin navigation">
|
<nav class="admin-nav" aria-label="Admin navigation">
|
||||||
<a class="@NavClass("Home")" asp-area="Admin" asp-controller="Home" asp-action="Index">Overview</a>
|
@foreach (var item in new[]
|
||||||
<a class="@NavClass("Accounts")" asp-area="Admin" asp-controller="Accounts" asp-action="Index">Accounts</a>
|
{
|
||||||
<a class="@NavClass("Tenants")" asp-area="Admin" asp-controller="Tenants" asp-action="Index">Tenants</a>
|
new { Permission = AdminPermissions.Home, Controller = "Home", Label = "Overview" },
|
||||||
<a class="@NavClass("NewsletterLists")" asp-area="Admin" asp-controller="NewsletterLists" asp-action="Index">Newsletter Lists</a>
|
new { Permission = AdminPermissions.AccountsIndex, Controller = "Accounts", Label = "Accounts" },
|
||||||
<a class="@NavClass("Subscriptions")" asp-area="Admin" asp-controller="Subscriptions" asp-action="Index">Subscriptions</a>
|
new { Permission = AdminPermissions.TenantsIndex, Controller = "Tenants", Label = "Tenants" },
|
||||||
<a class="@NavClass("OAuthClients")" asp-area="Admin" asp-controller="OAuthClients" asp-action="Index">OAuth Clients</a>
|
new { Permission = AdminPermissions.NewsletterListsIndex, Controller = "NewsletterLists", Label = "Newsletter Lists" },
|
||||||
<a class="@NavClass("AuditLogs")" asp-area="Admin" asp-controller="AuditLogs" asp-action="Index">Audit Logs</a>
|
new { Permission = AdminPermissions.SubscriptionsIndex, Controller = "Subscriptions", Label = "Subscriptions" },
|
||||||
<a class="@NavClass("Security")" asp-area="Admin" asp-controller="Security" asp-action="Index">Security</a>
|
new { Permission = AdminPermissions.OAuthClientsIndex, Controller = "OAuthClients", Label = "OAuth Clients" },
|
||||||
<a class="@NavClass("Blacklist")" asp-area="Admin" asp-controller="Blacklist" asp-action="Index">Blacklist</a>
|
new { Permission = AdminPermissions.AuditLogsIndex, Controller = "AuditLogs", Label = "Audit Logs" },
|
||||||
|
new { Permission = AdminPermissions.SecurityIndex, Controller = "Security", Label = "Security" },
|
||||||
|
new { Permission = AdminPermissions.BlacklistIndex, Controller = "Blacklist", Label = "Blacklist" }
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (await AdminPermissionChecker.HasPermissionAsync(User, item.Permission))
|
||||||
|
{
|
||||||
|
<a class="@NavClass(item.Controller)" asp-area="Admin" asp-controller="@item.Controller" asp-action="Index">@item.Label</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
<main class="admin-content">
|
<main class="admin-content">
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
@model IReadOnlyList<MemberCenter.Application.Models.Newsletter.SubscriptionDto>
|
@model IReadOnlyList<MemberCenter.Application.Models.Newsletter.SubscriptionDto>
|
||||||
|
|
||||||
<h1>Subscriptions</h1>
|
<h1>Subscriptions</h1>
|
||||||
<p><a href="/admin/subscriptions/export">Export CSV</a></p>
|
@if (await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.SubscriptionsExport))
|
||||||
|
{
|
||||||
|
<p><a href="/admin/subscriptions/export">Export CSV</a></p>
|
||||||
|
}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Email</th><th>List</th><th>Status</th><th>Created</th></tr>
|
<tr><th>Email</th><th>List</th><th>Status</th><th>Created</th></tr>
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
@model IReadOnlyList<MemberCenter.Application.Models.Admin.TenantDto>
|
@model IReadOnlyList<MemberCenter.Application.Models.Admin.TenantDto>
|
||||||
|
|
||||||
<h1>Tenants</h1>
|
<h1>Tenants</h1>
|
||||||
<p><a href="/admin/tenants/create">Create</a></p>
|
@if (await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.TenantsCreate))
|
||||||
|
{
|
||||||
|
<p><a href="/admin/tenants/create">Create</a></p>
|
||||||
|
}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Name</th><th>Tenant Id</th><th>Webhook Client Id</th><th>Domains</th><th>Status</th><th></th></tr>
|
<tr><th>Name</th><th>Tenant Id</th><th>Webhook Client Id</th><th>Domains</th><th>Status</th><th></th></tr>
|
||||||
@ -16,10 +19,16 @@
|
|||||||
<td>@string.Join(",", tenant.Domains)</td>
|
<td>@string.Join(",", tenant.Domains)</td>
|
||||||
<td>@tenant.Status</td>
|
<td>@tenant.Status</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/admin/tenants/edit/@tenant.Id">Edit</a>
|
@if (await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.TenantsEdit))
|
||||||
<form method="post" action="/admin/tenants/delete/@tenant.Id" style="display:inline">
|
{
|
||||||
<button type="submit">Delete</button>
|
<a href="/admin/tenants/edit/@tenant.Id">Edit</a>
|
||||||
</form>
|
}
|
||||||
|
@if (await AdminPermissionChecker.HasPermissionAsync(User, AdminPermissions.TenantsDelete))
|
||||||
|
{
|
||||||
|
<form method="post" action="/admin/tenants/delete/@tenant.Id" style="display:inline">
|
||||||
|
<button type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
@using MemberCenter.Web
|
@using MemberCenter.Web
|
||||||
@using MemberCenter.Web.Models
|
@using MemberCenter.Web.Models
|
||||||
|
@using MemberCenter.Application.Constants
|
||||||
|
@using MemberCenter.Application.Abstractions
|
||||||
|
@inject IAdminPermissionChecker AdminPermissionChecker
|
||||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
using MemberCenter.Application.Abstractions;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace MemberCenter.Web.Authorization;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||||
|
public sealed class AdminPermissionAttribute : TypeFilterAttribute
|
||||||
|
{
|
||||||
|
public AdminPermissionAttribute(string permissionKey)
|
||||||
|
: base(typeof(AdminPermissionFilter))
|
||||||
|
{
|
||||||
|
Arguments = [permissionKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AdminPermissionFilter : IAsyncAuthorizationFilter
|
||||||
|
{
|
||||||
|
private readonly string _permissionKey;
|
||||||
|
private readonly IAdminPermissionChecker _permissionChecker;
|
||||||
|
|
||||||
|
public AdminPermissionFilter(string permissionKey, IAdminPermissionChecker permissionChecker)
|
||||||
|
{
|
||||||
|
_permissionKey = permissionKey;
|
||||||
|
_permissionChecker = permissionChecker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||||
|
{
|
||||||
|
var hasPermission = await _permissionChecker.HasPermissionAsync(
|
||||||
|
context.HttpContext.User,
|
||||||
|
_permissionKey,
|
||||||
|
context.HttpContext.RequestAborted);
|
||||||
|
|
||||||
|
if (!hasPermission)
|
||||||
|
{
|
||||||
|
context.Result = new NotFoundResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -135,6 +135,9 @@ builder.Services.AddScoped<ISubscriptionAdminService, SubscriptionAdminService>(
|
|||||||
builder.Services.AddScoped<IAccountProvisioningService, AccountProvisioningService>();
|
builder.Services.AddScoped<IAccountProvisioningService, AccountProvisioningService>();
|
||||||
builder.Services.AddScoped<IProfileService, ProfileService>();
|
builder.Services.AddScoped<IProfileService, ProfileService>();
|
||||||
builder.Services.AddScoped<IAuthResourceRegistryService, AuthResourceRegistryService>();
|
builder.Services.AddScoped<IAuthResourceRegistryService, AuthResourceRegistryService>();
|
||||||
|
builder.Services.AddScoped<AdminPermissionService>();
|
||||||
|
builder.Services.AddScoped<IAdminPermissionChecker>(services => services.GetRequiredService<AdminPermissionService>());
|
||||||
|
builder.Services.AddScoped<IAdminPermissionSeeder>(services => services.GetRequiredService<AdminPermissionService>());
|
||||||
builder.Services.Configure<SendEngineWebhookOptions>(builder.Configuration.GetSection("SendEngine"));
|
builder.Services.Configure<SendEngineWebhookOptions>(builder.Configuration.GetSection("SendEngine"));
|
||||||
builder.Services.AddHttpClient<SendEngineWebhookPublisher>();
|
builder.Services.AddHttpClient<SendEngineWebhookPublisher>();
|
||||||
builder.Services.AddScoped<ISendEngineWebhookPublisher, SendEngineWebhookPublisher>();
|
builder.Services.AddScoped<ISendEngineWebhookPublisher, SendEngineWebhookPublisher>();
|
||||||
@ -155,6 +158,7 @@ builder.Services.AddHttpContextAccessor();
|
|||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
await EnsureAuthRegistryDefaultsAsync(app.Services);
|
await EnsureAuthRegistryDefaultsAsync(app.Services);
|
||||||
|
await EnsureAdminPermissionDefaultsAsync(app.Services);
|
||||||
|
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
@ -244,3 +248,10 @@ static async Task EnsureAuthRegistryDefaultsAsync(IServiceProvider services)
|
|||||||
var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
|
var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
|
||||||
await registry.EnsureDefaultsAsync();
|
await registry.EnsureDefaultsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async Task EnsureAdminPermissionDefaultsAsync(IServiceProvider services)
|
||||||
|
{
|
||||||
|
await using var scope = services.CreateAsyncScope();
|
||||||
|
var seeder = scope.ServiceProvider.GetRequiredService<IAdminPermissionSeeder>();
|
||||||
|
await seeder.EnsureDefaultsAsync();
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
@using MemberCenter.Application.Constants
|
||||||
|
@using MemberCenter.Application.Abstractions
|
||||||
|
@inject IAdminPermissionChecker AdminPermissionChecker
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@ -40,20 +43,29 @@
|
|||||||
<a asp-area="" asp-controller="Profile" asp-action="Subscriptions">Subscriptions</a>
|
<a asp-area="" asp-controller="Profile" asp-action="Subscriptions">Subscriptions</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (User.IsInRole("admin") || User.IsInRole("superuser"))
|
@if (User.IsInRole(AdminPermissions.AdminRole) || User.IsInRole(AdminPermissions.SuperuserRole))
|
||||||
{
|
{
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<span class="text-muted small text-uppercase">Admin</span>
|
<span class="text-muted small text-uppercase">Admin</span>
|
||||||
<div class="d-flex gap-3 flex-wrap">
|
<div class="d-flex gap-3 flex-wrap">
|
||||||
<a asp-area="Admin" asp-controller="Home" asp-action="Index">Overview</a>
|
@foreach (var item in new[]
|
||||||
<a asp-area="Admin" asp-controller="Accounts" asp-action="Index">Accounts</a>
|
{
|
||||||
<a asp-area="Admin" asp-controller="Tenants" asp-action="Index">Tenants</a>
|
new { Permission = AdminPermissions.Home, Controller = "Home", Label = "Overview" },
|
||||||
<a asp-area="Admin" asp-controller="NewsletterLists" asp-action="Index">Newsletter Lists</a>
|
new { Permission = AdminPermissions.AccountsIndex, Controller = "Accounts", Label = "Accounts" },
|
||||||
<a asp-area="Admin" asp-controller="Subscriptions" asp-action="Index">Subscriptions</a>
|
new { Permission = AdminPermissions.TenantsIndex, Controller = "Tenants", Label = "Tenants" },
|
||||||
<a asp-area="Admin" asp-controller="OAuthClients" asp-action="Index">OAuth Clients</a>
|
new { Permission = AdminPermissions.NewsletterListsIndex, Controller = "NewsletterLists", Label = "Newsletter Lists" },
|
||||||
<a asp-area="Admin" asp-controller="AuditLogs" asp-action="Index">Audit Logs</a>
|
new { Permission = AdminPermissions.SubscriptionsIndex, Controller = "Subscriptions", Label = "Subscriptions" },
|
||||||
<a asp-area="Admin" asp-controller="Security" asp-action="Index">Security</a>
|
new { Permission = AdminPermissions.OAuthClientsIndex, Controller = "OAuthClients", Label = "OAuth Clients" },
|
||||||
<a asp-area="Admin" asp-controller="Blacklist" asp-action="Index">Blacklist</a>
|
new { Permission = AdminPermissions.AuditLogsIndex, Controller = "AuditLogs", Label = "Audit Logs" },
|
||||||
|
new { Permission = AdminPermissions.SecurityIndex, Controller = "Security", Label = "Security" },
|
||||||
|
new { Permission = AdminPermissions.BlacklistIndex, Controller = "Blacklist", Label = "Blacklist" }
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (await AdminPermissionChecker.HasPermissionAsync(User, item.Permission))
|
||||||
|
{
|
||||||
|
<a asp-area="Admin" asp-controller="@item.Controller" asp-action="Index">@item.Label</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user