diff --git a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart index cb25f4c..05144e7 100644 --- a/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart +++ b/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart @@ -1171,7 +1171,8 @@ class MapSurveyController extends GetxController implements StyleEditable { } void startContactTool() { - if (!PermissionService.to.canContacts) { + if (!PermissionService.to + .canContacts(projectId: ProjectService.to.activeProject.value?.uuid)) { Get.snackbar( 'Nincs jogosultság', 'Kapcsolatok kezeléséhez jogosultság szükséges.', snackPosition: SnackPosition.BOTTOM); @@ -1248,7 +1249,8 @@ class MapSurveyController extends GetxController implements StyleEditable { Future saveContactAtPoint(LatLng point) async { activeEditTool.value = MapEditTool.none; - if (!PermissionService.to.canContacts) return; + if (!PermissionService.to.canContacts( + projectId: ProjectService.to.activeProject.value?.uuid)) return; final projectId = ProjectService.to.activeProject.value?.uuid; if (projectId == null) return; @@ -1275,7 +1277,8 @@ class MapSurveyController extends GetxController implements StyleEditable { } Future _loadContactMarkers() async { - if (!PermissionService.to.canContacts) { + if (!PermissionService.to + .canContacts(projectId: ProjectService.to.activeProject.value?.uuid)) { contactMarkers.clear(); contactsWithLocation.clear(); return; diff --git a/lib/services/permission_service.dart b/lib/services/permission_service.dart index e243e82..629d899 100644 --- a/lib/services/permission_service.dart +++ b/lib/services/permission_service.dart @@ -1,17 +1,14 @@ import 'package:get/get.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -/// Oldal-/funkció-szintű jogosultságok kezelése. +/// Oldal-/funkció-szintű jogosultságok kezelése — PROJEKTENKÉNT. /// -/// A jogosultságokat egy Supabase `app_permissions` tábla tárolja -/// (user_id + area), és a felhasználó a saját sorait olvashatja (RLS). -/// A service induláskor és bejelentkezéskor betölti a jelenlegi -/// felhasználó jogosultságait egy halmazba, amit a UI reaktívan figyel. -/// -/// Bővíthető: új védett terület = új 'area' string (pl. 'admin'), -/// a UI a [can] getterrel kérdez rá — kódmódosítás nélkül. A tényleges -/// védelmet a Supabase-oldali RLS adja (a `contacts`/admin táblákon), -/// ez a service csak a UI-t vezérli (menüpont elrejtése, üzenet). +/// A jogosultságokat a Supabase `app_permissions` tábla tárolja +/// (user_id + area + project_id), ahol a project_id NULL értéke +/// GLOBÁLIS jogot jelent (minden projektre érvényes — pl. admin). +/// A tényleges védelmet a Supabase-oldali RLS adja +/// (`has_permission(area, project_id)`), ez a service csak a UI-t +/// vezérli (menüpont elrejtése, üzenet). class PermissionService extends GetxService { static PermissionService get to => Get.find(); @@ -20,18 +17,31 @@ class PermissionService extends GetxService { SupabaseClient get _client => Supabase.instance.client; - /// A jelenlegi felhasználó engedélyezett területei. - final _areas = {}.obs; + /// projectId → engedélyezett területek. A `null` kulcs a GLOBÁLIS + /// (minden projektre érvényes) jogokat tárolja. + final _areasByProject = >{}.obs; final isLoaded = false.obs; - bool can(String area) => _areas.contains(area); - bool get canContacts => can(areaContacts); + /// [projectId] nélkül CSAK a globális jogokat nézi — projekt-specifikus + /// ellenőrzéshez mindig add meg az aktív projektet. + bool can(String area, {String? projectId}) { + if (_areasByProject[null]?.contains(area) ?? false) return true; + if (projectId != null) { + return _areasByProject[projectId]?.contains(area) ?? false; + } + return false; + } + + bool canContacts({String? projectId}) => + can(areaContacts, projectId: projectId); + + /// Az admin jogot szándékosan globálisnak tartjuk — ha ezt is + /// projektenként szeretnéd, ugyanígy paraméterezhető. bool get canAdmin => can(areaAdmin); @override void onInit() { super.onInit(); - // Induláskor és minden auth-változáskor újratöltjük. reload(); _client.auth.onAuthStateChange.listen((_) => reload()); } @@ -39,23 +49,28 @@ class PermissionService extends GetxService { Future reload() async { final user = _client.auth.currentUser; if (user == null) { - _areas.clear(); + _areasByProject.clear(); isLoaded.value = true; return; } try { final rows = await _client .from('terepi_seged_app_permissions') - .select('area') + .select('area, project_id') .eq('user_id', user.id); - _areas + + final map = >{}; + for (final r in rows) { + final pid = r['project_id'] as String?; + map.putIfAbsent(pid, () => {}).add(r['area'] as String); + } + _areasByProject ..clear() - ..addAll(rows.map((r) => r['area'] as String)); + ..addAll(map); } catch (_) { - // Hálózati hiba: nem adunk jogot (fail-closed), de nem is dobunk. - _areas.clear(); + _areasByProject.clear(); } finally { - _areas.refresh(); + _areasByProject.refresh(); isLoaded.value = true; } } diff --git a/lib/widgets/app_drawer.dart b/lib/widgets/app_drawer.dart index 3d1fcda..e9d706b 100644 --- a/lib/widgets/app_drawer.dart +++ b/lib/widgets/app_drawer.dart @@ -109,8 +109,9 @@ class AppDrawer extends StatelessWidget { ), Obx(() { final signedIn = AuthService.to.isSignedIn; + final projectId = ProjectService.to.activeProject.value?.uuid; final allowed = !Get.isRegistered() || - PermissionService.to.canContacts; + PermissionService.to.canContacts(projectId: projectId); if (!signedIn || !allowed) return const SizedBox.shrink(); return ListTile( leading: const Icon(Icons.phone_outlined), diff --git a/lib/widgets/map_edit_tools/map_edit_toolbar.dart b/lib/widgets/map_edit_tools/map_edit_toolbar.dart index 13055ae..01c7431 100644 --- a/lib/widgets/map_edit_tools/map_edit_toolbar.dart +++ b/lib/widgets/map_edit_tools/map_edit_toolbar.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:terepi_seged/enums/map_edit_tool.dart'; import 'package:terepi_seged/pages/map_survey/presentations/controllers/map_survey_controller.dart'; import 'package:terepi_seged/services/permission_service.dart'; +import 'package:terepi_seged/services/project_service.dart'; import 'map_toolbar_action.dart'; import 'map_toolbar_divider.dart'; @@ -66,7 +67,8 @@ class MapEditCompactToolbar extends StatelessWidget { selected: activeTool == MapEditTool.polygon, onTap: controller.startPolygonTool, ), - if (PermissionService.to.canContacts) + if (PermissionService.to.canContacts( + projectId: ProjectService.to.activeProject.value?.uuid)) ToolbarAction( icon: Icons.person_pin_circle_outlined, label: 'Kapcsolat',