commit a99248b2846d391e111716e99af7f80f435f029b Author: Foxyz Date: Fri Feb 20 01:54:08 2026 +0300 init diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..21504f8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.dockerignore +Dockerfile +build/ +.dart_tool/ +.git/ +.github/ +.gitignore +.idea/ +.packages diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a85790 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..42e2d7c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# Use latest stable channel SDK. +FROM dart:stable AS build + +# Resolve app dependencies. +WORKDIR /app +COPY pubspec.* ./ +RUN dart pub get + +# Copy app source code (except anything in .dockerignore) and AOT compile app. +COPY . . +RUN dart build cli --target bin/server.dart -o output + +# Build minimal serving image from AOT-compiled `/server` +# and the pre-built AOT-runtime in the `/runtime/` directory of the base image. +FROM scratch +COPY --from=build /runtime/ / +COPY --from=build /app/output/bundle/ /app/ + +# Start server. +EXPOSE 8080 +CMD ["/app/bin/server"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e695d9d --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +A server app built using [Shelf](https://pub.dev/packages/shelf), +configured to enable running with [Docker](https://www.docker.com/). + +This sample code handles HTTP GET requests to `/` and `/echo/` + +# Running the sample + +## Running with the Dart SDK + +You can run the example with the [Dart SDK](https://dart.dev/get-dart) +like this: + +``` +$ dart run bin/server.dart +Server listening on port 8080 +``` + +And then from a second terminal: +``` +$ curl http://0.0.0.0:8080 +Hello, World! +$ curl http://0.0.0.0:8080/echo/I_love_Dart +I_love_Dart +``` + +## Running with Docker + +If you have [Docker Desktop](https://www.docker.com/get-started) installed, you +can build and run with the `docker` command: + +``` +$ docker build . -t myserver +$ docker run -it -p 8080:8080 myserver +Server listening on port 8080 +``` + +And then from a second terminal: +``` +$ curl http://0.0.0.0:8080 +Hello, World! +$ curl http://0.0.0.0:8080/echo/I_love_Dart +I_love_Dart +``` + +You should see the logging printed in the first terminal: +``` +2021-05-06T15:47:04.620417 0:00:00.000158 GET [200] / +2021-05-06T15:47:08.392928 0:00:00.001216 GET [200] /echo/I_love_Dart +``` diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/bin/chat_server.dart b/bin/chat_server.dart new file mode 100644 index 0000000..94cf719 --- /dev/null +++ b/bin/chat_server.dart @@ -0,0 +1,59 @@ +import 'package:chat_server/config/config.dart'; +import 'package:chat_server/models.dart'; +import 'package:chat_server/objectbox.g.dart'; +import 'package:chat_server/src/auth/auth_middleware.dart'; +import 'package:chat_server/src/handlers/auth_handlers.dart'; +import 'package:chat_server/src/handlers/channel_handlers.dart'; +import 'package:chat_server/src/handlers/debug_handlers.dart'; +import 'package:chat_server/src/handlers/message_handlers.dart'; +import 'package:chat_server/src/handlers/user_handlers.dart'; +import 'package:hotreloader/hotreloader.dart'; +import 'package:relic/relic.dart'; + +// Global ObjectBox boxes +final Store store = openStore(); +final Box userBox = store.box(); +final Box channelBox = store.box(); +final Box messageBox = store.box(); + +RelicApp? _currentApp; + +Future startServer() async { + await _currentApp?.close(); + + _currentApp = RelicApp() + // Public routes + ..post('/login', loginHandler) + ..post('/register', registerHandler) + // Debug routes + ..get('/debug/users', debugUsersHandler) + ..get('/debug/messages', debugMessagesHandler) + ..get('/debug/clear', debugClearHandler) + // Protected routes + ..use('/api', jwtMiddleware) + // Channels + ..get('/api/channels', getChannelsHandler) + ..post('/api/channels', createChannelHandler) + // Messages + ..get('/api/messages', getMessagesHandler) + ..post('/api/messages', sendMessageHandler) + // Users + ..get('/api/users', getUsersHandler) + // Logging & fallback + ..use('/', logRequests()) + ..fallback = respondWith((_) => Response.notFound()); + + print('🚀 Server starting on http://localhost:${Config.port}'); + await _currentApp!.serve(port: Config.port); +} + +void main() async { + await HotReloader.create( + onAfterReload: (ctx) async { + print('🔄 Changes detected! Restarting server...'); + await startServer(); + }, + ); + + await startServer(); +} diff --git a/lib/config/config.dart b/lib/config/config.dart new file mode 100644 index 0000000..f90fe6e --- /dev/null +++ b/lib/config/config.dart @@ -0,0 +1,9 @@ +class Config { + static const int port = 9870; + + // TODO: Move to environment variable in production + static const String jwtSecret = 'your_super_secret_key'; + + // Token expiration + static const Duration tokenExpiry = Duration(days: 7); +} diff --git a/lib/models.dart b/lib/models.dart new file mode 100644 index 0000000..b71d84d --- /dev/null +++ b/lib/models.dart @@ -0,0 +1,122 @@ +import 'package:objectbox/objectbox.dart'; + +/// User entity - works for both HOME and GUEST servers +/// +/// On HOME server: +/// - isLocal = true +/// - stores password, encrypted private key +/// - this is where identity lives +/// +/// On GUEST server: +/// - isLocal = false +/// - homeServer contains the URL of user's home +/// - publicKey cached for verification +@Entity() +class User { + @Id() + int id = 0; + + // Global identifier: "username@homeserver.com" + @Unique() + String userId; + + // Basic profile + @Index() + String username; + String? displayName; + String? avatarUrl; + String? bio; + + // Identity + String publicKey; // Ed25519 public key, always stored + + // HOME SERVER ONLY: Auth & key backup + String? passwordHash; + String? encryptedPrivateKey; // Encrypted with user's password + String? keyPairSalt; + + // Server relationship + bool isLocal; // true = this is their home server + String? homeServer; // if guest, where is their home? (e.g., "https://my.server.com") + + // Server-specific + List roles; // ["admin", "moderator", "member"] + + @Property(type: PropertyType.date) + DateTime createdAt; + + @Property(type: PropertyType.date) + DateTime? lastSyncedAt; // When profile was last refreshed from home + + User({ + required this.userId, + required this.username, + required this.publicKey, + required this.isLocal, + this.displayName, + this.avatarUrl, + this.bio, + this.passwordHash, + this.encryptedPrivateKey, + this.keyPairSalt, + this.homeServer, + List? roles, + DateTime? createdAt, + DateTime? lastSyncedAt, + }) : roles = roles ?? ['member'], + createdAt = createdAt ?? DateTime.now(), + lastSyncedAt = lastSyncedAt; +} + +@Entity() +class Channel { + @Id() + int id = 0; + + @Unique() + String name; + + String? description; + + @Property(type: PropertyType.date) + DateTime createdAt; + + Channel({required this.name, this.description, DateTime? createdAt}) : createdAt = createdAt ?? DateTime.now(); +} + +@Entity() +class Message { + @Id() + int id = 0; + + int channelId; + + // Author info - includes enough for cross-server profile fetch + int authorLocalId; + String authorUserId; // "alice@homeserver.com" + String? authorHomeServer; // for profile fetching + + // Cached for display (snapshot at message time) + String authorUsername; + String? authorAvatarUrl; + + String content; + + @Property(type: PropertyType.date) + DateTime createdAt; + + @Property(type: PropertyType.date) + DateTime? editedAt; + + Message({ + required this.channelId, + required this.authorLocalId, + required this.authorUserId, + required this.authorUsername, + required this.content, + this.authorHomeServer, + this.authorAvatarUrl, + DateTime? createdAt, + this.editedAt, + }) : createdAt = createdAt ?? DateTime.now(); +} diff --git a/lib/objectbox-model.json b/lib/objectbox-model.json new file mode 100644 index 0000000..f6f821e --- /dev/null +++ b/lib/objectbox-model.json @@ -0,0 +1,136 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "4:8789103914473960036", + "lastPropertyId": "4:1292940478834995143", + "name": "ChatChannel", + "properties": [ + { + "id": "1:8326949064500635093", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:4261805573466490477", + "name": "name", + "indexId": "5:7489629953476729908", + "type": 9, + "flags": 2080 + }, + { + "id": "3:3159087232030070914", + "name": "description", + "type": 9 + }, + { + "id": "4:1292940478834995143", + "name": "createdAt", + "type": 10 + } + ], + "relations": [] + }, + { + "id": "5:3737221321740284134", + "lastPropertyId": "5:9202752608552942529", + "name": "ChatMessage", + "properties": [ + { + "id": "1:6937877991903739354", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:2320935452587221036", + "name": "content", + "type": 9 + }, + { + "id": "3:4252783758825304742", + "name": "senderId", + "indexId": "6:1080757755541990100", + "type": 6, + "flags": 8 + }, + { + "id": "4:3240604049002701987", + "name": "channelId", + "indexId": "7:2720778762517392200", + "type": 6, + "flags": 8 + }, + { + "id": "5:9202752608552942529", + "name": "createdAt", + "type": 10 + } + ], + "relations": [] + }, + { + "id": "6:6808595317639142314", + "lastPropertyId": "4:3756250635467596289", + "name": "ChatUser", + "properties": [ + { + "id": "1:5564900411312083069", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:3278062150453672411", + "name": "username", + "indexId": "8:3068871702763915304", + "type": 9, + "flags": 2048 + }, + { + "id": "3:7002102783671264440", + "name": "passwordHash", + "type": 9 + }, + { + "id": "4:3756250635467596289", + "name": "createdAt", + "type": 10 + } + ], + "relations": [] + } + ], + "lastEntityId": "6:6808595317639142314", + "lastIndexId": "8:3068871702763915304", + "lastRelationId": "0:0", + "lastSequenceId": "0:0", + "modelVersion": 5, + "modelVersionParserMinimum": 5, + "retiredEntityUids": [ + 240536022189472816, + 8077011728318081304, + 4705468874025958717 + ], + "retiredIndexUids": [], + "retiredPropertyUids": [ + 8544617182402792569, + 6117431287454466914, + 5437786674528658195, + 6072852454810870218, + 2542181165384894966, + 864989412971989074, + 8028710060941408150, + 1587682278007620331, + 4383284664306084614, + 8714147817359246506, + 7393508199779059949, + 3791306853268708842, + 6999720138118727677 + ], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file diff --git a/lib/objectbox.g.dart b/lib/objectbox.g.dart new file mode 100644 index 0000000..8a07c6f --- /dev/null +++ b/lib/objectbox.g.dart @@ -0,0 +1,418 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// This code was generated by ObjectBox. To update it run the generator again +// with `dart run build_runner build`. +// See also https://docs.objectbox.io/getting-started#generate-objectbox-code + +// ignore_for_file: camel_case_types, depend_on_referenced_packages +// coverage:ignore-file + +import 'dart:typed_data'; + +import 'package:flat_buffers/flat_buffers.dart' as fb; +import 'package:objectbox/internal.dart' + as obx_int; // generated code can access "internal" functionality +import 'package:objectbox/objectbox.dart' as obx; + +import 'models.dart'; + +export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file + +final _entities = [ + obx_int.ModelEntity( + id: const obx_int.IdUid(4, 8789103914473960036), + name: 'ChatChannel', + lastPropertyId: const obx_int.IdUid(4, 1292940478834995143), + flags: 0, + properties: [ + obx_int.ModelProperty( + id: const obx_int.IdUid(1, 8326949064500635093), + name: 'id', + type: 6, + flags: 1, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(2, 4261805573466490477), + name: 'name', + type: 9, + flags: 2080, + indexId: const obx_int.IdUid(5, 7489629953476729908), + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(3, 3159087232030070914), + name: 'description', + type: 9, + flags: 0, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(4, 1292940478834995143), + name: 'createdAt', + type: 10, + flags: 0, + ), + ], + relations: [], + backlinks: [], + ), + obx_int.ModelEntity( + id: const obx_int.IdUid(5, 3737221321740284134), + name: 'ChatMessage', + lastPropertyId: const obx_int.IdUid(5, 9202752608552942529), + flags: 0, + properties: [ + obx_int.ModelProperty( + id: const obx_int.IdUid(1, 6937877991903739354), + name: 'id', + type: 6, + flags: 1, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(2, 2320935452587221036), + name: 'content', + type: 9, + flags: 0, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(3, 4252783758825304742), + name: 'senderId', + type: 6, + flags: 8, + indexId: const obx_int.IdUid(6, 1080757755541990100), + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(4, 3240604049002701987), + name: 'channelId', + type: 6, + flags: 8, + indexId: const obx_int.IdUid(7, 2720778762517392200), + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(5, 9202752608552942529), + name: 'createdAt', + type: 10, + flags: 0, + ), + ], + relations: [], + backlinks: [], + ), + obx_int.ModelEntity( + id: const obx_int.IdUid(6, 6808595317639142314), + name: 'ChatUser', + lastPropertyId: const obx_int.IdUid(4, 3756250635467596289), + flags: 0, + properties: [ + obx_int.ModelProperty( + id: const obx_int.IdUid(1, 5564900411312083069), + name: 'id', + type: 6, + flags: 1, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(2, 3278062150453672411), + name: 'username', + type: 9, + flags: 2048, + indexId: const obx_int.IdUid(8, 3068871702763915304), + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(3, 7002102783671264440), + name: 'passwordHash', + type: 9, + flags: 0, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(4, 3756250635467596289), + name: 'createdAt', + type: 10, + flags: 0, + ), + ], + relations: [], + backlinks: [], + ), +]; + +/// Shortcut for [obx.Store.new] that passes [getObjectBoxModel] and for Flutter +/// apps by default a [directory] using `defaultStoreDirectory()` from the +/// ObjectBox Flutter library. +/// +/// Note: for desktop apps it is recommended to specify a unique [directory]. +/// +/// See [obx.Store.new] for an explanation of all parameters. +/// +/// For Flutter apps, also calls `loadObjectBoxLibraryAndroidCompat()` from +/// the ObjectBox Flutter library to fix loading the native ObjectBox library +/// on Android 6 and older. +obx.Store openStore({ + String? directory, + int? maxDBSizeInKB, + int? maxDataSizeInKB, + int? fileMode, + int? maxReaders, + bool queriesCaseSensitiveDefault = true, + String? macosApplicationGroup, +}) { + return obx.Store( + getObjectBoxModel(), + directory: directory, + maxDBSizeInKB: maxDBSizeInKB, + maxDataSizeInKB: maxDataSizeInKB, + fileMode: fileMode, + maxReaders: maxReaders, + queriesCaseSensitiveDefault: queriesCaseSensitiveDefault, + macosApplicationGroup: macosApplicationGroup, + ); +} + +/// Returns the ObjectBox model definition for this project for use with +/// [obx.Store.new]. +obx_int.ModelDefinition getObjectBoxModel() { + final model = obx_int.ModelInfo( + // If this version is not found, it means that this file was generated + // with an older version of the ObjectBox Dart generator. + // Please regenerate this file with the current generator version. + // Typically, this is done with `dart run build_runner build`. + generatorVersion: obx_int.GeneratorVersion.v2025_12_16, + entities: _entities, + lastEntityId: const obx_int.IdUid(6, 6808595317639142314), + lastIndexId: const obx_int.IdUid(8, 3068871702763915304), + lastRelationId: const obx_int.IdUid(0, 0), + lastSequenceId: const obx_int.IdUid(0, 0), + retiredEntityUids: const [ + 240536022189472816, + 8077011728318081304, + 4705468874025958717, + ], + retiredIndexUids: const [], + retiredPropertyUids: const [ + 8544617182402792569, + 6117431287454466914, + 5437786674528658195, + 6072852454810870218, + 2542181165384894966, + 864989412971989074, + 8028710060941408150, + 1587682278007620331, + 4383284664306084614, + 8714147817359246506, + 7393508199779059949, + 3791306853268708842, + 6999720138118727677, + ], + retiredRelationUids: const [], + modelVersion: 5, + modelVersionParserMinimum: 5, + version: 1, + ); + + final bindings = { + ChatChannel: obx_int.EntityDefinition( + model: _entities[0], + toOneRelations: (ChatChannel object) => [], + toManyRelations: (ChatChannel object) => {}, + getId: (ChatChannel object) => object.id, + setId: (ChatChannel object, int id) { + object.id = id; + }, + objectToFB: (ChatChannel object, fb.Builder fbb) { + final nameOffset = fbb.writeString(object.name); + final descriptionOffset = object.description == null + ? null + : fbb.writeString(object.description!); + fbb.startTable(5); + fbb.addInt64(0, object.id); + fbb.addOffset(1, nameOffset); + fbb.addOffset(2, descriptionOffset); + fbb.addInt64(3, object.createdAt.millisecondsSinceEpoch); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (obx.Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final nameParam = const fb.StringReader( + asciiOptimization: true, + ).vTableGet(buffer, rootOffset, 6, ''); + final descriptionParam = const fb.StringReader( + asciiOptimization: true, + ).vTableGetNullable(buffer, rootOffset, 8); + final createdAtParam = DateTime.fromMillisecondsSinceEpoch( + const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0), + ); + final object = ChatChannel( + name: nameParam, + description: descriptionParam, + createdAt: createdAtParam, + )..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + + return object; + }, + ), + ChatMessage: obx_int.EntityDefinition( + model: _entities[1], + toOneRelations: (ChatMessage object) => [], + toManyRelations: (ChatMessage object) => {}, + getId: (ChatMessage object) => object.id, + setId: (ChatMessage object, int id) { + object.id = id; + }, + objectToFB: (ChatMessage object, fb.Builder fbb) { + final contentOffset = fbb.writeString(object.content); + fbb.startTable(6); + fbb.addInt64(0, object.id); + fbb.addOffset(1, contentOffset); + fbb.addInt64(2, object.senderId); + fbb.addInt64(3, object.channelId); + fbb.addInt64(4, object.createdAt.millisecondsSinceEpoch); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (obx.Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final contentParam = const fb.StringReader( + asciiOptimization: true, + ).vTableGet(buffer, rootOffset, 6, ''); + final senderIdParam = const fb.Int64Reader().vTableGet( + buffer, + rootOffset, + 8, + 0, + ); + final channelIdParam = const fb.Int64Reader().vTableGet( + buffer, + rootOffset, + 10, + 0, + ); + final createdAtParam = DateTime.fromMillisecondsSinceEpoch( + const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0), + ); + final object = ChatMessage( + content: contentParam, + senderId: senderIdParam, + channelId: channelIdParam, + createdAt: createdAtParam, + )..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + + return object; + }, + ), + ChatUser: obx_int.EntityDefinition( + model: _entities[2], + toOneRelations: (ChatUser object) => [], + toManyRelations: (ChatUser object) => {}, + getId: (ChatUser object) => object.id, + setId: (ChatUser object, int id) { + object.id = id; + }, + objectToFB: (ChatUser object, fb.Builder fbb) { + final usernameOffset = fbb.writeString(object.username); + final passwordHashOffset = fbb.writeString(object.passwordHash); + fbb.startTable(5); + fbb.addInt64(0, object.id); + fbb.addOffset(1, usernameOffset); + fbb.addOffset(2, passwordHashOffset); + fbb.addInt64(3, object.createdAt.millisecondsSinceEpoch); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (obx.Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final usernameParam = const fb.StringReader( + asciiOptimization: true, + ).vTableGet(buffer, rootOffset, 6, ''); + final passwordHashParam = const fb.StringReader( + asciiOptimization: true, + ).vTableGet(buffer, rootOffset, 8, ''); + final createdAtParam = DateTime.fromMillisecondsSinceEpoch( + const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0), + ); + final object = ChatUser( + username: usernameParam, + passwordHash: passwordHashParam, + createdAt: createdAtParam, + )..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + + return object; + }, + ), + }; + + return obx_int.ModelDefinition(model, bindings); +} + +/// [ChatChannel] entity fields to define ObjectBox queries. +class ChatChannel_ { + /// See [ChatChannel.id]. + static final id = obx.QueryIntegerProperty( + _entities[0].properties[0], + ); + + /// See [ChatChannel.name]. + static final name = obx.QueryStringProperty( + _entities[0].properties[1], + ); + + /// See [ChatChannel.description]. + static final description = obx.QueryStringProperty( + _entities[0].properties[2], + ); + + /// See [ChatChannel.createdAt]. + static final createdAt = obx.QueryDateProperty( + _entities[0].properties[3], + ); +} + +/// [ChatMessage] entity fields to define ObjectBox queries. +class ChatMessage_ { + /// See [ChatMessage.id]. + static final id = obx.QueryIntegerProperty( + _entities[1].properties[0], + ); + + /// See [ChatMessage.content]. + static final content = obx.QueryStringProperty( + _entities[1].properties[1], + ); + + /// See [ChatMessage.senderId]. + static final senderId = obx.QueryIntegerProperty( + _entities[1].properties[2], + ); + + /// See [ChatMessage.channelId]. + static final channelId = obx.QueryIntegerProperty( + _entities[1].properties[3], + ); + + /// See [ChatMessage.createdAt]. + static final createdAt = obx.QueryDateProperty( + _entities[1].properties[4], + ); +} + +/// [ChatUser] entity fields to define ObjectBox queries. +class ChatUser_ { + /// See [ChatUser.id]. + static final id = obx.QueryIntegerProperty( + _entities[2].properties[0], + ); + + /// See [ChatUser.username]. + static final username = obx.QueryStringProperty( + _entities[2].properties[1], + ); + + /// See [ChatUser.passwordHash]. + static final passwordHash = obx.QueryStringProperty( + _entities[2].properties[2], + ); + + /// See [ChatUser.createdAt]. + static final createdAt = obx.QueryDateProperty( + _entities[2].properties[3], + ); +} diff --git a/lib/src/auth/auth_middleware.dart b/lib/src/auth/auth_middleware.dart new file mode 100644 index 0000000..813d9c7 --- /dev/null +++ b/lib/src/auth/auth_middleware.dart @@ -0,0 +1,34 @@ +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:relic/relic.dart'; + +import '../../config/config.dart'; + +final userIdProperty = ContextProperty('userId'); +final usernameProperty = ContextProperty('username'); + +/// JWT authentication middleware +final jwtMiddleware = createMiddleware( + onRequest: (request) { + final authHeader = request.headers.authorization; + + if (authHeader is! BearerAuthorizationHeader) { + return Response.unauthorized(body: Body.fromString('Bearer token required')); + } + + try { + final jwt = JWT.verify(authHeader.token, SecretKey(Config.jwtSecret)); + + final userId = jwt.payload['sub'] as String? ?? jwt.payload['id'] as String; + final username = jwt.payload['username'] as String?; + + userIdProperty[request] = userId; + if (username != null) { + usernameProperty[request] = username; + } + + return null; + } catch (e) { + return Response.forbidden(body: Body.fromString('Invalid or expired token')); + } + }, +); diff --git a/lib/src/auth/auth_service.dart b/lib/src/auth/auth_service.dart new file mode 100644 index 0000000..6d03bf8 --- /dev/null +++ b/lib/src/auth/auth_service.dart @@ -0,0 +1,34 @@ +import 'package:bcrypt/bcrypt.dart'; +import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; + +import '../../config/config.dart'; + +class AuthService { + /// Generates a JWT token for a user + static String generateToken(String userId, {String? username}) { + final payload = {'sub': userId, 'username': ?username}; + + final jwt = JWT(payload); + return jwt.sign(SecretKey(Config.jwtSecret), expiresIn: Config.tokenExpiry); + } + + /// Verifies a password against a bcrypt hash + static bool verifyPassword(String password, String hash) { + return BCrypt.checkpw(password, hash); + } + + /// Hashes a password using bcrypt + static String hashPassword(String password) { + return BCrypt.hashpw(password, BCrypt.gensalt()); + } + + /// Verifies a JWT token and returns the payload + static Map? verifyToken(String token) { + try { + final jwt = JWT.verify(token, SecretKey(Config.jwtSecret)); + return Map.from(jwt.payload as Map); + } catch (_) { + return null; + } + } +} diff --git a/lib/src/handlers/auth_handlers.dart b/lib/src/handlers/auth_handlers.dart new file mode 100644 index 0000000..b5b70df --- /dev/null +++ b/lib/src/handlers/auth_handlers.dart @@ -0,0 +1,54 @@ +import 'package:chat_server/models.dart'; +import 'package:chat_server/objectbox.g.dart'; +import 'package:relic/relic.dart'; + +import '../auth/auth_service.dart'; +import '../utils/extensions.dart'; + +Future loginHandler(Request req) async { + final body = await req.asJson(); + final username = body['username'] as String?; + final password = body['password'] as String?; + + if (username == null || password == null) { + return Response.badRequest(body: Body.fromString('Username and password required')); + } + + final query = userBox.query(ChatUser_.username.equals(username)).build(); + final user = query.findFirst(); + query.close(); + + if (user == null || !AuthService.verifyPassword(password, user.passwordHash)) { + return Response.unauthorized(body: Body.fromString('Invalid credentials')); + } + + final token = AuthService.generateToken(user.id.toString(), username: user.username); + + return Response.ok(body: {'token': token, 'userId': user.id, 'username': user.username}.toBody); +} + +Future registerHandler(Request req) async { + final body = await req.asJson(); + final username = body['username'] as String?; + final password = body['password'] as String?; + + if (username == null || password == null) { + return Response.badRequest(body: Body.fromString('Username and password required')); + } + + final query = userBox.query(ChatUser_.username.equals(username)).build(); + final existing = query.findFirst(); + query.close(); + + if (existing != null) { + return Response.badRequest(body: Body.fromString('User already exists')); + } + + final hashedPassword = AuthService.hashPassword(password); + final user = ChatUser(username: username, passwordHash: hashedPassword); + final userId = userBox.put(user); + + final token = AuthService.generateToken(userId.toString(), username: username); + + return Response.ok(body: {'token': token, 'userId': userId, 'username': username}.toBody); +} diff --git a/lib/src/handlers/channel_handlers.dart b/lib/src/handlers/channel_handlers.dart new file mode 100644 index 0000000..a386951 --- /dev/null +++ b/lib/src/handlers/channel_handlers.dart @@ -0,0 +1,37 @@ +import 'package:chat_server/models.dart'; +import 'package:chat_server/objectbox.g.dart'; +import 'package:relic/relic.dart'; + +import '../utils/extensions.dart'; + +Future getChannelsHandler(Request req) async { + final channels = channelBox.getAll(); + + return Response.ok( + body: { + 'channels': channels + .map((c) => {'id': c.id, 'name': c.name, 'description': c.description, 'createdAt': c.createdAt.toIso8601String()}) + .toList(), + }.toBody, + ); +} + +Future createChannelHandler(Request req) async { + final body = await req.asJson(); + final name = (body['name'] as String?)?.trim().toLowerCase().replaceAll(' ', '-'); + final description = body['description'] as String?; + + if (name == null || name.isEmpty) { + return Response.badRequest(body: Body.fromString('Channel name required')); + } + + final existing = channelBox.query(ChatChannel_.name.equals(name)).build().findFirst(); + if (existing != null) { + return Response.badRequest(body: Body.fromString('Channel already exists')); + } + + final channel = ChatChannel(name: name, description: description); + final id = channelBox.put(channel); + + return Response.ok(body: {'id': id, 'name': name, 'description': description, 'createdAt': channel.createdAt.toIso8601String()}.toBody); +} diff --git a/lib/src/handlers/debug_handlers.dart b/lib/src/handlers/debug_handlers.dart new file mode 100644 index 0000000..77aec83 --- /dev/null +++ b/lib/src/handlers/debug_handlers.dart @@ -0,0 +1,38 @@ +import 'package:chat_server/objectbox.g.dart'; +import 'package:relic/relic.dart'; + +import '../utils/extensions.dart'; + +Future debugUsersHandler(Request req) async { + return Response.ok( + body: { + 'users': userBox.getAll().map((u) => {'id': u.id, 'username': u.username}).toList(), + }.toBody, + ); +} + +Future debugMessagesHandler(Request req) async { + return Response.ok( + body: { + 'messages': messageBox + .getAll() + .map( + (m) => { + 'id': m.id, + 'content': m.content, + 'senderId': m.senderId, + 'channelId': m.channelId, + 'createdAt': m.createdAt.toIso8601String(), + }, + ) + .toList(), + }.toBody, + ); +} + +Future debugClearHandler(Request req) async { + messageBox.removeAll(); + channelBox.removeAll(); + userBox.removeAll(); + return Response.ok(body: {'cleared': true}.toBody); +} diff --git a/lib/src/handlers/message_handlers.dart b/lib/src/handlers/message_handlers.dart new file mode 100644 index 0000000..160a6ac --- /dev/null +++ b/lib/src/handlers/message_handlers.dart @@ -0,0 +1,85 @@ +import 'package:chat_server/models.dart'; +import 'package:chat_server/objectbox.g.dart'; +import 'package:relic/relic.dart'; + +import '../auth/auth_middleware.dart'; +import '../utils/extensions.dart'; + +Future getMessagesHandler(Request req) async { + final channelIdStr = req.url.queryParameters['channelId']; + + Query query; + + if (channelIdStr != null) { + final channelId = int.tryParse(channelIdStr); + if (channelId == null) { + return Response.badRequest(body: Body.fromString('Invalid channelId')); + } + query = messageBox.query(ChatMessage_.channelId.equals(channelId)).order(ChatMessage_.createdAt, flags: Order.descending).build(); + } else { + query = messageBox.query().order(ChatMessage_.createdAt, flags: Order.descending).build(); + } + + final messages = query.find(); + query.close(); + + // Get usernames for senders + final senderIds = messages.map((m) => m.senderId).toSet(); + final userQuery = userBox.query(ChatUser_.id.oneOf(senderIds.toList())).build(); + final users = userQuery.find(); + userQuery.close(); + + final userMap = {for (var u in users) u.id: u.username}; + + return Response.ok( + body: { + 'messages': messages + .map( + (m) => { + 'id': m.id, + 'content': m.content, + 'senderId': m.senderId, + 'senderName': userMap[m.senderId] ?? 'Unknown', + 'channelId': m.channelId, + 'createdAt': m.createdAt.toIso8601String(), + }, + ) + .toList(), + }.toBody, + ); +} + +Future sendMessageHandler(Request req) async { + final senderId = int.parse(userIdProperty[req]!); + + final body = await req.asJson(); + final content = body['content'] as String?; + final channelId = body['channelId'] as int?; + + if (content == null || content.isEmpty) { + return Response.badRequest(body: Body.fromString('Content required')); + } + + if (channelId == null) { + return Response.badRequest(body: Body.fromString('channelId required')); + } + + final channel = channelBox.get(channelId); + if (channel == null) { + return Response.badRequest(body: Body.fromString('Channel not found')); + } + + final message = ChatMessage(content: content, senderId: senderId, channelId: channelId); + + final id = messageBox.put(message); + + return Response.ok( + body: { + 'id': id, + 'content': content, + 'senderId': senderId, + 'channelId': channelId, + 'createdAt': message.createdAt.toIso8601String(), + }.toBody, + ); +} diff --git a/lib/src/handlers/user_handlers.dart b/lib/src/handlers/user_handlers.dart new file mode 100644 index 0000000..0943bfa --- /dev/null +++ b/lib/src/handlers/user_handlers.dart @@ -0,0 +1,14 @@ +import 'package:chat_server/objectbox.g.dart'; +import 'package:relic/relic.dart'; + +import '../utils/extensions.dart'; + +Future getUsersHandler(Request req) async { + final users = userBox.getAll(); + + return Response.ok( + body: { + 'users': users.map((u) => {'id': u.id, 'username': u.username, 'createdAt': u.createdAt.toIso8601String()}).toList(), + }.toBody, + ); +} diff --git a/lib/src/utils/extensions.dart b/lib/src/utils/extensions.dart new file mode 100644 index 0000000..e6969f2 --- /dev/null +++ b/lib/src/utils/extensions.dart @@ -0,0 +1,23 @@ +import 'dart:convert'; + +import 'package:relic/relic.dart'; + +extension RequestHelper on Request { + /// Parses the request body as JSON + Future> asJson() async { + final bodyText = await readAsString(); + return jsonDecode(bodyText); + } +} + +extension MapToBody on Map { + /// Converts a Map to a JSON Body + Body get toBody => Body.fromString(jsonEncode(this)); +} + +extension JsonBody on Body { + /// Creates a JSON body from a Map + static Body fromJson(Map json) { + return Body.fromString(jsonEncode(json)); + } +} diff --git a/objectbox.dll b/objectbox.dll new file mode 100644 index 0000000..a48171d Binary files /dev/null and b/objectbox.dll differ diff --git a/objectbox/data.mdb b/objectbox/data.mdb new file mode 100644 index 0000000..81986e6 Binary files /dev/null and b/objectbox/data.mdb differ diff --git a/objectbox/lock.mdb b/objectbox/lock.mdb new file mode 100644 index 0000000..c52aa29 Binary files /dev/null and b/objectbox/lock.mdb differ diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..e79dbd9 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,636 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "796d97d925add7ffcdf5595f33a2066a6e3cee97971e6dbef09b76b7880fd760" + url: "https://pub.dev" + source: hosted + version: "94.0.0" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "9c8ebb304d72c0a0c8764344627529d9503fc83d7d73e43ed727dc532f822e4b" + url: "https://pub.dev" + source: hosted + version: "10.0.2" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + bcrypt: + dependency: "direct main" + description: + name: bcrypt + sha256: "6073a700cbbc59f1d4ab27cd532755e3de5e676c4941f535f351374df849270b" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "39ad4ca8a2876779737c60e4228b4bcd35d4352ef7e14e47514093edc012c734" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + 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: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" + url: "https://pub.dev" + source: hosted + version: "8.12.3" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + 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: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_jsonwebtoken: + dependency: "direct main" + description: + name: dart_jsonwebtoken + sha256: "0de65691c1d736e9459f22f654ddd6fd8368a271d4e41aa07e53e6301eff5075" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "15a7db352c8fc6a4d2bc475ba901c25b39fe7157541da4c16eacce6f8be83e49" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + 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: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + 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" + flat_buffers: + dependency: transitive + description: + name: flat_buffers + sha256: "7c1de2d6eb5f3e61e5c50040841109f509deaaf2b12ec0d57b92456d9ea50345" + url: "https://pub.dev" + source: hosted + version: "25.9.23" + fox_utils2: + dependency: "direct main" + description: + path: "../fox_utils2" + relative: true + source: path + version: "1.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + 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: "direct main" + description: + name: hotreloader + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + url: "https://pub.dev" + source: hosted + version: "4.3.0" + http: + dependency: "direct dev" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" + url: "https://pub.dev" + source: hosted + version: "4.10.0" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + meta: + dependency: transitive + description: + name: meta + sha256: "9f29b9bcc8ee287b1a31e0d01be0eae99a930dbffdaecf04b3f3d82a969f296f" + url: "https://pub.dev" + source: hosted + version: "1.18.1" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + objectbox: + dependency: "direct main" + description: + name: objectbox + sha256: ac1e1ef852f60ea0af01fddb28811c8aa169f2b1ae9284e91e7b822abe5ed603 + url: "https://pub.dev" + source: hosted + version: "5.2.0" + objectbox_generator: + dependency: "direct dev" + description: + name: objectbox_generator + sha256: "28628fa65d4fe8ade868cd50aa323ecdfd6d435fcda2cc7484071b84031f8c5e" + url: "https://pub.dev" + source: hosted + version: "5.2.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + relic: + dependency: "direct main" + description: + name: relic + sha256: d87e88a2dd758b67a03bfbedd7f62ef81f44734a4851ec51547b51502cd2f114 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + relic_core: + dependency: transitive + description: + name: relic_core + sha256: caefeef3a85580357cf2d6f70c4a3f9de3e2375ec22196aa983b84c8f463c960 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + relic_io: + dependency: transitive + description: + name: relic_io + sha256: "9f2148ae0b8ca60e241c980e37216eb416e1342d51992266fcd5d18b8092b820" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + 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: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" + url: "https://pub.dev" + source: hosted + version: "1.29.0" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + test_core: + dependency: transitive + description: + name: test_core + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" + url: "https://pub.dev" + source: hosted + version: "0.6.15" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.11.0-296.5.beta <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..f5f2ad7 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,23 @@ +name: chat_server +description: A server app using the shelf package and Docker. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.11.0-296.5.beta + +dependencies: + relic: + hotreloader: + dart_jsonwebtoken: + bcrypt: + objectbox: + fox_utils2: + path: ../fox_utils2 + +dev_dependencies: + objectbox_generator: + build_runner: + http: + lints: + test: diff --git a/test/server_test.dart b/test/server_test.dart new file mode 100644 index 0000000..3081d87 --- /dev/null +++ b/test/server_test.dart @@ -0,0 +1,39 @@ +import 'dart:io'; + +import 'package:http/http.dart'; +import 'package:test/test.dart'; + +void main() { + final port = '8080'; + final host = 'http://0.0.0.0:$port'; + late Process p; + + setUp(() async { + p = await Process.start( + 'dart', + ['run', 'bin/server.dart'], + environment: {'PORT': port}, + ); + // Wait for server to start and print to stdout. + await p.stdout.first; + }); + + tearDown(() => p.kill()); + + test('Root', () async { + final response = await get(Uri.parse('$host/')); + expect(response.statusCode, 200); + expect(response.body, 'Hello, World!\n'); + }); + + test('Echo', () async { + final response = await get(Uri.parse('$host/echo/hello')); + expect(response.statusCode, 200); + expect(response.body, 'hello\n'); + }); + + test('404', () async { + final response = await get(Uri.parse('$host/foobar')); + expect(response.statusCode, 404); + }); +}