16 Mei 202411
Pada tutorial kali ini, kita akan membuat fitur untuk melakukan manajemen user. Fitur ini hanya bisa dilakukan oleh user yang mempunyai role sebagai admin.
Role admin dapat menambahkan user baru, modifikasi informasi user, menghapus user dari sistem, serta akses ke halaman-halaman UserManagement
.
Pertama, kita copy terlebih dahulu file _StatusMessage.cshtml
pada folder /Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml
ke /Pages/Shared
agar bisa kita gunakan pada fitur UserManagement
.
Kita lanjutkan dengan melakukan edit pada Index.cshtml
dan Index.cshtml.cs
. Kita ubah Index.cshtml
agar bisa menampilkan list dari user beserta fitur untuk manajemen user.
@page
@model UserManagementModel
@{
ViewData["Title"] = "User Management";
}
<h1>User Management</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<partial name="_StatusMessage" for="StatusMessage" />
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.User[0].Id)
</th>
<th>
@Html.DisplayNameFor(model => model.User[0].Email)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.User)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Id)
</td>
<td>
@Html.DisplayFor(modelItem => item.Email)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Pada halaman ini, kita menambahkan daftar user yang ada di aplikasi beserta empat link untuk melakukan penambahan user, melihat detail user, melakukan perubahan data pada user,
dan menghapus user. Daftar user akan menampilkan ID dan email seluruh user. Terdapat juga StatusMessage
sebagai partial view yang berfungsi untuk menampilkan
status ketika melakukan penambahan atau menghapus data.
Kemudian buka file Index.cshtml.cs
dan ubah kodenya menjadi seperti ini.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace BoilerplateWebApp.Pages;
[Authorize(Roles = "Admin")] // Penambahan info protected page sesuai role
public class UserManagementModel : PageModel
{
private readonly ILogger<UserManagementModel> _logger;
private readonly UserManager<IdentityUser> _userManager;
public UserManagementModel(ILogger<UserManagementModel> logger, UserManager<IdentityUser> userManager)
{
_logger = logger;
_userManager = userManager;
}
public new IList<IdentityUser> User { get; set; } = default!;
[TempData]
public string StatusMessage { get; set; } = string.Empty;
public void OnGet()
{
User = _userManager.Users.ToList();
_logger.LogInformation("");
}
}
Index.cshtml.cs
merupakan bagian dimana informasi seluruh user didapatkan. Kemudian file ini akan mengirimkan data tersebut kepada Index.cshtml
.
[Authorize(Roles = "Admin")]
ditambahkan agar rute yang ada di file ini hanya bisa diakses oleh user yang mempunyai role sebagai admin.
public new IList<IdentityUser> User { get; set; } = default!;
merupakan tempat untuk menyimpan seluruh informasi user. Nantinya, properti ini akan diupdate
saat _userManager.Users.ToList();
dipanggil untuk menampilkan informasi seluruh user.
Ketika teman-teman menjalankan aplikasi ini menggunakan dotnet watch
atau dotnet run
serta login sebagai admin, maka teman-teman akan melihat tampilan berikut.
Tampilan berikut menampilkan seluruh user yang sudah melakukan registrasi pada aplikasi ini.
Selanjutnya, mari kita buat fitur User Detail
. Ketika admin user melakukan klik pada tautan detail, maka akan ditampilkan detail dari user.
Buka file Detail.cshtml
dan tambahkan kode berikut.
@page
@model DetailsModel
@{
ViewData["Title"] = "User Management - Detail";
}
<h1>User Management - User Detail</h1>
<div>
<h4>User</h4>
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.User.Id)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.User.Id)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.User.Email)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.User.Email)
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.User.Id">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>
Rute User Detail
ini akan menampilkan informasi user berupa Id
dan Email
. Nantinya teman-teman bisa menambahkan informasi
apabia diperlukan contohnya seperti nama atau profile picture.
Kemudian edit Detail.cshtml.cs
agar bisa memberikan informasi user sesuai dengan ID user.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace BoilerplateWebApp.Pages;
[Authorize(Roles = "Admin")]
public class DetailsModel : PageModel
{
private readonly ILogger<DetailsModel> _logger;
private readonly UserManager<IdentityUser> _userManager;
public DetailsModel(ILogger<DetailsModel> logger, UserManager<IdentityUser> userManager)
{
_logger = logger;
_userManager = userManager;
}
public new IdentityUser User { get; set; } = default!;
public async Task<IActionResult> OnGetAsync(string? id)
{
if (id == null)
{
return NotFound();
}
var user = await _userManager.FindByIdAsync(id);
if (user == null)
{
return NotFound();
}
else
{
User = user;
}
return Page();
}
}
_userManager.FindByIdAsync(id);
merupakan API yang bisa kita gunakan untuk menampilkan informasi user berdasarkan ID user.
Data user akan disimpan pada property user agar bisa ditampilkan oleh Detail.cshtml
.
Tampilan halaman user detail akan menjadi seperti ini.
Setelah membuat fitur mengenai daftar user dan user detail, mari kita buat untuk edit informasi user. Fitur edit user ini juga hanya bisa dilakukan oleh user yang mempunyai role admin.
Buka file Edit.cshtml
dan kita tampilkan detail user serta opsi untuk mengubah role.
@page
@model EditModel
@{
ViewData["Title"] = "User Management - Detail";
}
<h1>User Management - Edit</h1>
<div>
<h4>User</h4>
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.User.Id)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.User.Id)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.User.Email)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.User.Email)
</dd>
<dt class="col-sm-2">
Role
</dt>
<dd class="col-sm-10">
<partial name="_StatusMessage" for="StatusMessage" />
<form asp-route-id="@Model.User.Id">
<select asp-for="SelectedRole" class="form-control">
@if(@Model.Role == "Admin")
{
<option value="Admin" selected>
<p>Admin - current role</p>
</option>
} else
{
<option value="Admin">
<p>Admin</p>
</option>
}
@if(@Model.Role == "User")
{
<option value="User" selected>
<p>User - current role</p>
</option>
} else
{
<option value="User">
<p>User</p>
</option>
}
</select>
<button type="submit" class="btn btn-primary mt-2">Save</button>
</form>
</dd>
</dl>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
Selanjutnya, buka file Edit.cshtml.cs
dan tambahkan kode berikut.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace BoilerplateWebApp.Pages;
[Authorize(Roles = "Admin")]
public class EditModel : PageModel
{
private readonly ILogger<EditModel> _logger;
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
public EditModel(ILogger<EditModel> logger, UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
{
_logger = logger;
_userManager = userManager;
_signInManager = signInManager;
}
[BindProperty]
public new IdentityUser User { get; set; } = default!;
public string Role { get; set; } = string.Empty;
[BindProperty]
public string SelectedRole { get; set; } = string.Empty;
[TempData]
public string StatusMessage { get; set; } = string.Empty;
public List<string> Roles { get; set; } = new List<string> { "Admin", "User" };
public async Task<IActionResult> OnGetAsync(string? id)
{
if (id == null)
{
return NotFound();
}
var user = await _userManager.FindByIdAsync(id);
if (user == null)
{
return NotFound();
}
else
{
User = user;
}
var roles = await _userManager.GetRolesAsync(user);
Role = roles.FirstOrDefault()!;
_logger.LogInformation("");
return Page();
}
public async Task<IActionResult> OnPostAsync(string? id)
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.FindByIdAsync(id!);
if (user == null)
{
return NotFound();
}
else
{
User = user;
}
var userRoles = await _userManager.GetRolesAsync(User);
var currentRole = userRoles.FirstOrDefault()!;
if (currentRole != SelectedRole)
{
if (!string.IsNullOrEmpty(currentRole))
{
await _userManager.RemoveFromRoleAsync(user, currentRole);
}
if (!string.IsNullOrEmpty(SelectedRole))
{
await _userManager.AddToRoleAsync(user, SelectedRole);
}
await _userManager.UpdateSecurityStampAsync(user);
var authenticatedUserId = _userManager.GetUserAsync(HttpContext.User).Result!.Id; // User yang sedang login
if (id == authenticatedUserId)
{
await _signInManager.SignOutAsync();
}
}
StatusMessage = "User role has been updated";
return Redirect($"/UserManagement/Edit?id={id}");
}
}
Method OnGetAsync
akan menampilkan data user yang akan kita edit. Kemudian apabila kita mengubah role user, maka OnPostAsync
akan memproses perubahan data tersebut. Pertama, user akan dicek apakah user memang valid. Apabila valid, kemudian akan dilanjutkan
dengan menghapus role user ini (_userManager.RemoveFromRoleAsync
) dan menambahkan role baru (_userManager.AddToRoleAsync
).
Kemudian, apabila user yang sedang login melakukan perubahan role terhadap informasinya sendiri, maka user tersebut akan logout.
Contoh skenario ini yaitu ketika user dengan role admin mengganti role menjadi user saat sedang login. Proses pengecekan ini terjadi
pada method OnPostAsync
yaitu di bagian ini.
Pada StatusMessage
, kita menambahkan informasi bahwa operasi edit ini telah berhasil dan akan ditampilkan oleh view pada frontend.
// kode sebelumnya
...
var authenticatedUserId = _userManager.GetUserAsync(HttpContext.User).Result!.Id; // User yang sedang login
if (id == authenticatedUserId)
{
await _signInManager.SignOutAsync();
}
...
Berikut tampilan halaman Edit dengan opsi untuk mengubah role user dan informasi mengenai role user.
Fitur selanjutnya untuk UserManagement
adalah fitur untuk menghapus user. Pada proses menghapus user, kita akan
membuat delete.cshtml
menjadi halaman konfirmasi untuk melakukan delete. Setelah proses ini selesai, user akan kita
arahkan kembali ke halaman list seluruh user.
Edit file delete.cshtml
dengan memasukkan kode berikut.
@page
@model DeleteModel
@{
ViewData["Title"] = "User Management - Delete";
}
<h1>User Management - Delete</h1>
<div>
<h4>User</h4>
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.User.Id)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.User.Id)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.User.Email)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.User.Email)
</dd>
<div class="mt-3">
@if(Model.User.Id == ViewData["authenticatedUserId"]!.ToString())
{
<div>Please visit <a href="/Identity/Account/Manage/PersonalData">this link</a> to remove your account</div>
}
else
{
<form asp-route-id="@Model.User.Id">
<button type="submit" class="btn btn-danger mt-2">Delete</button>
</form>
}
</div>
</dl>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
File ini menampilkan informasi user yang akan dihapus serta tombol Delete
sebagai trigger untuk menghapus data user.
Ketika user klik tombol Delete
, maka akan ada proses HTTP Post
ke delete.cshtml.cs
.
Selanjutnya kita buka delete.cshtml.cs
dan tambahkan kode berikut agar bisa mengolah data dari frontend.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace BoilerplateWebApp.Pages;
[Authorize(Roles = "Admin")]
public class DeleteModel : PageModel
{
private readonly ILogger<EditModel> _logger;
private readonly UserManager<IdentityUser> _userManager;
public DeleteModel(ILogger<EditModel> logger, UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
{
_logger = logger;
_userManager = userManager;
}
public new IdentityUser User { get; set; } = default!;
[TempData]
public string StatusMessage { get; set; } = string.Empty;
public async Task<IActionResult> OnGetAsync(string? id)
{
if (id == null)
{
return NotFound();
}
var user = await _userManager.FindByIdAsync(id);
if (user == null)
{
return NotFound();
}
else
{
User = user;
}
ViewData["authenticatedUserId"] = _userManager.GetUserAsync(HttpContext.User).Result!.Id;
_logger.LogInformation("");
return Page();
}
public async Task<IActionResult> OnPostAsync(string id)
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.FindByIdAsync(id);
if (user == null)
{
return NotFound();
}
var result = await _userManager.DeleteAsync(user);
if (!result.Succeeded)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
return Page();
}
StatusMessage = "User has been deleted";
return Redirect("/UserManagement/");
}
}
Method OnGetAsync
akan mengirimkan data mengenai user yang akan dihapus. Lalu, method OnPostAsync
akan melakukan
penghapusan data user dan akan melakukan redirect ke halaman list user. _userManager.DeleteAsync(user);
merupakan bagian
dimana data user dihapus dari aplikasi ini.
User yang login dan mempunyai role sebagai admin hanya bisa menghapus datanya dari halaman pada URL /Identity/Account/Manage/PersonalData
.
Proses cek dilakukan dengan membandingkan user yang login dengan info user yang ingin dihapus menggunakan
informasi ViewData["authenticatedUserId"]
. User yang sedang login dengan role admin akan diarahkan untuk mengunjungi halaman
/Identity/Account/Manage/PersonalData
.
Apabila user yang login berbeda dengan user yang ingin dihapus, maka akan terlihat tombol Delete
seperti ini.
Fitur terakhir yang kita buat yaitu fitur untuk menambahkan user baru. Buka file Create.cshtml
dan ubah menjadi seperti berikut.
@page
@model CreateModel
@{
ViewData["Title"] = "User Management - Create";
}
<h1>User Management - Create</h1>
<div>
<h4>New User</h4>
<div class="col-md-4 mb-4">
<form method="post">
<div class="text-danger" role="alert"></div>
<div class="mb-3">
<input asp-for="Email" class="form-control" autocomplete="username" aria-required="true" placeholder="[email protected]" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Create User</button>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
Create.cshtml
akan menampilkan form untuk membuat user baru. Admin bisa menambahkan user baru dengan menambahkan email dari user baru.
Email untuk user baru harus unik sehingga user satu dengan yang lain tidak akan memiliki email yang sama.
Selanjutnya, edit file Create.cshtml.cs
dan tambahkan kode berikut.
using System.ComponentModel.DataAnnotations;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
namespace BoilerplateWebApp.Pages;
[Authorize(Roles = "Admin")]
public class CreateModel : PageModel
{
private readonly ILogger<EditModel> _logger;
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly IUserStore<IdentityUser> _userStore;
private readonly IUserEmailStore<IdentityUser> _emailStore;
private readonly IEmailSender _emailSender;
public CreateModel(
ILogger<EditModel> logger,
UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> signInManager,
IUserStore<IdentityUser> userStore,
IEmailSender emailSender)
{
_logger = logger;
_userManager = userManager;
_signInManager = signInManager;
_userStore = userStore;
_emailStore = GetEmailStore();
_emailSender = emailSender;
}
[BindProperty]
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; } = String.Empty;
public string Password { get; set; } = "DevK@ge0nline";
[TempData]
public string StatusMessage { get; set; } = string.Empty;
public IActionResult OnGetAsync()
{
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = new IdentityUser
{
UserName = Email,
Email = Email,
EmailConfirmed = true
};
var result = await _userManager.CreateAsync(user, Password); // Membuat user dengan default password
if (result.Succeeded)
{
_logger.LogInformation("User created a new account without password.");
await _userManager.AddToRoleAsync(user, "User");
await _emailSender.SendEmailAsync(Email, "Confirm your email", $"Your default password is <b>{Password}</b>");
StatusMessage = "User has been created successfully.";
return RedirectToPage("/UserManagement/Index");
}
else
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
return Page();
}
private IUserEmailStore<IdentityUser> GetEmailStore()
{
if (!_userManager.SupportsUserEmail)
{
throw new NotSupportedException("The default UI requires a user store with email support.");
}
return (IUserEmailStore<IdentityUser>)_userStore;
}
}
User yang ditambahkan nanti akan mempunyai default Password
yaitu DevK@ge0nline
dan role User
. Method OnPostAsync()
akan menerima
data berupa email user baru dari frontend dan melakukan proses pembuatan user melalui _userManager.CreateAsync
. GetEmailStore()
akan kita gunakan nanti
untuk mengirimkan konfirmasi email kepada user baru.
Nah, saat ini aplikasi boilerplate ini sudah mempunyai user management yang hanya bisa diakses oleh user dengan role admin. Fitur ini merupakan penerapan konsep authorization dengan menggunakan informasi role. Teman-teman bisa menambahkan role lain dengan akses yang berbeda sesuai dengan kebutuhan aplikasi nantinya.
Pada bagian selanjutnya, kita akan menambahkan fitur file upload dan email notification untuk aplikasi ini sehingga aplikasi boilerplate ini menjadi lebih banyak fitur sebagai dasar pembuatan aplikasi-aplikasi lain nantinya.