@enfin/chat-server 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -0
- package/dist/bin/start.js +91 -0
- package/dist/chat-mongoose.module.js +48 -0
- package/dist/chat.constants.js +5 -0
- package/dist/chat.module.js +144 -0
- package/dist/controllers/rooms.controller.js +112 -0
- package/dist/controllers/upload.controller.js +123 -0
- package/dist/controllers/users.controller.js +101 -0
- package/dist/controllers/validation.controller.js +44 -0
- package/dist/dto/call.dto.js +21 -0
- package/dist/dto/message.dto.js +58 -0
- package/dist/dto/presence.dto.js +20 -0
- package/dist/dto/room.dto.js +48 -0
- package/dist/gateways/chat.gateway.js +439 -0
- package/dist/index.js +26 -0
- package/dist/interfaces/index.js +2 -0
- package/dist/main.js +38 -0
- package/dist/schemas/apikey.schema.js +33 -0
- package/dist/schemas/audiocall.schema.js +49 -0
- package/dist/schemas/index.js +34 -0
- package/dist/schemas/message.schema.js +40 -0
- package/dist/schemas/presence.schema.js +23 -0
- package/dist/schemas/room.schema.js +40 -0
- package/dist/schemas/user.schema.js +14 -0
- package/dist/services/apikey.service.js +120 -0
- package/dist/services/call.service.js +145 -0
- package/dist/services/message.service.js +108 -0
- package/dist/services/presence.service.js +65 -0
- package/dist/services/room.service.js +158 -0
- package/dist/services/storage/rate-limiter.service.js +57 -0
- package/dist/services/storage/storage.service.js +96 -0
- package/dist/services/storage/upload-handler.interface.js +2 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# @enfin/chat-server
|
|
2
|
+
|
|
3
|
+
NestJS chat module — Socket.IO gateway, MongoDB persistence, file uploads, presence, and audio calls.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @enfin/chat-server @enfin/chat-shared
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start (CLI)
|
|
12
|
+
|
|
13
|
+
For quick local testing:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx chat-server start --apiKey=chat_xxx --port=3002
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
- `--apiKey` (required): Your API key
|
|
21
|
+
- `--port` (default: 3002): Server port
|
|
22
|
+
- `--mongoUri` (default: mongodb://localhost:27017/chat_sdk): MongoDB connection string
|
|
23
|
+
- `--mode` (default: managed): `managed` or `external-db`
|
|
24
|
+
|
|
25
|
+
## Mount in Your NestJS App
|
|
26
|
+
|
|
27
|
+
### Option 1: Fresh MongoDB (no existing Mongoose)
|
|
28
|
+
|
|
29
|
+
Use `ChatMongooseModule` to create the connection, then mount `ChatModule`:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { Module } from '@nestjs/common';
|
|
33
|
+
import { ConfigModule } from '@nestjs/config';
|
|
34
|
+
import { ChatMongooseModule, ChatModule } from '@enfin/chat-server';
|
|
35
|
+
|
|
36
|
+
@Module({
|
|
37
|
+
imports: [
|
|
38
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
39
|
+
ChatMongooseModule.forRoot(process.env.MONGO_URI || 'mongodb://localhost:27017/myapp'),
|
|
40
|
+
ChatModule.forRoot({
|
|
41
|
+
apiKey: process.env.CHAT_API_KEY || 'chat_xxx',
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
export class AppModule {}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Option 2: Already Have Mongoose
|
|
49
|
+
|
|
50
|
+
If your app already calls `MongooseModule.forRoot(uri)`, skip `ChatMongooseModule`:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { Module } from '@nestjs/common';
|
|
54
|
+
import { ConfigModule } from '@nestjs/config';
|
|
55
|
+
import { MongooseModule } from '@nestjs/mongoose';
|
|
56
|
+
import { ChatModule } from '@enfin/chat-server';
|
|
57
|
+
|
|
58
|
+
@Module({
|
|
59
|
+
imports: [
|
|
60
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
61
|
+
MongooseModule.forRoot('mongodb://localhost:27017/myapp'),
|
|
62
|
+
ChatModule.forRoot({
|
|
63
|
+
apiKey: process.env.CHAT_API_KEY || 'chat_xxx',
|
|
64
|
+
}),
|
|
65
|
+
],
|
|
66
|
+
})
|
|
67
|
+
export class AppModule {}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
ChatModule registers its schemas on whatever Mongoose connection exists.
|
|
71
|
+
|
|
72
|
+
## ChatModuleOptions
|
|
73
|
+
|
|
74
|
+
| Option | Type | Required | Description |
|
|
75
|
+
|--------|------|----------|-------------|
|
|
76
|
+
| `apiKey` | string | Yes | Your platform API key |
|
|
77
|
+
| `mongoUri` | string | No | Customer-provided DB (external-db mode) |
|
|
78
|
+
| `mode` | `'managed'` \| `'external-db'` | No | Default: `managed` |
|
|
79
|
+
| `version` | string | No | API version string |
|
|
80
|
+
| `platformMongoUri` | string | No | Platform DB for key validation |
|
|
81
|
+
| `uploadDir` | string | No | Where uploaded files are written. Relative paths resolve against `process.cwd()`. Overrides `UPLOAD_DIR` env. |
|
|
82
|
+
| `fileUploadHandler` | function | No | Custom upload handler. Receives the parsed file and returns the URL. Use for S3 / Azure / GCS. See [Custom file upload handler](#custom-file-upload-handler). |
|
|
83
|
+
|
|
84
|
+
## Modes
|
|
85
|
+
|
|
86
|
+
### `managed` (default)
|
|
87
|
+
|
|
88
|
+
The chat server validates API keys against its own database. Use this when you control the keys and want the server to manage everything.
|
|
89
|
+
|
|
90
|
+
### `external-db`
|
|
91
|
+
|
|
92
|
+
The customer provides their own MongoDB. The server stores chat data there but still validates keys against your platform. Use this for customers who want isolation.
|
|
93
|
+
|
|
94
|
+
## Environment Variables
|
|
95
|
+
|
|
96
|
+
| Variable | Description |
|
|
97
|
+
|----------|-------------|
|
|
98
|
+
| `MONGO_URI` | Fallback MongoDB URI (used if not provided in options) |
|
|
99
|
+
| `PORT` | Server port (default: 3002) |
|
|
100
|
+
| `UPLOAD_DIR` | Where uploaded files are written. Relative paths resolve against `process.cwd()` (e.g. `public/images` → `<cwd>/public/images`). Absolute paths are used as-is. Default: `uploads` (i.e. `<cwd>/uploads`). |
|
|
101
|
+
|
|
102
|
+
### Configuring the upload directory
|
|
103
|
+
|
|
104
|
+
Resolution order: `options.uploadDir` → `process.env.UPLOAD_DIR` → `uploads` (relative to cwd).
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Default: <cwd>/uploads
|
|
108
|
+
node dist/main.js
|
|
109
|
+
|
|
110
|
+
# Relative path: <cwd>/public/images (the file URL is still /uploads/<uuid>.<ext>)
|
|
111
|
+
UPLOAD_DIR=public/images node dist/main.js
|
|
112
|
+
|
|
113
|
+
# Absolute path
|
|
114
|
+
UPLOAD_DIR=/var/data/chat-uploads node dist/main.js
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Or in NestJS options:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
ChatModule.forRoot({
|
|
121
|
+
apiKey: process.env.CHAT_API_KEY!,
|
|
122
|
+
uploadDir: 'public/images', // resolves to <cwd>/public/images
|
|
123
|
+
}),
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The directory is created automatically on startup if it does not exist. Uploaded files are served at `GET /uploads/<filename>` (handled by `ChatModule` — no extra `useStaticAssets` needed in your `main.ts`).
|
|
127
|
+
|
|
128
|
+
### Custom file upload handler
|
|
129
|
+
|
|
130
|
+
For production deployments you usually want files in object storage (S3, Azure Blob, Google Cloud Storage) instead of on the chat server's disk. Pass a `fileUploadHandler` to `ChatModule.forRoot` to take over the upload step.
|
|
131
|
+
|
|
132
|
+
The handler receives the file (already parsed by multer — `file.path` is on local disk and `file.buffer` is in memory) plus the validated `tenantId` and `roomId`, and returns the URL where the file is now reachable. The URL is stored in the `Message` exactly like the default behaviour — no frontend changes are needed.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
136
|
+
import { ChatModule, FileUploadHandler } from '@enfin/chat-server';
|
|
137
|
+
|
|
138
|
+
const s3 = new S3Client({ region: 'us-east-1' });
|
|
139
|
+
|
|
140
|
+
const uploadToS3: FileUploadHandler = async ({ file, tenantId, roomId }) => {
|
|
141
|
+
const key = `${tenantId}/${roomId}/${Date.now()}-${file.originalname}`;
|
|
142
|
+
await s3.send(new PutObjectCommand({
|
|
143
|
+
Bucket: 'my-chat-uploads',
|
|
144
|
+
Key: key,
|
|
145
|
+
Body: file.buffer ?? require('fs').createReadStream(file.path),
|
|
146
|
+
ContentType: file.mimetype,
|
|
147
|
+
}));
|
|
148
|
+
return {
|
|
149
|
+
fileUrl: `https://my-chat-uploads.s3.amazonaws.com/${key}`,
|
|
150
|
+
fileName: file.originalname,
|
|
151
|
+
fileType: file.mimetype,
|
|
152
|
+
fileSize: file.size,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
ChatModule.forRoot({
|
|
157
|
+
apiKey: process.env.CHAT_API_KEY!,
|
|
158
|
+
fileUploadHandler: uploadToS3,
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Notes:**
|
|
163
|
+
|
|
164
|
+
- If `fileUploadHandler` is not provided, the default multer-to-disk behaviour is unchanged.
|
|
165
|
+
- The handler is called **after** API key and room validation, so it only ever sees authorised uploads.
|
|
166
|
+
- Return any URL the frontend can `fetch` — absolute (S3, CDN) or relative (your own CDN in front of the bucket).
|
|
167
|
+
- For tenant isolation, namespace the storage key with `tenantId` (as shown above) so tenants cannot read each other's files.
|
|
168
|
+
- The built-in `GET /uploads/<filename>` route is still registered but will be unused when you return external URLs. It is harmless to leave in place.
|
|
169
|
+
|
|
170
|
+
## REST Endpoints
|
|
171
|
+
|
|
172
|
+
All endpoints require `x-api-key` header unless noted.
|
|
173
|
+
|
|
174
|
+
| Method | Path | Description |
|
|
175
|
+
|--------|-----|-------------|
|
|
176
|
+
| `POST` | `/api/validation/validate` | Validate API key |
|
|
177
|
+
| `POST` | `/api/users/register` | Register a user |
|
|
178
|
+
| `GET` | `/api/users` | List all users (with presence) |
|
|
179
|
+
| `GET` | `/api/rooms` | List rooms for a user |
|
|
180
|
+
| `POST` | `/api/rooms/direct` | Open/get direct room |
|
|
181
|
+
| `GET` | `/api/rooms/:roomId/messages` | Get room messages |
|
|
182
|
+
| `POST` | `/api/upload` | Upload file (multipart/form-data) |
|
|
183
|
+
|
|
184
|
+
## Socket Events
|
|
185
|
+
|
|
186
|
+
Namespace: `/chat`
|
|
187
|
+
|
|
188
|
+
### Connect
|
|
189
|
+
|
|
190
|
+
Handshake auth required:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
socket = io('http://localhost:3002/chat', {
|
|
194
|
+
auth: { apiKey: 'chat_xxx', userId: 'u1', userName: 'Alice' }
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Client → Server
|
|
199
|
+
|
|
200
|
+
| Event | Payload | Description |
|
|
201
|
+
|-------|--------|-------------|
|
|
202
|
+
| `message` | `{ roomId, content }` | Send message |
|
|
203
|
+
| `typing` | `{ roomId, isTyping }` | Typing indicator |
|
|
204
|
+
| `presence` | `{ status }` | Set presence (online/away/offline) |
|
|
205
|
+
| `join-room` | `{ roomId }` | Join a room |
|
|
206
|
+
| `leave-room` | `{ roomId }` | Leave a room |
|
|
207
|
+
| `call` | `{ to, offer }` | WebRTC offer |
|
|
208
|
+
| `call-accept` | `{ callId, answer }` | WebRTC answer |
|
|
209
|
+
| `call-reject` | `{ callId }` | Reject call |
|
|
210
|
+
| `call-end` | `{ callId }` | End call |
|
|
211
|
+
|
|
212
|
+
### Server → Client
|
|
213
|
+
|
|
214
|
+
| Event | Payload | Description |
|
|
215
|
+
|-------|--------|-------------|
|
|
216
|
+
| `message` | `{ roomId, messages[] }` | New message(s) |
|
|
217
|
+
| `typing` | `{ roomId, userId, isTyping }` | User typing |
|
|
218
|
+
| `presence:changed` | `{ userId, status }` | Presence update |
|
|
219
|
+
| `room:updated` | `{ room }` | Room updated |
|
|
220
|
+
| `call` | `{ callId, from, offer }` | Incoming call |
|
|
221
|
+
| `call-accepted` | `{ callId, answer }` | Call accepted |
|
|
222
|
+
| `call-rejected` | `{ callId }` | Call rejected |
|
|
223
|
+
| `call-ended` | `{ callId }` | Call ended |
|
|
224
|
+
| `error` | `{ code, message }` | Error event |
|
|
225
|
+
| `users:list` | `{ users[] }` | User list on join |
|
|
226
|
+
|
|
227
|
+
## File Uploads
|
|
228
|
+
|
|
229
|
+
POST to `/api/upload` with `multipart/form-data`:
|
|
230
|
+
|
|
231
|
+
- Field: `file`
|
|
232
|
+
- Header: `x-api-key`
|
|
233
|
+
- Body: `{ roomId: string }`
|
|
234
|
+
|
|
235
|
+
Response:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"success": true,
|
|
240
|
+
"fileUrl": "/uploads/uuid-filename.jpg",
|
|
241
|
+
"fileName": "photo.jpg",
|
|
242
|
+
"fileType": "image/jpeg",
|
|
243
|
+
"fileSize": 12345
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Max file size: 50MB. Files are served at `GET /uploads/<filename>` by the SDK's built-in controller — you do not need to register `useStaticAssets` in your `main.ts`.
|
|
248
|
+
|
|
249
|
+
> **Production:** the built-in disk storage is for development and small deployments. For production pass a `fileUploadHandler` to `ChatModule.forRoot` that streams the file to S3 / Azure Blob / GCS and returns the public URL. See [Custom file upload handler](#custom-file-upload-handler).
|
|
250
|
+
|
|
251
|
+
## Troubleshooting
|
|
252
|
+
|
|
253
|
+
### EADDRINUSE
|
|
254
|
+
|
|
255
|
+
Another process is on your port. Kill it or use a different port:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
# Find process
|
|
259
|
+
netstat -ano | findstr :3002
|
|
260
|
+
# Kill on Windows
|
|
261
|
+
taskkill /PID <pid> /F
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### MongoDB connection failed
|
|
265
|
+
|
|
266
|
+
Ensure MongoDB is running and the URI is correct. If using Docker:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
docker run -d -p 27017:27017 mongo
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Invalid apiKey
|
|
273
|
+
|
|
274
|
+
Keys must exist in your platform database. In `managed` mode, the server looks up the key on startup. In `external-db` mode, provide valid keys in your customer's DB.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
For frontend SDK, see `@shuhaib-enfin/chat`.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Standalone CLI entry point for the chat server.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx chat-server start --apiKey=chat_xxx --port=3002
|
|
8
|
+
* npx chat-server start --apiKey=chat_xxx --mongoUri=mongodb://localhost:27017/mydb
|
|
9
|
+
*
|
|
10
|
+
* This is intended for quick testing / local dev only.
|
|
11
|
+
* In production, mount ChatModule.forRoot in your own NestJS app.
|
|
12
|
+
*/
|
|
13
|
+
require("reflect-metadata");
|
|
14
|
+
const core_1 = require("@nestjs/core");
|
|
15
|
+
const common_1 = require("@nestjs/common");
|
|
16
|
+
const chat_module_1 = require("../chat.module");
|
|
17
|
+
function parseArgs() {
|
|
18
|
+
const args = {};
|
|
19
|
+
const argv = process.argv.slice(2);
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21
|
+
const arg = argv[i];
|
|
22
|
+
if (arg === 'start' && i + 1 < argv.length) {
|
|
23
|
+
const next = argv[i + 1];
|
|
24
|
+
if (!next.startsWith('--')) {
|
|
25
|
+
i++;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (arg.startsWith('--apiKey=')) {
|
|
30
|
+
args.apiKey = arg.split('=')[1];
|
|
31
|
+
}
|
|
32
|
+
else if (arg === '--apiKey' && i + 1 < argv.length) {
|
|
33
|
+
args.apiKey = argv[++i];
|
|
34
|
+
}
|
|
35
|
+
if (arg.startsWith('--port=')) {
|
|
36
|
+
args.port = arg.split('=')[1];
|
|
37
|
+
}
|
|
38
|
+
else if (arg === '--port' && i + 1 < argv.length) {
|
|
39
|
+
args.port = argv[++i];
|
|
40
|
+
}
|
|
41
|
+
if (arg.startsWith('--mongoUri=')) {
|
|
42
|
+
args.mongoUri = arg.split('=')[1];
|
|
43
|
+
}
|
|
44
|
+
else if (arg === '--mongoUri' && i + 1 < argv.length) {
|
|
45
|
+
args.mongoUri = argv[++i];
|
|
46
|
+
}
|
|
47
|
+
if (arg.startsWith('--mode=')) {
|
|
48
|
+
args.mode = arg.split('=')[1];
|
|
49
|
+
}
|
|
50
|
+
else if (arg === '--mode' && i + 1 < argv.length) {
|
|
51
|
+
args.mode = argv[++i];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return args;
|
|
55
|
+
}
|
|
56
|
+
async function bootstrap() {
|
|
57
|
+
const args = parseArgs();
|
|
58
|
+
if (!args.apiKey) {
|
|
59
|
+
console.error('Error: --apiKey is required');
|
|
60
|
+
console.error('Usage: npx chat-server start --apiKey=chat_xxx [--port=3002] [--mongoUri=mongodb://...]');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
const port = args.port || '3002';
|
|
64
|
+
const mongoUri = args.mongoUri || 'mongodb://localhost:27017/chat_sdk';
|
|
65
|
+
const mode = args.mode || 'managed';
|
|
66
|
+
console.log(`Starting chat-server on port ${port}...`);
|
|
67
|
+
console.log(`API Key: ${args.apiKey.slice(0, 12)}...`);
|
|
68
|
+
console.log(`MongoDB: ${mongoUri}`);
|
|
69
|
+
console.log(`Mode: ${mode}`);
|
|
70
|
+
const app = await core_1.NestFactory.create(chat_module_1.ChatModule.forRoot({
|
|
71
|
+
apiKey: args.apiKey,
|
|
72
|
+
mongoUri,
|
|
73
|
+
mode,
|
|
74
|
+
}));
|
|
75
|
+
app.enableCors({
|
|
76
|
+
origin: '*',
|
|
77
|
+
credentials: true,
|
|
78
|
+
});
|
|
79
|
+
app.useGlobalPipes(new common_1.ValidationPipe({
|
|
80
|
+
whitelist: true,
|
|
81
|
+
transform: true,
|
|
82
|
+
}));
|
|
83
|
+
app.setGlobalPrefix('api');
|
|
84
|
+
await app.listen(port);
|
|
85
|
+
console.log(`Chat server running at http://localhost:${port}`);
|
|
86
|
+
console.log(`API prefix: /api`);
|
|
87
|
+
}
|
|
88
|
+
bootstrap().catch((err) => {
|
|
89
|
+
console.error('Failed to start:', err);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var ChatMongooseModule_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ChatMongooseModule = void 0;
|
|
11
|
+
const common_1 = require("@nestjs/common");
|
|
12
|
+
const mongoose_1 = require("@nestjs/mongoose");
|
|
13
|
+
/**
|
|
14
|
+
* Helper module for hosts that do NOT already have a Mongoose connection.
|
|
15
|
+
*
|
|
16
|
+
* Use this BEFORE ChatModule.forRoot when your NestJS app is starting fresh
|
|
17
|
+
* and you want the chat SDK to manage MongoDB. If you already call
|
|
18
|
+
* MongooseModule.forRoot in your own app, skip this and just import
|
|
19
|
+
* ChatModule.forRoot — schemas are registered on whatever connection exists.
|
|
20
|
+
*
|
|
21
|
+
* Example:
|
|
22
|
+
*
|
|
23
|
+
* @Module({
|
|
24
|
+
* imports: [
|
|
25
|
+
* ChatMongooseModule.forRoot('mongodb://localhost:27017/myapp'),
|
|
26
|
+
* ChatModule.forRoot({ apiKey: 'chat_xxx' }),
|
|
27
|
+
* ],
|
|
28
|
+
* })
|
|
29
|
+
* export class AppModule {}
|
|
30
|
+
*
|
|
31
|
+
* Note: We use `global: true` on the DynamicModule metadata instead of the
|
|
32
|
+
* @Global() class decorator. The combination of @Global() + DynamicModule
|
|
33
|
+
* is the root cause of "Nest can't resolve dependencies of the MongooseCoreModule
|
|
34
|
+
* (MongooseConnectionName, ?)" in NestJS 10.4.
|
|
35
|
+
*/
|
|
36
|
+
let ChatMongooseModule = ChatMongooseModule_1 = class ChatMongooseModule {
|
|
37
|
+
static forRoot(uri) {
|
|
38
|
+
return {
|
|
39
|
+
module: ChatMongooseModule_1,
|
|
40
|
+
imports: [mongoose_1.MongooseModule.forRoot(uri)],
|
|
41
|
+
exports: [mongoose_1.MongooseModule],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
exports.ChatMongooseModule = ChatMongooseModule;
|
|
46
|
+
exports.ChatMongooseModule = ChatMongooseModule = ChatMongooseModule_1 = __decorate([
|
|
47
|
+
(0, common_1.Module)({})
|
|
48
|
+
], ChatMongooseModule);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.ChatModule = exports.ChatUploadsController = exports.CHAT_UPLOAD_HANDLER = exports.CHAT_UPLOAD_DIR = exports.CHAT_TENANT_ID = exports.CHAT_MODULE_OPTIONS = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const mongoose_1 = require("@nestjs/mongoose");
|
|
18
|
+
const path_1 = require("path");
|
|
19
|
+
const fs_1 = require("fs");
|
|
20
|
+
const chat_gateway_1 = require("./gateways/chat.gateway");
|
|
21
|
+
const message_service_1 = require("./services/message.service");
|
|
22
|
+
const room_service_1 = require("./services/room.service");
|
|
23
|
+
const presence_service_1 = require("./services/presence.service");
|
|
24
|
+
const call_service_1 = require("./services/call.service");
|
|
25
|
+
const storage_service_1 = require("./services/storage/storage.service");
|
|
26
|
+
const rate_limiter_service_1 = require("./services/storage/rate-limiter.service");
|
|
27
|
+
const schemas_1 = require("./schemas");
|
|
28
|
+
const apikey_service_1 = require("./services/apikey.service");
|
|
29
|
+
const validation_controller_1 = require("./controllers/validation.controller");
|
|
30
|
+
const users_controller_1 = require("./controllers/users.controller");
|
|
31
|
+
const rooms_controller_1 = require("./controllers/rooms.controller");
|
|
32
|
+
const upload_controller_1 = require("./controllers/upload.controller");
|
|
33
|
+
// IMPORTANT: Keep these as inline string literals (not module-level references) in any
|
|
34
|
+
// other file that imports from chat.module — circular imports cause the export to be
|
|
35
|
+
// undefined at decorator-evaluation time in CommonJS output. Use the string directly.
|
|
36
|
+
exports.CHAT_MODULE_OPTIONS = 'CHAT_MODULE_OPTIONS';
|
|
37
|
+
exports.CHAT_TENANT_ID = 'CHAT_TENANT_ID';
|
|
38
|
+
exports.CHAT_UPLOAD_DIR = 'CHAT_UPLOAD_DIR';
|
|
39
|
+
exports.CHAT_UPLOAD_HANDLER = 'CHAT_UPLOAD_HANDLER';
|
|
40
|
+
const logger = new common_1.Logger('ChatModule');
|
|
41
|
+
function resolveUploadDir(override) {
|
|
42
|
+
// Precedence: explicit option > UPLOAD_DIR env > default 'uploads' (relative to cwd)
|
|
43
|
+
// Relative paths (e.g. 'public/images', 'uploads') are resolved against cwd.
|
|
44
|
+
// Absolute paths (e.g. '/var/data/chat', 'C:\\storage\\chat') are used as-is.
|
|
45
|
+
const raw = override || process.env.UPLOAD_DIR || 'uploads';
|
|
46
|
+
const dir = (0, path_1.isAbsolute)(raw) ? raw : (0, path_1.join)(process.cwd(), raw);
|
|
47
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
48
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
return dir;
|
|
51
|
+
}
|
|
52
|
+
let ChatUploadsController = class ChatUploadsController {
|
|
53
|
+
constructor(uploadDir) {
|
|
54
|
+
this.uploadDir = uploadDir;
|
|
55
|
+
}
|
|
56
|
+
serveFile(filename, res) {
|
|
57
|
+
if (!filename || filename.includes('..') || filename.includes('\\') || filename.includes('/')) {
|
|
58
|
+
throw new common_1.BadRequestException('Invalid filename');
|
|
59
|
+
}
|
|
60
|
+
const filePath = (0, path_1.join)(this.uploadDir, filename);
|
|
61
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
62
|
+
throw new common_1.NotFoundException('File not found');
|
|
63
|
+
}
|
|
64
|
+
res.sendFile(filePath, (err) => {
|
|
65
|
+
if (err && !res.headersSent) {
|
|
66
|
+
res.status(500).end('Server error');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
exports.ChatUploadsController = ChatUploadsController;
|
|
72
|
+
__decorate([
|
|
73
|
+
(0, common_1.Get)(':filename'),
|
|
74
|
+
__param(0, (0, common_1.Param)('filename')),
|
|
75
|
+
__param(1, (0, common_1.Res)()),
|
|
76
|
+
__metadata("design:type", Function),
|
|
77
|
+
__metadata("design:paramtypes", [String, Object]),
|
|
78
|
+
__metadata("design:returntype", void 0)
|
|
79
|
+
], ChatUploadsController.prototype, "serveFile", null);
|
|
80
|
+
exports.ChatUploadsController = ChatUploadsController = __decorate([
|
|
81
|
+
(0, common_1.Controller)('uploads'),
|
|
82
|
+
__param(0, (0, common_1.Inject)('CHAT_UPLOAD_DIR')),
|
|
83
|
+
__metadata("design:paramtypes", [String])
|
|
84
|
+
], ChatUploadsController);
|
|
85
|
+
class ChatModule {
|
|
86
|
+
static forRoot(options) {
|
|
87
|
+
const mode = options.mode || 'managed';
|
|
88
|
+
const platformMongoUri = options.platformMongoUri || process.env.MONGO_URI || 'mongodb://localhost:27017/chat_sdk';
|
|
89
|
+
const customerMongoUri = options.mongoUri;
|
|
90
|
+
const adminValidationUrl = options.adminValidationUrl || process.env.ADMIN_API_VALIDATION_URL;
|
|
91
|
+
const finalMongoUri = customerMongoUri && customerMongoUri.trim().length > 0 ? customerMongoUri : platformMongoUri;
|
|
92
|
+
const uploadDir = resolveUploadDir(options.uploadDir);
|
|
93
|
+
logger.log(`========================================`);
|
|
94
|
+
logger.log(`[ChatModule.forRoot] mode=${mode}`);
|
|
95
|
+
logger.log(`[ChatModule.forRoot] apiKey=${options.apiKey?.slice(0, 12)}...`);
|
|
96
|
+
logger.log(`[ChatModule.forRoot] mongoUri=${finalMongoUri}`);
|
|
97
|
+
logger.log(`[ChatModule.forRoot] platformMongoUri=${platformMongoUri}`);
|
|
98
|
+
logger.log(`[ChatModule.forRoot] customerMongoUri=${customerMongoUri || '(not provided — using platform DB)'}`);
|
|
99
|
+
logger.log(`[ChatModule.forRoot] uploadDir=${uploadDir}`);
|
|
100
|
+
logger.log(`========================================`);
|
|
101
|
+
const optionsProvider = {
|
|
102
|
+
provide: exports.CHAT_MODULE_OPTIONS,
|
|
103
|
+
useValue: {
|
|
104
|
+
...options,
|
|
105
|
+
mode,
|
|
106
|
+
mongoUri: finalMongoUri,
|
|
107
|
+
platformMongoUri,
|
|
108
|
+
adminValidationUrl,
|
|
109
|
+
uploadDir,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const uploadDirProvider = {
|
|
113
|
+
provide: exports.CHAT_UPLOAD_DIR,
|
|
114
|
+
useValue: uploadDir,
|
|
115
|
+
};
|
|
116
|
+
const uploadHandlerProvider = {
|
|
117
|
+
provide: exports.CHAT_UPLOAD_HANDLER,
|
|
118
|
+
useValue: options.fileUploadHandler,
|
|
119
|
+
};
|
|
120
|
+
logger.log(`[ChatModule.forRoot] adminValidationUrl=${adminValidationUrl || '(not set — apiKey validation will fail)'}`);
|
|
121
|
+
logger.log(`[ChatModule.forRoot] fileUploadHandler=${options.fileUploadHandler ? 'custom' : 'default (multer disk)'}`);
|
|
122
|
+
return {
|
|
123
|
+
module: ChatModule,
|
|
124
|
+
imports: [
|
|
125
|
+
mongoose_1.MongooseModule.forFeature(schemas_1.schemas),
|
|
126
|
+
],
|
|
127
|
+
providers: [
|
|
128
|
+
optionsProvider,
|
|
129
|
+
uploadDirProvider,
|
|
130
|
+
uploadHandlerProvider,
|
|
131
|
+
chat_gateway_1.ChatGateway,
|
|
132
|
+
message_service_1.MessageService,
|
|
133
|
+
room_service_1.RoomService,
|
|
134
|
+
presence_service_1.PresenceService,
|
|
135
|
+
call_service_1.CallService,
|
|
136
|
+
apikey_service_1.ApiKeyService,
|
|
137
|
+
storage_service_1.StorageService,
|
|
138
|
+
rate_limiter_service_1.RateLimiterService,
|
|
139
|
+
],
|
|
140
|
+
controllers: [users_controller_1.UsersController, validation_controller_1.ValidationController, rooms_controller_1.RoomsController, upload_controller_1.UploadController, ChatUploadsController],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
exports.ChatModule = ChatModule;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.RoomsController = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const room_service_1 = require("../services/room.service");
|
|
18
|
+
const message_service_1 = require("../services/message.service");
|
|
19
|
+
const apikey_service_1 = require("../services/apikey.service");
|
|
20
|
+
let RoomsController = class RoomsController {
|
|
21
|
+
constructor(roomService, messageService, apiKeyService) {
|
|
22
|
+
this.roomService = roomService;
|
|
23
|
+
this.messageService = messageService;
|
|
24
|
+
this.apiKeyService = apiKeyService;
|
|
25
|
+
}
|
|
26
|
+
async resolveTenant(apiKey) {
|
|
27
|
+
const result = await this.apiKeyService.validateKey(apiKey);
|
|
28
|
+
if (!result.valid) {
|
|
29
|
+
throw new common_1.BadRequestException(result.error || 'Invalid apiKey');
|
|
30
|
+
}
|
|
31
|
+
return result.tenantId;
|
|
32
|
+
}
|
|
33
|
+
async listRooms(apiKey, userId, limit, skip) {
|
|
34
|
+
if (!apiKey) {
|
|
35
|
+
throw new common_1.BadRequestException('x-api-key header is required');
|
|
36
|
+
}
|
|
37
|
+
if (!userId) {
|
|
38
|
+
throw new common_1.BadRequestException('userId query param is required');
|
|
39
|
+
}
|
|
40
|
+
const tenantId = await this.resolveTenant(apiKey);
|
|
41
|
+
const parsedLimit = limit ? Math.min(Math.max(parseInt(limit, 10) || 50, 1), 200) : 50;
|
|
42
|
+
const parsedSkip = skip ? Math.max(parseInt(skip, 10) || 0, 0) : 0;
|
|
43
|
+
return this.roomService.getRooms(tenantId, userId, parsedLimit, parsedSkip);
|
|
44
|
+
}
|
|
45
|
+
async openDirectRoom(body) {
|
|
46
|
+
if (!body.userId || !Array.isArray(body.members) || body.members.length < 2) {
|
|
47
|
+
throw new common_1.BadRequestException('userId and members array are required');
|
|
48
|
+
}
|
|
49
|
+
if (!body.apiKey) {
|
|
50
|
+
throw new common_1.BadRequestException('apiKey is required');
|
|
51
|
+
}
|
|
52
|
+
const tenantId = await this.resolveTenant(body.apiKey);
|
|
53
|
+
const uniqueMembers = Array.from(new Set(body.members));
|
|
54
|
+
if (!uniqueMembers.includes(body.userId)) {
|
|
55
|
+
uniqueMembers.push(body.userId);
|
|
56
|
+
}
|
|
57
|
+
const room = await this.roomService.findOrCreateDirectRoom(tenantId, uniqueMembers, { userId: body.userId }, body.name);
|
|
58
|
+
const messages = await this.messageService.getMessages(tenantId, room._id);
|
|
59
|
+
return { room, messages: messages.messages };
|
|
60
|
+
}
|
|
61
|
+
async getRoomMessages(roomId, apiKey) {
|
|
62
|
+
if (!apiKey) {
|
|
63
|
+
throw new common_1.BadRequestException('x-api-key header is required');
|
|
64
|
+
}
|
|
65
|
+
const tenantId = await this.resolveTenant(apiKey);
|
|
66
|
+
console.log('========================================');
|
|
67
|
+
console.log('[CONTROLLER] GET rooms/:roomId/messages');
|
|
68
|
+
console.log('[CONTROLLER] roomId param =', roomId, 'type =', typeof roomId);
|
|
69
|
+
console.log('[CONTROLLER] tenantId =', tenantId);
|
|
70
|
+
console.log('========================================');
|
|
71
|
+
const messages = await this.messageService.getMessages(tenantId, roomId);
|
|
72
|
+
console.log(`[CONTROLLER] messageService.getMessages returned:`, JSON.stringify({
|
|
73
|
+
messageCount: messages?.messages?.length,
|
|
74
|
+
firstMessageId: messages?.messages?.[0]?._id,
|
|
75
|
+
firstMessageRoomId: messages?.messages?.[0]?.roomId,
|
|
76
|
+
firstMessageRoomIdType: typeof messages?.messages?.[0]?.roomId,
|
|
77
|
+
}));
|
|
78
|
+
return messages;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
exports.RoomsController = RoomsController;
|
|
82
|
+
__decorate([
|
|
83
|
+
(0, common_1.Get)(),
|
|
84
|
+
__param(0, (0, common_1.Headers)('x-api-key')),
|
|
85
|
+
__param(1, (0, common_1.Query)('userId')),
|
|
86
|
+
__param(2, (0, common_1.Query)('limit')),
|
|
87
|
+
__param(3, (0, common_1.Query)('skip')),
|
|
88
|
+
__metadata("design:type", Function),
|
|
89
|
+
__metadata("design:paramtypes", [String, String, String, String]),
|
|
90
|
+
__metadata("design:returntype", Promise)
|
|
91
|
+
], RoomsController.prototype, "listRooms", null);
|
|
92
|
+
__decorate([
|
|
93
|
+
(0, common_1.Post)('direct'),
|
|
94
|
+
__param(0, (0, common_1.Body)()),
|
|
95
|
+
__metadata("design:type", Function),
|
|
96
|
+
__metadata("design:paramtypes", [Object]),
|
|
97
|
+
__metadata("design:returntype", Promise)
|
|
98
|
+
], RoomsController.prototype, "openDirectRoom", null);
|
|
99
|
+
__decorate([
|
|
100
|
+
(0, common_1.Get)(':roomId/messages'),
|
|
101
|
+
__param(0, (0, common_1.Param)('roomId')),
|
|
102
|
+
__param(1, (0, common_1.Headers)('x-api-key')),
|
|
103
|
+
__metadata("design:type", Function),
|
|
104
|
+
__metadata("design:paramtypes", [String, String]),
|
|
105
|
+
__metadata("design:returntype", Promise)
|
|
106
|
+
], RoomsController.prototype, "getRoomMessages", null);
|
|
107
|
+
exports.RoomsController = RoomsController = __decorate([
|
|
108
|
+
(0, common_1.Controller)('rooms'),
|
|
109
|
+
__metadata("design:paramtypes", [room_service_1.RoomService,
|
|
110
|
+
message_service_1.MessageService,
|
|
111
|
+
apikey_service_1.ApiKeyService])
|
|
112
|
+
], RoomsController);
|