ハコヅミを作る #29:管理者の権限と「凍結」、登録数の上限を作る

#27で、Google アカウントでログインできるようになりました。一般に公開すると、自分以外の人が登録できるようになります。

その前に要るのが、迷惑な使い方に気づいて、止める手段です。

  • 管理者の権限と、管理画面の最小限(#138)
  • 会員区分と、登録数の上限(#139)

管理者に見えるのは、件数まで

管理画面に出すのは、ユーザーの一覧です。メールアドレス、名前、登録した日、最後に使った日、そして件数(ギア・消耗品・コンテナ・プラン・キャンプ・画像)を出します。

管理画面。ユーザーが1人1枚のカードで並んでいる。それぞれに、メールアドレス、「フリー」の札、名前、登録した日、最後に使った日、ギア・消耗品・コンテナ・プラン・キャンプ・画像の件数が出ている。いちばん上のユーザーには「凍結中」の赤い札と「凍結を解く」のボタン、次の2人には「凍結する」のボタンがある。いちばん下は自分で、「管理者」の札と「(自分)」が付き、ボタンはない

管理画面(データは架空)。出すのは件数まで。自分自身と管理者には、「凍結する」のボタンを出さない

先に決めたのは、管理者にも、ほかの人のデータの中身は見せないことです。ギアの名前、キャンプ場、メモ、画像は、管理画面に出しません。サーバーの問い合わせも、件数を数えるだけで、中身は取っていません。

迷惑な使い方(画像を大量に置く、など)は、件数を見れば気づけます。中身まで見られる画面は、作らなければ、漏れることもありません。

管理画面は、ないものとして扱う

管理者でない人が /admin を開くと、404 を返します。「権限がありません」ではなく、「そのページはありません」です。管理画面があること自体を、見せないためです。

最初の管理者は、D1 のコンソールで決める

権限は、users のテーブルに role(user/admin)の列を足して持ちます。では、最初の管理者は、どう決めるのでしょうか。

画面から管理者にできる機能を作ると、その機能を守る仕組みも要ります。管理者は自分1人なので、作らないことにしました。Cloudflare のダッシュボードの D1 のコンソールで、SQL を1行流します。

UPDATE users SET role = 'admin' WHERE email = '<自分のメールアドレス>';

Better Auth の「管理者」プラグインは、使わない

Better Auth には、管理者向けの機能をまとめたプラグインがあります。権限、利用の停止、なりすまし(ほかの人としてログインする)などです。

要るのは、権限と停止だけです。なりすましは、「ほかの人のデータを見せない」方針と合いません。プラグインは使わず、自分で列を2つ足すことにしました。動きは、hooks.server.ts を読めば分かる形にしています。


「停止」ではなく「凍結」と呼ぶ

管理者がアカウントを止める機能を、最初は「停止」と書いていました。Issue の下書きを読んでいて、引っかかりました。

#27で、有料会員が冬に休む「休止」を考えています。休止しても、登録したデータは見られるようにしたいところです。見られなくなると、フリー会員より機能が落ちてしまいます。では、「停止」は、どういう扱いなのでしょうか。

整理すると、2つは別のものでした。

誰が できること
休止 有料会員が、自分で選ぶ 見る・編集するはできる(フリーと同じ扱い)
凍結 管理者が、迷惑な使い方を止める 見ることも含めて、全部できない

言葉が似ていると、あとで混ざります。管理者が止めるほうは「凍結」と呼ぶことにして、Issue にも CLAUDE.md にも、そう書きました。

凍結された人は、どこにも届かない

凍結された人に出る画面。ヘッダーにはロゴとアプリ名、右上にログアウトのボタンだけがあり、メニューはない。見出し「このアカウントは凍結されています」の下のカードに「このアカウントは、いま使えません。」とログアウトのボタンがある

凍結された人は、どの画面を開いても、ここに移る。できるのは、ログアウトだけ

凍結された人は、ログインはできます。ただし、どの画面を開いても、「このアカウントは凍結されています」の画面に移ります。データは消しません。凍結を解けば、もとどおり使えます。

止める場所は、ここでも hooks.server.ts の1か所です。画面だけでなく、画面のデータ、フォームの送信、画像の配信も、すべてここを通ります。ページごとに「凍結されていないか」を書かなくて済みます。

本人が、自分の権限を変えられないこと

気をつけたのは、本人が自分の role を書き換えられないことです。Better Auth には、名前などを本人が変える処理(update-user)があります。ここに role: 'admin' を付けて送られたら、どうなるでしょうか。

Better Auth には、「この列は、本人からは受け付けない」と伝える設定があります。

user: {
	additionalFields: {
		role: { type: 'string', required: true, defaultValue: 'user', input: false },
		suspendedAt: { type: 'date', required: false, input: false }
	}
}

設定しただけで安心せず、E2E で、実際に送って確かめています。role や凍結の日時を付けて送っても、DB の値が変わらないことを見ました。


登録数に、上限を付ける

次は、上限です。課金はまだ先ですが、上限の仕組みは、公開の前に要ります。1人が大量に登録して、D1 や R2 を使い切ることを防ぐためです。

ギアの一覧の画面の上の部分。見出し「ギア一覧」の下に、小さく「登録数 4 / 300」と出ている。その下に、所有・欲しい・廃棄済のタブと、ギアの一覧がある

一覧の見出しの下に、今の数と上限を出す

上限の数は、テーブルに持つ

開発を始めたときから、CLAUDE.md の共通のルールに「プランごとの上限は、コードに直書きしない」と書いていました。plan_limits というテーブルを作り、区分ごとに1行で持ちます。

対象 フリーの上限(仮)
ギア 300
消耗品 100
コンテナ 50
プラン 20
キャンプ 100
画像 300

料金の話がまだなので、数は仮です。自分のデータ(ギア 56 件)より十分に多く、乱用は止まる数にしました。有料の行は、料金を決めるときに入れます。

実際に効く上限の決め方

その人に効く上限は、1つの関数で決めます。

  • 管理者は、無制限
  • ほかは、その人の区分の行
  • 区分の行がなければ、フリーの行を使う
  • フリーの行も無ければ、何も登録できない

下の2つは、入れ忘れへの備えです。有料の行を入れ忘れたまま、誰かを有料にしても、無制限にはなりません。「分からないときは、厳しいほうに倒す」としました。

上限に達したら、新しい登録だけを止める

コンテナの登録の画面。入力欄の代わりに、赤い文字で「コンテナの数が上限に達しています(2 / 2)」と出ている。その下に「これ以上は登録できません。不要なコンテナを削除すると、また登録できます。」「登録済みのものは、今までどおり見る・編集するができます。」と、「コンテナ一覧へ」のボタンがある

上限に達しているときの、登録の画面(撮るために、上限を一時的に 2 にしている)

上限に達したときは、登録の画面で、入力欄の代わりに案内を出します。入力し終わってから断られるより、親切だと考えました。もちろん、サーバーでも確かめています。画面を通さずに送っても、登録されません。

止めるのは、新しい登録だけです。見る・チェックする・編集する・削除するは、今までどおりできます。上限をあとで下げて、超えた状態になっても、データは消しません。

画像だけは、少し細かくなりました。止めるのは、「画像のないギアやキャンプに、新しく画像を付けるとき」だけです。すでにある画像の差し替えは、枚数が増えないので、上限に達していてもできます。


テスト用のユーザーが、上限に当たる

実装の前に、1つ困ることが分かっていました。

E2E は、テスト用のユーザー1人で、全部のテストを流します。ローカルの D1 は、テストのデータを消しません。前回(#28)に書いたとおり、ギアは、すぐに 1,000 件を超えます。フリーの上限(300 件)を効かせると、ほとんどのテストが落ちてしまいます。

かといって、「テストのときは上限を見ない」という分岐を、アプリのコードに入れたくはありません。

そこで、テストの準備で、ローカルの D1 にだけ、とても大きい上限の行を入れることにしました。テスト用のユーザーは、その区分にします。開発環境や本番の D1 には、この行は入りません。アプリのコードは、本番と同じものが動きます。

上限そのもののテストは、テストの中で別のユーザー(フリー)を作って行います。300 件を入れる代わりに、フリーの上限を一時的に小さい数に書き換え、終わったら戻します。

受入確認も、同じやり方で

人が画面で確かめるときも、300 件を手で登録するわけにはいきません。開発用の D1 のコンソールで、上限を一時的に小さくして確かめました。

管理者は無制限になるので、自分のアカウントでは確かめられません。ここで役に立ったのが、もう1つの Google アカウントです。凍結の確認でも、上限の確認でも、一般のユーザーとして使いました。公開の前に、「自分ではない人」として触ってみる、よい機会になっています。


おわりに

  • 管理者にも、ほかの人のデータの中身は見せない。見せる画面を作らなければ、漏れない
  • 似た言葉は、早めに分ける。「休止」は本人が選ぶ、「凍結」は管理者が止める
  • 権限や区分は、本人が書き換えられないことを、実際に送って確かめる
  • 上限の数は、テーブルに持つ。行がないときは、厳しいほうに倒す
  • 止めるのは、新しい登録だけ。超えたデータは消さない
  • テストのための分岐を、アプリに入れない。テストの側で、データを用意する

次回は、アカウントの画面と、エクスポート、退会です。

← トップページに戻る