WIP auth, added Drift for database and refined shared_models for data exchange

This commit is contained in:
Nate Anderson 2025-01-30 22:17:32 -07:00
parent d39e119bf4
commit 37e168e46b
15 changed files with 642 additions and 2 deletions

View File

@ -4,6 +4,8 @@ linter:
file_names: false
camel_case_types: true
prefer_single_quotes: true
public_member_api_docs: false
lines_longer_than_80_chars: false
analyzer:
exclude:

14
backend/build.yaml Normal file
View File

@ -0,0 +1,14 @@
targets:
$default:
builders:
drift_dev:
options:
# The directory where the test files are stored:
test_dir: drift/test/ # (default)
# The directory where the schema files are stored:
schema_dir: drift/schemas/ # (default)
databases:
db: lib/database.dart
# Optional: Add more databases
# another_db: lib/database2.dart

View File

@ -0,0 +1,49 @@
import 'dart:io';
import 'package:backend/database.dart';
import 'package:backend/service/db_access.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
final jwtSecret = _getSecret();
class Authenticator {
Future<String?> generateToken({required String username}) async {
final newUser = await Db.createUser(username: username);
if (newUser == null) return null;
final jwt = JWT(
{
'uid': newUser.uuid,
},
);
return jwt.sign(SecretKey(jwtSecret));
}
Future<User?> verifyToken(
String token,
) async {
try {
final payload = JWT.verify(
token,
SecretKey(jwtSecret),
);
final payloadData = payload.payload as Map<String, dynamic>;
final uuid = payloadData['uuid'] as String;
return await Db.getUser(uuid);
} catch (e) {
return null;
}
}
}
String _getSecret() {
final secret = Platform.environment['JWT_TOKEN_SECRET'];
if (secret == null || secret.isEmpty) {
throw Exception('JWT secret not configured. Define JWT_TOKEN_SECRET in environment.');
} else {
return secret;
}
}

53
backend/lib/database.dart Normal file
View File

@ -0,0 +1,53 @@
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
part 'database.g.dart';
class Users extends Table {
TextColumn get uuid => text().unique()();
TextColumn get gameRoom => text().references(GameRooms, #uuid).nullable()();
TextColumn get name => text().withLength(min: 2, max: 32)();
DateTimeColumn get createdAt => dateTime().nullable()();
}
enum GameStatus {
opened,
running,
closed,
}
class GameRooms extends Table {
TextColumn get uuid => text().unique()();
TextColumn get status => textEnum<GameStatus>()();
DateTimeColumn get createdAt => dateTime().nullable()();
}
@DriftDatabase(tables: [Users, GameRooms])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return NativeDatabase.createInBackground(File('./backend.db'));
}
@override
MigrationStrategy get migration {
return MigrationStrategy(
beforeOpen: (details) async {
// Statements to run to make sqlite performant, running in WAL mode, etc
await customStatement('PRAGMA foreign_keys = ON');
await customStatement('PRAGMA journal_mode=WAL');
await customStatement('PRAGMA busy_timeout=5000');
await customStatement('PRAGMA synchronous=NORMAL');
await customStatement('PRAGMA cache_size=10000');
await customStatement('PRAGMA temp_store=MEMORY');
await customStatement('PRAGMA mmap_size=268435456');
},
);
}
}

View File

@ -0,0 +1,24 @@
import 'package:backend/database.dart';
import 'package:drift/drift.dart';
import 'package:logging/logging.dart';
import 'package:uuid/uuid.dart';
final log = Logger('Db');
class Db {
static Future<User> getUser(String uuid) {
log.finer('Getting user $uuid');
return AppDatabase().managers.users.filter((f) => f.uuid.equals(uuid)).get().then((u) => u.first);
}
static Future<User?> createUser({required String username}) => AppDatabase()
.managers
.users
.createReturningOrNull(
(o) => o(createdAt: Value(DateTime.now()), uuid: const Uuid().v4(), name: username),
)
.catchError((Object err) {
log.severe('Failed to create user', err, StackTrace.current);
throw Exception(err.toString());
});
}

