feat: add Flutter web frontend with authentication and recipe management features

This commit is contained in:
Nils-Johan Gynther
2026-04-21 21:29:47 +02:00
parent 2acf66e4c4
commit 3996456f6f
19 changed files with 460 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
import 'dart:io';
import 'package:http/http.dart' as http;
/// Platform-neutral HTTP client wrapping the internal API base URL.
/// Base URL is read from the FLUTTER_API_URL_INTERNAL environment variable
/// (set by Docker) or falls back to localhost for local development.
class ApiClient {
final String baseUrl;
final http.Client _client;
ApiClient({http.Client? client})
: baseUrl = const String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'http://localhost:8080',
),
_client = client ?? http.Client();
Map<String, String> _headers({String? token}) => {
'Content-Type': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
};
Future<http.Response> get(String path, {String? token}) =>
_client.get(Uri.parse('$baseUrl$path'), headers: _headers(token: token));
Future<http.Response> post(String path, String body, {String? token}) =>
_client.post(Uri.parse('$baseUrl$path'),
headers: _headers(token: token), body: body);
Future<http.Response> put(String path, String body, {String? token}) =>
_client.put(Uri.parse('$baseUrl$path'),
headers: _headers(token: token), body: body);
Future<http.Response> delete(String path, {String? token}) =>
_client.delete(Uri.parse('$baseUrl$path'),
headers: _headers(token: token));
}
+4
View File
@@ -0,0 +1,4 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'api_client.dart';
final apiClientProvider = Provider<ApiClient>((ref) => ApiClient());
@@ -0,0 +1,9 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'token_storage.dart';
import 'web_token_storage.dart';
/// Bind ITokenStorage to the web adapter.
/// For mobile: swap WebTokenStorage for SecureTokenStorage here.
final tokenStorageProvider = Provider<ITokenStorage>((ref) {
return WebTokenStorage();
});
@@ -0,0 +1,7 @@
/// Platform-neutral contract for token storage.
/// Web implementation uses SharedPreferences; mobile uses flutter_secure_storage.
abstract interface class ITokenStorage {
Future<String?> getToken();
Future<void> saveToken(String token);
Future<void> deleteToken();
}
@@ -0,0 +1,26 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'token_storage.dart';
/// Web-adapter: stores JWT in SharedPreferences (localStorage on web).
/// Replace with flutter_secure_storage adapter for Android/iOS.
class WebTokenStorage implements ITokenStorage {
static const _key = 'auth_token';
@override
Future<String?> getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_key);
}
@override
Future<void> saveToken(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_key, token);
}
@override
Future<void> deleteToken() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_key);
}
}
+20
View File
@@ -0,0 +1,20 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../features/recipes/presentation/recipes_screen.dart';
import '../../features/auth/presentation/login_screen.dart';
final appRouterProvider = Provider<GoRouter>((ref) {
return GoRouter(
initialLocation: '/recipes',
routes: [
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/recipes',
builder: (context, state) => const RecipesScreen(),
),
],
);
});