Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dressca管理アプリを作成する #1453

Closed
2 tasks
KentaHizume opened this issue Jul 12, 2024 · 54 comments · Fixed by #2137
Closed
2 tasks

Dressca管理アプリを作成する #1453

KentaHizume opened this issue Jul 12, 2024 · 54 comments · Fixed by #2137
Assignees
Labels
target: Dressca サンプルアプリケーションDresscaに関係がある
Milestone

Comments

@KentaHizume
Copy link
Contributor

KentaHizume commented Jul 12, 2024

概要

  • 楽観同時実行制御を行うサンプルを追加する #1237
    を実装するにあたり、楽観同時実行制御が適切な業務シナリオを検討した。
  • 楽観同時実行制御に適した業務シナリオを考える #1450
    しかし、現在のBtoC向けのシナリオを中心とするDresscaアプリでは、
    適切な業務シナリオがなさそうであった。
    このことを解決するために、BtoB向けのシナリオを実現するDressca管理アプリを作成する。
    具体的には、カタログアイテムの管理画面を作成し、CatalogItemsテーブルへのCRUD操作を行う機能を持ったアプリケーション一式を実装する。

詳細

この対応に伴い、npm workspace(フロント)とソリューションフィルター(バック)を導入する必要があるので、
それぞれ先行して別のissueで対応し、mainにマージする。

制約条件

  • 認証部分は組み込まないが、認可の部分だけは組み込んでおく。

完了条件

  • 既存のDresscaが正常に動作すること
  • CIが正常に動作すること

ブランチ管理

ソリューションフィルター追加後の状態をベースにして、
develop/admin 配下で下記の3本で管理中

  • バックエンドのみ(レビュー用)
  • フロントエンドのみ(レビュー用)
  • フロント/バックの合体用(開発時はここからブランチを切り、変更点をバック、フロントに必要に応じてチェリーピックする、またがる場合は注意)

関連タスクまとめ

Maia側の対応はこちら

先行タスク(マージ予定の順に記載、随時更新)

  1. npm workspacesの実装、ドキュメント
  1. ソリューションフィルター追加、ドキュメント
  1. npm ciに変更

着手中

MSWを使ったフロントエンドの結合テスト

入力フォームのバリデーションのガイド作成(ガイドに従った実装にする)

エラーのレスポンスの方針、ガイド作成(ガイドに従った実装にする)

409をFilterで処理する
⇒仮実装済、論点あり

未着手

フロント

  • アイテム一覧のページネーション
  • アイテム一覧のソート
  • コメント見直し

バック

  • コメント見直し

別issueで対応

  • フロントエンドのエラーメッセージのリソースファイル化および多言語対応
  • dependabotのnpmのパッケージをまとめる
  • WebとWebAdminを直列にビルドする
@KentaHizume KentaHizume added target: Dressca サンプルアプリケーションDresscaに関係がある サンプルAP labels Jul 12, 2024
@KentaHizume KentaHizume added this to the v0.10 milestone Jul 12, 2024
@KentaHizume KentaHizume self-assigned this Jul 12, 2024
@KentaHizume
Copy link
Contributor Author

フロントエンド詳細

image

@KentaHizume
Copy link
Contributor Author

バックエンド詳細

image

@KentaHizume
Copy link
Contributor Author

全体像

image

@KentaHizume KentaHizume changed the title Dressca管理画面を作成する Dressca管理アプリを作成する Jul 12, 2024
@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 16, 2024

共通パッケージの作成

  • 共通パッケージに関しては、Viteのライブラリモードを使うことができそう。

https://ja.vitejs.dev/guide/build.html#library-mode

  • 下記に従ったところ、参照できる状態になった。

https://leaysgur.github.io/posts/2023/10/09/215205/

  • 共通モジュール側を修正した場合もホットリロードしてくれる。

  • 共通モジュールのエントリーポイントがイマイチなので要修正
    index.ts

import LoadingSpinner from "./components/LoadingSpinner.vue";
import assetHelper from "./helpers/assetHelper";
import currencyHelper from "./helpers/currencyHelper";
export { LoadingSpinner, assetHelper, currencyHelper}

エントリーポイントを分割すると下記のエラーになってしまう。

error TS2307: Cannot find module '@dressca-frontend/common/helpers' or its corresponding type declarations. 

https://nodejs.org/api/packages.html#package-entry-points

型定義ファイルが必要なように見えるが、
下記のパターンだと型定義ファイルなしで動作する。

  • package.json
  "exports": {
    ".": "./src/index.ts"
  },
  • vite.config.ts
      entry: resolve(__dirname, 'src/index.ts'),

Vite公式ドキュメントの設定に寄せると型定義ファイルを要求するエラーになるので、型定義ファイルを作ったほうがよさそう。

推奨設定に寄せたが、UMD形式で出力する必要はなさそう。
UMD形式の使い方として、CDN経由でブラウザからライブラリを参照する場合に使うため。

https://ja.vitejs.dev/guide/build#library-mode
https://dackdive.hateblo.jp/entry/2019/09/23/100000#3-UMD
https://zenn.dev/hashrock/scraps/b995f3c8bd2778

