diff --git a/zero.Core/Api/UserRolesApi.cs b/zero.Core/Api/UserRolesApi.cs index 66fbcf96..2f26c7c6 100644 --- a/zero.Core/Api/UserRolesApi.cs +++ b/zero.Core/Api/UserRolesApi.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Raven.Client.Documents; +using Raven.Client.Documents.Session; using System.Collections.Generic; using System.Linq; using System.Security.Claims; @@ -18,96 +19,58 @@ namespace zero.Core.Api protected UserManager UserManager { get; private set; } + protected RoleManager RoleManager { get; private set; } + private ClaimsPrincipal Principal => HttpContextAccessor.HttpContext?.User; - public UserRolesApi(IDocumentStore raven, IHttpContextAccessor httpContextAccessor, UserManager userManager) + public UserRolesApi(IDocumentStore raven, IHttpContextAccessor httpContextAccessor, UserManager userManager, RoleManager roleManager) { Raven = raven; HttpContextAccessor = httpContextAccessor; UserManager = userManager; + RoleManager = roleManager; } /// - public async Task GetUser() + public async Task> GetAll() { - User user = await UserManager.GetUserAsync(HttpContextAccessor.HttpContext.User); - return user; + using (IAsyncDocumentSession session = Raven.OpenAsyncSession()) + { + return await session.Query().OrderBy(x => x.Sort).ThenBy(x => x.Name).ToListAsync(); + } } /// - public async Task GetUserById(string id) + public async Task GetById(string id) { - User user = await UserManager.FindByIdAsync(id); - return user; + return await RoleManager.FindByIdAsync(id); } /// - public async Task GetUserByEmail(string email) - { - User user = await UserManager.FindByEmailAsync(email); - return user; - } - - - /// - public bool IsSuper() - { - return Principal.HasClaim(Constants.Auth.Claims.IsSuper, PermissionsValue.True); - } - - - /// - public bool IsAdmin() - { - return Principal.HasClaim(Constants.Auth.Claims.Role, "administrator"); // TODO use constant (in setup as well) - } - - - /// - public IList GetPermissions(string prefix = null) - { - return Principal.Claims - .Where(claim => claim.Type == Constants.Auth.Claims.Permission && (prefix == null || claim.Value.StartsWith(prefix))) - .Select(claim => new Permission(claim, prefix)) - .ToList(); - } + //public IList GetPermissions(string prefix = null) + //{ + // return Principal.Claims + // .Where(claim => claim.Type == Constants.Auth.Claims.Permission && (prefix == null || claim.Value.StartsWith(prefix))) + // .Select(claim => new Permission(claim, prefix)) + // .ToList(); + //} } public interface IUserRolesApi { /// - /// Get currently logged-in user + /// Get all user roles /// - Task GetUser(); + Task> GetAll(); /// - /// Find user by id + /// Get role by id /// - Task GetUserById(string id); - - /// - /// Find user by email - /// - Task GetUserByEmail(string email); - - /// - /// Whether the current user is the super user who created the zero instance - /// - bool IsSuper(); - - /// - /// Whether the current user belongs to the administrator role (will always return false if this role gets deleted) - /// - bool IsAdmin(); - - /// - /// Get all permissions for the current user with an optional prefix - /// - IList GetPermissions(string prefix = null); + Task GetById(string id); } } diff --git a/zero.Web/App/Components/Tables/table.vue b/zero.Web/App/Components/Tables/table.vue index 47f38061..64d4b381 100644 --- a/zero.Web/App/Components/Tables/table.vue +++ b/zero.Web/App/Components/Tables/table.vue @@ -299,7 +299,7 @@ .ui-table-cell { - display: inline-block; + display: inline-flex; align-items: center; flex: 1 1 5%; position: relative; @@ -439,10 +439,4 @@ } } } - - .ui-table .flag - { - position: relative; - top: 2px; - } \ No newline at end of file diff --git a/zero.Web/App/Resources/userRoles.js b/zero.Web/App/Resources/userRoles.js new file mode 100644 index 00000000..7ddf4e62 --- /dev/null +++ b/zero.Web/App/Resources/userRoles.js @@ -0,0 +1,16 @@ +import Axios from 'axios'; + +export default { + + // get role by id + getById(id) + { + return Axios.get('userRoles/getById', { params: { id } }).then(res => Promise.resolve(res.data)); + }, + + // get all roles + getAll() + { + return Axios.get('userRoles/getAll').then(res => Promise.resolve(res.data)); + } +}; \ No newline at end of file diff --git a/zero.Web/App/pages/settings/routes.js b/zero.Web/App/pages/settings/routes.js index aea70b3c..058db67d 100644 --- a/zero.Web/App/pages/settings/routes.js +++ b/zero.Web/App/pages/settings/routes.js @@ -1,13 +1,17 @@ -import { map as _map, find as _find } from 'underscore'; +import { map as _map, find as _find, isArray as _isArray } from 'underscore'; const alias = zero.alias.sections.settings; const section = _find(zero.sections, section => section.alias === alias); let routes = []; const detailPages = []; -detailPages[zero.alias.settings.users] = 'user'; +detailPages[zero.alias.settings.users] = [ + { view: 'user' }, + { view: 'role', path: 'role' } +]; detailPages[zero.alias.settings.countries] = 'country'; + if (section) { zero.settingsAreas.forEach(group => group.items.forEach(area => @@ -25,14 +29,38 @@ if (section) // add details page if (detailPages[area.alias]) { - routes.push({ - path: area.url + '/edit/:id', - name: alias + '-' + area.alias + '-edit', - component: () => import(`zero/pages/${alias}/${detailPages[area.alias]}`), - props: true, - meta: { - name: [area.name, section.name] - } + var config = detailPages[area.alias]; + var details = []; + + if (typeof config === 'string') + { + details.push({ + view: config, + path: 'edit' + }); + } + else if (_isArray(config)) + { + details = config; + } + else if (typeof config === 'object') + { + details.push(config); + } + + details.forEach(detail => + { + const path = detail.path || 'edit'; + + routes.push({ + path: area.url + '/' + path + '/:id', + name: alias + '-' + area.alias + '-' + path, + component: () => import(`zero/pages/${alias}/${detail.view}`), + props: true, + meta: { + name: [area.name, section.name] + } + }); }); } })); diff --git a/zero.Web/App/pages/settings/users.vue b/zero.Web/App/pages/settings/users.vue index b4139b80..1f8534a4 100644 --- a/zero.Web/App/pages/settings/users.vue +++ b/zero.Web/App/pages/settings/users.vue @@ -5,9 +5,21 @@ +
- {{_user.name}} - +