17
backend/main.dart Normal file
View File

@ -0,0 +1,17 @@
import 'dart:io';
import 'package:dart_frog/dart_frog.dart';
import 'package:logging/logging.dart';
Future<HttpServer> run(Handler handler, InternetAddress ip, int port) {
// 1. Execute any custom code prior to starting the server...
final String logLevel = Platform.environment['LOG_LEVEL'] ?? '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.time}: ${record.message}');
});
return serve(handler, ip, port);
}

View File

@ -14,6 +14,14 @@ packages:
description: dart
source: sdk
version: "0.3.3"
adaptive_number:
dependency: transitive
description:
name: adaptive_number
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
analyzer:
dependency: transitive
description:
@ -46,6 +54,110 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
build:
dependency: transitive
description:
name: build
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
url: "https://pub.dev"
source: hosted
version: "2.4.2"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948"
url: "https://pub.dev"
source: hosted
version: "4.0.3"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
url: "https://pub.dev"
source: hosted
version: "2.4.14"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2"
url: "https://pub.dev"
source: hosted
version: "8.9.3"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://pub.dev"
source: hosted
version: "2.0.3"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
url: "https://pub.dev"
source: hosted
version: "4.10.1"
collection:
dependency: transitive
description:
@ -86,6 +198,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
dart_frog_auth:
dependency: "direct main"
description:
name: dart_frog_auth
sha256: a4c05e6764a82f8997dee9fb68661a473afea92527a2dd8a6677e5a1b454b463
url: "https://pub.dev"
source: hosted
version: "1.2.0"
dart_jsonwebtoken:
dependency: "direct main"
description:
name: dart_jsonwebtoken
sha256: "06e02e18827d047f206e1051c15b493c9c29a2dba0f9b2a905d73748dec4f931"
url: "https://pub.dev"
source: hosted
version: "2.16.0"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
drift:
dependency: "direct main"
description:
name: drift
sha256: "76f23535e19a9f2be92f954e74d8802e96f526e5195d7408c1a20f6659043941"
url: "https://pub.dev"
source: hosted
version: "2.24.0"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: d1d90b0d55b22de412b77186f3bf3179a4b7e2acc4c8fb3a7aaf28a01abc194b
url: "https://pub.dev"
source: hosted
version: "2.24.0"
ed25519_edwards:
dependency: transitive
description:
name: ed25519_edwards
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
file:
dependency: transitive
description:
@ -94,6 +262,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
frontend_server_client:
dependency: transitive
description:
@ -110,6 +286,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.3"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
hotreloader:
dependency: transitive
description:
@ -158,8 +342,16 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.1"
logging:
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
logging:
dependency: "direct main"
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
@ -230,6 +422,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev"
source: hosted
version: "3.9.1"
pool:
dependency: transitive
description:
@ -246,6 +446,29 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.5"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
shared_models:
dependency: "direct main"
description:
path: "../shared_models"
relative: true
source: path
version: "1.0.0"
shelf:
dependency: transitive
description:
@ -286,6 +509,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.1"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
source_map_stack_trace:
dependency: transitive
description:
@ -310,6 +541,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqlite3:
dependency: "direct main"
description:
name: sqlite3
sha256: "35d3726fe18ab1463403a5cc8d97dbc81f2a0b08082e8173851363fcc97b6627"
url: "https://pub.dev"
source: hosted
version: "2.7.2"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee"
url: "https://pub.dev"
source: hosted
version: "0.41.0"
stack_trace:
dependency: transitive
description:
@ -374,6 +629,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.8"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
typed_data:
dependency: transitive
description:
@ -382,6 +645,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
very_good_analysis:
dependency: "direct dev"
description:
@ -447,4 +718,4 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.5.0 <4.0.0"
dart: ">=3.6.0 <4.0.0"

View File

