Flutterアプリケーション開発概論

【タスク削除】admin以上の権限でタスクを削除できるようにする

32LINE風チームタスク管理アプリを作りながら、ログイン・データベース・権限管理を学ぶ
FlutteriOSAndroidMacOSWindows基礎から学ぶ開発アプリ開発

このページでやること

このページでは、タスクを削除できるようにします。

ただし、誰でも削除できるのではなく、チーム内の権限が admin 以上の人だけ削除できるようにします。

今回の考え方は、こうです。

owner → 削除できる
admin → 削除できる
member → 削除できない
viewer → 削除できない

タスクの保存場所はここです。

teams/{teamId}/tasks/{taskId}

このタスクを、左スワイプで削除します。


今日のゴール

タスク一覧画面で、タスクを左スワイプしたときに、権限を確認して削除できるようにします。

タスクを左スワイプ
↓
自分のroleを確認
↓
owner または admin なら削除OK
↓
member または viewer なら削除NG

このページでは、Flutter側で権限チェックを入れます。

本番では、あとでFirestore Security Rules側にも必ず権限チェックを入れます。


このページで出てくる単語

単語一言説明
roleチーム内での権限
ownerチームの所有者
admin管理者
member一般メンバー
viewer閲覧のみ
Dismissibleスワイプ削除できるWidget
delete()Firestoreのデータを削除する命令
confirmDismiss削除前に確認する処理

npmや環境変数は必要?

このページでは、npmは使いません。

環境変数も設定しません。

必要なのは、すでに使っているこの2つです。

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';

このページでは、TaskListPage に削除処理を追加します。


Step 1:main.dartを開く

次のファイルを開きます。

lib/main.dart

ターミナルから開く場合はこちらです。

code lib/main.dart

Step 2:TaskListPageを探す

main.dart の中で、次を検索します。

class TaskListPage

VS Codeなら、次で検索できます。

command + F

この TaskListPage の中に、タスク削除処理を追加します。


Step 3:自分のroleを取得する関数を作る

まず、ログイン中ユーザーのチーム内権限を取得します。

_TaskListPageState の中に、次の関数を追加します。

Future<String?> getMyRole() async {
  final user = FirebaseAuth.instance.currentUser;

  if (user == null) {
    return null;
  }

  final memberDoc = await FirebaseFirestore.instance
      .collection('teams')
      .doc(widget.teamId)
      .collection('members')
      .doc(user.uid)
      .get();

  final data = memberDoc.data();

  return data?['role']?.toString();
}

この関数は、次の場所を見に行きます。

teams/{teamId}/members/{uid}

そこに保存されている role を取得します。


Step 4:admin以上か確認する関数を作る

次に、取得した role が削除できる権限かどうかを判定します。

_TaskListPageState の中に、次の関数を追加します。

bool canDeleteTask(String? role) {
  return role == 'owner' || role == 'admin';
}

意味はこうです。

role が owner なら true
role が admin なら true
それ以外は false

true は削除できるという意味です。

false は削除できないという意味です。


Step 5:タスク削除処理を作る

次に、Firestoreからタスクを削除する処理を作ります。

_TaskListPageState の中に、次の関数を追加します。

Future<void> deleteTask(String taskId) async {
  await FirebaseFirestore.instance
      .collection('teams')
      .doc(widget.teamId)
      .collection('tasks')
      .doc(taskId)
      .delete();
}

これは、次の場所を削除する処理です。

teams/{teamId}/tasks/{taskId}

Step 6:削除確認ダイアログを作る

いきなり削除すると危ないので、確認画面を出します。

_TaskListPageState の中に、次の関数を追加します。

Future<bool> confirmDeleteTask(String taskTitle) async {
  final result = await showDialog<bool>(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: const Text('タスクを削除しますか?'),
        content: Text('「$taskTitle」を削除します。'),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(false),
            child: const Text('キャンセル'),
          ),
          FilledButton(
            onPressed: () => Navigator.of(context).pop(true),
            style: FilledButton.styleFrom(
              backgroundColor: AppColors.danger,
            ),
            child: const Text('削除する'),
          ),
        ],
      );
    },
  );

  return result == true;
}

「削除する」を押すと true

「キャンセル」を押すと false になります。


Step 7:権限がないときのメッセージを作る

memberviewer が削除しようとしたときは、メッセージを出します。

_TaskListPageState の中に、次の関数を追加します。

void showNoPermissionMessage() {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('タスクを削除できるのはownerまたはadminのみです。'),
    ),
  );
}

SnackBar は、画面下に一時的に出るメッセージです。


