WIP still, tuning up auth and room wildcard with middleware
This commit is contained in:
parent
37e168e46b
commit
544d3b45ba
.gitignoreflake.nix
backend
.dart_frog
drift/schemas/db
lib
main.dartroutes
shared_models/lib
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,6 +1,7 @@
|
|||||||
###
|
###
|
||||||
# Dart
|
# Dart
|
||||||
###
|
###
|
||||||
|
|
||||||
# Files and directories created by pub
|
# Files and directories created by pub
|
||||||
**/.dart_tool/
|
**/.dart_tool/
|
||||||
**/.packages
|
**/.packages
|
||||||
@ -72,3 +73,6 @@ app.*.map.json
|
|||||||
|
|
||||||
**/llm-chat.md
|
**/llm-chat.md
|
||||||
|
|
||||||
|
# DB files
|
||||||
|
*.sqlite*
|
||||||
|
|
||||||
|
57
backend/.dart_frog/server.dart
Normal file
57
backend/.dart_frog/server.dart
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, implicit_dynamic_list_literal
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dart_frog/dart_frog.dart';
|
||||||
|
|
||||||
|
import '../main.dart' as entrypoint;
|
||||||
|
import '../routes/index.dart' as index;
|
||||||
|
import '../routes/create_room.dart' as create_room;
|
||||||
|
import '../routes/room/[roomCode]/join.dart' as room_$room_code_join;
|
||||||
|
import '../routes/auth/index.dart' as auth_index;
|
||||||
|
|
||||||
|
import '../routes/_middleware.dart' as middleware;
|
||||||
|
import '../routes/room/[roomCode]/_middleware.dart' as room_$room_code_middleware;
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
final address = InternetAddress.tryParse('') ?? InternetAddress.anyIPv6;
|
||||||
|
final port = int.tryParse(Platform.environment['PORT'] ?? '8080') ?? 8080;
|
||||||
|
hotReload(() => createServer(address, port));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<HttpServer> createServer(InternetAddress address, int port) {
|
||||||
|
final handler = Cascade().add(buildRootHandler()).handler;
|
||||||
|
return entrypoint.run(handler, address, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler buildRootHandler() {
|
||||||
|
final pipeline = const Pipeline().addMiddleware(middleware.middleware);
|
||||||
|
final router = Router()
|
||||||
|
..mount('/auth', (context) => buildAuthHandler()(context))
|
||||||
|
..mount('/room/<roomCode>', (context,roomCode,) => buildRoom$roomCodeHandler(roomCode,)(context))
|
||||||
|
..mount('/', (context) => buildHandler()(context));
|
||||||
|
return pipeline.addHandler(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler buildAuthHandler() {
|
||||||
|
final pipeline = const Pipeline();
|
||||||
|
final router = Router()
|
||||||
|
..all('/', (context) => auth_index.onRequest(context,));
|
||||||
|
return pipeline.addHandler(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler buildRoom$roomCodeHandler(String roomCode,) {
|
||||||
|
final pipeline = const Pipeline().addMiddleware(room_$room_code_middleware.middleware);
|
||||||
|
final router = Router()
|
||||||
|
..all('/join', (context) => room_$room_code_join.onRequest(context,roomCode,));
|
||||||
|
return pipeline.addHandler(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler buildHandler() {
|
||||||
|
final pipeline = const Pipeline();
|
||||||
|
final router = Router()
|
||||||
|
..all('/', (context) => index.onRequest(context,))..all('/create_room', (context) => create_room.onRequest(context,));
|
||||||
|
return pipeline.addHandler(router);
|
||||||
|
}
|
||||||
|
|
1
backend/drift/schemas/db/drift_schema_v1.json
Normal file
1
backend/drift/schemas/db/drift_schema_v1.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"game_rooms","was_declared_in_moor":false,"columns":[{"name":"uuid","getter_name":"uuid","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"status","getter_name":"status","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<GameStatus>(GameStatus.values)","dart_type_name":"GameStatus"}},{"name":"code","getter_name":"code","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":6,"max":6}}]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"strict":true}},{"id":1,"references":[0],"type":"table","data":{"name":"users","was_declared_in_moor":false,"columns":[{"name":"uuid","getter_name":"uuid","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"game_room_uuid","getter_name":"gameRoomUuid","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES game_rooms (uuid)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES game_rooms (uuid)"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":2,"max":32}}]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"strict":true}},{"id":2,"references":[0],"type":"index","data":{"on":0,"name":"idx_game_rooms_code","sql":"CREATE UNIQUE INDEX IF NOT EXISTS idx_game_rooms_code \nON game_rooms(code) \nWHERE status IN (\"opened\", \"running\");\n","unique":true,"columns":[]}}]}
|
@ -3,27 +3,43 @@ import 'dart:io';
|
|||||||
import 'package:backend/database.dart';
|
import 'package:backend/database.dart';
|
||||||
import 'package:backend/service/db_access.dart';
|
import 'package:backend/service/db_access.dart';
|
||||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:shared_models/user.dart';
|
||||||
|
|
||||||
final jwtSecret = _getSecret();
|
final jwtSecret = _getSecret();
|
||||||
|
const expTimeSecs = 3600;
|
||||||
|
|
||||||
|
final log = Logger('Authenticator');
|
||||||
|
|
||||||
|
enum JWTTokenStatus {
|
||||||
|
valid,
|
||||||
|
expired,
|
||||||
|
invalid,
|
||||||
|
}
|
||||||
|
|
||||||
class Authenticator {
|
class Authenticator {
|
||||||
Future<String?> generateToken({required String username}) async {
|
Future<String?> generateToken(CreateUserRequest req) async {
|
||||||
final newUser = await Db.createUser(username: username);
|
final newUser = await Db.createUser(username: req.username, roomCode: req.roomCode);
|
||||||
if (newUser == null) return null;
|
if (newUser == null) return null;
|
||||||
|
|
||||||
|
final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
final jwt = JWT(
|
final jwt = JWT(
|
||||||
{
|
{
|
||||||
'uid': newUser.uuid,
|
'uid': newUser.uuid,
|
||||||
|
'roomUuid': newUser.gameRoomUuid,
|
||||||
|
'iat': iat,
|
||||||
|
'exp': iat + expTimeSecs,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return jwt.sign(SecretKey(jwtSecret));
|
return jwt.sign(SecretKey(jwtSecret));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<User?> verifyToken(
|
Future<(User?, JWTTokenStatus)> verifyToken(
|
||||||
String token,
|
String token,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
|
log.info('Verifying jwt: ${token.substring(0, 10)}...${token.substring(token.length - 10)}');
|
||||||
final payload = JWT.verify(
|
final payload = JWT.verify(
|
||||||
token,
|
token,
|
||||||
SecretKey(jwtSecret),
|
SecretKey(jwtSecret),
|
||||||
@ -31,16 +47,47 @@ class Authenticator {
|
|||||||
|
|
||||||
final payloadData = payload.payload as Map<String, dynamic>;
|
final payloadData = payload.payload as Map<String, dynamic>;
|
||||||
|
|
||||||
|
final iat = payloadData['iat'] as int;
|
||||||
|
final exp = payloadData['exp'] as int;
|
||||||
|
|
||||||
|
if (iat + expTimeSecs != exp || DateTime.now().millisecondsSinceEpoch ~/ 1000 > exp) {
|
||||||
|
return (null, JWTTokenStatus.expired);
|
||||||
|
}
|
||||||
|
|
||||||
final uuid = payloadData['uuid'] as String;
|
final uuid = payloadData['uuid'] as String;
|
||||||
return await Db.getUser(uuid);
|
return (await Db.getUser(uuid), JWTTokenStatus.valid);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return (null, JWTTokenStatus.invalid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// load any env vars inside root of project's .env file, then looks for JWT_TOKEN_SECRET
|
||||||
String _getSecret() {
|
String _getSecret() {
|
||||||
final secret = Platform.environment['JWT_TOKEN_SECRET'];
|
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) {
|
if (secret == null || secret.isEmpty) {
|
||||||
throw Exception('JWT secret not configured. Define JWT_TOKEN_SECRET in environment.');
|
throw Exception('JWT secret not configured. Define JWT_TOKEN_SECRET in environment.');
|
||||||
} else {
|
} else {
|
||||||
|
@ -7,21 +7,34 @@ part 'database.g.dart';
|
|||||||
|
|
||||||
class Users extends Table {
|
class Users extends Table {
|
||||||
TextColumn get uuid => text().unique()();
|
TextColumn get uuid => text().unique()();
|
||||||
TextColumn get gameRoom => text().references(GameRooms, #uuid).nullable()();
|
TextColumn get gameRoomUuid => text().references(GameRooms, #uuid)();
|
||||||
TextColumn get name => text().withLength(min: 2, max: 32)();
|
TextColumn get name => text().withLength(min: 2, max: 32)();
|
||||||
DateTimeColumn get createdAt => dateTime().nullable()();
|
DateTimeColumn get createdAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isStrict => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GameStatus {
|
enum GameStatus {
|
||||||
opened,
|
open,
|
||||||
running,
|
running,
|
||||||
closed,
|
closed,
|
||||||
|
cancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
class GameRooms extends Table {
|
class GameRooms extends Table {
|
||||||
TextColumn get uuid => text().unique()();
|
TextColumn get uuid => text().unique()();
|
||||||
TextColumn get status => textEnum<GameStatus>()();
|
TextColumn get status => textEnum<GameStatus>()();
|
||||||
|
TextColumn get code => text().withLength(min: 6, max: 6)();
|
||||||
DateTimeColumn get createdAt => dateTime().nullable()();
|
DateTimeColumn get createdAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isStrict => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Set<Column>> get uniqueKeys => [
|
||||||
|
{code, status},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@DriftDatabase(tables: [Users, GameRooms])
|
@DriftDatabase(tables: [Users, GameRooms])
|
||||||
@ -32,7 +45,7 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
int get schemaVersion => 1;
|
int get schemaVersion => 1;
|
||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
static QueryExecutor _openConnection() {
|
||||||
return NativeDatabase.createInBackground(File('./backend.db'));
|
return NativeDatabase.createInBackground(File('./db.sqlite'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
47
backend/lib/middleware/auth_middleware.dart
Normal file
47
backend/lib/middleware/auth_middleware.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:backend/authenticator.dart';
|
||||||
|
import 'package:dart_frog/dart_frog.dart';
|
||||||
|
|
||||||
|
Authenticator? _authenticator;
|
||||||
|
|
||||||
|
Middleware authenticatorMiddlewareProvider() {
|
||||||
|
return provider<Authenticator>((context) => _authenticator ??= Authenticator());
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef Applies = Future<bool> Function(RequestContext context);
|
||||||
|
|
||||||
|
Future<bool> _defaultApplies(RequestContext context) async => true;
|
||||||
|
|
||||||
|
Middleware tokenAuthMiddleware({
|
||||||
|
Applies applies = _defaultApplies,
|
||||||
|
}) {
|
||||||
|
return (handler) => (context) async {
|
||||||
|
if (!await applies(context)) {
|
||||||
|
return handler(context);
|
||||||
|
}
|
||||||
|
final auth = context.read<Authenticator>();
|
||||||
|
// use `auth.verifyToken(token)` to check the jwt that came in the request header bearer
|
||||||
|
final authHeader = context.request.headers['authorization'];
|
||||||
|
final auths = authHeader?.split(' ');
|
||||||
|
if (authHeader == null || !authHeader.startsWith('Bearer ') || auths == null || auths.length != 2) {
|
||||||
|
log.fine('Denied request - No Auth - ${context.request.method.value} ${context.request.uri.path}');
|
||||||
|
return Response(statusCode: HttpStatus.unauthorized);
|
||||||
|
}
|
||||||
|
final token = auths.last;
|
||||||
|
|
||||||
|
final (user, tokStatus) = await auth.verifyToken(token);
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
log.fine(
|
||||||
|
'Denied request - Bad Auth:$tokStatus - ${context.request.method.value} ${context.request.uri.path}, no auth');
|
||||||
|
return Response(statusCode: HttpStatus.unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler(
|
||||||
|
context.provide(
|
||||||
|
() => user,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
24
backend/lib/middleware/logger.dart
Normal file
24
backend/lib/middleware/logger.dart
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import 'package:dart_frog/dart_frog.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
final log = Logger('ServerLogger');
|
||||||
|
|
||||||
|
Middleware loggerMiddleware() {
|
||||||
|
return (Handler handler) {
|
||||||
|
return (RequestContext context) async {
|
||||||
|
final request = context.request;
|
||||||
|
final startTime = DateTime.now();
|
||||||
|
|
||||||
|
final response = await handler(context);
|
||||||
|
|
||||||
|
final duration = DateTime.now().difference(startTime);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'${request.method.name} ${request.uri.path} '
|
||||||
|
'${response.statusCode} ${duration.inMilliseconds}ms',
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
@ -6,19 +6,40 @@ import 'package:uuid/uuid.dart';
|
|||||||
final log = Logger('Db');
|
final log = Logger('Db');
|
||||||
|
|
||||||
class Db {
|
class Db {
|
||||||
|
static final _db = AppDatabase();
|
||||||
|
|
||||||
static Future<User> getUser(String uuid) {
|
static Future<User> getUser(String uuid) {
|
||||||
log.finer('Getting user $uuid');
|
log.finer('Getting user $uuid');
|
||||||
return AppDatabase().managers.users.filter((f) => f.uuid.equals(uuid)).get().then((u) => u.first);
|
return _db.managers.users.filter((f) => f.uuid.equals(uuid)).getSingle();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<User?> createUser({required String username}) => AppDatabase()
|
static Future<User?> createUser({required String username, required String roomCode}) async {
|
||||||
.managers
|
final room = await _db.managers.gameRooms
|
||||||
.users
|
.filter((f) => f.code.equals(roomCode) & f.status.isIn([GameStatus.open, GameStatus.running]))
|
||||||
.createReturningOrNull(
|
.getSingleOrNull()
|
||||||
(o) => o(createdAt: Value(DateTime.now()), uuid: const Uuid().v4(), name: username),
|
.catchError((Object err) {
|
||||||
)
|
log.info('Failed to find available room:$roomCode', err, StackTrace.current);
|
||||||
.catchError((Object err) {
|
return null;
|
||||||
log.severe('Failed to create user', err, StackTrace.current);
|
});
|
||||||
throw Exception(err.toString());
|
if (room == null) return null;
|
||||||
});
|
return _db.managers.users
|
||||||
|
.createReturningOrNull(
|
||||||
|
(o) => o(createdAt: Value(DateTime.now()), uuid: const Uuid().v4(), name: username, gameRoomUuid: room.uuid),
|
||||||
|
)
|
||||||
|
.catchError((Object err) {
|
||||||
|
log.severe('Failed to create user', err, StackTrace.current);
|
||||||
|
throw Exception(err.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<GameRoom> createRoom({required String roomCode}) => _db.managers.gameRooms
|
||||||
|
.createReturning(
|
||||||
|
(o) => o(createdAt: Value(DateTime.now()), status: GameStatus.open, uuid: const Uuid().v4(), code: roomCode),
|
||||||
|
)
|
||||||
|
.catchError(
|
||||||
|
(Object err) {
|
||||||
|
log.severe('Failed to create room', err, StackTrace.current);
|
||||||
|
throw Exception(err.toString());
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,27 @@
|
|||||||
|
import 'dart:developer' as dev;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dart_frog/dart_frog.dart';
|
import 'package:dart_frog/dart_frog.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
Future<HttpServer> run(Handler handler, InternetAddress ip, int port) {
|
bool _listening = false;
|
||||||
// 1. Execute any custom code prior to starting the server...
|
|
||||||
|
|
||||||
final String logLevel = Platform.environment['LOG_LEVEL'] ?? 'INFO';
|
Future<HttpServer> run(Handler handler, InternetAddress ip, int port) async {
|
||||||
Logger.root.level =
|
final isDevelopment = (await dev.Service.getInfo()).serverUri != null;
|
||||||
Level.LEVELS.firstWhere((l) => l.name == logLevel, orElse: () => Level.INFO); // defaults to Level.INFO
|
if (!_listening) {
|
||||||
Logger.root.onRecord.listen((record) {
|
final logLevel = Platform.environment['LOG_LEVEL'] ?? (isDevelopment ? 'FINEST' : 'INFO');
|
||||||
stdout.writeln('${record.level.name}: ${record.time}: ${record.message}');
|
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"}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_listening = true;
|
||||||
|
}
|
||||||
|
|
||||||
return serve(handler, ip, port);
|
return serve(handler, ip, port);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,15 +1,8 @@
|
|||||||
// lib/routes/tasks/_middleware.dart
|
// lib/routes/tasks/_middleware.dart
|
||||||
|
import 'package:backend/middleware/auth_middleware.dart';
|
||||||
|
import 'package:backend/middleware/logger.dart';
|
||||||
import 'package:dart_frog/dart_frog.dart';
|
import 'package:dart_frog/dart_frog.dart';
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
final log = Logger('');
|
|
||||||
|
|
||||||
Handler middleware(Handler handler) {
|
Handler middleware(Handler handler) {
|
||||||
return handler.use(
|
return handler.use(loggerMiddleware()).use(authenticatorMiddlewareProvider());
|
||||||
(handler) => (context) async {
|
|
||||||
final request = context.request;
|
|
||||||
log.info('${request.method.value} ${request.uri.path}');
|
|
||||||
return await handler(context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,11 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:backend/authenticator.dart';
|
import 'package:backend/authenticator.dart';
|
||||||
import 'package:dart_frog/dart_frog.dart';
|
import 'package:dart_frog/dart_frog.dart';
|
||||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:shared_models/user.dart';
|
import 'package:shared_models/user.dart';
|
||||||
|
|
||||||
|
final log = Logger('auth/');
|
||||||
|
|
||||||
Future<Response> onRequest(RequestContext context) async {
|
Future<Response> onRequest(RequestContext context) async {
|
||||||
// Only allow POST requests
|
// Only allow POST requests
|
||||||
if (context.request.method != HttpMethod.post) {
|
if (context.request.method != HttpMethod.post) {
|
||||||
@ -18,28 +20,36 @@ Future<Response> onRequest(RequestContext context) async {
|
|||||||
|
|
||||||
// Generate token
|
// Generate token
|
||||||
final authenticator = context.read<Authenticator>();
|
final authenticator = context.read<Authenticator>();
|
||||||
final token = await authenticator.generateToken(username: createUserReq.username);
|
final token = await authenticator.generateToken(createUserReq);
|
||||||
|
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
|
final body = CreateUserResponse(
|
||||||
|
success: false,
|
||||||
|
token: null,
|
||||||
|
error: 'Room ${createUserReq.roomCode} requested is not available',
|
||||||
|
).toJson();
|
||||||
return Response.json(
|
return Response.json(
|
||||||
statusCode: HttpStatus.internalServerError,
|
statusCode: HttpStatus.badRequest,
|
||||||
body: {'error': 'Failed to generate token'},
|
body: body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the token
|
// Return the token
|
||||||
return Response.json(
|
return Response.json(
|
||||||
body: {'token': token},
|
body: CreateUserResponse(token: token, success: true).toJson(),
|
||||||
);
|
|
||||||
} on JWTParseException {
|
|
||||||
return Response.json(
|
|
||||||
statusCode: HttpStatus.badRequest,
|
|
||||||
body: {'error': 'Username is required'},
|
|
||||||
);
|
);
|
||||||
|
// }
|
||||||
|
// on JWTParseException {
|
||||||
|
// return Response.json(
|
||||||
|
// statusCode: HttpStatus.badRequest,
|
||||||
|
// body: {'error': 'Username is required'},
|
||||||
|
// );
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
log.severe('Error:', e);
|
||||||
|
final body = CreateUserResponse(success: false, token: null, error: 'Internal server error').toJson();
|
||||||
return Response.json(
|
return Response.json(
|
||||||
statusCode: HttpStatus.internalServerError,
|
statusCode: HttpStatus.internalServerError,
|
||||||
body: {'error': 'Internal server error'},
|
body: body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
42
backend/routes/create_room.dart
Normal file
42
backend/routes/create_room.dart
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:backend/service/db_access.dart';
|
||||||
|
import 'package:dart_frog/dart_frog.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:shared_models/room.dart';
|
||||||
|
|
||||||
|
final log = Logger('create_room');
|
||||||
|
|
||||||
|
Future<Response> onRequest(RequestContext context) async {
|
||||||
|
// Only allow POST requests
|
||||||
|
if (context.request.method != HttpMethod.post) {
|
||||||
|
return Response(statusCode: HttpStatus.methodNotAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate a random 6-letter room code
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
final random = Random();
|
||||||
|
final roomCode = String.fromCharCodes(
|
||||||
|
Iterable.generate(
|
||||||
|
6,
|
||||||
|
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the room
|
||||||
|
final room = await Db.createRoom(roomCode: roomCode);
|
||||||
|
|
||||||
|
// Return the room code
|
||||||
|
return Response.json(
|
||||||
|
body: CreateRoomResponse(success: true, roomCode: room.code).toJson(),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
log.severe('Error:', e);
|
||||||
|
return Response.json(
|
||||||
|
statusCode: HttpStatus.internalServerError,
|
||||||
|
body: CreateRoomResponse(success: false, roomCode: null, error: 'Internal server error').toJson(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
7
backend/routes/room/[roomCode]/_middleware.dart
Normal file
7
backend/routes/room/[roomCode]/_middleware.dart
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import 'package:backend/middleware/auth_middleware.dart';
|
||||||
|
import 'package:dart_frog/dart_frog.dart';
|
||||||
|
|
||||||
|
// Middleware to check for jwt tokens on all routes under /room/[roomCode]/
|
||||||
|
Handler middleware(Handler handler) {
|
||||||
|
return handler.use(tokenAuthMiddleware());
|
||||||
|
}
|
13
flake.nix
13
flake.nix
@ -28,15 +28,10 @@
|
|||||||
gtk3
|
gtk3
|
||||||
pcre
|
pcre
|
||||||
libepoxy
|
libepoxy
|
||||||
|
# For drift
|
||||||
|
sqlite
|
||||||
|
|
||||||
# This group all seem not strictly necessary -- commands like
|
# Dev deps
|
||||||
# `flutter run -d linux` seem to *work* fine without them, but
|
|
||||||
# the build does print messages about missing packages, like:
|
|
||||||
# Package mount was not found in the pkg-config search path.
|
|
||||||
# Perhaps you should add the directory containing `mount.pc'
|
|
||||||
# to the PKG_CONFIG_PATH environment variable
|
|
||||||
# To add to this list on NixOS upgrades, the Nix package
|
|
||||||
# `nix-index` is handy: then `nix-locate mount.pc`.
|
|
||||||
libuuid # for mount.pc
|
libuuid # for mount.pc
|
||||||
xorg.libXdmcp.dev
|
xorg.libXdmcp.dev
|
||||||
python310Packages.libselinux.dev # for libselinux.pc
|
python310Packages.libselinux.dev # for libselinux.pc
|
||||||
@ -48,11 +43,11 @@
|
|||||||
at-spi2-core.dev
|
at-spi2-core.dev
|
||||||
xorg.libXtst.out
|
xorg.libXtst.out
|
||||||
pcre2.dev
|
pcre2.dev
|
||||||
|
|
||||||
jdk11
|
jdk11
|
||||||
android-studio
|
android-studio
|
||||||
android-tools
|
android-tools
|
||||||
];
|
];
|
||||||
|
LD_LIBRARY_PATH = "${pkgs.sqlite.out}/lib";
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export PATH="$PATH":"$HOME/.pub-cache/bin"
|
export PATH="$PATH":"$HOME/.pub-cache/bin"
|
||||||
echo -e "\e[44m \e[0m"
|
echo -e "\e[44m \e[0m"
|
||||||
|
25
shared_models/lib/room.dart
Normal file
25
shared_models/lib/room.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'room.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CreateRoomRequest {
|
||||||
|
final bool success;
|
||||||
|
CreateRoomRequest({required this.success});
|
||||||
|
factory CreateRoomRequest.fromJson(Map<String, dynamic> json) => _$CreateRoomRequestFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CreateRoomRequestToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CreateRoomResponse {
|
||||||
|
final bool success;
|
||||||
|
final String? roomCode;
|
||||||
|
final String? error;
|
||||||
|
|
||||||
|
CreateRoomResponse({required this.success, required this.roomCode, this.error});
|
||||||
|
|
||||||
|
factory CreateRoomResponse.fromJson(Map<String, dynamic> json) => _$CreateRoomResponseFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CreateRoomResponseToJson(this);
|
||||||
|
}
|
@ -2,30 +2,26 @@ import 'package:json_annotation/json_annotation.dart';
|
|||||||
|
|
||||||
part 'user.g.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()
|
@JsonSerializable()
|
||||||
class CreateUserRequest {
|
class CreateUserRequest {
|
||||||
final String username;
|
final String username;
|
||||||
|
final String roomCode;
|
||||||
|
|
||||||
CreateUserRequest({required this.username});
|
CreateUserRequest({required this.username, required this.roomCode});
|
||||||
|
|
||||||
factory CreateUserRequest.fromJson(Map<String, dynamic> json) => _$CreateUserRequestFromJson(json);
|
factory CreateUserRequest.fromJson(Map<String, dynamic> json) => _$CreateUserRequestFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$CreateUserRequestToJson(this);
|
Map<String, dynamic> toJson() => _$CreateUserRequestToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CreateUserResponse {
|
||||||
|
final String? token;
|
||||||
|
final String? error;
|
||||||
|
final bool success;
|
||||||
|
|
||||||
|
CreateUserResponse({required this.token, required this.success, this.error});
|
||||||
|
|
||||||
|
factory CreateUserResponse.fromJson(Map<String, dynamic> json) => _$CreateUserResponseFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CreateUserResponseToJson(this);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user