To test - role and switch instance

This commit is contained in:
Thomas Fransolet 2026-03-13 17:40:55 +01:00
parent e8d899310e
commit b7bd8588fe
10 changed files with 747 additions and 22 deletions

View File

@ -15,6 +15,7 @@ class ManagerAppContext with ChangeNotifier{
ConfigurationDTO? selectedConfiguration; ConfigurationDTO? selectedConfiguration;
SectionDTO? selectedSection; SectionDTO? selectedSection;
bool? isLoading = false; bool? isLoading = false;
UserRole? role;
ManagerAppContext({this.email, this.accessToken}); ManagerAppContext({this.email, this.accessToken});

View File

@ -5,17 +5,19 @@ class Session {
String? token; String? token;
String? password; String? password;
String? instanceId; String? instanceId;
int? role;
Session({required this.rememberMe, this.host, this.email, this.token, this.password, this.instanceId}); Session({required this.rememberMe, this.host, this.email, this.token, this.password, this.instanceId, this.role});
factory Session.fromJson(Map<String, dynamic> json) { factory Session.fromJson(Map<String, dynamic> json) {
return new Session( return new Session(
rememberMe: json['rememberMe'] as bool, rememberMe: json['rememberMe'] as bool,
host: json['host'] as String, host: json['host'] as String?,
email: json['email'] as String, email: json['email'] as String?,
token: json['token'] as String, token: json['token'] as String?,
password: json['password'] as String, password: json['password'] as String?,
instanceId: json['instanceId'] as String, instanceId: json['instanceId'] as String?,
role: json['role'] as int?,
); );
} }
@ -26,12 +28,13 @@ class Session {
'email': email, 'email': email,
'token': token, 'token': token,
'password': password, 'password': password,
'instanceId': instanceId 'instanceId': instanceId,
'role': role,
}; };
} }
@override @override
String toString() { String toString() {
return '{rememberMe: $rememberMe, host: $host, email: $email, token: $token, password: $password, instanceId: $instanceId}'; return '{rememberMe: $rememberMe, host: $host, email: $email, token: $token, password: $password, instanceId: $instanceId, role: $role}';
} }
} }

View File

