From e202646f57a8c2fcb825f05604984b9c77737e2b Mon Sep 17 00:00:00 2001 From: Andreas Guldborg Hansen <95026056+A-Guldborg@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:13:52 +0100 Subject: [PATCH] Adding User Manager (#21) * Adding PoC user manager --- .../Generated/AnalogCoreV2/AnalogCoreV2.cs | 358 +++++++++++++++++- Shifty.App/Components/UserTable.razor | 125 ++++++ Shifty.App/Pages/UserManagement.razor | 24 ++ Shifty.App/Program.cs | 1 + Shifty.App/Repositories/AccountRepository.cs | 27 +- Shifty.App/Repositories/IAccountRepository.cs | 4 + Shifty.App/Services/IUserService.cs | 14 + Shifty.App/Services/UserService.cs | 30 ++ Shifty.App/Shared/NavMenu.razor | 1 + .../OpenApiSpecs/AnalogCoreV2.json | 310 ++++++++++++++- 10 files changed, 847 insertions(+), 47 deletions(-) create mode 100644 Shifty.App/Components/UserTable.razor create mode 100644 Shifty.App/Pages/UserManagement.razor create mode 100644 Shifty.App/Services/IUserService.cs create mode 100644 Shifty.App/Services/UserService.cs diff --git a/Shifty.Api/Generated/AnalogCoreV2/AnalogCoreV2.cs b/Shifty.Api/Generated/AnalogCoreV2/AnalogCoreV2.cs index de6c4eb..a2a7048 100644 --- a/Shifty.Api/Generated/AnalogCoreV2/AnalogCoreV2.cs +++ b/Shifty.Api/Generated/AnalogCoreV2/AnalogCoreV2.cs @@ -513,6 +513,115 @@ public virtual async System.Threading.Tasks.Task ApiV2Accou } } + /// + /// Updates the user group of a user + /// + /// id of the user whose userGroup will be updated + /// Update User Group information request + /// The update was processed + /// A server side error occurred. + public virtual System.Threading.Tasks.Task ApiV2AccountUserGroupAsync(int id, UpdateUserGroupRequest updateUserGroupRequest) + { + return ApiV2AccountUserGroupAsync(id, updateUserGroupRequest, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Updates the user group of a user + /// + /// id of the user whose userGroup will be updated + /// Update User Group information request + /// The update was processed + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task ApiV2AccountUserGroupAsync(int id, UpdateUserGroupRequest updateUserGroupRequest, System.Threading.CancellationToken cancellationToken) + { + if (id == null) + throw new System.ArgumentNullException("id"); + + if (updateUserGroupRequest == null) + throw new System.ArgumentNullException("updateUserGroupRequest"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append("api/v2/account/{id}/user-group"); + urlBuilder_.Replace("{id}", System.Uri.EscapeDataString(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture))); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = Newtonsoft.Json.JsonConvert.SerializeObject(updateUserGroupRequest, _settings.Value); + var content_ = new System.Net.Http.StringContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PATCH"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 204) + { + return; + } + else + if (status_ == 401) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException(" Invalid credentials ", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 404) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException(" User not found ", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + /// /// Resend account verification email if account is not already verified /// @@ -616,6 +725,115 @@ public virtual async System.Threading.Tasks.Task ApiV2AccountResendVerificationE } } + /// + /// Searches a user in the database + /// + /// The page number + /// A filter to search by Id, Name or Email. When an empty string is given, all users will be returned + /// The length of a page + /// Users, possible with filter applied + /// A server side error occurred. + public virtual System.Threading.Tasks.Task ApiV2AccountSearchAsync(int? pageNum, string filter, int? pageLength) + { + return ApiV2AccountSearchAsync(pageNum, filter, pageLength, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Searches a user in the database + /// + /// The page number + /// A filter to search by Id, Name or Email. When an empty string is given, all users will be returned + /// The length of a page + /// Users, possible with filter applied + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task ApiV2AccountSearchAsync(int? pageNum, string filter, int? pageLength, System.Threading.CancellationToken cancellationToken) + { + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append("api/v2/account/search?"); + if (pageNum != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("pageNum") + "=").Append(System.Uri.EscapeDataString(ConvertToString(pageNum, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + if (filter != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("filter") + "=").Append(System.Uri.EscapeDataString(ConvertToString(filter, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + if (pageLength != null) + { + urlBuilder_.Append(System.Uri.EscapeDataString("pageLength") + "=").Append(System.Uri.EscapeDataString(ConvertToString(pageLength, System.Globalization.CultureInfo.InvariantCulture))).Append("&"); + } + urlBuilder_.Length--; + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 401) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException(" Invalid credentials ", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + /// /// Get app configuration /// @@ -1329,7 +1547,7 @@ public virtual async System.Threading.Tasks.Task ApiV2Pr } /// - /// Returns a list of available products based on a account's user group + /// Returns a list of available products based on a account's user group. /// /// Successful request /// A server side error occurred. @@ -1340,7 +1558,7 @@ public virtual async System.Threading.Tasks.Task ApiV2Pr /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// - /// Returns a list of available products based on a account's user group + /// Returns a list of available products based on a account's user group. /// /// Successful request /// A server side error occurred. @@ -1389,6 +1607,12 @@ public virtual async System.Threading.Tasks.Task ApiV2Pr return objectResponse_.Object; } else + if (status_ == 401) + { + string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("Invalid credentials", status_, responseText_, headers_, null); + } + else { var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); @@ -2432,6 +2656,40 @@ public partial class EmailExistsRequest } + /// + /// Update the UserGroup property of a user + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.18.0.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v10.0.0.0))")] + public partial class UpdateUserGroupRequest + { + /// + /// The UserGroup of a user + /// + [Newtonsoft.Json.JsonProperty("userGroup", Required = Newtonsoft.Json.Required.Always)] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public UserGroup UserGroup { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.18.0.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v10.0.0.0))")] + public enum UserGroup + { + + [System.Runtime.Serialization.EnumMember(Value = @"Customer")] + Customer = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"Barista")] + Barista = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"Manager")] + Manager = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"Board")] + Board = 3, + + } + /// /// Resend Invite email request /// @@ -2447,6 +2705,82 @@ public partial class ResendAccountVerificationEmailRequest } + /// + /// Represents a search result + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.18.0.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v10.0.0.0))")] + public partial class UserSearchResponse + { + /// + /// The users that match the query + /// + [Newtonsoft.Json.JsonProperty("users", Required = Newtonsoft.Json.Required.Always)] + [System.ComponentModel.DataAnnotations.Required] + public System.Collections.Generic.ICollection Users { get; set; } = new System.Collections.ObjectModel.Collection(); + + /// + /// The number of users that match the query + /// + [Newtonsoft.Json.JsonProperty("totalUsers", Required = Newtonsoft.Json.Required.Always)] + public int TotalUsers { get; set; } + + } + + /// + /// Basic User details + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.18.0.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v10.0.0.0))")] + public partial class SimpleUserResponse + { + /// + /// User Id + /// + [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public int Id { get; set; } + + /// + /// User's Display Name + /// + [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Name { get; set; } + + /// + /// User's Email + /// + [Newtonsoft.Json.JsonProperty("email", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public string Email { get; set; } + + /// + /// User's User group relationship + /// + [Newtonsoft.Json.JsonProperty("userGroup", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public UserGroup UserGroup { get; set; } + + /// + /// User's State + /// + [Newtonsoft.Json.JsonProperty("state", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public UserState State { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.18.0.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v10.0.0.0))")] + public enum UserState + { + + [System.Runtime.Serialization.EnumMember(Value = @"Active")] + Active = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"Deleted")] + Deleted = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"PendingActivition")] + PendingActivition = 2, + + } + /// /// App Configuration /// @@ -2663,24 +2997,6 @@ public partial class ChangedProductResponse } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.18.0.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v10.0.0.0))")] - public enum UserGroup - { - - [System.Runtime.Serialization.EnumMember(Value = @"Customer")] - Customer = 0, - - [System.Runtime.Serialization.EnumMember(Value = @"Barista")] - Barista = 1, - - [System.Runtime.Serialization.EnumMember(Value = @"Manager")] - Manager = 2, - - [System.Runtime.Serialization.EnumMember(Value = @"Board")] - Board = 3, - - } - /// /// Initiate a new product add request. /// @@ -2830,7 +3146,7 @@ public partial class ProductResponse public bool IsPerk { get; set; } /// - /// Visibility of products for users + /// Visibility of products for users /// [Newtonsoft.Json.JsonProperty("visible", Required = Newtonsoft.Json.Required.Always)] public bool Visible { get; set; } diff --git a/Shifty.App/Components/UserTable.razor b/Shifty.App/Components/UserTable.razor new file mode 100644 index 0000000..fd6d41a --- /dev/null +++ b/Shifty.App/Components/UserTable.razor @@ -0,0 +1,125 @@ +@namespace Components +@using System.ComponentModel.DataAnnotations +@using Shifty.App.Services +@using Shifty.Api.Generated.AnalogCoreV1 +@using Shifty.Api.Generated.AnalogCoreV2 +@using Shared +@using LanguageExt.UnsafeValueAccess +@inject IUserService _userService +@inject ISnackbar Snackbar + + + + + Users + + + + + Id + Name + Email + UserGroup + + + @context.Id + @context.Name + @context.Email + @context.UserGroup + + + @context.Id + @context.Name + @context.Email + + + @foreach (var group in Enum.GetValues()) { + @group + } + + + + No matching records found + + + + + + + +@code +{ + private MudTable _table; + private string searchString = ""; + UserGroup UserGroupBeforeEdit; + + private async Task> LoadUsers(TableState state) + { + var result = await _userService.SearchUsers(searchString, state.Page, state.PageSize); + + return result.Match( + Succ: res => { + return new TableData(){ Items = res.Users.AsEnumerable(), TotalItems = res.TotalUsers};; + }, + Fail: error => { + Snackbar.Add(error.Message, Severity.Error); + return new TableData(){ Items = new List(), TotalItems = 0}; + } + ); + } + + private void OnSearch(string search) + { + searchString = search; + _table.ReloadServerData(); + } + + private void UpdateUserGroup(object element) + { + var user = (SimpleUserResponse)element; + + var result = _userService.UpdateUserGroupAsync(user.Id, user.UserGroup); + + result.Match( + Succ: user => { + Snackbar.Add("User updated", Severity.Success); + }, + Fail: error => { + Snackbar.Add(error.Message, Severity.Error); + } + ); + + StateHasChanged(); + } + + + private void BackupUser(object element) + { + UserGroupBeforeEdit = ((SimpleUserResponse)element).UserGroup; + StateHasChanged(); + } + + private void ResetUserOnCancel(object user) + { + ((SimpleUserResponse)user).UserGroup = UserGroupBeforeEdit; + StateHasChanged(); + Snackbar.Add("Canceled editing", Severity.Warning); + } +} \ No newline at end of file diff --git a/Shifty.App/Pages/UserManagement.razor b/Shifty.App/Pages/UserManagement.razor new file mode 100644 index 0000000..42f41be --- /dev/null +++ b/Shifty.App/Pages/UserManagement.razor @@ -0,0 +1,24 @@ +@page "/Users" +@using Components +@inject NavigationManager NavManager + +@if (_user is not null && _user.IsInRole("Board")) +{ + +} + +@code { + [CascadingParameter] public Task AuthTask { get; set; } + private System.Security.Claims.ClaimsPrincipal _user; + + protected override async Task OnInitializedAsync() + { + var authState = await AuthTask; + _user = authState.User; + + if (_user is null || !_user.IsInRole("Board")) + { + NavManager.NavigateTo("/"); + } + } +} \ No newline at end of file diff --git a/Shifty.App/Program.cs b/Shifty.App/Program.cs index 4fdaf4d..1116593 100644 --- a/Shifty.App/Program.cs +++ b/Shifty.App/Program.cs @@ -60,6 +60,7 @@ public static void ConfigureServices(IServiceCollection services, IConfiguration services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddMudServices(config => diff --git a/Shifty.App/Repositories/AccountRepository.cs b/Shifty.App/Repositories/AccountRepository.cs index c2557e8..29ffbde 100755 --- a/Shifty.App/Repositories/AccountRepository.cs +++ b/Shifty.App/Repositories/AccountRepository.cs @@ -1,21 +1,26 @@ +using System.Collections.Generic; using System.Threading.Tasks; using LanguageExt; +using LanguageExt.ClassInstances.Pred; using LanguageExt.Common; using Shifty.Api.Generated.AnalogCoreV1; +using Shifty.Api.Generated.AnalogCoreV2; using static LanguageExt.Prelude; namespace Shifty.App.Repositories { public class AccountRepository : IAccountRepository { - private readonly AnalogCoreV1 _client; + private readonly AnalogCoreV1 _v1client; + private readonly AnalogCoreV2 _v2client; - public AccountRepository(AnalogCoreV1 client) + public AccountRepository(AnalogCoreV1 v1client, AnalogCoreV2 v2client) { - _client = client; + _v1client = v1client; + _v2client = v2client; } - - public async Task> LoginAsync(string username, string password) + + public async Task> LoginAsync(string username, string password) { var dto = new LoginDto() { @@ -23,7 +28,17 @@ public async Task> LoginAsync(string username, string pa Password = password, Version = "2.1.0" }; - return await TryAsync(_client.ApiV1AccountLoginAsync(loginDto: dto)).ToEither(); + return await TryAsync(_v1client.ApiV1AccountLoginAsync(loginDto: dto)).ToEither(); + } + + public async Task> SearchUserAsync(string query, int page, int pageSize) + { + return await TryAsync(_v2client.ApiV2AccountSearchAsync(filter: query, pageNum: page, pageLength: pageSize)); + } + + public async Task> UpdateUserGroupAsync(int userId, UserGroup group) + { + return await TryAsync(_v2client.ApiV2AccountUserGroupAsync(userId, new(){UserGroup = group})); } } } \ No newline at end of file diff --git a/Shifty.App/Repositories/IAccountRepository.cs b/Shifty.App/Repositories/IAccountRepository.cs index 3b14244..7b5ab93 100644 --- a/Shifty.App/Repositories/IAccountRepository.cs +++ b/Shifty.App/Repositories/IAccountRepository.cs @@ -1,12 +1,16 @@ +using System.Collections.Generic; using System.Threading.Tasks; using LanguageExt; using LanguageExt.Common; using Shifty.Api.Generated.AnalogCoreV1; +using Shifty.Api.Generated.AnalogCoreV2; namespace Shifty.App.Repositories { public interface IAccountRepository { public Task> LoginAsync(string username, string password); + public Task> SearchUserAsync(string query, int page, int pageSize); + public Task> UpdateUserGroupAsync(int userId, UserGroup group); } } \ No newline at end of file diff --git a/Shifty.App/Services/IUserService.cs b/Shifty.App/Services/IUserService.cs new file mode 100644 index 0000000..c603def --- /dev/null +++ b/Shifty.App/Services/IUserService.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using LanguageExt; +using LanguageExt.Common; +using Shifty.Api.Generated.AnalogCoreV2; + +namespace Shifty.App.Services +{ + public interface IUserService + { + Task> SearchUsers(string query, int pageNumber = 0, int pageSize = 10); + Task> UpdateUserGroupAsync(int userId, UserGroup group); + } +} \ No newline at end of file diff --git a/Shifty.App/Services/UserService.cs b/Shifty.App/Services/UserService.cs new file mode 100644 index 0000000..235ed45 --- /dev/null +++ b/Shifty.App/Services/UserService.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Blazored.LocalStorage; +using LanguageExt; +using LanguageExt.Common; +using Shifty.Api.Generated.AnalogCoreV2; +using Shifty.App.Repositories; + +namespace Shifty.App.Services +{ + public class UserService : IUserService + { + private readonly IAccountRepository _accountRepository; + + public UserService(IAccountRepository accountRepository) + { + _accountRepository = accountRepository; + } + + public Task> SearchUsers(string query, int pageNumber = 0, int pageSize = 10) + { + return _accountRepository.SearchUserAsync(query, pageNumber, pageSize); + } + + public Task> UpdateUserGroupAsync(int userId, UserGroup group) + { + return _accountRepository.UpdateUserGroupAsync(userId, group); + } + } +} \ No newline at end of file diff --git a/Shifty.App/Shared/NavMenu.razor b/Shifty.App/Shared/NavMenu.razor index c5d6743..bcfcf88 100644 --- a/Shifty.App/Shared/NavMenu.razor +++ b/Shifty.App/Shared/NavMenu.razor @@ -4,6 +4,7 @@ { Issue vouchers Product Management + Manage users } diff --git a/Shifty.GenerateApi/OpenApiSpecs/AnalogCoreV2.json b/Shifty.GenerateApi/OpenApiSpecs/AnalogCoreV2.json index fddc00f..a3ae47d 100644 --- a/Shifty.GenerateApi/OpenApiSpecs/AnalogCoreV2.json +++ b/Shifty.GenerateApi/OpenApiSpecs/AnalogCoreV2.json @@ -208,6 +208,74 @@ } } }, + "/api/v2/account/{id}/user-group": { + "patch": { + "tags": [ + "Account" + ], + "summary": "Updates the user group of a user", + "operationId": "Account_UpdateAccountUserGroup", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "id of the user whose userGroup will be updated ", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 1 + } + ], + "requestBody": { + "x-name": "updateUserGroupRequest", + "description": "Update User Group information request ", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserGroupRequest" + } + } + }, + "required": true, + "x-position": 2 + }, + "responses": { + "204": { + "description": " The update was processed " + }, + "401": { + "description": " Invalid credentials ", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": " User not found ", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + }, + "security": [ + { + "jwt": [] + }, + { + "apikey": [] + } + ] + } + }, "/api/v2/account/resend-verification-email": { "post": { "tags": [ @@ -255,6 +323,82 @@ } } }, + "/api/v2/account/search": { + "get": { + "tags": [ + "Account" + ], + "summary": "Searches a user in the database", + "operationId": "Account_SearchUsers", + "parameters": [ + { + "name": "pageNum", + "in": "query", + "description": "The page number", + "schema": { + "type": "integer", + "format": "int32", + "maximum": 2147483647.0, + "minimum": 0.0 + }, + "x-position": 1 + }, + { + "name": "filter", + "in": "query", + "description": "A filter to search by Id, Name or Email. When an empty string is given, all users will be returned", + "schema": { + "type": "string", + "default": "" + }, + "x-position": 2 + }, + { + "name": "pageLength", + "in": "query", + "description": "The length of a page", + "schema": { + "type": "integer", + "format": "int32", + "default": 30, + "maximum": 100.0, + "minimum": 1.0 + }, + "x-position": 3 + } + ], + "responses": { + "401": { + "description": " Invalid credentials ", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "200": { + "description": "Users, possible with filter applied", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSearchResponse" + } + } + } + } + }, + "security": [ + { + "jwt": [] + }, + { + "apikey": [] + } + ] + } + }, "/api/v2/appconfig": { "get": { "tags": [ @@ -583,7 +727,7 @@ "tags": [ "Products" ], - "summary": "Returns a list of available products based on a account's user group", + "summary": "Returns a list of available products based on a account's user group.", "operationId": "Products_GetProducts", "responses": { "200": { @@ -598,8 +742,19 @@ } } } + }, + "401": { + "description": "Invalid credentials" } - } + }, + "security": [ + { + "jwt": [] + }, + { + "apikey": [] + } + ] } }, "/api/v2/products/all": { @@ -970,7 +1125,7 @@ "name": "John Doe", "email": "john@doe.com", "password": "[no example provided]", - "programme": 1 + "programmeId": 1 }, "additionalProperties": false, "required": [ @@ -1238,6 +1393,44 @@ } } }, + "UpdateUserGroupRequest": { + "type": "object", + "description": "Update the UserGroup property of a user", + "example": { + "UserGroup": "Barista" + }, + "additionalProperties": false, + "required": [ + "userGroup" + ], + "properties": { + "userGroup": { + "description": "The UserGroup of a user", + "example": "UserGroup.Barista ", + "oneOf": [ + { + "$ref": "#/components/schemas/UserGroup" + } + ] + } + } + }, + "UserGroup": { + "type": "string", + "description": "", + "x-enumNames": [ + "Customer", + "Barista", + "Manager", + "Board" + ], + "enum": [ + "Customer", + "Barista", + "Manager", + "Board" + ] + }, "ResendAccountVerificationEmailRequest": { "type": "object", "description": "Resend Invite email request", @@ -1258,6 +1451,98 @@ } } }, + "UserSearchResponse": { + "type": "object", + "description": "Represents a search result", + "example": { + "users": [ + { + "id": 12232, + "name": "John Doe", + "email": "johndoe@itu.dk", + "userGroup": "Barista", + "state": "Active" + } + ], + "totalUsers": 1 + }, + "additionalProperties": false, + "required": [ + "users", + "totalUsers" + ], + "properties": { + "users": { + "type": "array", + "description": "The users that match the query", + "example": "[\n {\n \"id\": 12232,\n \"name\": \"John Doe\",\n \"email\": \"johndoe@itu.dk\",\n \"userGroup\": \"Barista\",\n \"state\": \"Active\"\n }\n ],", + "items": { + "$ref": "#/components/schemas/SimpleUserResponse" + } + }, + "totalUsers": { + "type": "integer", + "description": "The number of users that match the query", + "format": "int32", + "example": 1 + } + } + }, + "SimpleUserResponse": { + "type": "object", + "description": "Basic User details", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer", + "description": "User Id", + "format": "int32", + "example": 1 + }, + "name": { + "type": "string", + "description": "User's Display Name", + "example": "Name" + }, + "email": { + "type": "string", + "description": "User's Email", + "example": "john@doe.test" + }, + "userGroup": { + "description": "User's User group relationship", + "example": "Barista", + "oneOf": [ + { + "$ref": "#/components/schemas/UserGroup" + } + ] + }, + "state": { + "description": "User's State", + "example": "Active", + "oneOf": [ + { + "$ref": "#/components/schemas/UserState" + } + ] + } + } + }, + "UserState": { + "type": "string", + "description": "", + "x-enumNames": [ + "Active", + "Deleted", + "PendingActivition" + ], + "enum": [ + "Active", + "Deleted", + "PendingActivition" + ] + }, "AppConfig": { "type": "object", "description": "App Configuration", @@ -1481,22 +1766,6 @@ } } }, - "UserGroup": { - "type": "string", - "description": "", - "x-enumNames": [ - "Customer", - "Barista", - "Manager", - "Board" - ], - "enum": [ - "Customer", - "Barista", - "Manager", - "Board" - ] - }, "AddProductRequest": { "type": "object", "description": "Initiate a new product add request.", @@ -1649,6 +1918,7 @@ "name": "Coffee clip card", "description": "Coffee clip card of 10 clips", "isPerk": true, + "visible": true, "AllowedUserGroups": [ "Manager", "Board" @@ -1703,7 +1973,7 @@ }, "visible": { "type": "boolean", - "description": "Visibility of products for users ", + "description": "Visibility of products for users", "example": true }, "allowedUserGroups": {