Blazor: Implementing the Role Based Routing in Blazor WebAssembly
In this article we will implement the Role-Based routing in Blazor WebAssembly application. In previous article, I have explained the Token Based Authentication and Policy-Based Authorization. In that article, I have explained the mechanism of using the UserName and RoleName in the JSON Web Token (JWT) claims. In the current article, we will use the same API project (I have provided the link at the end of this article.) for security and the Blazor WebAssembly project will invoke the API endpoints securely. Figure 1 shows the implementation guideline.
Figure 1: The Implementation
As shown in Figure 1, the execution takes place as follows:
- The Blazor WebAssembly client application make call to API application by sending the credentials.
- The API application generate JSON Web Token based on UserName and RoleName.
- The Token along with the UserName and RoleName is send to the client.
- The Blazor Application saves the Token UserName, and RoleName to Session Storage.
- Blazor WebAssembly client application uses the RoleName and renders the routes based on the RoleName.
Figure 2: The Blazor WebAssembly Routes
As shown in Figure 2, the Administrator role has all access to all features of the application e.g. Creating roles, approving users, and order management. The Manager role can perform Order Management except the Processing the order i.e. Approve or Reject the order. The Clerk role can only Read the order and create new order.
The Implementation
Step 1: Open Visual Studio 2022 and create a new Blazor WebAssembly application named Blazor_Client. In this project add a new folder named Models. Copy the Order.cs, ResponseObject.cs, and SecurityModels.cs class files from the API project and paste these files in the Models folder. we will be using the same classes in the Blazor WebAssembly project. In this project add NuGet Package named Blazored.SessionStorage so that received token will be saved in session storage.
Step 2: In the Pages folder add a new Razor Component named RegisterNewUser.razor. In this component we will write code for registering the new user for the application. The component is injected with HttpClient class to invoke createuser API endpoint. Listing 1 shows the code for the component.
@page "/registeruser" @using Blazor_Client.Models @using System.Text.Json @inject HttpClient httpClient @inject NavigationManager navigationManager <h3>Register New User</h3> <div class="container"> <EditForm Model="registerUser" OnValidSubmit="@CreateUser"> <DataAnnotationsValidator/> <ValidationSummary/> <div class="form-group"> <label for="Email">Email:</label> <InputText @bind-Value="registerUser.Email" class="form-control"/> <ValidationMessage For="@(() => registerUser.Email)"/> </div> <div class="form-group"> <label for="Password">Password:</label> <InputText @bind-Value="registerUser.Password" type="password" class="form-control" /> <ValidationMessage For="@(() => registerUser.Password)" /> </div> <div class="form-group"> <label for="ConfirmPassword">Confirm Password:</label> <InputText @bind-Value="registerUser.ConfirmPassword" type="password" class="form-control" /> <ValidationMessage For="@(() => registerUser.ConfirmPassword)" /> </div> <div class="btn-group-lg"> <button class="btn btn-warning" @onclick="Clear">Clear</button> <button type="submit" class="btn btn-success" >Save</button> </div> </EditForm> @if (!isSuccess) { <div class="form-group"> <strong> Sorry!!! The User Registration failed, please try after some time. </strong> </div> } else { if (secureResponse.StatucCode == 500) { <div class="form-group"> <strong> @secureResponse.Message </strong> </div> } else { <div class="form-group"> <strong> @secureResponse.Message </strong> </div> } } </div> @code { private RegisterUser registerUser = new RegisterUser(); bool isSuccess = false; private string url = "https://localhost:7291/api/createuser"; SecureResponse? secureResponse = new SecureResponse(); private async Task CreateUser() { try { var response = await httpClient.PostAsJsonAsync(url,registerUser); if (response.IsSuccessStatusCode) { isSuccess = true; var r = await response.Content.ReadAsStringAsync(); secureResponse = JsonSerializer.Deserialize<SecureResponse>(r); } } catch (Exception ex) { throw; } } private void Clear() { registerUser = new RegisterUser(); secureResponse = new SecureResponse(); } }
Listing 1: The RegisterNewUser component
Step 2: In the Pages folder, add a new razor component named Login.razor. In this component we will write code for invoking authuser API so that the credentials will be send to API to authenticate the user. Once the user successfully authenticated the received Token with details like UserName and RoleName will be stored in the Session Storage. Listing 2 shows code for the Login component.
@page "/login" @using Blazor_Client.Models @using Blazored.SessionStorage @using System.Text.Json @inject HttpClient httpClient; @inject NavigationManager navigationManager @inject ISessionStorageService session; <h3>Login</h3> <div class="container"> <div class="form-group"> <label for="Email">Email:</label> <InputText @bind-Value="user.Email" class="form-control" /> </div> <div class="form-group"> <label for="Password">Password:</label> <InputText @bind-Value="user.Password" type="password" class="form-control" /> </div> <div class="btn-group-lg"> <button class="btn btn-warning" @onclick="clear">Clear</button> <button class="btn btn-primary" @onclick="login">Login</button> </div> <hr/> @if (isLoginFailed) { <div class="alert alert-danger"> <strong> @errorMessage </strong> <button class="btn btn-warning" @onclick="gotohome">Go To Home Page</button> </div> } </div> @code { private LoginUser user = new LoginUser(); string url = "https://localhost:7291/api/authuser"; string errorMessage = string.Empty; private SecureResponse? securityResponse = new SecureResponse(); bool isLoginFailed = false; private async Task login() { var response = await httpClient.PostAsJsonAsync<LoginUser>(url, user); if (response.IsSuccessStatusCode) { var receivedResponse = await response.Content.ReadAsStringAsync(); securityResponse = JsonSerializer.Deserialize<SecureResponse>(receivedResponse); if (securityResponse?.StatucCode == 500) { errorMessage = $"The User {user.Email} is found, but role is not assigned to this user so the use cannot be authenticated"; isLoginFailed = true; } else { await session.SetItemAsync("authToken", securityResponse.Token); await session.SetItemAsync("RoleName", securityResponse.RoleName); await session.SetItemAsync("UserName", securityResponse.UserName); navigationManager.NavigateTo("/"); } } else { errorMessage = "Login failed. Please check your credentials."; } this.StateHasChanged(); } private void clear() { user = new LoginUser(); errorMessage = string.Empty; } private void gotohome() { navigationManager.NavigateTo("/"); } }
Listing 2: The Login component
Step 3: In the Pages folder, add a new razor component named ApproveUser.razor. In this component we will implement code for approving the user so that the user can login with the API application and can perform the order management operations. This component will be accessible only by the Administrator role. The component defines logic for invoking users and roles API to access users and roles so that role can be assigned to the user. The component in its OnInitializedAsync() method read the RoleName from the Session Storage. If this role is Administrator, then only the users and roles APIs are accessed to show users and roles to the Administrator role. The component has the approveuser() method. This method invokes the aproveuser API endpoint and using HTTP POST request the UserRole information sent to it along with the token in the HTTP header to assign role to the user. Listing 3 shows code for the component.
@page "/approveuser" @using Blazor_Client.Models; @using Blazored.SessionStorage @inject HttpClient httpClient; @inject NavigationManager navigationManager @inject ISessionStorageService session; <h3>Approve User</h3> <div class="container"> <table class="table table-bordered table-striped table-dark"> <tbody> <tr> <td>Select User:</td> <td> <InputSelect @bind-Value="selectedUser" TValue="string" class="form-control"> <option>Choose User....</option> @foreach (var item in users) { <option value="@item.UserName">@item.UserName</option> } </InputSelect> </td> </tr> <tr> <td>Select Role:</td> <td> <InputSelect @bind-Value="selectedRole" TValue="string" class="form-control"> <option>Choose Role....</option> @foreach (var item in roles) { <option value="@item.RoleName">@item.RoleName</option> } </InputSelect> </td> </tr> </tbody> </table> <div class="btn-group-lg"> <button class="btn btn-warning" @onclick="clear">Clear</button> <button class="btn btn-success" @onclick="approveuser">Save</button> </div> <hr/> @if (isSuccess) { <div class="container"> <strong> Congratulations!!! The User @selectedUser is successfully is assigned to Role @selectedRole </strong> </div> } else { <div class="container"> <strong> Sorry!!! The Operation is failed </strong> </div> } </div> @code { private string selectedUser = string.Empty; private string selectedRole = string.Empty; List<Users>? users = new List<Users>(); List<RoleData>? roles = new List<RoleData>(); bool isSuccess = false; string urlUsers = "https://localhost:7291/api/users"; string urlRoles = "https://localhost:7291/api/roles"; string url = "https://localhost:7291/api/approveuser"; protected override async Task OnInitializedAsync() { // Get the RoleName from the Session Storage var roleName = await session.GetItemAsync<string>("RoleName"); if (String.IsNullOrEmpty(roleName) || roleName.Trim() != "Administrator") navigationManager.NavigateTo("/accessdenied"); else { var token = await session.GetItemAsync<string>("authToken"); if (!string.IsNullOrEmpty(token)) { httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); } users = await httpClient.GetFromJsonAsync<List<Users>>(urlUsers); roles = await httpClient.GetFromJsonAsync<List<RoleData>>(urlRoles); } } private async Task approveuser() { try { var token = await session.GetItemAsync<string>("authToken"); if (!string.IsNullOrEmpty(token)) { httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); } var userRole = new UserRole() { UserName = selectedUser, RoleName = selectedRole }; var response = await httpClient.PostAsJsonAsync(url, userRole); if (response.IsSuccessStatusCode) { isSuccess = true; } } catch (Exception ex) { throw; } } private void clear() { selectedRole = string.Empty; selectedUser = string.Empty; } }
Listing 3: The ApproveUser component
Step 4: In the Pages folder, add a new razor component named AccessDenied.razor. This component will have code as shown in Listing 4.
@page "/accessdenied" <h3>Access Denied</h3> <div class="container"> <div class="alert alert-danger"> <strong> Sorry!! You do not have access to this resource. </strong> </div> </div>
Listing 4: The Access Denied Component
Step 5: In the Pages folder, add a new razor component named CreateRoles.razor. In this component we will add code for CreateRoles component. This component will be accessible to Administrator role so that the new role for the application can be created. This component invokes the createrole API endpoint by passing the token in the HTTP header. Listing 5 shows code for the component.
@page "/createrole" @using Blazor_Client.Models @using Blazor_Client.Models; @using Blazored.SessionStorage @inject HttpClient httpClient; @inject NavigationManager navigationManager @inject ISessionStorageService session; <h3>Create Roles</h3> <div class="container"> <div class="form-group"> <label for="RoleName">Enter the Role Name:</label> <InputText @bind-Value="role.RoleName" class="form-control"/> </div> <div class="btn-group-lg"> <button class="btn btn-warning" @onclick="Clear">Clear</button> <button class="btn btn-success" @onclick="CreateRole">Save</button> </div> <hr/> <div class="alert alert-warning"> <strong> @secureResponse.Message </strong> </div> </div> @code { private RoleData role = new RoleData(); SecureResponse? secureResponse = new SecureResponse(); string url = "https://localhost:7291/api/createrole"; string token = string.Empty; string RoleName = string.Empty; private async Task CreateRole() { try { token = await session.GetItemAsync<string>("authToken"); RoleName = await session.GetItemAsync<string>("RoleName"); if (RoleName == "Administrator") { if (!string.IsNullOrEmpty(token)) { httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); } var response = await httpClient.PostAsJsonAsync(url, role); if (response.IsSuccessStatusCode) secureResponse = await response.Content.ReadFromJsonAsync<SecureResponse>(); } else { navigationManager.NavigateTo("/accessdenied"); } } catch (Exception ex) { throw; } } private void Clear() { role = new RoleData(); } }
Listing 5: The CreateRoles component.
So far, we have added components for User and Role management. The ApproveUser, CreateRoles components will be accessible only by the Administrator role users.
Step 6: In the Pages folder, add new components named CreateOrder.razor. This component will be used to invoke the createorder API endpoint. This component read Token, UserName, and RoleName from the session storage and then using HttpClient the HTTP Post request is made to the API to create new order. The components use the EditForm component that is bound with the Order object. Once the order is successfully created then the user will navigate to the listorders component. Listing 6 shows the code for the component.
@page "/createorder" @using Blazor_Client.Models @using Blazored.SessionStorage @inject HttpClient httpClient @inject ISessionStorageService session; @inject NavigationManager navigationManager <h3>Create New Order</h3> <div class="container"> <EditForm Model="@order" OnValidSubmit="@SaveOrder"> <DataAnnotationsValidator/> <ValidationSummary/> <div class="form-group"> <label for="CustomerName">Customer Name</label> <InputText @bind-Value="order.CustomerName" class="form-control"/> <ValidationMessage For="@(()=>order.CustomerName)"/> </div> <div class="form-group"> <label for="ItemName">Item Name</label> <InputSelect @bind-Value="order.ItemName" TValue="string" class="form-control"> <option>Selected Item to Purchase....</option> @foreach (var item in items) { <option value="@item">@item</option> } </InputSelect> <ValidationMessage For="@(()=>order.ItemName)" /> </div> <div class="form-group"> <label for="OrderedDate">Ordered Date</label> <InputDate @bind-Value="order.OrderedDate" class="form-control" /> <ValidationMessage For="@(()=>order.OrderedDate)" /> </div> <div class="form-group"> <label for="Quantity">Ordered Quantity</label> <InputNumber @bind-Value="order.Quantity" class="form-control" /> <ValidationMessage For="@(()=>order.Quantity)" /> </div> <div class="form-group"> <label for="Quantity">Ordered Status</label> <InputText @bind-Value="order.OrderStatus" class="form-control" /> <ValidationMessage For="@(()=>order.OrderStatus)" /> </div> <div class="form-group"> <label for="Comments">Comments</label> <InputTextArea @bind-Value="order.Comments" class="form-control" /> <ValidationMessage For="@(()=>order.Comments)" /> </div> <div class="btn-group-lg"> <button class="btn btn-warning" @onclick="Clear">Clear</button> <button type="submit" class="btn btn-success" >Save</button> </div> </EditForm> </div> @code { private Order? order = new Order(); string currentUser = string.Empty; string token = string.Empty; string roleName = string.Empty; private string url = "https://localhost:7291/api/createorder"; private ResponseObject<Order>? createOrderResponse = new ResponseObject<Order>(); private List<string> items = new List<string>() { "Laptop","Mobile","Charger","Charger Cable","USB","Power Bank","Laptop Charger","Screen","Router" }; protected override void OnInitialized() { order.OrderedDate = DateOnly.FromDateTime(DateTime.Now); base.OnInitialized(); } private async Task SaveOrder() { currentUser = await session.GetItemAsync<string>("UserName"); token = await session.GetItemAsync<string>("authToken"); roleName = await session.GetItemAsync<string>("RoleName"); if (!string.IsNullOrEmpty(token) && !String.IsNullOrEmpty(currentUser) && !String.IsNullOrEmpty(roleName)) { httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); order.IsApproved = false; order.CreatedBy = currentUser; var response = await httpClient.PostAsJsonAsync(url, order); if (response.IsSuccessStatusCode) { navigationManager.NavigateTo("/listorders"); } } else { navigationManager.NavigateTo("/accessdenied"); } } private void Clear() { order = new Order(); } }
Listing 6: The Create Order component
Step 7: In the Pages folder, add a new razor component named ListOrders.razor. In this component we will add code to show list of orders. This component will be accessible to all roles of the application. This component invokes orders API endpoint and by sending the token the orders data is received from the API. Listing 7 shows the code for the component.
@page "/listorders" @using Blazored.SessionStorage @using Blazor_Client.Models @inject ISessionStorageService session @inject NavigationManager navigationManager @inject HttpClient httpClient; <h3>List Orders</h3> <div class="container"> <table class="table table-bordered table-striped"> <thead> <tr> <td>OrderId</td> <td>Customer Name</td> <td>Item Name</td> <td>Ordered Date</td> <td>Quantity</td> <td>TotalPrice</td> <td>Order Status</td> <td>Created By</td> <td>Updated By</td> <td>Updated Date</td> <td>Is Approved</td> <td>Comments</td> </tr> </thead> <tbody> @foreach (var item in orders) { <tr> <td>@item.OrderId</td> <td>@item.CustomerName</td> <td>@item.ItemName</td> <td>@item.OrderedDate</td> <td>@item.Quantity</td> <td>@item.TotalPrice</td> <td>@item.OrderStatus</td> <td>@item.CreatedBy</td> <td>@item.UpdatedBy</td> <td>@item.UpdatedDate</td> <td>@item.IsApproved</td> <td>@item.Comments</td> @if (roleName == "Manager") { <td> <button class="btn btn-warning" @onclick="@(()=>navigateToEdit(item.OrderId))">Edit</button> <button class="btn btn-danger" @onclick="@(()=>navigateToDelete(item.OrderId))">Delete</button> </td> } @if (roleName == "Administrator") { <td> <button class="btn btn-warning" @onclick="@(()=>navigateToEdit(item.OrderId))">Edit</button> <button class="btn btn-danger" @onclick="@(()=>navigateToApproveOrReject(item.OrderId))">Approve Or Reject</button> <button class="btn btn-dark" @onclick="@(()=>navigateToDelete(item.OrderId))">Delete</button> </td> } </tr> } </tbody> </table> <br/> <button class="btn btn-link" @onclick="navigateToHome">Home</button> </div> @code { private List<Order>? orders = new List<Order>(); string url = "https://localhost:7291/api/orders"; string roleName = string.Empty; protected override async Task OnInitializedAsync() { // Read the Role and Token from the session storage var currentUser = await session.GetItemAsync<string>("UserName"); var token = await session.GetItemAsync<string>("authToken"); roleName = await session.GetItemAsync<string>("RoleName"); if (!string.IsNullOrEmpty(token) && !String.IsNullOrEmpty(currentUser) && !String.IsNullOrEmpty(roleName)) { httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); orders = await httpClient.GetFromJsonAsync<List<Order>>(url); } else { navigationManager.NavigateTo("/accessdenied"); } } private void navigateToEdit(int id) { navigationManager.NavigateTo($"/editorder/{id}"); } private void navigateToDelete(int id) { navigationManager.NavigateTo($"/deleteorder/{id}"); } private void navigateToApproveOrReject(int id) { navigationManager.NavigateTo($"/approveorder/{id}"); } private void navigateToHome() { navigationManager.NavigateTo("/"); } }
Listing 7: The ListOrders component.
The most important part of the ListOrders component is that the orders data is shown in HTML table where user can Edit, Delete and Approve Reject orders. The HTML table shows Edit, Delete and Approve Reject order buttons. But these buttons will be shown based on the role of the current login user. If the current user role is Administrator, then all these buttons will be shown but for Manager user only Edit and Delete buttons will be shown.
Step 8: In the Pages folder, add a new component named EditOrder.razor. This will contain the EditOrder component. The code for this component invokes the updateorder API to update the order. The OnInitializedAsync() method of this component invokes the orders component to access the order to be updated based on the OrderId. Teh SaveOrder() method contains code to verify if the current logged in user of either of the Administrator or the Manager role, if yes then only the edit request can be made to edit the order else the accessdenied component will be navigated. Listing 8 shows the code for the EditOrder component.
@page "/editorder/{id:int}" @using Blazor_Client.Models @using Blazored.SessionStorage @inject HttpClient httpClient @inject ISessionStorageService session; @inject NavigationManager navigationManager <h3>Edit Order</h3> <div class="container"> <EditForm Model="@order" OnValidSubmit="@SaveOrder"> <DataAnnotationsValidator /> <ValidationSummary /> <div class="form-group"> <label for="CustomerName">Customer Name</label> <InputText @bind-Value="order.CustomerName" class="form-control" /> <ValidationMessage For="@(()=>order.CustomerName)" /> </div> <div class="form-group"> <label for="ItemName">Item Name</label> <InputSelect @bind-Value="order.ItemName" TValue="string" class="form-control"> <option>Selected Item to Purchase....</option> @foreach (var item in items) { <option value="@item">@item</option> } </InputSelect> <ValidationMessage For="@(()=>order.ItemName)" /> </div> <div class="form-group"> <label for="OrderedDate">Ordered Date</label> <InputDate @bind-Value="order.OrderedDate" class="form-control" /> <ValidationMessage For="@(()=>order.OrderedDate)" /> </div> <div class="form-group"> <label for="Quantity">Ordered Quantity</label> <InputNumber @bind-Value="order.Quantity" class="form-control" /> <ValidationMessage For="@(()=>order.Quantity)" /> </div> <div class="form-group"> <label for="Quantity">Ordered Status</label> <InputText @bind-Value="order.OrderStatus" class="form-control" /> <ValidationMessage For="@(()=>order.OrderStatus)" /> </div> <div class="form-group"> <label for="Comments">Comments</label> <InputTextArea @bind-Value="order.Comments" class="form-control" /> <ValidationMessage For="@(()=>order.Comments)" /> </div> <div class="btn-group-lg"> <button class="btn btn-warning" @onclick="Cancel">Cancel</button> <button type="submit" class="btn btn-success" >Save</button> </div> </EditForm> </div> @code { [Parameter] public int id { get; set; } private Order? order = new Order(); string currentUser = string.Empty; string token = string.Empty; string roleName = string.Empty; private string url = "https://localhost:7291/api/updateorder"; private EditContext editContext; private List<string> items = new List<string>() { "Laptop","Mobile","Charger","Charger Cable","USB","Power Bank","Laptop Charger","Screen","Router" }; protected override async Task OnInitializedAsync() { editContext = new EditContext(order); currentUser = await session.GetItemAsync<string>("UserName"); token = await session.GetItemAsync<string>("authToken"); roleName = await session.GetItemAsync<string>("RoleName"); if (!string.IsNullOrEmpty(token) && !String.IsNullOrEmpty(currentUser) && !String.IsNullOrEmpty(roleName)) { httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var receivedOrder = await httpClient.GetFromJsonAsync<ResponseObject<Order>>($"https://localhost:7291/api/orders/{id}"); if (receivedOrder == null) { navigationManager.NavigateTo("/recordnotfound"); } else { order = receivedOrder.Record; } } else { navigationManager.NavigateTo("/accessdenied"); } } private async Task SaveOrder() { if (!string.IsNullOrEmpty(token) && !String.IsNullOrEmpty(currentUser) && !String.IsNullOrEmpty(roleName)) { if (roleName == "Administrator" || roleName == "Manager") { httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); order.IsApproved = false; order.UpdatedBy = currentUser; order.UpdatedDate = DateOnly.FromDateTime(DateTime.Now); var response = await httpClient.PutAsJsonAsync($"{url}/{id}", order); if (response.IsSuccessStatusCode) { navigationManager.NavigateTo("/listorders"); } } else { navigationManager.NavigateTo("/accessdenied"); } } else { navigationManager.NavigateTo("/accessdenied"); } } private void Cancel() { navigationManager.NavigateTo("/listorders"); } }
Listing 8: The EditOrder component
Step 9: In the Pages folder, add a new razor component named DeleteOrder.razor. Same as the EditOrder component this component will also access the order to be edited in its OnInitializedAsync() method. The DeleteRecord() method of the DeleteOrder component will be used to invoke deleteorder API endpoint. The code in this method checks if the current login user if either Administrator or Manager, if yes then only the delete request will be made. Listing 9 shows code for the component.
@page "/deleteorder/{id:int}" @using Blazor_Client.Models @using Blazored.SessionStorage @inject HttpClient httpClient @inject ISessionStorageService session; @inject NavigationManager navigationManager <h3>Delete Order</h3> <div class="container"> <div class="form-group"> <label for="CustomerName">Customer Name</label> <InputText @bind-Value="order.CustomerName" class="form-control" readonly="readonly" /> </div> <div class="form-group"> <label for="ItemName">Item Name</label> <InputText @bind-Value="order.ItemName" class="form-control" readonly="readonly" /> </div> <div class="form-group"> <label for="OrderedDate">Ordered Date</label> <InputDate @bind-Value="order.OrderedDate" class="form-control" readonly="readonly" /> </div> <div class="form-group"> <label for="Quantity">Ordered Quantity</label> <InputNumber @bind-Value="order.Quantity" class="form-control" readonly="readonly" /> </div> <div class="form-group"> <label for="Comments">Comments</label> <InputTextArea @bind-Value="order.Comments" class="form-control" readonly="readonly" /> </div> <div class="btn-group-lg"> <button class="btn btn-warning" @onclick="Cancel">Clear</button> <button type="submit" class="btn btn-success" @onclick="DeleteRecord">Delete</button> </div> </div> @code { [Parameter] public int id { get; set; } private Order? order = new Order(); string currentUser = string.Empty; string token = string.Empty; string roleName = string.Empty; private string url = "https://localhost:7291/api/deleteorder"; private List<string> items = new List<string>() { "Laptop","Mobile","Charger","Charger Cable","USB","Power Bank","Laptop Charger","Screen","Router" }; protected override async Task OnInitializedAsync() { currentUser = await session.GetItemAsync<string>("UserName"); token = await session.GetItemAsync<string>("authToken"); roleName = await session.GetItemAsync<string>("RoleName"); if (!string.IsNullOrEmpty(token) && !String.IsNullOrEmpty(currentUser) && !String.IsNullOrEmpty(roleName)) { httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var receivedOrder = await httpClient.GetFromJsonAsync<ResponseObject<Order>>($"https://localhost:7291/api/orders/{id}"); if (receivedOrder == null) { navigationManager.NavigateTo("/recordnotfound"); } else { order = receivedOrder.Record; } } else { navigationManager.NavigateTo("/accessdenied"); } } private async Task DeleteRecord() { currentUser = await session.GetItemAsStringAsync("UserName"); token = await session.GetItemAsync<string>("authToken"); roleName = await session.GetItemAsync<string>("RoleName"); if (!string.IsNullOrEmpty(token) && !String.IsNullOrEmpty(currentUser) && !String.IsNullOrEmpty(roleName)) { if (roleName == "Administrator" || roleName == "Manager") { httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var response = await httpClient.DeleteFromJsonAsync<ResponseObject<Order>>($"{url}/{id}"); if (response.StatusCode == 201) { navigationManager.NavigateTo("/listorders"); } else { navigationManager.NavigateTo("/recordnotfound"); } } else { navigationManager.NavigateTo("/accessdenied"); } } else { navigationManager.NavigateTo("/accessdenied"); } } private void Cancel() { navigationManager.NavigateTo("/listorders"); } }
Listing 9: The DeleteOrder component
Step 10: In the Pages folder, add a new razon component named ApproveOrder.razor. This component will be only accessible to Administrator role. This component will be used to either Approve or Reject order. The Update() method of this component invokes processorder API endpoint to send orders data to Approve or Reject order. Listing 10 shows code for this component.
@page "/approveorder/{id:int}" @using Blazor_Client.Models @using Blazored.SessionStorage @inject HttpClient httpClient @inject ISessionStorageService session; @inject NavigationManager navigationManager <h3>Approve Order</h3> <div class="container"> <div class="form-group"> <label for="OrderId">Order Id</label> <InputNumber @bind-Value="order.OrderId" readonly="readonly" class="form-control" /> </div> <div class="form-group"> <label for="CustomerName">Customer Name</label> <InputText @bind-Value="order.CustomerName" readonly="readonly" class="form-control" /> </div> <div class="form-group"> <label for="ItemName">Item Name</label> <InputText @bind-Value="order.ItemName" readonly="readonly" class="form-control" /> </div> <div class="form-group"> <label for="IsApproved">Order Approve/Reject</label> <InputSelect class="form-control" @bind-Value="selection" TValue="string"> <option>Decide to Approve or Reject</option> @foreach (var item in OrderApproveReject) { <option value="@item">@item</option> } </InputSelect> </div> <div class="form-group"> <label for="Comments">Comments</label> <InputTextArea @bind-Value="order.Comments" class="form-control" /> </div> <div class="btn-group-lg"> <button class="btn btn-warning" @onclick="Cancel">Cancel</button> <button class="btn btn-success" @onclick="Update">Update Order</button> </div> </div> @code { [Parameter] public int id { get; set; } private List<string> OrderApproveReject = new List<string>() { "Approve", "Reject" }; private Order? order = new Order(); string currentUser = string.Empty; string token = string.Empty; string roleName = string.Empty; private string url = "https://localhost:7291/api/processorder"; private string? selection; protected override async Task OnInitializedAsync() { currentUser = await session.GetItemAsync<string>("UserName"); token = await session.GetItemAsync<string>("authToken"); roleName = await session.GetItemAsync<string>("RoleName"); if (!string.IsNullOrEmpty(token) && !String.IsNullOrEmpty(currentUser) && !String.IsNullOrEmpty(roleName)) { if (roleName == "Administrator") { httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var receivedOrder = await httpClient.GetFromJsonAsync<ResponseObject<Order>>($"https://localhost:7291/api/orders/{id}"); if (receivedOrder == null) { navigationManager.NavigateTo("/recordnotfound"); } else { order = receivedOrder.Record; this.StateHasChanged(); } } else { navigationManager.NavigateTo("/accessdenied"); } } else { navigationManager.NavigateTo("/accessdenied"); } } private async Task Update() { currentUser = await session.GetItemAsync<string>("UserName"); token = await session.GetItemAsync<string>("authToken"); roleName = await session.GetItemAsync<string>("RoleName"); if (!string.IsNullOrEmpty(token) && !String.IsNullOrEmpty(currentUser) && !String.IsNullOrEmpty(roleName)) { httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); if (selection == "Approve") { order.IsApproved = true; } else { order.IsApproved = false; } order.UpdatedDate = DateOnly.FromDateTime(DateTime.Now); order.UpdatedBy = currentUser; var response = await httpClient.PostAsJsonAsync($"{url}/{id}", order); if (response.IsSuccessStatusCode) { navigationManager.NavigateTo("/listorders"); } } else { navigationManager.NavigateTo("/accessdenied"); } } private void Cancel() { navigationManager.NavigateTo("/listorders"); } }
Listing 10: The ApproveOrder component
Step 11: In the Pages folder, add a new razor component named Lagout.razor. This component will be used to clear the session storage to make sure that the user cannot make any further calls. Listing 11 shows the code for Logout components.
@page "/logout" @using Blazored.SessionStorage @inject NavigationManager navigationManager @inject ISessionStorageService session; <h3>Logout</h3> <div class="container"> <strong> Are you sure you want to logout? </strong> <div class="btn btn-group-lg"> <button class="btn btn-danger" @onclick="Logoff">Logout</button> <button class="btn btn-warning" @onclick="Cancel">Cancel</button> </div> </div> @code { private async Task Logoff() { await session.ClearAsync(); this.StateHasChanged(); navigationManager.NavigateTo("/"); } private void Cancel() { navigationManager.NavigateTo("/"); } }
Listing 11: The Logout components
Step 12: In the Pages folder, add a new razor component named NotFoundRecord.razor. This component will show message if the record for Edit and Delete is not found. Listing 12 show the code for NotFoundRecord component.
@page "/recordnotfound" @inject NavigationManager navigationManager <div class="container"> <div class="alert alert-warning"> <strong> The Record you are looking for is not found or the reqested Operation is failed </strong> </div> </div> @code { private void backList() { navigationManager.NavigateTo("/listorders"); } }
Listing 12: The NotFoundRecord Component
Once we necessary components ready, it's a time for managing the routing based on roles.
Step 13: Modify the NavMenu.razor file in the Layout folder to define route based on the Roles. Figure 2 shows the access for each of the roles. Listing 13 shows the code for the routing.
@using Blazor_Client.Models @using Blazored.SessionStorage; @inject ISessionStorageService session; <div class="top-row ps-3 navbar navbar-dark"> <div class="container-fluid"> <a class="navbar-brand" href="">Blazor_Client</a> <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu"> <span class="navbar-toggler-icon"></span> </button> </div> </div> <div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu"> <nav class="nav flex-column"> <div class="nav-item px-3"> <NavLink class="nav-link" href="login"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Login </NavLink> </div> @if (String.IsNullOrEmpty(RoleName)) { <div class="nav-item px-3"> <NavLink class="nav-link" href="registeruser"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> New User </NavLink> </div> } @if (RoleName == "Administrator") { <div class="nav-item px-3"> <NavLink class="nav-link" href="approveuser"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Approve User </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="createrole"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Create Role </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="listorders"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> List All Orders </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="createorder"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Create Order </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="editorder"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Edit Order </NavLink> </div> } @if (RoleName == "Manager") { <div class="nav-item px-3"> <NavLink class="nav-link" href="listorders"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> List All Orders </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="createorder"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Create Order </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="editorder"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Edit Order </NavLink> </div> } @if (RoleName == "Clerk") { <div class="nav-item px-3"> <NavLink class="nav-link" href="listorders"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> List All Orders </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="createorder"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Create Order </NavLink> </div> } @if (!String.IsNullOrEmpty(RoleName)) { <div class="nav-item px-3"> <NavLink class="nav-link" href="logout"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Logout </NavLink> </div> } </nav> </div> @code { private bool collapseNavMenu = true; private SecureResponse response = new SecureResponse(); string? RoleName = string.Empty; string? UserName= string.Empty; protected override async Task OnInitializedAsync() { RoleName = await session.GetItemAsync<string>("RoleName"); UserName = await session.GetItemAsync<string>("UserName"); await Task.Delay(1000); this.StateHasChanged(); } private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; private void ToggleNavMenu() { collapseNavMenu = !collapseNavMenu; } private void StateHasChanged() { } }
Listing 13: The NavMenu.razor
As shown in Listing 13, the OnInitializedAsync() method, the role information is read from the Session Storage. Based on the RoleName read from the Session Storage the Navigation route links will be shown
Step 14: Modify the MainLayout.razor file to show the current username as shown in Listing 14.
@inherits LayoutComponentBase @using Blazored.SessionStorage @inject NavigationManager navigationManager @inject ISessionStorageService session; <div class="page"> <div class="sidebar"> <NavMenu /> </div> <main> <div class="top-row px-4"> <label style="color:red"> @UserName </label> <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> </div> <article class="content px-4"> @Body </article> </main> </div> @code { string UserName = string.Empty; protected override async Task OnInitializedAsync() { UserName = await session.GetItemAsync<string>("UserName"); } }
Listing 14: The MainLayout file
Step 15: Finally, modify the Program.cs to register the Session Storage service in Dependency container as shown in Listing 15.
.................. builder.Services.AddBlazoredSessionStorage(); ..................
Listing 15: The Registration of Session Storage Service
Run the Application. The following video will show the complete application execution based on all roles.
The code for this article can be downloaded from this link.