mgc 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/bin/generate.js +24 -0
- package/package.json +33 -0
- package/readme.md +4 -0
- package/services/filecopy.service.js +62 -0
- package/services/openai.service.js +20 -0
- package/templates/express/email/email.interface.ts +13 -0
- package/templates/express/email/email.service.ts +29 -0
- package/templates/express/email/templates/axp-user-notifications.hbs +52 -0
- package/templates/express/email/templates/forgot-password.hbs +57 -0
- package/templates/express/email/templates/mfa.hbs +64 -0
- package/templates/express/email/templates/registration-invite.hbs +60 -0
- package/templates/express/email/templates/welcome.hbs +16 -0
- package/templates/express/s3/S3.helper.ts +15 -0
- package/templates/express/s3/s3.interface.ts +18 -0
- package/templates/express/s3/s3.service.ts +121 -0
package/bin/generate.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
|
|
6
|
+
import { generateProject } from "../services/filecopy.service.js";
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.command("gen <modulePath>")
|
|
10
|
+
.description("Generate a module project")
|
|
11
|
+
.option(
|
|
12
|
+
"-p, --path <path>",
|
|
13
|
+
"The path where the component will get generated in."
|
|
14
|
+
)
|
|
15
|
+
.action((modulePath, options) => {
|
|
16
|
+
// options
|
|
17
|
+
const externalPath = options?.path;
|
|
18
|
+
|
|
19
|
+
const currentDir = path.join(process.cwd(), externalPath || "");
|
|
20
|
+
|
|
21
|
+
generateProject(modulePath, currentDir);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mgc",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A cli based tool for generating your saved modules",
|
|
5
|
+
"author": "Admond Tamang",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "bin/generate",
|
|
8
|
+
"bin": {
|
|
9
|
+
"cli": "bin/generate.js"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"cli",
|
|
16
|
+
"build-tools",
|
|
17
|
+
"module generate cli"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=10.x",
|
|
21
|
+
"npm": ">= 6.x"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"chalk": "^5.3.0",
|
|
29
|
+
"commander": "^11.0.0",
|
|
30
|
+
"dotenv": "^16.3.1",
|
|
31
|
+
"openai": "^3.3.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
export const copyFileSync = (source, target) => {
|
|
6
|
+
let targetFile = target;
|
|
7
|
+
|
|
8
|
+
// If target is a directory, a new file with the same name will be created
|
|
9
|
+
if (fs.existsSync(target)) {
|
|
10
|
+
if (fs.lstatSync(target).isDirectory()) {
|
|
11
|
+
targetFile = path.join(target, path.basename(source));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fs.writeFileSync(targetFile, fs.readFileSync(source));
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const copyFolderSync = (source, target) => {
|
|
19
|
+
if (!fs.existsSync(target)) {
|
|
20
|
+
fs.mkdirSync(target);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fs.readdirSync(source).forEach((file) => {
|
|
24
|
+
const sourcePath = path.join(source, file);
|
|
25
|
+
const targetPath = path.join(target, file);
|
|
26
|
+
|
|
27
|
+
if (fs.lstatSync(sourcePath).isDirectory()) {
|
|
28
|
+
copyFolderSync(sourcePath, targetPath);
|
|
29
|
+
} else {
|
|
30
|
+
copyFileSync(sourcePath, targetPath);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const getCurrentFolder = (name) => {
|
|
36
|
+
const result = name.split("/");
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(result) && result.length > 0) return result.pop();
|
|
39
|
+
else return name;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const generateProject = (modulePath, toGeneratePath) => {
|
|
43
|
+
// to get __dirname
|
|
44
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
45
|
+
const __dirname = path.dirname(__filename);
|
|
46
|
+
|
|
47
|
+
// copy template files and folders
|
|
48
|
+
const templatePath = path.join(__dirname, "../templates/" + modulePath);
|
|
49
|
+
|
|
50
|
+
// generate module folder
|
|
51
|
+
const moduleName = getCurrentFolder(modulePath);
|
|
52
|
+
const newPathToGenerate = toGeneratePath + "/" + moduleName;
|
|
53
|
+
|
|
54
|
+
// create module name if not created
|
|
55
|
+
if (fs.existsSync(newPathToGenerate)) throw new Error("Module already exist");
|
|
56
|
+
fs.mkdirSync(newPathToGenerate, { recursive: true });
|
|
57
|
+
|
|
58
|
+
// copy module to your dir
|
|
59
|
+
copyFolderSync(templatePath, newPathToGenerate);
|
|
60
|
+
|
|
61
|
+
console.log("Module generated successfully!");
|
|
62
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Configuration, OpenAIApi } from "openai";
|
|
2
|
+
|
|
3
|
+
export async function aiComponentGenerator(componentTemplate, prompt) {
|
|
4
|
+
const configuration = new Configuration({
|
|
5
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
6
|
+
});
|
|
7
|
+
const openAiApi = new OpenAIApi(configuration);
|
|
8
|
+
|
|
9
|
+
const generatedComponent = await openAiApi.createCompletion({
|
|
10
|
+
model: "text-davinci-003",
|
|
11
|
+
prompt: `Create a React component using this template "${componentTemplate}", but make the adjustments needed with these instructions as follows "${prompt}"`,
|
|
12
|
+
temperature: 0.7,
|
|
13
|
+
max_tokens: 2000,
|
|
14
|
+
top_p: 1.0,
|
|
15
|
+
frequency_penalty: 0.0,
|
|
16
|
+
presence_penalty: 1,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return generatedComponent.data.choices[0].text;
|
|
20
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Attachment } from 'nodemailer/lib/mailer';
|
|
2
|
+
|
|
3
|
+
export interface IEmailInterface {
|
|
4
|
+
from?: string;
|
|
5
|
+
to: string | Array<string>;
|
|
6
|
+
subject: string;
|
|
7
|
+
context?: Record<string, any>;
|
|
8
|
+
attachments?: Attachment[];
|
|
9
|
+
template?: EmailTemplate;
|
|
10
|
+
html?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type EmailTemplate = 'welcome' | 'mfa' | 'registration-invite' | 'forgot-password' | 'axp-user-notifications';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import handlebars from 'handlebars';
|
|
4
|
+
|
|
5
|
+
import { config } from '../../../config';
|
|
6
|
+
import { logger } from '../../../lib/logger';
|
|
7
|
+
import { nodeMailerTransporter } from '../../../lib/nodemailer';
|
|
8
|
+
|
|
9
|
+
import { IEmailInterface, EmailTemplate } from './email.interface';
|
|
10
|
+
|
|
11
|
+
export const sendMail = (emailData: IEmailInterface) => {
|
|
12
|
+
emailData.from = emailData.from || config.email.from;
|
|
13
|
+
|
|
14
|
+
// handlebars config
|
|
15
|
+
if (emailData.template) {
|
|
16
|
+
const emailTemplate = fs.readFileSync(path.join(__dirname, `./templates/${emailData.template}.hbs`), 'utf8');
|
|
17
|
+
const template = handlebars.compile(emailTemplate);
|
|
18
|
+
emailData.template = emailTemplate as EmailTemplate;
|
|
19
|
+
emailData.html = template(emailData.context);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
nodeMailerTransporter.sendMail(emailData, (err, info) => {
|
|
23
|
+
if (err) {
|
|
24
|
+
throw err;
|
|
25
|
+
} else {
|
|
26
|
+
logger.info(`[EmailService] - Email sent successfully with response id ${info.response}`);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<html lang='en'>
|
|
2
|
+
|
|
3
|
+
<head>
|
|
4
|
+
<title>{{title}}</title>
|
|
5
|
+
|
|
6
|
+
<meta charset='utf-8' />
|
|
7
|
+
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
.body { padding-bottom: 82px; font-size: 16px; background-color: rgba(0, 0, 0, 0.04); font-family: Arial,
|
|
11
|
+
sans-serif } .header { background-color: #042940; padding: 23px 48px; border-radius: 16px 16px 0px 0px; } .main {
|
|
12
|
+
max-width: 635px; margin: 0 auto; padding: 68px 0 51px 0; color: rgba(0, 0, 0, 0.74); line-height: 140%;
|
|
13
|
+
letter-spacing: 0.16px; border-radius: 16px; } .content { background-color: #fff; padding: 51px 48px 87px 48px;
|
|
14
|
+
border-radius: 0px 0px 16px 16px; } .content-main { margin: 24px 0 68px 0; } .content-main p { margin: 20px 0 0 0;
|
|
15
|
+
} .content-main p:first { margin: 0; } .mfa-code { color: #000; font-style: normal; font-weight: 500; } .greeting
|
|
16
|
+
{ color: #321D36; font-style: normal; font-weight: 500; } .copyright { color: rgba(17, 18, 24, 0.54); text-align:
|
|
17
|
+
center; font-size: 13px; font-family: Inter; line-height: 130%; } .salutation { font-family: Verdana, Geneva,
|
|
18
|
+
sans-serif; } .action-link { font-size: 14px; font-weight: 500; color: #004C95; line-height: 140%; letter-spacing:
|
|
19
|
+
0.42px; text-decoration: none; } .sub-text { color: rgba(0, 0, 0, 0.54); font-size: 14px; }
|
|
20
|
+
</style>
|
|
21
|
+
</head>
|
|
22
|
+
|
|
23
|
+
<body>
|
|
24
|
+
<div class='body'>
|
|
25
|
+
<div class='main'>
|
|
26
|
+
<div class='header'>
|
|
27
|
+
<img src='https://axp-data-dev.s3.us-east-2.amazonaws.com/public-assets/logo.png' alt='AXP Logo' />
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class='content'>
|
|
31
|
+
<p class='greeting salutation'>Hello,</p>
|
|
32
|
+
|
|
33
|
+
<div class='content-main'>
|
|
34
|
+
<p>{{{message}}} Click the link below to view in detail, or perform actions:</p>
|
|
35
|
+
|
|
36
|
+
<p><a href='{{dashboardUrl}}' class='action-link'>{{dashboardUrl}}</a></p>
|
|
37
|
+
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<p>
|
|
41
|
+
<div class='greeting'>Regards,</div>
|
|
42
|
+
AX Partners Digital Technology Team
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class='copyright'>© AX Partner Corporation 2023</div>
|
|
49
|
+
</div>
|
|
50
|
+
</body>
|
|
51
|
+
|
|
52
|
+
</html>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<html lang='en'>
|
|
2
|
+
|
|
3
|
+
<head>
|
|
4
|
+
<title>Password Reset Request</title>
|
|
5
|
+
|
|
6
|
+
<meta charset='utf-8' />
|
|
7
|
+
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
.body { padding-bottom: 82px; font-size: 16px; background-color: rgba(0, 0, 0, 0.04); font-family: Arial,
|
|
11
|
+
sans-serif } .header { background-color: #042940; padding: 23px 48px; border-radius: 16px 16px 0px 0px; } .main {
|
|
12
|
+
max-width: 635px; margin: 0 auto; padding: 68px 0 51px 0; color: rgba(0, 0, 0, 0.74); line-height: 140%;
|
|
13
|
+
letter-spacing: 0.16px; border-radius: 16px; } .content { background-color: #fff; padding: 51px 48px 87px 48px;
|
|
14
|
+
border-radius: 0px 0px 16px 16px; } .content-main { margin: 24px 0 68px 0; } .content-main p { margin: 20px 0 0 0;
|
|
15
|
+
} .content-main p:first { margin: 0; } .mfa-code { color: #000; font-style: normal; font-weight: 500; } .greeting
|
|
16
|
+
{ color: #321D36; font-style: normal; font-weight: 500; } .copyright { color: rgba(17, 18, 24, 0.54); text-align:
|
|
17
|
+
center; font-size: 13px; font-family: Inter; line-height: 130%; } .salutation { font-family: Verdana, Geneva,
|
|
18
|
+
sans-serif; } .action-link { font-size: 14px; font-weight: 500; color: #004C95; line-height: 140%; letter-spacing:
|
|
19
|
+
0.42px; text-decoration: none; } .sub-text { color: rgba(0, 0, 0, 0.54); font-size: 14px; }
|
|
20
|
+
</style>
|
|
21
|
+
</head>
|
|
22
|
+
|
|
23
|
+
<body>
|
|
24
|
+
<div class='body'>
|
|
25
|
+
<div class='main'>
|
|
26
|
+
<div class='header'>
|
|
27
|
+
<img src='https://axp-data-dev.s3.us-east-2.amazonaws.com/public-assets/logo.png' alt='AXP Logo' />
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class='content'>
|
|
31
|
+
<p class='greeting salutation'>Dear {{firstName}},</p>
|
|
32
|
+
|
|
33
|
+
<div class='content-main'>
|
|
34
|
+
<p>You recently requested a password reset for your AX Partner account. Click the link below to set a new
|
|
35
|
+
password:</p>
|
|
36
|
+
|
|
37
|
+
<p><a href='{{passwordResetUrl}}' class='action-link'>{{passwordResetUrl}}</a></p>
|
|
38
|
+
<div class='sub-text'>* This link is valid for a week</div>
|
|
39
|
+
|
|
40
|
+
<p>If you didn't make this request, please disregard this email. Your account remains secure.</p>
|
|
41
|
+
|
|
42
|
+
<p>If you need further assistance, please contact our support team at [support email/phone number].</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<p>
|
|
46
|
+
<div class='greeting'>Regards,</div>
|
|
47
|
+
AX Partners Digital Technology Team
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class='copyright'>© AX Partner Corporation 2023</div>
|
|
54
|
+
</div>
|
|
55
|
+
</body>
|
|
56
|
+
|
|
57
|
+
</html>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<html lang='en'>
|
|
2
|
+
|
|
3
|
+
<head>
|
|
4
|
+
<title>Multi Factor Authentication Code</title>
|
|
5
|
+
|
|
6
|
+
<meta charset='utf-8' />
|
|
7
|
+
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
.body { padding-bottom: 82px; font-size: 16px; background-color: rgba(0, 0, 0, 0.04); font-family: Arial,
|
|
11
|
+
sans-serif } .header { background-color: #042940; padding: 23px 48px; border-radius: 16px 16px 0px 0px; } .main {
|
|
12
|
+
max-width: 635px; margin: 0 auto; padding: 68px 0 51px 0; color: rgba(0, 0, 0, 0.74); line-height: 140%;
|
|
13
|
+
letter-spacing: 0.16px; border-radius: 16px; } .content { background-color: #fff; padding: 51px 48px 87px 48px;
|
|
14
|
+
border-radius: 0px 0px 16px 16px; } .content-main { margin: 24px 0 68px 0; } .content-main p { margin: 20px 0 0 0;
|
|
15
|
+
} .content-main p:first { margin: 0; } .mfa-code { color: #000; font-style: normal; font-weight: 500; } .greeting
|
|
16
|
+
{ color: #321D36; font-style: normal; font-weight: 500; } .copyright { color: rgba(17, 18, 24, 0.54); text-align:
|
|
17
|
+
center; font-size: 13px; font-family: Inter; line-height: 130%; } .salutation { font-family: Verdana, Geneva,
|
|
18
|
+
sans-serif; }
|
|
19
|
+
</style>
|
|
20
|
+
</head>
|
|
21
|
+
|
|
22
|
+
<body>
|
|
23
|
+
<div class='body'>
|
|
24
|
+
<div class='main'>
|
|
25
|
+
<div class='header'>
|
|
26
|
+
<img src='https://axp-data-dev.s3.us-east-2.amazonaws.com/public-assets/logo.png' alt='AXP Logo' />
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class='content'>
|
|
30
|
+
<p class='greeting salutation'>Dear {{firstName}},</p>
|
|
31
|
+
|
|
32
|
+
<div class='content-main'>
|
|
33
|
+
<p>To enhance the security of your AX Partner account, we have implemented Multi-Factor Authentication
|
|
34
|
+
(MFA). As part of this security measure, you are required to provide a verification code during the login
|
|
35
|
+
process. Please find your MFA code below:</p>
|
|
36
|
+
|
|
37
|
+
<p class='mfa-code'>MFA Code: {{mfaCode}}</p>
|
|
38
|
+
|
|
39
|
+
<p>Please follow the steps below to complete the login process:
|
|
40
|
+
<ol>
|
|
41
|
+
<li>Visit the AX Partner login page at {{loginUrl}}.</li>
|
|
42
|
+
<li>Enter your username and password as usual.</li>
|
|
43
|
+
<li>When prompted, enter the provided MFA code {{mfaCode}} in the designated field.</li>
|
|
44
|
+
</ol>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
<p>Please note that the MFA code is time-sensitive and will expire after a certain period. If the code
|
|
48
|
+
expires, you can request a new one by clicking on the "Resend Code" option on the login page.</p>
|
|
49
|
+
<p>Thank you for your cooperation in keeping your AX Partner account secure.</p>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<p>
|
|
53
|
+
<div class='greeting'>Regards,</div>
|
|
54
|
+
AX Partners Digital Technology Team
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class='copyright'>© AX Partner Corporation 2023</div>
|
|
61
|
+
</div>
|
|
62
|
+
</body>
|
|
63
|
+
|
|
64
|
+
</html>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<html lang='en'>
|
|
2
|
+
|
|
3
|
+
<head>
|
|
4
|
+
<title>Register Your Account</title>
|
|
5
|
+
|
|
6
|
+
<meta charset='utf-8' />
|
|
7
|
+
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
.body { padding-bottom: 82px; font-size: 16px; background-color: rgba(0, 0, 0, 0.04); font-family: Arial,
|
|
11
|
+
sans-serif } .header { background-color: #042940; padding: 23px 48px; border-radius: 16px 16px 0px 0px; } .main {
|
|
12
|
+
max-width: 635px; margin: 0 auto; padding: 68px 0 51px 0; color: rgba(0, 0, 0, 0.74); line-height: 140%;
|
|
13
|
+
letter-spacing: 0.16px; border-radius: 16px; } .content { background-color: #fff; padding: 51px 48px 87px 48px;
|
|
14
|
+
border-radius: 0px 0px 16px 16px; } .content-main { margin: 24px 0 68px 0; } .content-main p { margin: 20px 0 0 0;
|
|
15
|
+
} .content-main p:first { margin: 0; } .mfa-code { color: #000; font-style: normal; font-weight: 500; } .greeting
|
|
16
|
+
{ color: #321D36; font-style: normal; font-weight: 500; } .copyright { color: rgba(17, 18, 24, 0.54); text-align:
|
|
17
|
+
center; font-size: 13px; font-family: Inter; line-height: 130%; } .salutation { font-family: Verdana, Geneva,
|
|
18
|
+
sans-serif; } .action-link { font-size: 14px; font-weight: 500; color: #004C95; line-height: 140%; letter-spacing:
|
|
19
|
+
0.42px; text-decoration: none; } .sub-text { color: rgba(0, 0, 0, 0.54); font-size: 14px; }
|
|
20
|
+
</style>
|
|
21
|
+
</head>
|
|
22
|
+
|
|
23
|
+
<body>
|
|
24
|
+
<div class='body'>
|
|
25
|
+
<div class='main'>
|
|
26
|
+
<div class='header'>
|
|
27
|
+
<img src='https://axp-data-dev.s3.us-east-2.amazonaws.com/public-assets/logo.png' alt='AXP Logo' />
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class='content'>
|
|
31
|
+
<p class='greeting salutation'>Hello,</p>
|
|
32
|
+
|
|
33
|
+
<div class='content-main'>
|
|
34
|
+
<p>Thank you for choosing AX Partner as your trusted platform for [specific purpose]. We are excited to have
|
|
35
|
+
you join our community of.</p>
|
|
36
|
+
|
|
37
|
+
<p>To complete your registration and gain access to our exclusive features and resources, please click on
|
|
38
|
+
the following link:
|
|
39
|
+
</p>
|
|
40
|
+
<p><a href='{{inviteLink}}' class='action-link'>{{inviteLink}}</a></p>
|
|
41
|
+
<div class='sub-text'>* This link is valid for a week</div>
|
|
42
|
+
|
|
43
|
+
<p>By clicking the link, you will be directed to a secure registration page where you can set up your
|
|
44
|
+
account and provide the necessary information.</p>
|
|
45
|
+
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<p>
|
|
49
|
+
<div class='greeting'>Regards,</div>
|
|
50
|
+
AX Partners Digital Technology Team
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class='copyright'>© AX Partner Corporation 2023</div>
|
|
57
|
+
</div>
|
|
58
|
+
</body>
|
|
59
|
+
|
|
60
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
|
|
4
|
+
const randomBytes = promisify(crypto.randomBytes);
|
|
5
|
+
|
|
6
|
+
export const generateString = async (length: number) => {
|
|
7
|
+
const rawBytes = await randomBytes(length);
|
|
8
|
+
const result = rawBytes.toString('hex');
|
|
9
|
+
|
|
10
|
+
return result;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const getMime = (fileName: string) => {
|
|
14
|
+
return fileName.substring(fileName.lastIndexOf('.') + 1, fileName.length).toLowerCase();
|
|
15
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface IGeneratePresignedUrl {
|
|
2
|
+
bucket?: string;
|
|
3
|
+
fileName: string;
|
|
4
|
+
prefix?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface IFileUploadResult {
|
|
8
|
+
key: string;
|
|
9
|
+
mime: string;
|
|
10
|
+
completedUrl: string;
|
|
11
|
+
originalFileName: string;
|
|
12
|
+
createdAt: Date;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface IPutFilesOptions {
|
|
16
|
+
path?: string;
|
|
17
|
+
bucketName?: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import httpStatus from 'http-status';
|
|
3
|
+
import createError from 'http-errors';
|
|
4
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
5
|
+
import { GetObjectCommand, PutObjectCommand, HeadObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
|
6
|
+
|
|
7
|
+
import { config } from '../../../config';
|
|
8
|
+
import { s3Client } from '../../../lib/aws';
|
|
9
|
+
import { generateString, getMime } from './S3.helper';
|
|
10
|
+
import { msToSeconds } from '../../../utils/msToSeconds';
|
|
11
|
+
|
|
12
|
+
import { IFileUploadResult, IGeneratePresignedUrl, IPutFilesOptions } from './s3.interface';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generates a signed URL for accessing an S3 object.
|
|
16
|
+
* @param {string} key - The file name stored in s3.
|
|
17
|
+
* @returns {Promise<{key: string, signedUrl: string}>>} The signed URL.
|
|
18
|
+
*/
|
|
19
|
+
export const generateSignedURL = async (key: string) => {
|
|
20
|
+
const getObjectParams = {
|
|
21
|
+
Bucket: config.aws.bucket,
|
|
22
|
+
Key: key,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const command = new GetObjectCommand(getObjectParams);
|
|
26
|
+
const signedUrl = await getSignedUrl(s3Client, command, { expiresIn: 120 });
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
key,
|
|
30
|
+
signedUrl,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generates a presigned URL for uploading a file to an S3 bucket.
|
|
36
|
+
* @param {object} params - The parameters for generating the presigned URL.
|
|
37
|
+
* @param {string} params.bucket - The S3 bucket name.
|
|
38
|
+
* @param {string} params.fileName - The desired file name.
|
|
39
|
+
* @returns {Promise<{key: string, signedUrl: string}>} The presigned URL.
|
|
40
|
+
*/
|
|
41
|
+
export const generatePresignedUrl = async ({ prefix = '', bucket, fileName }: IGeneratePresignedUrl) => {
|
|
42
|
+
const mime = getMime(fileName);
|
|
43
|
+
|
|
44
|
+
const key = prefix + `${await generateString(10)}_${Date.now()}.` + mime;
|
|
45
|
+
|
|
46
|
+
const command = new PutObjectCommand({ Bucket: bucket || config.aws.bucket, Key: key });
|
|
47
|
+
const signedUrl = await getSignedUrl(s3Client, command, {
|
|
48
|
+
expiresIn: msToSeconds(config.aws.uploadSignedUrlExpiresIn),
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
key,
|
|
52
|
+
signedUrl,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Uploads multiple files to an S3 bucket.
|
|
58
|
+
* @param {any[]} files - The array of files to upload.
|
|
59
|
+
* @param {IOptions} [options] - The optional upload options.
|
|
60
|
+
* @returns {Promise<IFileUploadResult[]>} The array of file upload results.
|
|
61
|
+
*/
|
|
62
|
+
export const putFilesToBucket = async (files: any, options?: IPutFilesOptions): Promise<IFileUploadResult[]> => {
|
|
63
|
+
let path = options?.path;
|
|
64
|
+
|
|
65
|
+
const bucketName = options?.bucketName || config.aws.bucket;
|
|
66
|
+
|
|
67
|
+
return await Promise.all(
|
|
68
|
+
files.map(async (file: any) => {
|
|
69
|
+
const fileContent = fs.readFileSync(file.path);
|
|
70
|
+
|
|
71
|
+
const originalname = file.originalname;
|
|
72
|
+
const mime = getMime(originalname);
|
|
73
|
+
|
|
74
|
+
const filename = `${await generateString(10)}_${Date.now()}.` + mime;
|
|
75
|
+
if (path) path = path.startsWith('/') ? path.replace('/', '') : `${path}`;
|
|
76
|
+
|
|
77
|
+
// path from aws
|
|
78
|
+
const key = path ? `${path}/${filename}` : filename;
|
|
79
|
+
const filePath = `https://${bucketName}.s3.amazonaws.com/${key}`;
|
|
80
|
+
|
|
81
|
+
const command = new PutObjectCommand({
|
|
82
|
+
Bucket: bucketName,
|
|
83
|
+
Key: key,
|
|
84
|
+
Body: fileContent,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await s3Client.send(command);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
key,
|
|
91
|
+
mime,
|
|
92
|
+
completedUrl: filePath,
|
|
93
|
+
originalFileName: originalname,
|
|
94
|
+
createdAt: new Date(),
|
|
95
|
+
};
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const getFileSize = async (key: string): Promise<number> => {
|
|
101
|
+
const getObjectMetaDataParams = {
|
|
102
|
+
Bucket: config.aws.bucket,
|
|
103
|
+
Key: key,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const response = await s3Client.send(new HeadObjectCommand(getObjectMetaDataParams));
|
|
107
|
+
const sizeInBytes = response.ContentLength;
|
|
108
|
+
|
|
109
|
+
if (!sizeInBytes) throw createError(httpStatus.NOT_FOUND, 'File not found');
|
|
110
|
+
|
|
111
|
+
return sizeInBytes;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const deleteFile = (key: string) => {
|
|
115
|
+
const deleteObjectParams = {
|
|
116
|
+
Bucket: config.aws.bucket,
|
|
117
|
+
Key: key,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return s3Client.send(new DeleteObjectCommand(deleteObjectParams));
|
|
121
|
+
};
|