【権限変更】ownerだけがメンバーの権限を変更できるようにする
このページでやること
このページでは、既存メンバーの権限を変更できるようにします。
ただし、権限変更ができるのは owner だけにします。
owner → 権限変更できる
admin → 権限変更できない
member → 権限変更できない
viewer → 権限変更できない
変更できる権限は、この3つです。
admin
member
viewer
owner は最上位権限なので、この画面では選ばせません。
今日のゴール
メンバー一覧画面で、メンバーをタップすると、権限変更画面を開けるようにします。
メンバー一覧
↓
メンバーをタップ
↓
自分がownerか確認
↓
ownerなら権限変更画面を表示
↓
admin / member / viewer を選ぶ
↓
Firestoreを更新
保存場所はここです。
teams/{teamId}/members/{uid}
npmや環境変数は必要?
このページでは、npmは使いません。
環境変数も設定しません。
FlutterとFirestoreの既存設定だけで進めます。
必要なimportはこちらです。
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
Step 1:main.dartを開く
次のファイルを開きます。
lib/main.dart
ターミナルから開く場合はこちらです。
code lib/main.dart
Step 2:MemberListPageを探す
main.dart の中で、次を検索します。
class MemberListPage
VS Codeなら、次で検索できます。
command + F
今回は、_MemberListPageState の中に処理を追加します。
Step 3:権限変更用の変数を作る
_MemberListPageState の中に、次を追加します。
String selectedEditRole = 'member';
完成イメージです。
class _MemberListPageState extends State<MemberListPage> {
final emailController = TextEditingController();
String selectedMemberRole = 'member';
String selectedEditRole = 'member';
bool isAdding = false;
String? errorText;
@override
void dispose() {
emailController.dispose();
super.dispose();
}
}
selectedEditRole は、権限変更画面で選ばれている権限を覚える変数です。
Step 4:自分の権限を取得する関数を作る
まず、自分が owner かどうかを確認する必要があります。
_MemberListPageState の中に、次の関数を追加します。
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);
}
この関数は、Firestoreのここを見に行きます。
teams/{teamId}/members/{自分のuid}
Step 5:ownerかどうかを判定する関数を作る
_MemberListPageState の中に、次の関数を追加します。
bool canChangeMemberRole(TeamRole role) {
return role == TeamRole.owner;
}
意味はこうです。
ownerならtrue
owner以外ならfalse
このページでは、権限変更は owner だけにします。
Step 6:権限がないときのメッセージを作る
_MemberListPageState の中に、次の関数を追加します。
void showNoPermissionMessage() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('権限を変更できるのはオーナーのみです。'),
),
);
}
SnackBar は、画面下に出る短いメッセージです。
Step 7:MemberCardをタップできるようにする
今の MemberCard は、表示だけになっている場合があります。
MemberCard に onTap を追加します。
class MemberCard extends StatelessWidget {
const MemberCard({
super.key,
required this.displayName,
required this.email,
required this.role,
required this.onTap,
});
final String displayName;
final String email;
final TeamRole role;
final VoidCallback onTap;
追加したのは、この2つです。
required this.onTap,
final VoidCallback onTap;
Step 8:MemberCardをInkWellで包む
MemberCard の build() を、タップできる形にします。
@override
Widget build(BuildContext context) {
return AppCard(
onTap: onTap,
child: Row(
children: [
CircleAvatar(
backgroundColor: AppColors.lineGreen,
child: Text(
displayName.isNotEmpty ? displayName.substring(0, 1) : '?',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
displayName,
style: const TextStyle(
color: AppColors.text,
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
if (email.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
email,
style: const TextStyle(
color: AppColors.subText,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
],
],
),
),
_Label(text: teamRoleLabel(role)),
],
),
);
}
ここでは、すでに作ってある AppCard(onTap: ...) を使います。
Step 9:MemberCard呼び出しを修正する
MemberListPage の中で、MemberCard を返している部分を探します。
今は、次のようになっているはずです。
return MemberCard(
displayName: displayName,
email: email,
role: role,
);
これを、次のように変更します。
return MemberCard(
displayName: displayName,
email: email,
role: role,
onTap: () async {
final myRole = await getMyRole();
if (!canChangeMemberRole(myRole)) {
showNoPermissionMessage();
return;
}
if (role == TeamRole.owner) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('オーナーの権限はこの画面では変更できません。'),
),
);
return;
}
showEditMemberRoleSheet(
targetUid: doc.id,
displayName: displayName,
email: email,
currentRole: role,
);
},
);
これで、メンバーをタップしたときに権限チェックが入ります。
Step 10:権限変更ボトムシートを作る
_MemberListPageState の中に、次の関数を追加します。
Future<void> showEditMemberRoleSheet({
required String targetUid,
required String displayName,
required String email,
required TeamRole currentRole,
}) async {
selectedEditRole = currentRole.name;
errorText = null;
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
builder: (context) {
return StatefulBuilder(
builder: (context, setSheetState) {
return Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 20,
bottom: MediaQuery.of(context).viewInsets.bottom + 20,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 44,
height: 5,
margin: const EdgeInsets.only(bottom: 18),
decoration: BoxDecoration(
color: AppColors.border,
borderRadius: BorderRadius.circular(999),
),
),
const Text(
'権限を変更',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
color: AppColors.text,
),
),
const SizedBox(height: 12),
AppCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
displayName,
style: const TextStyle(
color: AppColors.text,
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
if (email.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
email,
style: const TextStyle(
color: AppColors.subText,
fontWeight: FontWeight.w600,
),
),
],
],
),
),
const SizedBox(height: 16),
const Text(
'新しい権限',
style: TextStyle(
color: AppColors.text,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ChoiceChip(
label: const Text('管理者'),
selected: selectedEditRole == 'admin',
onSelected: (_) {
setSheetState(() {
selectedEditRole = 'admin';
});
},
),
ChoiceChip(
label: const Text('メンバー'),
selected: selectedEditRole == 'member',
onSelected: (_) {
setSheetState(() {
selectedEditRole = 'member';
});
},
),
ChoiceChip(
label: const Text('閲覧者'),
selected: selectedEditRole == 'viewer',
onSelected: (_) {
setSheetState(() {
selectedEditRole = 'viewer';
});
},
),
],
),
const SizedBox(height: 10),
Container(
decoration: BoxDecoration(
color: AppColors.bg,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.border),
),
padding: const EdgeInsets.all(12),
child: Text(
roleDescription(selectedEditRole),
style: const TextStyle(
color: AppColors.subText,
height: 1.5,
fontWeight: FontWeight.w700,
),
),
),
if (errorText != null) ...[
const SizedBox(height: 12),
ErrorBox(message: errorText!),
],
const SizedBox(height: 16),
FilledButton(
onPressed: () async {
await updateMemberRole(
setSheetState: setSheetState,
targetUid: targetUid,
);
},
child: const Text('変更する'),
),
],
),
),
);
},
);
},
);
}
これで、権限変更用のボトムシートができます。
Step 11:権限説明 roleDescription を確認する
前のページで作った roleDescription() があるか確認します。
String roleDescription(String role) {
switch (role) {
case 'admin':
return 'タスク作成・編集・削除、メンバー管理ができます。';
case 'viewer':
return 'タスクやメンバーを確認できます。作成・編集・削除はできません。';
case 'member':
default:
return 'タスクの作成・編集ができます。削除やメンバー管理はできません。';
}
}
まだない場合は、_MemberListPageState の中に追加してください。
Step 12:Firestoreを更新する処理を作る
_MemberListPageState の中に、次の関数を追加します。
Future<void> updateMemberRole({
required StateSetter setSheetState,
required String targetUid,
}) async {
try {
await FirebaseFirestore.instance
.collection('teams')
.doc(widget.teamId)
.collection('members')
.doc(targetUid)
.update({
'role': selectedEditRole,
'updatedAt': FieldValue.serverTimestamp(),
});
if (mounted) {
Navigator.of(context).pop();
}
} on FirebaseException catch (e) {
setSheetState(() {
errorText = e.message ?? '権限変更に失敗しました。';
});
} catch (_) {
setSheetState(() {
errorText = '権限変更に失敗しました。';
});
}
}
これで、Firestore上の role を更新できます。
Step 13:更新する場所を確認する
今回、更新する場所はここです。
teams/{teamId}/members/{uid}
更新する項目は、この2つです。
role
updatedAt
コードではここです。
.update({
'role': selectedEditRole,
'updatedAt': FieldValue.serverTimestamp(),
});
Step 14:なぜownerは変更させないのか
owner は、チームの最上位権限です。
そのため、この画面では変更させません。
owner → 変更不可
admin → 変更可能
member → 変更可能
viewer → 変更可能
もし owner を簡単に変更できると、チーム管理者がいなくなったり、意図せず権限を失ったりする危険があります。
Step 15:保存する
main.dart を保存します。
Macの場合:
command + S
Windowsの場合:
Ctrl + S
Step 16:実行する
ターミナルで実行します。
flutter run
すでに起動している場合は、ターミナルで r を押します。
r
うまく反映されない場合は、R を押します。
R
Step 17:ownerで権限変更を確認する
自分の role が owner の状態で確認します。
ログイン
↓
トーク一覧
↓
チームをタップ
↓
右上のメンバーアイコン
↓
メンバーをタップ
権限変更画面が開けばOKです。
管理者
メンバー
閲覧者
たとえば、member を viewer に変更してみます。
Firestoreと画面表示が変われば成功です。
Step 18:adminで変更できないことを確認する
Firestoreで自分の role を admin に変えます。
teams
↓
作成したteamId
↓
members
↓
自分のuid
↓
role: admin
アプリに戻って、メンバーをタップします。
次のメッセージが出ればOKです。
権限を変更できるのはオーナーのみです。
Step 19:member / viewer でも確認する
同じように、自分の role を変更して確認します。
role: member
または、
role: viewer
どちらも、権限変更できなければOKです。
よくあるエラーと直し方
| エラー | 原因 | 直し方 |
|---|---|---|
selectedEditRole isn't defined | 変数がない | String selectedEditRole = 'member'; を追加 |
getMyRole isn't defined | 自分の権限取得関数がない | getMyRole() を追加 |
canChangeMemberRole isn't defined | 判定関数がない | canChangeMemberRole() を追加 |
showEditMemberRoleSheet isn't defined | ボトムシート関数がない | showEditMemberRoleSheet() を追加 |
updateMemberRole isn't defined | 更新処理がない | updateMemberRole() を追加 |
roleDescription isn't defined | 説明関数がない | roleDescription() を追加 |
| タップできない | MemberCardにonTapがない | onTap を追加 |
| ownerなのに変更できない | Firestoreのroleがownerでない | members/{uid} を確認 |
permission-denied が出たとき
開発中だけ、Firestore Rulesを次のようにして確認できます。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
これは学習用です。
本番公開では使わないでください。
本番では、Firestore Security Rules側でも owner だけが権限変更できるようにします。
注意:Flutter側だけでは本番の安全対策にならない
このページでは、Flutter側で owner だけ操作できるようにしています。
ただし、本番では、Firestore Rules側にも必ず制限を書きます。
Flutter側
↓
画面上の操作を制御する
Firestore Rules側
↓
本当にデータを守る
最終的な安全対策は、Firestore Rulesで行います。
最短作業まとめ
読むのが大変な人は、ここだけ見てください。
1. ownerだけ許可する
bool canChangeMemberRole(TeamRole role) {
return role == TeamRole.owner;
}
2. MemberCardをタップ可能にする
return MemberCard(
displayName: displayName,
email: email,
role: role,
onTap: () async {
final myRole = await getMyRole();
if (!canChangeMemberRole(myRole)) {
showNoPermissionMessage();
return;
}
if (role == TeamRole.owner) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('オーナーの権限はこの画面では変更できません。'),
),
);
return;
}
showEditMemberRoleSheet(
targetUid: doc.id,
displayName: displayName,
email: email,
currentRole: role,
);
},
);
3. Firestoreのroleを更新する
await FirebaseFirestore.instance
.collection('teams')
.doc(widget.teamId)
.collection('members')
.doc(targetUid)
.update({
'role': selectedEditRole,
'updatedAt': FieldValue.serverTimestamp(),
});
チェックリスト
□ main.dartを開いた
□ MemberListPageを探した
□ selectedEditRoleを作った
□ getMyRole()を作った
□ canChangeMemberRole()を作った
□ showNoPermissionMessage()を作った
□ MemberCardにonTapを追加した
□ MemberCard呼び出しで権限チェックした
□ owner以外は変更できないようにした
□ owner本人の権限は変更不可にした
□ showEditMemberRoleSheet()を作った
□ ChoiceChipでadmin/member/viewerを選べるようにした
□ updateMemberRole()を作った
□ Firestoreのroleを更新した
□ 保存した
□ flutter runで確認した
ミニ確認問題
Q1. メンバーの権限を変更できるのは誰ですか?
回答
owner だけです。
Q2. adminはメンバーの権限を変更できますか?
回答
この教材の設計では、できません。
権限変更は owner だけにします。
Q3. ownerの権限はこの画面で変更できますか?
回答
できません。
owner は最上位権限なので、この画面では変更させません。
Q4. 権限変更ではFirestoreのどこを更新しますか?
回答
次の場所を更新します。
teams/{teamId}/members/{uid}
Q5. このページでnpmや環境変数は必要ですか?
回答
必要ありません。
FlutterとFirestoreの既存設定だけで進めます。
このページのまとめ
- メンバー権限変更は
ownerだけにする。 admin、member、viewerは権限変更できない。owner自身の権限は、この画面では変更させない。MemberCardをタップできるようにする。- 権限変更画面はボトムシートで作る。
ChoiceChipでadmin、member、viewerを選ぶ。- Firestoreの
teams/{teamId}/members/{uid}のroleを更新する。 - 本番ではFirestore Security Rules側にも必ず制限を書く。
- このページではnpmや環境変数は不要。
次のページでやること
次のページでは、メンバー削除を作ります。
owner だけが、チームからメンバーを外せるようにします。
