From e7641f6aeccdcd299ef20de080db711f53c80138 Mon Sep 17 00:00:00 2001 From: Nate <n8r@tuta.io> Date: Sun, 9 Feb 2025 21:23:27 -0700 Subject: [PATCH] Frontend WIP - just websocket support left --- backend/lib/authenticator.dart | 1 + backend/lib/utils/environment.dart | 6 +- backend/main.dart | 49 +---- backend/routes/join_room.dart | 4 +- backend/scripts/pubspec.yaml | 2 +- backend/scripts/test_runner.dart | 2 +- backend/test_e2e/pubspec.yaml | 2 +- frontend/lib/features/room/game_room.dart | 42 +++++ frontend/lib/features/room/join_room.dart | 167 ++++++++++++++++++ .../lib/features/room/service/game_room.dart | 37 ++++ frontend/lib/main.dart | 100 +++-------- frontend/lib/providers/auth.dart | 52 +++++- frontend/lib/providers/dio.dart | 47 +++++ frontend/lib/providers/utility.dart | 10 ++ frontend/pubspec.lock | 34 +++- frontend/pubspec.yaml | 4 + shared_models/lib/fart_logger.dart | 54 ++++++ shared_models/pubspec.lock | 2 +- shared_models/pubspec.yaml | 1 + 19 files changed, 484 insertions(+), 132 deletions(-) create mode 100644 frontend/lib/features/room/game_room.dart create mode 100644 frontend/lib/features/room/join_room.dart create mode 100644 frontend/lib/features/room/service/game_room.dart create mode 100644 frontend/lib/providers/dio.dart create mode 100644 frontend/lib/providers/utility.dart create mode 100644 shared_models/lib/fart_logger.dart diff --git a/backend/lib/authenticator.dart b/backend/lib/authenticator.dart index aafba32..46db183 100644 --- a/backend/lib/authenticator.dart +++ b/backend/lib/authenticator.dart @@ -24,6 +24,7 @@ class Authenticator { final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000; final jwt = JWT( + header: {'algo': 'HS256'}, JWTBody(uuid: newUser.uuid, roomUuid: newUser.gameRoomUuid, iat: iat, exp: iat + expTimeSecs).toJson(), ); diff --git a/backend/lib/utils/environment.dart b/backend/lib/utils/environment.dart index ee2f896..477f20c 100644 --- a/backend/lib/utils/environment.dart +++ b/backend/lib/utils/environment.dart @@ -8,7 +8,7 @@ final log = Logger('Environment'); bool _isDevEnv = false; -void checkEnvironment(bool isDevEnv) { +void checkEnvironment({required bool isDevEnv}) { _isDevEnv = isDevEnv; getJWTSecret(); } @@ -46,7 +46,9 @@ String? getJWTSecret() { if (_isDevEnv) { log.warning('JWT secret not configured. Define JWT_TOKEN_SECRET in environment.'); final secret = List.generate( - 24, (_) => String.fromCharCode((65 + Random().nextInt(26)) + (Random().nextInt(2) == 0 ? 0 : 32))).join(); + 24, + (_) => String.fromCharCode((65 + Random().nextInt(26)) + (Random().nextInt(2) == 0 ? 0 : 32)), + ).join(); log.warning('Generated random JWT secret for development: $secret'); return secret; } else { diff --git a/backend/main.dart b/backend/main.dart index c69d4c6..b59fca5 100644 --- a/backend/main.dart +++ b/backend/main.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:backend/utils/environment.dart'; import 'package:dart_frog/dart_frog.dart'; -import 'package:logging/logging.dart'; +import 'package:shared_models/fart_logger.dart'; bool _listening = false; @@ -12,54 +12,11 @@ Future<HttpServer> run(Handler handler, InternetAddress ip, int port) async { // Logic to prevent multiple listeners with hot-reload // Changes to this are not hot-reloaded, you must restart the server if (!_listening) { - final logLevel = Platform.environment['LOG_LEVEL'] ?? (isDevelopment ? 'FINEST' : 'INFO'); - Logger.root.level = - Level.LEVELS.firstWhere((l) => l.name == logLevel, orElse: () => Level.INFO); // defaults to Level.INFO - Logger.root.onRecord.listen((record) { - writeLogRecord(record, record.level.value >= Level.SEVERE.value ? stderr : stdout); - }); + FartLogger.listen(isDevelopment: isDevelopment); _listening = true; } - checkEnvironment(isDevelopment); - - for (final lvl in Level.LEVELS) { - writeLogRecord(LogRecord(lvl, 'Test message', 'main'), stdout); - } + checkEnvironment(isDevEnv: isDevelopment); return serve(handler, ip, port); } - -const Map<String, String> _levelColors = { - 'FINEST': '\x1B[1;37m', // White - 'FINER': '\x1B[1;38m', // Gray - 'FINE': '\x1B[1;35m', // Purple - 'CONFIG': '\x1B[1;36m', // Cyan - 'INFO': '\x1B[1;32m', // Green - 'WARNING': '\x1B[1;33m', // Yellow - 'SEVERE': '\x1B[1;31m', // Red - 'SHOUT': '\x1B[1;38;5;52m\x1B[1;48;5;213m', // Red on pink -}; - -const String _resetColor = '\x1B[0m'; - -String _getColoredLevel(String levelName) { - return '${_levelColors[levelName] ?? ''}$levelName$_resetColor'; -} - -void writeLogRecord(LogRecord record, IOSink iosink) { - // Write the basic log message with colored level - iosink.writeln( - '[${_getColoredLevel(record.level.name)}]:[${record.loggerName}] ' - '${record.time}: ${record.message}', - ); - - // Additional details for severe logs - if (record.level.value >= Level.SEVERE.value) { - iosink.writeln( - '[${_getColoredLevel(record.level.name)}]:[${record.loggerName}] ' - '${record.error?.toString() ?? "No error provided"}\n' - '${record.stackTrace?.toString() ?? "No trace provided"}', - ); - } -} diff --git a/backend/routes/join_room.dart b/backend/routes/join_room.dart index d55ddf1..418df40 100644 --- a/backend/routes/join_room.dart +++ b/backend/routes/join_room.dart @@ -15,8 +15,8 @@ Future<Response> onRequest(RequestContext context) async { try { // Parse the request body - final body = await context.request.json() as Map<String, dynamic>; - final createUserReq = CreateUserRequest.fromJson(body); + final body = await context.request.json(); + final createUserReq = CreateUserRequest.fromJson(body as Map<String, dynamic>); // Generate token final authenticator = context.read<Authenticator>(); diff --git a/backend/scripts/pubspec.yaml b/backend/scripts/pubspec.yaml index 9fb8a2d..a4fa2db 100644 --- a/backend/scripts/pubspec.yaml +++ b/backend/scripts/pubspec.yaml @@ -3,6 +3,6 @@ environment: sdk: ">=3.0.0 <4.0.0" dev_dependencies: - test: ^1.24.0 http: ^1.1.0 + test: ^1.24.0 diff --git a/backend/scripts/test_runner.dart b/backend/scripts/test_runner.dart index 81ac548..6d1389d 100644 --- a/backend/scripts/test_runner.dart +++ b/backend/scripts/test_runner.dart @@ -61,7 +61,7 @@ void main() async { final sub = serverLogs.length > 10 ? serverLogs.length - 10 : 0; stdout.write("Server logs:\n${serverLogs.sublist(sub).join('\n')}"); } else { - stdout.writeln("💨 Passed like a light breeze 😮💨"); + stdout.writeln('💨 Passed like a light breeze 😮💨'); } // Exit with the same code as the test process diff --git a/backend/test_e2e/pubspec.yaml b/backend/test_e2e/pubspec.yaml index cb25b75..908e86f 100644 --- a/backend/test_e2e/pubspec.yaml +++ b/backend/test_e2e/pubspec.yaml @@ -7,7 +7,7 @@ environment: dependencies: backend: path: .. + http: ^1.1.0 shared_models: path: ../../shared_models test: ^1.24.0 - http: ^1.1.0 diff --git a/frontend/lib/features/room/game_room.dart b/frontend/lib/features/room/game_room.dart new file mode 100644 index 0000000..53d98d5 --- /dev/null +++ b/frontend/lib/features/room/game_room.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/providers/auth.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; + +final logger = Logger('GameRoomHome'); + +class GameRoomHome extends ConsumerStatefulWidget { + const GameRoomHome({super.key, this.roomUuid}); + + final String? roomUuid; + + @override + ConsumerState<GameRoomHome> createState() => _GameRoomHomeState(); +} + +class _GameRoomHomeState extends ConsumerState<GameRoomHome> { + @override + Widget build(BuildContext context) { + final jwtAsync = ref.watch(jwtNotifierProvider); + return Scaffold( + body: jwtAsync.when( + data: (jwt) { + if (jwt == null || jwt.roomUuid != widget.roomUuid) { + logger.fine('Tried to open room, but not authenticated / wrong room'); + // return home + context.go('/'); + } + return Column( + children: [ + Text('Authenticated.'), + Text('Welcome to room ${widget.roomUuid}'), + ], + ); + }, + loading: () => CircularProgressIndicator(), + error: (e, st) => Text('$e, $st'), + ), + ); + } +} diff --git a/frontend/lib/features/room/join_room.dart b/frontend/lib/features/room/join_room.dart new file mode 100644 index 0000000..43883dd --- /dev/null +++ b/frontend/lib/features/room/join_room.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/features/room/service/game_room.dart'; +import 'package:frontend/providers/auth.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; + +final logger = Logger('JoinRoomHome'); + +class JoinRoomHome extends ConsumerStatefulWidget { + const JoinRoomHome({super.key}); + + @override + ConsumerState<JoinRoomHome> createState() => _JoinRoomHomeState(); +} + +class _JoinRoomHomeState extends ConsumerState<JoinRoomHome> { + final _formKey = GlobalKey<FormState>(); + final _codeController = TextEditingController(); + final _nameController = TextEditingController(); + bool _isLoading = false; + + @override + void dispose() { + _codeController.dispose(); + _nameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final jwtAsync = ref.watch(jwtNotifierProvider); + + jwtAsync.whenData((jwt) { + logger.fine('Got jwt: ${jwt == null ? 'NULL' : jwt.toString().substring(10)}'); + if (jwt == null) return; + logger.fine('Navigating to game room screen'); + WidgetsBinding.instance.addPostFrameCallback( + (_) => context.go('/room/${jwt.roomUuid}'), + ); + }); + + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextFormField( + controller: _codeController, + textCapitalization: TextCapitalization.characters, + style: const TextStyle( + letterSpacing: 1.5, + fontWeight: FontWeight.bold, + ), + decoration: const InputDecoration( + labelText: 'Enter Room Code', + hintText: 'ABCDEF', + helperText: 'Enter 6 uppercase letters', + border: OutlineInputBorder(), + errorStyle: TextStyle(height: 0.8), + counterText: '', // Hides the built-in counter + ), + maxLength: 6, + textInputAction: TextInputAction.done, + inputFormatters: [ + // FilteringTextInputFormatter.allow(RegExp('[A-Z]')), + UpperCaseTextFormatter(), + ], + validator: (value) { + if (value == null || value.isEmpty) { + return 'Room code is required'; + } + if (value.length != 6) { + return 'Code must be exactly 6 characters'; + } + if (!RegExp(r'^[A-Z]{6}$').hasMatch(value)) { + return 'Only uppercase letters are allowed'; + } + return null; + }, + onChanged: (value) { + // Convert to uppercase while typing + if (value != value.toUpperCase()) { + _codeController.text = value.toUpperCase(); + _codeController.selection = TextSelection.fromPosition( + TextPosition(offset: _codeController.text.length), + ); + } + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Name', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.text, + maxLength: 20, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a name ya goof'; + } + return null; + }, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _isLoading + ? null + : () async { + if (_formKey.currentState!.validate()) { + setState(() => _isLoading = true); + try { + ref.read( + joinRoomProvider( + username: _nameController.text, + code: _codeController.text, + ), + ); + // ) + // .whenData( + // (response) { + // if (response != null && response.uuid != null) { + // logger.fine('Navigating to room ${response.uuid}'); + // // context.go('room/${response.uuid}'); + // } else { + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: Text('Unexpected error occurred.'), + // backgroundColor: Colors.red, + // ), + // ); + // } + // }, + // ); + } finally { + setState(() => _isLoading = false); + } + } + }, + child: _isLoading ? const CircularProgressIndicator() : const Text('Join Room'), + ), + ], + ), + ), + ), + ); + } +} + +class UpperCaseTextFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + return TextEditingValue( + text: newValue.text.toUpperCase(), + selection: newValue.selection, + ); + } +} diff --git a/frontend/lib/features/room/service/game_room.dart b/frontend/lib/features/room/service/game_room.dart new file mode 100644 index 0000000..7e010af --- /dev/null +++ b/frontend/lib/features/room/service/game_room.dart @@ -0,0 +1,37 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/providers/auth.dart'; +import 'package:frontend/providers/dio.dart'; +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_models/user.dart'; + +part 'game_room.g.dart'; + +final logger = Logger('services/joinRoom'); + +@riverpod +Future<JoinRoomResponse?> joinRoom(Ref ref, {required String username, required String code}) async { + final dio = ref.read(dioProvider); + + try { + final response = await dio.post<Map<String, dynamic>>( + '/join_room', + data: JoinRoomRequest(username: username, roomCode: code).toJson(), + ); + + if (response.statusCode == 200 && response.data != null) { + final joinResponse = JoinRoomResponse.fromJson(response.data!); + if (joinResponse.token != null) { + logger.fine('Setting token: ${joinResponse.token!.substring(10)}'); + await ref.read(jwtNotifierProvider.notifier).setJwt(joinResponse.token!); + } + return joinResponse; + } else { + logger.warning('Could not join room'); + } + } catch (e) { + logger.severe('Failed to join room', e, StackTrace.current); + return null; + } + return null; +} diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 2830484..ef0cbeb 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,7 +1,17 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/features/room/join_room.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shared_models/fart_logger.dart'; + +import 'features/room/game_room.dart'; void main() { - runApp(const MyApp()); + // determine if flutter app is dev or prod env + FartLogger.listen(isDevelopment: kDebugMode); + + runApp(ProviderScope(child: const MyApp())); } class MyApp extends StatelessWidget { @@ -9,87 +19,27 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', + final router = buildAppRouter(); + + return MaterialApp.router( + title: 'FartStack Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), + routerConfig: router, ); } } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - final String title; - - @override - State<MyHomePage> createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State<MyHomePage> { - int _counter = 0; - - void _incrementCounter() { - setState(() { - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), +GoRouter buildAppRouter() { + return GoRouter(routes: [ + GoRoute(path: '/', builder: (ctx, state) => JoinRoomHome()), + GoRoute( + path: '/room/:roomUuid', + builder: (ctx, state) => GameRoomHome( + roomUuid: state.pathParameters['roomUuid'], ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. - ); - } + ), + ]); } diff --git a/frontend/lib/providers/auth.dart b/frontend/lib/providers/auth.dart index d6c1967..1646e35 100644 --- a/frontend/lib/providers/auth.dart +++ b/frontend/lib/providers/auth.dart @@ -1,12 +1,60 @@ +import 'package:jwt_decoder/jwt_decoder.dart'; +import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_models/jwt.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'auth.g.dart'; +final logger = Logger('provider/auth'); + @riverpod class JwtNotifier extends _$JwtNotifier { @override - JWTBody? build() { - return null; + Future<JWTBody?> build() async { + if (!await SharedPreferencesAsync().containsKey('jwt')) { + logger.fine('No JWT saved to client'); + return null; + } + final jwtString = await SharedPreferencesAsync().getString('jwt'); + if (jwtString == null) { + logger.fine('Saved JWT came back null, removing key'); + SharedPreferencesAsync().remove('jwt'); + return null; + } + + final payload = JwtDecoder.tryDecode(jwtString); + if (payload == null) { + logger.fine('Failed to decode JWT, removing key.'); + SharedPreferencesAsync().remove('jwt'); + return null; + } + + try { + final body = JWTBody.fromJson(payload); + return body; + } catch (e) { + logger.shout('Failed to parse JWT payload to JWTBody, something is wrong:', e, StackTrace.current); + return null; + } + } + + Future<void> setJwt(String jwt) async { + final payload = JwtDecoder.tryDecode(jwt); + if (payload == null) { + logger.info('Tried to set JWT token that did not decode to payload'); + state = AsyncValue.error('JWT set to invalid token', StackTrace.current); + return; + } + + logger.fine('Saving jwt token to shared prefs'); + await SharedPreferencesAsync().setString('jwt', jwt); + + try { + final jwtBody = JWTBody.fromJson(payload); + state = AsyncValue.data(jwtBody); + } catch (e) { + state = AsyncError(e, StackTrace.current); + } } } diff --git a/frontend/lib/providers/dio.dart b/frontend/lib/providers/dio.dart new file mode 100644 index 0000000..edd4dc4 --- /dev/null +++ b/frontend/lib/providers/dio.dart @@ -0,0 +1,47 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'dio.g.dart'; + +final logger = Logger('Dio'); + +@riverpod +Dio dio(Ref ref) { + final dio = Dio(BaseOptions( + baseUrl: 'http://localhost:8080', + connectTimeout: const Duration(seconds: 5), + receiveTimeout: const Duration(seconds: 3), + )); + + dio.interceptors.add(LogInterceptor(responseBody: true)); + + return dio; +} + +// Create a custom LogInterceptor using the logger object +class CustomLogInterceptor extends Interceptor { + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + logger.info('REQUEST[${options.method}] => PATH: ${options.path}'); + return super.onRequest(options, handler); + } + + @override + // ignore: strict_raw_type + void onResponse(Response response, ResponseInterceptorHandler handler) { + logger.info( + 'RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}', + ); + return super.onResponse(response, handler); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + logger.severe( + 'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}', + ); + return super.onError(err, handler); + } +} diff --git a/frontend/lib/providers/utility.dart b/frontend/lib/providers/utility.dart new file mode 100644 index 0000000..ea1e8b1 --- /dev/null +++ b/frontend/lib/providers/utility.dart @@ -0,0 +1,10 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'utility.g.dart'; + +@riverpod +Future<SharedPreferencesAsync> sharedPreferencesAsync(Ref ref) async { + return SharedPreferencesAsync(); +} diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 83ca870..28bbd5c 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -230,6 +230,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + dio: + dependency: "direct main" + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a + url: "https://pub.dev" + source: hosted + version: "2.1.0" drift: dependency: transitive description: @@ -333,6 +349,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "04539267a740931c6d4479a10d466717ca5901c6fdfd3fcda09391bbb8ebd651" + url: "https://pub.dev" + source: hosted + version: "14.8.0" graphs: dependency: transitive description: @@ -389,6 +413,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" + jwt_decoder: + dependency: "direct main" + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" leak_tracker: dependency: transitive description: @@ -422,7 +454,7 @@ packages: source: hosted version: "5.1.1" logging: - dependency: transitive + dependency: "direct main" description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index b69c28e..2e16b7c 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -8,10 +8,14 @@ environment: sdk: ^3.6.0 dependencies: + dio: ^5.8.0+1 drift_flutter: ^0.2.4 flutter: sdk: flutter flutter_riverpod: ^2.6.1 + go_router: ^14.8.0 + jwt_decoder: ^2.0.1 + logging: ^1.3.0 riverpod_annotation: ^2.6.1 shared_models: path: ../shared_models diff --git a/shared_models/lib/fart_logger.dart b/shared_models/lib/fart_logger.dart new file mode 100644 index 0000000..1430ad1 --- /dev/null +++ b/shared_models/lib/fart_logger.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +import 'package:logging/logging.dart'; + +class FartLogger { + static void listen({required bool isDevelopment}) { + final logLevel = Platform.environment['LOG_LEVEL'] ?? (isDevelopment ? 'FINEST' : 'INFO'); + Logger.root.level = + Level.LEVELS.firstWhere((l) => l.name == logLevel, orElse: () => Level.INFO); // defaults to Level.INFO + Logger.root.onRecord.listen((record) { + _writeLogRecord(record, record.level.value >= Level.SEVERE.value ? stderr : stdout); + }); + } + + static void _writeLogRecord(LogRecord record, IOSink iosink) { + // Write the basic log message with colored level + iosink.writeln( + '[${_getColoredLevel(record.level.name)}]:[${record.loggerName}] ' + '${record.time}: ${record.message}', + ); + + // Additional details for severe logs + if (record.level.value >= Level.SEVERE.value) { + iosink.writeln( + '[${_getColoredLevel(record.level.name)}]:[${record.loggerName}] ' + '${record.error?.toString() ?? "No error provided"}\n' + '${record.stackTrace?.toString() ?? "No trace provided"}', + ); + } + } + + static void printLevels() { + for (final lvl in Level.LEVELS) { + _writeLogRecord(LogRecord(lvl, 'Test message', 'main'), stdout); + } + } + + static const Map<String, String> _levelColors = { + 'FINEST': '\x1B[1;37m', // White + 'FINER': '\x1B[1;38m', // Gray + 'FINE': '\x1B[1;35m', // Purple + 'CONFIG': '\x1B[1;36m', // Cyan + 'INFO': '\x1B[1;32m', // Green + 'WARNING': '\x1B[1;33m', // Yellow + 'SEVERE': '\x1B[1;31m', // Red + 'SHOUT': '\x1B[1;38;5;52m\x1B[1;48;5;213m', // Red on pink + }; + + static const String _resetColor = '\x1B[0m'; + + static String _getColoredLevel(String levelName) { + return '${_levelColors[levelName] ?? ''}$levelName$_resetColor'; + } +} diff --git a/shared_models/pubspec.lock b/shared_models/pubspec.lock index af97ed3..a75c4b0 100644 --- a/shared_models/pubspec.lock +++ b/shared_models/pubspec.lock @@ -263,7 +263,7 @@ packages: source: hosted version: "5.1.1" logging: - dependency: transitive + dependency: "direct main" description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 diff --git a/shared_models/pubspec.yaml b/shared_models/pubspec.yaml index 840151c..ab498b3 100644 --- a/shared_models/pubspec.yaml +++ b/shared_models/pubspec.yaml @@ -8,6 +8,7 @@ environment: dependencies: json_annotation: ^4.9.0 + logging: ^1.3.0 dev_dependencies: json_serializable: ^6.9.3