栽培管理アプリを作ってます⑫:複数人で使えるアプリにする方法【認証・権限管理の実装】

目次

はじめに

前回、気象庁APIで天気情報を自動取得する機能を実装しました。

今回は、「複数人で使えるアプリ」にするため、代表とスタッフ(農作業員)の権限管理機能を追加しました。

1. やりたかったこと(要件定義)

栽培管理アプリを農園の複数人で使えるようにしたい。具体的には:

代表(農園のオーナー)ができること

  • ✅ ハウス・畝の登録・編集・削除
  • ✅ 農薬・肥料・作業マスタの登録・編集・削除
  • ✅ 作業記録の登録・編集・削除
  • ✅ 収穫記録の登録・編集・削除
  • ✅ スタッフの追加・管理
  • ✅ 全ての履歴の閲覧

スタッフ(農作業員)ができること

  • ✅ ハウス・畝の閲覧
  • ✅ 農薬・肥料・作業マスタの閲覧
  • ✅ 作業記録の登録・編集(自分の作業のみ)
  • ✅ 収穫記録の登録・編集(自分の収穫のみ)
  • ✅ 全ての履歴の閲覧
  • ❌ マスタの編集・削除(できない)
  • ❌ ハウス・畝の編集・削除(できない)

要は:
スタッフは「作業の実行」はできるが、「設定の変更」はできない設計です。

2. 設計

栽培アプリの設計

users
├─ id
├─ email
└─ organization_name  ← 代表のみ

cult_workers  ← 別テーブルで役割を管理
├─ user_id
├─ organization_id  ← 所属農園
└─ role (owner/staff)

データ取得:
1. cult_workers から organization_id を取得
2. organization_id でデータをフィルタ

この設計を採用した経緯と失敗談は
別の記事「最初の設計ミス」にまとめています。


3. 認証の実装:セキュリティを考えた設計変更

Supabase Auth の基本

Supabaseには標準で認証機能が備わっています:

  • メールアドレス + パスワードでのログイン
  • パスワードリセット機能
  • セッション管理(ログイン状態の維持)

オーナーならともかく、農園スタッフがいちいちメルアド登録するか?と思って、「ユーザー名でもログインできる」機能を追加しました。

  1. 認証の実装

ユーザー名でログインできる機能を実装しました。

実装方法:Server Action

Next.js の Server Action を使って実装しました。

ログインの流れ:

  1. ユーザーが「suzuki」とパスワードを入力
  2. Server Action がサーバー内でユーザー名→メールアドレスに変換
  3. そのメールアドレスでログイン処理を実行
  4. Cookie を発行してセッション管理
  5. ブラウザには成功/失敗のみ返す

メールアドレスはクライアントに一切送らず、全ての処理がサーバー内で完結します。

スタッフ追加機能

代表者が新しいスタッフを追加する機能も、同様にServer Actionで実装しました:

  1. 代表者が「ユーザー名・作業者名・パスワード」を入力
  2. サーバー内でSupabase Authにアカウント作成
  3. データベースに登録
  4. ブラウザには「成功」だけ返す

当初Edge Functionで実装しましたが、セキュリティ上の問題が発覚。
詳しくは「Edge Functionの5つの問題とServer Actionへの移行」へ


4.権限管理の実装

代表とスタッフが同じ農園データを扱えるようにしました。

課題

普通に実装すると、各ユーザーが自分のIDでデータを作ってしまい、共有できません:

【ダメな例】

  • 代表AAA → ハウスデータ(owner_id: AAA)
  • スタッフBBB → ハウスデータ(owner_id: BBB)
    ❌ データがバラバラ

解決策

データは全て代表のIDで保存し、ログインユーザーと農園を紐付けるテーブル(cult_workers)を用意しました。

【良い例】

  • 代表AAA → ハウスデータ(owner_id: AAA)
  • スタッフBBB → ハウスデータ(owner_id: AAA) ← 代表のIDで保存
    ✅ 全員が同じデータにアクセス可能

実装のポイント

getOwnerIdFromUser() 関数を作成し、「このユーザーはどの農園に所属しているか?」を判定します。

全てのデータ取得で、この関数を使用:

// 修正前(NG)
eq('owner_id', user.id)  // スタッフは取得できない

// 修正後(OK)
const ownerId = await getOwnerIdFromUser(user.id);
eq('owner_id', ownerId)  // 代表もスタッフも同じIDで取得

これで実現できたこと:

  • ✅ 代表とスタッフが同じデータを見れる
  • ✅ データは代表のIDで一元管理
  • ✅ 作業記録には誰がやったかも残る

データベース設計と関数の詳細は
owner_id設計とgetOwnerIdFromUser関数の実装」へ


5. RLSの設定

データベースレベルでのアクセス制御(RLS: Row Level Security)を設定しました。

なぜRLSが必要?

アプリケーションレベルだけでなく、データベース側でも二重に保護するためです。

