init
This commit is contained in:
commit
a99248b284
25 files changed
+1876
No files matched your search
@@ -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
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user