またDresscaの使い方としては、CJS形式も不要なはずなので、
下記の設定もVite公式ドキュメントの設定から削ってよいはず。

  "main": "./dist/common-lib.umd.cjs",

  "exports": {
    ".": {
      "types": "./dist/types/src/index.d.ts",
      "import": "./dist/common-lib.js",
      "require": "./dist/common-lib.umd.cjs"
    }

@KentaHizume
Copy link
Contributor Author

モノレポ構成でワークスペースを追加する際の手順をドキュメント化したほうがよいかもしれない。

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 16, 2024

image
・/にホーム用のViewを作る
/に何も割り合てていないと警告が出て邪魔なため
image

・右上にドロップダウンメニューとリンクを作る
ここまではこのissueで対応する。

画面遷移とどこに更新、削除、追加の機能を持たせるかについて検討する。

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 16, 2024

バックエンド側のテストがないが、どこまで作る必要があるか検討する必要がある。
ApplicationCoreは触っておらず、Contoller周りの自動テストはあまり作っていないので、このissueの範囲ではヘルスチェックがあればよさそう?

結合テスト
IntegrationTestWebApplicationFactoryで複数のClientを出し分けられなさそうに見えるので、別のプロジェクトが必要。
⇒1つのアプリケーション内でのモジュールの結合を結合テストとカテゴライズするのは自然なので、問題ない。
管理アプリと顧客用アプリ間のテストは、システムテストにカテゴライズするべき。

単体テスト
Unittests配下にWeb.Adminフォルダを切る。

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 17, 2024

型定義ファイルを出力する方法を探す

vite-plugin-dts

vite v5.3.3 building for dev...
src/components/LoadingSpinner.vue:2:1 - error TS2347: Untyped function calls may not accept type arguments.

2 defineProps<{
  ~~~~~~~~~~~~~
3   show: boolean;
  ~~~~~~~~~~~~~~~~
4 }>();
  ~~~~
src/components/LoadingSpinner.vue:8:11 - error TS2339: Property 'show' does not exist on type '{}'.

8     v-if="show"
            ~~~~
src/helpers/assetHelper.ts:9:17 - error TS1343: The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', or 'nodenext'.

9       return `${import.meta.env.VITE_NO_ASSET_URL}`;
                  ~~~~~~~~~~~
src/helpers/assetHelper.ts:9:29 - error TS2339: Property 'env' does not exist on type 'ImportMeta'.

9       return `${import.meta.env.VITE_NO_ASSET_URL}`;
                              ~~~
src/helpers/assetHelper.ts:12:15 - error TS1343: The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', or 'nodenext'.

12     return `${import.meta.env.VITE_ASSET_URL}${assetCode}`;
                 ~~~~~~~~~~~
src/helpers/assetHelper.ts:12:27 - error TS2339: Property 'env' does not exist on type 'ImportMeta'.

12     return `${import.meta.env.VITE_ASSET_URL}${assetCode}`;
                             ~~~

✓ 5 modules transformed.
  • assetHelperはそれぞれのアプリに置いておいたほうがよさそう

tscを使う

外部プラグインを使わずに済むのでこちらで対応

https://blog.hey3.dev/posts/vite-lib-mode-to-publish-npm

ただし、先にcommonのtscの実行を挟まないと型定義ファイルがなくてCIが死んでしまう

https://github.com/AlesInfiny/maris/actions/runs/9969172624

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 17, 2024

CI用の依存関係の解決

上記の問題について対策する。

ただし、先にcommonのtscの実行を挟まないと型定義ファイルがなくてCIが死んでしまう
https://github.com/AlesInfiny/maris/actions/runs/9969172624

順序の制御

tscとビルド(Vite)でそれぞれ制御が必要

tsc

  • Project Referencesを使って tsconfig.jsonで制御する
    Referencesを読み込んでいい感じにしてくれる
$ npm run typecheck

> [email protected] typecheck 
> vue-tsc --build --force --verbose

[16:17:34] Projects in this build: 
    * apps/customer/tsconfig.node.json
    * apps/customer/tsconfig.app.json
    * apps/customer/tsconfig.vitest.json
    * apps/customer/tsconfig.json
    * apps/admin/tsconfig.node.json
    * packages/common/tsconfig.node.json
    * packages/common/tsconfig.app.json
    * packages/common/tsconfig.vitest.json
    * packages/common/tsconfig.json
    * apps/admin/tsconfig.app.json
    * apps/admin/tsconfig.vitest.json
    * apps/admin/tsconfig.json
    * tsconfig.json

ビルド

  • デフォルトの動作だと、ビルド後のJSを参照しに行くが、それがない場合はうまく解決できないようなので、vite.config.tsで、.tsに対してaliasを設定する必要がある。

https://zenn.dev/catnose99/articles/08cf9e475004b2

https://github.com/AlesInfiny/maris/actions/runs/9970328999

aliasを追加する場合、直接'../../packages/common/src/index.tsだとNGで、
fileURLToPathを使わないと実行時エラーになってしまう。

        '@dressca-frontend/common': fileURLToPath(
          new URL('../../packages/common/src/index.ts', import.meta.url),
        ),

@KentaHizume
Copy link
Contributor Author

要議論ポイント

  1. 共通パッケージをUMD形式で出力する必要はあるか
    • おそらくないが、Vite公式の設定に寄せるのであればこの形になる
    • ESMだけ出力するパターンの記事やサンプルが見つかっていない
  2. バックエンドのApplicationCoreは共有していて問題ないか
    • ドメイン駆動的には、CustomerAdminCatalogItemとAdminCatalogItemなど、コンテキストごとにEntityを分けて作っていくほうが正しいはず、らしい
      • これを実現する際にEntity FrameworkのDBとのマッピングがどういう感じになるのかあまりわかっていない。
    • ただ、クリーンアーキテクチャにしましょうということは言っているが、ドメイン駆動でやりましょうということは特に主張していないので、依存関係の向きとレイヤーの責務が妥当であれば問題ないのでは
  3. バックエンドのテストプロジェクトの切り方をどうするか
  4. モノレポ構成のドキュメント化のスコープ

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 19, 2024

このissueで対応する範囲に関しては、一旦出そろった。

https://github.com/AlesInfiny/maris/tree/develop/Dressca%E7%AE%A1%E7%90%86%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B

dependabotについては問題ないはずだが、マージ後に想定通り動作するか念のため確認する必要がある。

https://dev.classmethod.jp/articles/using-dependabot-version-updates-in-npm-workspaces/

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 22, 2024

共通パッケージをUMD形式で出力する必要はあるか

  • ESMのみの出力の場合、CJSからのインポートができなくなる。
  • 古いアプリをモノレポに移行してそこから共通パッケージを使いたい、のようなユースケースで動かない。
    ただViteを使う前提であれば、アプリ自体についてJS⇒ESMにコードベースを移行することになるはず。

Pure ESM package

Node.js パッケージ作り方 Pure ESM package と TypeScript 対応 令和最新版

@KentaHizume
Copy link
Contributor Author

別ポートでViteのプロキシサーバーを上げる場合はCypressも同じポートで上げるようにしておく必要がある。

package.json

    "test:e2e": "start-server-and-test dev http://localhost:6173/ 'cypress open --e2e --browser chrome'",
    "test:e2e:ci": "start-server-and-test dev http://localhost:6173/ 'cypress run'",

cypress.config.ts

export default defineConfig({
  e2e: {
    specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
    baseUrl: 'http://localhost:6173',
  },
});

@KentaHizume
Copy link
Contributor Author

スレッドが長くなってきたためMSW関連は

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 23, 2024

Entity FrameworkのDBとのマッピング

現在Entity Framework上のEntityと、DDD上のEntityが偶然1:1対応しているが、1:1対応する必然性はない。
DDD上のEntityが存在していて、EntityFramework上のEntityが存在しないこともある(Accountはそうなっている。)
DBでの永続化の実装がどのようになっていても関係ない(むしろ依存してはならない)。

実装としては、

  • ApplicationCoreのリポジトリでは、DDD上のEntity単位でリポジトリのインターフェースを定義する
  • Repositoryの実装中で、ApplicationCoreのEntityと、EntityFrameworkの"Entity"とのマッピングを行う
    ことで、DBの実装を隠蔽したまま、ApplicatinCoreをドメインごとに切り出すことができる。

具体的には、
CatalogItemsテーブル(Entity Framework上のEntity) 100カラム

  • AdminCatalogItems(Entity) 100カラム抽出
  • CustomerCatalogItems(Entity) 10カラム抽出
    のようなことが想定される。

CatalogItemsの場合は、
Customerから更新することがないので、DBContextが分かれることになりそう。

Maia(MyBatis)の場合

Maiaの場合は既にリポジトリでORマッパー上のEntityとDDD上のEntityを詰め替えるようになっている。
名称はtranslator。

https://github.com/AlesInfiny/maia/blob/main/samples/web-csr/dressca-backend/infrastructure/src/main/java/com/dressca/infrastructure/repository/mybatis/translator/EntityTranslator.java

https://maia.alesinfiny.org/app-architecture/client-side-rendering/backend-application/infrastructure/#repository-implementation
アプリケーションコア層で定義するリポジトリ ( インターフェース ) の入出力は、ドメインモデルを中心とするデータ型で定義します。 インフラストラクチャ層でのデータベースとの API は、テーブル構造を元にしたテーブルエンティティで行うため、リポジトリでドメインモデルとの変換処理が必要です。

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 23, 2024

画面デザイン

デザイン

tailwind.configを設定するのが正しいらしい。
https://zenn.dev/gunnar/articles/e3e3da3297cb6b

レイアウト

業務アプリ向け画面なのでtoCアプリと被らないように。

機能

  • カタログアイテム一覧の表示、検索
  • CRUD操作
    が必要

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 24, 2024

コンテキストごとにエンティティとアプリケーションサービスを分ける

  • 現状のCatalogItemについて、AdminCatalogItemとCustomerCatalogItemの2つのコンテキストに分割することを想定し、その場合に考えられる構成を洗い出す。
  • Customer/Adminという軸と、Catalog/Ordering/…という2軸の掛け算状態になっているのがややこしくなっている原因に見える。

現状

  • Maris
    image

  • Maia
    image

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 25, 2024

権限管理を行うレイヤーについて

権限がドメイン知識かどうかに議論の余地があり、諸説ある。

具体的には、下記の2パターンが考えられる。
(1)権限をドメインの知識と考える場合
⇒Entityに実装する
(2)権限をアプリケーション固有の知識と考える場合
⇒ApplicationServiceに実装する

little-hands/ddd-q-and-a#121
https://zenn.dev/loglass/articles/76e559f1a13776#ddd%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E8%AA%8D%E5%8F%AF

ただ、出典のリンク先が死んでいるが、
clean architecture著者のRobert Martinは、
権限はアプリケーション固有の知識で、アプリケーションサービスで権限を確認するという考え方をしているそう。

https://kenfdev.hateblo.jp/entry/2020/01/13/115032

セキュリティはアプリケーション固有の問題であり、インタラクターに属する。コントローラは現在のユーザーの認証情報にアクセスし、その情報をインタラクターに渡す。インタラクターは、認証サービスを使用して、特定のインタラクションが認証されていることを確認します。ビジネスオブジェクトはそれについて何も知らない。(DeepL翻訳)

.NETで実装する場合、DIコンテナ経由で、ASP.NET Identityから提供されているインターフェースUserManager<IdentityUser>をApplicationServiceに引き渡してユーザ情報を取得することが想定されるため、
Entity側にさらに引き渡す処理を書くよりもApplicationService内で完結させたほうがコードベースの見通しがよくなる。

ASP.NET Core でのロール ベースの認可

ASP.NET Identityを使用したチュートリアルでは、
ControllerまたはAction単位での認可になるように見える。

[Authorize(Roles = "Administrator, PowerUser")]
public class ControlAllPanelController : Controller
{
    public IActionResult SetTime() =>
        Content("Administrator || PowerUser");

    [Authorize(Roles = "Administrator")]
    public IActionResult ShutDown() =>
        Content("Administrator only");
}

Spring Securityの場合

https://spring.pleiades.io/spring-security/reference/servlet/authorization/architecture.html
https://qiita.com/y-yamagata/items/7232b9969fcb7ce85b54
https://www.kimullaa.com/posts/201605080505/

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 25, 2024

ドメイン

ドメイン サブドメイン アクター 業務概要
カタログ カタログを閲覧する 管理者、担当者 カタログアイテムの一覧を表示する。
カタログ カタログを管理する 管理者 カタログアイテムを、登録・変更・削除する。

ユースケース

ユースケース図

image

ユースケース記述

カタログアイテムの一覧を表示する

アクター

管理者、担当者

開始条件

カタログアイテム一覧画面に遷移

事前条件

ユーザーが認証されていること

メインフロー

  1. システムはカタログアイテムの閲覧権限があることを確認する。
  2. システムはカタログアイテムを取得する。
  3. システムはカタログアイテムを一覧表示する。

代替フロー

  • No.1 カタログアイテムの閲覧権限がない場合
    システムはログインユーザーに権限がないことを通知する。

例外フロー

事後条件

カタログアイテムを更新する

アクター

管理者

開始条件

カタログアイテム編集画面の更新ボタンを押下

事前条件

ユーザーが認証されていること

メインフロー

  1. 管理者は更新ボタンを押下する。
  2. システムは項目のバリデーションを行う。
  3. システムはカタログアイテムの更新権限があることを確認する。
  4. システムはカタログアイテムのデータを更新する。
  5. システムは更新の成功を通知する。
  6. システムはカタログアイテム編集画面を更新する。

代替フロー

  • No.2 項目が妥当でない場合
    システムはカタログアイテム編集画面に管理者に該当の項目と理由を表示する。
  • No.3 ログインユーザーにカタログ管理権限がない場合
    システムは管理者に権限がないことを通知する。

例外フロー

  • No.4 カタログアイテムの更新に失敗した場合
    システムは更新の失敗を通知する。

事後条件

カタログアイテムを削除する

アクター

管理者

開始条件

カタログアイテム編集画面の削除ボタンを押下

事前条件

ユーザーが認証されていること

メインフロー

  1. 管理者は削除ボタンを押下する。
  2. システムは削除要否を再確認する。
  3. 管理者はOKボタンを押下する。
  4. システムはカタログアイテムの削除権限があることを確認する。
  5. システムはカタログアイテムのデータを削除する。
  6. システムは削除の成功を通知する。
  7. システムはカタログ一覧画面に遷移する。

代替フロー

  • No.3 管理者がキャンセルボタンを押下した場合
    システムはカタログアイテム編集画面を表示する。
  • No.4 ログインユーザーにカタログ管理権限がない場合
    システムは管理者に権限がないことを通知する。

例外フロー

  • No.6 カタログアイテムの削除に失敗した場合
    システムは削除の失敗を通知する。

事後条件

カタログアイテムを追加する

アクター

管理者

開始条件

カタログアイテム追加画面の追加ボタンを押下

事前条件

ユーザーが認証されていること

メインフロー

  1. 管理者は追加ボタンを押下する。
  2. システムは追加要否を再確認する。
  3. 管理者はOKボタンを押下する。
  4. システムはカタログアイテムの追加権限があることを確認する。
  5. システムはカタログアイテムのデータを追加する。
  6. システムは追加の成功を通知する。
  7. システムはカタログ一覧画面に遷移する。

代替フロー

  • No.3 管理者がキャンセルボタンを押下した場合
    システムはカタログアイテム追加画面を表示する。
  • No.4 ログインユーザーにカタログ管理権限がない場合
    システムは管理者に権限がないことを通知する。

例外フロー

  • No.5 カタログアイテムの追加に失敗した場合
    システムは削除の失敗を通知する。

事後条件

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 25, 2024

画面

URL

No. 名称 概要 URL
1 トップ画面 管理機能の一覧を表示します。 ~/
2 カタログ管理画面 カタログの管理機能の一覧を表示します。 ~/catalog/
3 カタログアイテム一覧画面 カタログアイテムの一覧を表示します。 ~/catalog/items
4 カタログアイテム編集画面 カタログアイテムの情報を閲覧します。カタログアイテムを更新・削除できます。 ~/catalog/items/edit/:id
5 カタログアイテム追加画面 カタログアイテムの情報を入力し、カタログアイテムを追加できます。 ~/catalog/items/add
6 ログイン画面 認証を行います。 ~/login

画面遷移

image

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 25, 2024

画面モック

ログイン画面

項目名 UI バリデーション 備考
ユーザーID 入力フォーム 必須
パスワード 入力フォーム 必須

image

トップ画面

下記の画面へのリンクを表示。

  • カタログ管理画面

カタログ管理画面

下記の2画面へのリンクを表示。

  • カタログアイテム一覧画面
  • カタログアイテム追加画面
    image

カタログアイテム一覧画面

項目名 フォーマット
操作 ボタン
アイテムID 数値
画像 画像
アイテム名 テキスト
説明 テキスト
単価 数値、カンマ区切り
商品コード テキスト
カテゴリ テキスト
ブランド テキスト
最終更新日時 時刻

image

カタログアイテム編集画面

項目名 UI 更新可否 バリデーション 備考
アイテムID テキストフィールド ×
アイテム名 入力フォーム 256文字以下 DBはnvarchar(512)
説明 入力フォーム 1024文字以下 DBはnvarchar(MAX)
単価 入力フォーム 整数18桁以下 DBはdecimal(18, 6)
商品コード コンボボックス 英数字128文字以下 商品ドメインについては今回実装しない
カテゴリ ドロップダウンリスト カタログカテゴリ集約から取得
ブランド ドロップダウンリスト カタログブランド集約から取得
画像 - × × 画像更新については今回実装しない
最終更新日時 テキストフィールド × 表示専用

image

カタログアイテム追加画面

項目名 UI バリデーション 備考
アイテム名 入力フォーム 256文字以下
説明 入力フォーム 1024文字以下
単価 入力フォーム 整数18桁以下
商品コード コンボボックス 英数字128文字以下 商品ドメインについては今回実装しない
カテゴリ ドロップダウンリスト カタログカテゴリ集約から取得
ブランド ドロップダウンリスト カタログカテゴリ集約から取得

image

ナビゲーションバー

toCのDresscaが横なのでわかりやすいように管理アプリは縦にする

  • トップへのリンク
  • メニュー
  • ユーザーのロール
  • ユーザー名
    image

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 26, 2024

認可

  • 認証(ログイン中のユーザーが誰であるか)は済んでいることが前提
  • TODO:どこに配置するか、組み込んだ際のシーケンス図

権限設計

image

ER

erDiagram
    User ||--|{ UserRole : "" 
    UserRole }|--|| Role : ""
    Role ||--|{ RolePermission : ""
    RolePermission }|--|| Permission : ""

User{
    bigint Id PK
}

UserRole{
    bigint Id PK
    bigint UserId FK
    bigint RoleId FK
}

Role{
    binint Id PK
    varchar Name
}

RolePermission{
    bigint Id PK
    bigint RoleId FK
    bigint PermissionId FK
}

Permission{
    bigint Id PK
    bigint Name
}
Loading

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 26, 2024

シーケンス図

  • カタログアイテム更新ユースケース
sequenceDiagram
    participant Screen as カタログアイテム編集画面
    
    participant Controller as CatalogController
    
    participant CatalogManageAppService as CatalogManagementeApplicationService

    participant CatalogItem as CatalogItem

    participant CatalogRepo as CatalogRepository

    Screen ->> Controller:更新ボタンを押下
    activate Controller
    Note right of Screen: PutCatalogItemsRequest

    Controller ->> CatalogManageAppService: カタログアイテムを更新
    Note right of Controller: カタログアイテムID<br/>アイテム名<br/>説明<br/>単価<br/>商品コード<br/>カテゴリID<br/>ブランドID
    activate CatalogManageAppService
    CatalogManageAppService ->> CatalogRepo:エンティティを取得
    Note right of CatalogManageAppService: カタログアイテムID
    activate CatalogRepo
    CatalogRepo -->> CatalogRepo: DBアクセス(SELECT)
    CatalogRepo -->> CatalogManageAppService: エンティティを返却
    Note left of CatalogRepo: 更新前カタログアイテムエンティティ
    activate CatalogManageAppService
    CatalogManageAppService ->> CatalogItem: エンティティを更新
    Note right of CatalogManageAppService: 更新前カタログアイテムエンティティ<br/>アイテム名<br/>説明<br/>単価<br/>商品コード<br/>カテゴリID<br/>ブランドID
    activate CatalogItem
    CatalogItem -->> CatalogItem: エンティティを更新
    activate CatalogItem
    CatalogItem -->> CatalogManageAppService: エンティティを返却
    Note right of CatalogManageAppService: 更新後カタログアイテムエンティティ
    activate CatalogManageAppService
    CatalogManageAppService ->> CatalogRepo: カタログアイテムテーブルに永続化
    Note right of CatalogManageAppService: 更新後カタログアイテムエンティティ
    activate CatalogRepo
    CatalogRepo -->> CatalogRepo: DBアクセス(UPDATE)
    activate CatalogRepo
    CatalogRepo -->> CatalogManageAppService: なし
    activate CatalogManageAppService
    CatalogManageAppService -->> Controller: なし
    activate Controller
    Controller -->> Screen: HTTP 204 No Content
Loading

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Jul 30, 2024

決めるべきこと

  1. 認可のインターフェースのアーキテクチャ

  2. 使用する認可モデル

https://www.osohq.com/academy/what-is-authorization

認可モデルの定式化

  • アクター
    管理者、担当者
  • アクション
    閲覧、更新、削除
  • リソース
    カタログ、注文

認可のインターフェース

  • 判断(decision)
    認可インターフェースを実装する方法、
    アクター、アクション、リソースに基づき結果を返却する。
    例:管理者(アクター)はカタログ(リソース)の更新(アクション)が許可(判断)されている

  • 適用(enforcement)
    判断に応じて何をするかを決定する方法
    例:担当者(アクター)はカタログ(リソース)の更新(アクション)が許可されていない(判断)ので、403 Forbiddenをレスポンス(適用)する。

認可の判断の実装の選択肢

認可の判断をするためには2つの情報が必要

  • 認可データ
    例:アリスは管理者である

  • 認可ロジック
    例:管理者は注文の更新が許可されている

  1. 集中モデル(centralized)
    認可データにアクセス可能なcentral authorityが、認可ロジックをインプットとして認可を判断する。

  2. 分散モデル(decentralized)
    既にアクセス可能な認可データを用いて、アプリケーションが認可を判断する。

  3. hybrid
    1と2のハイブリッド。decentralizedアプローチをとるが、複数のサービス、アプリケーションに適用される。

分散モデルの場合
業務サービスごとに、認可データと認可ロジックを持つ。
最もシンプルで、決定に必要なデータの取得のしやすさという点で優れている
業務サービスの数が増えた場合に、コードが重複する。

ApplicationCoreにAuthorizationApplicationServiceを作成する
    ApplicationServiceから別のApplicationServiceを呼ぶのはまずい?

集中モデルの場合
業務サービスとは異なる認可サービスを立ち上げる。
業務サービスは認可データを持ち、認可サービスは認可ロジックを持つ。
認可サービスは業務サービスから認可データを受け取って、認可を判断する。
業務サービスと認可サービスに依存関係ができてしまう。
業務サービスから認可データをもらわないと認可を判断できない。

管理アプリとは別のAPIサービスを上げて、APIコールする

推奨される設定

a. IDプロバイダーを使用して認証する。
b. アプリケーションに認可を適用させる。
c. 認可インターフェースを追加して、認可ロジックと業務ロジックを分離する。
d. 認可データは、アプリケーション内に持つ。

認可モデル

https://www.keepersecurity.com/blog/ja/2024/03/19/the-different-types-of-authorization-models/

単純なモデル~複雑なモデル
Role-Based Access Control (RBAC)
ロールベースで考える
管理者なので、注文を編集してよい。

Relationship-Based Access Control (ReBAC)
ロールと、リソースとの関係性から考える
管理者であり、かつ注文の作成者なので、編集してよい。

Attribute-Based Access Control (ABAC)
ロールと、リソースとの関係性と、属性から考える
ロールは属性の一種
ロールはリレーションシップの形式
ReBACはABACのすべてをカバーするわけではないが、ユースケースの多くをカバーしている。

選択肢としては、
・RBAC
・ReBAC
の2択

@KentaHizume
Copy link
Contributor Author

認可のインターフェースのアーキテクチャ案

1. 既存のApplicationCoreに入れる

分散モデル

Dressca
├ Dressca.ApplicationCore
│ ├ ApplicationService
│ ├    ├ AdminAuthorizationApplicationService
│ ├ Authorization
│ ├    ├ User
│ ├    ├ Role
│ ├    ├ Permission
├ Dressca.Web
├ Dressca.Web.Admin

2. 新しくAuthorizationパッケージを切る

集中モデル

Dressca
├ Dressca.ApplicationCore
├ Dressca.Authorization
│ ├ ApplicationService
│ ├    ├ AdminAuthorizationApplicationService
│ ├ Authorization
│ ├    ├ User
│ ├    ├ Role
│ ├    ├ Permission
├ Dressca.Web
├ Dressca.Web.Admin

3. 別のサービスを上げる

集中モデル

Dressca
├ Dressca.ApplicationCore
├ Dressca.Authorization
│ ├ ApplicationService
│ ├    ├ AdminAuthorizationApplicationService
│ ├ Authorization
│ ├    ├ User
│ ├    ├ Role
│ ├    ├ Permission
├ Dressca.Web
├ Dressca.Web.Admin
├ Dressca.Web.Authorization

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Aug 9, 2024

別issueだが、パイプライン等での実行時は、npm install よりも npm ci (Clean Install)のほうがよい。
(npm installはpackage.jsonベースでinstallするが、package.jsonはハットやチルダでバージョン範囲が指定できるので、バージョンがブレることがある。)

package-lock.jsonはルートのpackage.jsonと同じフォルダに作られ、
各ワークスペースには作られないので、
.NETのプロジェクト側のSpaRootとSpaProxyLaunchCommandの設定も合わせる必要がある。

    <SpaRoot>..\..\..\dressca-frontend</SpaRoot>
    <SpaProxyLaunchCommand>npm run dev:customer</SpaProxyLaunchCommand>
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />

    <SpaRoot>..\..\..\dressca-frontend</SpaRoot>
    <SpaProxyLaunchCommand>npm run dev:admin</SpaProxyLaunchCommand>
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Aug 16, 2024

ログアウトボタンの実装

App.vueからログアウト処理を呼ぼうとするとエラーで落ちてしまう。

import { logoutAsync } from './services/authentication/authentication-service';
            <div class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="logout" @click="logoutAsync">ログアウト</div>

Error: [🍍]: "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"?

authentication-serviceを下記のように修正すると解決する。
あまり直感的ではない…。

https://pinia.vuejs.org/core-concepts/outside-component-usage.html

authentication-service.ts

NG

const authenticationStore = useAuthenticationStore();

export async function logoutAsync() {
  authenticationStore.signOutAsync();
}

OK

  • function内でuseStoreを宣言
export async function logoutAsync() {
  const authenticationStore = useAuthenticationStore();
  authenticationStore.signOutAsync();
}

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Aug 20, 2024

UnauthorizedErrorがうまくハンドリングできない

エラーハンドラーの下記の箇所で、
[Vue warn]: inject() can only be used inside setup() or functional components.
のエラーが出て期待通り動作しない。

routerがコンポーネントの外から使われているのがまずいらしいが、よい解決策が見つからない。

    if (error instanceof UnauthorizedError) {
      if (handlingUnauthorizedError) {
        handlingUnauthorizedError();
      } else {
        const router = useRouter();
        const routingStore = useRoutingStore();
        routingStore.setRedirectFrom(router.currentRoute.value.path.slice(1));
        router.push({ name: 'authentication/login' });
        showToast('ログインしてください。');
      }

exportしてあるrouterを持ってくることで解決

import { router } from '@/router';

⇒これをやると循環参照になってしまう。

npm ERR! Lifecycle script `lint` failed with error:
  13:1  error  Dependency cycle via @/router:1=>@/router/catalog/catalog:2  import/no-cycle

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Aug 21, 2024

vue-router周りの問題点

1. エラーハンドラーからrouterをうまく操作できない

既存のエラーハンドラーでも発生する。

2. 初回のログイン時、うまくルートに遷移しない

/authentication/login から / に遷移してほしいが、ログイン画面から遷移しない。
もう一度ログインボタンを押下すると遷移する。

  • ログを仕込むとpushされているように見える。
  • isAuthenticated
  console.log("リダイレクトパス")
  console.log(routingStore.redirectFrom);
  router.push({ path: routingStore.redirectFrom });
  console.log("pushed");

image

  • 下記を仕込むとなぜかfalseになり、2回目のログイン時はtrueになっている
    storeの更新よりも先にrouter.pushが動いた結果、ナビゲーションガードが再度弾いている?
  console.log(authenticationStore.isAuthenticated)
  router.push({ path: routingStore.redirectFrom });
  routingStore.deleteRedirectFrom();

authentication-serviceの中でawaitしていなかったのが原因。

export async function loginAsync() {
  const authenticationStore = useAuthenticationStore();
  authenticationStore.signInAsync();
}

@KentaHizume
Copy link
Contributor Author

B to CのCの意味として、CustomerよりもConsumerのほうが一般的なため、
Consumerに改名する。

https://dictionary.cambridge.org/ja/dictionary/english/business-to-consumer
(business-to-consumerはヒットするが、buisiness-to-customerはヒットしない)

@KentaHizume
Copy link
Contributor Author

yupのメッセージをコード上にべた書きしているので、多言語対応とは別に要修正

ファイル ./src/config/yup.config.ts を作成し、以下のように記述します。

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Aug 27, 2024

ロールの認可NGの場合のステータスコードを403から変更したい

[Authorize(Roles = "Admin")]をつけているメソッドをコールして認可NGの場合、
デフォルトだと403 Forbiddenが返却されてしまう。

image

下記の実装でかわりに404を返すようにカスタマイズできる?

https://learn.microsoft.com/ja-jp/aspnet/core/security/authorization/customizingauthorizationmiddlewareresponse?view=aspnetcore-8.0

カスタマイズできた、より詳しい回答もあった。

https://learn.microsoft.com/en-us/answers/questions/1187461/not-able-to-implement-iauthorizationmiddlewareresu

@tsuna-can-se
Copy link
Contributor

ここでHTTPステータスコードの変換処理を行うためのクラスを呼び出してます。
https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs#L194

具体実装は以下のクラスしかなく、特に設定とかで書き換えることもできなさそうでした。
https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authorization/Policy/src/AuthorizationMiddlewareResultHandler.cs#L40

なのでMSページの案内方法以外の手段はなさそうですね。

@KentaHizume
Copy link
Contributor Author

@tsuna-can-se
ありがとうございます、先ほどMSページの案内に従った実装で動作することを確認できました。

@KentaHizume
Copy link
Contributor Author

①フロントエンド用ブランチでpackage.jsonとpackage-lock.jsonを最新化(※このときnpm installしている)
②合体用ブランチにマージし、package-lock.jsonが競合するのでフロントエンド用ブランチ側を取得
③合体用ブランチでWeb.Adminプロジェクトをデバッグ実行(npm installする)
するとpackage-lock.jsonに謎の差分が出現し始める現象が起きているが、原因不明…。

④合体用ブランチに③で変更されたpackage-lock.jsonをコミット
し、
まっさらな状態に合体用ブランチをクローンしてデバッグ実行すると特に問題なく動作する。

最後にmainと合流するタイミングで解決すればよいとは思われるので、現象のみ記載。

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Sep 12, 2024

アイテムの論理削除

カタログアイテムの削除機能について、マスタ系のテーブルであるため、CatalogItemsテーブルのレコードは物理削除するのではなく、論理削除とするべき。
そのために、CatalogItemsテーブルに有効かどうか判定するカラムを追加する必要がある。

他のAPIに関しても論理削除されたアイテムをAPI経由で取得や更新できないようにするべき。

あわせてConsumer側も改修する必要がある。
具体的にはカタログアイテムの一覧を取得する際には有効なアイテムだけGETするべき。

公開フラグのようなもののほうがよい?
あるいは公開フラグと論理削除フラグの両方を持つほうがよいか。

シナリオ

  1. (Consumer)買い物かごにアイテム1を入れる
  2. (Admin)アイテム1を論理削除
  3. (Consumer)注文
    というケースが発生した場合に、どうするかを決める必要がある。

現状の注文(CheckoutAsync)フロー

  1. 買い物かごに入っているアイテムのIDのリストを持ってくる
  2. IDをキーにアイテムの情報を持ってくる
  3. アイテムの情報から注文の情報を作成
  4. 注文のレコードを永続化
  5. 買い物かごを空にする

Case1:注文できる

買い物かごに入れた時点で有効なアイテムであれば、注文できてしかるべきと考える

Case2:注文できない

買い物かごに入れて1時間後に注文したら、販売が終わっていて買えなかった、というケースは不自然でない。
その場合はIDをキーにアイテムの情報を持ってくる際に、その時点で有効なアイテムかどうかを確認し、
持ってこれなかった場合は、エラーを投げて注文処理を中止し、無効なアイテムを買い物かごから削除して買い物かご画面に戻る。

考慮ポイント

  • 有効なアイテムと無効なアイテムが混在する場合は有効なアイテムだけ買えたほうがよいという考え方もある?
  • ECサイトで何かが在庫切れで買えなかった場合はどれが在庫切れで買えなかったか表示してくれる気がする

アイテムの状態

細かく考えると下記の状態を持ちうる?

  • 論理削除フラグ(有効フラグ)(削除されたら、ConsumerもAdminも見れない、エンドユーザーから見ると物理削除と同じ)
  • 公開/非公開フラグ(Consumerには非公開、Adminには見える、Adminで公開/非公開に更新できる)
  • 販売中/売り切れフラグ(ConsumerにもAdminにも見える)

ただ、フラグ間の整合性を保つ必要がある
有効、公開、販売中 ⇒一般的なアイテム
有効、公開、売り切れ ⇒売り切れ表示されているアイテム
有効、非公開、販売中 ⇒変?
有効、非公開、売り切れ ⇒売り切れたが、在庫補充の予定があるので管理上残しているアイテム
無効、公開、販売中 ⇒無効のレコードは無効にした時点の情報を持つと考えるとすべて自然か
無効、公開、売り切れ
無効、非公開、販売中
無効、非公開、売り切れ

そうなると販売中⇒(有効∧公開)は満たすべきか

要確認

CatalogItemsテーブルに対してGetしている処理について、
探しに行ったIDのアイテムがないと何かしらエラーを吐くはずなので、
その箇所については対処しておく
(物理削除でも論理削除でも手当てが必要なはず)

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Sep 13, 2024

アイテムが削除されていた場合の挙動調査、検討

CatalogRepository.FindAsyncを呼び出すユースケースは下記の5つ。

CatalogDomainServiceに指定したアイテムIDがすべて存在するかチェックする
ExistsAllAsyncメソッドが実装済みで、買い物かごへの追加時と数量更新時は既にチェックが行われている。
(存在しない場合は業務例外CatalogItemNotExistingInRepositoryExceptionをスロー)

(1)カタログ情報取得

CatalogApplicationService.GetCatalogItemsAsync
⇒削除されているものは持ってこれない(表示されない)で、
特にエラーや通知なく続行は不自然ではないので、手当て不要

(2)買い物かご情報取得

BasketItemsController.GetBasketItemsAsync

ShopingApplicationService.GetBasketItemsAsync

CatalogRepository.FindAsync

(3)注文(チェックアウト)

OrdersController.CheckoutAsync

ShopingApplicationService.CheckoutAsync

CatalogRepository.FindAsync

(4)買い物かごに追加

ShopingApplicationService.AddItemToBasketAsync

CatalogDomainService.ExistsAllAsync

CatalogRepository.FindAsync
⇒手当済みだが、フロントには400 Bad Requestしか飛んでいないはず

(5)数量更新

ShoppingApplicationService.SetBasketItemsQuantitiesAsync

CatalogDomainService.ExistsAllAsync

CatalogRepository.FindAsync
⇒手当済みだが、フロントには400 Bad Requestしか飛んでいないはず

(6)買い物かごから削除

見落とし、BadRequestになってしまう

Tobe整理

期待値

ユースケース バック フロント
カタログ情報取得 存在しないアイテムは取得しない 何もしない
買い物かご情報取得 業務例外をスロー、存在しないアイテムをかごから削除 取得失敗を通知
注文 業務例外をスロー、存在しないアイテムをかごから削除 注文失敗を通知
買い物かごにアイテムを追加 業務例外をスロー 追加失敗を通知
数量を更新 業務例外をスロー、存在しないアイテムをかごから削除 更新失敗を通知

現状

ユースケース バック フロント
カタログ情報取得 存在しないアイテムは取得しない 何もしない
買い物かご情報取得
注文 システムエラー、買い物かごには入ったまま 500エラー、サーバーエラー通知、エラーページへ遷移
買い物かごにアイテムを追加 業務例外をスロー 追加失敗を通知
数量を更新 業務例外スロー、買い物かごには入ったまま 400エラー、更新失敗を通知

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Sep 25, 2024

下記のissueについて、Adminにも反映する必要がある

@kenjiyoshid-a
Copy link
Contributor

kenjiyoshid-a commented Oct 9, 2024

更新処理で、適合するカタログブランドとカタログカテゴリが見つからなかった場合の例外について

コントローラ側で、CatalogBrandNotExistingInRepositoryExceptionおよびCatalogCategoryNotExistingInRepositoryExceptionをキャッチして400(BadRequest)を返す処理を追記する。
併せて、OpenAPIの該当箇所にも修正を行う。

@KentaHizume
CatalogBrandNotExistingInRepositoryExceptionおよびCatalogCategoryNotExistingInRepositoryExceptionについてはユーザーの入力による例外ではなく、システム例外であるため500で問題ないようです。失礼いたしました。

@kenjiyoshid-a
Copy link
Contributor

kenjiyoshid-a commented Oct 15, 2024

web-adminのCatalogItemsControllerで発生が想定される401について

以下のメソッドにて、PermissionDeniedExceptionがcatchされた際に、Unauthorized(401の画面)を返すようになっています。

  • PostCatalogItemAsync
  • DeleteCatalogItemAsync
  • PutCatalogItemAsync
    ここはアクセス許可が下りなかったことによって生じる例外なので403、もしくはセキュリティリスクを考慮して404にした方がいいですか?

401が返ってきた際に、フロントエンド側でログイン画面にリダイレクトされる仕様になっているため、401で問題なし。

@KentaHizume
Copy link
Contributor Author

@kenjiyoshid-a
こちらの件、本日お話させていただいた通り、
不認可を意味する業務エラー検出時は、
未認証を意味する 401 Unauthorizedではなく、
404 Not Found(意味的には403 Forbiddenのほうが正しいが、クライアントに認可の構造が漏れること防ぐための慣例に従う)
を返すようにMaris側を修正します。

        catch (PermissionDeniedException ex)
        {
            this.logger.LogWarning(Events.PermissionDenied, ex, ex.Message);
            return Unauthorized();
        }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
target: Dressca サンプルアプリケーションDresscaに関係がある
Projects
None yet
3 participants