RLSがないと:
悪意あるユーザーが開発者ツールで直接データベースにアクセスして、他の農園のデータを覗いたり、自分の権限を書き換えたりできてしまいます。

RLSがあると:
データベース側で「このユーザーはこのデータだけOK」と制限されるため、どんな方法でアクセスしても安全です。

設定した権限

マスタデータ(農薬・作物・ハウスなど)

  • 代表:見る・追加・編集・削除すべてOK
  • スタッフ:見るだけOK

作業記録

  • 代表:すべての記録を操作可能
  • スタッフ:自分が登録した記録だけ編集・削除可能

実装のポイント

SECURITY DEFINER関数を2つ作成しました:

  1. my_org_id() → 自分の所属農園IDを返す
  2. my_worker_id() → 自分の作業者IDを返す

これらの関数を使ってRLSポリシーを設定することで、循環参照の問題を回避しました。

ポリシー例(農薬マスタの閲覧権限):

CREATE POLICY "view_own_pesticides" ON cult_pesticides 
FOR SELECT
USING (
  owner_id = auth.uid()              -- 代表
  OR owner_id = public.my_org_id()   -- スタッフ(所属農園)
);

これで実現できたこと:

  • ✅ 代表は全データにアクセス可能
  • ✅ スタッフはマスタを閲覧のみ
  • ✅ スタッフは自分の作業記録だけ編集可能
  • ✅ 他の農園のデータは完全にブロック

RLS実装で遭遇した「循環参照問題」とSECURITY DEFINER関数の詳細は
RLS設定で大苦戦:循環参照とSECURITY DEFINER関数」へ

実装後に発覚した問題

実装後、実際に使ってみると3つの問題が発覚しました。

問題1: getOwnerIdFromUser()の適用漏れ

症状: スタッフでログインするとデータが見えない

原因: 一部のページでuser.idを直接使っていた

解決: 全ページでgetOwnerIdFromUser()を使うように修正

問題2: Client ComponentからのDB直接アクセス

症状: スタッフが農薬マスタを登録できてしまう

原因: 農薬登録だけClient Componentで直接Supabaseを呼んでいた

解決: Server Actionに変更し、ownerIdを使用

問題3: 作業履歴の表示ロジック

症状: 同じ日の複数作業が1つしか表示されない

原因: log_typeだけで重複判定していた

解決: work_type_idfertilizer_idで詳細に区別

各問題の詳細なコードと解決プロセスは
実装後に見つかった3つのバグと修正方法」へ

7. 最終的な構成

実装が完了したシステムの全体像をまとめます。

動作フロー

スタッフがログインして作業登録する流れ

1. ログイン

  • ユーザー名 “suzuki” を入力
  • Server Action でユーザー名→メール変換とログインを実行
  • Cookie が自動付与される
  • メールアドレスはクライアントに返さない(セキュリティ)

2. 画面表示

  • getOwnerIdFromUser(suzuki.id) を実行
  • ownerId = 代表のID (AAA)
  • ハウス一覧を owner_id = AAA で取得
  • → 代表のハウスが表示される

3. 作業登録

  • 「作業記録」を選択
  • Server Action: createLogAction() 実行
  • ownerId = AAA, worker_id = suzuki で保存
  • RLS: owner_id (AAA)auth.uid() (BBB) の所属農園 → 許可

4. 履歴表示

  • 代表・スタッフ両方が owner_id = AAA の全作業を閲覧可能
  • 作業者名も表示される

8. まとめ

実装できたこと

✅ 代表とスタッフの権限管理
✅ ユーザー名ログイン
✅ スタッフ追加機能
✅ RLSによるデータアクセス制御
✅ 作業履歴の正しい表示
セキュリティを考慮した認証システム(Edge Function → Server Action)


苦労したポイント

1. 設計の複雑さ

  • 出荷アプリとの設計の違い
  • owner_id vs company_id 問題

2. 全ファイルの修正

  • 15ファイル以上を修正
  • user.idownerId への変更

3. RLSの複雑さ

  • テーブルごとに異なるポリシー
  • 循環参照の問題
  • SECURITY DEFINER 関数の理解

4. 予期しないバグ

  • 農薬だけ登録できてしまう問題
  • 作業履歴の上書き問題
  • 「ようこそ 未設定さん」問題

5. セキュリティ設計の見直し

  • Edge Function の問題点の発覚
  • Server Action への全面移行

今後の改善予定

  • スタッフ編集機能(パスワード変更、メールアドレス設定)
  • 管理者権限を持つスタッフ
  • 作業者によるフィルタ機能
  • 出荷アプリとの設計統一(大規模リファクタ)

おわりに

複数人で使えるアプリにするため、認証・権限管理を実装しました。

設計ミスや予期しないバグに何度も悩まされましたが、特にセキュリティの問題を早期に発見して修正できたことは大きな学びでした。


目次