@ -0,0 +1,246 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:manager_app/Models/managerContext.dart';
import 'package:manager_app/app_context.dart';
import 'package:manager_app/constants.dart';
import 'package:manager_api_new/api.dart';
import 'package:provider/provider.dart';
class ApiKeysScreen extends StatefulWidget {
const ApiKeysScreen({Key? key}) : super(key: key);
@override
_ApiKeysScreenState createState() => _ApiKeysScreenState();
}
class _ApiKeysScreenState extends State<ApiKeysScreen> {
List<Map<String, dynamic>> _keys = [];
bool _loading = true;
static String _appTypeName(int? v) {
switch (v) {
case 0: return 'VisitApp';
case 1: return 'TabletApp';
default: return 'Other';
}
}
Future<void> _loadKeys(ManagerAppContext ctx) async {
try {
final response = await ctx.clientAPI!.apiKeyApi!.apiKeyGetApiKeysWithHttpInfo();
if (response.statusCode == 200) {
final List<dynamic> json = jsonDecode(utf8.decode(response.bodyBytes));
setState(() {
_keys = json.cast<Map<String, dynamic>>();
_loading = false;
});
}
} catch (e) {
setState(() => _loading = false);
}
}
Future<String?> _createKey(ManagerAppContext ctx, String name, ApiKeyAppType appType) async {
final response = await ctx.clientAPI!.apiKeyApi!.apiKeyCreateApiKeyWithHttpInfo(
CreateApiKeyRequest(name: name, appType: appType),
);
if (response.statusCode == 200 || response.statusCode == 201) {
final Map<String, dynamic> json = jsonDecode(utf8.decode(response.bodyBytes));
await _loadKeys(ctx);
return json['key'] as String?;
}
return null;
}
Future<void> _revokeKey(ManagerAppContext ctx, String id) async {
await ctx.clientAPI!.apiKeyApi!.apiKeyRevokeApiKey(id);
await _loadKeys(ctx);
}
void _showCreateDialog(BuildContext context, ManagerAppContext ctx) {
final nameCtrl = TextEditingController();
ApiKeyAppType selectedType = ApiKeyAppType.number0;
showDialog(
context: context,
builder: (_) => StatefulBuilder(builder: (ctx2, setLocal) {
return AlertDialog(
title: const Text('Créer une clé API'),
content: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(controller: nameCtrl, decoration: const InputDecoration(labelText: 'Nom')),
const SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Type d\'application', style: TextStyle(fontSize: 12, color: Colors.grey)),
DropdownButton<ApiKeyAppType>(
value: selectedType,
isExpanded: true,
items: ApiKeyAppType.values
.map((t) => DropdownMenuItem(value: t, child: Text(_appTypeName(t.value))))
.toList(),
onChanged: (v) => setLocal(() => selectedType = v!),
),
],
),
]),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx2), child: const Text('Annuler')),
ElevatedButton(
onPressed: () async {
Navigator.pop(ctx2);
final plainKey = await _createKey(ctx, nameCtrl.text, selectedType);
if (plainKey != null && context.mounted) {
_showPlainKeyDialog(context, plainKey);
}
},
child: const Text('Créer'),
),
],
);
}),
);
}
void _showPlainKeyDialog(BuildContext context, String plainKey) {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => AlertDialog(
title: const Text('Clé API créée'),
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text(
'Copiez cette clé maintenant — elle ne sera plus affichée.',
style: TextStyle(color: Colors.orange, fontWeight: FontWeight.w600),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.grey.shade300),
),
child: Row(children: [
Expanded(child: SelectableText(plainKey, style: const TextStyle(fontFamily: 'monospace', fontSize: 13))),
IconButton(
icon: const Icon(Icons.copy, size: 18),
tooltip: 'Copier',
onPressed: () => Clipboard.setData(ClipboardData(text: plainKey)),
),
]),
),
]),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('J\'ai copié la clé'),
),
],
),
);
}
void _confirmRevoke(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> key) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Révoquer la clé API'),
content: Text('Révoquer « ${key['name']} » ? Les apps utilisant cette clé perdront l\'accès.'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Annuler')),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () async {
Navigator.pop(context);
await _revokeKey(ctx, key['id'] as String);
},
child: const Text('Révoquer', style: TextStyle(color: Colors.white)),
),
],
),
);
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final ctx = Provider.of<AppContext>(context, listen: false).getContext() as ManagerAppContext;
_loadKeys(ctx);
});
}
@override
Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context);
final managerCtx = appContext.getContext() as ManagerAppContext;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Clés API', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: kPrimaryColor)),
ElevatedButton.icon(
onPressed: () => _showCreateDialog(context, managerCtx),
icon: const Icon(Icons.add),
label: const Text('Créer une clé'),
),
],
),
const SizedBox(height: 16),
if (_loading)
const Center(child: CircularProgressIndicator())
else if (_keys.isEmpty)
const Center(child: Text('Aucune clé API'))
else
Expanded(
child: SingleChildScrollView(
child: DataTable(
columns: const [
DataColumn(label: Text('Nom')),
DataColumn(label: Text('Type')),
DataColumn(label: Text('Créée le')),
DataColumn(label: Text('Statut')),
DataColumn(label: Text('Actions')),
],
rows: _keys.map((key) {
final isActive = key['isActive'] as bool? ?? false;
final dateRaw = key['dateCreation'] as String?;
final date = dateRaw != null
? DateTime.tryParse(dateRaw)?.toLocal()
: null;
final dateStr = date != null
? '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'
: '';
return DataRow(cells: [
DataCell(Text(key['name'] as String? ?? '')),
DataCell(Text(_appTypeName(key['appType'] as int?))),
DataCell(Text(dateStr)),
DataCell(Chip(
label: Text(isActive ? 'Active' : 'Révoquée',
style: TextStyle(color: isActive ? Colors.green.shade700 : Colors.red.shade700, fontSize: 12)),
backgroundColor: isActive ? Colors.green.shade50 : Colors.red.shade50,
side: BorderSide(color: isActive ? Colors.green.shade200 : Colors.red.shade200),
)),
DataCell(isActive
? IconButton(
icon: const Icon(Icons.block, color: Colors.red),
tooltip: 'Révoquer',
onPressed: () => _confirmRevoke(context, managerCtx, key),
)
: const SizedBox()),
]);
}).toList(),
),
),
),
],
);
}
}

