offbyt 1.0.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 +2 -0
- package/cli/index.js +2 -0
- package/cli.js +206 -0
- package/core/detector/detectAxios.js +107 -0
- package/core/detector/detectFetch.js +148 -0
- package/core/detector/detectForms.js +55 -0
- package/core/detector/detectSocket.js +341 -0
- package/core/generator/generateControllers.js +17 -0
- package/core/generator/generateModels.js +25 -0
- package/core/generator/generateRoutes.js +17 -0
- package/core/generator/generateServer.js +18 -0
- package/core/generator/generateSocket.js +160 -0
- package/core/index.js +14 -0
- package/core/ir/IRTypes.js +25 -0
- package/core/ir/buildIR.js +83 -0
- package/core/parser/parseJS.js +26 -0
- package/core/parser/parseTS.js +27 -0
- package/core/rules/relationRules.js +38 -0
- package/core/rules/resourceRules.js +32 -0
- package/core/rules/schemaInference.js +26 -0
- package/core/scanner/scanProject.js +58 -0
- package/deploy/cloudflare.js +41 -0
- package/deploy/cloudflareWorker.js +122 -0
- package/deploy/connect.js +198 -0
- package/deploy/flyio.js +51 -0
- package/deploy/index.js +322 -0
- package/deploy/netlify.js +29 -0
- package/deploy/railway.js +215 -0
- package/deploy/render.js +195 -0
- package/deploy/utils.js +383 -0
- package/deploy/vercel.js +29 -0
- package/index.js +18 -0
- package/lib/generator/advancedCrudGenerator.js +475 -0
- package/lib/generator/crudCodeGenerator.js +486 -0
- package/lib/generator/irBasedGenerator.js +360 -0
- package/lib/ir-builder/index.js +16 -0
- package/lib/ir-builder/irBuilder.js +330 -0
- package/lib/ir-builder/rulesEngine.js +353 -0
- package/lib/ir-builder/templateEngine.js +193 -0
- package/lib/ir-builder/templates/index.js +14 -0
- package/lib/ir-builder/templates/model.template.js +47 -0
- package/lib/ir-builder/templates/routes-generic.template.js +66 -0
- package/lib/ir-builder/templates/routes-user.template.js +105 -0
- package/lib/ir-builder/templates/routes.template.js +102 -0
- package/lib/ir-builder/templates/validation.template.js +15 -0
- package/lib/ir-integration.js +349 -0
- package/lib/modes/benchmark.js +162 -0
- package/lib/modes/configBasedGenerator.js +2258 -0
- package/lib/modes/connect.js +1125 -0
- package/lib/modes/doctorAi.js +172 -0
- package/lib/modes/generateApi.js +435 -0
- package/lib/modes/interactiveSetup.js +548 -0
- package/lib/modes/offline.clean.js +14 -0
- package/lib/modes/offline.enhanced.js +787 -0
- package/lib/modes/offline.js +295 -0
- package/lib/modes/offline.v2.js +13 -0
- package/lib/modes/sync.js +629 -0
- package/lib/scanner/apiEndpointExtractor.js +387 -0
- package/lib/scanner/authPatternDetector.js +54 -0
- package/lib/scanner/frontendScanner.js +642 -0
- package/lib/utils/apiClientGenerator.js +242 -0
- package/lib/utils/apiScanner.js +95 -0
- package/lib/utils/codeInjector.js +350 -0
- package/lib/utils/doctor.js +381 -0
- package/lib/utils/envGenerator.js +36 -0
- package/lib/utils/loadTester.js +61 -0
- package/lib/utils/performanceAnalyzer.js +298 -0
- package/lib/utils/resourceDetector.js +281 -0
- package/package.json +20 -0
- package/templates/.env.template +31 -0
- package/templates/advanced.model.template.js +201 -0
- package/templates/advanced.route.template.js +341 -0
- package/templates/auth.middleware.template.js +87 -0
- package/templates/auth.routes.template.js +238 -0
- package/templates/auth.user.model.template.js +78 -0
- package/templates/cache.middleware.js +34 -0
- package/templates/chat.models.template.js +260 -0
- package/templates/chat.routes.template.js +478 -0
- package/templates/compression.middleware.js +19 -0
- package/templates/database.config.js +74 -0
- package/templates/errorHandler.middleware.js +54 -0
- package/templates/express/controller.ejs +26 -0
- package/templates/express/model.ejs +9 -0
- package/templates/express/route.ejs +18 -0
- package/templates/express/server.ejs +16 -0
- package/templates/frontend.env.template +14 -0
- package/templates/model.template.js +86 -0
- package/templates/package.production.json +51 -0
- package/templates/package.template.json +41 -0
- package/templates/pagination.utility.js +110 -0
- package/templates/production.server.template.js +233 -0
- package/templates/rateLimiter.middleware.js +36 -0
- package/templates/requestLogger.middleware.js +19 -0
- package/templates/response.helper.js +179 -0
- package/templates/route.template.js +130 -0
- package/templates/security.middleware.js +78 -0
- package/templates/server.template.js +91 -0
- package/templates/socket.server.template.js +433 -0
- package/templates/utils.helper.js +157 -0
- package/templates/validation.middleware.js +63 -0
- package/templates/validation.schema.js +128 -0
- package/utils/fileWriter.js +15 -0
- package/utils/logger.js +18 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
import User from '../models/User.js';
|
|
4
|
+
import { authenticateToken } from '../middleware/auth.js';
|
|
5
|
+
|
|
6
|
+
const router = express.Router();
|
|
7
|
+
|
|
8
|
+
// Ensure JWT_SECRET is consistent across all auth checks
|
|
9
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'your_super_secret_jwt_key_change_this_in_production';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate JWT Token
|
|
13
|
+
*/
|
|
14
|
+
const generateToken = (user) => {
|
|
15
|
+
return jwt.sign(
|
|
16
|
+
{
|
|
17
|
+
id: user._id,
|
|
18
|
+
email: user.email,
|
|
19
|
+
name: user.name,
|
|
20
|
+
role: user.role
|
|
21
|
+
},
|
|
22
|
+
JWT_SECRET,
|
|
23
|
+
{ expiresIn: process.env.JWT_EXPIRE || process.env.JWT_EXPIRES_IN || '7d' }
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* POST /api/auth/signup
|
|
29
|
+
* Register new user
|
|
30
|
+
*/
|
|
31
|
+
router.post('/signup', async (req, res, next) => {
|
|
32
|
+
try {
|
|
33
|
+
const { name, email, password } = req.body;
|
|
34
|
+
|
|
35
|
+
// Validation
|
|
36
|
+
if (!name || !email || !password) {
|
|
37
|
+
return res.status(400).json({
|
|
38
|
+
success: false,
|
|
39
|
+
message: 'Please provide name, email, and password',
|
|
40
|
+
code: 'MISSING_FIELDS'
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if user already exists
|
|
45
|
+
const existingUser = await User.findOne({ email });
|
|
46
|
+
if (existingUser) {
|
|
47
|
+
return res.status(409).json({
|
|
48
|
+
success: false,
|
|
49
|
+
message: 'User with this email already exists',
|
|
50
|
+
code: 'EMAIL_EXISTS'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate email format
|
|
55
|
+
const emailRegex = /^\S+@\S+\.\S+$/;
|
|
56
|
+
if (!emailRegex.test(email)) {
|
|
57
|
+
return res.status(400).json({
|
|
58
|
+
success: false,
|
|
59
|
+
message: 'Please provide a valid email',
|
|
60
|
+
code: 'INVALID_EMAIL'
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate password length
|
|
65
|
+
if (password.length < 6) {
|
|
66
|
+
return res.status(400).json({
|
|
67
|
+
success: false,
|
|
68
|
+
message: 'Password must be at least 6 characters',
|
|
69
|
+
code: 'WEAK_PASSWORD'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Create new user (password will be hashed by pre-save hook)
|
|
74
|
+
const user = new User({
|
|
75
|
+
name: name.trim(),
|
|
76
|
+
email: email.toLowerCase().trim(),
|
|
77
|
+
password
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await user.save();
|
|
81
|
+
|
|
82
|
+
// Generate JWT token
|
|
83
|
+
const token = generateToken(user);
|
|
84
|
+
|
|
85
|
+
res.status(201).json({
|
|
86
|
+
success: true,
|
|
87
|
+
message: 'Account created successfully',
|
|
88
|
+
data: {
|
|
89
|
+
token,
|
|
90
|
+
user: user.toJSON()
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error.name === 'ValidationError') {
|
|
95
|
+
return res.status(400).json({
|
|
96
|
+
success: false,
|
|
97
|
+
message: 'Validation Error',
|
|
98
|
+
code: 'VALIDATION_ERROR',
|
|
99
|
+
errors: Object.values(error.errors).map(e => e.message)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
next(error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* POST /api/auth/login
|
|
108
|
+
* User login with email and password
|
|
109
|
+
*/
|
|
110
|
+
router.post('/login', async (req, res, next) => {
|
|
111
|
+
try {
|
|
112
|
+
const { email, password } = req.body;
|
|
113
|
+
|
|
114
|
+
// Validation
|
|
115
|
+
if (!email || !password) {
|
|
116
|
+
return res.status(400).json({
|
|
117
|
+
success: false,
|
|
118
|
+
message: 'Please provide email and password',
|
|
119
|
+
code: 'MISSING_FIELDS'
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Find user and include password (excluded by default)
|
|
124
|
+
const user = await User.findOne({ email }).select('+password');
|
|
125
|
+
|
|
126
|
+
if (!user || !user.isActive) {
|
|
127
|
+
return res.status(401).json({
|
|
128
|
+
success: false,
|
|
129
|
+
message: 'Invalid email or password',
|
|
130
|
+
code: 'INVALID_CREDENTIALS'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Verify password
|
|
135
|
+
const isPasswordValid = await user.comparePassword(password);
|
|
136
|
+
|
|
137
|
+
if (!isPasswordValid) {
|
|
138
|
+
return res.status(401).json({
|
|
139
|
+
success: false,
|
|
140
|
+
message: 'Invalid email or password',
|
|
141
|
+
code: 'INVALID_CREDENTIALS'
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Update last login
|
|
146
|
+
user.lastLogin = new Date();
|
|
147
|
+
await user.save();
|
|
148
|
+
|
|
149
|
+
// Generate JWT token
|
|
150
|
+
const token = generateToken(user);
|
|
151
|
+
|
|
152
|
+
res.status(200).json({
|
|
153
|
+
success: true,
|
|
154
|
+
message: 'Login successful',
|
|
155
|
+
data: {
|
|
156
|
+
token,
|
|
157
|
+
user: user.toJSON()
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
next(error);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* GET /api/auth/profile
|
|
167
|
+
* Get current user profile (Protected route)
|
|
168
|
+
*/
|
|
169
|
+
router.get('/profile', authenticateToken, async (req, res, next) => {
|
|
170
|
+
try {
|
|
171
|
+
const user = await User.findById(req.user.id);
|
|
172
|
+
|
|
173
|
+
if (!user || !user.isActive) {
|
|
174
|
+
return res.status(404).json({
|
|
175
|
+
success: false,
|
|
176
|
+
message: 'User not found',
|
|
177
|
+
code: 'USER_NOT_FOUND'
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
res.status(200).json({
|
|
182
|
+
success: true,
|
|
183
|
+
data: {
|
|
184
|
+
user: user.toJSON()
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
} catch (error) {
|
|
188
|
+
next(error);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* PUT /api/auth/profile
|
|
194
|
+
* Update user profile (Protected route)
|
|
195
|
+
*/
|
|
196
|
+
router.put('/profile', authenticateToken, async (req, res, next) => {
|
|
197
|
+
try {
|
|
198
|
+
const { name } = req.body;
|
|
199
|
+
const user = await User.findById(req.user.id);
|
|
200
|
+
|
|
201
|
+
if (!user) {
|
|
202
|
+
return res.status(404).json({
|
|
203
|
+
success: false,
|
|
204
|
+
message: 'User not found',
|
|
205
|
+
code: 'USER_NOT_FOUND'
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (name) {
|
|
210
|
+
user.name = name.trim();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await user.save();
|
|
214
|
+
|
|
215
|
+
res.status(200).json({
|
|
216
|
+
success: true,
|
|
217
|
+
message: 'Profile updated successfully',
|
|
218
|
+
data: {
|
|
219
|
+
user: user.toJSON()
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
} catch (error) {
|
|
223
|
+
next(error);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* POST /api/auth/logout
|
|
229
|
+
* Logout user (clears token on frontend)
|
|
230
|
+
*/
|
|
231
|
+
router.post('/logout', authenticateToken, (req, res) => {
|
|
232
|
+
res.status(200).json({
|
|
233
|
+
success: true,
|
|
234
|
+
message: 'Logged out successfully'
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
export default router;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
import bcrypt from 'bcryptjs';
|
|
3
|
+
|
|
4
|
+
const userSchema = new mongoose.Schema({
|
|
5
|
+
name: {
|
|
6
|
+
type: String,
|
|
7
|
+
required: [true, 'Please provide a name'],
|
|
8
|
+
trim: true,
|
|
9
|
+
minlength: [2, 'Name must be at least 2 characters']
|
|
10
|
+
},
|
|
11
|
+
email: {
|
|
12
|
+
type: String,
|
|
13
|
+
required: [true, 'Please provide an email'],
|
|
14
|
+
unique: true,
|
|
15
|
+
lowercase: true,
|
|
16
|
+
match: [/^\S+@\S+\.\S+$/, 'Please provide a valid email'],
|
|
17
|
+
index: true
|
|
18
|
+
},
|
|
19
|
+
password: {
|
|
20
|
+
type: String,
|
|
21
|
+
required: [true, 'Please provide a password'],
|
|
22
|
+
minlength: [6, 'Password must be at least 6 characters'],
|
|
23
|
+
select: false // Don't return password by default
|
|
24
|
+
},
|
|
25
|
+
role: {
|
|
26
|
+
type: String,
|
|
27
|
+
enum: ['student', 'organizer', 'admin'],
|
|
28
|
+
default: 'student'
|
|
29
|
+
},
|
|
30
|
+
isActive: {
|
|
31
|
+
type: Boolean,
|
|
32
|
+
default: true
|
|
33
|
+
},
|
|
34
|
+
lastLogin: Date,
|
|
35
|
+
createdAt: {
|
|
36
|
+
type: Date,
|
|
37
|
+
default: Date.now
|
|
38
|
+
}
|
|
39
|
+
}, {
|
|
40
|
+
timestamps: true,
|
|
41
|
+
collection: 'users'
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Hash password before saving
|
|
45
|
+
userSchema.pre('save', async function(next) {
|
|
46
|
+
if (!this.isModified('password')) {
|
|
47
|
+
return next();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const salt = await bcrypt.genSalt(parseInt(process.env.BCRYPT_ROUNDS || 10));
|
|
52
|
+
this.password = await bcrypt.hash(this.password, salt);
|
|
53
|
+
next();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
next(error);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Method to compare password
|
|
60
|
+
userSchema.methods.comparePassword = async function(candidatePassword) {
|
|
61
|
+
try {
|
|
62
|
+
return await bcrypt.compare(candidatePassword, this.password);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error('Error comparing passwords');
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Return user without password
|
|
69
|
+
userSchema.methods.toJSON = function() {
|
|
70
|
+
const obj = this.toObject();
|
|
71
|
+
delete obj.password;
|
|
72
|
+
delete obj.__v;
|
|
73
|
+
return obj;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const User = mongoose.model('User', userSchema);
|
|
77
|
+
|
|
78
|
+
export default User;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caching Middleware
|
|
3
|
+
* Implements HTTP caching for GET requests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const cacheMiddleware = (duration = 60) => {
|
|
7
|
+
return (req, res, next) => {
|
|
8
|
+
// Only cache GET requests
|
|
9
|
+
if (req.method !== 'GET') {
|
|
10
|
+
return next();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Set cache headers
|
|
14
|
+
res.set('Cache-Control', `public, max-age=${duration}`);
|
|
15
|
+
|
|
16
|
+
// Skip if no-cache header is set
|
|
17
|
+
if (req.headers['cache-control'] === 'no-cache') {
|
|
18
|
+
res.set('Cache-Control', 'no-cache');
|
|
19
|
+
return next();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
next();
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// No-cache for sensitive endpoints
|
|
27
|
+
export const noCacheMiddleware = (req, res, next) => {
|
|
28
|
+
res.set('Cache-Control', 'private, no-cache, no-store, must-revalidate');
|
|
29
|
+
res.set('Pragma', 'no-cache');
|
|
30
|
+
res.set('Expires', '0');
|
|
31
|
+
next();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default cacheMiddleware;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Models Templates
|
|
3
|
+
*
|
|
4
|
+
* Provides Mongoose models for:
|
|
5
|
+
* - Message
|
|
6
|
+
* - Conversation
|
|
7
|
+
* - MessageReaction (optional)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const messageModelTemplate = `import mongoose from 'mongoose';
|
|
11
|
+
|
|
12
|
+
const messageSchema = new mongoose.Schema({
|
|
13
|
+
conversation: {
|
|
14
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
15
|
+
ref: 'Conversation',
|
|
16
|
+
required: true,
|
|
17
|
+
index: true
|
|
18
|
+
},
|
|
19
|
+
sender: {
|
|
20
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
21
|
+
ref: 'User',
|
|
22
|
+
required: true,
|
|
23
|
+
index: true
|
|
24
|
+
},
|
|
25
|
+
content: {
|
|
26
|
+
type: String,
|
|
27
|
+
required: true,
|
|
28
|
+
maxlength: 10000
|
|
29
|
+
},
|
|
30
|
+
type: {
|
|
31
|
+
type: String,
|
|
32
|
+
enum: ['text', 'image', 'video', 'audio', 'file', 'system'],
|
|
33
|
+
default: 'text'
|
|
34
|
+
},
|
|
35
|
+
metadata: {
|
|
36
|
+
type: mongoose.Schema.Types.Mixed,
|
|
37
|
+
default: {}
|
|
38
|
+
},
|
|
39
|
+
// For file messages
|
|
40
|
+
fileUrl: String,
|
|
41
|
+
fileName: String,
|
|
42
|
+
fileSize: Number,
|
|
43
|
+
fileType: String,
|
|
44
|
+
|
|
45
|
+
// Message status
|
|
46
|
+
status: {
|
|
47
|
+
type: String,
|
|
48
|
+
enum: ['sent', 'delivered', 'read'],
|
|
49
|
+
default: 'sent'
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// Read receipts
|
|
53
|
+
readBy: [{
|
|
54
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
55
|
+
ref: 'User'
|
|
56
|
+
}],
|
|
57
|
+
readAt: Date,
|
|
58
|
+
|
|
59
|
+
// Edited
|
|
60
|
+
isEdited: {
|
|
61
|
+
type: Boolean,
|
|
62
|
+
default: false
|
|
63
|
+
},
|
|
64
|
+
editedAt: Date,
|
|
65
|
+
|
|
66
|
+
// Deleted
|
|
67
|
+
isDeleted: {
|
|
68
|
+
type: Boolean,
|
|
69
|
+
default: false
|
|
70
|
+
},
|
|
71
|
+
deletedAt: Date,
|
|
72
|
+
|
|
73
|
+
// Reply/Thread
|
|
74
|
+
replyTo: {
|
|
75
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
76
|
+
ref: 'Message'
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Reactions
|
|
80
|
+
reactions: [{
|
|
81
|
+
user: {
|
|
82
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
83
|
+
ref: 'User'
|
|
84
|
+
},
|
|
85
|
+
emoji: String,
|
|
86
|
+
createdAt: {
|
|
87
|
+
type: Date,
|
|
88
|
+
default: Date.now
|
|
89
|
+
}
|
|
90
|
+
}],
|
|
91
|
+
|
|
92
|
+
timestamp: {
|
|
93
|
+
type: Date,
|
|
94
|
+
default: Date.now,
|
|
95
|
+
index: true
|
|
96
|
+
}
|
|
97
|
+
}, {
|
|
98
|
+
timestamps: true
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Indexes for performance
|
|
102
|
+
messageSchema.index({ conversation: 1, timestamp: -1 });
|
|
103
|
+
messageSchema.index({ sender: 1, timestamp: -1 });
|
|
104
|
+
messageSchema.index({ conversation: 1, timestamp: 1 });
|
|
105
|
+
|
|
106
|
+
// Virtuals
|
|
107
|
+
messageSchema.virtual('readCount').get(function() {
|
|
108
|
+
return this.readBy ? this.readBy.length : 0;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
messageSchema.set('toJSON', { virtuals: true });
|
|
112
|
+
messageSchema.set('toObject', { virtuals: true });
|
|
113
|
+
|
|
114
|
+
const Message = mongoose.model('Message', messageSchema);
|
|
115
|
+
|
|
116
|
+
export default Message;
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
export const conversationModelTemplate = `import mongoose from 'mongoose';
|
|
120
|
+
|
|
121
|
+
const conversationSchema = new mongoose.Schema({
|
|
122
|
+
name: {
|
|
123
|
+
type: String,
|
|
124
|
+
trim: true
|
|
125
|
+
},
|
|
126
|
+
type: {
|
|
127
|
+
type: String,
|
|
128
|
+
enum: ['direct', 'group', 'channel'],
|
|
129
|
+
default: 'direct',
|
|
130
|
+
required: true
|
|
131
|
+
},
|
|
132
|
+
participants: [{
|
|
133
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
134
|
+
ref: 'User',
|
|
135
|
+
required: true
|
|
136
|
+
}],
|
|
137
|
+
admins: [{
|
|
138
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
139
|
+
ref: 'User'
|
|
140
|
+
}],
|
|
141
|
+
createdBy: {
|
|
142
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
143
|
+
ref: 'User',
|
|
144
|
+
required: true
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// Last message info
|
|
148
|
+
lastMessage: {
|
|
149
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
150
|
+
ref: 'Message'
|
|
151
|
+
},
|
|
152
|
+
lastMessageAt: {
|
|
153
|
+
type: Date,
|
|
154
|
+
default: Date.now,
|
|
155
|
+
index: true
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// Group/Channel settings
|
|
159
|
+
description: String,
|
|
160
|
+
avatar: String,
|
|
161
|
+
|
|
162
|
+
// Muted users
|
|
163
|
+
mutedBy: [{
|
|
164
|
+
user: {
|
|
165
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
166
|
+
ref: 'User'
|
|
167
|
+
},
|
|
168
|
+
mutedUntil: Date
|
|
169
|
+
}],
|
|
170
|
+
|
|
171
|
+
// Archived
|
|
172
|
+
archivedBy: [{
|
|
173
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
174
|
+
ref: 'User'
|
|
175
|
+
}],
|
|
176
|
+
|
|
177
|
+
// Pinned messages
|
|
178
|
+
pinnedMessages: [{
|
|
179
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
180
|
+
ref: 'Message'
|
|
181
|
+
}],
|
|
182
|
+
|
|
183
|
+
// Group settings
|
|
184
|
+
settings: {
|
|
185
|
+
allowMemberMessages: {
|
|
186
|
+
type: Boolean,
|
|
187
|
+
default: true
|
|
188
|
+
},
|
|
189
|
+
allowFileSharing: {
|
|
190
|
+
type: Boolean,
|
|
191
|
+
default: true
|
|
192
|
+
},
|
|
193
|
+
maxMembers: {
|
|
194
|
+
type: Number,
|
|
195
|
+
default: 100
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
isActive: {
|
|
200
|
+
type: Boolean,
|
|
201
|
+
default: true
|
|
202
|
+
}
|
|
203
|
+
}, {
|
|
204
|
+
timestamps: true
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Indexes
|
|
208
|
+
conversationSchema.index({ participants: 1, lastMessageAt: -1 });
|
|
209
|
+
conversationSchema.index({ type: 1, lastMessageAt: -1 });
|
|
210
|
+
conversationSchema.index({ createdBy: 1 });
|
|
211
|
+
|
|
212
|
+
// Methods
|
|
213
|
+
conversationSchema.methods.isParticipant = function(userId) {
|
|
214
|
+
return this.participants.some(p => p.toString() === userId.toString());
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
conversationSchema.methods.isAdmin = function(userId) {
|
|
218
|
+
return this.admins.some(a => a.toString() === userId.toString());
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
conversationSchema.methods.addParticipant = async function(userId) {
|
|
222
|
+
if (!this.isParticipant(userId)) {
|
|
223
|
+
this.participants.push(userId);
|
|
224
|
+
await this.save();
|
|
225
|
+
}
|
|
226
|
+
return this;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
conversationSchema.methods.removeParticipant = async function(userId) {
|
|
230
|
+
this.participants = this.participants.filter(
|
|
231
|
+
p => p.toString() !== userId.toString()
|
|
232
|
+
);
|
|
233
|
+
await this.save();
|
|
234
|
+
return this;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Statics
|
|
238
|
+
conversationSchema.statics.findByUser = function(userId) {
|
|
239
|
+
return this.find({ participants: userId, isActive: true })
|
|
240
|
+
.populate('participants', 'name email avatar')
|
|
241
|
+
.populate('lastMessage')
|
|
242
|
+
.sort({ lastMessageAt: -1 });
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
conversationSchema.statics.findDirectConversation = function(user1Id, user2Id) {
|
|
246
|
+
return this.findOne({
|
|
247
|
+
type: 'direct',
|
|
248
|
+
participants: { $all: [user1Id, user2Id], $size: 2 }
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const Conversation = mongoose.model('Conversation', conversationSchema);
|
|
253
|
+
|
|
254
|
+
export default Conversation;
|
|
255
|
+
`;
|
|
256
|
+
|
|
257
|
+
export const chatModelsExport = `export { messageModelTemplate, conversationModelTemplate };
|
|
258
|
+
`;
|
|
259
|
+
|
|
260
|
+
export default { messageModelTemplate, conversationModelTemplate };
|