Refactor code structure for improved readability and maintainability

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Nils-Johan Gynther
2026-04-23 21:14:46 +02:00
parent cd4274575e
commit db1128ceaf
49 changed files with 285993 additions and 175 deletions
+1 -1
View File
@@ -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),
+9
View File
@@ -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';
+26
View File
@@ -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';
+11 -1
View File
@@ -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(),
),
],
),
],
+22 -8
View File
@@ -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),