diff --git a/.gitignore b/.gitignore index 2d72c7c..3e2ac3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ ### # Dart ### + # Files and directories created by pub **/.dart_tool/ **/.packages @@ -72,3 +73,6 @@ app.*.map.json **/llm-chat.md +# DB files +*.sqlite* + diff --git a/backend/.dart_frog/server.dart b/backend/.dart_frog/server.dart new file mode 100644 index 0000000..2c08f7c --- /dev/null +++ b/backend/.dart_frog/server.dart @@ -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); +} + diff --git a/backend/drift/schemas/db/drift_schema_v1.json b/backend/drift/schemas/db/drift_schema_v1.json new file mode 100644 index 0000000..0d3b6d9 --- /dev/null +++ b/backend/drift/schemas/db/drift_schema_v1.json @@ -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":[]}}]} \ No newline at end of file diff --git a/backend/lib/authenticator.dart b/backend/lib/authenticator.dart index d1f44b1..5ea1c74 100644 --- a/backend/lib/authenticator.dart +++ b/backend/lib/authenticator.dart @@ -3,27 +3,43 @@ import 'dart:io'; import 'package:backend/database.dart'; import 'package:backend/service/db_access.dart'; import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:logging/logging.dart'; +import 'package:shared_models/user.dart'; final jwtSecret = _getSecret(); +const expTimeSecs = 3600; + +final log = Logger('Authenticator'); + +enum JWTTokenStatus { + valid, + expired, + invalid, +} class Authenticator { - Future<String?> generateToken({required String username}) async { - final newUser = await Db.createUser(username: username); + Future<String?> generateToken(CreateUserRequest req) async { + final newUser = await Db.createUser(username: req.username, roomCode: req.roomCode); if (newUser == null) return null; + final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000; final jwt = JWT( { 'uid': newUser.uuid, + 'roomUuid': newUser.gameRoomUuid, + 'iat': iat, + 'exp': iat + expTimeSecs, }, ); return jwt.sign(SecretKey(jwtSecret)); } - Future<User?> verifyToken( + Future<(User?, JWTTokenStatus)> verifyToken( String token, ) async { try { + log.info('Verifying jwt: ${token.substring(0, 10)}...${token.substring(token.length - 10)}'); final payload = JWT.verify( token, SecretKey(jwtSecret), @@ -31,16 +47,47 @@ class Authenticator { 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; - return await Db.getUser(uuid); + return (await Db.getUser(uuid), JWTTokenStatus.valid); } 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() { - 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) { throw Exception('JWT secret not configured. Define JWT_TOKEN_SECRET in environment.'); } else { diff --git a/backend/lib/database.dart b/backend/lib/database.dart index 65fad21..1508170 100644 --- a/backend/lib/database.dart +++ b/backend/lib/database.dart @@ -7,21 +7,34 @@ part 'database.g.dart'; class Users extends Table { 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)(); DateTimeColumn get createdAt => dateTime().nullable()(); + + @override + bool get isStrict => true; } enum GameStatus { - opened, + open, running, closed, + cancelled, } class GameRooms extends Table { TextColumn get uuid => text().unique()(); TextColumn get status => textEnum<GameStatus>()(); + TextColumn get code => text().withLength(min: 6, max: 6)(); DateTimeColumn get createdAt => dateTime().nullable()(); + + @override + bool get isStrict => true; + + @override + List<Set<Column>> get uniqueKeys => [ + {code, status}, + ]; } @DriftDatabase(tables: [Users, GameRooms]) @@ -32,7 +45,7 @@ class AppDatabase extends _$AppDatabase { int get schemaVersion => 1; static QueryExecutor _openConnection() { - return NativeDatabase.createInBackground(File('./backend.db')); + return NativeDatabase.createInBackground(File('./db.sqlite')); } @override diff --git a/backend/lib/middleware/auth_middleware.dart b/backend/lib/middleware/auth_middleware.dart new file mode 100644 index 0000000..ec0fc44 --- /dev/null +++ b/backend/lib/middleware/auth_middleware.dart @@ -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, + ), + ); + }; +} diff --git a/backend/lib/middleware/logger.dart b/backend/lib/middleware/logger.dart new file mode 100644 index 0000000..730e97e --- /dev/null +++ b/backend/lib/middleware/logger.dart @@ -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; + }; + }; +} diff --git a/backend/lib/service/db_access.dart b/backend/lib/service/db_access.dart index b559722..b6966d5 100644 --- a/backend/lib/service/db_access.dart +++ b/backend/lib/service/db_access.dart @@ -6,19 +6,40 @@ import 'package:uuid/uuid.dart'; final log = Logger('Db'); class Db { + static final _db = AppDatabase(); + 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); + return _db.managers.users.filter((f) => f.uuid.equals(uuid)).getSingle(); } - 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()); - }); + static Future<User?> createUser({required String username, required String roomCode}) async { + final room = await _db.managers.gameRooms + .filter((f) => f.code.equals(roomCode) & f.status.isIn([GameStatus.open, GameStatus.running])) + .getSingleOrNull() + .catchError((Object err) { + log.info('Failed to find available room:$roomCode', err, StackTrace.current); + return null; + }); + 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()); + }, + ); } diff --git a/backend/main.dart b/backend/main.dart index a3ef3f4..686a600 100644 --- a/backend/main.dart +++ b/backend/main.dart @@ -1,17 +1,27 @@ +import 'dart:developer' as dev; 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... +bool _listening = false; - 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}'); - }); +Future<HttpServer> run(Handler handler, InternetAddress ip, int port) async { + final isDevelopment = (await dev.Service.getInfo()).serverUri != null; + 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"}', + ); + } + }); + _listening = true; + } return serve(handler, ip, port); } diff --git a/backend/routes/[roomCode]/_middleware.dart b/backend/routes/[roomCode]/_middleware.dart deleted file mode 100644 index 8b435b4..0000000 --- a/backend/routes/[roomCode]/_middleware.dart +++ /dev/null @@ -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, - ), - ); -} diff --git a/backend/routes/_middleware.dart b/backend/routes/_middleware.dart index 5438077..090508d 100644 --- a/backend/routes/_middleware.dart +++ b/backend/routes/_middleware.dart @@ -1,15 +1,8 @@ // 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: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); - }, - ); + return handler.use(loggerMiddleware()).use(authenticatorMiddlewareProvider()); } diff --git a/backend/routes/auth/index.dart b/backend/routes/auth/index.dart index 08e47bc..d9cf4e6 100644 --- a/backend/routes/auth/index.dart +++ b/backend/routes/auth/index.dart @@ -2,9 +2,11 @@ import 'dart:io'; import 'package:backend/authenticator.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'; +final log = Logger('auth/'); + Future<Response> onRequest(RequestContext context) async { // Only allow POST requests if (context.request.method != HttpMethod.post) { @@ -18,28 +20,36 @@ Future<Response> onRequest(RequestContext context) async { // Generate token final authenticator = context.read<Authenticator>(); - final token = await authenticator.generateToken(username: createUserReq.username); + final token = await authenticator.generateToken(createUserReq); if (token == null) { + final body = CreateUserResponse( + success: false, + token: null, + error: 'Room ${createUserReq.roomCode} requested is not available', + ).toJson(); return Response.json( - statusCode: HttpStatus.internalServerError, - body: {'error': 'Failed to generate token'}, + statusCode: HttpStatus.badRequest, + body: body, ); } // Return the token return Response.json( - body: {'token': token}, - ); - } on JWTParseException { - return Response.json( - statusCode: HttpStatus.badRequest, - body: {'error': 'Username is required'}, + body: CreateUserResponse(token: token, success: true).toJson(), ); + // } + // on JWTParseException { + // return Response.json( + // statusCode: HttpStatus.badRequest, + // body: {'error': 'Username is required'}, + // ); } catch (e) { + log.severe('Error:', e); + final body = CreateUserResponse(success: false, token: null, error: 'Internal server error').toJson(); return Response.json( statusCode: HttpStatus.internalServerError, - body: {'error': 'Internal server error'}, + body: body, ); } } diff --git a/backend/routes/create_room.dart b/backend/routes/create_room.dart new file mode 100644 index 0000000..6ff734c --- /dev/null +++ b/backend/routes/create_room.dart @@ -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(), + ); + } +} diff --git a/backend/routes/room/[roomCode]/_middleware.dart b/backend/routes/room/[roomCode]/_middleware.dart new file mode 100644 index 0000000..3350691 --- /dev/null +++ b/backend/routes/room/[roomCode]/_middleware.dart @@ -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()); +} diff --git a/backend/routes/[roomCode]/join.dart b/backend/routes/room/[roomCode]/join.dart similarity index 100% rename from backend/routes/[roomCode]/join.dart rename to backend/routes/room/[roomCode]/join.dart diff --git a/flake.nix b/flake.nix index 4ec0c99..5940ef4 100644 --- a/flake.nix +++ b/flake.nix @@ -28,15 +28,10 @@ gtk3 pcre libepoxy + # For drift + sqlite - # This group all seem not strictly necessary -- commands like - # `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`. + # Dev deps libuuid # for mount.pc xorg.libXdmcp.dev python310Packages.libselinux.dev # for libselinux.pc @@ -48,11 +43,11 @@ at-spi2-core.dev xorg.libXtst.out pcre2.dev - jdk11 android-studio android-tools ]; + LD_LIBRARY_PATH = "${pkgs.sqlite.out}/lib"; shellHook = '' export PATH="$PATH":"$HOME/.pub-cache/bin" echo -e "\e[44m \e[0m" diff --git a/shared_models/lib/room.dart b/shared_models/lib/room.dart new file mode 100644 index 0000000..9fbb011 --- /dev/null +++ b/shared_models/lib/room.dart @@ -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); +} diff --git a/shared_models/lib/user.dart b/shared_models/lib/user.dart index 43d4464..a5d7697 100644 --- a/shared_models/lib/user.dart +++ b/shared_models/lib/user.dart @@ -2,30 +2,26 @@ 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; + final String roomCode; - CreateUserRequest({required this.username}); - + CreateUserRequest({required this.username, required this.roomCode}); factory CreateUserRequest.fromJson(Map<String, dynamic> json) => _$CreateUserRequestFromJson(json); 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); +}