express-genix 2.0.1 → 3.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 +13 -1
- package/index.js +22 -1
- package/lib/features.js +8 -0
- package/lib/generator.js +77 -2
- package/package.json +2 -2
- package/templates/config/queue.js.ejs +29 -0
- package/templates/config/schema.prisma.ejs +2 -0
- package/templates/controllers/adminController.js.ejs +109 -0
- package/templates/controllers/authController.js.ejs +9 -4
- package/templates/controllers/userController.js.ejs +1 -0
- package/templates/core/app.js.ejs +50 -1
- package/templates/core/env.ejs +17 -0
- package/templates/core/env.example.ejs +17 -0
- package/templates/core/package.json.ejs +8 -1
- package/templates/core/server.js.ejs +5 -2
- package/templates/graphql/resolvers.js.ejs +61 -0
- package/templates/graphql/typeDefs.js.ejs +53 -0
- package/templates/jobs/worker.js.ejs +60 -0
- package/templates/middleware/auditLog.js.ejs +62 -0
- package/templates/middleware/metrics.js.ejs +65 -0
- package/templates/middleware/rbac.js.ejs +86 -0
- package/templates/middleware/upload.js.ejs +50 -0
- package/templates/models/User.mongo.js.ejs +29 -0
- package/templates/models/User.postgres.js.ejs +7 -1
- package/templates/routes/adminRoutes.js.ejs +150 -0
- package/templates/routes/index.js.ejs +6 -0
- package/templates/routes/jobRoutes.js.ejs +85 -0
- package/templates/routes/uploadRoutes.js.ejs +100 -0
- package/templates/services/authService.js.ejs +1 -0
- package/templates/services/emailService.js.ejs +88 -0
- package/templates/services/userService.mongodb.js.ejs +32 -2
- package/templates/services/userService.postgres.js.ejs +33 -1
- package/templates/services/userService.prisma.js.ejs +50 -6
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { uploadImage, uploadAny } = require('../middleware/upload');
|
|
5
|
+
const { success } = require('../utils/response');
|
|
6
|
+
const { AppError } = require('../utils/errors');
|
|
7
|
+
<% if (hasAuth) { %>const { authenticateToken } = require('../middleware/auth');<% } %>
|
|
8
|
+
|
|
9
|
+
const router = express.Router();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @swagger
|
|
13
|
+
* /uploads/single:
|
|
14
|
+
* post:
|
|
15
|
+
* summary: Upload a single image
|
|
16
|
+
* tags: [Uploads]<% if (hasAuth) { %>
|
|
17
|
+
* security:
|
|
18
|
+
* - bearerAuth: []<% } %>
|
|
19
|
+
* requestBody:
|
|
20
|
+
* required: true
|
|
21
|
+
* content:
|
|
22
|
+
* multipart/form-data:
|
|
23
|
+
* schema:
|
|
24
|
+
* type: object
|
|
25
|
+
* properties:
|
|
26
|
+
* file:
|
|
27
|
+
* type: string
|
|
28
|
+
* format: binary
|
|
29
|
+
* responses:
|
|
30
|
+
* 200:
|
|
31
|
+
* description: File uploaded
|
|
32
|
+
* 400:
|
|
33
|
+
* description: Invalid file
|
|
34
|
+
*/
|
|
35
|
+
router.post('/single',
|
|
36
|
+
<% if (hasAuth) { %>authenticateToken,<% } %>
|
|
37
|
+
uploadImage.single('file'),
|
|
38
|
+
(req, res) => {
|
|
39
|
+
if (!req.file) {
|
|
40
|
+
throw new AppError('No file uploaded', 400);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return success(res, {
|
|
44
|
+
message: 'File uploaded successfully',
|
|
45
|
+
file: {
|
|
46
|
+
filename: req.file.filename,
|
|
47
|
+
originalName: req.file.originalname,
|
|
48
|
+
size: req.file.size,
|
|
49
|
+
mimetype: req.file.mimetype,
|
|
50
|
+
path: `/uploads/${req.file.filename}`,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @swagger
|
|
58
|
+
* /uploads/multiple:
|
|
59
|
+
* post:
|
|
60
|
+
* summary: Upload multiple files (max 5)
|
|
61
|
+
* tags: [Uploads]<% if (hasAuth) { %>
|
|
62
|
+
* security:
|
|
63
|
+
* - bearerAuth: []<% } %>
|
|
64
|
+
* requestBody:
|
|
65
|
+
* required: true
|
|
66
|
+
* content:
|
|
67
|
+
* multipart/form-data:
|
|
68
|
+
* schema:
|
|
69
|
+
* type: object
|
|
70
|
+
* properties:
|
|
71
|
+
* files:
|
|
72
|
+
* type: array
|
|
73
|
+
* items:
|
|
74
|
+
* type: string
|
|
75
|
+
* format: binary
|
|
76
|
+
* responses:
|
|
77
|
+
* 200:
|
|
78
|
+
* description: Files uploaded
|
|
79
|
+
*/
|
|
80
|
+
router.post('/multiple',
|
|
81
|
+
<% if (hasAuth) { %>authenticateToken,<% } %>
|
|
82
|
+
uploadAny.array('files', 5),
|
|
83
|
+
(req, res) => {
|
|
84
|
+
if (!req.files || req.files.length === 0) {
|
|
85
|
+
throw new AppError('No files uploaded', 400);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const files = req.files.map((file) => ({
|
|
89
|
+
filename: file.filename,
|
|
90
|
+
originalName: file.originalname,
|
|
91
|
+
size: file.size,
|
|
92
|
+
mimetype: file.mimetype,
|
|
93
|
+
path: `/uploads/${file.filename}`,
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
return success(res, { message: `${files.length} file(s) uploaded`, files });
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
module.exports = router;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const nodemailer = require('nodemailer');
|
|
2
|
+
const logger = require('../utils/logger');
|
|
3
|
+
|
|
4
|
+
let transporter;
|
|
5
|
+
|
|
6
|
+
const getTransporter = () => {
|
|
7
|
+
if (transporter) return transporter;
|
|
8
|
+
|
|
9
|
+
if (process.env.NODE_ENV === 'production') {
|
|
10
|
+
transporter = nodemailer.createTransport({
|
|
11
|
+
host: process.env.SMTP_HOST,
|
|
12
|
+
port: parseInt(process.env.SMTP_PORT, 10) || 587,
|
|
13
|
+
secure: process.env.SMTP_SECURE === 'true',
|
|
14
|
+
auth: {
|
|
15
|
+
user: process.env.SMTP_USER,
|
|
16
|
+
pass: process.env.SMTP_PASS,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
} else {
|
|
20
|
+
// Use Ethereal for development (fake SMTP)
|
|
21
|
+
transporter = nodemailer.createTransport({
|
|
22
|
+
host: 'smtp.ethereal.email',
|
|
23
|
+
port: 587,
|
|
24
|
+
auth: {
|
|
25
|
+
user: process.env.SMTP_USER || '',
|
|
26
|
+
pass: process.env.SMTP_PASS || '',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return transporter;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const sendMail = async ({ to, subject, html, text }) => {
|
|
35
|
+
try {
|
|
36
|
+
const transport = getTransporter();
|
|
37
|
+
const info = await transport.sendMail({
|
|
38
|
+
from: process.env.SMTP_FROM || '"App" <noreply@example.com>',
|
|
39
|
+
to,
|
|
40
|
+
subject,
|
|
41
|
+
html,
|
|
42
|
+
text: text || subject,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
logger.info(`Email sent: ${info.messageId}`);
|
|
46
|
+
|
|
47
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
48
|
+
const previewUrl = nodemailer.getTestMessageUrl(info);
|
|
49
|
+
if (previewUrl) {
|
|
50
|
+
logger.info(`Preview URL: ${previewUrl}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return info;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.error('Failed to send email:', error);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const sendWelcomeEmail = async (email, username) => {
|
|
62
|
+
return sendMail({
|
|
63
|
+
to: email,
|
|
64
|
+
subject: 'Welcome!',
|
|
65
|
+
html: `
|
|
66
|
+
<h1>Welcome, ${username}!</h1>
|
|
67
|
+
<p>Thank you for creating an account. We're glad to have you.</p>
|
|
68
|
+
`,
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const sendPasswordResetEmail = async (email, resetToken) => {
|
|
73
|
+
const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:3000'}/reset-password?token=${resetToken}`;
|
|
74
|
+
|
|
75
|
+
return sendMail({
|
|
76
|
+
to: email,
|
|
77
|
+
subject: 'Password Reset Request',
|
|
78
|
+
html: `
|
|
79
|
+
<h1>Password Reset</h1>
|
|
80
|
+
<p>You requested a password reset. Click the link below to set a new password:</p>
|
|
81
|
+
<p><a href="${resetUrl}">Reset Password</a></p>
|
|
82
|
+
<p>This link expires in 1 hour.</p>
|
|
83
|
+
<p>If you did not request this, please ignore this email.</p>
|
|
84
|
+
`,
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
module.exports = { sendMail, sendWelcomeEmail, sendPasswordResetEmail, getTransporter };
|
|
@@ -22,7 +22,34 @@ const updateById = async (id, updateData) => {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
const deleteById = async (id) => {
|
|
25
|
-
|
|
25
|
+
<% if (hasSoftDelete) { %> const user = await User.findById(id);
|
|
26
|
+
if (user) return user.softDelete();
|
|
27
|
+
return null;
|
|
28
|
+
<% } else { %> return await User.findByIdAndDelete(id);
|
|
29
|
+
<% } %>};
|
|
30
|
+
<% if (hasSoftDelete) { %>
|
|
31
|
+
const restoreById = async (id) => {
|
|
32
|
+
const user = await User.findOne({ _id: id, includeDeleted: true });
|
|
33
|
+
if (user) return user.restore();
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const findDeleted = async ({ page = 1, limit = 20 } = {}) => {
|
|
38
|
+
const skip = (page - 1) * limit;
|
|
39
|
+
const [users, total] = await Promise.all([
|
|
40
|
+
User.find({ deletedAt: { $ne: null }, includeDeleted: true }).select('-password').skip(skip).limit(limit),
|
|
41
|
+
User.countDocuments({ deletedAt: { $ne: null } }),
|
|
42
|
+
]);
|
|
43
|
+
return { users, total, page, totalPages: Math.ceil(total / limit) };
|
|
44
|
+
};
|
|
45
|
+
<% } %>
|
|
46
|
+
const findAll = async ({ page = 1, limit = 20 } = {}) => {
|
|
47
|
+
const skip = (page - 1) * limit;
|
|
48
|
+
const [users, total] = await Promise.all([
|
|
49
|
+
User.find().select('-password').skip(skip).limit(limit).sort({ createdAt: -1 }),
|
|
50
|
+
User.countDocuments(),
|
|
51
|
+
]);
|
|
52
|
+
return { users, total, page, totalPages: Math.ceil(total / limit) };
|
|
26
53
|
};
|
|
27
54
|
|
|
28
55
|
module.exports = {
|
|
@@ -31,4 +58,7 @@ module.exports = {
|
|
|
31
58
|
create,
|
|
32
59
|
updateById,
|
|
33
60
|
deleteById,
|
|
34
|
-
|
|
61
|
+
findAll,
|
|
62
|
+
<% if (hasSoftDelete) { %> restoreById,
|
|
63
|
+
findDeleted,
|
|
64
|
+
<% } %>};
|
|
@@ -21,6 +21,35 @@ const updateById = async (id, updateData) => {
|
|
|
21
21
|
const deleteById = async (id) => {
|
|
22
22
|
return await User.destroy({ where: { id } });
|
|
23
23
|
};
|
|
24
|
+
<% if (hasSoftDelete) { %>
|
|
25
|
+
const restoreById = async (id) => {
|
|
26
|
+
return await User.restore({ where: { id } });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const findDeleted = async ({ page = 1, limit = 20 } = {}) => {
|
|
30
|
+
const offset = (page - 1) * limit;
|
|
31
|
+
const { rows: users, count: total } = await User.findAndCountAll({
|
|
32
|
+
where: { deletedAt: { [require('sequelize').Op.ne]: null } },
|
|
33
|
+
attributes: { exclude: ['password'] },
|
|
34
|
+
order: [['deletedAt', 'DESC']],
|
|
35
|
+
limit,
|
|
36
|
+
offset,
|
|
37
|
+
paranoid: false,
|
|
38
|
+
});
|
|
39
|
+
return { users, total, page, totalPages: Math.ceil(total / limit) };
|
|
40
|
+
};
|
|
41
|
+
<% } %>
|
|
42
|
+
|
|
43
|
+
const findAll = async ({ page = 1, limit = 20 } = {}) => {
|
|
44
|
+
const offset = (page - 1) * limit;
|
|
45
|
+
const { rows: users, count: total } = await User.findAndCountAll({
|
|
46
|
+
attributes: { exclude: ['password'] },
|
|
47
|
+
order: [['createdAt', 'DESC']],
|
|
48
|
+
limit,
|
|
49
|
+
offset,
|
|
50
|
+
});
|
|
51
|
+
return { users, total, page, totalPages: Math.ceil(total / limit) };
|
|
52
|
+
};
|
|
24
53
|
|
|
25
54
|
module.exports = {
|
|
26
55
|
findById,
|
|
@@ -28,4 +57,7 @@ module.exports = {
|
|
|
28
57
|
create,
|
|
29
58
|
updateById,
|
|
30
59
|
deleteById,
|
|
31
|
-
|
|
60
|
+
findAll,
|
|
61
|
+
<% if (hasSoftDelete) { %> restoreById,
|
|
62
|
+
findDeleted,
|
|
63
|
+
<% } %>};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
const { prisma } = require('../config/database');
|
|
2
2
|
|
|
3
3
|
const findById = async (id) => {
|
|
4
|
-
return await prisma.user.
|
|
5
|
-
where: { id },
|
|
6
|
-
select: { id: true, username: true, email: true, createdAt: true, updatedAt: true },
|
|
4
|
+
return await prisma.user.findFirst({
|
|
5
|
+
where: { id<% if (hasSoftDelete) { %>, deletedAt: null<% } %> },
|
|
6
|
+
select: { id: true, username: true, email: true, role: true, createdAt: true, updatedAt: true },
|
|
7
7
|
});
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
const findByEmail = async (email) => {
|
|
11
|
-
return await prisma.user.
|
|
11
|
+
return await prisma.user.findFirst({ where: { email<% if (hasSoftDelete) { %>, deletedAt: null<% } %> } });
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
const create = async (userData) => {
|
|
@@ -24,7 +24,48 @@ const updateById = async (id, updateData) => {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const deleteById = async (id) => {
|
|
27
|
-
return await prisma.user.
|
|
27
|
+
<% if (hasSoftDelete) { %> return await prisma.user.update({
|
|
28
|
+
where: { id },
|
|
29
|
+
data: { deletedAt: new Date() },
|
|
30
|
+
});
|
|
31
|
+
<% } else { %> return await prisma.user.delete({ where: { id } });
|
|
32
|
+
<% } %>};
|
|
33
|
+
<% if (hasSoftDelete) { %>
|
|
34
|
+
const restoreById = async (id) => {
|
|
35
|
+
return await prisma.user.update({
|
|
36
|
+
where: { id },
|
|
37
|
+
data: { deletedAt: null },
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const findDeleted = async ({ page = 1, limit = 20 } = {}) => {
|
|
42
|
+
const skip = (page - 1) * limit;
|
|
43
|
+
const [users, total] = await Promise.all([
|
|
44
|
+
prisma.user.findMany({
|
|
45
|
+
where: { deletedAt: { not: null } },
|
|
46
|
+
select: { id: true, username: true, email: true, role: true, deletedAt: true, createdAt: true },
|
|
47
|
+
orderBy: { deletedAt: 'desc' },
|
|
48
|
+
skip,
|
|
49
|
+
take: limit,
|
|
50
|
+
}),
|
|
51
|
+
prisma.user.count({ where: { deletedAt: { not: null } } }),
|
|
52
|
+
]);
|
|
53
|
+
return { users, total, page, totalPages: Math.ceil(total / limit) };
|
|
54
|
+
};
|
|
55
|
+
<% } %>
|
|
56
|
+
|
|
57
|
+
const findAll = async ({ page = 1, limit = 20 } = {}) => {
|
|
58
|
+
const skip = (page - 1) * limit;
|
|
59
|
+
const [users, total] = await Promise.all([
|
|
60
|
+
prisma.user.findMany({
|
|
61
|
+
select: { id: true, username: true, email: true, role: true, createdAt: true, updatedAt: true },
|
|
62
|
+
orderBy: { createdAt: 'desc' },
|
|
63
|
+
skip,
|
|
64
|
+
take: limit,
|
|
65
|
+
}),
|
|
66
|
+
prisma.user.count(),
|
|
67
|
+
]);
|
|
68
|
+
return { users, total, page, totalPages: Math.ceil(total / limit) };
|
|
28
69
|
};
|
|
29
70
|
|
|
30
71
|
module.exports = {
|
|
@@ -33,4 +74,7 @@ module.exports = {
|
|
|
33
74
|
create,
|
|
34
75
|
updateById,
|
|
35
76
|
deleteById,
|
|
36
|
-
|
|
77
|
+
findAll,
|
|
78
|
+
<% if (hasSoftDelete) { %> restoreById,
|
|
79
|
+
findDeleted,
|
|
80
|
+
<% } %>};
|