Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Dappi.Core/Models/MethodRouteEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Dappi.Core.Models;

public class MethodRouteEntry
{
public string MethodName { get; set; } = string.Empty;
public string HttpRoute { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Dappi.HeadlessCms.UsersAndPermissions.Controllers
{
public class RolePermissionDto
{
public string PermissionName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public bool Selected { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Dappi.HeadlessCms.UsersAndPermissions.Database;
using Dappi.HeadlessCms.UsersAndPermissions.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Dappi.HeadlessCms.UsersAndPermissions.Controllers;

[ApiController]
[Route("api/[controller]")]
public class UsersAndPermissionsController(
IDbContextAccessor usersAndPermissionsDb,
AvailablePermissionsRepository availablePermissionsRepository
) : ControllerBase
{
private readonly UsersAndPermissionsDbContext _usersAndPermissionsDb =
usersAndPermissionsDb.DbContext;

[HttpGet]
public async Task<ActionResult<Dictionary<string, List<RolePermissionDto>>>> GetRolePermissions(
string roleName,
CancellationToken cancellationToken
)
{
if (string.IsNullOrWhiteSpace(roleName))
return BadRequest("Role name is required.");

var role = await _usersAndPermissionsDb
.AppRoles.Include(r => r.Permissions)
.FirstOrDefaultAsync(r => r.Name == roleName, cancellationToken);

if (role is null)
return NotFound($"Role '{roleName}' not found.");

var assignedPermissionNames = role
.Permissions.Select(p => p.Name)
.Where(n => !string.IsNullOrWhiteSpace(n))
.ToHashSet(StringComparer.OrdinalIgnoreCase);

var availablePermissions = availablePermissionsRepository.GetAllPermissions();

var result = new Dictionary<string, List<RolePermissionDto>>(
StringComparer.OrdinalIgnoreCase
);

foreach (var perm in availablePermissions)
{
if (string.IsNullOrWhiteSpace(perm.Name))
continue;

var parts = perm.Name.Split(':', 2);
if (parts.Length < 2)
continue;

var controllerName = parts[0];
var methodName = parts[1];

if (!result.TryGetValue(controllerName, out var list))
{
list = new List<RolePermissionDto>();
result[controllerName] = list;
}

list.Add(
new RolePermissionDto
{
PermissionName = methodName,
Description = perm.Description ?? string.Empty,
Selected = assignedPermissionNames.Contains(perm.Name),
}
);
}

return Ok(result);
}
}
24 changes: 24 additions & 0 deletions Dappi.HeadlessCms.UsersAndPermissions/Core/AppPermission.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Dappi.HeadlessCms.UsersAndPermissions.Core
{
public class AppPermission
{
private AppPermission() { } // For EF Core

public AppPermission(string name, string description)
{
Name = name;
Description = description;
}

public int Id { get; private set; }
public string Name { get; private set; }
public string Description { get; private set; }

public List<AppRole> Roles { get; private set; } = [];

public override string ToString()
{
return Name;
}
}
}
61 changes: 61 additions & 0 deletions Dappi.HeadlessCms.UsersAndPermissions/Core/AppRole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
namespace Dappi.HeadlessCms.UsersAndPermissions.Core
{
public class AppRole
{
public int Id { get; private set; }
public string Name { get; private set; }

private readonly List<AppPermission> _permissions = [];
public IEnumerable<IAppUser> Users { get; set; }

private AppRole() { } // For EF Core

public IReadOnlyList<AppPermission> Permissions => _permissions.AsReadOnly();

public bool IsDefaultForAuthenticatedUser { get; private set; }

public AppRole(string name, IEnumerable<AppPermission> permissions)
{
Name = name;
_permissions.AddRange(permissions);
}

public bool HasPermission(string permissionName) =>
_permissions.Any(p => p.Name == permissionName);

public static AppRole CreateDefaultPublicUserRole(IEnumerable<AppPermission> permissions)
{
return new AppRole(UsersAndPermissionsConstants.DefaultRoles.Public, permissions);
}

public static AppRole CreateDefaultAuthenticatedUserRole(
IEnumerable<AppPermission> permissions
)
{
return new AppRole(UsersAndPermissionsConstants.DefaultRoles.Authenticated, permissions)
{
IsDefaultForAuthenticatedUser = true,
};
}

public void AddPermission(AppPermission permission)
{
if (_permissions.All(p => p.Name != permission.Name))
_permissions.Add(permission);
}

public void RemovePermission(AppPermission permission)
{
var existing = _permissions.FirstOrDefault(p => p.Name == permission.Name);
if (existing != null)
_permissions.Remove(existing);
}

public void ClearPermissions() => _permissions.Clear();

public override string ToString()
{
return Name;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using System.Reflection;

namespace Dappi.HeadlessCms.UsersAndPermissions.Core
{
public interface IRoleStage<TUser>
where TUser : IAppUser
{
IControllerStage<TUser> ForRole(string roleName, bool isDefaultForAuthenticated = false);
AppRole[] Build();
}

public interface IControllerStage<TUser>
where TUser : IAppUser
{
IPermissionStage<TUser> ForController(string controllerName);
}

public interface IPermissionStage<TUser>
where TUser : IAppUser
{
IPermissionStage<TUser> Allow(string methodName);
IPermissionStage<TUser> AllowAll();
IPermissionStage<TUser> Deny(string methodName);
IPermissionStage<TUser> ForController(string controllerName);
IRoleStage<TUser> And();
}

public class AppRoleAndPermissionsBuilder<TUser>
: IRoleStage<TUser>,
IControllerStage<TUser>,
IPermissionStage<TUser>
where TUser : IAppUser
{
private record RoleEntry(
string RoleName,
string Controller,
string Method,
bool IsDefaultForAuthenticated
);

private readonly List<RoleEntry> _entries = [];
private string? _currentRole;
private string? _currentController;
private bool _currentIsDefaultForAuthenticated;

public static IRoleStage<TUser> Create() => new AppRoleAndPermissionsBuilder<TUser>();

public IControllerStage<TUser> ForRole(
string roleName,
bool isDefaultForAuthenticated = false
)
{
_currentRole = roleName ?? throw new ArgumentNullException(nameof(roleName));
_currentIsDefaultForAuthenticated = isDefaultForAuthenticated;
return this;
}

IPermissionStage<TUser> IControllerStage<TUser>.ForController(string controllerName) =>
SetController(controllerName);

IPermissionStage<TUser> IPermissionStage<TUser>.ForController(string controllerName) =>
SetController(controllerName);

public IPermissionStage<TUser> Allow(string methodName)
{
AddEntry(methodName);
return this;
}

public IPermissionStage<TUser> Deny(string methodName)
{
_entries.RemoveAll(e =>
e.RoleName == _currentRole
&& e.Controller == _currentController
&& e.Method == methodName
);
return this;
}

public IPermissionStage<TUser> AllowAll()
{
if (_currentController == null)
throw new InvalidOperationException("Call ForController() before AllowAll().");

var controllerType =
AppDomain
.CurrentDomain.GetAssemblies()
.SelectMany(a =>
{
try
{
return a.GetTypes();
}
catch
{
return Array.Empty<Type>();
}
})
.FirstOrDefault(t => t.Name == _currentController)
?? throw new InvalidOperationException(
$"Controller type '{_currentController}' not found."
);

var methods = controllerType
.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.Where(m => !m.IsSpecialName)
.Select(m => m.Name)
.Distinct();

foreach (var method in methods)
AddEntry(method);

return this;
}

public IRoleStage<TUser> And() => this;

public AppRole[] Build()
{
return _entries
.GroupBy(e => e.RoleName)
.Select(roleGroup =>
{
var permissions = roleGroup
.Select(e => new AppPermission($"{e.Controller}:{e.Method}", ""))
.ToList();

var isDefault = roleGroup.First().IsDefaultForAuthenticated;

return isDefault
? AppRole.CreateDefaultAuthenticatedUserRole(permissions)
: new AppRole(roleGroup.Key, permissions);
})
.ToArray();
}

private AppRoleAndPermissionsBuilder<TUser> SetController(string controllerName)
{
_currentController =
controllerName ?? throw new ArgumentNullException(nameof(controllerName));
return this;
}

private void AddEntry(string methodName)
{
if (_currentRole == null)
throw new InvalidOperationException("No role selected.");
if (_currentController == null)
throw new InvalidOperationException("No controller selected.");
if (string.IsNullOrWhiteSpace(methodName))
throw new ArgumentException("Method name required.", nameof(methodName));

var alreadyAdded = _entries.Any(e =>
e.RoleName == _currentRole
&& e.Controller == _currentController
&& e.Method == methodName
);

if (!alreadyAdded)
_entries.Add(
new RoleEntry(
_currentRole,
_currentController,
methodName,
_currentIsDefaultForAuthenticated
)
);
}
}
}
11 changes: 11 additions & 0 deletions Dappi.HeadlessCms.UsersAndPermissions/Core/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Dappi.HeadlessCms.UsersAndPermissions.Core
{
public static class UsersAndPermissionsConstants
{
public static class DefaultRoles
{
public const string Authenticated = nameof(Authenticated);
public const string Public = nameof(Public);
}
}
}
8 changes: 8 additions & 0 deletions Dappi.HeadlessCms.UsersAndPermissions/Core/IAppUser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Dappi.HeadlessCms.UsersAndPermissions.Core
{
public interface IAppUser
{
int RoleId { get; }
AppRole? Role { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.DynamicLinq" Version="9.7.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dappi.Core\Dappi.Core.csproj" />
</ItemGroup>
</Project>
Loading
Loading