This commit is contained in:
Foxyz committed 2026-02-20 01:54:08 +03:00
commit a99248b284
25 files changed
+1876

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
.dockerignore
Dockerfile
build/
.dart_tool/
.git/
.github/
.gitignore
.idea/
.packages
+3
View File
@@ -0,0 +1,3 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
+3
View File
@@ -0,0 +1,3 @@
## 1.0.0
- Initial version.
+21
View File
@@ -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"]
+49
View File
@@ -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/<message>`
# 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
```
+30
View File
@@ -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
+59
View File
@@ -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<ChatUser> userBox = store.box<ChatUser>();
final Box<ChatChannel> channelBox = store.box<ChatChannel>();
final Box<ChatMessage> messageBox = store.box<ChatMessage>();
RelicApp? _currentApp;
Future<void> 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();
}
+9
View File
@@ -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);
}
+122
View File
@@ -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<String> 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<String>? 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();
}
+136
View File
@@ -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
}
+418
View File
@@ -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>[
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>[
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: <obx_int.ModelRelation>[],
backlinks: <obx_int.ModelBacklink>[],
),
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>[
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: <obx_int.ModelRelation>[],
backlinks: <obx_int.ModelBacklink>[],
),
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>[
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: <obx_int.ModelRelation>[],
backlinks: <obx_int.ModelBacklink>[],
),
];
/// 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 = <Type, obx_int.EntityDefinition>{
ChatChannel: obx_int.EntityDefinition<ChatChannel>(
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<ChatMessage>(
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<ChatUser>(
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<ChatChannel>(
_entities[0].properties[0],
);
/// See [ChatChannel.name].
static final name = obx.QueryStringProperty<ChatChannel>(
_entities[0].properties[1],
);
/// See [ChatChannel.description].
static final description = obx.QueryStringProperty<ChatChannel>(
_entities[0].properties[2],
);
/// See [ChatChannel.createdAt].
static final createdAt = obx.QueryDateProperty<ChatChannel>(
_entities[0].properties[3],
);
}
/// [ChatMessage] entity fields to define ObjectBox queries.
class ChatMessage_ {
/// See [ChatMessage.id].
static final id = obx.QueryIntegerProperty<ChatMessage>(
_entities[1].properties[0],
);
/// See [ChatMessage.content].
static final content = obx.QueryStringProperty<ChatMessage>(
_entities[1].properties[1],
);
/// See [ChatMessage.senderId].
static final senderId = obx.QueryIntegerProperty<ChatMessage>(
_entities[1].properties[2],
);
/// See [ChatMessage.channelId].
static final channelId = obx.QueryIntegerProperty<ChatMessage>(
_entities[1].properties[3],
);
/// See [ChatMessage.createdAt].
static final createdAt = obx.QueryDateProperty<ChatMessage>(
_entities[1].properties[4],
);
}
/// [ChatUser] entity fields to define ObjectBox queries.
class ChatUser_ {
/// See [ChatUser.id].
static final id = obx.QueryIntegerProperty<ChatUser>(
_entities[2].properties[0],
);
/// See [ChatUser.username].
static final username = obx.QueryStringProperty<ChatUser>(
_entities[2].properties[1],
);
/// See [ChatUser.passwordHash].
static final passwordHash = obx.QueryStringProperty<ChatUser>(
_entities[2].properties[2],
);
/// See [ChatUser.createdAt].
static final createdAt = obx.QueryDateProperty<ChatUser>(
_entities[2].properties[3],
);
}
+34
View File
@@ -0,0 +1,34 @@
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:relic/relic.dart';
import '../../config/config.dart';
final userIdProperty = ContextProperty<String>('userId');
final usernameProperty = ContextProperty<String>('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'));
}
},
);
+34
View File
@@ -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<String, dynamic>? verifyToken(String token) {
try {
final jwt = JWT.verify(token, SecretKey(Config.jwtSecret));
return Map<String, dynamic>.from(jwt.payload as Map);
} catch (_) {
return null;
}
}
}
+54
View File
@@ -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<Response> 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<Response> 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);
}
+37
View File
@@ -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<Response> 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<Response> 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);
}
+38
View File
@@ -0,0 +1,38 @@
import 'package:chat_server/objectbox.g.dart';
import 'package:relic/relic.dart';
import '../utils/extensions.dart';
Future<Response> debugUsersHandler(Request req) async {
return Response.ok(
body: {
'users': userBox.getAll().map((u) => {'id': u.id, 'username': u.username}).toList(),
}.toBody,
);
}
Future<Response> 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<Response> debugClearHandler(Request req) async {
messageBox.removeAll();
channelBox.removeAll();
userBox.removeAll();
return Response.ok(body: {'cleared': true}.toBody);
}
+85
View File
@@ -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<Response> getMessagesHandler(Request req) async {
final channelIdStr = req.url.queryParameters['channelId'];
Query<ChatMessage> 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<Response> 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,
);
}
+14
View File
@@ -0,0 +1,14 @@
import 'package:chat_server/objectbox.g.dart';
import 'package:relic/relic.dart';
import '../utils/extensions.dart';
Future<Response> 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,
);
}
+23
View File
@@ -0,0 +1,23 @@
import 'dart:convert';
import 'package:relic/relic.dart';
extension RequestHelper on Request {
/// Parses the request body as JSON
Future<Map<String, dynamic>> asJson() async {
final bodyText = await readAsString();
return jsonDecode(bodyText);
}
}
extension MapToBody on Map<String, dynamic> {
/// 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<String, dynamic> json) {
return Body.fromString(jsonEncode(json));
}
}
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+636
View File
@@ -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"
+23
View File
@@ -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:
+39
View File
@@ -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);
});
}