feat: 手机号登录、邮箱验证码注册

This commit is contained in:
xiaojunnuo
2024-11-29 19:00:05 +08:00
parent 87bbf6f140
commit 7b55337c5e
55 changed files with 2150 additions and 337 deletions
@@ -71,7 +71,7 @@ const development = {
type: 'better-sqlite3',
database: './data/db.sqlite',
synchronize: false, // 如果第一次使用,不存在表,有同步的需求可以写 true
logging: false,
logging: true,
// 配置实体模型 或者 entities: '/entity',
entities: ['**/modules/**/entity/*.js', ...libServerEntities, ...commercialEntities, PipelineEntity, FlywayHistory, UserEntity],
@@ -12,7 +12,7 @@ export type RegisterReq = {
phoneCode?: string;
validateCode: string;
imageCode: string;
imgCode: string;
randomStr: string;
};
@@ -40,16 +40,17 @@ export class RegisterController extends BaseController {
}
if (body.type === 'username') {
if (sysPublicSettings.usernameRegisterEnabled) {
if (sysPublicSettings.usernameRegisterEnabled === false) {
throw new Error('当前站点已禁止用户名注册功能');
}
await this.codeService.checkCaptcha(body.randomStr, body.imgCode);
const newUser = await this.userService.register(body.type, {
username: body.username,
password: body.password,
} as any);
return this.ok(newUser);
} else if (body.type === 'mobile') {
if (sysPublicSettings.mobileRegisterEnabled) {
if (sysPublicSettings.mobileRegisterEnabled === false) {
throw new Error('当前站点已禁止手机号注册功能');
}
//验证短信验证码
@@ -88,7 +88,7 @@ export class CodeService {
/**
*/
async sendEmailCode(email: string, randomStr: string) {
console.assert(!email, '手机号不能为空');
console.assert(!email, 'Email不能为空');
console.assert(!randomStr, 'randomStr不能为空');
const code = randomNumber(4);
@@ -98,7 +98,7 @@ export class CodeService {
receivers: [email],
});
const key = this.buildEmailCodeKey(email, code);
const key = this.buildEmailCodeKey(email, randomStr);
cache.set(key, code, {
ttl: 5 * 60 * 1000, //5分钟
});
@@ -1,6 +1,6 @@
import { AliyunAccess, AliyunClient } from '@certd/plugin-plus';
import { logger } from '@certd/basic';
import { ISmsService, PluginInputs, SmsPluginCtx } from './api.js';
import { AliyunAccess, AliyunClient } from '@certd/plugin-lib';
export type AliyunSmsConfig = {
accessId: string;
regionId: string;
@@ -154,30 +154,30 @@ export class UserService extends BaseService<UserEntity> {
}
async register(type: string, user: UserEntity) {
if (type !== 'username') {
if (!user.password) {
user.password = simpleNanoId();
}
if (!user.username) {
user.username = 'user_' + simpleNanoId();
if (!user.password) {
user.password = simpleNanoId();
}
if (type === 'username') {
const username = user.username;
const old = await this.findOne([{ username: username }, { mobile: username }, { email: username }]);
if (old != null) {
throw new CommonException('用户名已被注册');
}
}
if (type === 'mobile') {
user.nickName = user.mobile.substring(0, 3) + '****' + user.mobile.substring(7);
}
} else if (type === 'mobile') {
const mobile = user.mobile;
const old = await this.findOne({ username: user.username });
if (old != null) {
throw new CommonException('用户名已被注册');
}
if (user.mobile) {
const old = await this.findOne({ mobile: user.mobile });
user.nickName = mobile.substring(0, 3) + '****' + mobile.substring(7);
const old = await this.findOne([{ username: mobile }, { mobile: mobile }, { email: mobile }]);
if (old != null) {
throw new CommonException('手机号已被注册');
}
}
if (user.email) {
const old = await this.findOne({ email: user.email });
} else if (type === 'email') {
const email = user.email;
const old = await this.findOne([{ username: email }, { mobile: email }, { email: email }]);
if (old != null) {
throw new CommonException('邮箱已被注册');
}
@@ -186,10 +186,10 @@ export class UserService extends BaseService<UserEntity> {
let newUser: UserEntity = UserEntity.of({
username: user.username,
password: user.password,
nickName: user.nickName || user.username,
avatar: user.avatar || '',
email: user.email || '',
mobile: user.mobile || '',
nickName: user.nickName || user.username,
avatar: user.avatar || '',
phoneCode: user.phoneCode || '86',
status: 1,
passwordVersion: 2,
@@ -12,3 +12,4 @@ export * from './plugin-qiniu/index.js';
export * from './plugin-woai/index.js';
export * from './plugin-cachefly/index.js';
export * from './plugin-gcore/index.js';
export * from './plugin-qnap/index.js';
@@ -1,7 +1,7 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { Autowire } from '@certd/pipeline';
import { AliyunAccess, AliyunClient } from '@certd/plugin-plus';
import { AliyunAccess, AliyunClient } from '@certd/plugin-lib';
@IsDnsProvider({
name: 'aliyun',
@@ -1,5 +1,5 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { AliyunAccess, AliyunClient, AliyunSslClient, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-plus';
import { AliyunAccess, AliyunClient, AliyunSslClient, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { optionsUtils } from '@certd/basic/dist/utils/util.options.js';
@IsTaskPlugin({
@@ -1,6 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import dayjs from 'dayjs';
import { AliyunAccess, AliyunClient, createCertDomainGetterInputDefine } from '@certd/plugin-plus';
import { AliyunAccess, AliyunClient, createCertDomainGetterInputDefine } from '@certd/plugin-lib';
import { CertInfo } from '@certd/plugin-cert';
@IsTaskPlugin({
@@ -1,5 +1,5 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { AliyunAccess } from '@certd/plugin-plus';
import { AliyunAccess } from '@certd/plugin-lib';
import { CertInfo } from '@certd/plugin-cert';
@IsTaskPlugin({
name: 'DeployCertToAliyunOSS',
@@ -1,13 +1,7 @@
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo } from '@certd/plugin-cert';
import {
AbstractPlusTaskPlugin,
AliyunAccess,
AliyunClient,
AliyunSslClient,
createCertDomainGetterInputDefine,
createRemoteSelectInputDefine,
} from '@certd/plugin-plus';
import { AliyunAccess, AliyunClient, AliyunSslClient, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { AbstractPlusTaskPlugin } from '@certd/plugin-plus';
@IsTaskPlugin({
name: 'AliyunDeployCertToWaf',
@@ -1,6 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
import { AliyunAccess } from '@certd/plugin-plus';
import { AliyunSslClient } from '@certd/plugin-plus';
import { AliyunAccess } from '@certd/plugin-lib';
import { AliyunSslClient } from '@certd/plugin-lib';
/**
* 华东1(杭州) cn-hangzhou cas.aliyuncs.com cas-vpc.cn-hangzhou.aliyuncs.com
@@ -1,6 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo, CertReader } from '@certd/plugin-cert';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-plus';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { optionsUtils } from '@certd/basic/dist/utils/util.options.js';
@IsTaskPlugin({
@@ -1 +0,0 @@
export * from './ssh-access.js';
@@ -1,105 +0,0 @@
import { AccessInput, BaseAccess, IsAccess } from '@certd/pipeline';
import { ConnectConfig } from 'ssh2';
@IsAccess({
name: 'ssh',
title: '主机登录授权',
desc: '',
input: {},
})
export class SshAccess extends BaseAccess implements ConnectConfig {
@AccessInput({
title: '主机地址',
component: {
placeholder: '主机域名或IP地址',
},
required: true,
})
host!: string;
@AccessInput({
title: '端口',
value: 22,
component: {
name: 'a-input-number',
placeholder: '22',
},
rules: [{ required: true, message: '此项必填' }],
})
port!: number;
@AccessInput({
title: '用户名',
value: 'root',
rules: [{ required: true, message: '此项必填' }],
})
username!: string;
@AccessInput({
title: '密码',
component: {
name: 'a-input-password',
vModel: 'value',
},
encrypt: true,
helper: '登录密码或密钥必填一项',
})
password!: string;
@AccessInput({
title: '私钥登录',
helper: '私钥或密码必填一项',
component: {
name: 'a-textarea',
vModel: 'value',
},
encrypt: true,
})
privateKey!: string;
@AccessInput({
title: '私钥密码',
helper: '如果你的私钥有密码的话',
component: {
name: 'a-input-password',
vModel: 'value',
},
encrypt: true,
})
passphrase!: string;
@AccessInput({
title: 'socks代理',
helper: 'socks代理配置,格式:socks5://user:password@host:port',
component: {
name: 'a-input',
vModel: 'value',
placeholder: 'socks5://user:password@host:port',
},
encrypt: false,
})
socksProxy!: string;
@AccessInput({
title: '是否Windows',
helper: '如果是Windows主机,请勾选此项\n并且需要windows[安装OpenSSH](https://certd.docmirror.cn/guide/use/host/windows.html)',
component: {
name: 'a-switch',
vModel: 'checked',
},
})
windows = false;
@AccessInput({
title: '命令编码',
helper: '如果是Windows主机,且出现乱码了,请尝试设置为GBK',
component: {
name: 'a-select',
vModel: 'value',
options: [
{ value: '', label: '默认' },
{ value: 'GBK', label: 'GBK' },
{ value: 'UTF8', label: 'UTF-8' },
],
},
})
encoding: string;
}
new SshAccess();
@@ -1,3 +1 @@
export * from './access/index.js';
export * from './lib/ssh.js';
export * from './plugin/index.js';
@@ -1,368 +0,0 @@
// @ts-ignore
import ssh2, { ConnectConfig, ExecOptions } from 'ssh2';
import path from 'path';
import * as _ from 'lodash-es';
import { ILogger } from '@certd/basic';
import { SshAccess } from '../access/index.js';
import stripAnsi from 'strip-ansi';
import { SocksClient } from 'socks';
import { SocksProxy, SocksProxyType } from 'socks/typings/common/constants.js';
export class AsyncSsh2Client {
conn: ssh2.Client;
logger: ILogger;
connConf: SshAccess & ssh2.ConnectConfig;
windows = false;
encoding: string;
constructor(connConf: SshAccess, logger: ILogger) {
this.connConf = connConf;
this.logger = logger;
this.windows = connConf.windows || false;
this.encoding = connConf.encoding;
}
convert(iconv: any, buffer: Buffer) {
if (this.encoding) {
return iconv.decode(buffer, this.encoding);
}
return buffer.toString();
}
async connect() {
this.logger.info(`开始连接,${this.connConf.host}:${this.connConf.port}`);
if (this.connConf.socksProxy) {
this.logger.info(`使用代理${this.connConf.socksProxy}`);
if (typeof this.connConf.port === 'string') {
this.connConf.port = parseInt(this.connConf.port);
}
const proxyOption: SocksProxy = this.parseSocksProxyFromUri(this.connConf.socksProxy);
const info = await SocksClient.createConnection({
proxy: proxyOption,
command: 'connect',
destination: {
host: this.connConf.host,
port: this.connConf.port,
},
});
this.logger.info('代理连接成功');
this.connConf.sock = info.socket;
}
return new Promise((resolve, reject) => {
try {
const conn = new ssh2.Client();
conn
.on('error', (err: any) => {
this.logger.error('连接失败', err);
reject(err);
})
.on('ready', () => {
this.logger.info('连接成功');
this.conn = conn;
resolve(this.conn);
})
.connect(this.connConf);
} catch (e) {
reject(e);
}
});
}
async getSftp() {
return new Promise((resolve, reject) => {
this.logger.info('获取sftp');
this.conn.sftp((err: any, sftp: any) => {
if (err) {
reject(err);
return;
}
resolve(sftp);
});
});
}
async fastPut(options: { sftp: any; localPath: string; remotePath: string }) {
const { sftp, localPath, remotePath } = options;
return new Promise((resolve, reject) => {
this.logger.info(`开始上传:${localPath} => ${remotePath}`);
sftp.fastPut(localPath, remotePath, (err: Error) => {
if (err) {
reject(err);
this.logger.error('请确认路径是否包含文件名,路径本身不能是目录,路径不能有*?之类的特殊符号,要有写入权限');
return;
}
this.logger.info(`上传文件成功:${localPath} => ${remotePath}`);
resolve({});
});
});
}
async exec(script: string) {
if (!script) {
this.logger.info('script 为空,取消执行');
return;
}
let iconv: any = await import('iconv-lite');
iconv = iconv.default;
return new Promise((resolve, reject) => {
this.logger.info(`执行命令:[${this.connConf.host}][exec]: \n` + script);
this.conn.exec(script, (err: Error, stream: any) => {
if (err) {
reject(err);
return;
}
let data = '';
stream
.on('close', (code: any, signal: any) => {
this.logger.info(`[${this.connConf.host}][close]:code:${code}`);
if (code === 0) {
resolve(data);
} else {
reject(new Error(data));
}
})
.on('data', (ret: Buffer) => {
const out = this.convert(iconv, ret);
data += out;
this.logger.info(`[${this.connConf.host}][info]: ` + out.trimEnd());
})
.on('error', (err: any) => {
reject(err);
this.logger.error(err);
})
.stderr.on('data', (ret: Buffer) => {
const err = this.convert(iconv, ret);
data += err;
this.logger.info(`[${this.connConf.host}][error]: ` + err.trimEnd());
});
});
});
}
async shell(script: string | string[]): Promise<string[]> {
return new Promise<any>((resolve, reject) => {
this.logger.info(`执行shell脚本:[${this.connConf.host}][shell]: ` + script);
this.conn.shell((err: Error, stream: any) => {
if (err) {
reject(err);
return;
}
const output: string[] = [];
function ansiHandle(data: string) {
data = data.replace(/\[[0-9]+;1H/g, '\n');
data = stripAnsi(data);
return data;
}
stream
.on('close', () => {
this.logger.info('Stream :: close');
resolve(output);
})
.on('data', (ret: Buffer) => {
const data = ansiHandle(ret.toString());
this.logger.info(data);
output.push(data);
})
.on('error', (err: any) => {
reject(err);
this.logger.error(err);
})
.stderr.on('data', (ret: Buffer) => {
const data = ansiHandle(ret.toString());
output.push(data);
this.logger.info(`[${this.connConf.host}][error]: ` + data);
});
//保证windows下正常退出
const exit = '\r\nexit\r\n';
stream.end(script + exit);
});
});
}
end() {
if (this.conn) {
this.conn.end();
this.conn.destroy();
this.conn = null;
}
}
private parseSocksProxyFromUri(socksProxyUri: string): SocksProxy {
const url = new URL(socksProxyUri);
let type: SocksProxyType = 5;
if (url.protocol.startsWith('socks4')) {
type = 4;
}
const proxy: SocksProxy = {
host: url.hostname,
port: parseInt(url.port),
type,
};
if (url.username) {
proxy.userId = url.username;
}
if (url.password) {
proxy.password = url.password;
}
return proxy;
}
}
export class SshClient {
logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
/**
*
* @param connectConf
{
host: '192.168.100.100',
port: 22,
username: 'frylock',
password: 'nodejsrules'
}
* @param options
*/
async uploadFiles(options: { connectConf: SshAccess; transports: any; mkdirs: boolean }) {
const { connectConf, transports, mkdirs } = options;
await this._call({
connectConf,
callable: async (conn: AsyncSsh2Client) => {
const sftp = await conn.getSftp();
this.logger.info('开始上传');
for (const transport of transports) {
if (mkdirs !== false) {
const filePath = path.dirname(transport.remotePath);
let mkdirCmd = `mkdir -p ${filePath} `;
if (conn.windows) {
if (filePath.indexOf('/') > -1) {
this.logger.info('--------------------------');
this.logger.info('请注意:windows下,文件目录分隔应该写成\\而不是/');
this.logger.info('--------------------------');
}
const isCmd = await this.isCmd(conn);
if (!isCmd) {
mkdirCmd = `New-Item -ItemType Directory -Path "${filePath}" -Force`;
} else {
mkdirCmd = `if not exist "${filePath}" mkdir "${filePath}"`;
}
}
await conn.exec(mkdirCmd);
}
await conn.fastPut({ sftp, ...transport });
}
this.logger.info('文件全部上传成功');
},
});
}
async isCmd(conn: AsyncSsh2Client) {
const spec = await conn.exec('echo %COMSPEC%');
if (spec.toString().trim() === '%COMSPEC%') {
return false;
} else {
return true;
}
}
/**
*
* Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
* Start-Service sshd
*
* Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\cmd.exe"
* @param options
*/
async exec(options: { connectConf: SshAccess; script: string | Array<string>; env?: any }): Promise<string[]> {
let { script } = options;
const { connectConf } = options;
// this.logger.info('命令:', script);
return await this._call({
connectConf,
callable: async (conn: AsyncSsh2Client) => {
let isWinCmd = false;
const isLinux = !connectConf.windows;
const envScripts = [];
if (connectConf.windows) {
isWinCmd = await this.isCmd(conn);
}
if (options.env) {
for (const key in options.env) {
if (isLinux) {
envScripts.push(`export ${key}=${options.env[key]}`);
} else if (isWinCmd) {
//win cmd
envScripts.push(`set ${key}=${options.env[key]}`);
} else {
//powershell
envScripts.push(`$env:${key}="${options.env[key]}"`);
}
}
}
if (isWinCmd) {
//组合成&&的形式
if (typeof script === 'string') {
script = script.split('\n');
}
script = envScripts.concat(script);
script = script as Array<string>;
script = script.join(' && ');
} else {
if (_.isArray(script)) {
script = script as Array<string>;
script = script.join('\n');
}
if (envScripts.length > 0) {
script = envScripts.join('\n') + '\n' + script;
}
}
await conn.exec(script);
},
});
}
//废弃
async shell(options: { connectConf: SshAccess; script: string | Array<string> }): Promise<string[]> {
let { script } = options;
const { connectConf } = options;
if (_.isArray(script)) {
script = script as Array<string>;
if (connectConf.windows) {
script = script.join('\r\n');
} else {
script = script.join('\n');
}
} else {
if (connectConf.windows) {
script = script.replaceAll('\n', '\r\n');
}
}
return await this._call({
connectConf,
callable: async (conn: AsyncSsh2Client) => {
return await conn.shell(script as string);
},
});
}
async _call(options: { connectConf: SshAccess; callable: any }): Promise<string[]> {
const { connectConf, callable } = options;
const conn = new AsyncSsh2Client(connectConf, this.logger);
try {
await conn.connect();
} catch (e: any) {
if (e.message?.indexOf('All configured authentication methods failed') > -1) {
this.logger.error(e);
throw new Error('登录失败,请检查用户名/密码/密钥是否正确');
}
throw e;
}
try {
return await callable(conn);
} finally {
conn.end();
}
}
}
@@ -1,5 +1,5 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { SshClient } from '../../lib/ssh.js';
import { SshClient } from '@certd/plugin-lib';
@IsTaskPlugin({
name: 'hostShellExecute',
@@ -1,9 +1,8 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
import { SshClient } from '../../lib/ssh.js';
import { CertInfo, CertReader, CertReaderHandleContext } from '@certd/plugin-cert';
import * as fs from 'fs';
import { SshAccess } from '../../access/index.js';
import dayjs from 'dayjs';
import { SshAccess, SshClient } from '@certd/plugin-lib';
@IsTaskPlugin({
name: 'uploadCertToHost',
@@ -1,7 +1,7 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { HuaweiAccess } from '../../access/index.js';
import { CertInfo } from '@certd/plugin-cert';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-plus';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { resetLogConfigure } from '@certd/basic';
@IsTaskPlugin({
@@ -2,10 +2,10 @@ import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipel
import fs from 'fs';
import path from 'path';
import dayjs from 'dayjs';
import { SshAccess, SshClient } from '../../plugin-host/index.js';
import { AbstractPlusTaskPlugin } from '@certd/plugin-plus';
import JSZip from 'jszip';
import * as os from 'node:os';
import { SshAccess, SshClient } from '@certd/plugin-lib';
const defaultBackupDir = 'certd_backup';
const defaultFilePrefix = 'db-backup';
@@ -1,7 +1,8 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine, QiniuAccess, QiniuClient } from '@certd/plugin-plus';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { CertInfo } from '@certd/plugin-cert';
import { optionsUtils } from '@certd/basic/dist/utils/util.options.js';
import { QiniuAccess, QiniuClient } from '@certd/plugin-plus';
@IsTaskPlugin({
name: 'QiniuDeployCertToCDN',
@@ -1,9 +1,9 @@
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo } from '@certd/plugin-cert';
import { AbstractPlusTaskPlugin } from '@certd/plugin-plus';
import { SshAccess, SshClient } from '../../plugin-host/index.js';
import { tmpdir } from 'node:os';
import fs from 'fs';
import { SshAccess, SshClient } from '@certd/plugin-lib';
@IsTaskPlugin({
name: 'QnapDeploy',
@@ -1,7 +1,8 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { createRemoteSelectInputDefine, TencentAccess } from '@certd/plugin-plus';
import { TencentAccess } from '@certd/plugin-plus';
import { CertInfo } from '@certd/plugin-cert';
import { TencentSslClient } from '../../lib/index.js';
import { createRemoteSelectInputDefine } from '@certd/plugin-lib';
@IsTaskPlugin({
name: 'TencentDeployCertToCDNv2',
@@ -1,6 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo } from '@certd/plugin-cert';
import { createRemoteSelectInputDefine } from '@certd/plugin-plus';
import { createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { TencentSslClient } from '../../lib/index.js';
@IsTaskPlugin({