+
+ + + {{role.name}} + + +
+
+ +
+

+
@@ -15,27 +27,136 @@ \ No newline at end of file diff --git a/zero.Web/Controllers/BackofficeController.cs b/zero.Web/Controllers/BackofficeController.cs index ae5a7ecf..9a688e95 100644 --- a/zero.Web/Controllers/BackofficeController.cs +++ b/zero.Web/Controllers/BackofficeController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; using System.Threading.Tasks; using zero.Core; using zero.Core.Api; @@ -33,7 +34,7 @@ namespace zero.Web.Controllers } - protected async Task As(T model) where TTarget : class, new() where T : IZeroEntity + protected IActionResult As(T model) where TTarget : class, new() where T : IZeroEntity { if (model == null) { @@ -47,18 +48,49 @@ namespace zero.Web.Controllers TTarget result = Mapper.Map(model); - //if (result is EditModel) - //{ - // (result as EditModel).Meta = new EditModelMeta() - // { - // Token = await Token.Get(model) - // }; - //} + return Json(result); + } + + + protected IActionResult As(IEnumerable model) where TTarget : class, new() where T : IZeroEntity + { + if (model == null) + { + return new StatusCodeResult(404); + } + + IList result = new List(); + + foreach (T item in model) + { + result.Add(Mapper.Map(item)); + } return Json(result); } + protected IActionResult As(ListResult model) where TTarget : class, new() where T : IZeroEntity + { + if (model == null) + { + return new StatusCodeResult(404); + } + + IList list = new List(); + + foreach (T item in model.Items) + { + list.Add(Mapper.Map(item)); + } + + return Json(new ListResult(list, model.TotalItems, model.Page, model.PageSize) + { + Statistics = model.Statistics + }); + } + + protected TTarget Map(T model) where TTarget : class, new() { return Mapper.Map(model); diff --git a/zero.Web/Controllers/CountriesController.cs b/zero.Web/Controllers/CountriesController.cs index 0e8f5bd6..5880b999 100644 --- a/zero.Web/Controllers/CountriesController.cs +++ b/zero.Web/Controllers/CountriesController.cs @@ -28,7 +28,7 @@ namespace zero.Web.Controllers [AddToken] public async Task GetById([FromQuery] string id) { - return await As(await Api.GetById(id)); + return As(await Api.GetById(id)); } @@ -37,7 +37,7 @@ namespace zero.Web.Controllers /// public async Task GetAll([FromQuery] ListQuery query) { - return Json(await Api.GetByQuery("en-US", query)); + return As(await Api.GetByQuery("en-US", query)); } @@ -48,7 +48,8 @@ namespace zero.Web.Controllers [ZeroAuthorize(Permissions.Settings.Countries, PermissionsValue.Write)] public async Task Save([FromBody] CountryEditModel model) { - return Json(await Api.Save(Mapper.Map(model))); + Country country = Mapper.Map(model, await Api.GetById(model.Id)); + return Json(await Api.Save(country)); } diff --git a/zero.Web/Controllers/UserRolesController.cs b/zero.Web/Controllers/UserRolesController.cs new file mode 100644 index 00000000..e24a4f4a --- /dev/null +++ b/zero.Web/Controllers/UserRolesController.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using zero.Core; +using zero.Core.Api; +using zero.Core.Entities; +using zero.Core.Identity; +using zero.Web.Mapper; +using zero.Web.Models; + +namespace zero.Web.Controllers +{ + [ZeroAuthorize(Permissions.Settings.Users, PermissionsValue.Read)] + public class UserRolesController : BackofficeController + { + private IUserRolesApi Api { get; set; } + + public UserRolesController(IZeroConfiguration config, IUserRolesApi api, IMapper mapper, IToken token) : base(config, mapper, token) + { + Api = api; + } + + + /// + /// Get role by id + /// + public async Task GetById([FromQuery] string id) + { + return As(await Api.GetById(id)); + } + + + /// + /// Get all roles + /// + public async Task GetAll() + { + return As(await Api.GetAll()); + } + } +} diff --git a/zero.Web/Controllers/UsersController.cs b/zero.Web/Controllers/UsersController.cs index 1a15365c..940d32b2 100644 --- a/zero.Web/Controllers/UsersController.cs +++ b/zero.Web/Controllers/UsersController.cs @@ -25,7 +25,7 @@ namespace zero.Web.Controllers /// public async Task GetById([FromQuery] string id) { - return await As(await Api.GetUserById(id)); + return As(await Api.GetUserById(id)); } @@ -34,7 +34,7 @@ namespace zero.Web.Controllers /// public async Task GetAll([FromQuery] ListQuery query) { - return Json(await Api.GetByQuery(query, "zero.applications.1-A")); + return As(await Api.GetByQuery(query, "zero.applications.1-A")); } } } diff --git a/zero.Web/Mapper/CountryMapperConfig.cs b/zero.Web/Mapper/CountryMapperConfig.cs index 75f8f884..2ddf6760 100644 --- a/zero.Web/Mapper/CountryMapperConfig.cs +++ b/zero.Web/Mapper/CountryMapperConfig.cs @@ -29,6 +29,15 @@ namespace zero.Web.Mapper target.LanguageId = source.LanguageId; target.Code = source.Code; }); + + config.CreateMap((source, target) => + { + target.Id = source.Id; + target.Name = source.Name; + target.IsActive = source.IsActive; + target.IsPreferred = source.IsPreferred; + target.Code = source.Code; + }); } } } diff --git a/zero.Web/Mapper/UserMapperConfig.cs b/zero.Web/Mapper/UserMapperConfig.cs index 965faa6e..7214a988 100644 --- a/zero.Web/Mapper/UserMapperConfig.cs +++ b/zero.Web/Mapper/UserMapperConfig.cs @@ -1,4 +1,5 @@ -using zero.Core.Entities; +using System; +using zero.Core.Entities; using zero.Web.Models; namespace zero.Web.Mapper @@ -36,6 +37,44 @@ namespace zero.Web.Mapper target.Claims = source.Claims; target.LockoutEnd = source.LockoutEnd; }); + + config.CreateMap((source, target) => + { + target.Id = source.Id; + target.Name = source.Name; + target.IsActive = source.IsActive && (!source.LockoutEnabled || !source.LockoutEnd.HasValue); + target.Email = source.Email; + target.Avatar = "http://localhost:14051/media/UserAvatars/09eb6d4d41894a44a9585b94bf9cff41.jpg?width=50&height=50&mode=crop"; // TODO //source.Avatar?.Source; + target.Roles = String.Join(", ", source.Roles); // TODO get name from alias + }); + + config.CreateMap((source, target) => + { + target.Id = source.Id; + target.Name = source.Name; + target.IsActive = source.IsActive; + target.CreatedDate = source.CreatedDate; + target.Description = source.Description; + target.Icon = source.Icon; + target.Claims = source.Claims; + }); + + config.CreateMap((source, target) => + { + target.Name = source.Name; + target.IsActive = source.IsActive; + target.Description = source.Description; + target.Icon = source.Icon; + target.Claims = source.Claims; + }); + + config.CreateMap((source, target) => + { + target.Id = source.Id; + target.Name = source.Name; + target.CountClaims = source.Claims.Count; + target.Icon = source.Icon; + }); } } } diff --git a/zero.Web/Models/CountryListModel.cs b/zero.Web/Models/CountryListModel.cs new file mode 100644 index 00000000..d4b011b0 --- /dev/null +++ b/zero.Web/Models/CountryListModel.cs @@ -0,0 +1,15 @@ +using System; + +namespace zero.Web.Models +{ + public class CountryListModel : ListModel + { + public string Name { get; set; } + + public bool IsActive { get; set; } + + public bool IsPreferred { get; set; } + + public string Code { get; set; } + } +} diff --git a/zero.Web/Models/ListModel.cs b/zero.Web/Models/ListModel.cs new file mode 100644 index 00000000..20e98f1e --- /dev/null +++ b/zero.Web/Models/ListModel.cs @@ -0,0 +1,10 @@ +namespace zero.Web.Models +{ + public abstract class ListModel + { + /// + /// Id of the entity + /// + public string Id { get; set; } + } +} diff --git a/zero.Web/Models/UserListModel.cs b/zero.Web/Models/UserListModel.cs new file mode 100644 index 00000000..49405cbf --- /dev/null +++ b/zero.Web/Models/UserListModel.cs @@ -0,0 +1,15 @@ +namespace zero.Web.Models +{ + public class UserListModel : ListModel + { + public string Name { get; set; } + + public bool IsActive { get; set; } + + public string Email { get; set; } + + public string Roles { get; set; } + + public string Avatar { get; set; } + } +} diff --git a/zero.Web/Models/UserRoleEditModel.cs b/zero.Web/Models/UserRoleEditModel.cs new file mode 100644 index 00000000..2860d6d8 --- /dev/null +++ b/zero.Web/Models/UserRoleEditModel.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using zero.Core.Entities; + +namespace zero.Web.Models +{ + public class UserRoleEditModel : EditModel + { + public string Name { get; set; } + + public bool IsActive { get; set; } + + public DateTimeOffset CreatedDate { get; set; } + + public string Description { get; set; } + + public string Icon { get; set; } + + public List Claims { get; set; } + } +} diff --git a/zero.Web/Models/UserRoleListModel.cs b/zero.Web/Models/UserRoleListModel.cs new file mode 100644 index 00000000..20e11d3e --- /dev/null +++ b/zero.Web/Models/UserRoleListModel.cs @@ -0,0 +1,11 @@ +namespace zero.Web.Models +{ + public class UserRoleListModel : ListModel + { + public string Name { get; set; } + + public int CountClaims { get; set; } + + public string Icon { get; set; } + } +} diff --git a/zero.Web/Resources/Localization/zero.en-us.json b/zero.Web/Resources/Localization/zero.en-us.json index 463dbaa9..96ab77cf 100644 --- a/zero.Web/Resources/Localization/zero.en-us.json +++ b/zero.Web/Resources/Localization/zero.en-us.json @@ -113,6 +113,10 @@ "user": { "name": "User", + "users": "Users", + "roles": "Roles", + "count_permissions": "{count} permissions", + "one_permission": "1 permission", "fields": { "name_placeholder": "Enter your real name or a username", "email": "Email", @@ -125,7 +129,8 @@ "roles": "Roles", "roles_text": "Add default permissions from roles", "sections": "Sections", - "sections_text": "Specify access to backoffice sections" + "sections_text": "Specify access to backoffice sections", + "isActive": "Can login" } }, diff --git a/zero.Web/Sass/Core/_settings.scss b/zero.Web/Sass/Core/_settings.scss index bbaf4d6e..9ca4873e 100644 --- a/zero.Web/Sass/Core/_settings.scss +++ b/zero.Web/Sass/Core/_settings.scss @@ -97,6 +97,17 @@ } +:root +{ + --color-accent-yellow: #ffd100; +} + +.color-yellow +{ + color: var(--color-accent-yellow); +} + + // font sizes $font-size: 14px;