Step 8:TaskCardをDismissibleで包む

TaskListPage の中で、TaskCard を返している場所を探します。

今は、次のような形になっているはずです。

return TaskCard(
  title: title,
  description: description,
  status: status,
  priority: priority,
  assigneeEmail: assigneeEmail,
  onTap: () {
    showEditTaskSheet(
      taskId: doc.id,
      currentTitle: title,
      currentDescription: description,
      currentAssigneeEmail: assigneeEmail,
      currentPriority: priority,
      currentStatus: status,
    );
  },
);

これを、次のコードに変更します。

return Dismissible(
  key: ValueKey(doc.id),
  direction: DismissDirection.endToStart,
  background: Container(
    alignment: Alignment.centerRight,
    padding: const EdgeInsets.only(right: 20),
    decoration: BoxDecoration(
      color: AppColors.danger,
      borderRadius: BorderRadius.circular(18),
    ),
    child: const Icon(
      Icons.delete,
      color: Colors.white,
    ),
  ),
  confirmDismiss: (_) async {
    final role = await getMyRole();

    if (!canDeleteTask(role)) {
      showNoPermissionMessage();
      return false;
    }

    return confirmDeleteTask(title);
  },
  onDismissed: (_) async {
    await deleteTask(doc.id);
  },
  child: TaskCard(
    title: title,
    description: description,
    status: status,
    priority: priority,
    assigneeEmail: assigneeEmail,
    onTap: () {
      showEditTaskSheet(
        taskId: doc.id,
        currentTitle: title,
        currentDescription: description,
        currentAssigneeEmail: assigneeEmail,
        currentPriority: priority,
        currentStatus: status,
      );
    },
  ),
);

これで、左スワイプ削除に権限チェックが入ります。


Step 9:confirmDismissの流れを確認する

今回の一番大事な部分はここです。

confirmDismiss: (_) async {
  final role = await getMyRole();

  if (!canDeleteTask(role)) {
    showNoPermissionMessage();
    return false;
  }

  return confirmDeleteTask(title);
},

流れはこうです。

左スワイプする
↓
自分のroleを取得する
↓
owner/admin か確認する
↓
権限なし → 削除しない
↓
権限あり → 確認ダイアログを出す

return false にすると、削除されません。

return true になると、削除が進みます。


Step 10:onDismissedでFirestoreから削除する

この部分で、Firestoreからタスクを削除します。

onDismissed: (_) async {
  await deleteTask(doc.id);
},

doc.id は、タスクのIDです。

つまり、次の {taskId} にあたります。

teams/{teamId}/tasks/{taskId}

Step 11:保存する

main.dart を保存します。

Macの場合:

command + S

Windowsの場合:

Ctrl + S

Step 12:実行する

ターミナルで実行します。

flutter run

すでに起動している場合は、ターミナルで r を押します。

r

うまく反映されない場合は、R を押します。

R

Step 13:ownerで削除を確認する

まず、チーム作成者でログインします。

チーム作成者は owner になっています。

トーク一覧
↓
チームをタップ
↓
タスク一覧
↓
タスクを左スワイプ
↓
削除する

タスクが一覧から消えれば成功です。


Step 14:adminで確認する

Firestoreで、自分の roleadmin に変えると確認できます。

Firebase Consoleで、次の場所を開きます。

Firestore Database
↓
teams
↓
作成したteamId
↓
members
↓
自分のuid

role を次のように変更します。

role: admin

アプリに戻り、タスクを左スワイプします。

削除できればOKです。


Step 15:memberで削除できないことを確認する

同じ場所で、role を次のように変更します。

role: member

アプリに戻り、タスクを左スワイプします。

次のメッセージが出て、削除されなければ成功です。

タスクを削除できるのはownerまたはadminのみです。

Step 16:viewerで削除できないことを確認する

最後に、role を次のように変更します。

role: viewer

タスクを左スワイプして、削除できないことを確認します。

viewer → 削除できない

これで、権限チェックの基本が確認できます。


よくあるエラーと直し方

| エラー | 原因 | 直し方 |

|—|—|

| getMyRole isn't defined | 関数がない | getMyRole() を追加 |

| canDeleteTask isn't defined | 関数がない | canDeleteTask() を追加 |

| deleteTask isn't defined | 関数がない | deleteTask() を追加 |

| 左スワイプできない | Dismissible で包んでいない | TaskCardDismissible で包む |

| 権限なしでも削除される | confirmDismiss がない | confirmDismiss でrole確認する |

| role がnullになる | members/{uid} がない | Firestoreでmembersを確認 |

