Compare commits
1
Commits
master
..
a99248b284
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a99248b284 |
No files matched your search
@@ -0,0 +1,9 @@
|
|||||||
|
.dockerignore
|
||||||
|
Dockerfile
|
||||||
|
build/
|
||||||
|
.dart_tool/
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
.gitignore
|
||||||
|
.idea/
|
||||||
|
.packages
|
||||||
+2
-22
@@ -1,23 +1,3 @@
|
|||||||
# Dart
|
# https://dart.dev/guides/libraries/private-files
|
||||||
|
# Created by `dart pub`
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
.packages
|
|
||||||
|
|
||||||
# ObjectBox generated
|
|
||||||
*.g.dart
|
|
||||||
objectbox-model.json
|
|
||||||
|
|
||||||
# Build
|
|
||||||
build/
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.iml
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
-248
@@ -1,248 +0,0 @@
|
|||||||
# Chat Server Architecture - Complete Reference
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Self-hosted Discord alternative with decentralized identity. Each user has a "home server" where their identity lives, and can join other "guest servers" seamlessly using cryptographic signatures.
|
|
||||||
|
|
||||||
## Core Architecture Decisions
|
|
||||||
|
|
||||||
### Home Server Model
|
|
||||||
- **NO identity server** - just use one regular server as "home"
|
|
||||||
- **NO federation protocol** - servers don't talk to each other
|
|
||||||
- **Client does everything** - syncs, cross-server requests, profile updates
|
|
||||||
|
|
||||||
### Identity System
|
|
||||||
- Ed25519 keypair generated on registration
|
|
||||||
- Private key = proof of identity
|
|
||||||
- Public key stored on home server at `/.well-known/users/:username`
|
|
||||||
- userId format: `username@serverhost`
|
|
||||||
|
|
||||||
### Single Sign-On Flow
|
|
||||||
1. User registers/logs in ONCE on home server
|
|
||||||
2. Gets JWT token + encrypted keypair
|
|
||||||
3. To join other servers: sign a message with private key
|
|
||||||
4. Other server verifies signature against home server's public key
|
|
||||||
5. No password ever sent to guest servers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What's Implemented
|
|
||||||
|
|
||||||
### ✅ Done
|
|
||||||
- User model with keypair, home server reference
|
|
||||||
- Channel and Message models
|
|
||||||
- Crypto service (keypair generation, signing, verification)
|
|
||||||
- JWT authentication middleware
|
|
||||||
- Register handler (creates keypair)
|
|
||||||
- Login handler (returns encrypted keypair for device sync)
|
|
||||||
- Join server handler (signature-based verification - stub)
|
|
||||||
- Public endpoints for cross-server identity verification
|
|
||||||
- Channel CRUD
|
|
||||||
- Message CRUD with author caching
|
|
||||||
|
|
||||||
### ❌ Not Implemented
|
|
||||||
- HTTP client for cross-server public key fetching
|
|
||||||
- WebSocket for real-time messages
|
|
||||||
- Profile sync (push updates to all servers)
|
|
||||||
- Sticker packs (future feature)
|
|
||||||
- File uploads
|
|
||||||
- Permissions system (roles exist but not enforced)
|
|
||||||
- Message editing/deletion permissions
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Database Models
|
|
||||||
|
|
||||||
```dart
|
|
||||||
// User - works for both HOME and GUEST servers
|
|
||||||
@Entity()
|
|
||||||
class User {
|
|
||||||
int id;
|
|
||||||
String userId; // "alice@homeserver.com"
|
|
||||||
String username;
|
|
||||||
String? displayName, avatarUrl, bio;
|
|
||||||
|
|
||||||
String publicKey; // Ed25519, always stored
|
|
||||||
|
|
||||||
// HOME SERVER ONLY:
|
|
||||||
String? passwordHash;
|
|
||||||
String? encryptedPrivateKey; // For device sync
|
|
||||||
String? keyPairSalt;
|
|
||||||
|
|
||||||
bool isLocal; // true = this is home server
|
|
||||||
String? homeServer; // if guest, where is home?
|
|
||||||
List<String> roles; // Server-specific
|
|
||||||
DateTime createdAt, lastSyncedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity()
|
|
||||||
class Channel {
|
|
||||||
int id;
|
|
||||||
String name;
|
|
||||||
String? description;
|
|
||||||
DateTime createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity()
|
|
||||||
class Message {
|
|
||||||
int id;
|
|
||||||
int channelId;
|
|
||||||
|
|
||||||
// Author info (includes cross-server fetch capability)
|
|
||||||
int authorLocalId;
|
|
||||||
String authorUserId; // "alice@home.server"
|
|
||||||
String? authorHomeServer;
|
|
||||||
|
|
||||||
// Cached snapshot at message time
|
|
||||||
String authorUsername;
|
|
||||||
String? authorAvatarUrl;
|
|
||||||
|
|
||||||
String content;
|
|
||||||
DateTime createdAt, editedAt;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### Public (No Auth)
|
|
||||||
```
|
|
||||||
POST /auth/register → Create account on HOME server (generates keypair)
|
|
||||||
POST /auth/login → Login to HOME server (returns keypair backup)
|
|
||||||
POST /auth/join → Join THIS server with signature (no password)
|
|
||||||
|
|
||||||
GET /.well-known/users/:username → Public profile + public key
|
|
||||||
GET /.well-known/verify/:userId → Verify user exists
|
|
||||||
GET /.well-known/server → Server info
|
|
||||||
```
|
|
||||||
|
|
||||||
### Protected (JWT Required)
|
|
||||||
```
|
|
||||||
GET /api/me → Current user profile
|
|
||||||
|
|
||||||
GET /api/channels → List channels
|
|
||||||
POST /api/channels → Create channel
|
|
||||||
GET /api/channels/:id → Get channel
|
|
||||||
|
|
||||||
GET /api/messages?channelId=X → Get messages
|
|
||||||
POST /api/messages → Send message
|
|
||||||
PUT /api/messages/:id → Edit message
|
|
||||||
DELETE /api/messages/:id → Delete message
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
```
|
|
||||||
lib/
|
|
||||||
├── server.dart # Entry point, routes, ObjectBox init
|
|
||||||
├── models.dart # User, Channel, Message entities
|
|
||||||
├── objectbox.g.dart # Generated
|
|
||||||
│
|
|
||||||
├── config/config.dart # Server host, JWT secret, port
|
|
||||||
│
|
|
||||||
└── src/
|
|
||||||
├── auth/
|
|
||||||
│ ├── crypto_service.dart # Keypair, signing, encryption
|
|
||||||
│ └── auth_middleware.dart # JWT verification
|
|
||||||
│
|
|
||||||
├── handlers/
|
|
||||||
│ ├── auth_handlers.dart # register, login, join, me
|
|
||||||
│ ├── public_handlers.dart # .well-known/*
|
|
||||||
│ ├── channel_handlers.dart
|
|
||||||
│ └── message_handlers.dart
|
|
||||||
│
|
|
||||||
└── utils/extensions.dart # Request/Response helpers
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Join Server Flow (Needs Implementation)
|
|
||||||
|
|
||||||
```
|
|
||||||
CLIENT GUEST SERVER HOME SERVER
|
|
||||||
│ │ │
|
|
||||||
│ POST /auth/join │ │
|
|
||||||
│ {userId, homeServer, timestamp, │ │
|
|
||||||
│ signature} │ │
|
|
||||||
│─────────────────────────────────────►│ │
|
|
||||||
│ │ │
|
|
||||||
│ │ GET /.well-known/users/ │
|
|
||||||
│ │ alice │
|
|
||||||
│ │────────────────────────►│
|
|
||||||
│ │ │
|
|
||||||
│ │ {publicKey, ...} │
|
|
||||||
│ │◄────────────────────────│
|
|
||||||
│ │ │
|
|
||||||
│ │ verify signature │
|
|
||||||
│ │ create shadow account │
|
|
||||||
│ │ │
|
|
||||||
│ {token, localId} │ │
|
|
||||||
│◄─────────────────────────────────────│ │
|
|
||||||
```
|
|
||||||
|
|
||||||
The `fetchPublicKeyFromHomeServer()` function in `auth_handlers.dart` needs actual HTTP implementation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
- **Framework**: Relic 1.0 (Dart web server)
|
|
||||||
- **Database**: ObjectBox
|
|
||||||
- **Auth**: JWT + Ed25519 signatures
|
|
||||||
- **Crypto**: crypton package for Ed25519
|
|
||||||
- **Password**: bcrypt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Design Decisions Rationale
|
|
||||||
|
|
||||||
| Decision | Why |
|
|
||||||
|----------|-----|
|
|
||||||
| No identity server | Avoids hosting another service, one server = home |
|
|
||||||
| No server-to-server | Eliminates federation complexity, client handles everything |
|
|
||||||
| Keypair identity | Allows passwordless joins, proves identity cryptographically |
|
|
||||||
| Message author caching | Messages readable even if home server is down |
|
|
||||||
| userId format `user@server` | Simple, familiar, self-documenting |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Constraints
|
|
||||||
|
|
||||||
- User CANNOT quit home server (that's where identity lives)
|
|
||||||
- User CANNOT change home server easily (migration = future feature)
|
|
||||||
- Home server MUST be online to join NEW servers
|
|
||||||
- Home server offline = can still use already-joined servers (cached keys)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Future Features (Out of Scope for MVP)
|
|
||||||
|
|
||||||
1. **Sticker Packs** - Telegram-style, live on community servers, clients cache
|
|
||||||
2. **Home Server Migration** - Sign migration certificate, chain of trust
|
|
||||||
3. **File Uploads** - With cross-server URLs
|
|
||||||
4. **Message Forwarding** - Embed with link to original
|
|
||||||
5. **Permissions** - Role-based access to channels
|
|
||||||
6. **WebSocket** - Real-time message delivery
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Config to Change
|
|
||||||
|
|
||||||
```dart
|
|
||||||
// lib/config/config.dart
|
|
||||||
static const String serverHost = 'your-actual-domain.com'; // CHANGE THIS
|
|
||||||
static const String jwtSecret = 'use-env-variable'; // CHANGE THIS
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. Implement HTTP client in `fetchPublicKeyFromHomeServer()`
|
|
||||||
2. Add WebSocket for real-time
|
|
||||||
3. Add profile sync (client pushes to all servers on change)
|
|
||||||
4. Test cross-server join flow
|
|
||||||
5. Add proper error handling and logging
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- Initial version.
|
||||||
+21
@@ -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"]
|
||||||
@@ -1,46 +1,49 @@
|
|||||||
# Chat Server
|
A server app built using [Shelf](https://pub.dev/packages/shelf),
|
||||||
|
configured to enable running with [Docker](https://www.docker.com/).
|
||||||
|
|
||||||
A self-hosted Discord alternative with decentralized identity using the Home Server model.
|
This sample code handles HTTP GET requests to `/` and `/echo/<message>`
|
||||||
|
|
||||||
## Architecture
|
# Running the sample
|
||||||
|
|
||||||
Each user has a **home server** where their identity lives (keypair), and can seamlessly join other **guest servers** using cryptographic signatures.
|
## Running with the Dart SDK
|
||||||
|
|
||||||
- **NO identity server** - just use one regular server as "home"
|
You can run the example with the [Dart SDK](https://dart.dev/get-dart)
|
||||||
- **NO federation protocol** - servers don't talk to each other
|
like this:
|
||||||
- **Client does everything** - syncs, cross-server requests, profile updates
|
|
||||||
|
|
||||||
See [ARCHITECTURE.md](./ARCHITECTURE.md) for full details.
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
dart pub get
|
|
||||||
|
|
||||||
# Generate ObjectBox code
|
|
||||||
dart run build_runner build
|
|
||||||
|
|
||||||
# Run server
|
|
||||||
dart run lib/server.dart
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
```
|
||||||
lib/
|
$ dart run bin/server.dart
|
||||||
├── server.dart # Entry point, routes
|
Server listening on port 8080
|
||||||
├── models.dart # User, Channel, Message entities
|
|
||||||
├── objectbox.g.dart # Generated
|
|
||||||
├── config/config.dart # Server config
|
|
||||||
└── src/
|
|
||||||
├── auth/ # Keypair, JWT, middleware
|
|
||||||
├── handlers/ # API endpoints
|
|
||||||
└── utils/ # Helpers
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tech Stack
|
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
|
||||||
|
```
|
||||||
|
|
||||||
- **Framework**: Relic 1.0
|
## Running with Docker
|
||||||
- **Database**: ObjectBox
|
|
||||||
- **Auth**: JWT + Ed25519 signatures
|
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
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
class Config {
|
class Config {
|
||||||
// Server settings
|
|
||||||
static const int port = 9870;
|
static const int port = 9870;
|
||||||
|
|
||||||
// IMPORTANT: Set this to your actual server host/domain
|
|
||||||
// Used for generating userId: "username@serverHost"
|
|
||||||
static const String serverHost = 'localhost:9870';
|
|
||||||
static const String serverName = 'Chat Server';
|
|
||||||
|
|
||||||
// JWT settings
|
|
||||||
// TODO: Move to environment variable in production
|
// TODO: Move to environment variable in production
|
||||||
static const String jwtSecret = 'your_super_secret_key_change_in_production';
|
static const String jwtSecret = 'your_super_secret_key';
|
||||||
|
|
||||||
|
// Token expiration
|
||||||
static const Duration tokenExpiry = Duration(days: 7);
|
static const Duration tokenExpiry = Duration(days: 7);
|
||||||
}
|
}
|
||||||
+1
-5
@@ -81,11 +81,7 @@ class Channel {
|
|||||||
@Property(type: PropertyType.date)
|
@Property(type: PropertyType.date)
|
||||||
DateTime createdAt;
|
DateTime createdAt;
|
||||||
|
|
||||||
Channel({
|
Channel({required this.name, this.description, DateTime? createdAt}) : createdAt = createdAt ?? DateTime.now();
|
||||||
required this.name,
|
|
||||||
this.description,
|
|
||||||
DateTime? createdAt,
|
|
||||||
}) : createdAt = createdAt ?? DateTime.now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
|||||||
@@ -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],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import 'package:chat_server/models.dart';
|
|
||||||
import 'package:chat_server/objectbox.g.dart';
|
|
||||||
import 'package:hotreloader/hotreloader.dart';
|
|
||||||
import 'package:relic/relic.dart';
|
|
||||||
|
|
||||||
import 'config/config.dart';
|
|
||||||
import 'src/auth/auth_middleware.dart';
|
|
||||||
import 'src/handlers/auth_handlers.dart';
|
|
||||||
import 'src/handlers/channel_handlers.dart';
|
|
||||||
import 'src/handlers/message_handlers.dart';
|
|
||||||
import 'src/handlers/public_handlers.dart';
|
|
||||||
import 'src/utils/extensions.dart';
|
|
||||||
|
|
||||||
// Global ObjectBox store and boxes
|
|
||||||
late final Store store;
|
|
||||||
late final Box<User> userBox;
|
|
||||||
late final Box<Channel> channelBox;
|
|
||||||
late final Box<Message> messageBox;
|
|
||||||
|
|
||||||
RelicApp? _currentApp;
|
|
||||||
|
|
||||||
Future<void> startServer() async {
|
|
||||||
await _currentApp?.close();
|
|
||||||
|
|
||||||
_currentApp = RelicApp()
|
|
||||||
// ============ PUBLIC ENDPOINTS (no auth) ============
|
|
||||||
|
|
||||||
// Auth
|
|
||||||
..post('/auth/register', registerHandler)
|
|
||||||
..post('/auth/login', loginHandler)
|
|
||||||
..post('/auth/join', joinServerHandler)
|
|
||||||
|
|
||||||
// Public identity verification (for cross-server)
|
|
||||||
..get('/.well-known/users/<username>', publicUserHandler)
|
|
||||||
..get('/.well-known/verify/<userId>', publicVerifyHandler)
|
|
||||||
..get('/.well-known/server', publicServerInfoHandler)
|
|
||||||
|
|
||||||
// ============ PROTECTED ENDPOINTS (requires auth) ============
|
|
||||||
..use('/api', jwtMiddleware)
|
|
||||||
|
|
||||||
// Current user
|
|
||||||
..get('/api/me', getMeHandler)
|
|
||||||
|
|
||||||
// Channels
|
|
||||||
..get('/api/channels', getChannelsHandler)
|
|
||||||
..post('/api/channels', createChannelHandler)
|
|
||||||
..get('/api/channels/<channelId>', getChannelHandler)
|
|
||||||
|
|
||||||
// Messages
|
|
||||||
..get('/api/messages', getMessagesHandler)
|
|
||||||
..post('/api/messages', sendMessageHandler)
|
|
||||||
..put('/api/messages/<messageId>', editMessageHandler)
|
|
||||||
..delete('/api/messages/<messageId>', deleteMessageHandler)
|
|
||||||
|
|
||||||
// ============ LOGGING & FALLBACK ============
|
|
||||||
..use('/', logRequests())
|
|
||||||
..fallback = respondWith((_) => Response.notFound());
|
|
||||||
|
|
||||||
print('');
|
|
||||||
print('╔════════════════════════════════════════════╗');
|
|
||||||
print('║ 🚀 Server starting... ║');
|
|
||||||
print('║ Host: ${Config.serverHost.padRight(34)}║');
|
|
||||||
print('║ URL: http://localhost:${Config.port.toString().padRight(26)}║');
|
|
||||||
print('╚════════════════════════════════════════════╝');
|
|
||||||
print('');
|
|
||||||
|
|
||||||
await _currentApp!.serve(port: Config.port);
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() async {
|
|
||||||
// Initialize ObjectBox
|
|
||||||
store = openStore();
|
|
||||||
userBox = store.box<User>();
|
|
||||||
channelBox = store.box<Channel>();
|
|
||||||
messageBox = store.box<Message>();
|
|
||||||
|
|
||||||
// Enable hot reload in development
|
|
||||||
await HotReloader.create(
|
|
||||||
onAfterReload: (ctx) async {
|
|
||||||
print('🔄 Changes detected! Restarting server...');
|
|
||||||
await startServer();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await startServer();
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
|
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||||
import 'package:relic/relic.dart';
|
import 'package:relic/relic.dart';
|
||||||
|
|
||||||
import '../../config/config.dart';
|
import '../../config/config.dart';
|
||||||
import '../utils/extensions.dart';
|
|
||||||
import 'crypto_service.dart';
|
|
||||||
|
|
||||||
final userIdProperty = ContextProperty<String>('userId');
|
final userIdProperty = ContextProperty<String>('userId');
|
||||||
final usernameProperty = ContextProperty<String>('username');
|
final usernameProperty = ContextProperty<String>('username');
|
||||||
final userLocalIdProperty = ContextProperty<int>('userLocalId');
|
|
||||||
|
|
||||||
/// JWT authentication middleware
|
/// JWT authentication middleware
|
||||||
final jwtMiddleware = createMiddleware(
|
final jwtMiddleware = createMiddleware(
|
||||||
@@ -14,32 +12,23 @@ final jwtMiddleware = createMiddleware(
|
|||||||
final authHeader = request.headers.authorization;
|
final authHeader = request.headers.authorization;
|
||||||
|
|
||||||
if (authHeader is! BearerAuthorizationHeader) {
|
if (authHeader is! BearerAuthorizationHeader) {
|
||||||
return Response.unauthorized(
|
return Response.unauthorized(body: Body.fromString('Bearer token required'));
|
||||||
body: Body.fromString('Bearer token required'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final payload = AuthService.verifyToken(authHeader.token);
|
try {
|
||||||
if (payload == null) {
|
final jwt = JWT.verify(authHeader.token, SecretKey(Config.jwtSecret));
|
||||||
return Response.forbidden(
|
|
||||||
body: Body.fromString('Invalid or expired token'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final userId = payload['sub'] as String?;
|
final userId = jwt.payload['sub'] as String? ?? jwt.payload['id'] as String;
|
||||||
final username = payload['username'] as String?;
|
final username = jwt.payload['username'] as String?;
|
||||||
|
|
||||||
if (userId == null) {
|
|
||||||
return Response.forbidden(
|
|
||||||
body: Body.fromString('Invalid token payload'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
userIdProperty[request] = userId;
|
userIdProperty[request] = userId;
|
||||||
if (username != null) {
|
if (username != null) {
|
||||||
usernameProperty[request] = username;
|
usernameProperty[request] = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null; // Continue to handler
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:bcrypt/bcrypt.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
import 'package:crypton/crypton.dart';
|
|
||||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
|
||||||
|
|
||||||
import '../config/config.dart';
|
|
||||||
|
|
||||||
class KeyPair {
|
|
||||||
final String publicKey;
|
|
||||||
final String privateKey;
|
|
||||||
|
|
||||||
KeyPair({required this.publicKey, required this.privateKey});
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'publicKey': publicKey,
|
|
||||||
'privateKey': privateKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
factory KeyPair.fromJson(Map<String, dynamic> json) {
|
|
||||||
return KeyPair(
|
|
||||||
publicKey: json['publicKey'] as String,
|
|
||||||
privateKey: json['privateKey'] as String,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CryptoService {
|
|
||||||
/// Generate a new Ed25519 key pair
|
|
||||||
static KeyPair generateKeyPair() {
|
|
||||||
final keyPair = ECKeypair.fromRandom();
|
|
||||||
return KeyPair(
|
|
||||||
publicKey: keyPair.publicKey.toString(),
|
|
||||||
privateKey: keyPair.privateKey.toString(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sign data with private key
|
|
||||||
static String sign(String data, String privateKey) {
|
|
||||||
final privKey = ECPrivateKey.fromString(privateKey);
|
|
||||||
final signature = privKey.createSignature(data);
|
|
||||||
return signature.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify signature with public key
|
|
||||||
static bool verify(String data, String signature, String publicKey) {
|
|
||||||
try {
|
|
||||||
final pubKey = ECPublicKey.fromString(publicKey);
|
|
||||||
final sig = ECSignature.fromString(signature);
|
|
||||||
return pubKey.verifySignature(data, sig);
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encrypt private key with password (for storage/backup)
|
|
||||||
static EncryptedKey encryptPrivateKey(String privateKey, String password) {
|
|
||||||
final salt = BCrypt.gensalt();
|
|
||||||
final key = _deriveKey(password, salt);
|
|
||||||
final encrypted = _xorEncrypt(privateKey, key);
|
|
||||||
return EncryptedKey(
|
|
||||||
encryptedData: encrypted,
|
|
||||||
salt: salt,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decrypt private key with password
|
|
||||||
static String? decryptPrivateKey(
|
|
||||||
String encryptedData, String salt, String password) {
|
|
||||||
try {
|
|
||||||
final key = _deriveKey(password, salt);
|
|
||||||
return _xorDecrypt(encryptedData, key);
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static String _deriveKey(String password, String salt) {
|
|
||||||
final bytes = utf8.encode(password + salt);
|
|
||||||
final hash = sha256.convert(bytes);
|
|
||||||
return hash.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
static String _xorEncrypt(String data, String key) {
|
|
||||||
final dataBytes = utf8.encode(data);
|
|
||||||
final keyBytes = utf8.encode(key);
|
|
||||||
final encrypted = <int>[];
|
|
||||||
for (var i = 0; i < dataBytes.length; i++) {
|
|
||||||
encrypted.add(dataBytes[i] ^ keyBytes[i % keyBytes.length]);
|
|
||||||
}
|
|
||||||
return base64Encode(encrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String _xorDecrypt(String encrypted, String key) {
|
|
||||||
final encryptedBytes = base64Decode(encrypted);
|
|
||||||
final keyBytes = utf8.encode(key);
|
|
||||||
final decrypted = <int>[];
|
|
||||||
for (var i = 0; i < encryptedBytes.length; i++) {
|
|
||||||
decrypted.add(encryptedBytes[i] ^ keyBytes[i % keyBytes.length]);
|
|
||||||
}
|
|
||||||
return utf8.decode(decrypted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EncryptedKey {
|
|
||||||
final String encryptedData;
|
|
||||||
final String salt;
|
|
||||||
|
|
||||||
EncryptedKey({required this.encryptedData, required this.salt});
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'encryptedData': encryptedData,
|
|
||||||
'salt': salt,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class AuthService {
|
|
||||||
/// Generate JWT token for 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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify JWT token and return 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hash password with bcrypt
|
|
||||||
static String hashPassword(String password) {
|
|
||||||
return BCrypt.hashpw(password, BCrypt.gensalt());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify password against hash
|
|
||||||
static bool verifyPassword(String password, String hash) {
|
|
||||||
return BCrypt.checkpw(password, hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate a nonce for signature verification
|
|
||||||
static String generateNonce() {
|
|
||||||
final bytes = List<int>.generate(32, (_) => DateTime.now().microsecondsSinceEpoch % 256);
|
|
||||||
final randomBytes = sha256.convert(bytes as Uint8List);
|
|
||||||
return randomBytes.toString().substring(0, 32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,293 +2,53 @@ import 'package:chat_server/models.dart';
|
|||||||
import 'package:chat_server/objectbox.g.dart';
|
import 'package:chat_server/objectbox.g.dart';
|
||||||
import 'package:relic/relic.dart';
|
import 'package:relic/relic.dart';
|
||||||
|
|
||||||
import '../auth/auth_middleware.dart';
|
import '../auth/auth_service.dart';
|
||||||
import '../auth/crypto_service.dart';
|
|
||||||
import '../utils/extensions.dart';
|
import '../utils/extensions.dart';
|
||||||
|
|
||||||
/// Register on HOME server
|
|
||||||
/// Creates user with keypair for identity
|
|
||||||
Future<Response> registerHandler(Request req) async {
|
|
||||||
final body = await req.asJson();
|
|
||||||
final username = (body['username'] as String?)?.trim();
|
|
||||||
final password = body['password'] as String?;
|
|
||||||
|
|
||||||
if (username == null || username.isEmpty) {
|
|
||||||
return Response.badRequest(
|
|
||||||
body: Body.fromString('Username required'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password == null || password.length < 6) {
|
|
||||||
return Response.badRequest(
|
|
||||||
body: Body.fromString('Password must be at least 6 characters'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if username exists
|
|
||||||
final query = userBox.query(User_.username.equals(username)).build();
|
|
||||||
final existing = query.findFirst();
|
|
||||||
query.close();
|
|
||||||
|
|
||||||
if (existing != null) {
|
|
||||||
return Response.badRequest(
|
|
||||||
body: Body.fromString('Username already taken'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate keypair for identity
|
|
||||||
final keyPair = CryptoService.generateKeyPair();
|
|
||||||
|
|
||||||
// Encrypt private key with password
|
|
||||||
final encryptedKey = CryptoService.encryptPrivateKey(
|
|
||||||
keyPair.privateKey,
|
|
||||||
password,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Hash password
|
|
||||||
final passwordHash = AuthService.hashPassword(password);
|
|
||||||
|
|
||||||
// Create user with userId format: "username@this.server"
|
|
||||||
final userId = '${username}@${Config.serverHost}';
|
|
||||||
|
|
||||||
final user = User(
|
|
||||||
userId: userId,
|
|
||||||
username: username,
|
|
||||||
publicKey: keyPair.publicKey,
|
|
||||||
isLocal: true,
|
|
||||||
passwordHash: passwordHash,
|
|
||||||
encryptedPrivateKey: encryptedKey.encryptedData,
|
|
||||||
keyPairSalt: encryptedKey.salt,
|
|
||||||
roles: ['member'],
|
|
||||||
);
|
|
||||||
|
|
||||||
final localId = userBox.put(user);
|
|
||||||
|
|
||||||
// Generate token
|
|
||||||
final token = AuthService.generateToken(userId, username);
|
|
||||||
|
|
||||||
return Response.ok(
|
|
||||||
body: {
|
|
||||||
'token': token,
|
|
||||||
'userId': userId,
|
|
||||||
'username': username,
|
|
||||||
'publicKey': keyPair.publicKey,
|
|
||||||
'encryptedPrivateKey': encryptedKey.encryptedData,
|
|
||||||
'keyPairSalt': encryptedKey.salt,
|
|
||||||
'localId': localId,
|
|
||||||
}.toBody,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Login on HOME server
|
|
||||||
/// Returns token + encrypted keypair for device sync
|
|
||||||
Future<Response> loginHandler(Request req) async {
|
Future<Response> loginHandler(Request req) async {
|
||||||
final body = await req.asJson();
|
final body = await req.asJson();
|
||||||
final username = body['username'] as String?;
|
final username = body['username'] as String?;
|
||||||
final password = body['password'] as String?;
|
final password = body['password'] as String?;
|
||||||
|
|
||||||
if (username == null || password == null) {
|
if (username == null || password == null) {
|
||||||
return Response.badRequest(
|
return Response.badRequest(body: Body.fromString('Username and password required'));
|
||||||
body: Body.fromString('Username and password required'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final query = userBox.query(User_.username.equals(username)).build();
|
final query = userBox.query(ChatUser_.username.equals(username)).build();
|
||||||
final user = query.findFirst();
|
final user = query.findFirst();
|
||||||
query.close();
|
query.close();
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null || !AuthService.verifyPassword(password, user.passwordHash)) {
|
||||||
return Response.unauthorized(
|
return Response.unauthorized(body: Body.fromString('Invalid credentials'));
|
||||||
body: Body.fromString('Invalid credentials'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is actually their home server
|
final token = AuthService.generateToken(user.id.toString(), username: user.username);
|
||||||
if (!user.isLocal) {
|
|
||||||
return Response.badRequest(
|
return Response.ok(body: {'token': token, 'userId': user.id, 'username': user.username}.toBody);
|
||||||
body: Body.fromString('This is not your home server. Login there instead.'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify password
|
Future<Response> registerHandler(Request req) async {
|
||||||
if (!AuthService.verifyPassword(password, user.passwordHash!)) {
|
|
||||||
return Response.unauthorized(
|
|
||||||
body: Body.fromString('Invalid credentials'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate token
|
|
||||||
final token = AuthService.generateToken(user.userId, user.username);
|
|
||||||
|
|
||||||
return Response.ok(
|
|
||||||
body: {
|
|
||||||
'token': token,
|
|
||||||
'userId': user.userId,
|
|
||||||
'username': user.username,
|
|
||||||
'displayName': user.displayName,
|
|
||||||
'avatarUrl': user.avatarUrl,
|
|
||||||
'publicKey': user.publicKey,
|
|
||||||
'encryptedPrivateKey': user.encryptedPrivateKey,
|
|
||||||
'keyPairSalt': user.keyPairSalt,
|
|
||||||
'localId': user.id,
|
|
||||||
}.toBody,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Join a GUEST server using signature verification
|
|
||||||
/// No password required - proves identity via private key signature
|
|
||||||
Future<Response> joinServerHandler(Request req) async {
|
|
||||||
final body = await req.asJson();
|
final body = await req.asJson();
|
||||||
final userId = body['userId'] as String?; // "alice@homeserver.com"
|
|
||||||
final homeServer = body['homeServer'] as String?; // "https://homeserver.com"
|
|
||||||
final timestamp = body['timestamp'] as int?;
|
|
||||||
final signature = body['signature'] as String?;
|
|
||||||
final username = body['username'] as String?;
|
final username = body['username'] as String?;
|
||||||
|
final password = body['password'] as String?;
|
||||||
|
|
||||||
// Validate required fields
|
if (username == null || password == null) {
|
||||||
if (userId == null || homeServer == null || timestamp == null || signature == null) {
|
return Response.badRequest(body: Body.fromString('Username and password required'));
|
||||||
return Response.badRequest(
|
|
||||||
body: Body.fromString('userId, homeServer, timestamp, and signature required'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check timestamp is recent (within 5 minutes)
|
final query = userBox.query(ChatUser_.username.equals(username)).build();
|
||||||
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
final existing = query.findFirst();
|
||||||
if ((now - timestamp).abs() > 300) {
|
|
||||||
return Response.badRequest(
|
|
||||||
body: Body.fromString('Timestamp expired. Please sync your clock.'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user already exists on this server
|
|
||||||
final existingQuery = userBox.query(User_.userId.equals(userId)).build();
|
|
||||||
final existingUser = existingQuery.findFirst();
|
|
||||||
existingQuery.close();
|
|
||||||
|
|
||||||
if (existingUser != null) {
|
|
||||||
// User already joined - just return a new token
|
|
||||||
final token = AuthService.generateToken(existingUser.userId, existingUser.username);
|
|
||||||
userLocalIdProperty[req] = existingUser.id;
|
|
||||||
|
|
||||||
return Response.ok(
|
|
||||||
body: {
|
|
||||||
'token': token,
|
|
||||||
'userId': existingUser.userId,
|
|
||||||
'username': existingUser.username,
|
|
||||||
'localId': existingUser.id,
|
|
||||||
'alreadyJoined': true,
|
|
||||||
}.toBody,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch public key from home server
|
|
||||||
final publicKey = await fetchPublicKeyFromHomeServer(homeServer, username ?? userId.split('@').first);
|
|
||||||
|
|
||||||
if (publicKey == null) {
|
|
||||||
return Response.badRequest(
|
|
||||||
body: Body.fromString('Could not verify identity with home server'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify signature
|
|
||||||
// Signature is over: "{userId}:{timestamp}"
|
|
||||||
final messageToVerify = '$userId:$timestamp';
|
|
||||||
final isValid = CryptoService.verify(messageToVerify, signature, publicKey);
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
return Response.forbidden(
|
|
||||||
body: Body.fromString('Invalid signature'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create local shadow account
|
|
||||||
final user = User(
|
|
||||||
userId: userId,
|
|
||||||
username: username ?? userId.split('@').first,
|
|
||||||
publicKey: publicKey,
|
|
||||||
isLocal: false,
|
|
||||||
homeServer: homeServer,
|
|
||||||
roles: ['member'],
|
|
||||||
);
|
|
||||||
|
|
||||||
final localId = userBox.put(user);
|
|
||||||
|
|
||||||
// Generate local token
|
|
||||||
final token = AuthService.generateToken(userId, user.username);
|
|
||||||
|
|
||||||
return Response.ok(
|
|
||||||
body: {
|
|
||||||
'token': token,
|
|
||||||
'userId': userId,
|
|
||||||
'username': user.username,
|
|
||||||
'localId': localId,
|
|
||||||
'alreadyJoined': false,
|
|
||||||
}.toBody,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch public key from user's home server
|
|
||||||
Future<String?> fetchPublicKeyFromHomeServer(
|
|
||||||
String homeServer,
|
|
||||||
String username,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
// Remove trailing slash
|
|
||||||
final baseUrl = homeServer.replaceAll(RegExp(r'/$'), '');
|
|
||||||
|
|
||||||
// Use http client to fetch
|
|
||||||
// Endpoint: GET /.well-known/users/{username}
|
|
||||||
// Returns: { publicKey, userId, ... }
|
|
||||||
|
|
||||||
// For now, return null to indicate we need http client
|
|
||||||
// This will be implemented with actual HTTP client
|
|
||||||
print('Would fetch: $baseUrl/.well-known/users/$username');
|
|
||||||
|
|
||||||
// TODO: Implement actual HTTP request
|
|
||||||
// final response = await http.get(Uri.parse('$baseUrl/.well-known/users/$username'));
|
|
||||||
// if (response.statusCode == 200) {
|
|
||||||
// final data = jsonDecode(response.body);
|
|
||||||
// return data['publicKey'] as String;
|
|
||||||
// }
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} catch (e) {
|
|
||||||
print('Error fetching public key: $e');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current user's profile
|
|
||||||
Future<Response> getMeHandler(Request req) async {
|
|
||||||
final userId = userIdProperty[req];
|
|
||||||
|
|
||||||
if (userId == null) {
|
|
||||||
return Response.unauthorized(
|
|
||||||
body: Body.fromString('Not authenticated'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final query = userBox.query(User_.userId.equals(userId)).build();
|
|
||||||
final user = query.findFirst();
|
|
||||||
query.close();
|
query.close();
|
||||||
|
|
||||||
if (user == null) {
|
if (existing != null) {
|
||||||
return Response.notFound(
|
return Response.badRequest(body: Body.fromString('User already exists'));
|
||||||
body: Body.fromString('User not found'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.ok(
|
final hashedPassword = AuthService.hashPassword(password);
|
||||||
body: {
|
final user = ChatUser(username: username, passwordHash: hashedPassword);
|
||||||
'userId': user.userId,
|
final userId = userBox.put(user);
|
||||||
'username': user.username,
|
|
||||||
'displayName': user.displayName,
|
final token = AuthService.generateToken(userId.toString(), username: username);
|
||||||
'avatarUrl': user.avatarUrl,
|
|
||||||
'bio': user.bio,
|
return Response.ok(body: {'token': token, 'userId': userId, 'username': username}.toBody);
|
||||||
'isLocal': user.isLocal,
|
|
||||||
'homeServer': user.homeServer,
|
|
||||||
'roles': user.roles,
|
|
||||||
'createdAt': user.createdAt.toIso8601String(),
|
|
||||||
}.toBody,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -4,79 +4,34 @@ import 'package:relic/relic.dart';
|
|||||||
|
|
||||||
import '../utils/extensions.dart';
|
import '../utils/extensions.dart';
|
||||||
|
|
||||||
/// Get all channels
|
|
||||||
Future<Response> getChannelsHandler(Request req) async {
|
Future<Response> getChannelsHandler(Request req) async {
|
||||||
final channels = channelBox.getAll();
|
final channels = channelBox.getAll();
|
||||||
|
|
||||||
return Response.ok(
|
return Response.ok(
|
||||||
body: {
|
body: {
|
||||||
'channels': channels
|
'channels': channels
|
||||||
.map((c) => {
|
.map((c) => {'id': c.id, 'name': c.name, 'description': c.description, 'createdAt': c.createdAt.toIso8601String()})
|
||||||
'id': c.id,
|
|
||||||
'name': c.name,
|
|
||||||
'description': c.description,
|
|
||||||
'createdAt': c.createdAt.toIso8601String(),
|
|
||||||
})
|
|
||||||
.toList(),
|
.toList(),
|
||||||
}.toBody,
|
}.toBody,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a channel (requires 'admin' role)
|
|
||||||
Future<Response> createChannelHandler(Request req) async {
|
Future<Response> createChannelHandler(Request req) async {
|
||||||
final body = await req.asJson();
|
final body = await req.asJson();
|
||||||
final name = (body['name'] as String?)?.trim().toLowerCase().replaceAll(' ', '-');
|
final name = (body['name'] as String?)?.trim().toLowerCase().replaceAll(' ', '-');
|
||||||
final description = body['description'] as String?;
|
final description = body['description'] as String?;
|
||||||
|
|
||||||
if (name == null || name.isEmpty) {
|
if (name == null || name.isEmpty) {
|
||||||
return Response.badRequest(
|
return Response.badRequest(body: Body.fromString('Channel name required'));
|
||||||
body: Body.fromString('Channel name required'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if channel exists
|
final existing = channelBox.query(ChatChannel_.name.equals(name)).build().findFirst();
|
||||||
final existing = channelBox.query(Channel_.name.equals(name)).build().findFirst();
|
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
return Response.badRequest(
|
return Response.badRequest(body: Body.fromString('Channel already exists'));
|
||||||
body: Body.fromString('Channel already exists'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final channel = Channel(name: name, description: description);
|
final channel = ChatChannel(name: name, description: description);
|
||||||
final id = channelBox.put(channel);
|
final id = channelBox.put(channel);
|
||||||
|
|
||||||
return Response.ok(
|
return Response.ok(body: {'id': id, 'name': name, 'description': description, 'createdAt': channel.createdAt.toIso8601String()}.toBody);
|
||||||
body: {
|
|
||||||
'id': id,
|
|
||||||
'name': name,
|
|
||||||
'description': description,
|
|
||||||
'createdAt': channel.createdAt.toIso8601String(),
|
|
||||||
}.toBody,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get channel by ID
|
|
||||||
Future<Response> getChannelHandler(Request req, String channelIdStr) async {
|
|
||||||
final channelId = int.tryParse(channelIdStr);
|
|
||||||
if (channelId == null) {
|
|
||||||
return Response.badRequest(
|
|
||||||
body: Body.fromString('Invalid channel ID'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final channel = channelBox.get(channelId);
|
|
||||||
if (channel == null) {
|
|
||||||
return Response.notFound(
|
|
||||||
body: Body.fromString('Channel not found'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.ok(
|
|
||||||
body: {
|
|
||||||
'id': channel.id,
|
|
||||||
'name': channel.name,
|
|
||||||
'description': channel.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);
|
||||||
|
}
|
||||||
@@ -5,234 +5,81 @@ import 'package:relic/relic.dart';
|
|||||||
import '../auth/auth_middleware.dart';
|
import '../auth/auth_middleware.dart';
|
||||||
import '../utils/extensions.dart';
|
import '../utils/extensions.dart';
|
||||||
|
|
||||||
/// Get messages for a channel
|
|
||||||
/// Query params: channelId (required), limit, before
|
|
||||||
Future<Response> getMessagesHandler(Request req) async {
|
Future<Response> getMessagesHandler(Request req) async {
|
||||||
final channelIdStr = req.url.queryParameters['channelId'];
|
final channelIdStr = req.url.queryParameters['channelId'];
|
||||||
final limitStr = req.url.queryParameters['limit'] ?? '50';
|
|
||||||
|
|
||||||
if (channelIdStr == null) {
|
Query<ChatMessage> query;
|
||||||
return Response.badRequest(
|
|
||||||
body: Body.fromString('channelId required'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (channelIdStr != null) {
|
||||||
final channelId = int.tryParse(channelIdStr);
|
final channelId = int.tryParse(channelIdStr);
|
||||||
final limit = int.tryParse(limitStr) ?? 50;
|
|
||||||
|
|
||||||
if (channelId == null) {
|
if (channelId == null) {
|
||||||
return Response.badRequest(
|
return Response.badRequest(body: Body.fromString('Invalid channelId'));
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check channel exists
|
final messages = query.find();
|
||||||
final channel = channelBox.get(channelId);
|
|
||||||
if (channel == null) {
|
|
||||||
return Response.notFound(
|
|
||||||
body: Body.fromString('Channel not found'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get messages
|
|
||||||
final query = messageBox
|
|
||||||
.query(Message_.channelId.equals(channelId))
|
|
||||||
.order(Message_.createdAt, flags: Order.descending)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
final messages = query.find(limit: limit);
|
|
||||||
query.close();
|
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(
|
return Response.ok(
|
||||||
body: {
|
body: {
|
||||||
'messages': messages
|
'messages': messages
|
||||||
.map((m) => {
|
.map(
|
||||||
|
(m) => {
|
||||||
'id': m.id,
|
'id': m.id,
|
||||||
'content': m.content,
|
'content': m.content,
|
||||||
'authorId': m.authorUserId,
|
'senderId': m.senderId,
|
||||||
'authorUsername': m.authorUsername,
|
'senderName': userMap[m.senderId] ?? 'Unknown',
|
||||||
'authorAvatarUrl': m.authorAvatarUrl,
|
|
||||||
'authorHomeServer': m.authorHomeServer,
|
|
||||||
'channelId': m.channelId,
|
'channelId': m.channelId,
|
||||||
'createdAt': m.createdAt.toIso8601String(),
|
'createdAt': m.createdAt.toIso8601String(),
|
||||||
'editedAt': m.editedAt?.toIso8601String(),
|
},
|
||||||
})
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
'channelId': channelId,
|
|
||||||
'limit': limit,
|
|
||||||
}.toBody,
|
}.toBody,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a message
|
|
||||||
Future<Response> sendMessageHandler(Request req) async {
|
Future<Response> sendMessageHandler(Request req) async {
|
||||||
final userId = userIdProperty[req];
|
final senderId = int.parse(userIdProperty[req]!);
|
||||||
|
|
||||||
if (userId == null) {
|
|
||||||
return Response.unauthorized(
|
|
||||||
body: Body.fromString('Not authenticated'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final body = await req.asJson();
|
final body = await req.asJson();
|
||||||
final content = body['content'] as String?;
|
final content = body['content'] as String?;
|
||||||
final channelId = body['channelId'] as int?;
|
final channelId = body['channelId'] as int?;
|
||||||
|
|
||||||
if (content == null || content.trim().isEmpty) {
|
if (content == null || content.isEmpty) {
|
||||||
return Response.badRequest(
|
return Response.badRequest(body: Body.fromString('Content required'));
|
||||||
body: Body.fromString('Content required'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channelId == null) {
|
if (channelId == null) {
|
||||||
return Response.badRequest(
|
return Response.badRequest(body: Body.fromString('channelId required'));
|
||||||
body: Body.fromString('channelId required'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check channel exists
|
|
||||||
final channel = channelBox.get(channelId);
|
final channel = channelBox.get(channelId);
|
||||||
if (channel == null) {
|
if (channel == null) {
|
||||||
return Response.notFound(
|
return Response.badRequest(body: Body.fromString('Channel not found'));
|
||||||
body: Body.fromString('Channel not found'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user info for caching
|
final message = ChatMessage(content: content, senderId: senderId, channelId: channelId);
|
||||||
final userQuery = userBox.query(User_.userId.equals(userId)).build();
|
|
||||||
final user = userQuery.findFirst();
|
|
||||||
userQuery.close();
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
return Response.unauthorized(
|
|
||||||
body: Body.fromString('User not found'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create message with cached author info
|
|
||||||
final message = Message(
|
|
||||||
channelId: channelId,
|
|
||||||
authorLocalId: user.id,
|
|
||||||
authorUserId: user.userId,
|
|
||||||
authorUsername: user.username,
|
|
||||||
authorAvatarUrl: user.avatarUrl,
|
|
||||||
authorHomeServer: user.homeServer,
|
|
||||||
content: content.trim(),
|
|
||||||
);
|
|
||||||
|
|
||||||
final id = messageBox.put(message);
|
final id = messageBox.put(message);
|
||||||
|
|
||||||
return Response.ok(
|
return Response.ok(
|
||||||
body: {
|
body: {
|
||||||
'id': id,
|
'id': id,
|
||||||
'content': message.content,
|
'content': content,
|
||||||
'authorId': user.userId,
|
'senderId': senderId,
|
||||||
'authorUsername': user.username,
|
|
||||||
'authorAvatarUrl': user.avatarUrl,
|
|
||||||
'channelId': channelId,
|
'channelId': channelId,
|
||||||
'createdAt': message.createdAt.toIso8601String(),
|
'createdAt': message.createdAt.toIso8601String(),
|
||||||
}.toBody,
|
}.toBody,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Edit a message
|
|
||||||
Future<Response> editMessageHandler(Request req, String messageIdStr) async {
|
|
||||||
final userId = userIdProperty[req];
|
|
||||||
|
|
||||||
if (userId == null) {
|
|
||||||
return Response.unauthorized(
|
|
||||||
body: Body.fromString('Not authenticated'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final messageId = int.tryParse(messageIdStr);
|
|
||||||
if (messageId == null) {
|
|
||||||
return Response.badRequest(
|
|
||||||
body: Body.fromString('Invalid message ID'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final message = messageBox.get(messageId);
|
|
||||||
if (message == null) {
|
|
||||||
return Response.notFound(
|
|
||||||
body: Body.fromString('Message not found'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user to verify ownership
|
|
||||||
final userQuery = userBox.query(User_.userId.equals(userId)).build();
|
|
||||||
final user = userQuery.findFirst();
|
|
||||||
userQuery.close();
|
|
||||||
|
|
||||||
if (user == null || message.authorLocalId != user.id) {
|
|
||||||
return Response.forbidden(
|
|
||||||
body: Body.fromString('You can only edit your own messages'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final body = await req.asJson();
|
|
||||||
final content = body['content'] as String?;
|
|
||||||
|
|
||||||
if (content == null || content.trim().isEmpty) {
|
|
||||||
return Response.badRequest(
|
|
||||||
body: Body.fromString('Content required'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
message.content = content.trim();
|
|
||||||
message.editedAt = DateTime.now();
|
|
||||||
messageBox.put(message);
|
|
||||||
|
|
||||||
return Response.ok(
|
|
||||||
body: {
|
|
||||||
'id': message.id,
|
|
||||||
'content': message.content,
|
|
||||||
'editedAt': message.editedAt?.toIso8601String(),
|
|
||||||
}.toBody,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a message
|
|
||||||
Future<Response> deleteMessageHandler(Request req, String messageIdStr) async {
|
|
||||||
final userId = userIdProperty[req];
|
|
||||||
|
|
||||||
if (userId == null) {
|
|
||||||
return Response.unauthorized(
|
|
||||||
body: Body.fromString('Not authenticated'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final messageId = int.tryParse(messageIdStr);
|
|
||||||
if (messageId == null) {
|
|
||||||
return Response.badRequest(
|
|
||||||
body: Body.fromString('Invalid message ID'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final message = messageBox.get(messageId);
|
|
||||||
if (message == null) {
|
|
||||||
return Response.notFound(
|
|
||||||
body: Body.fromString('Message not found'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user to verify ownership
|
|
||||||
final userQuery = userBox.query(User_.userId.equals(userId)).build();
|
|
||||||
final user = userQuery.findFirst();
|
|
||||||
userQuery.close();
|
|
||||||
|
|
||||||
if (user == null || message.authorLocalId != user.id) {
|
|
||||||
return Response.forbidden(
|
|
||||||
body: Body.fromString('You can only delete your own messages'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
messageBox.remove(messageId);
|
|
||||||
|
|
||||||
return Response.ok(
|
|
||||||
body: {
|
|
||||||
'deleted': true,
|
|
||||||
'id': messageId,
|
|
||||||
}.toBody,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import 'package:chat_server/models.dart';
|
|
||||||
import 'package:chat_server/objectbox.g.dart';
|
|
||||||
import 'package:relic/relic.dart';
|
|
||||||
|
|
||||||
import '../utils/extensions.dart';
|
|
||||||
|
|
||||||
/// Public endpoint for cross-server identity verification
|
|
||||||
/// Called by other servers to verify a user's public key
|
|
||||||
///
|
|
||||||
/// GET /.well-known/users/{username}
|
|
||||||
/// Returns: { userId, publicKey, username, displayName, avatarUrl }
|
|
||||||
Future<Response> publicUserHandler(Request req, String username) async {
|
|
||||||
final query = userBox.query(
|
|
||||||
User_.username.equals(username) & User_.isLocal.equals(true),
|
|
||||||
).build();
|
|
||||||
|
|
||||||
final user = query.findFirst();
|
|
||||||
query.close();
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
return Response.notFound(
|
|
||||||
body: Body.fromString('User not found'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.ok(
|
|
||||||
body: {
|
|
||||||
'userId': user.userId,
|
|
||||||
'username': user.username,
|
|
||||||
'displayName': user.displayName,
|
|
||||||
'avatarUrl': user.avatarUrl,
|
|
||||||
'bio': user.bio,
|
|
||||||
'publicKey': user.publicKey,
|
|
||||||
'homeServer': Config.serverHost,
|
|
||||||
'createdAt': user.createdAt.toIso8601String(),
|
|
||||||
}.toBody,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Public endpoint to verify a user exists
|
|
||||||
/// Lightweight check without full profile
|
|
||||||
///
|
|
||||||
/// GET /.well-known/verify/{userId}
|
|
||||||
/// Returns: { exists, userId, publicKey }
|
|
||||||
Future<Response> publicVerifyHandler(Request req, String userId) async {
|
|
||||||
final query = userBox.query(
|
|
||||||
User_.userId.equals(userId) & User_.isLocal.equals(true),
|
|
||||||
).build();
|
|
||||||
|
|
||||||
final user = query.findFirst();
|
|
||||||
query.close();
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
return Response.ok(
|
|
||||||
body: {
|
|
||||||
'exists': false,
|
|
||||||
'userId': userId,
|
|
||||||
}.toBody,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.ok(
|
|
||||||
body: {
|
|
||||||
'exists': true,
|
|
||||||
'userId': user.userId,
|
|
||||||
'publicKey': user.publicKey,
|
|
||||||
'username': user.username,
|
|
||||||
}.toBody,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Public server info
|
|
||||||
///
|
|
||||||
/// GET /.well-known/server
|
|
||||||
/// Returns: { serverHost, serverName, version }
|
|
||||||
Future<Response> publicServerInfoHandler(Request req) async {
|
|
||||||
return Response.ok(
|
|
||||||
body: {
|
|
||||||
'serverHost': Config.serverHost,
|
|
||||||
'serverName': Config.serverName,
|
|
||||||
'version': '0.1.0',
|
|
||||||
'features': ['identity', 'chat', 'sticker-packs'],
|
|
||||||
}.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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,8 +2,6 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:relic/relic.dart';
|
import 'package:relic/relic.dart';
|
||||||
|
|
||||||
import '../config/config.dart';
|
|
||||||
|
|
||||||
extension RequestHelper on Request {
|
extension RequestHelper on Request {
|
||||||
/// Parses the request body as JSON
|
/// Parses the request body as JSON
|
||||||
Future<Map<String, dynamic>> asJson() async {
|
Future<Map<String, dynamic>> asJson() async {
|
||||||
@@ -17,7 +15,7 @@ extension MapToBody on Map<String, dynamic> {
|
|||||||
Body get toBody => Body.fromString(jsonEncode(this));
|
Body get toBody => Body.fromString(jsonEncode(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BodyHelper on Body {
|
extension JsonBody on Body {
|
||||||
/// Creates a JSON body from a Map
|
/// Creates a JSON body from a Map
|
||||||
static Body fromJson(Map<String, dynamic> json) {
|
static Body fromJson(Map<String, dynamic> json) {
|
||||||
return Body.fromString(jsonEncode(json));
|
return Body.fromString(jsonEncode(json));
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+636
@@ -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"
|
||||||
+16
-14
@@ -1,21 +1,23 @@
|
|||||||
name: chat_server
|
name: chat_server
|
||||||
description: A self-hosted chat server with home server identity model.
|
description: A server app using the shelf package and Docker.
|
||||||
version: 0.1.0
|
version: 1.0.0
|
||||||
|
# repository: https://github.com/my_org/my_repo
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: ^3.11.0-296.5.beta
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
relic: ^1.0.0
|
relic:
|
||||||
objectbox: ^4.0.0
|
hotreloader:
|
||||||
dart_jsonwebtoken: ^2.14.0
|
dart_jsonwebtoken:
|
||||||
bcrypt: ^1.1.3
|
bcrypt:
|
||||||
crypton: ^2.2.1
|
objectbox:
|
||||||
crypto: ^3.0.3
|
fox_utils2:
|
||||||
collection: ^1.18.0
|
path: ../fox_utils2
|
||||||
hotreloader: ^1.0.6
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
objectbox_generator: ^4.0.0
|
objectbox_generator:
|
||||||
build_runner: ^2.4.0
|
build_runner:
|
||||||
lints: ^4.0.0
|
http:
|
||||||
|
lints:
|
||||||
|
test:
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user