Skip to content

Commit fdfbb3e

Browse files
committed
feat: Add Users and Permissions system
1 parent eb7caea commit fdfbb3e

20 files changed

Lines changed: 1413 additions & 54 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Dappi.Core.Models;
2+
3+
public class MethodRouteEntry
4+
{
5+
public string MethodName { get; set; } = string.Empty;
6+
public string HttpRoute { get; set; } = string.Empty;
7+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using Dappi.HeadlessCms.UsersAndPermissions.Database;
2+
using Dappi.HeadlessCms.UsersAndPermissions.Services;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.EntityFrameworkCore;
5+
6+
namespace Dappi.HeadlessCms.UsersAndPermissions.Controllers;
7+
8+
public class RolePermissionDto
9+
{
10+
public string PermissionName { get; set; } = string.Empty;
11+
public string Description { get; set; } = string.Empty;
12+
public bool Selected { get; set; }
13+
}
14+
15+
[ApiController]
16+
[Route("api/users-and-permissions/[controller]")]
17+
public class RolesController : ControllerBase
18+
{
19+
private readonly UsersAndPermissionsDbContext _usersAndPermissionsDb;
20+
private readonly AvailablePermissionsRepository _availablePermissionsRepository;
21+
22+
public RolesController(
23+
IDbContextAccessor usersAndPermissionsDb,
24+
AvailablePermissionsRepository availablePermissionsRepository
25+
)
26+
{
27+
_usersAndPermissionsDb = usersAndPermissionsDb.DbContext;
28+
_availablePermissionsRepository = availablePermissionsRepository;
29+
}
30+
31+
[HttpGet()]
32+
public async Task<ActionResult<Dictionary<string, List<RolePermissionDto>>>> GetRolePermissions(
33+
string roleName,
34+
CancellationToken cancellationToken
35+
)
36+
{
37+
if (string.IsNullOrWhiteSpace(roleName))
38+
return BadRequest("Role name is required.");
39+
40+
// 1. Load the role and its assigned permissions
41+
var role = await _usersAndPermissionsDb
42+
.AppRoles.Include(r => r.Permissions) // adjust nav property name
43+
.FirstOrDefaultAsync(r => r.Name == roleName, cancellationToken);
44+
45+
if (role is null)
46+
return NotFound($"Role '{roleName}' not found.");
47+
48+
var assignedPermissionNames = role
49+
.Permissions.Select(p => p.Name)
50+
.Where(n => !string.IsNullOrWhiteSpace(n))
51+
.ToHashSet(StringComparer.OrdinalIgnoreCase);
52+
53+
// 2. Get all available permissions from AvailablePermissionsRepository
54+
var availablePermissions = _availablePermissionsRepository.GetAllPermissions();
55+
56+
// 3. Build grouped dictionary
57+
var result = new Dictionary<string, List<RolePermissionDto>>(
58+
StringComparer.OrdinalIgnoreCase
59+
);
60+
61+
foreach (var perm in availablePermissions)
62+
{
63+
if (string.IsNullOrWhiteSpace(perm.Name))
64+
continue;
65+
66+
var parts = perm.Name.Split(':', 2);
67+
if (parts.Length < 2)
68+
continue;
69+
70+
var controllerName = parts[0];
71+
var methodName = parts[1];
72+
73+
if (!result.TryGetValue(controllerName, out var list))
74+
{
75+
list = new List<RolePermissionDto>();
76+
result[controllerName] = list;
77+
}
78+
79+
list.Add(
80+
new RolePermissionDto
81+
{
82+
PermissionName = methodName,
83+
Description = perm.Description ?? string.Empty,
84+
Selected = assignedPermissionNames.Contains(perm.Name),
85+
}
86+
);
87+
}
88+
89+
return Ok(result);
90+
}
91+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace Dappi.HeadlessCms.UsersAndPermissions.Core
2+
{
3+
public class AppPermission
4+
{
5+
private AppPermission() { } // For EF Core
6+
7+
public AppPermission(string name, string description)
8+
{
9+
Name = name;
10+
Description = description;
11+
}
12+
13+
public int Id { get; private set; }
14+
public string Name { get; private set; }
15+
public string Description { get; private set; }
16+
17+
public List<AppRole> Roles { get; private set; } = [];
18+
19+
public override string ToString()
20+
{
21+
return Name;
22+
}
23+
}
24+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
namespace Dappi.HeadlessCms.UsersAndPermissions.Core
2+
{
3+
public class AppRole
4+
{
5+
public int Id { get; private set; }
6+
public string Name { get; private set; }
7+
8+
private readonly List<AppPermission> _permissions = [];
9+
public IEnumerable<IAppUser> Users { get; set; }
10+
11+
private AppRole() { } // For EF Core
12+
13+
public IReadOnlyList<AppPermission> Permissions => _permissions.AsReadOnly();
14+
15+
public bool IsDefaultForAuthenticatedUser { get; private set; }
16+
17+
public AppRole(string name, IEnumerable<AppPermission> permissions)
18+
{
19+
Name = name;
20+
_permissions.AddRange(permissions);
21+
}
22+
23+
public bool HasPermission(string permissionName) =>
24+
_permissions.Any(p => p.Name == permissionName);
25+
26+
public static AppRole CreateDefaultPublicUserRole(IEnumerable<AppPermission> permissions)
27+
{
28+
return new AppRole(UsersAndPermissionsConstants.DefaultRoles.Public, permissions);
29+
}
30+
31+
public static AppRole CreateDefaultAuthenticatedUserRole(
32+
IEnumerable<AppPermission> permissions
33+
)
34+
{
35+
return new AppRole(UsersAndPermissionsConstants.DefaultRoles.Authenticated, permissions)
36+
{
37+
IsDefaultForAuthenticatedUser = true,
38+
};
39+
}
40+
41+
public void AddPermission(AppPermission permission)
42+
{
43+
if (_permissions.All(p => p.Name != permission.Name))
44+
_permissions.Add(permission);
45+
}
46+
47+
public void RemovePermission(AppPermission permission)
48+
{
49+
var existing = _permissions.FirstOrDefault(p => p.Name == permission.Name);
50+
if (existing != null)
51+
_permissions.Remove(existing);
52+
}
53+
54+
public void ClearPermissions() => _permissions.Clear();
55+
56+
public override string ToString()
57+
{
58+
return Name;
59+
}
60+
}
61+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
using System.Reflection;
2+
3+
namespace Dappi.HeadlessCms.UsersAndPermissions.Core
4+
{
5+
public interface IRoleStage<TUser>
6+
where TUser : IAppUser
7+
{
8+
IControllerStage<TUser> ForRole(string roleName, bool isDefaultForAuthenticated = false);
9+
AppRole[] Build();
10+
}
11+
12+
public interface IControllerStage<TUser>
13+
where TUser : IAppUser
14+
{
15+
IPermissionStage<TUser> ForController(string controllerName);
16+
}
17+
18+
public interface IPermissionStage<TUser>
19+
where TUser : IAppUser
20+
{
21+
IPermissionStage<TUser> Allow(string methodName);
22+
IPermissionStage<TUser> AllowAll();
23+
IPermissionStage<TUser> Deny(string methodName);
24+
IPermissionStage<TUser> ForController(string controllerName);
25+
IRoleStage<TUser> And();
26+
}
27+
28+
public class AppRoleAndPermissionsBuilder<TUser>
29+
: IRoleStage<TUser>,
30+
IControllerStage<TUser>,
31+
IPermissionStage<TUser>
32+
where TUser : IAppUser
33+
{
34+
private record RoleEntry(
35+
string RoleName,
36+
string Controller,
37+
string Method,
38+
bool IsDefaultForAuthenticated
39+
);
40+
41+
private readonly List<RoleEntry> _entries = [];
42+
private string? _currentRole;
43+
private string? _currentController;
44+
private bool _currentIsDefaultForAuthenticated;
45+
46+
public static IRoleStage<TUser> Create() => new AppRoleAndPermissionsBuilder<TUser>();
47+
48+
public IControllerStage<TUser> ForRole(
49+
string roleName,
50+
bool isDefaultForAuthenticated = false
51+
)
52+
{
53+
_currentRole = roleName ?? throw new ArgumentNullException(nameof(roleName));
54+
_currentIsDefaultForAuthenticated = isDefaultForAuthenticated;
55+
return this;
56+
}
57+
58+
IPermissionStage<TUser> IControllerStage<TUser>.ForController(string controllerName) =>
59+
SetController(controllerName);
60+
61+
IPermissionStage<TUser> IPermissionStage<TUser>.ForController(string controllerName) =>
62+
SetController(controllerName);
63+
64+
public IPermissionStage<TUser> Allow(string methodName)
65+
{
66+
AddEntry(methodName);
67+
return this;
68+
}
69+
70+
public IPermissionStage<TUser> Deny(string methodName)
71+
{
72+
_entries.RemoveAll(e =>
73+
e.RoleName == _currentRole
74+
&& e.Controller == _currentController
75+
&& e.Method == methodName
76+
);
77+
return this;
78+
}
79+
80+
public IPermissionStage<TUser> AllowAll()
81+
{
82+
if (_currentController == null)
83+
throw new InvalidOperationException("Call ForController() before AllowAll().");
84+
85+
var controllerType =
86+
AppDomain
87+
.CurrentDomain.GetAssemblies()
88+
.SelectMany(a =>
89+
{
90+
try
91+
{
92+
return a.GetTypes();
93+
}
94+
catch
95+
{
96+
return Array.Empty<Type>();
97+
}
98+
})
99+
.FirstOrDefault(t => t.Name == _currentController)
100+
?? throw new InvalidOperationException(
101+
$"Controller type '{_currentController}' not found."
102+
);
103+
104+
var methods = controllerType
105+
.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
106+
.Where(m => !m.IsSpecialName)
107+
.Select(m => m.Name)
108+
.Distinct();
109+
110+
foreach (var method in methods)
111+
AddEntry(method);
112+
113+
return this;
114+
}
115+
116+
public IRoleStage<TUser> And() => this;
117+
118+
public AppRole[] Build()
119+
{
120+
return _entries
121+
.GroupBy(e => e.RoleName)
122+
.Select(roleGroup =>
123+
{
124+
var permissions = roleGroup
125+
.Select(e => new AppPermission($"{e.Controller}:{e.Method}", ""))
126+
.ToList();
127+
128+
var isDefault = roleGroup.First().IsDefaultForAuthenticated;
129+
130+
return isDefault
131+
? AppRole.CreateDefaultAuthenticatedUserRole(permissions)
132+
: new AppRole(roleGroup.Key, permissions);
133+
})
134+
.ToArray();
135+
}
136+
137+
private AppRoleAndPermissionsBuilder<TUser> SetController(string controllerName)
138+
{
139+
_currentController =
140+
controllerName ?? throw new ArgumentNullException(nameof(controllerName));
141+
return this;
142+
}
143+
144+
private void AddEntry(string methodName)
145+
{
146+
if (_currentRole == null)
147+
throw new InvalidOperationException("No role selected.");
148+
if (_currentController == null)
149+
throw new InvalidOperationException("No controller selected.");
150+
if (string.IsNullOrWhiteSpace(methodName))
151+
throw new ArgumentException("Method name required.", nameof(methodName));
152+
153+
var alreadyAdded = _entries.Any(e =>
154+
e.RoleName == _currentRole
155+
&& e.Controller == _currentController
156+
&& e.Method == methodName
157+
);
158+
159+
if (!alreadyAdded)
160+
_entries.Add(
161+
new RoleEntry(
162+
_currentRole,
163+
_currentController,
164+
methodName,
165+
_currentIsDefaultForAuthenticated
166+
)
167+
);
168+
}
169+
}
170+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Dappi.HeadlessCms.UsersAndPermissions.Core
2+
{
3+
public static class UsersAndPermissionsConstants
4+
{
5+
public static class DefaultRoles
6+
{
7+
public const string Authenticated = nameof(Authenticated);
8+
public const string Public = nameof(Public);
9+
}
10+
}
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Dappi.HeadlessCms.UsersAndPermissions.Core
2+
{
3+
public interface IAppUser
4+
{
5+
int RoleId { get; }
6+
AppRole? Role { get; }
7+
}
8+
}

0 commit comments

Comments
 (0)