diff --git a/lib/Models/managerContext.dart b/lib/Models/managerContext.dart index ea7ca4f..5f62546 100644 --- a/lib/Models/managerContext.dart +++ b/lib/Models/managerContext.dart @@ -15,6 +15,7 @@ class ManagerAppContext with ChangeNotifier{ ConfigurationDTO? selectedConfiguration; SectionDTO? selectedSection; bool? isLoading = false; + UserRole? role; ManagerAppContext({this.email, this.accessToken}); diff --git a/lib/Models/session.dart b/lib/Models/session.dart index 9b67980..696c6a2 100644 --- a/lib/Models/session.dart +++ b/lib/Models/session.dart @@ -5,17 +5,19 @@ class Session { String? token; String? password; 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 json) { return new Session( rememberMe: json['rememberMe'] as bool, - host: json['host'] as String, - email: json['email'] as String, - token: json['token'] as String, - password: json['password'] as String, - instanceId: json['instanceId'] as String, + host: json['host'] as String?, + email: json['email'] as String?, + token: json['token'] as String?, + password: json['password'] as String?, + instanceId: json['instanceId'] as String?, + role: json['role'] as int?, ); } @@ -26,12 +28,13 @@ class Session { 'email': email, 'token': token, 'password': password, - 'instanceId': instanceId + 'instanceId': instanceId, + 'role': role, }; } @override 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}'; } } \ No newline at end of file diff --git a/lib/Screens/ApiKeys/api_keys_screen.dart b/lib/Screens/ApiKeys/api_keys_screen.dart new file mode 100644 index 0000000..7ee3600 --- /dev/null +++ b/lib/Screens/ApiKeys/api_keys_screen.dart @@ -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 { + List> _keys = []; + bool _loading = true; + + static String _appTypeName(int? v) { + switch (v) { + case 0: return 'VisitApp'; + case 1: return 'TabletApp'; + default: return 'Other'; + } + } + + Future _loadKeys(ManagerAppContext ctx) async { + try { + final response = await ctx.clientAPI!.apiKeyApi!.apiKeyGetApiKeysWithHttpInfo(); + if (response.statusCode == 200) { + final List json = jsonDecode(utf8.decode(response.bodyBytes)); + setState(() { + _keys = json.cast>(); + _loading = false; + }); + } + } catch (e) { + setState(() => _loading = false); + } + } + + Future _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 json = jsonDecode(utf8.decode(response.bodyBytes)); + await _loadKeys(ctx); + return json['key'] as String?; + } + return null; + } + + Future _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( + 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 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(context, listen: false).getContext() as ManagerAppContext; + _loadKeys(ctx); + }); + } + + @override + Widget build(BuildContext context) { + final appContext = Provider.of(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(), + ), + ), + ), + ], + ); + } +} diff --git a/lib/Screens/Main/main_screen.dart b/lib/Screens/Main/main_screen.dart index 7d8cfee..1abdec8 100644 --- a/lib/Screens/Main/main_screen.dart +++ b/lib/Screens/Main/main_screen.dart @@ -8,11 +8,13 @@ import 'package:go_router/go_router.dart'; import 'package:manager_app/Models/managerContext.dart'; import 'package:manager_app/Models/menu.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/Kiosk_devices/kiosk_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/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/constants.dart'; import 'package:manager_app/main.dart'; @@ -55,6 +57,7 @@ class _MainScreenState extends State { menu.sections!.add(MenuSection(name: "Statistiques", type: "statistics", menuId: 7, subMenu: [])); } + if(currentPosition.value == null) { if (widget.instance.isMobile!) { currentPosition.value = 1; @@ -72,6 +75,72 @@ class _MainScreenState extends State { selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance); } + Future _showSwitchInstanceDialog(BuildContext context, ManagerAppContext managerCtx) async { + List? 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) { return Container( width: isDrawer ? null : 250, // fixed width on sidebar, null on drawer for full width @@ -196,7 +265,7 @@ class _MainScreenState extends State { ), ), ), - // Footer: Email + Logout button + // Footer: Email + Switch instance (SuperAdmin) + Logout Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -206,6 +275,12 @@ class _MainScreenState extends State { style: TextStyle(color: kBodyTextColor, fontSize: 16, fontWeight: FontWeight.w300, fontFamily: "Helvetica"), 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( icon: Icon(Icons.logout, color: kPrimaryColor), onPressed: () async { @@ -238,6 +313,16 @@ class _MainScreenState extends State { final appContext = Provider.of(context); 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; bool isMobile = size.width < 850; @@ -312,6 +397,12 @@ class _MainScreenState extends State { case "statistics": currentPosition = 7; break; + case "users": + currentPosition = 8; + break; + case "apikeys": + currentPosition = 9; + break; } } @@ -372,6 +463,16 @@ class _MainScreenState extends State { padding: EdgeInsets.all(8.0), 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: return Text('Hellow default'); } diff --git a/lib/Screens/Users/users_screen.dart b/lib/Screens/Users/users_screen.dart new file mode 100644 index 0000000..aaa2cd5 --- /dev/null +++ b/lib/Screens/Users/users_screen.dart @@ -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 { + List> _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 _allowedRoles(int callerRoleValue) => + [0, 1, 2, 3].where((r) => r >= callerRoleValue).toList(); + + Future _loadUsers(ManagerAppContext ctx) async { + try { + final response = await ctx.clientAPI!.userApi!.userGetWithHttpInfo(); + if (response.statusCode == 200) { + final List json = jsonDecode(utf8.decode(response.bodyBytes)); + setState(() { + _users = json.cast>(); + _loading = false; + }); + } + } catch (e) { + setState(() => _loading = false); + } + } + + Future _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 _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 _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( + 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 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( + 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 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(context, listen: false).getContext() as ManagerAppContext; + _loadUsers(ctx); + }); + } + + @override + Widget build(BuildContext context) { + final appContext = Provider.of(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(), + ), + ), + ), + ], + ); + } +} diff --git a/lib/Screens/login_screen.dart b/lib/Screens/login_screen.dart index 5766d90..75819e0 100644 --- a/lib/Screens/login_screen.dart +++ b/lib/Screens/login_screen.dart @@ -67,6 +67,7 @@ class _LoginScreenState extends State { var accessToken = this.token; var instanceId = this.instanceId; var pinCode = this.pinCode; + UserRole? userRole; /*print("accessToken"); print(accessToken);*/ if(accessToken == null) { @@ -84,6 +85,7 @@ class _LoginScreenState extends State { accessToken = token.accessToken!; instanceId = token.instanceId!; pinCode = token.pinCode; + userRole = token.role; showNotification( kSuccess, kWhite, 'Connexion réussie', context, null); @@ -122,13 +124,14 @@ class _LoginScreenState extends State { managerAppContext.instanceId = instanceId; managerAppContext.pinCode = pinCode; managerAppContext.accessToken = accessToken; + managerAppContext.role = userRole; managerAppContext.clientAPI = clientAPI; setAccessToken(accessToken); InstanceDTO? instance = await managerAppContext.clientAPI!.instanceApi!.instanceGetDetail(managerAppContext.instanceId!); 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); if(instance!.isMobile!) { diff --git a/lib/client.dart b/lib/client.dart index ba4ef27..57ab715 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -44,6 +44,9 @@ class Client { StatsApi? _statsApi; StatsApi? get statsApi => _statsApi; + ApiKeyApi? _apiKeyApi; + ApiKeyApi? get apiKeyApi => _apiKeyApi; + Client(String path) { _apiClient = ApiClient(basePath: path); //basePath: "https://192.168.31.140"); @@ -62,5 +65,6 @@ class Client { _sectionAgendaApi = SectionAgendaApi(_apiClient); _sectionEventApi = SectionEventApi(_apiClient); _statsApi = StatsApi(_apiClient); + _apiKeyApi = ApiKeyApi(_apiClient); } } diff --git a/lib/main.dart b/lib/main.dart index 33cd679..e191adc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -193,6 +193,7 @@ Future getInstanceInfo(ManagerAppContext managerAppContext) async managerAppContext.host = managerAppContext.host ?? session.host; managerAppContext.accessToken = managerAppContext.accessToken ?? session.token; managerAppContext.email = managerAppContext.email ?? session.email; + managerAppContext.role = managerAppContext.role ?? UserRole.fromJson(session.role); if(session.instanceId != null) { if(managerAppContext.clientAPI == null) { diff --git a/manager_api_new/lib/model/user_role.dart b/manager_api_new/lib/model/user_role.dart index b00777a..d8bb390 100644 --- a/manager_api_new/lib/model/user_role.dart +++ b/manager_api_new/lib/model/user_role.dart @@ -23,17 +23,17 @@ class UserRole { int toJson() => value; - static const number0 = UserRole._(0); - static const number1 = UserRole._(1); - static const number2 = UserRole._(2); - static const number3 = UserRole._(3); + static const SuperAdmin = UserRole._(0); + static const InstanceAdmin = UserRole._(1); + static const ContentEditor = UserRole._(2); + static const Viewer = UserRole._(3); /// List of all possible values in this [enum][UserRole]. static const values = [ - number0, - number1, - number2, - number3, + SuperAdmin, + InstanceAdmin, + ContentEditor, + Viewer, ]; static UserRole? fromJson(dynamic value) => @@ -76,15 +76,27 @@ class UserRoleTypeTransformer { /// and users are still using an old app with the old code. UserRole? decode(dynamic data, {bool allowNull = true}) { 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) { case 0: - return UserRole.number0; + return UserRole.SuperAdmin; case 1: - return UserRole.number1; + return UserRole.InstanceAdmin; case 2: - return UserRole.number2; + return UserRole.ContentEditor; case 3: - return UserRole.number3; + return UserRole.Viewer; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/verify_dto.dart b/verify_dto.dart new file mode 100644 index 0000000..2fc517e --- /dev/null +++ b/verify_dto.dart @@ -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 toJson() => {'language': language, 'value': value}; + static TranslationDTO fromJson(Map json) => + TranslationDTO(language: json['language'], value: json['value']); +} + +class EventAddressDTOGeometry { + String? type; + Object? coordinates; + EventAddressDTOGeometry({this.type, this.coordinates}); + Map toJson() => {'type': type, 'coordinates': coordinates}; +} + +class GuidedStepDTO { + List? title; + EventAddressDTOGeometry? geometry; + + GuidedStepDTO({this.title, this.geometry}); + + Map toJson() { + final json = {}; + 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(); + 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}'); +}