Revolution #1

Merged
nuark merged 28 commits from revolution into master 2023-05-08 08:50:16 +03:00
4 changed files with 445 additions and 5 deletions
Showing only changes of commit 2d812b20c4 - Show all commits

View 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;
}
}
}

View 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>();
}
}

View file

@ -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(

View file

@ -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"