diff --git a/CHANGELOG.md b/CHANGELOG.md index c84dc19a..66ddc98d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change log ------------------------------ +[Unreleased] +* Introduced dynamic role management with CRUD APIs and UI allowing custom permission assignments. + [1.10.0] * Use publish timeline virtual id to compare the version between client. To enable this feature the client should use version >=1.8.0 . diff --git a/src/AgileConfig.Server.Apisite/Controllers/AdminController.cs b/src/AgileConfig.Server.Apisite/Controllers/AdminController.cs index 9fab1f10..7167c1d9 100644 --- a/src/AgileConfig.Server.Apisite/Controllers/AdminController.cs +++ b/src/AgileConfig.Server.Apisite/Controllers/AdminController.cs @@ -79,7 +79,8 @@ private async Task LoginSuccessful(string userName) { var user = (await _userService.GetUsersByNameAsync(userName)).First(); var userRoles = await _userService.GetUserRolesAsync(user.Id); - var jwt = _jwtService.GetToken(user.Id, user.UserName, userRoles.Any(r => r == Role.Admin || r == Role.SuperAdmin)); + var jwt = _jwtService.GetToken(user.Id, user.UserName, + userRoles.Any(r => r.Id == SystemRoleConstants.AdminId || r.Id == SystemRoleConstants.SuperAdminId)); var userFunctions = await _permissionService.GetUserPermission(user.Id); _tinyEventBus.Fire(new LoginEvent(user.UserName)); @@ -89,7 +90,7 @@ private async Task LoginSuccessful(string userName) status = "ok", token = jwt, type = "Bearer", - currentAuthority = userRoles.Select(r => r.ToString()), + currentAuthority = userRoles.Select(r => r.Code), currentFunctions = userFunctions }; } @@ -140,7 +141,7 @@ public async Task OidcLoginByCode(string code) Source = UserSource.SSO }; await _userService.AddAsync(newUser); - await _userService.UpdateUserRolesAsync(newUser.Id, new List { Role.NormalUser }); + await _userService.UpdateUserRolesAsync(newUser.Id, new List { SystemRoleConstants.OperatorId }); } else if (user.Status == UserStatus.Deleted) { diff --git a/src/AgileConfig.Server.Apisite/Controllers/HomeController.cs b/src/AgileConfig.Server.Apisite/Controllers/HomeController.cs index 24cde12c..dea61ad2 100644 --- a/src/AgileConfig.Server.Apisite/Controllers/HomeController.cs +++ b/src/AgileConfig.Server.Apisite/Controllers/HomeController.cs @@ -69,7 +69,7 @@ public async Task Current() { userId = userId, userName, - currentAuthority = userRoles.Select(r => r.ToString()), + currentAuthority = userRoles.Select(r => r.Code), currentFunctions = userFunctions } }); diff --git a/src/AgileConfig.Server.Apisite/Controllers/RoleController.cs b/src/AgileConfig.Server.Apisite/Controllers/RoleController.cs new file mode 100644 index 00000000..ba1b917d --- /dev/null +++ b/src/AgileConfig.Server.Apisite/Controllers/RoleController.cs @@ -0,0 +1,178 @@ +using AgileConfig.Server.Apisite.Filters; +using AgileConfig.Server.Apisite.Models; +using AgileConfig.Server.Common; +using AgileConfig.Server.Common.Resources; +using AgileConfig.Server.Data.Entity; +using AgileConfig.Server.IService; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +namespace AgileConfig.Server.Apisite.Controllers +{ + [Authorize] + public class RoleController : Controller + { + private readonly IRoleService _roleService; + + private static readonly IReadOnlyList SupportedFunctions = new List + { + Functions.App_Add, + Functions.App_Edit, + Functions.App_Delete, + Functions.App_Auth, + + Functions.Config_Add, + Functions.Config_Edit, + Functions.Config_Delete, + Functions.Config_Publish, + Functions.Config_Offline, + + Functions.Node_Add, + Functions.Node_Delete, + + Functions.Client_Disconnect, + + Functions.User_Add, + Functions.User_Edit, + Functions.User_Delete, + + Functions.Role_Add, + Functions.Role_Edit, + Functions.Role_Delete + }; + + public RoleController(IRoleService roleService) + { + _roleService = roleService; + } + + [HttpGet] + public async Task List() + { + var roles = await _roleService.GetAllAsync(); + // Filter out Super Administrator role to prevent it from being assigned through the frontend + var vms = roles + .Where(r => r.Id != SystemRoleConstants.SuperAdminId) + .Select(ToViewModel) + .OrderByDescending(r => r.IsSystem) + .ThenBy(r => r.Name) + .ToList(); + + return Json(new + { + success = true, + data = vms + }); + } + + [HttpGet] + public IActionResult SupportedPermissions() + { + return Json(new + { + success = true, + data = SupportedFunctions + }); + } + + [TypeFilter(typeof(PermissionCheckAttribute), Arguments = new object[] { "Role.Add", Functions.Role_Add })] + [HttpPost] + public async Task Add([FromBody] RoleVM model) + { + if (model == null) + { + throw new ArgumentNullException(nameof(model)); + } + + var role = new Role + { + Id = model.Id, + Name = model.Name, + Description = model.Description ?? string.Empty, + IsSystem = false + }; + + await _roleService.CreateAsync(role, model.Functions ?? Enumerable.Empty()); + + return Json(new { success = true }); + } + + [TypeFilter(typeof(PermissionCheckAttribute), Arguments = new object[] { "Role.Edit", Functions.Role_Edit })] + [HttpPost] + public async Task Edit([FromBody] RoleVM model) + { + if (model == null) + { + throw new ArgumentNullException(nameof(model)); + } + + var role = new Role() + { + Id = model.Id, + Name = model.Name, + Description = model.Description ?? string.Empty, + IsSystem = model.IsSystem + }; + + var result = await _roleService.UpdateAsync(role, model.Functions ?? Enumerable.Empty()); + + return Json(new + { + success = result, + message = result ? string.Empty : Messages.UpdateRoleFailed + }); + } + + [TypeFilter(typeof(PermissionCheckAttribute), Arguments = new object[] { "Role.Delete", Functions.Role_Delete })] + [HttpPost] + public async Task Delete(string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException(nameof(id)); + } + + var result = await _roleService.DeleteAsync(id); + return Json(new + { + success = result, + message = result ? string.Empty : Messages.DeleteRoleFailed + }); + } + + private static RoleVM ToViewModel(Role role) + { + return new RoleVM + { + Id = role.Id, + Name = role.Name, + Description = role.Description, + IsSystem = role.IsSystem, + Functions = ParseFunctions(role.FunctionsJson) + }; + } + + private static List ParseFunctions(string json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return new List(); + } + + try + { + var funcs = JsonSerializer.Deserialize>(json); + return funcs ?? new List(); + } + catch + { + return new List(); + } + } + } +} diff --git a/src/AgileConfig.Server.Apisite/Controllers/UserController.cs b/src/AgileConfig.Server.Apisite/Controllers/UserController.cs index 2057e8b2..43891047 100644 --- a/src/AgileConfig.Server.Apisite/Controllers/UserController.cs +++ b/src/AgileConfig.Server.Apisite/Controllers/UserController.cs @@ -66,8 +66,9 @@ public async Task Search(string userName, string team, int curren Id = item.Id, UserName = item.UserName, Team = item.Team, - UserRoles = roles, - UserRoleNames = roles.Select(r => r.ToDesc()).ToList() + UserRoleIds = roles.Select(r => r.Id).ToList(), + UserRoleNames = roles.Select(r => r.Name).ToList(), + UserRoleCodes = roles.Select(r => r.Code).ToList() }; vms.Add(vm); } @@ -112,7 +113,13 @@ public async Task Add([FromBody] UserVM model) user.UserName = model.UserName; var addUserResult = await _userService.AddAsync(user); - var addUserRoleResult = await _userService.UpdateUserRolesAsync(user.Id, model.UserRoles); + var roleIds = model.UserRoleIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList() ?? new List(); + if (!roleIds.Any()) + { + roleIds.Add(SystemRoleConstants.OperatorId); + } + + var addUserRoleResult = await _userService.UpdateUserRolesAsync(user.Id, roleIds); if (addUserResult) { @@ -149,7 +156,13 @@ public async Task Edit([FromBody] UserVM model) user.UpdateTime = DateTime.Now; var result = await _userService.UpdateAsync(user); - var reuslt1 = await _userService.UpdateUserRolesAsync(user.Id, model.UserRoles); + var roleIds = model.UserRoleIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList() ?? new List(); + if (!roleIds.Any()) + { + roleIds.Add(SystemRoleConstants.OperatorId); + } + + var reuslt1 = await _userService.UpdateUserRolesAsync(user.Id, roleIds); if (result) { @@ -235,7 +248,7 @@ public async Task Delete(string userId) [HttpGet] public async Task AdminUsers() { - var adminUsers = await _userService.GetUsersByRoleAsync(Role.Admin); + var adminUsers = await _userService.GetUsersByRoleAsync(SystemRoleConstants.AdminId); adminUsers = adminUsers.Where(x => x.Status == UserStatus.Normal).ToList(); return Json(new { diff --git a/src/AgileConfig.Server.Apisite/Filters/PermissionCheckAttribute.cs b/src/AgileConfig.Server.Apisite/Filters/PermissionCheckAttribute.cs index 21cc5083..5e6752c5 100644 --- a/src/AgileConfig.Server.Apisite/Filters/PermissionCheckAttribute.cs +++ b/src/AgileConfig.Server.Apisite/Filters/PermissionCheckAttribute.cs @@ -191,7 +191,6 @@ protected static readonly } }.ToFrozenDictionary(); - protected const string GlobalMatchPatten = "GLOBAL_{0}"; protected const string AppMatchPatten = "APP_{0}_{1}"; private readonly IPermissionService _permissionService; @@ -232,8 +231,8 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context var userFunctions = await _permissionService.GetUserPermission(userId); //judge global - var matchKey = string.Format(GlobalMatchPatten, _functionKey); - if (userFunctions.Contains(matchKey)) + var matchKey = _functionKey; + if (userFunctions.Contains(_functionKey)) { await base.OnActionExecutionAsync(context, next); return; diff --git a/src/AgileConfig.Server.Apisite/Models/RoleVM.cs b/src/AgileConfig.Server.Apisite/Models/RoleVM.cs new file mode 100644 index 00000000..8b3d6191 --- /dev/null +++ b/src/AgileConfig.Server.Apisite/Models/RoleVM.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace AgileConfig.Server.Apisite.Models +{ + [ExcludeFromCodeCoverage] + public class RoleVM + { + public string Id { get; set; } + + [Required] + [MaxLength(128)] + public string Name { get; set; } + + [MaxLength(512)] + public string Description { get; set; } + + public bool IsSystem { get; set; } + + public List Functions { get; set; } + } +} diff --git a/src/AgileConfig.Server.Apisite/Models/UserVM.cs b/src/AgileConfig.Server.Apisite/Models/UserVM.cs index 7248bb03..1764623f 100644 --- a/src/AgileConfig.Server.Apisite/Models/UserVM.cs +++ b/src/AgileConfig.Server.Apisite/Models/UserVM.cs @@ -23,10 +23,12 @@ public class UserVM [MaxLength(50, ErrorMessage = "团队长度不能超过50位")] public string Team { get; set; } - public List UserRoles { get; set; } + public List UserRoleIds { get; set; } public List UserRoleNames { get; set; } + public List UserRoleCodes { get; set; } + public UserStatus Status { get; set; } } } diff --git a/src/AgileConfig.Server.Apisite/appsettings.Development.json b/src/AgileConfig.Server.Apisite/appsettings.Development.json index d53837f4..153499ac 100644 --- a/src/AgileConfig.Server.Apisite/appsettings.Development.json +++ b/src/AgileConfig.Server.Apisite/appsettings.Development.json @@ -31,7 +31,7 @@ "removeServiceInterval": 0, // Remove a service if it has been unresponsive longer than this many seconds; <= 0 keeps the service. Default is 0. "pathBase": "", // When using reverse proxies, set this to the base path (must start with /xxx). "adminConsole": true, - "saPassword": "123456", // Password for the super administrator account. + "saPassword": "", // Password for the super administrator account. "defaultApp": "myapp", // Default application to create on every restart. "cluster": false, // Cluster mode: automatically join the node list on startup, discover container IP, default port 5000 (ideal for Docker Compose). "preview_mode": false, diff --git a/src/AgileConfig.Server.Common/Resources/Messages.cs b/src/AgileConfig.Server.Common/Resources/Messages.cs index a2ae1ae5..5cb71c48 100644 --- a/src/AgileConfig.Server.Common/Resources/Messages.cs +++ b/src/AgileConfig.Server.Common/Resources/Messages.cs @@ -195,5 +195,7 @@ public static string GetString(string name, CultureInfo culture, params object[] public static string UpdateUserFailed => GetString(nameof(UpdateUserFailed)); public static string ResetUserPasswordFailed => GetString(nameof(ResetUserPasswordFailed)); public static string DeleteUserFailed => GetString(nameof(DeleteUserFailed)); + public static string UpdateRoleFailed => GetString(nameof(UpdateRoleFailed)); + public static string DeleteRoleFailed => GetString(nameof(DeleteRoleFailed)); } } diff --git a/src/AgileConfig.Server.Common/Resources/Messages.en-US.resx b/src/AgileConfig.Server.Common/Resources/Messages.en-US.resx index c04b66f9..9242dd2a 100644 --- a/src/AgileConfig.Server.Common/Resources/Messages.en-US.resx +++ b/src/AgileConfig.Server.Common/Resources/Messages.en-US.resx @@ -421,4 +421,10 @@ Failed to delete user, please check error logs + + Failed to update role, please check error logs + + + Failed to delete role, please check error logs + diff --git a/src/AgileConfig.Server.Common/Resources/Messages.zh-CN.resx b/src/AgileConfig.Server.Common/Resources/Messages.zh-CN.resx index 5249cfb0..1834d13e 100644 --- a/src/AgileConfig.Server.Common/Resources/Messages.zh-CN.resx +++ b/src/AgileConfig.Server.Common/Resources/Messages.zh-CN.resx @@ -421,4 +421,10 @@ 删除用户失败,请查看错误日志 + + 更新角色失败,请查看错误日志 + + + 删除角色失败,请查看错误日志 + diff --git a/src/AgileConfig.Server.Common/SystemRoleConstants.cs b/src/AgileConfig.Server.Common/SystemRoleConstants.cs new file mode 100644 index 00000000..1a768ed7 --- /dev/null +++ b/src/AgileConfig.Server.Common/SystemRoleConstants.cs @@ -0,0 +1,13 @@ +namespace AgileConfig.Server.Common +{ + public static class SystemRoleConstants + { + public const string SuperAdminId = "00000000-0000-0000-0000-000000000001"; + public const string AdminId = "00000000-0000-0000-0000-000000000002"; + public const string OperatorId = "00000000-0000-0000-0000-000000000003"; + + public const string SuperAdminCode = "SuperAdmin"; + public const string AdminCode = "Admin"; + public const string OperatorCode = "Operator"; + } +} diff --git a/src/AgileConfig.Server.Data.Abstraction/IRoleDefinitionRepository.cs b/src/AgileConfig.Server.Data.Abstraction/IRoleDefinitionRepository.cs new file mode 100644 index 00000000..0c948825 --- /dev/null +++ b/src/AgileConfig.Server.Data.Abstraction/IRoleDefinitionRepository.cs @@ -0,0 +1,8 @@ +using AgileConfig.Server.Data.Entity; + +namespace AgileConfig.Server.Data.Abstraction +{ + public interface IRoleDefinitionRepository : IRepository + { + } +} diff --git a/src/AgileConfig.Server.Data.Entity/RoleDefinition.cs b/src/AgileConfig.Server.Data.Entity/RoleDefinition.cs new file mode 100644 index 00000000..b8b5c5ee --- /dev/null +++ b/src/AgileConfig.Server.Data.Entity/RoleDefinition.cs @@ -0,0 +1,38 @@ +using FreeSql.DataAnnotations; +using MongoDB.Bson.Serialization.Attributes; +using System; +using AgileConfig.Server.Common; + +namespace AgileConfig.Server.Data.Entity +{ + [Table(Name = "agc_role")] + [OraclePrimaryKeyName("agc_role_pk")] + public class Role : IEntity + { + [Column(Name = "id", StringLength = 64)] + public string Id { get; set; } + + [Column(Name = "code", StringLength = 64)] + public string Code { get; set; } + + [Column(Name = "name", StringLength = 128)] + public string Name { get; set; } + + [Column(Name = "description", StringLength = 512)] + public string Description { get; set; } + + [Column(Name = "is_system")] + public bool IsSystem { get; set; } + + [Column(Name = "functions", StringLength = -1)] + public string FunctionsJson { get; set; } + + [Column(Name = "create_time")] + [BsonDateTimeOptions(Kind = DateTimeKind.Local)] + public DateTime CreateTime { get; set; } + + [Column(Name = "update_time")] + [BsonDateTimeOptions(Kind = DateTimeKind.Local)] + public DateTime? UpdateTime { get; set; } + } +} diff --git a/src/AgileConfig.Server.Data.Entity/UserRole.cs b/src/AgileConfig.Server.Data.Entity/UserRole.cs index 78b7b16a..018860c8 100644 --- a/src/AgileConfig.Server.Data.Entity/UserRole.cs +++ b/src/AgileConfig.Server.Data.Entity/UserRole.cs @@ -1,14 +1,13 @@ using FreeSql.DataAnnotations; using System; -using System.ComponentModel; -using MongoDB.Bson.Serialization.Attributes; using AgileConfig.Server.Common; +using MongoDB.Bson.Serialization.Attributes; namespace AgileConfig.Server.Data.Entity { [Table(Name = "agc_user_role")] [OraclePrimaryKeyName("agc_user_role_pk")] - public class UserRole: IEntity + public class UserRole : IEntity { [Column(Name = "id", StringLength = 36)] public string Id { get; set; } @@ -16,22 +15,14 @@ public class UserRole: IEntity [Column(Name = "user_id", StringLength = 50)] public string UserId { get; set; } + [Column(Name = "role_id", StringLength = 64)] + public string RoleId { get; set; } + [Column(Name = "role")] - public Role Role { get; set; } + public int? LegacyRoleValue { get; set; } [Column(Name = "create_time")] [BsonDateTimeOptions(Kind = DateTimeKind.Local)] public DateTime CreateTime { get; set; } - - } - - public enum Role - { - [Description("Super Administrator")] - SuperAdmin = 0, - [Description("Administrator")] - Admin = 1, - [Description("Operator")] - NormalUser = 2, } } diff --git a/src/AgileConfig.Server.Data.Freesql/EnsureTables.cs b/src/AgileConfig.Server.Data.Freesql/EnsureTables.cs index f3f29ae7..e198e59a 100644 --- a/src/AgileConfig.Server.Data.Freesql/EnsureTables.cs +++ b/src/AgileConfig.Server.Data.Freesql/EnsureTables.cs @@ -76,6 +76,7 @@ public static void Ensure(IFreeSql instance) instance.CodeFirst.SyncStructure(); instance.CodeFirst.SyncStructure(); instance.CodeFirst.SyncStructure(); + instance.CodeFirst.SyncStructure(); instance.CodeFirst.SyncStructure(); instance.CodeFirst.SyncStructure(); instance.CodeFirst.SyncStructure(); diff --git a/src/AgileConfig.Server.Data.Repository.Freesql/AgileConfig.Server.Data.Repository.Freesql.csproj b/src/AgileConfig.Server.Data.Repository.Freesql/AgileConfig.Server.Data.Repository.Freesql.csproj index 2173543c..929a7219 100644 --- a/src/AgileConfig.Server.Data.Repository.Freesql/AgileConfig.Server.Data.Repository.Freesql.csproj +++ b/src/AgileConfig.Server.Data.Repository.Freesql/AgileConfig.Server.Data.Repository.Freesql.csproj @@ -9,6 +9,7 @@ + diff --git a/src/AgileConfig.Server.Data.Repository.Freesql/FreesqlRepositoryServiceRegister.cs b/src/AgileConfig.Server.Data.Repository.Freesql/FreesqlRepositoryServiceRegister.cs index 55cf7580..732489cc 100644 --- a/src/AgileConfig.Server.Data.Repository.Freesql/FreesqlRepositoryServiceRegister.cs +++ b/src/AgileConfig.Server.Data.Repository.Freesql/FreesqlRepositoryServiceRegister.cs @@ -17,6 +17,7 @@ public void AddFixedRepositories(IServiceCollection sc) sc.AddScoped(); sc.AddScoped(); sc.AddScoped(); + sc.AddScoped(); sc.AddSingleton(); } diff --git a/src/AgileConfig.Server.Data.Repository.Freesql/RoleDefinitionRepository.cs b/src/AgileConfig.Server.Data.Repository.Freesql/RoleDefinitionRepository.cs new file mode 100644 index 00000000..02605e62 --- /dev/null +++ b/src/AgileConfig.Server.Data.Repository.Freesql/RoleDefinitionRepository.cs @@ -0,0 +1,13 @@ +using AgileConfig.Server.Data.Abstraction; +using AgileConfig.Server.Data.Entity; +using AgileConfig.Server.Data.Freesql; + +namespace AgileConfig.Server.Data.Repository.Freesql +{ + public class RoleDefinitionRepository : FreesqlRepository, IRoleDefinitionRepository + { + public RoleDefinitionRepository(IFreeSqlFactory freeSqlFactory) : base(freeSqlFactory.Create()) + { + } + } +} diff --git a/src/AgileConfig.Server.Data.Repository.Freesql/SysInitRepository.cs b/src/AgileConfig.Server.Data.Repository.Freesql/SysInitRepository.cs index e1cfbfe6..6e4325f7 100644 --- a/src/AgileConfig.Server.Data.Repository.Freesql/SysInitRepository.cs +++ b/src/AgileConfig.Server.Data.Repository.Freesql/SysInitRepository.cs @@ -1,7 +1,11 @@ -using AgileConfig.Server.Common; +using AgileConfig.Server.Common; using AgileConfig.Server.Data.Abstraction; using AgileConfig.Server.Data.Entity; using AgileConfig.Server.Data.Freesql; +using AgileConfig.Server.IService; +using System; +using System.Collections.Generic; +using System.Text.Json; namespace AgileConfig.Server.Data.Repository.Freesql; @@ -17,15 +21,15 @@ public SysInitRepository(IFreeSqlFactory freeSqlFactory) public string? GetDefaultEnvironmentFromDb() { var setting = freeSqlFactory.Create().Select().Where(x => x.Id == SystemSettings.DefaultEnvironmentKey) - .ToOne(); + .ToOne(); - return setting?.Value; - } + return setting?.Value; + } public string? GetJwtTokenSecret() { var setting = freeSqlFactory.Create().Select().Where(x => x.Id == SystemSettings.DefaultJwtSecretKey) - .ToOne(); + .ToOne(); return setting?.Value; } @@ -37,78 +41,183 @@ public void SaveInitSetting(Setting setting) public bool InitSa(string password) { - if (string.IsNullOrEmpty(password)) - { - throw new ArgumentNullException(nameof(password)); - } + if (string.IsNullOrEmpty(password)) + { + throw new ArgumentNullException(nameof(password)); + } var newSalt = Guid.NewGuid().ToString("N"); - password = Encrypt.Md5((password + newSalt)); + password = Encrypt.Md5((password + newSalt)); - var sql = freeSqlFactory.Create(); + var sql = freeSqlFactory.Create(); + + EnsureSystemRoles(sql); - var user = new User(); + var user = new User(); user.Id = SystemSettings.SuperAdminId; - user.Password = password; + user.Password = password; user.Salt = newSalt; user.Status = UserStatus.Normal; user.Team = ""; user.CreateTime = DateTime.Now; user.UserName = SystemSettings.SuperAdminUserName; - sql.Insert(user).ExecuteAffrows(); + sql.Insert(user).ExecuteAffrows(); + var now = DateTime.Now; var userRoles = new List(); - userRoles.Add(new UserRole() - { - Id = Guid.NewGuid().ToString("N"), - Role = Role.SuperAdmin, - UserId = SystemSettings.SuperAdminId + userRoles.Add(new UserRole() + { + Id = Guid.NewGuid().ToString("N"), + RoleId = SystemRoleConstants.SuperAdminId, + UserId = SystemSettings.SuperAdminId, + CreateTime = now }); - userRoles.Add(new UserRole() - { - Id = Guid.NewGuid().ToString("N"), - Role = Role.Admin, - UserId = SystemSettings.SuperAdminId + userRoles.Add(new UserRole() + { + Id = Guid.NewGuid().ToString("N"), +RoleId = SystemRoleConstants.AdminId, + UserId = SystemSettings.SuperAdminId, + CreateTime = now }); sql.Insert(userRoles).ExecuteAffrows(); - return true; + return true; } - public bool HasSa() + public bool HasSa() { - var anySa = freeSqlFactory.Create().Select().Any(x => x.Id == SystemSettings.SuperAdminId); + var anySa = freeSqlFactory.Create().Select().Any(x => x.Id == SystemSettings.SuperAdminId); - return anySa; + return anySa; } public bool InitDefaultApp(string appName) { if (string.IsNullOrEmpty(appName)) { - throw new ArgumentNullException(nameof(appName)); - } + throw new ArgumentNullException(nameof(appName)); + } var sql = freeSqlFactory.Create(); var anyDefaultApp = sql.Select().Any(x => x.Id == appName); - ; - if (!anyDefaultApp) - { - sql.Insert(new App() - { - Id = appName, - Name = appName, - Group = "", - Secret = "", - CreateTime = DateTime.Now, - Enabled = true, - Type = AppType.PRIVATE, - AppAdmin = SystemSettings.SuperAdminId - }).ExecuteAffrows(); + ; + if (!anyDefaultApp) + { + sql.Insert(new App() + { + Id = appName, + Name = appName, + Group = "", + Secret = "", +CreateTime = DateTime.Now, + Enabled = true, + Type = AppType.PRIVATE, + AppAdmin = SystemSettings.SuperAdminId + }).ExecuteAffrows(); } - return true; + return true; + } + + private static void EnsureSystemRoles(IFreeSql sql) + { + // Super Admin gets all permissions + var superAdminPermissions = GetSuperAdminPermissions(); +EnsureRole(sql, SystemRoleConstants.SuperAdminId, SystemRoleConstants.SuperAdminCode, "Super Administrator", superAdminPermissions); + + // Admin gets all permissions + var adminPermissions = GetAdminPermissions(); + EnsureRole(sql, SystemRoleConstants.AdminId, SystemRoleConstants.AdminCode, "Administrator", adminPermissions); + + // Operator gets limited permissions + var operatorPermissions = GetOperatorPermissions(); + EnsureRole(sql, SystemRoleConstants.OperatorId, SystemRoleConstants.OperatorCode, "Operator", operatorPermissions); + } + + private static List GetSuperAdminPermissions() + { +// SuperAdmin has all permissions +return new List + { + Functions.App_Add, + Functions.App_Edit, + Functions.App_Delete, + Functions.App_Auth, + + Functions.Config_Add, + Functions.Config_Edit, + Functions.Config_Delete, + Functions.Config_Publish, + Functions.Config_Offline, + + Functions.Node_Add, + Functions.Node_Delete, + + Functions.Client_Disconnect, + + Functions.User_Add, + Functions.User_Edit, + Functions.User_Delete, + + Functions.Role_Add, + Functions.Role_Edit, + Functions.Role_Delete + }; + } + + private static List GetAdminPermissions() + { + // Admin has all permissions same as SuperAdmin + return GetSuperAdminPermissions(); + } + + private static List GetOperatorPermissions() + { + // Operator has limited permissions: + // - App: Add, Edit + // - Config: Add, Edit, Delete, Publish, Offline + return new List + { + Functions.App_Add, + Functions.App_Edit, + + Functions.Config_Add, +Functions.Config_Edit, + Functions.Config_Delete, + Functions.Config_Publish, + Functions.Config_Offline + }; + } + + private static void EnsureRole(IFreeSql sql, string id, string code, string name, List functions) + { + var role = sql.Select().Where(x => x.Id == id).First(); + var functionsJson = JsonSerializer.Serialize(functions); + + if (role == null) + { + sql.Insert(new Role + { + Id = id, + Code = code, +Name = name, + Description = name, + IsSystem = true, + FunctionsJson = functionsJson, + CreateTime = DateTime.Now + }).ExecuteAffrows(); + } + else + { + role.Code = code; + role.Name = name; + role.Description = name; + role.IsSystem = true; + role.FunctionsJson = functionsJson; + role.UpdateTime = DateTime.Now; + sql.Update().SetSource(role).ExecuteAffrows(); + } } -} \ No newline at end of file +} diff --git a/src/AgileConfig.Server.Data.Repository.Mongodb/MongodbRepositoryServiceRegister.cs b/src/AgileConfig.Server.Data.Repository.Mongodb/MongodbRepositoryServiceRegister.cs index 8ad9bf5a..6f0c23c8 100644 --- a/src/AgileConfig.Server.Data.Repository.Mongodb/MongodbRepositoryServiceRegister.cs +++ b/src/AgileConfig.Server.Data.Repository.Mongodb/MongodbRepositoryServiceRegister.cs @@ -1,4 +1,5 @@ -using AgileConfig.Server.Data.Abstraction.DbProvider; +using AgileConfig.Server.Data.Abstraction; +using AgileConfig.Server.Data.Abstraction.DbProvider; namespace AgileConfig.Server.Data.Repository.Mongodb { @@ -15,6 +16,7 @@ public void AddFixedRepositories(IServiceCollection sc) sc.AddScoped(); sc.AddScoped(); sc.AddScoped(); + sc.AddScoped(); sc.AddSingleton(); } diff --git a/src/AgileConfig.Server.Data.Repository.Mongodb/RoleDefinitionRepository.cs b/src/AgileConfig.Server.Data.Repository.Mongodb/RoleDefinitionRepository.cs new file mode 100644 index 00000000..b3f7a870 --- /dev/null +++ b/src/AgileConfig.Server.Data.Repository.Mongodb/RoleDefinitionRepository.cs @@ -0,0 +1,17 @@ +using AgileConfig.Server.Data.Abstraction; +using AgileConfig.Server.Data.Entity; +using Microsoft.Extensions.Configuration; + +namespace AgileConfig.Server.Data.Repository.Mongodb +{ + public class RoleDefinitionRepository : MongodbRepository, IRoleDefinitionRepository + { + public RoleDefinitionRepository(string? connectionString) : base(connectionString) + { + } + + public RoleDefinitionRepository(IConfiguration configuration) : base(configuration) + { + } + } +} diff --git a/src/AgileConfig.Server.Data.Repository.Mongodb/SysInitRepository.cs b/src/AgileConfig.Server.Data.Repository.Mongodb/SysInitRepository.cs index f2a7b174..6d40e5b1 100644 --- a/src/AgileConfig.Server.Data.Repository.Mongodb/SysInitRepository.cs +++ b/src/AgileConfig.Server.Data.Repository.Mongodb/SysInitRepository.cs @@ -1,4 +1,9 @@ using AgileConfig.Server.Common; +using AgileConfig.Server.Data.Entity; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Text.Json; namespace AgileConfig.Server.Data.Repository.Mongodb; @@ -14,6 +19,7 @@ public SysInitRepository(IConfiguration configuration) private MongodbAccess _settingAccess => new MongodbAccess(_connectionString); private MongodbAccess _userAccess => new MongodbAccess(_connectionString); private MongodbAccess _userRoleAccess => new MongodbAccess(_connectionString); + private MongodbAccess _roleAccess => new MongodbAccess(_connectionString); private MongodbAccess _appAccess => new MongodbAccess(_connectionString); private readonly IConfiguration _configuration; @@ -48,6 +54,8 @@ public bool InitSa(string password) var newSalt = Guid.NewGuid().ToString("N"); password = Encrypt.Md5((password + newSalt)); + EnsureSystemRoles(); + var user = new User(); user.Id = SystemSettings.SuperAdminId; user.Password = password; @@ -59,18 +67,21 @@ public bool InitSa(string password) _userAccess.Collection.InsertOne(user); + var now = DateTime.Now; var userRoles = new List(); userRoles.Add(new UserRole() { Id = Guid.NewGuid().ToString("N"), - Role = Role.SuperAdmin, - UserId = SystemSettings.SuperAdminId + RoleId = SystemRoleConstants.SuperAdminId, + UserId = SystemSettings.SuperAdminId, + CreateTime = now }); userRoles.Add(new UserRole() { Id = Guid.NewGuid().ToString("N"), - Role = Role.Admin, - UserId = SystemSettings.SuperAdminId + RoleId = SystemRoleConstants.AdminId, + UserId = SystemSettings.SuperAdminId, + CreateTime = now }); _userRoleAccess.Collection.InsertMany(userRoles); @@ -111,4 +122,39 @@ public bool InitDefaultApp(string appName) return true; } + + private void EnsureSystemRoles() + { + EnsureRole(SystemRoleConstants.SuperAdminId, SystemRoleConstants.SuperAdminCode, "Super Administrator"); + EnsureRole(SystemRoleConstants.AdminId, SystemRoleConstants.AdminCode, "Administrator"); + EnsureRole(SystemRoleConstants.OperatorId, SystemRoleConstants.OperatorCode, "Operator"); + } + + private void EnsureRole(string id, string code, string name) + { + var role = _roleAccess.MongoQueryable.FirstOrDefault(x => x.Id == id); + if (role == null) + { + _roleAccess.Collection.InsertOne(new Role + { + Id = id, + Code = code, + Name = name, + Description = name, + IsSystem = true, + FunctionsJson = JsonSerializer.Serialize(new List()), + CreateTime = DateTime.Now + }); + } + else + { + role.Code = code; + role.Name = name; + role.Description = name; + role.IsSystem = true; + role.FunctionsJson = role.FunctionsJson ?? JsonSerializer.Serialize(new List()); + role.UpdateTime = DateTime.Now; + _roleAccess.Collection.ReplaceOne(x => x.Id == id, role, new ReplaceOptions { IsUpsert = true }); + } + } } \ No newline at end of file diff --git a/src/AgileConfig.Server.IService/IPermissionService.cs b/src/AgileConfig.Server.IService/IPermissionService.cs index 8ec6f6b8..a3a57104 100644 --- a/src/AgileConfig.Server.IService/IPermissionService.cs +++ b/src/AgileConfig.Server.IService/IPermissionService.cs @@ -25,6 +25,10 @@ public static class Functions public const string User_Add = "USER_ADD"; public const string User_Edit = "USER_EDIT"; public const string User_Delete = "USER_DELETE"; + + public const string Role_Add = "ROLE_ADD"; + public const string Role_Edit = "ROLE_EDIT"; + public const string Role_Delete = "ROLE_DELETE"; } public interface IPermissionService diff --git a/src/AgileConfig.Server.IService/IRoleService.cs b/src/AgileConfig.Server.IService/IRoleService.cs new file mode 100644 index 00000000..d5abaea7 --- /dev/null +++ b/src/AgileConfig.Server.IService/IRoleService.cs @@ -0,0 +1,16 @@ +using AgileConfig.Server.Data.Entity; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AgileConfig.Server.IService +{ + public interface IRoleService + { + Task> GetAllAsync(); + Task GetAsync(string id); + Task GetByCodeAsync(string code); + Task CreateAsync(Role role, IEnumerable functions); + Task UpdateAsync(Role role, IEnumerable functions); + Task DeleteAsync(string id); + } +} diff --git a/src/AgileConfig.Server.IService/IUserService.cs b/src/AgileConfig.Server.IService/IUserService.cs index 9bc566fb..29ed7639 100644 --- a/src/AgileConfig.Server.IService/IUserService.cs +++ b/src/AgileConfig.Server.IService/IUserService.cs @@ -21,12 +21,12 @@ public interface IUserService: IDisposable Task UpdateAsync(User user); - Task UpdateUserRolesAsync(string userId, List roles); + Task UpdateUserRolesAsync(string userId, List roleIds); Task ValidateUserPassword(string userName, string password); - Task> GetUsersByRoleAsync(Role role); + Task> GetUsersByRoleAsync(string roleId); } } diff --git a/src/AgileConfig.Server.Service/PermissionService.cs b/src/AgileConfig.Server.Service/PermissionService.cs index 018d39a1..44363908 100644 --- a/src/AgileConfig.Server.Service/PermissionService.cs +++ b/src/AgileConfig.Server.Service/PermissionService.cs @@ -1,246 +1,288 @@ -using AgileConfig.Server.Data.Entity; +using AgileConfig.Server.Data.Entity; using AgileConfig.Server.IService; using System.Collections.Generic; using System.Threading.Tasks; using System.Linq; using AgileConfig.Server.Data.Abstraction; +using AgileConfig.Server.Common; +using System.Text.Json; namespace AgileConfig.Server.Service { public class PermissionService : IPermissionService { - private readonly IUserRoleRepository _userRoleRepository; - private readonly IUserAppAuthRepository _userAppAuthRepository; - private readonly IAppRepository _appRepository; - - public PermissionService( - IUserRoleRepository userRoleRepository, - IUserAppAuthRepository userAppAuthRepository, - IAppRepository appRepository) + private readonly IUserRoleRepository _userRoleRepository; + private readonly IRoleDefinitionRepository _roleDefinitionRepository; + private readonly IUserAppAuthRepository _userAppAuthRepository; + private readonly IAppRepository _appRepository; + + public PermissionService( + IUserRoleRepository userRoleRepository, + IRoleDefinitionRepository roleDefinitionRepository, + IUserAppAuthRepository userAppAuthRepository, + IAppRepository appRepository) { _userRoleRepository = userRoleRepository; - _userAppAuthRepository = userAppAuthRepository; - _appRepository = appRepository; - } + _roleDefinitionRepository = roleDefinitionRepository; + _userAppAuthRepository = userAppAuthRepository; + _appRepository = appRepository; + } private static readonly List Template_SuperAdminPermissions = [ - "GLOBAL_" + Functions.App_Add, - "GLOBAL_" + Functions.App_Delete, - "GLOBAL_" + Functions.App_Edit, - "GLOBAL_" + Functions.App_Auth, + Functions.App_Add, + Functions.App_Delete, + Functions.App_Edit, + Functions.App_Auth, - "GLOBAL_" + Functions.Config_Add, - "GLOBAL_" + Functions.Config_Delete, - "GLOBAL_" + Functions.Config_Edit, - "GLOBAL_" + Functions.Config_Offline, - "GLOBAL_" + Functions.Config_Publish, + Functions.Config_Add, + Functions.Config_Delete, + Functions.Config_Edit, + Functions.Config_Offline, + Functions.Config_Publish, - "GLOBAL_" + Functions.Node_Add, - "GLOBAL_" + Functions.Node_Delete, + Functions.Node_Add, + Functions.Node_Delete, - "GLOBAL_" + Functions.Client_Disconnect, +Functions.Client_Disconnect, - "GLOBAL_" + Functions.User_Add, - "GLOBAL_" + Functions.User_Edit, - "GLOBAL_" + Functions.User_Delete + Functions.User_Add, + Functions.User_Edit, + Functions.User_Delete, + Functions.Role_Add, + Functions.Role_Edit, + Functions.Role_Delete ]; private static readonly List Template_NormalAdminPermissions = [ - "GLOBAL_" + Functions.App_Add, - "GLOBAL_" + Functions.Node_Add, - "GLOBAL_" + Functions.Node_Delete, - "GLOBAL_" + Functions.Client_Disconnect, + Functions.App_Add, + Functions.Node_Add, + Functions.Node_Delete, + Functions.Client_Disconnect, - "GLOBAL_" + Functions.User_Add, - "GLOBAL_" + Functions.User_Edit, - "GLOBAL_" + Functions.User_Delete, + Functions.User_Add, + Functions.User_Edit, + Functions.User_Delete, + Functions.Role_Add, + Functions.Role_Edit, + Functions.Role_Delete, - "APP_{0}_" + Functions.App_Delete, - "APP_{0}_" + Functions.App_Edit, - "APP_{0}_" + Functions.App_Auth, + "APP_{0}_" + Functions.App_Delete, + "APP_{0}_" + Functions.App_Edit, + "APP_{0}_" + Functions.App_Auth, - "APP_{0}_" + Functions.Config_Add, - "APP_{0}_" + Functions.Config_Delete, - "APP_{0}_" + Functions.Config_Edit, - "APP_{0}_" + Functions.Config_Offline, - "APP_{0}_" + Functions.Config_Publish - ]; + "APP_{0}_" + Functions.Config_Add, + "APP_{0}_" + Functions.Config_Delete, + "APP_{0}_" + Functions.Config_Edit, + "APP_{0}_" + Functions.Config_Offline, + "APP_{0}_" + Functions.Config_Publish + ]; - private static readonly List Template_NormalUserPermissions_Edit = + private static readonly List Template_NormalUserPermissions_Edit = [ "APP_{0}_" + Functions.Config_Add, - "APP_{0}_" + Functions.Config_Delete, - "APP_{0}_" + Functions.Config_Edit + "APP_{0}_" + Functions.Config_Delete, + "APP_{0}_" + Functions.Config_Edit ]; private static readonly List Template_NormalUserPermissions_Publish = [ "APP_{0}_" + Functions.Config_Offline, - "APP_{0}_" + Functions.Config_Publish - ]; + "APP_{0}_" + Functions.Config_Publish + ]; - private async Task> GetAdminUserFunctions(string userId) + private async Task> GetAdminUserFunctions(string userId) { var userFunctions = new List(); - // Retrieve applications where the user is an administrator. - var adminApps = await GetUserAdminApps(userId); - Template_NormalAdminPermissions.Where(x => x.StartsWith("GLOBAL_")).ToList().ForEach( - key => { - userFunctions.Add(key); - } - ); - Template_NormalUserPermissions_Edit.Where(x => x.StartsWith("GLOBAL_")).ToList().ForEach( - key => { - userFunctions.Add(key); - } - ); - Template_NormalUserPermissions_Publish.Where(x => x.StartsWith("GLOBAL_")).ToList().ForEach( - key => { - userFunctions.Add(key); - } - ); - foreach (var app in adminApps) - { - foreach (var temp in Template_NormalAdminPermissions) - { - if (temp.StartsWith("APP_{0}_")) - { - userFunctions.Add(string.Format(temp, app.Id)); - } + // Retrieve applications where the user is an administrator. + var adminApps = await GetUserAdminApps(userId); + Template_NormalAdminPermissions.Where(x => !x.StartsWith("APP_")).ToList().ForEach( + key => { + userFunctions.Add(key); + } + ); + Template_NormalUserPermissions_Edit.Where(x => !x.StartsWith("APP_")).ToList().ForEach( + key => { + userFunctions.Add(key); + } + ); + Template_NormalUserPermissions_Publish.Where(x => !x.StartsWith("APP_")).ToList().ForEach( + key => { + userFunctions.Add(key); + } + ); + foreach (var app in adminApps) + { + foreach (var temp in Template_NormalAdminPermissions) + { + if (temp.StartsWith("APP_{0}_")) + { + userFunctions.Add(string.Format(temp, app.Id)); + } } - } + } //EditConfigPermissionKey var editPermissionApps = await GetUserAuthApp(userId, EditConfigPermissionKey); - foreach (var app in editPermissionApps) - { - foreach (var temp in Template_NormalUserPermissions_Edit) - { - if (temp.StartsWith("APP_{0}_")) - { - userFunctions.Add(string.Format(temp, app.Id)); - } - } + foreach (var app in editPermissionApps) + { + foreach (var temp in Template_NormalUserPermissions_Edit) + { + if (temp.StartsWith("APP_{0}_")) + { + userFunctions.Add(string.Format(temp, app.Id)); + } + } } - //PublishConfigPermissionKey - var publishPermissionApps = await GetUserAuthApp(userId, PublishConfigPermissionKey); - foreach (var app in publishPermissionApps) + //PublishConfigPermissionKey + var publishPermissionApps = await GetUserAuthApp(userId, PublishConfigPermissionKey); + foreach (var app in publishPermissionApps) { - foreach (var temp in Template_NormalUserPermissions_Publish) - { - if (temp.StartsWith("APP_{0}_")) - { - userFunctions.Add(string.Format(temp, app.Id)); - } - } - } + foreach (var temp in Template_NormalUserPermissions_Publish) + { + if (temp.StartsWith("APP_{0}_")) + { + userFunctions.Add(string.Format(temp, app.Id)); + } + } + } return userFunctions; - } + } private async Task> GetNormalUserFunctions(string userId) { - var userFunctions = new List(); - //EditConfigPermissionKey - var editPermissionApps = await GetUserAuthApp(userId, EditConfigPermissionKey); + var userFunctions = new List(); + //EditConfigPermissionKey + var editPermissionApps = await GetUserAuthApp(userId, EditConfigPermissionKey); foreach (var app in editPermissionApps) - { + { foreach (var temp in Template_NormalUserPermissions_Edit) { - if (temp.StartsWith("GLOBAL_")) - { - userFunctions.Add(temp); - } - if (temp.StartsWith("APP_{0}_")) - { - userFunctions.Add(string.Format(temp, app.Id)); - } - } - } - //PublishConfigPermissionKey - var publishPermissionApps = await GetUserAuthApp(userId, PublishConfigPermissionKey); - foreach (var app in publishPermissionApps) + if (!temp.StartsWith("APP_")) + { + userFunctions.Add(temp); + } + if (temp.StartsWith("APP_{0}_")) { + userFunctions.Add(string.Format(temp, app.Id)); + } + } + } + //PublishConfigPermissionKey + var publishPermissionApps = await GetUserAuthApp(userId, PublishConfigPermissionKey); + foreach (var app in publishPermissionApps) + { foreach (var temp in Template_NormalUserPermissions_Publish) - { - if (temp.StartsWith("GLOBAL_")) - { - userFunctions.Add(temp); - } - if (temp.StartsWith("APP_{0}_")) - { - userFunctions.Add(string.Format(temp, app.Id)); - } - } - } + { + if (!temp.StartsWith("APP_")) + { + userFunctions.Add(temp); + } + if (temp.StartsWith("APP_{0}_")) + { + userFunctions.Add(string.Format(temp, app.Id)); + } + } + } return userFunctions; } /// - /// Retrieve the permission template for a user based on roles. + /// Retrieve the permission template for a user based on roles. /// - /// Identifier of the user requesting permissions. + /// Identifier of the user requesting permissions. /// List of permission keys granted to the user. public async Task> GetUserPermission(string userId) { - var userRoles = await _userRoleRepository.QueryAsync(x => x.UserId == userId); - if (userRoles.Any(x=>x.Role == Role.SuperAdmin)) - { - return Template_SuperAdminPermissions; - } + var userRoles = await _userRoleRepository.QueryAsync(x => x.UserId == userId); + var roleIds = userRoles.Select(x => x.RoleId).Distinct().ToList(); + if (!roleIds.Any()) + { + return new List(); + } - var userFunctions = new List(); - // Compute permissions for regular administrators. - if (userRoles.Any(x=>x.Role == Role.Admin)) + var roleDefinitions = await _roleDefinitionRepository.QueryAsync(x => roleIds.Contains(x.Id)); + var systemRoles = roleDefinitions.Where(r => r.IsSystem).ToList(); + var customRoles = roleDefinitions.Where(r => !r.IsSystem).ToList(); + + var customFunctions = customRoles.SelectMany(GetRoleFunctions).ToList(); + + if (systemRoles.Any(r => r.Id == SystemRoleConstants.SuperAdminId)) + { + return Template_SuperAdminPermissions.Concat(customFunctions).Distinct().ToList(); + } + + var userFunctions = new List(); + if (systemRoles.Any(r => r.Id == SystemRoleConstants.AdminId)) { - userFunctions.AddRange(await GetAdminUserFunctions(userId)); + userFunctions.AddRange(await GetAdminUserFunctions(userId)); } - // Compute permissions for regular users. - if (userRoles.Any(x => x.Role == Role.NormalUser)) + + if (systemRoles.Any(r => r.Id == SystemRoleConstants.OperatorId)) { - userFunctions.AddRange(await GetNormalUserFunctions(userId)); + userFunctions.AddRange(await GetNormalUserFunctions(userId)); + } + + userFunctions.AddRange(customFunctions); + + return userFunctions.Distinct().ToList(); + } + + private static IEnumerable GetRoleFunctions(Role role) + { + if (role == null || string.IsNullOrWhiteSpace(role.FunctionsJson)) + { + return Enumerable.Empty(); } - return userFunctions.Distinct().ToList(); + try + { + var functions = JsonSerializer.Deserialize>(role.FunctionsJson); + return functions ?? Enumerable.Empty(); + } + catch + { + return Enumerable.Empty(); + } } - /// - /// Retrieve applications where the user has been explicitly authorized. + /// + /// Retrieve applications where the user has been explicitly authorized. /// - /// Identifier of the user whose application authorizations are requested. - /// Permission key used to filter authorized applications. + /// Identifier of the user whose application authorizations are requested. + /// Permission key used to filter authorized applications. /// List of applications the user can access for the specified permission. private async Task> GetUserAuthApp(string userId, string authPermissionKey) { var apps = new List(); - var userAuths = - await _userAppAuthRepository.QueryAsync(x => x.UserId == userId && x.Permission == authPermissionKey); - foreach (var appAuth in userAuths) - { + var userAuths = + await _userAppAuthRepository.QueryAsync(x => x.UserId == userId && x.Permission == authPermissionKey); + foreach (var appAuth in userAuths) + { var app = await _appRepository.GetAsync(appAuth.AppId); - if (app!= null) - { - apps.Add(app); - } - } + if (app!= null) + { + apps.Add(app); + } + } - return apps; - } + return apps; + } - /// - /// Retrieve applications managed by the user. + /// + /// Retrieve applications managed by the user. /// /// Identifier of the user who administers the applications. /// List of applications where the user is the administrator. private async Task> GetUserAdminApps(string userId) { - return await _appRepository.QueryAsync(x => x.AppAdmin == userId); + return await _appRepository.QueryAsync(x => x.AppAdmin == userId); } public string EditConfigPermissionKey => "EDIT_CONFIG"; - public string PublishConfigPermissionKey => "PUBLISH_CONFIG"; + public string PublishConfigPermissionKey => "PUBLISH_CONFIG"; } } diff --git a/src/AgileConfig.Server.Service/RoleService.cs b/src/AgileConfig.Server.Service/RoleService.cs new file mode 100644 index 00000000..7edb9c70 --- /dev/null +++ b/src/AgileConfig.Server.Service/RoleService.cs @@ -0,0 +1,139 @@ +using AgileConfig.Server.Data.Abstraction; +using AgileConfig.Server.Data.Entity; +using AgileConfig.Server.IService; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +namespace AgileConfig.Server.Service +{ + public class RoleService : IRoleService + { + private readonly IRoleDefinitionRepository _roleDefinitionRepository; + private readonly IUserRoleRepository _userRoleRepository; + + public RoleService(IRoleDefinitionRepository roleDefinitionRepository, IUserRoleRepository userRoleRepository) + { + _roleDefinitionRepository = roleDefinitionRepository; + _userRoleRepository = userRoleRepository; + } + + public async Task CreateAsync(Role role, IEnumerable functions) + { + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + if (string.IsNullOrWhiteSpace(role.Id)) + { + role.Id = Guid.NewGuid().ToString("N"); + } + + if (await ExistsWithSameCode(role)) + { + throw new InvalidOperationException($"Role code '{role.Code}' already exists."); + } + + role.CreateTime = DateTime.Now; + role.FunctionsJson = SerializeFunctions(functions); + + await _roleDefinitionRepository.InsertAsync(role); + return role; + } + + public async Task DeleteAsync(string id) + { + var role = await _roleDefinitionRepository.GetAsync(id); + if (role == null) + { + return false; + } + + if (role.IsSystem) + { + throw new InvalidOperationException("System roles cannot be deleted."); + } + + var userRoles = await _userRoleRepository.QueryAsync(x => x.RoleId == id); + if (userRoles.Any()) + { + await _userRoleRepository.DeleteAsync(userRoles); + } + + await _roleDefinitionRepository.DeleteAsync(role); + return true; + } + + public async Task> GetAllAsync() + { + return await _roleDefinitionRepository.AllAsync(); + } + + public Task GetAsync(string id) + { + return _roleDefinitionRepository.GetAsync(id); + } + + public async Task GetByCodeAsync(string code) + { + var roles = await _roleDefinitionRepository.QueryAsync(x => x.Code == code); + return roles.FirstOrDefault(); + } + + public async Task UpdateAsync(Role role, IEnumerable functions) + { + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + var dbRole = await _roleDefinitionRepository.GetAsync(role.Id); + if (dbRole == null) + { + return false; + } + + if (dbRole.IsSystem && !string.Equals(dbRole.Code, role.Code, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("System role code cannot be changed."); + } + + if (!string.Equals(dbRole.Code, role.Code, StringComparison.OrdinalIgnoreCase) && await ExistsWithSameCode(role)) + { + throw new InvalidOperationException($"Role code '{role.Code}' already exists."); + } + + dbRole.Code = role.Code; + dbRole.Name = role.Name; + dbRole.Description = role.Description; + dbRole.IsSystem = role.IsSystem; + dbRole.FunctionsJson = SerializeFunctions(functions); + dbRole.UpdateTime = DateTime.Now; + + await _roleDefinitionRepository.UpdateAsync(dbRole); + return true; + } + + private async Task ExistsWithSameCode(Role role) + { + if (string.IsNullOrWhiteSpace(role.Code)) + { + return false; + } + + var sameCodeRoles = await _roleDefinitionRepository.QueryAsync(x => x.Code == role.Code); + return sameCodeRoles.Any(x => !string.Equals(x.Id, role.Id, StringComparison.OrdinalIgnoreCase)); + } + + private static string SerializeFunctions(IEnumerable functions) + { + var normalized = functions?.Where(f => !string.IsNullOrWhiteSpace(f)).Select(f => f.Trim()).Distinct().ToList() + ?? new List(); + + return JsonSerializer.Serialize(normalized); + } + } +} diff --git a/src/AgileConfig.Server.Service/ServiceCollectionExt.cs b/src/AgileConfig.Server.Service/ServiceCollectionExt.cs index ba797c69..48290faa 100644 --- a/src/AgileConfig.Server.Service/ServiceCollectionExt.cs +++ b/src/AgileConfig.Server.Service/ServiceCollectionExt.cs @@ -26,6 +26,7 @@ public static void AddBusinessServices(this IServiceCollection sc) sc.AddScoped(); sc.AddScoped(); sc.AddScoped(); + sc.AddScoped(); sc.AddScoped(); sc.AddScoped(); diff --git a/src/AgileConfig.Server.Service/UserService.cs b/src/AgileConfig.Server.Service/UserService.cs index 343545d8..4e4198d9 100644 --- a/src/AgileConfig.Server.Service/UserService.cs +++ b/src/AgileConfig.Server.Service/UserService.cs @@ -13,12 +13,14 @@ public class UserService : IUserService { private readonly IUserRepository _userRepository; private readonly IUserRoleRepository _userRoleRepository; + private readonly IRoleDefinitionRepository _roleDefinitionRepository; - public UserService(IUserRepository userRepository, IUserRoleRepository userRoleRepository) + public UserService(IUserRepository userRepository, IUserRoleRepository userRoleRepository, IRoleDefinitionRepository roleDefinitionRepository) { _userRepository = userRepository; _userRoleRepository = userRoleRepository; + _roleDefinitionRepository = roleDefinitionRepository; } public async Task AddAsync(User user) @@ -54,8 +56,38 @@ public Task GetUserAsync(string id) public async Task> GetUserRolesAsync(string userId) { var userRoles = await _userRoleRepository.QueryAsync(x => x.UserId == userId); + var migratedRoles = new List(); + foreach (var userRole in userRoles) + { + if (string.IsNullOrEmpty(userRole.RoleId) && userRole.LegacyRoleValue.HasValue) + { + var mappedRoleId = MapLegacyRole(userRole.LegacyRoleValue.Value); + if (!string.IsNullOrEmpty(mappedRoleId)) + { + userRole.RoleId = mappedRoleId; + userRole.LegacyRoleValue = null; + if (userRole.CreateTime == default) + { + userRole.CreateTime = DateTime.Now; + } + migratedRoles.Add(userRole); + } + } + } - return userRoles.Select(x => x.Role).ToList(); + if (migratedRoles.Any()) + { + await _userRoleRepository.UpdateAsync(migratedRoles); + } + + var roleIds = userRoles.Select(x => x.RoleId).Distinct().ToList(); + if (!roleIds.Any()) + { + return new List(); + } + + var roles = await _roleDefinitionRepository.QueryAsync(x => roleIds.Contains(x.Id)); + return roles.OrderBy(r => roleIds.IndexOf(r.Id)).ToList(); } @@ -65,18 +97,21 @@ public async Task UpdateAsync(User user) return true; } - public async Task UpdateUserRolesAsync(string userId, List roles) + public async Task UpdateUserRolesAsync(string userId, List roleIds) { var dbUserRoles = await _userRoleRepository.QueryAsync(x => x.UserId == userId); await _userRoleRepository.DeleteAsync(dbUserRoles); var userRoles = new List(); - roles.ForEach(x => + var now = DateTime.Now; + roleIds.Distinct().ToList().ForEach(x => { userRoles.Add(new UserRole { Id = Guid.NewGuid().ToString("N"), UserId = userId, - Role = x + RoleId = x, + LegacyRoleValue = null, + CreateTime = now }); }); @@ -88,6 +123,7 @@ public void Dispose() { _userRepository.Dispose(); _userRoleRepository.Dispose(); + _roleDefinitionRepository.Dispose(); } public Task> GetAll() @@ -111,11 +147,22 @@ public async Task ValidateUserPassword(string userName, string password) return false; } - public async Task> GetUsersByRoleAsync(Role role) + public async Task> GetUsersByRoleAsync(string roleId) { - var userRoles = await _userRoleRepository.QueryAsync(x => x.Role == role); + var userRoles = await _userRoleRepository.QueryAsync(x => x.RoleId == roleId); var userIds = userRoles.Select(x => x.UserId).Distinct().ToList(); return await _userRepository.QueryAsync(x => userIds.Contains(x.Id)); } + + private static string MapLegacyRole(int legacyRole) + { + return legacyRole switch + { + 0 => SystemRoleConstants.SuperAdminId, + 1 => SystemRoleConstants.AdminId, + 2 => SystemRoleConstants.OperatorId, + _ => string.Empty + }; + } } } diff --git a/src/AgileConfig.Server.UI/react-ui-antd/config/routes.ts b/src/AgileConfig.Server.UI/react-ui-antd/config/routes.ts index c8bdaee9..751e3850 100644 --- a/src/AgileConfig.Server.UI/react-ui-antd/config/routes.ts +++ b/src/AgileConfig.Server.UI/react-ui-antd/config/routes.ts @@ -91,6 +91,13 @@ component: './User', authority: ['Admin'], }, + { + name: 'list.role-list', + icon: 'SafetyCertificate', + path: '/roles', + component: './Role', + authority: ['Admin'], + }, { name: 'list.logs-list', icon: 'Bars', diff --git a/src/AgileConfig.Server.UI/react-ui-antd/src/components/Authorized/AuthorizedElement.tsx b/src/AgileConfig.Server.UI/react-ui-antd/src/components/Authorized/AuthorizedElement.tsx index 5e9d4f08..6525299c 100644 --- a/src/AgileConfig.Server.UI/react-ui-antd/src/components/Authorized/AuthorizedElement.tsx +++ b/src/AgileConfig.Server.UI/react-ui-antd/src/components/Authorized/AuthorizedElement.tsx @@ -12,11 +12,12 @@ export const checkUserPermission = (functions:string[],judgeKey:string, appid:st if (appid) { appId = appid ; } - let matchKey = ('GLOBAL_'+ judgeKey); - let key = functions.find(x=>x === matchKey); + // Check for global permission (without GLOBAL_ prefix) + let key = functions.find(x=>x === judgeKey); if (key) return true; - matchKey = ('APP_'+ appId + '_' + judgeKey); + // Check for app-specific permission + let matchKey = ('APP_'+ appId + '_' + judgeKey); key = functions.find(x=>x === matchKey); if (key) return true; @@ -28,7 +29,7 @@ const AuthorizedEle: React.FunctionComponent = (props)=>{ let functions:string[] = []; if (props.authority) { functions = props.authority; - } else { +} else { functions = getFunctions(); } @@ -36,4 +37,3 @@ const AuthorizedEle: React.FunctionComponent = (props)=>{ }; export default AuthorizedEle; - diff --git a/src/AgileConfig.Server.UI/react-ui-antd/src/locales/en-US/menu.ts b/src/AgileConfig.Server.UI/react-ui-antd/src/locales/en-US/menu.ts index f1a6286c..67e3e8fc 100644 --- a/src/AgileConfig.Server.UI/react-ui-antd/src/locales/en-US/menu.ts +++ b/src/AgileConfig.Server.UI/react-ui-antd/src/locales/en-US/menu.ts @@ -57,5 +57,6 @@ export default { 'menu.list.service-list': 'Services', 'menu.list.config-list': 'Configurations', 'menu.list.user-list': 'User', + 'menu.list.role-list': 'Role', 'menu.list.logs-list': 'Log', }; diff --git a/src/AgileConfig.Server.UI/react-ui-antd/src/locales/en-US/pages.ts b/src/AgileConfig.Server.UI/react-ui-antd/src/locales/en-US/pages.ts index 1dac9c9c..54795148 100644 --- a/src/AgileConfig.Server.UI/react-ui-antd/src/locales/en-US/pages.ts +++ b/src/AgileConfig.Server.UI/react-ui-antd/src/locales/en-US/pages.ts @@ -158,7 +158,7 @@ export default { 'pages.client.disconnect_message': `Are you sure to disconnect the client ?`, 'pages.logs.table.appname': `AppName`, - 'pages.logs.table.type': 'Type', + 'pages.logs.table.type': 'Role', 'pages.logs.table.type.0': 'Normal', 'pages.logs.table.type.1': 'Warn', 'pages.logs.table.time': `LogTime`, @@ -252,7 +252,7 @@ export default { // User Management 'pages.user.table.cols.username': 'Username', 'pages.user.table.cols.team': 'Team', - 'pages.user.table.cols.usertype': 'Type', + 'pages.user.table.cols.userrole': 'Role', 'pages.user.table.cols.status': 'Status', 'pages.user.table.cols.action': 'Action', 'pages.user.table.cols.action.edit': 'Edit', @@ -264,16 +264,54 @@ export default { 'pages.user.form.username': 'Username', 'pages.user.form.password': 'Password', 'pages.user.form.team': 'Team', - 'pages.user.form.usertype': 'Type', + 'pages.user.form.userrole': 'Role', 'pages.user.form.status': 'Status', - 'pages.user.usertype.normaluser': 'Operator', - 'pages.user.usertype.admin': 'Administrator', - 'pages.user.usertype.superadmin': 'Super Administrator', 'pages.user.status.normal': 'Normal', 'pages.user.status.deleted': 'Deleted', 'pages.user.confirm_reset': 'Are you sure to reset user', 'pages.user.confirm_delete': 'Are you sure to delete user', 'pages.user.reset_password_default': "'s password to default password【123456】?", + 'pages.role.table.cols.name': 'Name', + 'pages.role.table.cols.description': 'Description', + 'pages.role.table.cols.system': 'System Role', + 'pages.role.system.yes': 'Yes', + 'pages.role.system.no': 'No', + 'pages.role.table.cols.functions': 'Permissions', + 'pages.role.table.cols.action': 'Action', + 'pages.role.table.cols.action.add': 'Add Role', + 'pages.role.table.cols.action.edit': 'Edit', + 'pages.role.table.cols.action.delete': 'Delete', + 'pages.role.form.title.add': 'Add Role', + 'pages.role.form.title.edit': 'Edit Role', + 'pages.role.form.name': 'Name', + 'pages.role.form.description': 'Description', + 'pages.role.form.functions': 'Permissions', + 'pages.role.confirm_delete': 'Are you sure you want to delete this role?', + 'pages.role.delete_success': 'Role deleted successfully', + 'pages.role.delete_fail': 'Failed to delete role', + 'pages.role.save_success': 'Role saved successfully', + 'pages.role.save_fail': 'Failed to save role', + 'pages.role.load_failed': 'Failed to load roles', + 'pages.role.permissions.load_failed': 'Failed to load permissions', + 'pages.role.permissions.all': 'All', + 'pages.role.permissions.APP_ADD': 'Add App', + 'pages.role.permissions.APP_EDIT': 'Edit App', + 'pages.role.permissions.APP_DELETE': 'Delete App', + 'pages.role.permissions.APP_AUTH': 'Authorize App', + 'pages.role.permissions.CONFIG_ADD': 'Add Config', + 'pages.role.permissions.CONFIG_EDIT': 'Edit Config', + 'pages.role.permissions.CONFIG_DELETE': 'Delete Config', + 'pages.role.permissions.CONFIG_PUBLISH': 'Publish Config', + 'pages.role.permissions.CONFIG_OFFLINE': 'Offline Config', + 'pages.role.permissions.NODE_ADD': 'Add Node', + 'pages.role.permissions.NODE_DELETE': 'Delete Node', + 'pages.role.permissions.CLIENT_DISCONNECT': 'Disconnect Client', + 'pages.role.permissions.USER_ADD': 'Add User', + 'pages.role.permissions.USER_EDIT': 'Edit User', + 'pages.role.permissions.USER_DELETE': 'Delete User', + 'pages.role.permissions.ROLE_ADD': 'Add Role', + 'pages.role.permissions.ROLE_EDIT': 'Edit Role', + 'pages.role.permissions.ROLE_DELETE': 'Delete Role', // Service Management 'pages.service.table.cols.servicename': 'Service Name', diff --git a/src/AgileConfig.Server.UI/react-ui-antd/src/locales/zh-CN/menu.ts b/src/AgileConfig.Server.UI/react-ui-antd/src/locales/zh-CN/menu.ts index 093efa0a..2b996625 100644 --- a/src/AgileConfig.Server.UI/react-ui-antd/src/locales/zh-CN/menu.ts +++ b/src/AgileConfig.Server.UI/react-ui-antd/src/locales/zh-CN/menu.ts @@ -27,6 +27,7 @@ export default { 'menu.list.node-list': '节点', 'menu.list.config-list': '配置项', 'menu.list.user-list': '用户', + 'menu.list.role-list': '角色', 'menu.list.client-list': '客户端', 'menu.list.service-list': '服务', 'menu.list.logs-list': '日志', diff --git a/src/AgileConfig.Server.UI/react-ui-antd/src/locales/zh-CN/pages.ts b/src/AgileConfig.Server.UI/react-ui-antd/src/locales/zh-CN/pages.ts index e5f0f783..7c61030a 100644 --- a/src/AgileConfig.Server.UI/react-ui-antd/src/locales/zh-CN/pages.ts +++ b/src/AgileConfig.Server.UI/react-ui-antd/src/locales/zh-CN/pages.ts @@ -250,7 +250,7 @@ export default { // User management 'pages.user.table.cols.username': '用户名', 'pages.user.table.cols.team': '团队', - 'pages.user.table.cols.usertype': '类型', + 'pages.user.table.cols.userrole': '角色', 'pages.user.table.cols.status': '状态', 'pages.user.table.cols.action': '操作', 'pages.user.table.cols.action.edit': '编辑', @@ -262,16 +262,54 @@ export default { 'pages.user.form.username': '用户名', 'pages.user.form.password': '密码', 'pages.user.form.team': '团队', - 'pages.user.form.usertype': '类型', + 'pages.user.form.userrole': '角色', 'pages.user.form.status': '状态', - 'pages.user.usertype.normaluser': '操作员', - 'pages.user.usertype.admin': '管理员', - 'pages.user.usertype.superadmin': '超级管理员', 'pages.user.status.normal': '正常', 'pages.user.status.deleted': '已删除', 'pages.user.confirm_reset': '确定重置用户', 'pages.user.confirm_delete': '确定删除用户', 'pages.user.reset_password_default': '的密码为默认密码【123456】?', + 'pages.role.table.cols.name': '名称', + 'pages.role.table.cols.description': '描述', + 'pages.role.table.cols.system': '系统角色', + 'pages.role.system.yes': '是', + 'pages.role.system.no': '否', + 'pages.role.table.cols.functions': '权限', + 'pages.role.table.cols.action': '操作', + 'pages.role.table.cols.action.add': '新增角色', + 'pages.role.table.cols.action.edit': '编辑', + 'pages.role.table.cols.action.delete': '删除', + 'pages.role.form.title.add': '新增角色', + 'pages.role.form.title.edit': '编辑角色', + 'pages.role.form.name': '角色名称', + 'pages.role.form.description': '描述', + 'pages.role.form.functions': '权限', + 'pages.role.confirm_delete': '确定删除该角色吗?', + 'pages.role.delete_success': '删除角色成功', + 'pages.role.delete_fail': '删除角色失败', + 'pages.role.save_success': '保存角色成功', + 'pages.role.save_fail': '保存角色失败', + 'pages.role.load_failed': '加载角色失败', + 'pages.role.permissions.load_failed': '加载权限列表失败', + 'pages.role.permissions.all': '所有权限', + 'pages.role.permissions.APP_ADD': '新增应用', + 'pages.role.permissions.APP_EDIT': '编辑应用', + 'pages.role.permissions.APP_DELETE': '删除应用', + 'pages.role.permissions.APP_AUTH': '应用授权', + 'pages.role.permissions.CONFIG_ADD': '新增配置', + 'pages.role.permissions.CONFIG_EDIT': '编辑配置', + 'pages.role.permissions.CONFIG_DELETE': '删除配置', + 'pages.role.permissions.CONFIG_PUBLISH': '发布配置', + 'pages.role.permissions.CONFIG_OFFLINE': '下线配置', + 'pages.role.permissions.NODE_ADD': '新增节点', + 'pages.role.permissions.NODE_DELETE': '删除节点', + 'pages.role.permissions.CLIENT_DISCONNECT': '断开客户端', + 'pages.role.permissions.USER_ADD': '新增用户', + 'pages.role.permissions.USER_EDIT': '编辑用户', + 'pages.role.permissions.USER_DELETE': '删除用户', + 'pages.role.permissions.ROLE_ADD': '新增角色', + 'pages.role.permissions.ROLE_EDIT': '编辑角色', + 'pages.role.permissions.ROLE_DELETE': '删除角色', // Service Management 'pages.service.table.cols.servicename': '服务名', diff --git a/src/AgileConfig.Server.UI/react-ui-antd/src/pages/Role/data.d.ts b/src/AgileConfig.Server.UI/react-ui-antd/src/pages/Role/data.d.ts new file mode 100644 index 00000000..e7d45f5d --- /dev/null +++ b/src/AgileConfig.Server.UI/react-ui-antd/src/pages/Role/data.d.ts @@ -0,0 +1,15 @@ +export type RoleItem = { + id: string; + name: string; + description?: string; + isSystem: boolean; + functions: string[]; +}; + +export type RoleFormValues = { + id?: string; + name: string; + description?: string; + functions: string[]; + isSystem?: boolean; +}; diff --git a/src/AgileConfig.Server.UI/react-ui-antd/src/pages/Role/index.tsx b/src/AgileConfig.Server.UI/react-ui-antd/src/pages/Role/index.tsx new file mode 100644 index 00000000..7ed394fa --- /dev/null +++ b/src/AgileConfig.Server.UI/react-ui-antd/src/pages/Role/index.tsx @@ -0,0 +1,272 @@ +import { ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { PageContainer } from '@ant-design/pro-layout'; +import ProTable, { ActionType, ProColumns } from '@ant-design/pro-table'; +import { Button, message, Modal, Space, Tag } from 'antd'; +import { ModalForm, ProFormSelect, ProFormText } from '@ant-design/pro-form'; +import React, { useEffect, useRef, useState } from 'react'; +import { useIntl } from 'umi'; +import type { RoleFormValues, RoleItem } from './data'; +import { createRole, deleteRole, fetchSupportedRolePermissions, queryRoles, updateRole } from '@/services/role'; + +const { confirm } = Modal; + +const RolePage: React.FC = () => { + const actionRef = useRef(); + const intl = useIntl(); + const [createModalVisible, setCreateModalVisible] = useState(false); + const [updateModalVisible, setUpdateModalVisible] = useState(false); + const [currentRole, setCurrentRole] = useState(); + const [supportedPermissions, setSupportedPermissions] = useState([]); + + useEffect(() => { + loadPermissions(); + }, []); + + const loadPermissions = async () => { + try { + const response = await fetchSupportedRolePermissions(); + if (response?.success && Array.isArray(response.data)) { + setSupportedPermissions(response.data); + } else { + message.error(intl.formatMessage({ id: 'pages.role.permissions.load_failed', defaultMessage: 'Failed to load permissions' })); + } + } catch (error) { + message.error(intl.formatMessage({ id: 'pages.role.permissions.load_failed', defaultMessage: 'Failed to load permissions' })); + } + }; + + const permissionOptions = supportedPermissions.map((item) => ({ + value: item, + label: intl.formatMessage({ id: `pages.role.permissions.${item}`, defaultMessage: item }), + })); + + const handleCreate = async (values: RoleFormValues) => { + const hide = message.loading(intl.formatMessage({ id: 'saving', defaultMessage: 'Saving...' })); + try { + const response = await createRole(values); + hide(); + if (response?.success) { + message.success(intl.formatMessage({ id: 'pages.role.save_success', defaultMessage: 'Role saved successfully' })); + return true; + } + message.error(response?.message || intl.formatMessage({ id: 'pages.role.save_fail', defaultMessage: 'Failed to save role' })); + return false; + } catch (error) { + hide(); + message.error(intl.formatMessage({ id: 'pages.role.save_fail', defaultMessage: 'Failed to save role' })); + return false; + } + }; + + const handleUpdate = async (values: RoleFormValues) => { + const hide = message.loading(intl.formatMessage({ id: 'saving', defaultMessage: 'Saving...' })); + try { + const response = await updateRole(values); + hide(); + if (response?.success) { + message.success(intl.formatMessage({ id: 'pages.role.save_success', defaultMessage: 'Role saved successfully' })); + return true; + } + message.error(response?.message || intl.formatMessage({ id: 'pages.role.save_fail', defaultMessage: 'Failed to save role' })); + return false; + } catch (error) { + hide(); + message.error(intl.formatMessage({ id: 'pages.role.save_fail', defaultMessage: 'Failed to save role' })); + return false; + } + }; + + const handleDelete = (role: RoleItem) => { + confirm({ + icon: , + title: intl.formatMessage({ id: 'pages.role.confirm_delete', defaultMessage: 'Are you sure to delete this role?' }), + content: `${role.name}`, + onOk: async () => { + const hide = message.loading(intl.formatMessage({ id: 'deleting', defaultMessage: 'Deleting...' })); + try { + const response = await deleteRole(role.id); + hide(); + if (response?.success) { + message.success(intl.formatMessage({ id: 'pages.role.delete_success', defaultMessage: 'Role deleted successfully' })); + actionRef.current?.reload(); + } else { + message.error(response?.message || intl.formatMessage({ id: 'pages.role.delete_fail', defaultMessage: 'Failed to delete role' })); + } + } catch (error) { + hide(); + message.error(intl.formatMessage({ id: 'pages.role.delete_fail', defaultMessage: 'Failed to delete role' })); + } + }, + }); + }; + + const columns: ProColumns[] = [ + { + title: intl.formatMessage({ id: 'pages.role.table.cols.name', defaultMessage: 'Name' }), + dataIndex: 'name', + }, + { + title: intl.formatMessage({ id: 'pages.role.table.cols.description', defaultMessage: 'Description' }), + dataIndex: 'description', + search: false, + }, + { + title: intl.formatMessage({ id: 'pages.role.table.cols.system', defaultMessage: 'System Role' }), + dataIndex: 'isSystem', + search: false, + render: (_, record) => ( + + {record.isSystem + ? intl.formatMessage({ id: 'pages.role.system.yes', defaultMessage: 'Yes' }) + : intl.formatMessage({ id: 'pages.role.system.no', defaultMessage: 'No' })} + + ), + }, + { + title: intl.formatMessage({ id: 'pages.role.table.cols.functions', defaultMessage: 'Permissions' }), + dataIndex: 'functions', + search: false, + render: (_, record) => { + if (record.functions?.length === supportedPermissions.length) { + return {intl.formatMessage({ id: 'pages.role.permissions.all', defaultMessage: 'All' })}; + } + return ( + + {record.functions?.map((fn) => ( + {intl.formatMessage({ id: `pages.role.permissions.${fn}`, defaultMessage: fn })} + ))} + + ); + }, + }, + { + title: intl.formatMessage({ id: 'pages.role.table.cols.action', defaultMessage: 'Action' }), + valueType: 'option', + render: (_, record) => [ + { + setCurrentRole(record); + setUpdateModalVisible(true); + }} + > + {intl.formatMessage({ id: 'pages.role.table.cols.action.edit', defaultMessage: 'Edit' })} + , + !record.isSystem ? ( + + ) : null, + ], + }, + ]; + + return ( + + + actionRef={actionRef} + rowKey="id" + search={false} + columns={columns} + request={async () => { + const response = await queryRoles(); + const data = response?.data || []; + // filter super admin role + const filteredData = data.filter((role: RoleItem) => role.name !== 'Super Administrator'); + return { + data: filteredData, + success: response?.success ?? false, + }; + }} + toolBarRender={() => [ + , + ]} + /> + + + title={intl.formatMessage({ id: 'pages.role.form.title.add', defaultMessage: 'Add Role' })} + visible={createModalVisible} + onVisibleChange={setCreateModalVisible} + initialValues={{ functions: [] }} + onFinish={async (values) => { + const success = await handleCreate(values); + if (success) { + setCreateModalVisible(false); + actionRef.current?.reload(); + } + return success; + }} + > + + + + + + {updateModalVisible && ( + + title={intl.formatMessage({ id: 'pages.role.form.title.edit', defaultMessage: 'Edit Role' })} + visible={updateModalVisible} + onVisibleChange={(visible) => { + setUpdateModalVisible(visible); + if (!visible) { + setCurrentRole(undefined); + } + }} + initialValues={{ + ...currentRole, + functions: currentRole?.functions || [], + }} + onFinish={async (values) => { + const success = await handleUpdate({ ...values, id: currentRole?.id, isSystem: currentRole?.isSystem }); + if (success) { + setUpdateModalVisible(false); + setCurrentRole(undefined); + actionRef.current?.reload(); + } + return success; + }} + > + + + + + )} + + ); +}; + +export default RolePage; diff --git a/src/AgileConfig.Server.UI/react-ui-antd/src/pages/User/comps/updateUser.tsx b/src/AgileConfig.Server.UI/react-ui-antd/src/pages/User/comps/updateUser.tsx index 6d1c16f6..c99ea038 100644 --- a/src/AgileConfig.Server.UI/react-ui-antd/src/pages/User/comps/updateUser.tsx +++ b/src/AgileConfig.Server.UI/react-ui-antd/src/pages/User/comps/updateUser.tsx @@ -1,32 +1,34 @@ import { useIntl } from "@/.umi/plugin-locale/localeExports"; -import { getAuthority } from "@/utils/authority"; import { ModalForm, ProFormSelect, ProFormText } from "@ant-design/pro-form"; import React from 'react'; import { UserItem } from "../data"; + +type RoleOption = { + value: string; + label: string; + code?: string; + isSystem?: boolean; +}; + export type UpdateUserProps = { onSubmit: (values: UserItem) => Promise; onCancel: () => void; updateModalVisible: boolean; value: UserItem | undefined ; - setValue: React.Dispatch> + setValue: React.Dispatch>; + roleOptions: RoleOption[]; + defaultRoleIds: string[]; }; const UpdateForm : React.FC = (props)=>{ const intl = useIntl(); - const hasUserRole = (role:string) => { - const authority = getAuthority(); - if (Array.isArray(authority)) { - if (authority.find(x=> x === role)) { - return true; - } - } - - return false; - } return ( - 0 ? props.value.userRoleIds : props.defaultRoleIds + }} visible={props.updateModalVisible} modalProps={ { @@ -61,32 +63,14 @@ const UpdateForm : React.FC = (props)=>{ required: true, }, ]} - label={intl.formatMessage({id: 'pages.user.form.usertype'})} - name="userRoles" - mode="multiple" - options = { - hasUserRole('SuperAdmin')?[ - { - value: 1, - label: intl.formatMessage({ - id: 'pages.user.usertype.admin', - }), - }, - { - value: 2, - label: intl.formatMessage({ - id: 'pages.user.usertype.normaluser', - }), - } - ]:[{ - value: 2, - label: intl.formatMessage({ - id: 'pages.user.usertype.normaluser', - }), - }] - } - > - + label={intl.formatMessage({id: 'pages.user.form.userrole'})} + name="userRoleIds" + mode="multiple" + options={props.roleOptions.map(r => ({ + value: r.value, + label: r.label, + }))} + /> ); } diff --git a/src/AgileConfig.Server.UI/react-ui-antd/src/pages/User/data.d.ts b/src/AgileConfig.Server.UI/react-ui-antd/src/pages/User/data.d.ts index 6fec930c..12caf431 100644 --- a/src/AgileConfig.Server.UI/react-ui-antd/src/pages/User/data.d.ts +++ b/src/AgileConfig.Server.UI/react-ui-antd/src/pages/User/data.d.ts @@ -3,8 +3,9 @@ export type UserItem = { userName: string, team: string, status: number, - userRoles: number[], - userRoleNames: string[] + userRoleIds: string[], + userRoleNames: string[], + userRoleCodes: string[] }; export type UserListParams = { name?: string; diff --git a/src/AgileConfig.Server.UI/react-ui-antd/src/pages/User/index.tsx b/src/AgileConfig.Server.UI/react-ui-antd/src/pages/User/index.tsx index b5a02096..f3a796d7 100644 --- a/src/AgileConfig.Server.UI/react-ui-antd/src/pages/User/index.tsx +++ b/src/AgileConfig.Server.UI/react-ui-antd/src/pages/User/index.tsx @@ -2,15 +2,23 @@ import { ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-layout'; import ProTable, { ActionType, ProColumns } from '@ant-design/pro-table'; import { Button, FormInstance, message,Modal, Space, Tag } from 'antd'; -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { UserItem } from './data'; import { queryUsers, addUser, delUser, editUser, resetPassword } from './service'; +import { queryRoles } from '@/services/role'; import { useIntl, getIntl, getLocale } from 'umi'; import { ModalForm, ProFormSelect, ProFormText } from '@ant-design/pro-form'; import UpdateUser from './comps/updateUser'; import { getAuthority } from '@/utils/authority'; const { confirm } = Modal; + +type RoleOption = { + value: string; + label: string; + code: string; + isSystem: boolean; +}; const handleAdd = async (fields: UserItem) => { const intl = getIntl(getLocale()); const hide = message.loading(intl.formatMessage({ @@ -125,15 +133,32 @@ const hasUserRole = (role:string) => { } const checkUserListModifyPermission = (user:UserItem) => { - const authMap = { 'SuperAdmin': 0,'Admin':1,'NormalUser':2}; - let currentAuthNum = 2; - const roles = getAuthority(); - if (Array.isArray(roles)) { - let max = roles.map(x=> authMap[x]).sort((a, b) => a - b)[0]; - currentAuthNum = max; + // Lower number means higher privilege + const authMap:Record = { SuperAdmin: 0, Admin: 1, NormalUser: 2 }; + const myRoles = getAuthority(); + if (!Array.isArray(myRoles) || myRoles.length === 0) return false; + + // If current user is SuperAdmin -> can edit anyone except themselves (optional) + if (myRoles.includes('SuperAdmin')) { + // Prevent editing own account if desired + if (user.userName && user.userName === (typeof localStorage !== 'undefined' ? localStorage.getItem('currentUserName') : undefined)) { + return false; // disallow self-edit via list (modal still available maybe) + } + return true; } - let userAuthNum = user.userRoles.sort((a, b) => a - b)[0]; + // Determine current user's minimal privilege level + const currentAuthNum = myRoles + .map(r => authMap[r] ?? 999) + .reduce((min, v) => v < min ? v : min, 999); + + // Determine target user's minimal privilege level + const targetCodes = user.userRoleCodes || []; + const userAuthNum = targetCodes.length > 0 + ? targetCodes.map(c => authMap[c] ?? 999).reduce((min, v) => v < min ? v : min, 999) + : 999; + + // Allow edit only if current privilege strictly higher than target (numerically lower) return currentAuthNum < userAuthNum; } @@ -145,6 +170,39 @@ const userList:React.FC = () => { const [createModalVisible, handleModalVisible] = useState(false); const [updateModalVisible, setUpdateModalVisible] = useState(false); const [currentRow, setCurrentRow] = useState(); + const [roleOptions, setRoleOptions] = useState([]); + + useEffect(() => { + loadRoles(); + }, []); + + const loadRoles = async () => { + try { + const response = await queryRoles(); + if (response?.success && Array.isArray(response.data)) { + const options = response.data.map((role: any) => ({ + value: role.id, + label: role.name, + code: role.code, + isSystem: role.isSystem, + })); + setRoleOptions(options); + } else { + message.error(intl.formatMessage({ id: 'pages.role.load_failed', defaultMessage: 'Failed to load roles' })); + } + } catch (error) { + message.error(intl.formatMessage({ id: 'pages.role.load_failed', defaultMessage: 'Failed to load roles' })); + } + }; + + const getDefaultRoleIds = () => { + const normalRole = roleOptions.find(option => option.code === 'NormalUser'); + return normalRole ? [normalRole.value] : []; + }; + + const availableRoleOptions = hasUserRole('SuperAdmin') + ? roleOptions + : roleOptions.filter(option => option.code !== 'SuperAdmin'); const columns: ProColumns[] = [ { title: intl.formatMessage({ @@ -160,22 +218,29 @@ const userList:React.FC = () => { }, { title: intl.formatMessage({ - id: 'pages.user.table.cols.usertype', + id: 'pages.user.table.cols.userrole', }), - dataIndex: 'userRoles', + dataIndex: 'userRoleNames', search: false, renderFormItem: (_, { defaultRender }) => { return defaultRender(_); }, render: (_, record) => ( - {record.userRoleNames?.map((name:string) => ( - - {name} - - ))} + {record.userRoleNames?.map((name:string, index:number) => { + const code = record.userRoleCodes?.[index]; + let color = 'blue'; + if (code === 'SuperAdmin') { + color = 'red'; + } else if (code === 'Admin') { + color = 'gold'; + } + return ( + + {name} + + ); + })} ), }, @@ -290,7 +355,17 @@ const userList:React.FC = () => { } width="400px" visible={createModalVisible} - onVisibleChange={handleModalVisible} + initialValues={{ + userRoleIds: getDefaultRoleIds(), + }} + onVisibleChange={(visible) => { + handleModalVisible(visible); + if (visible) { + addFormRef.current?.setFieldsValue({ userRoleIds: getDefaultRoleIds() }); + } else { + addFormRef.current?.resetFields(); + } + }} onFinish={ async (value) => { const success = await handleAdd(value as UserItem); @@ -339,32 +414,13 @@ const userList:React.FC = () => { }, ]} label={intl.formatMessage({ - id: 'pages.user.form.usertype' + id: 'pages.user.form.userrole' })} - name="userRoles" - mode="multiple" - options = {hasUserRole('SuperAdmin')?[ - { - value: 1, - label: intl.formatMessage({ - id: 'pages.user.usertype.admin' - }), - }, - { - value: 2, - label: intl.formatMessage({ - id: 'pages.user.usertype.normaluser' - }), - } - ]:[ - { - value: 2, - label: intl.formatMessage({ - id: 'pages.user.usertype.normaluser' - }), - }]} + name="userRoleIds" + mode="multiple" + options = {availableRoleOptions} > - + { @@ -373,6 +429,8 @@ const userList:React.FC = () => { value={currentRow} setValue={setCurrentRow} updateModalVisible={updateModalVisible} + roleOptions={availableRoleOptions} + defaultRoleIds={getDefaultRoleIds()} onCancel={ () => { setCurrentRow(undefined); @@ -389,10 +447,10 @@ const userList:React.FC = () => { actionRef.current.reload(); } } - addFormRef.current?.resetFields(); } - }/> - } + } + /> + } ); diff --git a/src/AgileConfig.Server.UI/react-ui-antd/src/services/role.ts b/src/AgileConfig.Server.UI/react-ui-antd/src/services/role.ts new file mode 100644 index 00000000..6c5de07e --- /dev/null +++ b/src/AgileConfig.Server.UI/react-ui-antd/src/services/role.ts @@ -0,0 +1,36 @@ +import request from '@/utils/request'; + +export async function queryRoles() { + return request('role/list', { + method: 'GET', + }); +} + +export async function fetchSupportedRolePermissions() { + return request('role/supportedPermissions', { + method: 'GET', + }); +} + +export async function createRole(data: any) { + return request('role/add', { + method: 'POST', + data, + }); +} + +export async function updateRole(data: any) { + return request('role/edit', { + method: 'POST', + data, + }); +} + +export async function deleteRole(id: string) { + return request('role/delete', { + method: 'POST', + params: { + id, + }, + }); +}