Refactor code structure for improved readability and maintainability
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -24,7 +24,7 @@ class ApiClient {
|
||||
if (token != null) 'Authorization': 'Bearer $token',
|
||||
};
|
||||
|
||||
Future<Map<String, dynamic>> getJson(String path, {String? token}) async {
|
||||
Future<dynamic> getJson(String path, {String? token}) async {
|
||||
final response = await _client.get(
|
||||
Uri.parse('$baseUrl$path'),
|
||||
headers: _headers(token: token),
|
||||
|
||||
@@ -28,6 +28,15 @@ class PantryApiPaths {
|
||||
static String remove(int id) => '/pantry/$id';
|
||||
}
|
||||
|
||||
class UserApiPaths {
|
||||
static const me = '/users/me';
|
||||
static const list = '/users';
|
||||
static String setRole(int id) => '/users/$id/role';
|
||||
static String setPremium(int id) => '/users/$id/premium';
|
||||
static String delete(int id) => '/users/$id';
|
||||
static String resetPassword(int id) => '/users/$id/reset-password';
|
||||
}
|
||||
|
||||
class MealPlanApiPaths {
|
||||
static const list = '/meal-plan';
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'dart:convert';
|
||||
|
||||
/// Decodes a JWT token payload without verifying signature.
|
||||
/// Returns the decoded claims or an empty map on failure.
|
||||
Map<String, dynamic> decodeJwtPayload(String token) {
|
||||
try {
|
||||
final parts = token.split('.');
|
||||
if (parts.length != 3) return {};
|
||||
// Normalize base64url to standard base64.
|
||||
final payload = base64Url.normalize(parts[1]);
|
||||
final decoded = utf8.decode(base64Url.decode(payload));
|
||||
return json.decode(decoded) as Map<String, dynamic>;
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the role claim from a JWT token. Defaults to 'user'.
|
||||
String jwtRole(String? token) {
|
||||
if (token == null || token.isEmpty) return 'user';
|
||||
final claims = decodeJwtPayload(token);
|
||||
return claims['role'] as String? ?? 'user';
|
||||
}
|
||||
|
||||
/// Returns true if the JWT token contains role == 'admin'.
|
||||
bool jwtIsAdmin(String? token) => jwtRole(token) == 'admin';
|
||||
@@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
|
||||
import '../ui/app_shell.dart';
|
||||
import '../ui/async_state_views.dart';
|
||||
import '../../features/auth/data/auth_providers.dart';
|
||||
import '../../core/auth/jwt_decoder.dart';
|
||||
import '../../features/auth/presentation/login_screen.dart';
|
||||
import '../../features/profile/presentation/profile_screen.dart';
|
||||
import '../../features/recipes/presentation/create_recipe_screen.dart';
|
||||
@@ -20,6 +21,7 @@ import '../../features/inventory/presentation/consumption_history_screen.dart';
|
||||
import '../../features/meal_plan/presentation/meal_plan_screen.dart';
|
||||
import '../../features/pantry/presentation/pantry_screen.dart';
|
||||
import '../../features/import/presentation/import_screen.dart';
|
||||
import '../../features/admin/presentation/admin_screen.dart';
|
||||
|
||||
final appRouterProvider = Provider<GoRouter>((ref) {
|
||||
final authState = ref.watch(authStateProvider);
|
||||
@@ -28,7 +30,7 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||
initialLocation: '/',
|
||||
redirect: (context, state) {
|
||||
final isLoading = authState.isLoading;
|
||||
final token = authState.valueOrNull;
|
||||
final token = authState.maybeWhen(data: (t) => t, orElse: () => null);
|
||||
final isLoggedIn = token != null && token.isNotEmpty;
|
||||
final location = state.matchedLocation;
|
||||
final isSplash = location == '/';
|
||||
@@ -194,6 +196,14 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||
path: '/profile',
|
||||
builder: (context, state) => const ProfileScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin',
|
||||
redirect: (context, state) {
|
||||
final token = ref.read(authStateProvider).maybeWhen(data: (t) => t, orElse: () => null);
|
||||
return jwtIsAdmin(token) ? null : '/recipes';
|
||||
},
|
||||
builder: (context, state) => const AdminScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -4,6 +4,13 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../features/auth/data/auth_providers.dart';
|
||||
|
||||
const _adminDestination = _AppDestination(
|
||||
path: '/admin',
|
||||
title: 'Admin',
|
||||
icon: Icons.admin_panel_settings_outlined,
|
||||
label: 'Admin',
|
||||
);
|
||||
|
||||
class AppShell extends ConsumerWidget {
|
||||
final String location;
|
||||
final Widget child;
|
||||
@@ -14,7 +21,7 @@ class AppShell extends ConsumerWidget {
|
||||
required this.child,
|
||||
});
|
||||
|
||||
static const _destinations = [
|
||||
static const _baseDestinations = [
|
||||
_AppDestination(
|
||||
path: '/recipes',
|
||||
title: 'Recept',
|
||||
@@ -53,8 +60,13 @@ class AppShell extends ConsumerWidget {
|
||||
),
|
||||
];
|
||||
|
||||
int _selectedIndex() {
|
||||
final index = _destinations.indexWhere(
|
||||
List<_AppDestination> _destinations(bool isAdmin) => [
|
||||
..._baseDestinations,
|
||||
if (isAdmin) _adminDestination,
|
||||
];
|
||||
|
||||
int _selectedIndex(List<_AppDestination> destinations) {
|
||||
final index = destinations.indexWhere(
|
||||
(destination) => location.startsWith(destination.path),
|
||||
);
|
||||
return index < 0 ? 0 : index;
|
||||
@@ -62,8 +74,10 @@ class AppShell extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedIndex = _selectedIndex();
|
||||
final selectedDestination = _destinations[selectedIndex];
|
||||
final isAdmin = ref.watch(isAdminProvider);
|
||||
final dests = _destinations(isAdmin);
|
||||
final selectedIndex = _selectedIndex(dests);
|
||||
final selectedDestination = dests[selectedIndex];
|
||||
final isWide = MediaQuery.of(context).size.width >= 900;
|
||||
|
||||
Future<void> logout() async {
|
||||
@@ -74,7 +88,7 @@ class AppShell extends ConsumerWidget {
|
||||
}
|
||||
|
||||
void navigateTo(int index) {
|
||||
final target = _destinations[index].path;
|
||||
final target = dests[index].path;
|
||||
if (target != location && context.mounted) {
|
||||
context.go(target);
|
||||
}
|
||||
@@ -98,7 +112,7 @@ class AppShell extends ConsumerWidget {
|
||||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: navigateTo,
|
||||
labelType: NavigationRailLabelType.all,
|
||||
destinations: _destinations
|
||||
destinations: dests
|
||||
.map(
|
||||
(destination) => NavigationRailDestination(
|
||||
icon: Icon(destination.icon),
|
||||
@@ -117,7 +131,7 @@ class AppShell extends ConsumerWidget {
|
||||
: NavigationBar(
|
||||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: navigateTo,
|
||||
destinations: _destinations
|
||||
destinations: dests
|
||||
.map(
|
||||
(destination) => NavigationDestination(
|
||||
icon: Icon(destination.icon),
|
||||
|
||||
Reference in New Issue
Block a user