Added better logging and websocket, more readme details

This commit is contained in:
Nathan Anderson 2025-02-05 22:41:08 -07:00
parent 76ea825509
commit fdc49bcb8d
8 changed files with 156 additions and 66 deletions

View File

@ -2,13 +2,15 @@
Flutter and Dart full stack template, Lovingly called FartStack
## TLDR
Optimising for iteration speed, correctness, and deployability.
Dev is done all in dart, with the flutter framework as a frontend. The
backend uses [dart_frog](), a dart backend framework with a focus on developer
experience. This whole stack revolves around my Dart expertise, so its not about
being the fastest stack, but the quickest stack I can build in. Because building
a product is like a fart: You can't be sure its not crap until the follow-through.
Because building a product is like a fart: You can't be sure its not crap until the follow-through.
## HowTo
Dev is done all in dart, with the flutter framework as a frontend. The backend uses [dart_frog](https://dartfrog.vgv.dev/), a dart backend
framework with a focus on developer experience. This whole stack revolves around my Dart expertise, so its not about
being blazingly fast, but the quickest stack I can build in with a statically-typed language.
### Installing
@ -21,9 +23,11 @@ If not, well, you need to setup `flutter`, `dart` (should get installed with flu
This codebase makes use of [RPS](https://pub.dev/packages/rps/install), just to standardize calling
various tools, like Make, but for dart.
- Frontend is started with `cd frontend` and `flutter run` (not included in rps because there are lots of cli flags I change often with flutter run)
- Frontend is started with `cd frontend` and `flutter run` (not included in rps because there are lots of cli flags I
change often with flutter run)
- Backend is started with `cd backend` and `rps dev` or `dart_frog dev`
- The `build_runners` (codegen) for each subproject can all be run `cd backend` and `rps watch` for development or `rps build` for a one off codegen
- The `build_runners` (codegen) for each subproject can all be run `cd backend` and `rps watch` for development or `rps
build` for a one off codegen
- If you dont want to bother installing rps, you can run the script directly:
```sh
dart backend/scripts/run_build_runner.dart
@ -41,7 +45,7 @@ dart run build_runner watch # or other args
End to end tests can be run with `cd backend` and `rps e2e`.
All end to end tests are located in the `backend/test_e2e/tests` folder.
There is a helper script that compiles and launches the backend and runs some end to end tests through.
There is a helper script that compiles and launches the backend and runs some end to end tests through it.
#### Frontend
@ -54,3 +58,16 @@ WIP
## Backend `./backend/`
Uses the great [dart frog](https://dartfrog.vgv.dev/) framework. Its an file-based framework, so to create a new route on `/new_route/bingo`,
you would create a new file in `routes/new_route/bingo.dart` that contains a function
```dart
Response onRequest(RequestContext context) {
return Response
}
```
Easy Peasy.
Go to the docks for more.
In the box, fartstack has authentication with JWT tokens setup

View File

@ -1,13 +1,12 @@
import 'dart:io';
import 'package:backend/database.dart';
import 'package:backend/service/db_access.dart';
import 'package:backend/utils/environment.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:logging/logging.dart';
import 'package:shared_models/jwt.dart';
import 'package:shared_models/user.dart';
final jwtSecret = _getSecret();
final jwtSecret = getJWTSecret();
const expTimeSecs = 3600;
final log = Logger('Authenticator');
@ -28,7 +27,7 @@ class Authenticator {
JWTBody(uuid: newUser.uuid, roomUuid: newUser.gameRoomUuid, iat: iat, exp: iat + expTimeSecs).toJson(),
);
return (jwt.sign(SecretKey(jwtSecret)), newUser);
return (jwt.sign(SecretKey(jwtSecret!)), newUser);
}
Future<(User?, JWTTokenStatus)> verifyToken(
@ -38,7 +37,7 @@ class Authenticator {
log.info('Verifying jwt: ${token.substring(0, 10)}...${token.substring(token.length - 10)}');
final payload = JWT.verify(
token,
SecretKey(jwtSecret),
SecretKey(jwtSecret!),
);
final jwt = JWTBody.fromJson(payload.payload as Map<String, dynamic>);
@ -54,36 +53,3 @@ class Authenticator {
}
}
}
// load any env vars inside root of project's .env file, then looks for JWT_TOKEN_SECRET
String _getSecret() {
final envs = {...Platform.environment};
try {
final result = Process.runSync('git', ['rev-parse', '--show-toplevel']);
if (result.exitCode != 0) {
log.warning('Failed to get git root directory: ${result.stderr}');
throw Exception('Failed to get git root directory');
}
final rootDir = (result.stdout as String).trim();
final envFile = File('$rootDir/.env');
if (envFile.existsSync()) {
for (final line in envFile.readAsLinesSync()) {
if (line.trim().isEmpty || line.startsWith('#')) continue;
final parts = line.split('=');
if (parts.length != 2) continue;
final key = parts[0].trim();
final value = parts[1].trim();
envs[key] = value;
}
}
} catch (e) {
log.warning('Failed to load .env file: $e');
}
// check for secret
final secret = envs['JWT_TOKEN_SECRET'];
if (secret == null || secret.isEmpty) {
throw Exception('JWT secret not configured. Define JWT_TOKEN_SECRET in environment.');
} else {
return secret;
}
}

View File

@ -0,0 +1,59 @@
// load any env vars inside root of project's .env file, then looks for JWT_TOKEN_SECRET
import 'dart:io';
import 'dart:math';
import 'package:logging/logging.dart';
final log = Logger('Environment');
bool _isDevEnv = false;
void checkEnvironment(bool isDevEnv) {
_isDevEnv = isDevEnv;
getJWTSecret();
}
String? getJWTSecret() {
final envs = {...Platform.environment};
if (_isDevEnv) {
log.fine('Trying to load .env file...');
try {
final result = Process.runSync('git', ['rev-parse', '--show-toplevel']);
if (result.exitCode != 0) {
log.warning('Failed to get git root directory: ${result.stderr}');
throw Exception('Failed to get git root directory');
}
final rootDir = (result.stdout as String).trim();
final envFile = File('$rootDir/.env');
if (envFile.existsSync()) {
for (final line in envFile.readAsLinesSync()) {
if (line.trim().isEmpty || line.startsWith('#')) continue;
final parts = line.split('=');
if (parts.length != 2) continue;
final key = parts[0].trim();
final value = parts[1].trim();
envs[key] = value;
}
}
} catch (e) {
log.warning('Failed to load .env file: $e');
}
}
// check for secret
final secret = envs['JWT_TOKEN_SECRET'];
if (secret == null || secret.isEmpty) {
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();
log.warning('Generated random JWT secret for development: $secret');
return secret;
} else {
log.severe('Stopping prod server because JWT secret is not defined.');
throw Exception('JWT secret not configured. Define JWT_TOKEN_SECRET in environment.');
}
} else {
return secret;
}
}

View File

@ -1,6 +1,7 @@
import 'dart:developer' as dev;
import 'dart:io';
import 'package:backend/utils/environment.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:logging/logging.dart';
@ -8,20 +9,57 @@ bool _listening = false;
Future<HttpServer> run(Handler handler, InternetAddress ip, int port) async {
final isDevelopment = (await dev.Service.getInfo()).serverUri != null;
// 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) {
stdout.writeln('[${record.level.name}]:[${record.loggerName}] ${record.time}: ${record.message}');
if (record.level.value >= Level.WARNING.value) {
stdout.writeln(
'[${record.level.name}]:[${record.loggerName}] ${record.error ?? "No error provided"}\n${record.stackTrace ?? "No trace provided"}',
);
}
writeLogRecord(record, record.level.value >= Level.SEVERE.value ? stderr : stdout);
});
_listening = true;
}
checkEnvironment(isDevelopment);
for (final lvl in Level.LEVELS) {
writeLogRecord(LogRecord(lvl, 'Test message', 'main'), stdout);
}
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"}',
);
}
}

View File

@ -206,6 +206,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
dart_frog_web_socket:
dependency: "direct main"
description:
name: dart_frog_web_socket
sha256: "02844c5fd59721824fdcf16e81dbfbfd54a9dfcc8a89a856483960a44159e1f6"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
dart_jsonwebtoken:
dependency: "direct main"
description:
@ -513,10 +521,10 @@ packages:
dependency: transitive
description:
name: shelf_web_socket
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "1.0.4"
source_gen:
dependency: transitive
description:
@ -693,22 +701,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "2.4.0"
webkit_inspection_protocol:
dependency: transitive
description:

View File

@ -9,6 +9,7 @@ environment:
dependencies:
dart_frog: ^1.1.0
dart_frog_auth: ^1.2.0
dart_frog_web_socket: ^1.0.0
dart_jsonwebtoken: ^2.16.0
drift: ^2.24.0
logging: ^1.3.0

9
backend/routes/ws.dart Normal file
View File

@ -0,0 +1,9 @@
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_frog_web_socket/dart_frog_web_socket.dart';
Future<Response> onRequest(RequestContext context) async {
final handler = webSocketHandler((channel, protocol) {
channel.stream.listen(print);
});
return handler(context);
}

View File

@ -69,7 +69,7 @@ Future<void> _waitForServer() async {
}
} catch (e) {
attempts++;
await Future.delayed(const Duration(seconds: 1));
await Future<void>.delayed(const Duration(seconds: 1));
}
}
throw Exception('Server failed to start after $maxAttempts seconds');