Added better logging and websocket, more readme details
This commit is contained in:
parent
76ea825509
commit
fdc49bcb8d
README.md
backend
35
README.md
35
README.md
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
59
backend/lib/utils/environment.dart
Normal file
59
backend/lib/utils/environment.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -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"}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
9
backend/routes/ws.dart
Normal 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);
|
||||
}
|
@ -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');
|
||||
|
Loading…
Reference in New Issue
Block a user