@ -8,10 +8,18 @@ environment:
dependencies:
dart_frog: ^1.1.0
dart_frog_auth: ^1.2.0
dart_jsonwebtoken: ^2.16.0
drift: ^2.24.0
logging: ^1.3.0
shared_models:
path: ../shared_models
sqlite3: ^2.7.2
uuid: ^4.5.1
dev_dependencies:
build_runner: ^2.4.14
drift_dev: ^2.24.0
mocktail: ^1.0.3
test: ^1.25.5
very_good_analysis: ^5.1.0

View File

@ -0,0 +1,17 @@
import 'package:backend/authenticator.dart';
import 'package:backend/database.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_frog_auth/dart_frog_auth.dart';
Handler middleware(Handler handler) {
return handler.use(
bearerAuthentication<User>(
authenticator: (context, token) async {
final authenticator = context.read<Authenticator>();
return authenticator.verifyToken(token);
},
// says to apply the middleware to all routes
applies: (_) async => true,
),
);
}

View File

@ -0,0 +1,5 @@
import 'package:dart_frog/dart_frog.dart';
Response onRequest(RequestContext context, String roomCode) {
return Response(body: 'Joined $roomCode!');
}

View File

@ -0,0 +1,15 @@
// lib/routes/tasks/_middleware.dart
import 'package:dart_frog/dart_frog.dart';
import 'package:logging/logging.dart';
final log = Logger('');
Handler middleware(Handler handler) {
return handler.use(
(handler) => (context) async {
final request = context.request;
log.info('${request.method.value} ${request.uri.path}');
return await handler(context);
},
);
}

View File

@ -0,0 +1,45 @@
import 'dart:io';
import 'package:backend/authenticator.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:shared_models/user.dart';
Future<Response> onRequest(RequestContext context) async {
// Only allow POST requests
if (context.request.method != HttpMethod.post) {
return Response(statusCode: HttpStatus.methodNotAllowed);
}
try {
// Parse the request body
final body = await context.request.json() as Map<String, dynamic>;
final createUserReq = CreateUserRequest.fromJson(body);
// Generate token
final authenticator = context.read<Authenticator>();
final token = await authenticator.generateToken(username: createUserReq.username);
if (token == null) {
return Response.json(
statusCode: HttpStatus.internalServerError,
body: {'error': 'Failed to generate token'},
);
}
// Return the token
return Response.json(
body: {'token': token},
);
} on JWTParseException {
return Response.json(
statusCode: HttpStatus.badRequest,
body: {'error': 'Username is required'},
);
} catch (e) {
return Response.json(
statusCode: HttpStatus.internalServerError,
body: {'error': 'Internal server error'},
);
}
}

View File

@ -0,0 +1,31 @@
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
final String id;
final String name;
final String? roomId;
User({
required this.id,
required this.name,
this.roomId,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
@JsonSerializable()
class CreateUserRequest {
final String username;
CreateUserRequest({required this.username});
factory CreateUserRequest.fromJson(Map<String, dynamic> json) => _$CreateUserRequestFromJson(json);
Map<String, dynamic> toJson() => _$CreateUserRequestToJson(this);
}

View File

@ -62,6 +62,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948"
url: "https://pub.dev"
source: hosted
version: "4.0.3"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
url: "https://pub.dev"
source: hosted
version: "2.4.14"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2"
url: "https://pub.dev"
source: hosted
version: "8.9.3"
checked_yaml:
dependency: transitive
description:
@ -70,6 +118,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
url: "https://pub.dev"
source: hosted
version: "4.10.1"
collection:
dependency: transitive
description:
@ -118,6 +174,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
frontend_server_client:
dependency: transitive
description:
@ -134,6 +198,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.3"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
http_multi_server:
dependency: transitive
description:
@ -366,6 +438,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@ -406,6 +486,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.8"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
typed_data:
dependency: transitive
description:

View File

@ -11,5 +11,6 @@ dependencies:
dev_dependencies:
json_serializable: ^6.9.3
build_runner: ^2.4.14
lints: ^5.0.0
test: ^1.24.0