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

【TeamRole enum】Firestoreのrole文字列をDartの権限として扱う

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

このページでやること

このページでは、Firestoreに保存している role の文字列を、Dart側で安全に扱えるようにします。

Firestoreには、次のように保存しています。

role: owner

これを、Dartでは TeamRole という形で扱います。

TeamRole.owner

こうすることで、権限チェックのコードが分かりやすくなります。


今日のゴール

今まで文字列で見ていた role を、Dartの enum に変換します。

Firestore
↓
role: owner

Dart
↓
TeamRole.owner

最終的に、次のように書けるようにします。

if (role.canDeleteTask) {
  // タスク削除OK
}

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

単語一言説明
enum決まった種類だけを扱うための型
TeamRoleチーム内の権限を表すDartの型
ownerオーナー
admin管理者
memberメンバー
viewer閲覧者
extensionenumに便利機能を追加する書き方
fromString文字列からenumに変換する関数

npmや環境変数は必要?

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

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

このページでやることは、Dartコードを追加するだけです。

main.dartを開く
↓
TeamRole enumを作る
↓
Firestoreのrole文字列をTeamRoleに変換する
↓
権限チェックを読みやすくする

Step 1:main.dartを開く

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

lib/main.dart

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

code lib/main.dart

Step 2:enumとは何かを理解する

enum は、使える値を決めておくための書き方です。

今回は、権限はこの4つだけです。

owner
admin
member
viewer

そのため、Dartでは次のように書きます。

enum TeamRole {
  owner,
  admin,
  member,
  viewer,
}

これで、TeamRole には4つの値しか入らなくなります。


Step 3:TeamRoleを追加する

AppColorsTeamCard より上のあたりに、次のコードを追加します。

enum TeamRole {
  owner,
  admin,
  member,
  viewer,
}

これだけで、Dart側で権限を型として扱えるようになります。

TeamRole.owner
TeamRole.admin
TeamRole.member
TeamRole.viewer

Step 4:Firestoreの文字列をTeamRoleに変換する

Firestoreから取れる role は文字列です。

owner
admin
member
viewer

これを TeamRole に変える関数を作ります。

TeamRole の下に、次のコードを追加します。

TeamRole teamRoleFromString(String? value) {
  switch (value) {
    case 'owner':
      return TeamRole.owner;
    case 'admin':
      return TeamRole.admin;
    case 'viewer':
      return TeamRole.viewer;
    case 'member':
    default:
      return TeamRole.member;
  }
}

これで、文字列をDartの権限に変換できます。

'owner'  → TeamRole.owner
'admin'  → TeamRole.admin
'member' → TeamRole.member
'viewer' → TeamRole.viewer

知らない値が来た場合は、安全のため member にします。


Step 5:TeamRoleを日本語表示に変える

画面では、日本語で表示した方が分かりやすいです。

次のコードを追加します。

String teamRoleLabel(TeamRole role) {
  switch (role) {
    case TeamRole.owner:
      return 'オーナー';
    case TeamRole.admin:
      return '管理者';
    case TeamRole.member:
      return 'メンバー';
    case TeamRole.viewer:
      return '閲覧者';
  }
}

これで、画面表示を変換できます。

TeamRole.owner  → オーナー
TeamRole.admin  → 管理者
TeamRole.member → メンバー
TeamRole.viewer → 閲覧者

Step 6:権限チェック用の関数を作る

次に、できる操作を判定する関数を作ります。

まずはタスク削除です。

bool canDeleteTaskByRole(TeamRole role) {
  return role == TeamRole.owner || role == TeamRole.admin;
}

意味はこうです。

owner → タスク削除OK
admin → タスク削除OK
member → タスク削除NG
viewer → タスク削除NG

Step 7:タスク作成できるか判定する

タスク作成は、owneradminmember ができます。

bool canCreateTaskByRole(TeamRole role) {
  return role == TeamRole.owner ||
      role == TeamRole.admin ||
      role == TeamRole.member;
}

viewer は作成できません。


