@adaptivestone/framework 5.0.0-alpha.9 → 5.0.0-beta.10
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/CHANGELOG.md +123 -1
- package/Cli.js +3 -4
- package/cluster.js +0 -2
- package/commands/CreateUser.js +35 -0
- package/commands/Documentation.js +4 -0
- package/commands/DropIndex.js +14 -5
- package/commands/GenerateRandomBytes.js +21 -0
- package/commands/GetOpenApiJson.js +17 -2
- package/commands/SyncIndexes.js +1 -0
- package/commands/migration/Create.js +21 -10
- package/commands/migration/Migrate.js +1 -0
- package/config/auth.js +4 -1
- package/config/ipDetector.js +14 -0
- package/controllers/Auth.js +22 -31
- package/controllers/Home.js +1 -1
- package/eslint.config.js +68 -0
- package/folderConfig.js +0 -1
- package/helpers/files.js +9 -12
- package/helpers/logger.js +0 -1
- package/helpers/yup.js +2 -4
- package/jsconfig.json +4 -4
- package/models/Lock.js +107 -0
- package/models/User.js +26 -3
- package/modules/AbstractCommand.js +26 -2
- package/modules/AbstractController.js +19 -26
- package/modules/AbstractModel.d.ts +48 -0
- package/modules/AbstractModel.js +37 -10
- package/modules/Base.d.ts +4 -4
- package/modules/Base.js +13 -2
- package/modules/BaseCli.js +99 -11
- package/package.json +28 -25
- package/server.d.ts +9 -7
- package/server.js +45 -11
- package/services/cache/Cache.d.ts +1 -1
- package/services/cache/Cache.js +9 -7
- package/services/http/HttpServer.js +11 -16
- package/services/http/middleware/AbstractMiddleware.js +3 -3
- package/services/http/middleware/GetUserByToken.js +3 -2
- package/services/http/middleware/I18n.js +22 -23
- package/services/http/middleware/IpDetector.js +59 -0
- package/services/http/middleware/Pagination.js +7 -6
- package/services/http/middleware/RateLimiter.js +10 -4
- package/services/http/middleware/RequestLogger.js +3 -3
- package/services/http/middleware/RequestParser.js +1 -1
- package/services/messaging/email/templates/.gitkeep +0 -0
- package/services/validate/ValidateService.js +5 -5
- package/services/validate/drivers/AbstractValidator.js +2 -2
- package/services/validate/drivers/CustomValidator.js +2 -2
- package/services/validate/drivers/YupValidator.js +3 -3
- package/tests/setup.js +8 -6
- package/tests/setupVitest.js +9 -7
- package/types/ICommandArguments.d.ts +41 -0
- package/types/TFoldersConfig.d.ts +7 -4
- package/vitest.config.js +4 -3
- package/.eslintrc.cjs +0 -41
- package/commands/Generate.js +0 -14
- package/config/mail.js +0 -29
- package/controllers/Auth.test.js +0 -451
- package/controllers/Home.test.js +0 -12
- package/models/Migration.test.js +0 -20
- package/models/Sequence.test.js +0 -43
- package/models/User.test.js +0 -143
- package/modules/Modules.test.js +0 -18
- package/services/cache/Cache.test.js +0 -81
- package/services/http/middleware/Auth.test.js +0 -57
- package/services/http/middleware/Cors.test.js +0 -147
- package/services/http/middleware/GetUserByToken.test.js +0 -108
- package/services/http/middleware/I18n.test.js +0 -96
- package/services/http/middleware/PrepareAppInfo.test.js +0 -26
- package/services/http/middleware/RateLimiter.test.js +0 -233
- package/services/http/middleware/RequestParser.test.js +0 -121
- package/services/http/middleware/Role.test.js +0 -93
- package/services/messaging/email/index.js +0 -217
- package/services/messaging/email/templates/emptyTemplate/html.pug +0 -9
- package/services/messaging/email/templates/emptyTemplate/subject.pug +0 -1
- package/services/messaging/email/templates/emptyTemplate/text.pug +0 -1
- package/services/messaging/index.js +0 -3
- package/services/validate/ValidateService.test.js +0 -107
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import { setTimeout } from 'node:timers/promises';
|
|
2
|
-
import crypto from 'node:crypto';
|
|
3
|
-
import { beforeAll, afterAll, describe, it, expect } from 'vitest';
|
|
4
|
-
|
|
5
|
-
import RateLimiter from './RateLimiter.js';
|
|
6
|
-
|
|
7
|
-
let mongoRateLimiter;
|
|
8
|
-
|
|
9
|
-
describe('rate limiter methods', () => {
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
await setTimeout(20);
|
|
12
|
-
|
|
13
|
-
mongoRateLimiter = new RateLimiter(global.server.app, {
|
|
14
|
-
driver: 'mongo',
|
|
15
|
-
limiterOptions: {
|
|
16
|
-
keyPrefix: `mongo_${Date.now()}_${crypto.randomUUID()}}`,
|
|
17
|
-
},
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
afterAll(async () => {
|
|
22
|
-
// we need to wait because redis mongo ask mongo to create indexes
|
|
23
|
-
await setTimeout(200);
|
|
24
|
-
});
|
|
25
|
-
it('have description fields', async () => {
|
|
26
|
-
expect.assertions(1);
|
|
27
|
-
const middleware = new RateLimiter(global.server.app, {
|
|
28
|
-
driver: 'redis',
|
|
29
|
-
});
|
|
30
|
-
expect(middleware.constructor.description).toBeDefined();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('can create redis rateLimiter', async () => {
|
|
34
|
-
expect.assertions(1);
|
|
35
|
-
|
|
36
|
-
const redisRateLimiter = new RateLimiter(global.server.app, {
|
|
37
|
-
driver: 'redis',
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
expect(redisRateLimiter.limiter).toBeDefined();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('can not create rateLimiter with unknown driver', async () => {
|
|
44
|
-
expect.assertions(1);
|
|
45
|
-
|
|
46
|
-
const rateLimiter = new RateLimiter(global.server.app, {
|
|
47
|
-
driver: 'unknown',
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
expect(rateLimiter.limiter).toBeNull();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('generateConsumeKey works correctly', async () => {
|
|
54
|
-
expect.assertions(1);
|
|
55
|
-
|
|
56
|
-
const redisRateLimiter = new RateLimiter(global.server.app, {
|
|
57
|
-
driver: 'redis',
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const res = await redisRateLimiter.gerenateConsumeKey({
|
|
61
|
-
ip: '192.168.0.0',
|
|
62
|
-
appInfo: {
|
|
63
|
-
user: {
|
|
64
|
-
id: 'someId',
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
expect(res).toBe('192.168.0.0__someId');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('generateConsumeKey with request works correctly', async () => {
|
|
73
|
-
expect.assertions(1);
|
|
74
|
-
|
|
75
|
-
const redisRateLimiter = new RateLimiter(global.server.app, {
|
|
76
|
-
driver: 'redis',
|
|
77
|
-
consumeKeyComponents: {
|
|
78
|
-
request: ['email'],
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const res = await redisRateLimiter.gerenateConsumeKey({
|
|
83
|
-
ip: '192.168.0.0',
|
|
84
|
-
body: {
|
|
85
|
-
email: 'foo@example.com',
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
expect(res).toBe('192.168.0.0__foo@example.com');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('middleware without driver should fail', async () => {
|
|
93
|
-
expect.assertions(2);
|
|
94
|
-
const rateLimiter = new RateLimiter(global.server.app, {
|
|
95
|
-
driver: 'unknown',
|
|
96
|
-
});
|
|
97
|
-
const req = {
|
|
98
|
-
appInfo: {},
|
|
99
|
-
};
|
|
100
|
-
let status;
|
|
101
|
-
let isSend;
|
|
102
|
-
await rateLimiter.middleware(
|
|
103
|
-
req,
|
|
104
|
-
{
|
|
105
|
-
status(statusCode) {
|
|
106
|
-
status = statusCode;
|
|
107
|
-
return this;
|
|
108
|
-
},
|
|
109
|
-
json() {
|
|
110
|
-
isSend = true;
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
() => {},
|
|
114
|
-
);
|
|
115
|
-
expect(status).toBe(500);
|
|
116
|
-
expect(isSend).toBeTruthy();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const makeOneRequest = async ({ rateLimiter, driver, request }) => {
|
|
120
|
-
let realRateLimiter = rateLimiter;
|
|
121
|
-
if (!realRateLimiter) {
|
|
122
|
-
realRateLimiter = new RateLimiter(global.server.app, {
|
|
123
|
-
driver,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
const req = {
|
|
127
|
-
appInfo: {},
|
|
128
|
-
...request,
|
|
129
|
-
};
|
|
130
|
-
let status;
|
|
131
|
-
let isSend = false;
|
|
132
|
-
let isNextCalled = false;
|
|
133
|
-
await realRateLimiter.middleware(
|
|
134
|
-
req,
|
|
135
|
-
{
|
|
136
|
-
status(statusCode) {
|
|
137
|
-
status = statusCode;
|
|
138
|
-
return this;
|
|
139
|
-
},
|
|
140
|
-
json() {
|
|
141
|
-
isSend = true;
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
() => {
|
|
145
|
-
isNextCalled = true;
|
|
146
|
-
},
|
|
147
|
-
);
|
|
148
|
-
return { status, isSend, isNextCalled };
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
it('middleware should works with a mongo drivers', async () => {
|
|
152
|
-
expect.assertions(1);
|
|
153
|
-
const { isNextCalled } = await makeOneRequest({
|
|
154
|
-
rateLimiter: mongoRateLimiter,
|
|
155
|
-
request: { ip: '10.10.0.1' },
|
|
156
|
-
});
|
|
157
|
-
expect(isNextCalled).toBeTruthy();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('middleware should works with a memory drivers', async () => {
|
|
161
|
-
expect.assertions(1);
|
|
162
|
-
const { isNextCalled } = await makeOneRequest({
|
|
163
|
-
driver: 'memory',
|
|
164
|
-
request: { ip: '10.10.0.1' },
|
|
165
|
-
});
|
|
166
|
-
expect(isNextCalled).toBeTruthy();
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('middleware should works with a redis drivers', async () => {
|
|
170
|
-
expect.assertions(1);
|
|
171
|
-
const { isNextCalled } = await makeOneRequest({
|
|
172
|
-
driver: 'redis',
|
|
173
|
-
request: { ip: '10.10.0.1' },
|
|
174
|
-
});
|
|
175
|
-
expect(isNextCalled).toBeTruthy();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('middleware should rate limits for us. mongo driver', async () => {
|
|
179
|
-
expect.assertions(2);
|
|
180
|
-
|
|
181
|
-
const middlewares = Array.from({ length: 20 }, () =>
|
|
182
|
-
makeOneRequest({ rateLimiter: mongoRateLimiter }),
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
const data = await Promise.all(middlewares);
|
|
186
|
-
|
|
187
|
-
const status = data.find((obj) => obj.status === 429);
|
|
188
|
-
const isSend = data.find((obj) => obj.isSend);
|
|
189
|
-
|
|
190
|
-
expect(status.status).toBe(429);
|
|
191
|
-
expect(isSend.isSend).toBeTruthy();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('middleware should rate limits for us. memory driver', async () => {
|
|
195
|
-
expect.assertions(2);
|
|
196
|
-
|
|
197
|
-
const rateLimiter = new RateLimiter(global.server.app, {
|
|
198
|
-
driver: 'memory',
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const middlewares = Array.from({ length: 20 }, () =>
|
|
202
|
-
makeOneRequest({ rateLimiter }),
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
const data = await Promise.all(middlewares);
|
|
206
|
-
|
|
207
|
-
const status = data.find((obj) => obj.status === 429);
|
|
208
|
-
const isSend = data.find((obj) => obj.isSend);
|
|
209
|
-
|
|
210
|
-
expect(status.status).toBe(429);
|
|
211
|
-
expect(isSend.isSend).toBeTruthy();
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('middleware should rate limits for us. redis driver', async () => {
|
|
215
|
-
expect.assertions(2);
|
|
216
|
-
|
|
217
|
-
const rateLimiter = new RateLimiter(global.server.app, {
|
|
218
|
-
driver: 'redis',
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const middlewares = Array.from({ length: 20 }, () =>
|
|
222
|
-
makeOneRequest({ rateLimiter }),
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
const data = await Promise.all(middlewares);
|
|
226
|
-
|
|
227
|
-
const status = data.find((obj) => obj.status === 429);
|
|
228
|
-
const isSend = data.find((obj) => obj.isSend);
|
|
229
|
-
|
|
230
|
-
expect(status.status).toBe(429);
|
|
231
|
-
expect(isSend.isSend).toBeTruthy();
|
|
232
|
-
});
|
|
233
|
-
});
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'node:http';
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { PersistentFile } from 'formidable';
|
|
4
|
-
|
|
5
|
-
import RequestParser from './RequestParser.js';
|
|
6
|
-
|
|
7
|
-
describe('reqest parser limiter methods', () => {
|
|
8
|
-
it('have description fields', async () => {
|
|
9
|
-
expect.assertions(1);
|
|
10
|
-
const middleware = new RequestParser(global.server.app);
|
|
11
|
-
expect(middleware.constructor.description).toBeDefined();
|
|
12
|
-
});
|
|
13
|
-
it('middleware that works', async () => {
|
|
14
|
-
expect.assertions(4);
|
|
15
|
-
|
|
16
|
-
await new Promise((done) => {
|
|
17
|
-
// from https://github.com/node-formidable/formidable/blob/master/test-node/standalone/promise.test.js
|
|
18
|
-
|
|
19
|
-
const server = createServer(async (req, res) => {
|
|
20
|
-
req.appInfo = {};
|
|
21
|
-
const middleware = new RequestParser(global.server.app);
|
|
22
|
-
middleware.middleware(req, {}, (err) => {
|
|
23
|
-
expect(err).toBeUndefined();
|
|
24
|
-
expect(req.body.title).toBeDefined();
|
|
25
|
-
expect(req.body.multipleFiles).toBeDefined();
|
|
26
|
-
expect(
|
|
27
|
-
req.body.multipleFiles[0] instanceof PersistentFile,
|
|
28
|
-
).toBeTruthy();
|
|
29
|
-
|
|
30
|
-
res.writeHead(200);
|
|
31
|
-
res.end('ok');
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
server.listen(null, async () => {
|
|
35
|
-
const chosenPort = server.address().port;
|
|
36
|
-
const body = `----13068458571765726332503797717\r
|
|
37
|
-
Content-Disposition: form-data; name="title"\r
|
|
38
|
-
\r
|
|
39
|
-
a\r
|
|
40
|
-
----13068458571765726332503797717\r
|
|
41
|
-
Content-Disposition: form-data; name="multipleFiles"; filename="x.txt"\r
|
|
42
|
-
Content-Type: application/x-javascript\r
|
|
43
|
-
\r
|
|
44
|
-
\r
|
|
45
|
-
\r
|
|
46
|
-
a\r
|
|
47
|
-
b\r
|
|
48
|
-
c\r
|
|
49
|
-
d\r
|
|
50
|
-
\r
|
|
51
|
-
----13068458571765726332503797717--\r
|
|
52
|
-
`;
|
|
53
|
-
await fetch(String(new URL(`http:localhost:${chosenPort}/`)), {
|
|
54
|
-
method: 'POST',
|
|
55
|
-
|
|
56
|
-
headers: {
|
|
57
|
-
'Content-Length': body.length,
|
|
58
|
-
Host: `localhost:${chosenPort}`,
|
|
59
|
-
'Content-Type':
|
|
60
|
-
'multipart/form-data; boundary=--13068458571765726332503797717',
|
|
61
|
-
},
|
|
62
|
-
body,
|
|
63
|
-
}).catch((err) => {
|
|
64
|
-
console.error(err);
|
|
65
|
-
done(err);
|
|
66
|
-
});
|
|
67
|
-
server.close(() => {
|
|
68
|
-
done();
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
it('middleware with a problem', async () => {
|
|
74
|
-
expect.assertions(1);
|
|
75
|
-
|
|
76
|
-
await new Promise((done) => {
|
|
77
|
-
// from https://github.com/node-formidable/formidable/blob/master/test-node/standalone/promise.test.js
|
|
78
|
-
|
|
79
|
-
const server = createServer(async (req, res) => {
|
|
80
|
-
req.appInfo = {};
|
|
81
|
-
const middleware = new RequestParser(global.server.app);
|
|
82
|
-
let status;
|
|
83
|
-
|
|
84
|
-
const resp = {
|
|
85
|
-
status: (code) => {
|
|
86
|
-
status = code;
|
|
87
|
-
return resp;
|
|
88
|
-
},
|
|
89
|
-
json: () => resp,
|
|
90
|
-
};
|
|
91
|
-
await middleware.middleware(req, resp, () => {});
|
|
92
|
-
expect(status).toBe(400);
|
|
93
|
-
// expect(err).toBeDefined();
|
|
94
|
-
|
|
95
|
-
res.writeHead(200);
|
|
96
|
-
res.end('ok');
|
|
97
|
-
});
|
|
98
|
-
server.listen(null, async () => {
|
|
99
|
-
const chosenPort = server.address().port;
|
|
100
|
-
const body = 'someBadBody';
|
|
101
|
-
|
|
102
|
-
await fetch(String(new URL(`http:localhost:${chosenPort}/`)), {
|
|
103
|
-
method: 'POST',
|
|
104
|
-
|
|
105
|
-
headers: {
|
|
106
|
-
'Content-Length': body.length,
|
|
107
|
-
Host: `localhost:${chosenPort}`,
|
|
108
|
-
'Content-Type': 'badContentType',
|
|
109
|
-
},
|
|
110
|
-
body,
|
|
111
|
-
}).catch((err) => {
|
|
112
|
-
console.error(err);
|
|
113
|
-
done(err);
|
|
114
|
-
});
|
|
115
|
-
server.close(() => {
|
|
116
|
-
done();
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
});
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import Role from './Role.js';
|
|
3
|
-
|
|
4
|
-
describe('role middleware methods', () => {
|
|
5
|
-
it('have description fields', async () => {
|
|
6
|
-
expect.assertions(1);
|
|
7
|
-
const middleware = new Role(global.server.app);
|
|
8
|
-
expect(middleware.constructor.description).toBeDefined();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('middleware pass when user presented with a right role', async () => {
|
|
12
|
-
expect.assertions(1);
|
|
13
|
-
let isCalled = false;
|
|
14
|
-
const nextFunction = () => {
|
|
15
|
-
isCalled = true;
|
|
16
|
-
};
|
|
17
|
-
const req = {
|
|
18
|
-
appInfo: {
|
|
19
|
-
user: {
|
|
20
|
-
roles: ['role1', 'role2'],
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
const middleware = new Role(global.server.app, {
|
|
25
|
-
roles: ['admin', 'role1'],
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
await middleware.middleware(req, {}, nextFunction);
|
|
29
|
-
expect(isCalled).toBeTruthy();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('middleware NOT pass when user NOT presented', async () => {
|
|
33
|
-
expect.assertions(3);
|
|
34
|
-
let isCalled = false;
|
|
35
|
-
let status;
|
|
36
|
-
let isSend;
|
|
37
|
-
const nextFunction = () => {
|
|
38
|
-
isCalled = true;
|
|
39
|
-
};
|
|
40
|
-
const req = {
|
|
41
|
-
appInfo: {}, // no user
|
|
42
|
-
};
|
|
43
|
-
const middleware = new Role(global.server.app);
|
|
44
|
-
await middleware.middleware(
|
|
45
|
-
req,
|
|
46
|
-
{
|
|
47
|
-
status(statusCode) {
|
|
48
|
-
status = statusCode;
|
|
49
|
-
return this;
|
|
50
|
-
},
|
|
51
|
-
json() {
|
|
52
|
-
isSend = true;
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
nextFunction,
|
|
56
|
-
);
|
|
57
|
-
expect(isCalled).toBeFalsy();
|
|
58
|
-
expect(status).toBe(401);
|
|
59
|
-
expect(isSend).toBeTruthy();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('middleware NOT pass when user have a wrong role', async () => {
|
|
63
|
-
expect.assertions(3);
|
|
64
|
-
let isCalled = false;
|
|
65
|
-
let status;
|
|
66
|
-
let isSend;
|
|
67
|
-
const nextFunction = () => {
|
|
68
|
-
isCalled = true;
|
|
69
|
-
};
|
|
70
|
-
const req = {
|
|
71
|
-
appInfo: {
|
|
72
|
-
user: { roles: ['role1', 'role2'] },
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
const middleware = new Role(global.server.app, { roles: ['admin'] });
|
|
76
|
-
await middleware.middleware(
|
|
77
|
-
req,
|
|
78
|
-
{
|
|
79
|
-
status(statusCode) {
|
|
80
|
-
status = statusCode;
|
|
81
|
-
return this;
|
|
82
|
-
},
|
|
83
|
-
json() {
|
|
84
|
-
isSend = true;
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
nextFunction,
|
|
88
|
-
);
|
|
89
|
-
expect(isCalled).toBeFalsy();
|
|
90
|
-
expect(status).toBe(403);
|
|
91
|
-
expect(isSend).toBeTruthy();
|
|
92
|
-
});
|
|
93
|
-
});
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import * as url from 'node:url';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
|
-
import nodemailer from 'nodemailer';
|
|
6
|
-
import sendMail from 'nodemailer-sendmail-transport';
|
|
7
|
-
import stub from 'nodemailer-stub-transport';
|
|
8
|
-
import pug from 'pug';
|
|
9
|
-
import juice from 'juice';
|
|
10
|
-
import { convert } from 'html-to-text';
|
|
11
|
-
import Base from '../../../modules/Base.js';
|
|
12
|
-
|
|
13
|
-
const mailTransports = {
|
|
14
|
-
sendMail,
|
|
15
|
-
stub,
|
|
16
|
-
smtp: (data) => data,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
class Mail extends Base {
|
|
20
|
-
/**
|
|
21
|
-
* Construct mail class
|
|
22
|
-
* @param {object} app
|
|
23
|
-
* @param {string} template template name
|
|
24
|
-
* @param {object} [templateData={}] data to render in template. Object with value that available inside template
|
|
25
|
-
* @param {object} [i18n] data to render in template
|
|
26
|
-
*/
|
|
27
|
-
constructor(app, template, templateData = {}, i18n = null) {
|
|
28
|
-
super(app);
|
|
29
|
-
const dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
|
30
|
-
if (!path.isAbsolute(template)) {
|
|
31
|
-
if (
|
|
32
|
-
fs.existsSync(
|
|
33
|
-
`${this.app.foldersConfig.emails}/${path.basename(template)}`,
|
|
34
|
-
)
|
|
35
|
-
) {
|
|
36
|
-
this.template = `${this.app.foldersConfig.emails}/${path.basename(
|
|
37
|
-
template,
|
|
38
|
-
)}`;
|
|
39
|
-
} else if (
|
|
40
|
-
fs.existsSync(
|
|
41
|
-
path.join(dirname, `/templates/${path.basename(template)}`),
|
|
42
|
-
)
|
|
43
|
-
) {
|
|
44
|
-
this.template = path.join(
|
|
45
|
-
dirname,
|
|
46
|
-
`/templates/${path.basename(template)}`,
|
|
47
|
-
);
|
|
48
|
-
} else {
|
|
49
|
-
this.template = path.join(dirname, `/templates/emptyTemplate`);
|
|
50
|
-
this.logger.error(
|
|
51
|
-
`Template '${template}' not found. Using 'emptyTemplate' as a fallback`,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
this.templateData = templateData;
|
|
56
|
-
this.i18n = i18n ?? {
|
|
57
|
-
t: (str) => str,
|
|
58
|
-
locale: 'en', // todo change it to config
|
|
59
|
-
};
|
|
60
|
-
this.locale = this.i18n?.language;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Render template
|
|
65
|
-
* @param {object} type and fullpath
|
|
66
|
-
* @param {object} templateData
|
|
67
|
-
* @returns string
|
|
68
|
-
*/
|
|
69
|
-
// eslint-disable-next-line class-methods-use-this
|
|
70
|
-
async #renderTemplateFile({ type, fullPath } = {}, templateData = {}) {
|
|
71
|
-
if (!type) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
switch (type) {
|
|
76
|
-
case 'html':
|
|
77
|
-
case 'text':
|
|
78
|
-
case 'css':
|
|
79
|
-
return fs.promises.readFile(fullPath, { encoding: 'utf8' });
|
|
80
|
-
case 'pug': {
|
|
81
|
-
const compiledFunction = pug.compileFile(fullPath);
|
|
82
|
-
return compiledFunction(templateData);
|
|
83
|
-
}
|
|
84
|
-
default:
|
|
85
|
-
throw new Error(`Template type ${type} is not supported`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Render template
|
|
91
|
-
* @return {Promise}
|
|
92
|
-
*/
|
|
93
|
-
async renderTemplate() {
|
|
94
|
-
const files = await fs.promises.readdir(this.template);
|
|
95
|
-
const templates = {};
|
|
96
|
-
for (const file of files) {
|
|
97
|
-
const [name, extension] = file.split('.');
|
|
98
|
-
templates[name] = {
|
|
99
|
-
type: extension,
|
|
100
|
-
fullPath: path.join(this.template, file),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!templates.html || !templates.subject) {
|
|
105
|
-
throw new Error(
|
|
106
|
-
'Template HTML and Subject must be provided. Please follow documentation for details https://framework.adaptivestone.com/docs/email',
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
const mailConfig = this.app.getConfig('mail');
|
|
110
|
-
|
|
111
|
-
const templateDataToRender = {
|
|
112
|
-
locale: this.locale,
|
|
113
|
-
t: this.i18n.t.bind(this.i18n),
|
|
114
|
-
...mailConfig.globalVariablesToTemplates,
|
|
115
|
-
...this.templateData,
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const [htmlRendered, subjectRendered, textRendered, extraCss] =
|
|
119
|
-
await Promise.all([
|
|
120
|
-
this.#renderTemplateFile(templates.html, templateDataToRender),
|
|
121
|
-
this.#renderTemplateFile(templates.subject, templateDataToRender),
|
|
122
|
-
this.#renderTemplateFile(templates.text, templateDataToRender),
|
|
123
|
-
this.#renderTemplateFile(templates.style),
|
|
124
|
-
]);
|
|
125
|
-
|
|
126
|
-
juice.tableElements = ['TABLE'];
|
|
127
|
-
|
|
128
|
-
const juiceResourcesAsync = promisify(juice.juiceResources);
|
|
129
|
-
|
|
130
|
-
const inlinedHTML = await juiceResourcesAsync(htmlRendered, {
|
|
131
|
-
preserveImportant: true,
|
|
132
|
-
webResources: mailConfig.webResources,
|
|
133
|
-
extraCss,
|
|
134
|
-
});
|
|
135
|
-
return {
|
|
136
|
-
htmlRaw: htmlRendered,
|
|
137
|
-
subject: subjectRendered,
|
|
138
|
-
text: textRendered,
|
|
139
|
-
inlinedHTML,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Send email
|
|
145
|
-
* @param {string} to email send to
|
|
146
|
-
* @param {string} [from = mailConfig.from]
|
|
147
|
-
* @param {object} [aditionalNodemailerOptions = {}] additional option to nodemailer
|
|
148
|
-
* @return {Promise}
|
|
149
|
-
*/
|
|
150
|
-
async send(to, from = null, aditionalNodemailerOptions = {}) {
|
|
151
|
-
const { subject, text, inlinedHTML } = await this.renderTemplate();
|
|
152
|
-
|
|
153
|
-
return this.constructor.sendRaw(
|
|
154
|
-
this.app,
|
|
155
|
-
to,
|
|
156
|
-
subject,
|
|
157
|
-
inlinedHTML,
|
|
158
|
-
text,
|
|
159
|
-
from,
|
|
160
|
-
aditionalNodemailerOptions,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Send provided text (html) to email. Low level function. All data should be prepared before sending (like inline styles)
|
|
166
|
-
* @param {import('../../../server.js').default['app']} app application
|
|
167
|
-
* @param {string} to send to
|
|
168
|
-
* @param {string} subject email topic
|
|
169
|
-
* @param {string} html hmlt body of emain
|
|
170
|
-
* @param {string} [text] if not provided will be generated from html string
|
|
171
|
-
* @param {string} [from = mailConfig.from] from. If not provided will be grabbed from config
|
|
172
|
-
* @param {object} [additionalNodeMailerOption = {}] any otipns to pass to nodemailer https://nodemailer.com/message/
|
|
173
|
-
*/
|
|
174
|
-
static async sendRaw(
|
|
175
|
-
app,
|
|
176
|
-
to,
|
|
177
|
-
subject,
|
|
178
|
-
html,
|
|
179
|
-
text = null,
|
|
180
|
-
from = null,
|
|
181
|
-
additionalNodeMailerOption = {},
|
|
182
|
-
) {
|
|
183
|
-
if (!app || !to || !subject || !html) {
|
|
184
|
-
throw new Error('App, to, subject and html is required fields.');
|
|
185
|
-
}
|
|
186
|
-
const mailConfig = app.getConfig('mail');
|
|
187
|
-
if (!from) {
|
|
188
|
-
// eslint-disable-next-line no-param-reassign
|
|
189
|
-
from = mailConfig.from;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (!text) {
|
|
193
|
-
// eslint-disable-next-line no-param-reassign
|
|
194
|
-
text = convert(html, {
|
|
195
|
-
selectors: [{ selector: 'img', format: 'skip' }],
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
const transportConfig = mailConfig.transports[mailConfig.transport];
|
|
199
|
-
const transport = mailTransports[mailConfig.transport];
|
|
200
|
-
const transporter = nodemailer.createTransport(transport(transportConfig));
|
|
201
|
-
|
|
202
|
-
return transporter.sendMail({
|
|
203
|
-
from,
|
|
204
|
-
to,
|
|
205
|
-
subject,
|
|
206
|
-
text,
|
|
207
|
-
html,
|
|
208
|
-
...additionalNodeMailerOption,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
static get loggerGroup() {
|
|
213
|
-
return 'email_';
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export default Mail;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
= `New message`
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
= `Good day`
|