Membuat Boilerplate Aplikasi Web Dengan Menggunakan ASP.NET Core - Bagian 3

16 Mei 202411 

Tutorial ini terdiri dari lima bagian.

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.

Status Info

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.

Index Page

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.

User List

User Detail

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.

User Detail

Edit User

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.

User Detail

Delete 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.

User Detail

Apabila user yang login berbeda dengan user yang ingin dihapus, maka akan terlihat tombol Delete seperti ini.

User Detail

Create User

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.

Source Code Tutorial - Github DevKage
Suka konten ini? ❤️ Dukung DevKage melalui Saweria.