Frontend WIP - just websocket support left
This commit is contained in:
		
							parent
							
								
									f73bd53214
								
							
						
					
					
						commit
						e7641f6aec
					
				@ -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(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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"}',
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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>();
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										42
									
								
								frontend/lib/features/room/game_room.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								frontend/lib/features/room/game_room.dart
									
									
									
									
									
										Normal file
									
								
							@ -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'),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										167
									
								
								frontend/lib/features/room/join_room.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								frontend/lib/features/room/join_room.dart
									
									
									
									
									
										Normal file
									
								
							@ -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,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								frontend/lib/features/room/service/game_room.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								frontend/lib/features/room/service/game_room.dart
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
}
 | 
			
		||||
@ -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.
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
    ),
 | 
			
		||||
  ]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										47
									
								
								frontend/lib/providers/dio.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								frontend/lib/providers/dio.dart
									
									
									
									
									
										Normal file
									
								
							@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								frontend/lib/providers/utility.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/lib/providers/utility.dart
									
									
									
									
									
										Normal file
									
								
							@ -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();
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										54
									
								
								shared_models/lib/fart_logger.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								shared_models/lib/fart_logger.dart
									
									
									
									
									
										Normal file
									
								
							@ -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';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -263,7 +263,7 @@ packages:
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "5.1.1"
 | 
			
		||||
  logging:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: logging
 | 
			
		||||
      sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ environment:
 | 
			
		||||
 | 
			
		||||
dependencies:
 | 
			
		||||
  json_annotation: ^4.9.0
 | 
			
		||||
  logging: ^1.3.0
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  json_serializable: ^6.9.3
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user