View File

@ -8,11 +8,13 @@ import 'package:go_router/go_router.dart';
import 'package:manager_app/Models/managerContext.dart'; import 'package:manager_app/Models/managerContext.dart';
import 'package:manager_app/Models/menu.dart'; import 'package:manager_app/Models/menu.dart';
import 'package:manager_app/Models/menuSection.dart'; import 'package:manager_app/Models/menuSection.dart';
import 'package:manager_app/Screens/ApiKeys/api_keys_screen.dart';
import 'package:manager_app/Screens/Configurations/configurations_screen.dart'; import 'package:manager_app/Screens/Configurations/configurations_screen.dart';
import 'package:manager_app/Screens/Kiosk_devices/kiosk_screen.dart'; import 'package:manager_app/Screens/Kiosk_devices/kiosk_screen.dart';
import 'package:manager_app/Screens/Resources/resources_screen.dart'; import 'package:manager_app/Screens/Resources/resources_screen.dart';
import 'package:manager_app/Screens/Statistics/statistics_screen.dart'; import 'package:manager_app/Screens/Statistics/statistics_screen.dart';
import 'package:manager_app/Screens/Applications/app_configuration_link_screen.dart'; import 'package:manager_app/Screens/Applications/app_configuration_link_screen.dart';
import 'package:manager_app/Screens/Users/users_screen.dart';
import 'package:manager_app/app_context.dart'; import 'package:manager_app/app_context.dart';
import 'package:manager_app/constants.dart'; import 'package:manager_app/constants.dart';
import 'package:manager_app/main.dart'; import 'package:manager_app/main.dart';
@ -55,6 +57,7 @@ class _MainScreenState extends State<MainScreen> {
menu.sections!.add(MenuSection(name: "Statistiques", type: "statistics", menuId: 7, subMenu: [])); menu.sections!.add(MenuSection(name: "Statistiques", type: "statistics", menuId: 7, subMenu: []));
} }
if(currentPosition.value == null) { if(currentPosition.value == null) {
if (widget.instance.isMobile!) { if (widget.instance.isMobile!) {
currentPosition.value = 1; currentPosition.value = 1;
@ -72,6 +75,72 @@ class _MainScreenState extends State<MainScreen> {
selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance); selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance);
} }
Future<void> _showSwitchInstanceDialog(BuildContext context, ManagerAppContext managerCtx) async {
List<Instance>? instances;
try {
instances = await managerCtx.clientAPI!.instanceApi!.instanceGet();
} catch (_) {}
if (!context.mounted) return;
final instanceList = instances ?? [];
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Changer d\'instance'),
content: SizedBox(
width: 400,
child: instanceList.isEmpty
? const Text('Aucune instance trouvée')
: ListView.builder(
shrinkWrap: true,
itemCount: instanceList.length,
itemBuilder: (_, i) {
final inst = instanceList[i];
final isCurrent = inst.id == managerCtx.instanceId;
return ListTile(
leading: Icon(
Icons.business,
color: isCurrent ? kPrimaryColor : kBodyTextColor,
),
title: Text(
inst.name,
style: TextStyle(
color: isCurrent ? kPrimaryColor : kBodyTextColor,
fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal,
),
),
trailing: isCurrent ? const Icon(Icons.check, color: Colors.green) : null,
onTap: isCurrent
? null
: () async {
Navigator.pop(context);
final newInstance = await managerCtx.clientAPI!.instanceApi!.instanceGetDetail(inst.id);
if (newInstance != null && context.mounted) {
setState(() {
managerCtx.instanceId = newInstance.id;
managerCtx.instanceDTO = newInstance;
menu.sections = [];
});
final view = newInstance.isMobile! ? 'mobile'
: newInstance.isTablet! ? 'kiosk'
: newInstance.isWeb! ? 'web'
: 'vr';
context.go('/main/$view');
}
},
);
},
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Fermer')),
],
),
);
}
Widget buildMenu(BuildContext context, AppContext appContext, ManagerAppContext managerAppContext, bool isDrawer) { Widget buildMenu(BuildContext context, AppContext appContext, ManagerAppContext managerAppContext, bool isDrawer) {
return Container( return Container(
width: isDrawer ? null : 250, // fixed width on sidebar, null on drawer for full width width: isDrawer ? null : 250, // fixed width on sidebar, null on drawer for full width
@ -196,7 +265,7 @@ class _MainScreenState extends State<MainScreen> {
), ),
), ),
), ),
// Footer: Email + Logout button // Footer: Email + Switch instance (SuperAdmin) + Logout
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
@ -206,6 +275,12 @@ class _MainScreenState extends State<MainScreen> {
style: TextStyle(color: kBodyTextColor, fontSize: 16, fontWeight: FontWeight.w300, fontFamily: "Helvetica"), style: TextStyle(color: kBodyTextColor, fontSize: 16, fontWeight: FontWeight.w300, fontFamily: "Helvetica"),
maxLines: 1, maxLines: 1,
), ),
if ((appContext.getContext() as ManagerAppContext).role == UserRole.SuperAdmin)
IconButton(
icon: Icon(Icons.swap_horiz, color: kPrimaryColor),
tooltip: 'Changer d\'instance',
onPressed: () => _showSwitchInstanceDialog(context, appContext.getContext() as ManagerAppContext),
),
IconButton( IconButton(
icon: Icon(Icons.logout, color: kPrimaryColor), icon: Icon(Icons.logout, color: kPrimaryColor),
onPressed: () async { onPressed: () async {
@ -238,6 +313,16 @@ class _MainScreenState extends State<MainScreen> {
final appContext = Provider.of<AppContext>(context); final appContext = Provider.of<AppContext>(context);
ManagerAppContext managerAppContext = appContext.getContext(); ManagerAppContext managerAppContext = appContext.getContext();
// Synchronise les items de menu sensibles au rôle à chaque rebuild
final role = managerAppContext.role;
final hasAdminItems = menu.sections!.any((s) => s.menuId == 8);
if (role != null && role.value <= 1 && !hasAdminItems) {
menu.sections!.add(MenuSection(name: "Utilisateurs", type: "users", menuId: 8, subMenu: []));
menu.sections!.add(MenuSection(name: "Clés API", type: "apikeys", menuId: 9, subMenu: []));
} else if ((role == null || role.value > 1) && hasAdminItems) {
menu.sections!.removeWhere((s) => s.menuId == 8 || s.menuId == 9);
}
Size size = MediaQuery.of(context).size; Size size = MediaQuery.of(context).size;
bool isMobile = size.width < 850; bool isMobile = size.width < 850;
@ -312,6 +397,12 @@ class _MainScreenState extends State<MainScreen> {
case "statistics": case "statistics":
currentPosition = 7; currentPosition = 7;
break; break;
case "users":
currentPosition = 8;
break;
case "apikeys":
currentPosition = 9;
break;
} }
} }
@ -372,6 +463,16 @@ class _MainScreenState extends State<MainScreen> {
padding: EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: StatisticsScreen() child: StatisticsScreen()
); );
case 'users':
return const Padding(
padding: EdgeInsets.all(8.0),
child: UsersScreen()
);
case 'apikeys':
return const Padding(
padding: EdgeInsets.all(8.0),
child: ApiKeysScreen()
);
default: default:
return Text('Hellow default'); return Text('Hellow default');
} }