Step 8:タスク編集できるか判定する

タスク編集も、最初は owneradminmember ができる設計にします。

bool canEditTaskByRole(TeamRole role) {
  return role == TeamRole.owner ||
      role == TeamRole.admin ||
      role == TeamRole.member;
}

Step 9:メンバー管理できるか判定する

メンバー管理は、owneradmin だけにします。

bool canManageMembersByRole(TeamRole role) {
  return role == TeamRole.owner || role == TeamRole.admin;
}

Step 10:チーム削除できるか判定する

チーム削除は強い操作なので、owner だけにします。

bool canDeleteTeamByRole(TeamRole role) {
  return role == TeamRole.owner;
}

Step 11:ここまでの完成コード

TeamRole 周りのコードは、次の形になります。

enum TeamRole {
  owner,
  admin,
  member,
  viewer,
}

TeamRole teamRoleFromString(String? value) {
  switch (value) {
    case 'owner':
      return TeamRole.owner;
    case 'admin':
      return TeamRole.admin;
    case 'viewer':
      return TeamRole.viewer;
    case 'member':
    default:
      return TeamRole.member;
  }
}

String teamRoleLabel(TeamRole role) {
  switch (role) {
    case TeamRole.owner:
      return 'オーナー';
    case TeamRole.admin:
      return '管理者';
    case TeamRole.member:
      return 'メンバー';
    case TeamRole.viewer:
      return '閲覧者';
  }
}

bool canCreateTaskByRole(TeamRole role) {
  return role == TeamRole.owner ||
      role == TeamRole.admin ||
      role == TeamRole.member;
}

bool canEditTaskByRole(TeamRole role) {
  return role == TeamRole.owner ||
      role == TeamRole.admin ||
      role == TeamRole.member;
}

bool canDeleteTaskByRole(TeamRole role) {
  return role == TeamRole.owner || role == TeamRole.admin;
}

bool canManageMembersByRole(TeamRole role) {
  return role == TeamRole.owner || role == TeamRole.admin;
}

bool canDeleteTeamByRole(TeamRole role) {
  return role == TeamRole.owner;
}

まずは、この形でOKです。


Step 12:getMyRoleをTeamRole対応にする

前のページでは、自分のroleを文字列で取得していました。

Future<String?> getMyRole() async {
  ...
}

これを、TeamRole を返す形に変えます。

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

  if (user == null) {
    return TeamRole.viewer;
  }

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

  final data = memberDoc.data();
  final roleText = data?['role']?.toString();

  return teamRoleFromString(roleText);
}

ログイン状態が分からない場合は、安全のため viewer にします。

不明な人
↓
見るだけ扱い

Step 13:タスク削除チェックを変更する

前のコードでは、このように書いていました。

final role = await getMyRole();

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

これを、次のように変更します。

final role = await getMyRole();

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

roleTeamRole になったので、文字列ではなくDartの権限として判定できます。


Step 14:SnackBarの表示を少し分かりやすくする

権限がないときのメッセージは、次のままでOKです。

void showNoPermissionMessage() {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('この操作を行う権限がありません。'),
    ),
  );
}

タスク削除専用にするなら、次でもOKです。

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

Step 15:保存する

main.dart を保存します。

Macの場合:

command + S

Windowsの場合:

Ctrl + S

Step 16:実行する

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

flutter run

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

r

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

R

Step 17:動作確認する

アプリを開きます。

ログイン
↓
トーク一覧
↓
チームをタップ
↓
タスク一覧
↓
タスクを左スワイプ

自分の roleowner または admin の場合、削除確認が出ればOKです。

タスクを削除しますか?

member または viewer の場合、削除できなければOKです。

この操作を行う権限がありません。

Step 18:Firestoreでroleを変えて確認する

Firebase Consoleを開きます。

https://console.firebase.google.com/

次の場所を開きます。

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

role を変更して確認します。

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

よくあるエラーと直し方

