diff --git a/.gitignore b/.gitignore index a424e59e..ea244870 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ build release .vscode/settings.json /pkg-cache -*.cjs \ No newline at end of file +*.cjs +core/.key diff --git a/core/src/app.ts b/core/src/app.ts index 8173c4c0..4f0c7938 100644 --- a/core/src/app.ts +++ b/core/src/app.ts @@ -1,14 +1,13 @@ import { existsSync } from 'fs' -import { exists } from 'fs-extra' import { readFile, rm, writeFile } from 'fs/promises' import { join } from 'path' import { argv, cwd } from 'process' -import prompts, { PromptObject } from 'prompts' +import prompts from 'prompts' import 'reflect-metadata' import { env, PKG_MODE } from '.' import { Plugins } from './controller/plugins' import { SocketController } from './controller/socket' -import { Auth, encrypt } from './functions/key' +import { Auth } from '@/controller/auth' import { generatePort } from './functions/port' interface Args { @@ -26,22 +25,9 @@ const argsList: Args[] = [ (async () => { prompts.override((await import('yargs')).argv) - if (!(await exists('.key'))) { - const questions: PromptObject[] = [ - { name: 'email', message: 'Email', type: 'text', warn: 'Apenas cadastrado em paymentbot.com' }, - { name: 'password', message: 'Senha', type: 'password', warn: 'Apenas cadastrado em paymentbot.com' }, - { name: 'token', message: 'Token', type: 'password', warn: 'Visível no Dashboard' } - ] - const response = await prompts(questions) - - if (Object.keys(response).length !== 3 || Object.entries(response).filter(([, content]) => content === '').length > 0) throw new Error('Formulário não respondido!') - await encrypt(JSON.stringify(response)) - } - const auth = new Auth() - - await auth.init() + await auth.login() await auth.validator() @@ -65,30 +51,30 @@ const argsList: Args[] = [ } switch (args[argNum]) { - case 'info': { - const packageJSON = JSON.parse(await readFile(join(__dirname, '../package.json'), { encoding: 'utf-8' })) as Record - const infos = ['name', 'version', 'description', 'author', 'license'].reverse() - console.info(Object.entries(packageJSON).reverse().filter(([key]) => infos.includes(key)).reduce((object, [key, value]) => ({ [key]: value, ...object }), {})) - break - } - case 'port': { - argNum++ - socket.listen(args[argNum]) - break - } + case 'info': { + const packageJSON = JSON.parse(await readFile(join(__dirname, '../package.json'), { encoding: 'utf-8' })) as Record + const infos = ['name', 'version', 'description', 'author', 'license'].reverse() + console.info(Object.entries(packageJSON).reverse().filter(([key]) => infos.includes(key)).reduce((object, [key, value]) => ({ [key]: value, ...object }), {})) + break + } + case 'port': { + argNum++ + socket.listen(args[argNum]) + break + } } } ['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT', 'SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2', 'SIGTERM' ].forEach(function (sig) { - process.on(sig, async function () { - if (PKG_MODE) { - for await (const plugin of await SocketController.io.fetchSockets()) { - if (plugin) plugin.emit('kill') - } + process.on(sig, async function () { + if (PKG_MODE) { + for await (const plugin of await SocketController.io.fetchSockets()) { + if (plugin) plugin.emit('kill') } - process.exit() - }); + } + process.exit() + }); }); })() diff --git a/core/src/controller/auth.ts b/core/src/controller/auth.ts new file mode 100644 index 00000000..05423b13 --- /dev/null +++ b/core/src/controller/auth.ts @@ -0,0 +1,220 @@ +import { Blowfish, enc } from 'crypto-js' +import { readFile, rm, writeFile } from 'fs/promises' +import { RootPATH } from '..' +import { api, key } from '../../package.json' +import { CronJob } from 'cron' +import prompts, { Choice, PromptObject } from 'prompts' +import { exists } from 'fs-extra' + +type Credentials = { + email: string + password: string + token: string +} + +interface User { + name: string; + email: string; + uuid: string; +} + +interface AccessToken { + token: string; + expireIn: number; +} + +interface RefreshToken { + token: string; + expireIn: number; +} + +interface AuthData { + user: User; + accessToken: AccessToken; + refreshToken: RefreshToken; +} +interface BotInfo { + uuid: string; + name: string; + enabled: boolean; + expired: boolean; + expire_at: string; + created_at: string; +} + +export class Auth { + public static user: User + public static accessToken: AccessToken + public static bot: BotInfo | undefined + private credentials: Credentials | undefined + + async askCredentials ({ question }: { question?: (keyof Credentials)[] }): Promise { + const questions: PromptObject[] = [ + { name: 'email', message: 'Email', type: 'text', warn: 'Apenas cadastrado em paymentbot.com' }, + { name: 'password', message: 'Senha', type: 'password', warn: 'Apenas cadastrado em paymentbot.com' }, + { name: 'token', message: 'Token', type: 'password', warn: 'Visível no Dashboard' } + ] + + const filter = questions.filter((propmt) => question === undefined || question?.includes(propmt.name as keyof Credentials)) + const response = await prompts(filter) as Credentials + + if (Object.keys(response).length !== filter.length || Object.entries(response).filter(([, content]) => content === '').length > 0) throw new Error('Formulário não respondido!') + await this.encryptCredentials(response) + return response + } + + async encryptCredentials(credentials: Credentials): Promise { + console.log(credentials) + credentials = { + ...(await this.decryptCredentials()), + ...credentials + } + console.log(credentials) + await writeFile(`${RootPATH}/.key`, Blowfish.encrypt(JSON.stringify(credentials), key).toString()) + } + + async decryptCredentials(): Promise { + const encrypted = await exists(`${RootPATH}/.key`) ? Blowfish.decrypt(await readFile(`${RootPATH}/.key`, { encoding: 'utf-8' }), key).toString(enc.Utf8) : '{}' + return JSON.parse(encrypted) + } + + + async initialize() { + const credentials = await this.decryptCredentials() + const keys = ['email', 'password', 'token'] + let hasError = false + + for (const key of keys) { + const credential = credentials[key as keyof Credentials] + if (credential === undefined || credential === '') hasError = true + } + if (hasError) { + await this.askCredentials({}) + await this.initialize() + return + } + this.credentials = credentials + } + + async login(): Promise { + if (this.credentials === undefined) { + await this.initialize() + } + + const response = await fetch(`${api}/auth/login`, { + method: 'POST', + body: JSON.stringify({ email: this.credentials?.email, password: this.credentials?.password }), + headers: { + "Content-Type": "application/json", + } + }) + + if (!response.ok) { + const choices: Choice[] = [ + { title: 'Deslogar', description: 'Removerá seção atual', value: 'logout' }, + { title: 'Tentar Novamente', description: 'Tentar novamente fazer o login', value: 'try_again' } + ] + + const conclusion = await prompts({ + type: 'select', + name: 'Error', + message: `Erro ${response.statusText} ao tentar logar!`, + choices, + initial: 1 + }) + + switch (conclusion.Error) { + case 'logout': { + await this.logout() + await this.askCredentials({}) + await this.initialize() + await this.login() + } + case 'try_again': { + await this.login() + } + } + return + } + const data = await response.json() as AuthData + + Auth.user = data.user + Auth.accessToken = data.accessToken + + console.log(`\n👋 Olá ${data.user.name}\n`) + return data + } + + + async logout () { + await rm(`${RootPATH}/.key`) + } + + async validator() { + if (this.credentials === undefined) { + await this.initialize() + } + + const response = await fetch(`${api}/bots/${this.credentials?.token}`, { + headers: { + Authorization: `Bearer ${Auth.accessToken.token}` + } + }) + + if (!response.ok) { + console.log(`☝️ Então ${Auth.user.name}, não achei o registro do seu bot!`) + const choices: Choice[] = [ + { title: 'Mudar Token', value: 'change' }, + { title: 'Tentar Novamente', value: 'try_again' }, + { title: 'Deslogar', value: 'logout' } + ] + + const conclusion = await prompts({ + name: 'Error', + type: 'select', + choices, + message: `Ocorreu um erro ${response.statusText}`, + initial: 1 + }) + + switch (conclusion.Error) { + case 'change': { + await this.askCredentials({ question: ['token'] }) + await this.initialize() + await this.login() + await this.validator() + break + } + case 'try_again': { + await this.validator() + break + } + case 'logout': { + await this.logout() + await this.askCredentials({}) + await this.initialize() + await this.login() + await this.validator() + break + } + } + + return + } + + const data = await response.json() as BotInfo + + if (data.expired) { + console.log('❌ Bot expirou!') + } else if (!data.enabled) { + console.log('❌ Bot desabilitado!') + } + + Auth.bot = data + } + + async startCron (): Promise { + const job = new CronJob('* * * * *', () => this.validator()) + job.start() + } +} \ No newline at end of file diff --git a/core/src/functions/key.ts b/core/src/functions/key.ts deleted file mode 100644 index 39dd2468..00000000 --- a/core/src/functions/key.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Blowfish, enc } from 'crypto-js' -import { readFile, rm, writeFile } from 'fs/promises' -import { RootPATH } from '..' -import { api, key } from '../../package.json' -import { CronJob } from 'cron' -import prompts, { Choice, PromptObject } from 'prompts' - -interface DataKey { - email: string - password: string - token: string -} - -interface User { - name: string; - email: string; - uuid: string; -} - -interface AccessToken { - token: string; - expireIn: number; -} - -interface RefreshToken { - token: string; - expireIn: number; -} - -interface AuthData { - user: User; - accessToken: AccessToken; - refreshToken: RefreshToken; -} -interface BotInfo { - uuid: string; - name: string; - enabled: boolean; - expired: boolean; - expire_at: string; - created_at: string; -} - -export class Auth { - public static user: User - public static accessToken: AccessToken - public static bot: BotInfo | undefined - private localData: DataKey | undefined - - async init() { - this.localData = await decrypt() - } - - async login(): Promise { - const response = await fetch(`${api}/auth/login`, { - method: 'POST', - body: JSON.stringify({ email: this.localData?.email, password: this.localData?.password }), - headers: { - "Content-Type": "application/json", - } - }) - - if (!response.ok) { - prompts.override(((await import('yargs')).argv)) - - const choices: Choice[] = [ - { title: 'Deslogar', description: 'Removerá seção atual', value: 'deslogar' }, - { title: 'Tentar Novamente', description: 'Tentar novamente fazer o login', value: 'try_again' } - ] - - const conclusion = await prompts({ - type: 'select', - name: 'Error', - message: `Erro ${response.statusText} ao tentar logar!`, - choices, - initial: 1 - }) - - switch (conclusion.Error) { - case 'deslogar': { - await rm(`${RootPATH}/.key`) - process.exit() - } - case 'try_again': { - await this.login() - } - } - return - } - const data = await response.json() as AuthData - - Auth.user = data.user - Auth.accessToken = data.accessToken - - console.log(`\n👋 Olá ${data.user.name}\n`) - return data - } - - async validator() { - const response = await fetch(`${api}/bots/${this.localData?.token}`, { - headers: { - Authorization: `Bearer ${Auth.accessToken.token}` - } - }) - - if (!response.ok) throw new Error(`☝️ Então ${Auth.user.name}, não achei o registro do seu bot!`) - - const data = await response.json() as BotInfo - - if (data.expired) { - console.log('❌ Bot expirou!') - } else if (!data.enabled) { - console.log('❌ Bot desabilitado!') - } - - Auth.bot = data - } - - async cron () { - CronJob.from({ - cronTime: '* * * * *', - onTick: () => this.validator(), - start: true - }) - } -} - -export async function encrypt(content: string) { - await writeFile(`${RootPATH}/.key`, Blowfish.encrypt(content, key).toString()) -} - -export async function decrypt(): Promise { - const content = Blowfish.decrypt(await readFile(`${RootPATH}/.key`, { encoding: 'utf-8' }), key).toString(enc.Utf8) - return JSON.parse(content) -} \ No newline at end of file