| permission-denied | Firestore Rulesで拒否 | 開発用Rulesを確認 |


roleがnullになるとき

role が取れないときは、Firestoreを確認します。

teams
↓
作成したteamId
↓
members
↓
自分のuid

中に、次があるか確認してください。

role: owner

または、

role: admin

members/{uid} がない場合、チーム作成時のowner登録ができていない可能性があります。


permission-denied が出たとき

開発中だけ、Firestore Rulesを次のようにして確認できます。

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}

これは学習用です。

本番公開では使わないでください。

本番では、Flutter側だけでなく、Firestore Rules側でも owner / admin だけ削除できるようにします。


注意:Flutter側の権限チェックだけでは不十分

このページでは、Flutterの画面側で削除を止めています。

ただし、本番ではこれだけでは不十分です。

理由は、アプリの画面を通さずにFirestoreへ直接アクセスされる可能性があるからです。

そのため、本番では必ずFirestore Security Rulesにも制限を書きます。

Flutter側
↓
ユーザーに分かりやすく制御する

Firestore Rules側
↓
本当にデータを守る

この教材では、まずFlutter側で動きを理解します。


最短作業まとめ

読むのが大変な人は、ここだけ見てください。

1. 自分のroleを取得

Future<String?> getMyRole() async {
  final user = FirebaseAuth.instance.currentUser;

  if (user == null) {
    return null;
  }

  final memberDoc = await FirebaseFirestore.instance
      .collection('teams')
      .doc(widget.teamId)
      .collection('members')
      .doc(user.uid)
      .get();

  final data = memberDoc.data();

  return data?['role']?.toString();
}

2. admin以上か確認

bool canDeleteTask(String? role) {
  return role == 'owner' || role == 'admin';
}

3. タスクを削除

Future<void> deleteTask(String taskId) async {
  await FirebaseFirestore.instance
      .collection('teams')
      .doc(widget.teamId)
      .collection('tasks')
      .doc(taskId)
      .delete();
}

4. Dismissibleで包む

return Dismissible(
  key: ValueKey(doc.id),
  direction: DismissDirection.endToStart,
  confirmDismiss: (_) async {
    final role = await getMyRole();

    if (!canDeleteTask(role)) {
      showNoPermissionMessage();
      return false;
    }

    return confirmDeleteTask(title);
  },
  onDismissed: (_) async {
    await deleteTask(doc.id);
  },
  child: TaskCard(
    title: title,
    description: description,
    status: status,
    priority: priority,
    assigneeEmail: assigneeEmail,
    onTap: () {
      showEditTaskSheet(
        taskId: doc.id,
        currentTitle: title,
        currentDescription: description,
        currentAssigneeEmail: assigneeEmail,
        currentPriority: priority,
        currentStatus: status,
      );
    },
  ),
);

チェックリスト

□ main.dartを開いた
□ TaskListPageを探した
□ getMyRole()を作った
□ canDeleteTask()を作った
□ deleteTask()を作った
□ confirmDeleteTask()を作った
□ showNoPermissionMessage()を作った
□ TaskCardをDismissibleで包んだ
□ directionをendToStartにした
□ confirmDismissでroleを確認した
□ owner/adminなら削除できるようにした
□ member/viewerなら削除できないようにした
□ onDismissedでdeleteTask(doc.id)を呼んだ
□ 保存した
□ flutter runで確認した

ミニ確認問題

Q1. タスクを削除できる権限はどれですか?

回答

owneradmin です。


Q2. member はタスクを削除できますか?

回答

できません。


Q3. 自分の権限はFirestoreのどこから取得しますか?

回答

次の場所から取得します。

teams/{teamId}/members/{uid}

Q4. タスク削除で使うFirestoreの命令は何ですか?

回答

delete() です。


Q5. このページでnpmや環境変数は必要ですか?

回答

必要ありません。

このページでは、FlutterとFirestoreの既存設定だけで進めます。


このページのまとめ

  • タスク削除には Dismissible を使う。
  • 左スワイプ削除は DismissDirection.endToStart を使う。
  • 自分の権限は teams/{teamId}/members/{uid} から取得する。
  • owneradmin は削除できる。
  • memberviewer は削除できない。
  • Flutter側の権限チェックは、ユーザー体験のために入れる。
  • 本番ではFirestore Security Rules側にも必ず制限を書く。
  • このページではnpmや環境変数は不要。

次のページでやること

次のページでは、メンバー一覧画面を作ります。

teams/{teamId}/members を取得して、チームに参加しているユーザーを表示します。

教材トップへ戻る