情報応用演習Ⅰ(2024)

【T9a】簡易ブログソフトウェアの作成 Part.Ⅰ ~ 認証機能の組み込み(14/14)

プロジェクトタイプASP.NET Core Web アプリ(Model-View-Controller)
プロジェクト名T9a
ソリューション名PIT9
ターゲットフレームワーク.NET 8.0(長期的なサポート)
最上位レベルのステートメントを使用しない使用する(チェックオフ)
注意
  • 本ページの作業内容は 前のページまでの続き になっていることに注意せよ.
    • 先に前のページまでをすべて読み,指示されている作業を済ませてから本ページを読むこと.
    • プロジェクトの作成作業については準備作業を参照せよ.

9a-14. ユーザー情報の削除

次にAccountsコントローラーの仕上げとして,ユーザー情報の削除のためのアクションとビューを追加しよう. このアクションもまたEditUserアクションと同様に,ログインしているユーザーが誰であるかで削除できる対象が異なる. この関係は表9a-13-1と同じで,「管理者ユーザー」は自分以外のすべてのユーザーを削除することができ, 「通常ユーザー」は自分自身のみを削除することができる.

それではユーザー情報の削除のためのアクションメソッドを定義しよう. ユーザー情報の削除はAccountsコントローラーのDeleteUserアクションで行うので,そのためのアクションメソッドを追加しよう. Controllers/AccountsController.cs のAccountsControllerクラスに__に示す内容を追記しよう.

Accountsコントローラーの追記内容(DeleteUserアクション(GET用))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// DeleteUserアクション(GET用)
[Authorize(Roles = IdentityDataSeeder.AllRoles)]
public async Task<IActionResult> DeleteUser(string? id)
{
    if (string.IsNullOrEmpty(id))
        return NotFound();

    var userName = id;

    var targetUser = await _userManager.FindByNameAsync(userName);

    if (targetUser == null)
        return NotFound();

    var currentUser = await _userManager.GetUserAsync(User);
    bool isAdminUser = await IsAdminUserAsync(currentUser);

    if ((isAdminUser && await IsAdminUserAsync(targetUser))   // 管理者ユーザーが同じ管理者ユーザーを削除しようとしている場合や
        || (!isAdminUser && targetUser.Id != currentUser!.Id))// 通常ユーザーが自分自身以外を削除しようとしている場合は
        return Forbid();                                      // アクセスを拒否する.

    ViewBag.CurrentUser = currentUser;                        // 現在のユーザー情報をViewBagに入れておく(表示に使う).
    ViewBag.IsAdminUser = isAdminUser;                        // 現在のユーザーが管理者かどうかもViewBagに入れておく(表示に使う).

    return View(targetUser);
}
Accountsコントローラーの追記内容(DeleteUserアクション(POST用))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// DeleteUserアクション(POST用)
[Authorize(Roles = IdentityDataSeeder.AllRoles)]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteUser(string? id, [Bind("UserName")] BlogUser blogUser)
{
    if (string.IsNullOrEmpty(id))
        return NotFound();

    var userName = id;

    if (userName != blogUser.UserName)
        return NotFound();

    var targetUser = await _userManager.FindByNameAsync(userName);

    if (targetUser == null)
        return NotFound();

    var currentUser = await _userManager.GetUserAsync(User);
    bool isAdminUser = await IsAdminUserAsync(currentUser);

    if ((isAdminUser && await IsAdminUserAsync(targetUser))
        || (!isAdminUser && targetUser.Id != currentUser!.Id))
        return Forbid();

    ViewBag.CurrentUser = currentUser;
    ViewBag.IsAdminUser = isAdminUser;

    if (targetUser.Id == currentUser!.Id)           // 自分自身を削除しようとしている場合(通常ユーザーの退会処理):
    {                                               //
        await _signInManager.SignOutAsync();        //    サインアウトする.
        await _userManager.DeleteAsync(targetUser); //    そのうえでユーザー情報を削除する.
        return Redirect("/");                       //    トップページ( / )にリダイレクトする.
    }                                               //
    else                                            // それ以外の場合(管理者ユーザーによるアカウントの削除処理):
    {                                               //
        await _userManager.DeleteAsync(targetUser); //    ユーザー情報を削除する.
        return RedirectToAction(nameof(Index));     //    ユーザー一覧画面(Index)にリダイレクトする.
    }                                               //
}

