From 2d812b20c4d9a961fc7c4c249dfa041f771023ce Mon Sep 17 00:00:00 2001 From: Andrew nuark G Date: Sat, 29 Apr 2023 01:08:18 +0700 Subject: [PATCH] Made possible updating security for user groups on tables --- lib/models/table_access.dart | 24 ++ lib/pages/dialogs/group_acl_dialog.dart | 406 ++++++++++++++++++++ lib/pages/home_panels/users_list_panel.dart | 10 + pubspec.lock | 10 +- 4 files changed, 445 insertions(+), 5 deletions(-) create mode 100644 lib/models/table_access.dart create mode 100644 lib/pages/dialogs/group_acl_dialog.dart diff --git a/lib/models/table_access.dart b/lib/models/table_access.dart new file mode 100644 index 0000000..5a9eea2 --- /dev/null +++ b/lib/models/table_access.dart @@ -0,0 +1,24 @@ +enum TableAccess { + read("r"), + write("w"), + readWrite("rw"), + none(""); + + final String def; + + const TableAccess(this.def); + + static TableAccess fromString(String? def) { + switch (def) { + case "r": + return TableAccess.read; + case "w": + return TableAccess.write; + case "rw": + return TableAccess.readWrite; + case "none": + default: + return TableAccess.none; + } + } +} diff --git a/lib/pages/dialogs/group_acl_dialog.dart b/lib/pages/dialogs/group_acl_dialog.dart new file mode 100644 index 0000000..6daa5cb --- /dev/null +++ b/lib/pages/dialogs/group_acl_dialog.dart @@ -0,0 +1,406 @@ +import 'package:data_table_2/data_table_2.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart' hide Response; +import 'package:recase/recase.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:tuuli_api/tuuli_api.dart'; +import 'package:tuuli_app/api_controller.dart'; +import 'package:tuuli_app/models/group_definition.dart'; +import 'package:tuuli_app/models/table_access.dart'; + +class GroupACLController extends GetxController { + final GroupDefinition group; + + GroupACLController(this.group); + + @override + void onInit() { + super.onInit(); + + refreshData(); + } + + final _tables = [].obs; + List get tables => _tables.toList(); + + final _access = {}.obs; + Map get access => _access; + + final _allowedColumns = {}.obs; + Map get allowedColumns => _allowedColumns; + + Future refreshData() async { + await refreshTables(); + + for (final table in tables) { + await refreshTableAccess(table); + } + } + + Future refreshTables() async { + try { + final resp = await ApiController.to.apiClient.listTables(); + + final respData = resp.data; + if (respData == null) { + throw Exception("No data in response"); + } + + _tables.clear(); + _tables.addAll(respData.where((e) => e.hidden != true)); + } on DioError catch (e) { + final respData = e.response?.data; + if (respData != null) { + Get.snackbar( + "Error trying to get tables", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to get tables", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to get tables", + "$e", + ); + } + } + + Future refreshTableAccess(TableDefinition table) async { + try { + final resp = await ApiController.to.apiClient.getItemsFromTable( + tableName: "table_access", + itemsSelector: ItemsSelector( + fields: ["access_type", "allowed_columns"], + where: [ + ColumnConditionCompat( + column: "user_group_id", + operator_: ColumnConditionCompatOperator.eq, + value: group.id, + ), + ColumnConditionCompat( + column: "table_name", + operator_: ColumnConditionCompatOperator.eq, + value: table.tableName, + ), + ], + ), + ); + + final respData = resp.data; + if (respData == null) { + throw Exception("No data in response"); + } + + if (respData.isNotEmpty) { + _access[table.tableId] = + TableAccess.fromString(respData.first["access_type"]); + _allowedColumns[table.tableId] = respData.first["allowed_columns"]; + } else { + _access[table.tableId] = TableAccess.none; + _allowedColumns[table.tableId] = null; + } + } on DioError catch (e) { + final respData = e.response?.data; + if (respData != null) { + Get.snackbar( + "Error trying to get tables access", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to get tables access", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to get tables access", + "$e", + ); + } + } + + Future updateTableAccess( + TableDefinition table, { + required bool read, + required bool write, + }) async { + final oldTableAccess = _access[table.tableId]; + var newTableAccess = TableAccess.none; + if (read && write) { + newTableAccess = TableAccess.readWrite; + } else if (read) { + newTableAccess = TableAccess.read; + } else if (write) { + newTableAccess = TableAccess.write; + } + + try { + Response resp; + if (newTableAccess == TableAccess.none) { + resp = await ApiController.to.apiClient.deleteItemFromTable( + tableName: "table_access", + columnConditionCompat: [ + ColumnConditionCompat( + column: "user_group_id", + operator_: ColumnConditionCompatOperator.eq, + value: group.id, + ), + ColumnConditionCompat( + column: "table_name", + operator_: ColumnConditionCompatOperator.eq, + value: table.tableName, + ), + ], + ); + } else if (oldTableAccess == TableAccess.none) { + resp = await ApiController.to.apiClient.createItem( + tableName: "table_access", + itemDefinition: { + "user_group_id": group.id, + "table_name": table.tableName, + "access_type": newTableAccess.def, + }, + ); + } else { + resp = await ApiController.to.apiClient.updateItemInTable( + tableName: "table_access", + itemUpdate: ItemUpdate( + item: { + "access_type": newTableAccess.def, + }, + oldItem: { + "user_group_id": group.id, + "table_name": table.tableName, + }, + ), + ); + } + + final respData = resp.data; + if (respData == null) { + throw Exception("No data in response"); + } + + refreshTableAccess(table); + } on DioError catch (e) { + final respData = e.response?.data; + if (respData != null) { + Get.snackbar( + "Error trying to update tables access", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to update tables access", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to update tables access", + "$e", + ); + } + } + + Future changeAllowedColumns(TableDefinition table) async { + final tableColumns = table.columns + .split(",") + .map((e) => e.split(":").first) + .where((e) => e.isNotEmpty) + .toList(); + final currentlyAvailableColumns = + _allowedColumns[table.tableId]!.split(","); + + if (currentlyAvailableColumns.length == 1 && + currentlyAvailableColumns.first == "*") { + currentlyAvailableColumns.clear(); + currentlyAvailableColumns.addAll(tableColumns); + } + + final selectedColumns = {}.obs; + for (final column in tableColumns) { + selectedColumns[column] = currentlyAvailableColumns.contains(column); + } + + final confirm = await Get.dialog( + AlertDialog( + title: const Text("Allowed columns"), + content: Obx( + () => Wrap( + children: [ + ElevatedButton( + onPressed: () { + for (final column in tableColumns) { + selectedColumns[column] = !selectedColumns[column]!; + } + }, + child: const Text("Swap all"), + ), + ...selectedColumns.entries.map((e) { + return CheckboxListTile( + title: Text(e.key), + value: e.value, + onChanged: (value) => selectedColumns[e.key] = value!, + ); + }), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => Get.back(result: true), + child: const Text("Ok"), + ), + ], + ), + ); + + if (confirm != true) return; + + if (selectedColumns.values.every((e) => !e)) { + await Get.dialog( + AlertDialog( + title: const Text("Error"), + content: const Text("At least one column must be selected"), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text("Ok"), + ), + ], + ), + ); + return; + } + + try { + Response resp = + await ApiController.to.apiClient.updateItemInTable( + tableName: "table_access", + itemUpdate: ItemUpdate( + item: { + "allowed_columns": selectedColumns.keys + .where((k) => selectedColumns[k]!) + .join(","), + }, + oldItem: { + "user_group_id": group.id, + "table_name": table.tableName, + }, + ), + ); + + final respData = resp.data; + if (respData == null) { + throw Exception("No data in response"); + } + + refreshTableAccess(table); + } on DioError catch (e) { + final respData = e.response?.data; + if (respData != null) { + Get.snackbar( + "Error trying to update tables access", + "${respData['error']}", + ); + } else { + Get.snackbar( + "Error trying to update tables access", + "$e", + ); + } + } catch (e) { + Get.snackbar( + "Error trying to update tables access", + "$e", + ); + } + } +} + +class GroupACLDialog extends GetView { + const GroupACLDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Group ACL'), + content: Obx( + () => DataTable2( + columns: const [ + DataColumn2(label: Text('Table'), size: ColumnSize.L), + DataColumn2(label: Text('Read'), size: ColumnSize.S), + DataColumn2(label: Text('Write'), size: ColumnSize.S), + DataColumn2(label: Text('Allowed columns'), size: ColumnSize.S), + ], + empty: const Text("No tables"), + rows: controller.access.entries.map((e) { + final table = controller.tables.firstWhere( + (element) => element.tableId == e.key, + ); + final tableAccess = e.value; + final read = tableAccess == TableAccess.read || + tableAccess == TableAccess.readWrite; + final write = tableAccess == TableAccess.write || + tableAccess == TableAccess.readWrite; + return DataRow(cells: [ + DataCell(Text(table.tableName.pascalCase)), + DataCell(Checkbox( + value: read, + onChanged: (value) => controller.updateTableAccess( + table, + read: value ?? false, + write: write, + ), + )), + DataCell(Checkbox( + value: write, + onChanged: (value) => controller.updateTableAccess( + table, + read: read, + write: value ?? false, + ), + )), + controller.allowedColumns[table.tableId] == null + ? DataCell.empty + : DataCell( + Text(controller.allowedColumns[table.tableId]!), + onTap: () => controller.changeAllowedColumns(table), + ), + ]); + }).toList(growable: false), + ).constrained(width: Get.width * 0.9, height: Get.height * 0.9), + ), + actions: [ + TextButton( + onPressed: () { + Get.back(); + }, + child: const Text('Close'), + ), + ], + ); + } + + static Future show(GroupDefinition group) async { + Get.lazyPut(() => GroupACLController(group)); + + await Get.dialog( + const GroupACLDialog(), + barrierDismissible: false, + ); + + Get.delete(); + } +} diff --git a/lib/pages/home_panels/users_list_panel.dart b/lib/pages/home_panels/users_list_panel.dart index 9e8f73c..161ad6f 100644 --- a/lib/pages/home_panels/users_list_panel.dart +++ b/lib/pages/home_panels/users_list_panel.dart @@ -9,6 +9,7 @@ import 'package:tuuli_app/api_controller.dart'; import 'package:tuuli_app/models/group_definition.dart'; import 'package:tuuli_app/models/user_definition.dart'; import 'package:tuuli_app/models/user_in_group_definition.dart'; +import 'package:tuuli_app/pages/dialogs/group_acl_dialog.dart'; enum UserListPanelTab { users, @@ -651,6 +652,10 @@ class UserListPanelController extends GetxController { ); } } + + Future changeGroupSecurity(GroupDefinition group) async { + await GroupACLDialog.show(group); + } } class UsersListPanel extends GetView { @@ -840,6 +845,11 @@ class UsersListPanel extends GetView { icon: const Icon(Icons.delete_forever), onPressed: () => controller.deleteGroup(group), ), + if (group.id != 2) + IconButton( + icon: const Icon(Icons.security), + onPressed: () => controller.changeGroupSecurity(group), + ), ], ), subtitle: Text( diff --git a/pubspec.lock b/pubspec.lock index c4794f3..bb0d9b6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -500,10 +500,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "3e58242edc02624f2c712e3f8bea88e0e341c4ae1abd3a6ff661318a3aefd829" + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" url: "https://pub.dev" source: hosted - version: "2.0.26" + version: "2.0.27" path_provider_foundation: dependency: transitive description: @@ -612,10 +612,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "5d7b3bd0400bdd0c03e59a3d3d5314651141a145b58196cd9018b12a2adc0c1b" + sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" shared_preferences_foundation: dependency: transitive description: @@ -786,7 +786,7 @@ packages: description: path: "." ref: master - resolved-ref: ab60426db27a2441107e529f527df0e502dae104 + resolved-ref: "116030611798bdb81a19c2504f1ad7adb6547725" url: "https://glab.nuark.xyz/nuark/tuuli_api.git" source: git version: "1.0.1"