View File

@ -0,0 +1,270 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:manager_app/Models/managerContext.dart';
import 'package:manager_app/app_context.dart';
import 'package:manager_app/constants.dart';
import 'package:provider/provider.dart';
class UsersScreen extends StatefulWidget {
const UsersScreen({Key? key}) : super(key: key);
@override
_UsersScreenState createState() => _UsersScreenState();
}
class _UsersScreenState extends State<UsersScreen> {
List<Map<String, dynamic>> _users = [];
bool _loading = true;
static String _roleName(int? v) {
switch (v) {
case 0: return 'SuperAdmin';
case 1: return 'InstanceAdmin';
case 2: return 'ContentEditor';
case 3: return 'Viewer';
default: return '';
}
}
// Roles the caller is allowed to assign (can't assign higher than own role)
List<int> _allowedRoles(int callerRoleValue) =>
[0, 1, 2, 3].where((r) => r >= callerRoleValue).toList();
Future<void> _loadUsers(ManagerAppContext ctx) async {
try {
final response = await ctx.clientAPI!.userApi!.userGetWithHttpInfo();
if (response.statusCode == 200) {
final List<dynamic> json = jsonDecode(utf8.decode(response.bodyBytes));
setState(() {
_users = json.cast<Map<String, dynamic>>();
_loading = false;
});
}
} catch (e) {
setState(() => _loading = false);
}
}
Future<void> _createUser(ManagerAppContext ctx, String email,
String firstName, String lastName, String password, int roleValue) async {
final body = {
'email': email,
'firstName': firstName,
'lastName': lastName,
'password': password,
'role': roleValue,
};
await ctx.clientAPI!.apiApi!.invokeAPI(
'/api/User', 'POST', [], body, {}, {}, 'application/json');
await _loadUsers(ctx);
}
Future<void> _updateUser(ManagerAppContext ctx, String id, String firstName,
String lastName, int roleValue) async {
final body = {
'id': id,
'firstName': firstName,
'lastName': lastName,
'role': roleValue,
};
await ctx.clientAPI!.apiApi!.invokeAPI(
'/api/User', 'PUT', [], body, {}, {}, 'application/json');
await _loadUsers(ctx);
}
Future<void> _deleteUser(ManagerAppContext ctx, String id) async {
await ctx.clientAPI!.userApi!.userDeleteUser(id);
await _loadUsers(ctx);
}
void _showCreateDialog(BuildContext context, ManagerAppContext ctx) {
final callerRole = ctx.role?.value ?? 2;
final emailCtrl = TextEditingController();
final firstCtrl = TextEditingController();
final lastCtrl = TextEditingController();
final passCtrl = TextEditingController();
int selectedRole = callerRole; // default: same as caller
showDialog(
context: context,
builder: (_) => StatefulBuilder(builder: (ctx2, setLocal) {
return AlertDialog(
title: const Text('Créer un utilisateur'),
content: SingleChildScrollView(
child: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(controller: emailCtrl, decoration: const InputDecoration(labelText: 'Email')),
TextField(controller: firstCtrl, decoration: const InputDecoration(labelText: 'Prénom')),
TextField(controller: lastCtrl, decoration: const InputDecoration(labelText: 'Nom')),
TextField(controller: passCtrl, obscureText: true, decoration: const InputDecoration(labelText: 'Mot de passe')),
const SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Rôle', style: TextStyle(fontSize: 12, color: Colors.grey)),
DropdownButton<int>(
value: selectedRole,
isExpanded: true,
items: _allowedRoles(callerRole)
.map((r) => DropdownMenuItem(value: r, child: Text(_roleName(r))))
.toList(),
onChanged: (v) => setLocal(() => selectedRole = v!),
),
],
),
]),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx2), child: const Text('Annuler')),
ElevatedButton(
onPressed: () async {
Navigator.pop(ctx2);
await _createUser(ctx, emailCtrl.text, firstCtrl.text,
lastCtrl.text, passCtrl.text, selectedRole);
},
child: const Text('Créer'),
),
],
);
}),
);
}
void _showEditDialog(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> user) {
final callerRole = ctx.role?.value ?? 2;
final firstCtrl = TextEditingController(text: user['firstName'] as String? ?? '');
final lastCtrl = TextEditingController(text: user['lastName'] as String? ?? '');
int selectedRole = (user['role'] as int?) ?? callerRole;
showDialog(
context: context,
builder: (_) => StatefulBuilder(builder: (ctx2, setLocal) {
return AlertDialog(
title: const Text('Modifier l\'utilisateur'),
content: SingleChildScrollView(
child: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(controller: firstCtrl, decoration: const InputDecoration(labelText: 'Prénom')),
TextField(controller: lastCtrl, decoration: const InputDecoration(labelText: 'Nom')),
const SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Rôle', style: TextStyle(fontSize: 12, color: Colors.grey)),
DropdownButton<int>(
value: selectedRole,
isExpanded: true,
items: _allowedRoles(callerRole)
.map((r) => DropdownMenuItem(value: r, child: Text(_roleName(r))))
.toList(),
onChanged: (v) => setLocal(() => selectedRole = v!),
),
],
),
]),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx2), child: const Text('Annuler')),
ElevatedButton(
onPressed: () async {
Navigator.pop(ctx2);
await _updateUser(ctx, user['id'] as String, firstCtrl.text,
lastCtrl.text, selectedRole);
},
child: const Text('Enregistrer'),
),
],
);
}),
);
}
void _confirmDelete(BuildContext context, ManagerAppContext ctx, Map<String, dynamic> user) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Supprimer l\'utilisateur'),
content: Text('Supprimer ${user['email']} ?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Annuler')),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () async {
Navigator.pop(context);
await _deleteUser(ctx, user['id'] as String);
},
child: const Text('Supprimer', style: TextStyle(color: Colors.white)),
),
],
),
);
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final ctx = Provider.of<AppContext>(context, listen: false).getContext() as ManagerAppContext;
_loadUsers(ctx);
});
}
@override
Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context);
final managerCtx = appContext.getContext() as ManagerAppContext;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Utilisateurs', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: kPrimaryColor)),
ElevatedButton.icon(
onPressed: () => _showCreateDialog(context, managerCtx),
icon: const Icon(Icons.add),
label: const Text('Créer un utilisateur'),
),
],
),
const SizedBox(height: 16),
if (_loading)
const Center(child: CircularProgressIndicator())
else if (_users.isEmpty)
const Center(child: Text('Aucun utilisateur'))
else
Expanded(
child: SingleChildScrollView(
child: DataTable(
columns: const [
DataColumn(label: Text('Email')),
DataColumn(label: Text('Prénom')),
DataColumn(label: Text('Nom')),
DataColumn(label: Text('Rôle')),
DataColumn(label: Text('Actions')),
],
rows: _users.map((user) {
return DataRow(cells: [
DataCell(Text(user['email'] as String? ?? '')),
DataCell(Text(user['firstName'] as String? ?? '')),
DataCell(Text(user['lastName'] as String? ?? '')),
DataCell(Text(_roleName(user['role'] as int?))),
DataCell(Row(children: [
IconButton(
icon: Icon(Icons.edit, color: kPrimaryColor),
onPressed: () => _showEditDialog(context, managerCtx, user),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _confirmDelete(context, managerCtx, user),
),
])),
]);
}).toList(),
),
),
),
],
);
}
}