エラー原因直し方
TeamRole isn't definedenumを書いていないenum TeamRole を追加
teamRoleFromString isn't defined変換関数がないteamRoleFromString() を追加
canDeleteTaskByRole isn't defined判定関数がないcanDeleteTaskByRole() を追加
Future<TeamRole>でエラー戻り値がStringのままreturn teamRoleFromString(roleText); にする
ずっとmember扱いになるFirestoreのrole文字列が違うowner/admin/member/viewer にする
削除できないroleがmember/viewerFirestoreのroleを確認
permission-deniedFirestore Rulesで拒否開発用Rulesを確認

role文字列が間違っているとき

Firestoreの role が次のように間違っているとします。

role: administrator

この場合、teamRoleFromString() では member 扱いになります。

case 'member':
default:
  return TeamRole.member;

最初は、Firestoreの値を必ずこの4つにそろえてください。

owner
admin
member
viewer

viewerを安全な初期値にする考え方

ログインしていない場合や、メンバー情報がない場合は、強い権限を与えない方が安全です。

そのため、次のように viewer にする考え方もあります。

if (user == null) {
  return TeamRole.viewer;
}

ただし、知らない文字列が来た場合は、今回は member にしています。

より安全にしたい場合は、ここも viewer にしてOKです。

case 'viewer':
default:
  return TeamRole.viewer;

安全重視なら、defaultviewer がおすすめです。


最短作業まとめ

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

1. TeamRoleを作る

enum TeamRole {
  owner,
  admin,
  member,
  viewer,
}

2. 文字列からTeamRoleに変える

TeamRole teamRoleFromString(String? value) {
  switch (value) {
    case 'owner':
      return TeamRole.owner;
    case 'admin':
      return TeamRole.admin;
    case 'viewer':
      return TeamRole.viewer;
    case 'member':
    default:
      return TeamRole.member;
  }
}

3. タスク削除できるか判定する

bool canDeleteTaskByRole(TeamRole role) {
  return role == TeamRole.owner || role == TeamRole.admin;
}

4. getMyRoleを変更する

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

  if (user == null) {
    return TeamRole.viewer;
  }

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

  final data = memberDoc.data();
  final roleText = data?['role']?.toString();

  return teamRoleFromString(roleText);
}

5. 保存して実行

flutter run

チェックリスト

□ main.dartを開いた
□ TeamRole enumを作った
□ ownerを追加した
□ adminを追加した
□ memberを追加した
□ viewerを追加した
□ teamRoleFromString()を作った
□ teamRoleLabel()を作った
□ canCreateTaskByRole()を作った
□ canEditTaskByRole()を作った
□ canDeleteTaskByRole()を作った
□ canManageMembersByRole()を作った
□ canDeleteTeamByRole()を作った
□ getMyRole()をFuture<TeamRole>にした
□ タスク削除の判定をTeamRoleで行った
□ 保存した
□ flutter runで確認した

ミニ確認問題

Q1. enum は何のために使いますか?

回答

決まった種類だけを安全に扱うために使います。

今回なら、owneradminmemberviewer だけを扱います。


Q2. Firestoreの role: owner はDartでは何に変換しますか?

回答

TeamRole.owner に変換します。


Q3. タスク削除できるTeamRoleはどれですか?

回答

TeamRole.ownerTeamRole.admin です。


Q4. teamRoleFromString() は何をする関数ですか?

回答

Firestoreの文字列を、Dartの TeamRole に変換する関数です。


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

回答

必要ありません。

このページでは、Dartの enum と変換関数を追加するだけです。


このページのまとめ

  • Firestoreには role を文字列で保存する。
  • Dart側では TeamRole enumとして扱う。
  • teamRoleFromString() で文字列からenumに変換する。
  • teamRoleLabel() で日本語表示に変換する。
  • canDeleteTaskByRole() のように権限判定を関数化する。
  • 文字列のまま判定するより、enumにした方が読みやすくなる。
  • 本番ではFlutter側だけでなく、Firestore Security Rules側にも権限制御を書く。
  • このページではnpmや環境変数は不要。

次のページでやること

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

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

教材トップへ戻る