_の引数なしのDeleteUser()メソッドがGET用であり, _の引数ありのDeleteUser()メソッドがPOST用のアクションメソッドである. どちらのメソッドも,処理の前半はEditUserアクションとおおむね同じであるが,操作の可否を判定するロジックが少々異なる (_の18~20行目および_の23~25行目). ここでは表9a-13-1にもとづいて,以下の条件のいずれかに当てはまる場合は Forbid()メソッド1を呼び出してアクセスを拒否している.

  • 管理者ユーザーでログインており,同じ管理者ユーザーグループに属するユーザー(自分を含む)を削除しようとしている.
  • 通常ユーザーでログインしており,かつ操作対象のユーザーとログイン中のユーザーが異なる.

実際のユーザーの削除処理は_DeleteUser()メソッドの後半(30~40行目)で行っている.

自分自身を削除しようとしている場合,つまりは「通常ユーザー」が退会しようとしている場合は,SignInManager<TUser>クラスの .SignOutAsync()メソッドを 使っていったんログアウト処理を行い,そのうえでUserManager<TUser>クラスの .DeleteAsync()メソッドを 用いてユーザー情報を削除している.それ以外の場合,つまりは「管理者ユーザー」が「通常ユーザー」を削除しようとしている場合は, 単純に.DeleteAsync()メソッドでユーザー情報を削除している.

次にこのDeleteUserアクションのためのビューを作成しよう. Views/Accounts/DeleteUser.cshtml を_のように記述する.

Views/Accounts/DeleteUser.cshtmlの内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@model BlogUser

@{
    ViewData["Title"] = "ユーザー削除";

    var currentUser = (BlogUser)ViewBag.CurrentUser; // ログイン中のユーザー情報を取得する.
    var isAdminUser = (bool)ViewBag.IsAdminUser;     // ログイン中のユーザーが管理者ユーザーであるかどうかを取得する.

    if (Model is null) throw new ArgumentNullException(nameof(Model)); // ad-hoc! 非null保証のための回避策
}

<form asp-action="DeleteUser">
    @if (currentUser.Id == Model.Id)
    {
        <p>アカウントを削除しますか?</p>
        <p><strong>警告:アカウントを削除するとあなたの作成したすべての記事が失われます.</strong></p>
    }
    else
    {
        <p>ユーザー @Model.UserName を削除しますか?</p>
    }
    <input type="hidden" asp-for="@Model.UserName" />
    <input type="submit" value="削除" />
</form>

特筆すべきAPIは使用していないが,13~21行目では自分自身を削除している場合とそうではない場合で,表示するメッセージを切り替えている.

ここまで書けたら実行してみよう.プロジェクトを起動して「ログイン」のリンクをクリックしadminとして ログインしよう(起動時にadmin以外でログイン済みの場合はいったんログアウトする). 起動時に既にadminでログイン済みの場合は画面上部の「ユーザー一覧」のリンクをクリックしてユーザー一覧画面(Index)を表示させる. 適当なユーザーのリンクをクリックしてユーザー情報詳細画面(UserDetails)を表示させよう. 「退会(削除)」のリンクをクリックしてユーザー削除確認画面(DeleteUser)を表示させ,「削除」ボタンをクリックしてみよう(__). ユーザー一覧画面(Index)に遷移し,先ほど選択したユーザーが削除されていることが分かるはずである.

またadmin以外のユーザーでログインして削除,つまりは退会処理ができることを確認しておこう.いったんログアウトして, 「ログイン」のリンクをクリックしadmin以外のユーザーとしてログインする.正しくログインできれば, そのユーザーの情報を表示したユーザー情報詳細画面(UserDetails)に遷移するはずである(_). 「退会(削除)」のリンクをクリックしてユーザー削除確認画面(DeleteUser)を表示させ,「削除」ボタンをクリックしてみよう. 自動的にログアウト処理が行われトップページに遷移することが分かるはずである(__).

実行結果

これにてAccoutsコントローラーのすべての機能が完成した.次節ではウェブログソフトウェアとしての要の機能である「記事」を操作するための コントローラーの作成を行う. 次に進む前に,混乱を防ぐため Visual Studio のエディタをすべて閉じておこう.Visual Studio のいずれかのエディタのタブを右クリックして 「すべててのドキュメントを閉じる」をクリックすれば,エディタをすべて閉じることができる


  1. Forbid()メソッドは403 ForbiddenのHTTPレスポンスを返すメソッドである. ↩︎

Last updated on 2024-06-19
Published on 2024-06-19

Powered by Hugo. Theme by TechDoc. Designed by Thingsym.