From 9cfec69422b82e3a453c51a1eb01e756bbbdc0f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=A7=81Ash=C3=BB=EA=A7=82?= <30575805+Ashu11-A@users.noreply.github.com> Date: Sun, 7 Jul 2024 00:24:37 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=A8=F0=9F=8F=BC=E2=80=8D=F0=9F=92=BB?= =?UTF-8?q?=20Implementing=20Crypt=20and=20Lang?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +- locales/en/translation.json | 40 ++++++++ locales/pt-BR/translation.json | 33 ++++++ package-lock.json | 121 +++++++++++++++++++++- package.json | 9 ++ src/class/crypt.ts | 179 +++++++++++++++++++++++++++++++++ src/class/pages.ts | 6 +- src/cli.ts | 5 + src/controller/cloudflare.ts | 43 +++----- src/controller/lang.ts | 60 +++++++++++ src/index.ts | 27 ++--- src/lib/validate.ts | 9 ++ src/pages/dns/create.ts | 7 +- src/pages/dns/delete.ts | 3 +- src/pages/dns/edit.ts | 3 +- src/pages/dns/search.ts | 3 +- src/pages/zones.ts | 3 +- src/types/crypt.ts | 5 + 18 files changed, 508 insertions(+), 54 deletions(-) create mode 100644 locales/en/translation.json create mode 100644 locales/pt-BR/translation.json create mode 100644 src/class/crypt.ts create mode 100644 src/controller/lang.ts create mode 100644 src/lib/validate.ts create mode 100644 src/types/crypt.ts diff --git a/.gitignore b/.gitignore index 69fb08d..4af1411 100755 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ /node_modules /dist .env -/data +/cache /coverage + +*.pem +*.key +*.hash diff --git a/locales/en/translation.json b/locales/en/translation.json new file mode 100644 index 0000000..a2ede5b --- /dev/null +++ b/locales/en/translation.json @@ -0,0 +1,40 @@ +{ + "crypt": { + "question": "How would you like to protect your local information?", + "generate_title": "Generate random password", + "generate_description": "It will be used to encrypt your information", + "define_title": "Define password", + "define_description": "It will be used to encrypt your information", + "your_password": "🔐 Your Password:", + "weak_password": "Password too weak! (1 Uppercase letter, 1 Lowercase letter, 1 Number, 1 Special character)", + "file_change": "⚠️ A change was detected in a protected file!", + "delete_file": "🗑️ Deleting encrypted files!", + "sensitive_information": "⚠️ Sensitive information being read" + }, + "authenticate": { + "registered": "📝 Registered at:", + "hello": "👋 Hello {{name}}", + "change_token": "🔄 Change Token", + "try_again": "🔁 Try Again", + "logout": "↪️ Logout" + }, + "database": { + "starting": "🗂️ Starting Database", + "initialized": "✨ Database initialized with {{length}} entries", + "invalid_entity": "⛔ {{tableName}} Invalid entity, loaded entities:" + }, + "error": { + "unstable": "🔴 {{element}} unstable", + "no_found": "🚫 No {{name}} found", + "not_exist": "❌ {{name}} does not exist!", + "not_select": "❌ No option selected!", + "no_possible": "❌ It is not possible to continue!", + "undefined": "❌ {{element}} undefined!", + "invalid": "❌ {{element}} invalid!", + "no_reply": "❌ Form not replied!", + "expired": "❌ {{element}} expired!", + "disabled": "❌ {{element}} disabled!", + "an_error_occurred": "❌ An error occurred:", + "timeout": "⏳ Timeout of {{time}} seconds... trying after the timeout" + } + } \ No newline at end of file diff --git a/locales/pt-BR/translation.json b/locales/pt-BR/translation.json new file mode 100644 index 0000000..ff3b463 --- /dev/null +++ b/locales/pt-BR/translation.json @@ -0,0 +1,33 @@ +{ + "crypt": { + "question": "Como deseja proteger suas informações locais?", + "generate_title": "Gerar senha aleatória", + "generate_description": "Será usada para criptografar suas informações", + "define_title": "Definir senha", + "define_description": "Será usada para criptografar suas informações", + "your_password": "🔐 Sua Senha:", + "weak_password": "Senha muito fraca! (1 Letra maiúscula, 1 Letra Minuscula, 1 Numero, 1 Carácter especial)", + "file_change": "⚠️ Foi detectado uma mudança em um arquivo protegido!", + "delete_file": "🗑️ Deletando arquivos encriptados!", + "sensitive_information": "⚠️ Informações sensíveis sendo lidas" + }, + "database": { + "starting": "🗂️ Iniciando Banco de dados", + "initialized": "✨ Banco de dados inicializado com {{length}} entries", + "invalid_entity": "⛔ {{tableName}} Entidade invalida, Entidades carregadas:" + }, + "error": { + "unstable": "🔴 {{element}} instável", + "no_found": "🚫 Nenhum {{name}} encontrado", + "not_exist": "❌ {{name}} não existe!", + "not_select": "❌ Nenhuma opção selecionada!", + "no_possible": "❌ Não é possivel continuar!", + "undefined": "❌ {{element}} indefinido!", + "invalid": "❌ {{element}} invalido!", + "no_reply": "❌ Formulário não respondido!", + "expired": "❌ {{element}} expired!", + "disabled": "❌ {{element}} disabled!", + "an_error_occurred": "❌ Ocorreu um erro:", + "timeout": "⏳ Timeout de {{time}} segundos... tentando após o timeout" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d4ccf72..987d62b 100755 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,21 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "argon2": "^0.40.3", "chalk": "^5.3.0", + "check-password-strength": "^2.0.10", "cloudflare": "^3.4.0", + "country-code-to-flag-emoji": "^1.3.3", + "crypto-js": "^4.2.0", "dotenv": "^16.4.5", "enmap": "^6.0.2", "enquirer": "^2.4.1", "glob": "^10.4.2", + "i18next": "^23.11.5", + "i18next-fs-backend": "^2.3.1", "inquirer": "^9.3.3", "inquirer-autocomplete-standalone": "^0.8.1", + "node-forge": "^1.3.1", "ora": "^8.0.1", "typescript": "^5.5.3" }, @@ -30,9 +37,11 @@ "@babel/preset-typescript": "^7.24.7", "@eslint/js": "^9.6.0", "@jest/globals": "^29.7.0", + "@types/crypto-js": "^4.2.2", "@types/inquirer": "^9.0.7", "@types/jest": "^29.5.12", "@types/node": "^20.14.9", + "@types/node-forge": "^1.3.11", "@types/prompts": "^2.4.9", "babel-plugin-module-resolver": "^5.0.2", "eslint": "^9.6.0", @@ -1898,7 +1907,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3413,6 +3421,14 @@ "node": ">= 8" } }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3719,6 +3735,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -3803,6 +3825,15 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prompts": { "version": "2.4.9", "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", @@ -4187,6 +4218,20 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, + "node_modules/argon2": { + "version": "0.40.3", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.40.3.tgz", + "integrity": "sha512-FrSmz4VeM91jwFvvjsQv9GYp6o/kARWoYKjbjDB2U5io1H3e5X67PYGclFDeQff6UXIhUd4aHR3mxCdBbMMuQw==", + "hasInstallScript": true, + "dependencies": { + "@phc/format": "^1.0.0", + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" + }, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -4737,6 +4782,11 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, + "node_modules/check-password-strength": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/check-password-strength/-/check-password-strength-2.0.10.tgz", + "integrity": "sha512-HRM5ICPmtnNtLnTv2QrfVkq1IxI9z3bzYpDJ1k5ixwD9HtJGHuv265R6JmHOV6r8wLhQMlULnIUVpkrC2yaiCw==" + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5029,6 +5079,14 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/country-code-to-flag-emoji": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/country-code-to-flag-emoji/-/country-code-to-flag-emoji-1.3.3.tgz", + "integrity": "sha512-9LBl79JRjaJ1xICP+Z/18ftIqtYM4UxZshejmKeSBQnkFdjvvW0k+6dV3j/rdn5aEuWdiV6PaHd4nvzB7Z4fsw==", + "funding": { + "url": "https://github.com/wojtekmaj/country-code-to-flag-emoji?sponsor=1" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -5100,6 +5158,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", @@ -6250,6 +6313,33 @@ "ms": "^2.0.0" } }, + "node_modules/i18next": { + "version": "23.11.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz", + "integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-fs-backend": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.3.1.tgz", + "integrity": "sha512-tvfXskmG/9o+TJ5Fxu54sSO5OkY6d+uMn+K6JiUGLJrwxAVfer+8V3nU8jq3ts9Pe5lXJv4b1N7foIjJ8Iy2Gg==" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -8261,6 +8351,14 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.0.0.tgz", + "integrity": "sha512-ipO7rsHEBqa9STO5C5T10fj732ml+5kLN1cAG8/jdHd56ldQeGj3Q7+scUS+VHK/qy1zLEwC4wMK5+yM0btPvw==", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -8298,6 +8396,24 @@ } } }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -9008,8 +9124,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", diff --git a/package.json b/package.json index 750e168..5d53598 100755 --- a/package.json +++ b/package.json @@ -48,9 +48,11 @@ "@babel/preset-typescript": "^7.24.7", "@eslint/js": "^9.6.0", "@jest/globals": "^29.7.0", + "@types/crypto-js": "^4.2.2", "@types/inquirer": "^9.0.7", "@types/jest": "^29.5.12", "@types/node": "^20.14.9", + "@types/node-forge": "^1.3.11", "@types/prompts": "^2.4.9", "babel-plugin-module-resolver": "^5.0.2", "eslint": "^9.6.0", @@ -65,14 +67,21 @@ "typescript-eslint": "^8.0.0-alpha.39" }, "dependencies": { + "argon2": "^0.40.3", "chalk": "^5.3.0", + "check-password-strength": "^2.0.10", "cloudflare": "^3.4.0", + "country-code-to-flag-emoji": "^1.3.3", + "crypto-js": "^4.2.0", "dotenv": "^16.4.5", "enmap": "^6.0.2", "enquirer": "^2.4.1", "glob": "^10.4.2", + "i18next": "^23.11.5", + "i18next-fs-backend": "^2.3.1", "inquirer": "^9.3.3", "inquirer-autocomplete-standalone": "^0.8.1", + "node-forge": "^1.3.1", "ora": "^8.0.1", "typescript": "^5.5.3" } diff --git a/src/class/crypt.ts b/src/class/crypt.ts new file mode 100644 index 0000000..d04268b --- /dev/null +++ b/src/class/crypt.ts @@ -0,0 +1,179 @@ +import { Lang } from '@/controller/lang.js' +import { i18, rootPath } from '@/index.js' +import { exists } from '@/lib/exists.js' +import { isJson } from '@/lib/validate.js' +import { DataCrypted } from '@/types/crypt.js' +import * as argon2 from 'argon2' +import { passwordStrength } from 'check-password-strength' +import { watch } from 'chokidar' +import { randomBytes } from 'crypto' +import CryptoJS from 'crypto-js' +import 'dotenv/config' +import { readFile, rm, writeFile } from 'fs/promises' +import forge from 'node-forge' +import { join } from 'path' +import prompts from 'prompts' + +export const credentials = new Map() + +export class Crypt { + async checker () { + if (!(await exists(join(rootPath, '..', '.env'))) && process.env?.token === undefined) await this.create() + if (!(await exists(join(rootPath, '..', 'privateKey.pem'))) || !(await exists(join(rootPath, '..', 'publicKey.pem')))) await this.genKeys() + + for (const path of ['.key', '.hash']) { + const wather = watch(path, { cwd: rootPath }) + + wather.on('change', async () => { + console.log() + console.log(i18('crypt.file_change')) + await this.validate() + }) + } + } + + async genKeys () { + const { privateKey, publicKey } = forge.pki.rsa.generateKeyPair(4096) + + await writeFile('privateKey.pem', forge.pki.privateKeyToPem(privateKey)) + await writeFile('publicKey.pem', forge.pki.publicKeyToPem(publicKey)) + } + + async privateKey () { + if (!(await exists(join(rootPath, '..', 'privateKey.pem')))) throw new Error(i18('error.not_exist', { name: 'PrivateKey' })) + return forge.pki.privateKeyFromPem(await readFile(join(rootPath, '..', 'privateKey.pem'), { encoding: 'utf8' })) + } + + async publicKey () { + if (!await (exists(join(rootPath, '..', 'privateKey.pem')))) throw new Error(i18('error.not_exist', { name: 'PublicKey' })) + return forge.pki.publicKeyFromPem(await readFile(join(rootPath, '..', 'publicKey.pem'), { encoding: 'utf8' })) + } + + async encript (data: string) { + return (await this.publicKey()).encrypt(data, 'RSA-OAEP') + } + + async decrypt (data: string) { + return (await this.privateKey()).decrypt(data, 'RSA-OAEP') + } + + async create () { + const select = await prompts({ + name: 'type', + type: 'select', + message: i18('crypt.question'), + initial: 0, + choices: [ + { + title: i18('crypt.generate_title'), + description: i18('crypt.generate_description'), + value: 'random' + }, + { + title: i18('crypt.define_title'), + description: i18('crypt.define_description'), + value: 'defined' + } + ] + }) + + switch (select.type) { + case 'random': { + const password = randomBytes(256).toString('hex') + await writeFile(join(rootPath, '..', '.env'), `token=${password}`) + credentials.set('token', password) + await this.write({}) + break + } + case 'defined': { + const key = await prompts({ + name: 'value', + type: 'password', + message: i18('crypt.your_password'), + validate: (value: string) => passwordStrength(value).id < 2 ? i18('crypt.weak_password') : true + }) + if (key.value === undefined) throw new Error(i18('error.undefined', { element: 'Password' })) + await writeFile(join(rootPath, '..', '.env'), `token=${key.value}`) + credentials.set('token', key.value) + await this.write({}) + break + } + default: throw new Error(i18('error.not_select')) + } + } + + getToken (): string | undefined { + let token = process.env.token + + if (token === undefined) token = credentials.get('token') as string + + return token + } + + async validate () { + const data = await readFile(join(rootPath, '..', '.key'), { encoding: 'utf-8' }).catch(() => '') + const dataHash = await readFile(join(rootPath, '..', '.hash'), { encoding: 'utf-8' }).catch(() => '') + + const invalid = async () => { + await this.delete() + throw new Error((i18('error.invalid', { element: 'Hash' }), i18('crypt.delete_file'))) + } + + const hash = await argon2.verify(dataHash, data).catch(async () => await invalid()) + if (!hash) await invalid() + } + + async delete () { + if (await exists(join(rootPath, '..', '.key'))) await rm(join(rootPath, '..', '.key')) + if (await exists(join(rootPath, '..', '.hash'))) await rm(join(rootPath, '..', '.hash')) + if (await exists(join(rootPath, '..', '.env'))) await rm(join(rootPath, '..', '.env')) + } + + async read (ephemeral?: boolean): Promise { + const token = this.getToken() + if (token === undefined) return + const existKey = await exists(join(rootPath, '..', '.key')) + if (!existKey) return undefined + + await this.validate() + if (!ephemeral) console.log(i18('crypt.sensitive_information'), '\n') + const data = await readFile(join(rootPath, '..', '.key'), { encoding: 'utf-8' }).catch(() => '') + try { + const TripleDESCrypt = CryptoJS.TripleDES.decrypt(data, token).toString(CryptoJS.enc.Utf8) + const BlowfishCrypt = CryptoJS.Blowfish.decrypt(TripleDESCrypt, token).toString(CryptoJS.enc.Utf8) + const AESCrypt = CryptoJS.AES.decrypt(BlowfishCrypt, token).toString(CryptoJS.enc.Utf8) + + const outputData = JSON.parse(AESCrypt) as DataCrypted + + if (outputData.language !== undefined) new Lang().setLanguage(outputData.language) + + + for (const [key, value] of Object.entries(outputData) as Array<[string, string | object | boolean | number]>) { + credentials.set(key, value) + } + + + return outputData + } catch { + await this.delete() + throw new Error(i18('error.invalid', { element: 'token' })) + } + } + + async write (value: Record | string | object) { + if (!isJson(value)) throw new Error(i18('error.invalid', { element: '.key' })) + + const token = this.getToken() + if (token === undefined) return + + const data = Object.assign(await this.read(true) ?? {}, value) + + const AESCrypt = CryptoJS.AES.encrypt(JSON.stringify(data), token).toString() + const BlowfishCrypt = CryptoJS.Blowfish.encrypt(AESCrypt, token).toString() + const TripleDESCrypt = CryptoJS.TripleDES.encrypt(BlowfishCrypt, token).toString() + const hashCrypt = await argon2.hash(TripleDESCrypt) + + await writeFile(join(rootPath, '..', '.key'), TripleDESCrypt) + await writeFile(join(rootPath, '..', '.hash'), hashCrypt) + } +} diff --git a/src/class/pages.ts b/src/class/pages.ts index 626ce16..f27a9c9 100755 --- a/src/class/pages.ts +++ b/src/class/pages.ts @@ -1,4 +1,4 @@ -import { __dirname, page } from '@/index.js' +import { rootPath, page } from '@/index.js' import { PageStructure, PageTypes } from '@/types/page.js' import chalk from 'chalk' import 'dotenv/config' @@ -68,9 +68,9 @@ export class Page { } static async register() { - const pages = await glob('pages/**/*.{ts,js}', { cwd: __dirname, ignore: ['pages/**/*.d.ts'] }) + const pages = await glob('pages/**/*.{ts,js}', { cwd: rootPath, ignore: ['pages/**/*.d.ts'] }) for (const page of pages) { - await import(join(__dirname, page)) + await import(join(rootPath, page)) } } diff --git a/src/cli.ts b/src/cli.ts index a55a1d2..1204657 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,8 @@ #!/usr/bin/env node import { writeFile } from 'fs/promises' import { join } from 'path' +import { Crypt } from './class/crypt.js' +import { Lang } from './controller/lang.js' import { exists } from './lib/exists.js' const pkgPath = join(process.cwd(), 'package.json') @@ -14,4 +16,7 @@ if (!(await exists(pkgPath))) { }) } +if ((await new Crypt().read(true))?.language === undefined) await new Lang().selectLanguage() +await new Crypt().checker() + await import('./app.js') \ No newline at end of file diff --git a/src/controller/cloudflare.ts b/src/controller/cloudflare.ts index 9cdfdd5..b8b9cdd 100755 --- a/src/controller/cloudflare.ts +++ b/src/controller/cloudflare.ts @@ -1,42 +1,26 @@ +import { credentials, Crypt } from '@/class/crypt.js' import { Questions } from '@/class/questions.js' -import { exists } from '@/lib/exists.js' import Cloudflare from 'cloudflare' import 'dotenv/config' -import { readFile, writeFile } from 'fs/promises' export async function checker() { - let email: string | undefined - let key: string | undefined - let data = await exists('.env') ? await readFile('.env', { encoding: 'utf-8' }) ?? '' : '' - - if ([undefined, ''].includes(process.env.CLOUDFLARE_EMAIL)) { - email = await new Questions({ message: 'Email do cloudflare está indefinido!' }).ask('Email do Cloudflare') - } - if ([undefined, ''].includes(process.env.CLOUDFLARE_API_KEY)) { - key = await new Questions({ message: 'Token do cloudflare está indefinido!' }).ask('Token do Cloudflare') - } + const data = await new Crypt().read() + let email = data?.email + let token = data?.token - if (email !== undefined) { - const regex = /CLOUDFLARE_EMAIL="(?:[^"]|"")*"/im - - data = regex.test(data) - ? data.replace(regex, `CLOUDFLARE_EMAIL="${email}"`) - : data += `\nCLOUDFLARE_EMAIL="${email}"` + if ([undefined, ''].includes(email)) { + email = await new Questions({ message: 'Email do cloudflare está indefinido!' }).ask('Email do Cloudflare') } - if (key !== undefined) { - const regex = /CLOUDFLARE_API_KEY="(?:[^"]|"")*"/im - - data = regex.test(data) - ? data.replace(regex, `CLOUDFLARE_API_KEY="${email}"`) - : data += `\nCLOUDFLARE_API_KEY="${key}"` + if ([undefined, ''].includes(token)) { + token = await new Questions({ message: 'Token do cloudflare está indefinido!' }).ask('Token do Cloudflare') } - await writeFile('.env', data) + await new Crypt().write({ email, token }) } -const client = async () => { +const createClient = async () => { if (!process.env.isTest) await checker(); (await import('dotenv')).config({ override: true }) return new Cloudflare({ @@ -45,4 +29,9 @@ const client = async () => { }) } -export default client +/** + * Cloudflare client instance. + * + * @type {Cloudflare} + */ +export const client: Cloudflare = await createClient() diff --git a/src/controller/lang.ts b/src/controller/lang.ts new file mode 100644 index 0000000..8dc985f --- /dev/null +++ b/src/controller/lang.ts @@ -0,0 +1,60 @@ +import { Crypt } from '@/class/crypt.js' +import { rootPath } from '@/index.js' +import { exists } from '@/lib/exists.js' +import flags from 'country-code-to-flag-emoji' +import { glob } from 'glob' +import i18next from 'i18next' +import Backend, { FsBackendOptions } from 'i18next-fs-backend' +import { join } from 'path' +import prompts, { Choice } from 'prompts' + +export class Lang { + async setLanguage (lang: string, change?: boolean) { + const path = join(rootPath, '..', 'locales', lang) + const crypt = new Crypt() + + if (!(await exists(path))) { + console.log(`⛔ The selected language (${lang}) does not exist, using English by default`) + if (change) await crypt.write({ language: 'en' }) + i18next.changeLanguage('en') + return + } + await i18next.changeLanguage(lang).then(async () => { + if (change) await crypt.write({ language: lang }) + }) + } + + async selectLanguage () { + const path = join(rootPath, '..', 'locales') + const allLangs = (await glob('**/*.json', { cwd: path })).map((lang) => lang.split('/')[0]) + const langs = [] + for (const lang of allLangs) { + if (langs.filter((langExist) => langExist === lang).length == 0) langs.push(lang) + } + const choices: Choice[] = langs.map((lang) => ({ title: `${flags(lang)} - ${lang}`, value: lang } satisfies Choice)) + const response = await prompts({ + name: 'Language', + type: 'select', + choices, + message: 'Which language should I continue with?', + initial: 1 + }) + if (response.Language === undefined) throw new Error('Please select a language') + + this.setLanguage(response.Language, true) + } + /** + * Inicializar i18 + */ + async create () { + return await i18next.use(Backend).init({ + debug: false, + lng: 'en', + backend: { + loadPath: join(rootPath, '..', 'locales', '{{lng}}', '{{ns}}.json'), + } + }) + + } +} + \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 5ad15e7..ec03e34 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,24 +1,17 @@ -import { fileURLToPath } from 'url' -import { dirname } from 'path' import Cache from '@/class/cache.js' -import cloudflare from '@/controller/cloudflare.js' -import { Zone } from 'cloudflare/resources/zones/zones.mjs' import { Record } from 'cloudflare/resources/dns/records.mjs' -import Cloudflare from 'cloudflare' +import { Zone } from 'cloudflare/resources/zones/zones.mjs' +import { TFunction } from 'i18next' +import { dirname } from 'path' +import { fileURLToPath } from 'url' +import { Lang } from './controller/lang.js' /** * The directory name of the current module. * * @type {string} */ -export const __dirname: string = dirname(fileURLToPath(import.meta.url)) - -/** - * Cloudflare client instance. - * - * @type {Cloudflare} - */ -export const client: Cloudflare = await cloudflare() +export const rootPath: string = dirname(fileURLToPath(import.meta.url)) /** * Cache for pagination data. @@ -47,3 +40,11 @@ export const zone: Cache<{ name: string, id: string }> = new Cache<{ name: strin * @type {Cache} */ export const records: Cache = new Cache('records') + + +/** + * Package language controller + * + * @type {TFunction<'translation'>} + */ +export const i18: TFunction<'translation'> = await new Lang().create() \ No newline at end of file diff --git a/src/lib/validate.ts b/src/lib/validate.ts new file mode 100644 index 0000000..5585e1c --- /dev/null +++ b/src/lib/validate.ts @@ -0,0 +1,9 @@ +export function isJson (value: string | object) { + try { + if (typeof value === 'string') JSON.parse(value) + if (typeof value === 'object') return true + return true + } catch { + return false + } +} \ No newline at end of file diff --git a/src/pages/dns/create.ts b/src/pages/dns/create.ts index 9f74c7d..5a0cad4 100755 --- a/src/pages/dns/create.ts +++ b/src/pages/dns/create.ts @@ -1,11 +1,12 @@ import { Page } from '@/class/pages.js' import { Questions } from '@/class/questions.js' -import { client, zone } from '@/index.js' +import { zone } from '@/index.js' import { extractTypes, Properties } from '@/lib/extractTypes.js' import { PageTypes } from '@/types/page.js' import chalk from 'chalk' -import { __dirname } from '@/index.js' +import { rootPath } from '@/index.js' import { join } from 'path' +import { client } from '@/controller/cloudflare.js' enum RecordsType { ARecord = 'A', @@ -57,7 +58,7 @@ new Page({ * Isso converte as tipagens do cloudflare. * @returns {Record | undefined} */ - const pathToCloudflare = join(__dirname, '../', 'node_modules/cloudflare/src/resources/dns/records.ts') + const pathToCloudflare = join(rootPath, '..', 'node_modules/cloudflare/src/resources/dns/records.ts') const properties = extractTypes(pathToCloudflare, record) if (properties === undefined) throw new Error(`Não foi possivel achar os types de ${record}`) diff --git a/src/pages/dns/delete.ts b/src/pages/dns/delete.ts index 85c1763..7fcc1fd 100755 --- a/src/pages/dns/delete.ts +++ b/src/pages/dns/delete.ts @@ -1,6 +1,7 @@ import { Page } from '@/class/pages.js' import { Questions } from '@/class/questions.js' -import { client, records, zone } from '@/index.js' +import { client } from '@/controller/cloudflare.js' +import { records, zone } from '@/index.js' import { PageTypes } from '@/types/page.js' import { ListChoiceOptions } from 'inquirer' diff --git a/src/pages/dns/edit.ts b/src/pages/dns/edit.ts index 819bef0..21df925 100755 --- a/src/pages/dns/edit.ts +++ b/src/pages/dns/edit.ts @@ -1,6 +1,7 @@ import { Page } from '@/class/pages.js' import { Questions } from '@/class/questions.js' -import { client, records, zone } from '@/index.js' +import { client } from '@/controller/cloudflare.js' +import { records, zone } from '@/index.js' import { PageTypes } from '@/types/page.js' import { ListChoiceOptions } from 'inquirer' diff --git a/src/pages/dns/search.ts b/src/pages/dns/search.ts index 6366690..1e66869 100755 --- a/src/pages/dns/search.ts +++ b/src/pages/dns/search.ts @@ -1,6 +1,7 @@ import { Page } from '@/class/pages.js' import { Questions } from '@/class/questions.js' -import { client, records, zone } from '@/index.js' +import { client } from '@/controller/cloudflare.js' +import { records, zone } from '@/index.js' import { PageTypes } from '@/types/page.js' import { ListChoiceOptions } from 'inquirer' diff --git a/src/pages/zones.ts b/src/pages/zones.ts index ae65f90..2f36f4f 100755 --- a/src/pages/zones.ts +++ b/src/pages/zones.ts @@ -1,6 +1,7 @@ import { Page } from '@/class/pages.js' import { Questions } from '@/class/questions.js' -import { client, zone, zones } from '@/index.js' +import { client } from '@/controller/cloudflare.js' +import { zone, zones } from '@/index.js' import { PageTypes } from '@/types/page.js' import { ListChoiceOptions } from 'inquirer' diff --git a/src/types/crypt.ts b/src/types/crypt.ts new file mode 100644 index 0000000..0877d4f --- /dev/null +++ b/src/types/crypt.ts @@ -0,0 +1,5 @@ +export interface DataCrypted { + email: string + token: string + language: string +} \ No newline at end of file