View File

@ -67,6 +67,7 @@ class _LoginScreenState extends State<LoginScreen> {
var accessToken = this.token; var accessToken = this.token;
var instanceId = this.instanceId; var instanceId = this.instanceId;
var pinCode = this.pinCode; var pinCode = this.pinCode;
UserRole? userRole;
/*print("accessToken"); /*print("accessToken");
print(accessToken);*/ print(accessToken);*/
if(accessToken == null) { if(accessToken == null) {
@ -84,6 +85,7 @@ class _LoginScreenState extends State<LoginScreen> {
accessToken = token.accessToken!; accessToken = token.accessToken!;
instanceId = token.instanceId!; instanceId = token.instanceId!;
pinCode = token.pinCode; pinCode = token.pinCode;
userRole = token.role;
showNotification( showNotification(
kSuccess, kWhite, 'Connexion réussie', context, null); kSuccess, kWhite, 'Connexion réussie', context, null);
@ -122,13 +124,14 @@ class _LoginScreenState extends State<LoginScreen> {
managerAppContext.instanceId = instanceId; managerAppContext.instanceId = instanceId;
managerAppContext.pinCode = pinCode; managerAppContext.pinCode = pinCode;
managerAppContext.accessToken = accessToken; managerAppContext.accessToken = accessToken;
managerAppContext.role = userRole;
managerAppContext.clientAPI = clientAPI; managerAppContext.clientAPI = clientAPI;
setAccessToken(accessToken); setAccessToken(accessToken);
InstanceDTO? instance = await managerAppContext.clientAPI!.instanceApi!.instanceGetDetail(managerAppContext.instanceId!); InstanceDTO? instance = await managerAppContext.clientAPI!.instanceApi!.instanceGetDetail(managerAppContext.instanceId!);
managerAppContext.instanceDTO = instance; managerAppContext.instanceDTO = instance;
FileHelper().writeSession(new Session(rememberMe: true, instanceId: instanceId, host: host, token: accessToken, password: password, email: email)); FileHelper().writeSession(new Session(rememberMe: true, instanceId: instanceId, host: host, token: accessToken, password: password, email: email, role: userRole?.value));
appContext.setContext(managerAppContext); appContext.setContext(managerAppContext);
if(instance!.isMobile!) { if(instance!.isMobile!) {

View File

@ -44,6 +44,9 @@ class Client {
StatsApi? _statsApi; StatsApi? _statsApi;
StatsApi? get statsApi => _statsApi; StatsApi? get statsApi => _statsApi;
ApiKeyApi? _apiKeyApi;
ApiKeyApi? get apiKeyApi => _apiKeyApi;
Client(String path) { Client(String path) {
_apiClient = ApiClient(basePath: path); _apiClient = ApiClient(basePath: path);
//basePath: "https://192.168.31.140"); //basePath: "https://192.168.31.140");
@ -62,5 +65,6 @@ class Client {
_sectionAgendaApi = SectionAgendaApi(_apiClient); _sectionAgendaApi = SectionAgendaApi(_apiClient);
_sectionEventApi = SectionEventApi(_apiClient); _sectionEventApi = SectionEventApi(_apiClient);
_statsApi = StatsApi(_apiClient); _statsApi = StatsApi(_apiClient);
_apiKeyApi = ApiKeyApi(_apiClient);
} }
} }

View File

@ -193,6 +193,7 @@ Future<InstanceDTO?> getInstanceInfo(ManagerAppContext managerAppContext) async
managerAppContext.host = managerAppContext.host ?? session.host; managerAppContext.host = managerAppContext.host ?? session.host;
managerAppContext.accessToken = managerAppContext.accessToken ?? session.token; managerAppContext.accessToken = managerAppContext.accessToken ?? session.token;
managerAppContext.email = managerAppContext.email ?? session.email; managerAppContext.email = managerAppContext.email ?? session.email;
managerAppContext.role = managerAppContext.role ?? UserRole.fromJson(session.role);
if(session.instanceId != null) { if(session.instanceId != null) {
if(managerAppContext.clientAPI == null) { if(managerAppContext.clientAPI == null) {

View File

@ -23,17 +23,17 @@ class UserRole {
int toJson() => value; int toJson() => value;
static const number0 = UserRole._(0); static const SuperAdmin = UserRole._(0);
static const number1 = UserRole._(1); static const InstanceAdmin = UserRole._(1);
static const number2 = UserRole._(2); static const ContentEditor = UserRole._(2);
static const number3 = UserRole._(3); static const Viewer = UserRole._(3);
/// List of all possible values in this [enum][UserRole]. /// List of all possible values in this [enum][UserRole].
static const values = <UserRole>[ static const values = <UserRole>[
number0, SuperAdmin,
number1, InstanceAdmin,
number2, ContentEditor,
number3, Viewer,
]; ];
static UserRole? fromJson(dynamic value) => static UserRole? fromJson(dynamic value) =>
@ -76,15 +76,27 @@ class UserRoleTypeTransformer {
/// and users are still using an old app with the old code. /// and users are still using an old app with the old code.
UserRole? decode(dynamic data, {bool allowNull = true}) { UserRole? decode(dynamic data, {bool allowNull = true}) {
if (data != null) { if (data != null) {
// Handle string values (backend uses JsonStringEnumConverter)
if (data is String) {
switch (data) {
case 'SuperAdmin': return UserRole.SuperAdmin;
case 'InstanceAdmin': return UserRole.InstanceAdmin;
case 'ContentEditor': return UserRole.ContentEditor;
case 'Viewer': return UserRole.Viewer;
default:
if (!allowNull) throw ArgumentError('Unknown enum value to decode: $data');
}
return null;
}
switch (data) { switch (data) {
case 0: case 0:
return UserRole.number0; return UserRole.SuperAdmin;
case 1: case 1:
return UserRole.number1; return UserRole.InstanceAdmin;
case 2: case 2:
return UserRole.number2; return UserRole.ContentEditor;
case 3: case 3:
return UserRole.number3; return UserRole.Viewer;
default: default:
if (!allowNull) { if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data'); throw ArgumentError('Unknown enum value to decode: $data');

84
verify_dto.dart Normal file
View File

@ -0,0 +1,84 @@
import 'dart:convert';
// Mocking the behavior of the DTOs
class TranslationDTO {
String? language;
String? value;
TranslationDTO({this.language, this.value});
Map<String, dynamic> toJson() => {'language': language, 'value': value};
static TranslationDTO fromJson(Map<String, dynamic> json) =>
TranslationDTO(language: json['language'], value: json['value']);
}
class EventAddressDTOGeometry {
String? type;
Object? coordinates;
EventAddressDTOGeometry({this.type, this.coordinates});
Map<String, dynamic> toJson() => {'type': type, 'coordinates': coordinates};
}
class GuidedStepDTO {
List<TranslationDTO>? title;
EventAddressDTOGeometry? geometry;
GuidedStepDTO({this.title, this.geometry});
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.title != null) {
json['title'] = this.title!.map((v) => v.toJson()).toList();
}
if (this.geometry != null) {
json['geometry'] = this.geometry; // The reported bug: no .toJson() call
}
return json;
}
static GuidedStepDTO fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return GuidedStepDTO(
title: json['title'] != null
? (json['title'] as List)
.map((i) => TranslationDTO.fromJson(i))
.toList()
: null,
geometry: json['geometry'] is Map
? EventAddressDTOGeometry(
type: json['geometry']['type'],
coordinates: json['geometry']['coordinates'],
)
: (json['geometry'] is EventAddressDTOGeometry
? json['geometry']
: null),
);
}
return GuidedStepDTO();
}
}
void main() {
var geo = EventAddressDTOGeometry(type: 'Point', coordinates: [1.0, 2.0]);
var step = GuidedStepDTO(
title: [TranslationDTO(language: 'FR', value: 'Test')],
geometry: geo,
);
print('Original geometry: ${step.geometry.runtimeType}');
var jsonMap = step.toJson();
print('Value in JSON map for geometry: ${jsonMap['geometry'].runtimeType}');
// This is what I do in showNewOrUpdateGuidedStep.dart
var clonedStep = GuidedStepDTO.fromJson(jsonMap);
print('Cloned geometry: ${clonedStep.geometry.runtimeType}');
// What happens if we actually JSON encode/decode?
var encoded = jsonEncode(jsonMap);
var decoded = jsonDecode(encoded);
print('Decoded value for geometry: ${decoded['geometry'].runtimeType}');
var clonedStepFromJSON = GuidedStepDTO.fromJson(decoded);
print(
'Cloned from actual JSON geometry: ${clonedStepFromJSON.geometry.runtimeType}');
}