Made possible updating security for user groups on tables
This commit is contained in:
parent
330216e37a
commit
2d812b20c4
4 changed files with 445 additions and 5 deletions
24
lib/models/table_access.dart
Normal file
24
lib/models/table_access.dart
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
406
lib/pages/dialogs/group_acl_dialog.dart
Normal file
406
lib/pages/dialogs/group_acl_dialog.dart
Normal file
|
|
@ -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 = <TableDefinition>[].obs;
|
||||||
|
List<TableDefinition> get tables => _tables.toList();
|
||||||
|
|
||||||
|
final _access = <String, TableAccess>{}.obs;
|
||||||
|
Map<String, TableAccess> get access => _access;
|
||||||
|
|
||||||
|
final _allowedColumns = <String, String?>{}.obs;
|
||||||
|
Map<String, String?> get allowedColumns => _allowedColumns;
|
||||||
|
|
||||||
|
Future<void> refreshData() async {
|
||||||
|
await refreshTables();
|
||||||
|
|
||||||
|
for (final table in tables) {
|
||||||
|
await refreshTableAccess(table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> 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<void> 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<void> 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<OkResponse> 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<void> 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 = <String, bool>{}.obs;
|
||||||
|
for (final column in tableColumns) {
|
||||||
|
selectedColumns[column] = currentlyAvailableColumns.contains(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
final confirm = await Get.dialog<bool>(
|
||||||
|
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<OkResponse> 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<GroupACLController> {
|
||||||
|
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<void> show(GroupDefinition group) async {
|
||||||
|
Get.lazyPut<GroupACLController>(() => GroupACLController(group));
|
||||||
|
|
||||||
|
await Get.dialog(
|
||||||
|
const GroupACLDialog(),
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
Get.delete<GroupACLController>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import 'package:tuuli_app/api_controller.dart';
|
||||||
import 'package:tuuli_app/models/group_definition.dart';
|
import 'package:tuuli_app/models/group_definition.dart';
|
||||||
import 'package:tuuli_app/models/user_definition.dart';
|
import 'package:tuuli_app/models/user_definition.dart';
|
||||||
import 'package:tuuli_app/models/user_in_group_definition.dart';
|
import 'package:tuuli_app/models/user_in_group_definition.dart';
|
||||||
|
import 'package:tuuli_app/pages/dialogs/group_acl_dialog.dart';
|
||||||
|
|
||||||
enum UserListPanelTab {
|
enum UserListPanelTab {
|
||||||
users,
|
users,
|
||||||
|
|
@ -651,6 +652,10 @@ class UserListPanelController extends GetxController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> changeGroupSecurity(GroupDefinition group) async {
|
||||||
|
await GroupACLDialog.show(group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UsersListPanel extends GetView<UserListPanelController> {
|
class UsersListPanel extends GetView<UserListPanelController> {
|
||||||
|
|
@ -840,6 +845,11 @@ class UsersListPanel extends GetView<UserListPanelController> {
|
||||||
icon: const Icon(Icons.delete_forever),
|
icon: const Icon(Icons.delete_forever),
|
||||||
onPressed: () => controller.deleteGroup(group),
|
onPressed: () => controller.deleteGroup(group),
|
||||||
),
|
),
|
||||||
|
if (group.id != 2)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.security),
|
||||||
|
onPressed: () => controller.changeGroupSecurity(group),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
|
|
|
||||||
10
pubspec.lock
10
pubspec.lock
|
|
@ -500,10 +500,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: "3e58242edc02624f2c712e3f8bea88e0e341c4ae1abd3a6ff661318a3aefd829"
|
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.26"
|
version: "2.0.27"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -612,10 +612,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: "5d7b3bd0400bdd0c03e59a3d3d5314651141a145b58196cd9018b12a2adc0c1b"
|
sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.4"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -786,7 +786,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: master
|
ref: master
|
||||||
resolved-ref: ab60426db27a2441107e529f527df0e502dae104
|
resolved-ref: "116030611798bdb81a19c2504f1ad7adb6547725"
|
||||||
url: "https://glab.nuark.xyz/nuark/tuuli_api.git"
|
url: "https://glab.nuark.xyz/nuark/tuuli_api.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue