diff --git a/src/class/AccountUtil.ts b/src/class/AccountUtil.ts index cac764a..339f48d 100644 --- a/src/class/AccountUtil.ts +++ b/src/class/AccountUtil.ts @@ -1,3 +1,4 @@ +import { randomBytes } from 'crypto'; import { AccountInterface } from '../models'; import { Client } from '..'; @@ -22,20 +23,22 @@ export default class AccountUtil { let passHash = await this.client.util.createHash(tempPass); passHash = passHash.replace(/[$]/g, '\\$').replace('\n', ''); const acctName = this.client.users.get(data.userID).username.replace(/[!@#$%^&*(),.?":{}|<>]/g, '-').replace(/\s/g, '-'); const etcPasswd = `${acctName},${data.userID},,`; + const code = randomBytes(3).toString('hex').toUpperCase(); - const accountInterface = await this.client.util.createAccount(passHash, etcPasswd, data.username, data.userID, data.emailAddress, moderator); + const accountInterface = await this.client.util.createAccount(passHash, etcPasswd, data.username, data.userID, data.emailAddress, moderator, code); await this.client.util.createModerationLog(data.userID, moderatorMember, 0); this.client.util.transport.sendMail({ to: data.emailAddress, from: 'Library of Code sp-us | Cloud Services ', - subject: 'Your account has been created', + subject: 'Your account has been created!', html: `

Library of Code | Cloud Services

Your Cloud Account has been created, welcome! Please see below for some details regarding your account and our services

Username: ${data.username}

+

Support Key: ${code} || You may be asked for this support key when contacting Library of Code, please keep the code in a safe area.

SSH Login:

ssh ${data.username}@cloud.libraryofcode.org

Useful information

How to log in:

@@ -61,13 +64,15 @@ export default class AccountUtil { `, }); + this.client.guilds.get('446067825673633794').members.get(data.userID).addRole('546457886440685578'); const dmChannel = await this.client.getDMChannel(data.userID).catch(); dmChannel.createMessage('<:loc:607695848612167700> **Thank you for creating an account with us!** <:loc:607695848612167700>\n' + `Please log into your account by running \`ssh ${data.username}@cloud.libraryofcode.org\` in your terminal, then use the password \`${tempPass}\` to log in.\n` + `You will be asked to change your password, \`(current) UNIX password\` is \`${tempPass}\`, then create a password that is at least 12 characters long, with at least one number, special character, and an uppercase letter\n` + 'Bear in mind that when you enter your password, it will be blank, so be careful not to type in your password incorrectly.\n' + 'You may now return to Modmail, and continue setting up your account from there.\n\n' - + 'An email containing some useful information has also been sent').catch(); + + 'An email containing some useful information has also been sent.\n' + + `Your support key is \`${code}\`. Pin this message, you may need this key to contact Library of Code in the future.`).catch(); return { account: accountInterface, tempPass }; } } diff --git a/src/class/Util.ts b/src/class/Util.ts index 28cb962..fb093d2 100644 --- a/src/class/Util.ts +++ b/src/class/Util.ts @@ -147,12 +147,12 @@ export default class Util { return tempPass; } - public async createAccount(hash: string, etcPasswd: string, username: string, userID: string, emailAddress: string, moderatorID: string): Promise { + public async createAccount(hash: string, etcPasswd: string, username: string, userID: string, emailAddress: string, moderatorID: string, code: string): Promise { await this.exec(`useradd -m -p ${hash} -c ${etcPasswd} -s /bin/bash ${username}`); await this.exec(`chage -d0 ${username}`); const account = new this.client.db.Account({ - username, userID, emailAddress, createdBy: moderatorID, createdAt: new Date(), locked: false, tier: 1, ssInit: false, homepath: `/home/${username}`, + username, userID, emailAddress, createdBy: moderatorID, createdAt: new Date(), locked: false, tier: 1, supportKey: code, ssInit: false, homepath: `/home/${username}`, }); return account.save(); } diff --git a/src/commands/createaccount.ts b/src/commands/createaccount.ts index c345066..15c04b5 100644 --- a/src/commands/createaccount.ts +++ b/src/commands/createaccount.ts @@ -24,17 +24,17 @@ export default class CreateAccount extends Command { try { if (message.channel instanceof PrivateChannel || message.channel instanceof GroupChannel) return message; // Stop TS being gay if (!args[2]) return this.client.commands.get('help').run(message, [this.name]); - if (!message.channel.guild.members.has(args[0])) return message.channel.createMessage(`${this.client.stores.emojis.error} ***User not found***`); - if (message.channel.guild.members.get(args[0]).bot) return message.channel.createMessage(`${this.client.stores.emojis.error} ***I cannot create accounts for bots***`); + if (!message.channel.guild.members.has(args[0])) return message.channel.createMessage(`${this.client.stores.emojis.error} ***User not found.***`); + if (message.channel.guild.members.get(args[0]).bot) return message.channel.createMessage(`${this.client.stores.emojis.error} ***I cannot create accounts for bots.***`); const checkUser = await this.client.db.Account.findOne({ userID: args[0] }); - if (checkUser) return message.channel.createMessage(`${this.client.stores.emojis.error} ***<@${args[0]}> already has an account***`); + if (checkUser) return message.channel.createMessage(`${this.client.stores.emojis.error} ***<@${args[0]}> already has an account.***`); const checkEmail = await this.client.db.Account.findOne({ emailAddress: args[1] }); - if (checkEmail) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account already exists with this email address***`); + if (checkEmail) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account already exists with this email address.***`); const checkAccount = await this.client.db.Account.findOne({ username: args[2] }); - if (checkAccount) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account already exists with this username***`); + if (checkAccount) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account already exists with this username.***`); - if (!this.client.util.isValidEmail(args[1])) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid email address supplied***`); - if (!/^[a-z][-a-z0-9]*$/.test(args[2])) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid username supplied***`); + if (!this.client.util.isValidEmail(args[1])) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid email address supplied.***`); + if (!/^[a-z][-a-z0-9]*$/.test(args[2])) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid username supplied.***`); const confirmation = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Creating account...***`); const data = await this.client.util.accounts.createAccount({ userID: args[0], username: args[2], emailAddress: args[1] }, message.author.id); diff --git a/src/commands/emailcode.ts b/src/commands/emailcode.ts new file mode 100644 index 0000000..ad203c1 --- /dev/null +++ b/src/commands/emailcode.ts @@ -0,0 +1,43 @@ +/* eslint-disable consistent-return */ +import { randomBytes } from 'crypto'; +import { Message } from 'eris'; +import { Client } from '..'; +import { Command, RichEmbed } from '../class'; + +export default class EmailCode extends Command { + constructor(client: Client) { + super(client); + this.name = 'emailcode'; + this.description = 'Sends a code to an email address to use for address verification.'; + this.usage = `${this.client.config.prefix}emailcode `; + this.permissions = { roles: ['446104438969466890'] }; + this.aliases = ['code']; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { + try { + const code = randomBytes(5).toString('hex'); + if (!this.client.util.isValidEmail(args[0])) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid email address supplied.***`); + this.client.util.transport.sendMail({ + to: args[0], + from: 'Library of Code sp-us | Cloud Services ', + subject: 'Email Verification Code', + html: ` + + +

Library of Code | Cloud Services

+

Please provide the code provided below to the Staff member working with you on account creation.

+

${code}

+

Want to support us?

+

You can support us on Patreon! Head to our Patreon page and feel free to donate as much or as little as you want!
Donating $5 or more will grant you Tier 3, which means we will manage your account at your request, longer certificates, increased Tier limits as well as some roles in the server!

+ Library of Code sp-us | Support Team + + `, + }); + message.channel.createMessage(`***${this.client.stores.emojis.success} Code: \`${code}\` | Email Address: ${args[0]}***`); + } catch (error) { + await this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/index.ts b/src/commands/index.ts index f9d20aa..0abb76d 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -4,6 +4,7 @@ export { default as createaccount } from './createaccount'; export { default as cwg } from './cwg'; export { default as deleteaccount } from './deleteaccount'; export { default as disk } from './disk'; +export { default as emailcode } from './emailcode'; export { default as eval } from './eval'; export { default as exec } from './exec'; export { default as help } from './help'; @@ -19,6 +20,7 @@ export { default as resetpassword } from './resetpassword'; export { default as restart } from './restart'; export { default as securesign } from './securesign'; export { default as sysinfo } from './sysinfo'; +export { default as tier } from './tier'; export { default as unban } from './unban'; export { default as unlock } from './unlock'; export { default as warn } from './warn'; diff --git a/src/commands/tier.ts b/src/commands/tier.ts new file mode 100644 index 0000000..a86d2e6 --- /dev/null +++ b/src/commands/tier.ts @@ -0,0 +1,41 @@ +/* eslint-disable consistent-return */ +import { Message } from 'eris'; +import { Client } from '..'; +import { Command, RichEmbed } from '../class'; + +export default class Tier extends Command { + constructor(client: Client) { + super(client); + this.name = 'tier'; + this.description = 'Changes the tier level for an account.'; + this.usage = `${this.client.config.prefix}tier <1 | 2 | 3>`; + this.permissions = { roles: ['446104438969466890'] }; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { + try { + if (!args.length) return this.client.commands.get('help').run(message, [this.name]); + const edit = await message.channel.createMessage(`***${this.client.stores.emojis.loading} Editing tier...***`); + const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0].replace(/[<@!>]/gi, '') }] }); + if (!account) return edit.edit(`***${this.client.stores.emojis.error} Cannot find user.***`); + if (account.root) return edit.edit(`***${this.client.stores.emojis.error} Permission denied.***`); + if (Number.isNaN(Number(args[1]))) return edit.edit(`***${this.client.stores.emojis.error} The tier you provided is not a valid number. It should be between 1 and 3.***`); + if (Number(args[1]) > 3 || Number(args[1]) < 1) return edit.edit(`***${this.client.stores.emojis.error} You can only choose a Tier between 1 and 3.***`); + message.delete(); + await account.updateOne({ $set: { tier: Number(args[1]) } }); + message.channel.createMessage(`***${this.client.stores.emojis.success} Tier for ${account.username} has been changed to ${args[1]}.***`); + const embed = new RichEmbed(); + embed.setTitle('Cloud Account | Tier Change'); + embed.setColor('#0099ff'); + embed.addField('User', `${account.username} | <@${account.userID}>`, true); + embed.addField('Moderator', `<@${message.author.id}>`, true); + embed.addField('Old Tier -> New Tier', `${account.tier} -> ${args[1]}`, true); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + this.client.createMessage('580950455581147146', { embed }); this.client.getDMChannel(account.userID).then((channel) => channel.createMessage({ embed })).catch(); + } catch (error) { + await this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/whois.ts b/src/commands/whois.ts index 38109bf..48465a7 100644 --- a/src/commands/whois.ts +++ b/src/commands/whois.ts @@ -39,6 +39,7 @@ export default class Whois extends Command { embed.addField('ID', account.userID, true); embed.addField('Email Address', account.emailAddress, true); embed.addField('Tier', String(account.tier), true); + embed.addField('Support Key', account.supportKey, true); embed.addField('Created By', `<@${this.client.users.get(account.createdBy).id}>`, true); embed.addField('Created At', moment(account.createdAt).format('dddd, MMMM Do YYYY, h:mm:ss A'), true); const cpuUsage = await this.client.util.exec(`top -b -n 1 -u ${account.username} | awk 'NR>7 { sum += $9; } END { print sum; }'`); diff --git a/src/intervals/checkStaffStatus.ts b/src/intervals/checkStaffStatus.ts index bfff788..0d318de 100644 --- a/src/intervals/checkStaffStatus.ts +++ b/src/intervals/checkStaffStatus.ts @@ -1,5 +1,6 @@ /* eslint-disable no-await-in-loop */ import { Client } from '..'; +import { RichEmbed } from '../class'; export default function checkStaffStatus(client: Client) { setInterval(async () => { @@ -26,8 +27,17 @@ export default function checkStaffStatus(client: Client) { await client.db.Account.updateOne({ username: acc.username }, { $set: { 'permissions.staff': false } }); } - if (acc.permissions.staff && acc.tier > 3) { + if (acc.permissions.staff && acc.tier < 3) { await client.db.Account.updateOne({ username: acc.username }, { $set: { tier: 3 } }); + const embed = new RichEmbed(); + embed.setTitle('Cloud Account | Tier Change'); + embed.setColor('#0099ff'); + embed.addField('User', `${acc.username} | <@${acc.userID}>`, true); + embed.addField('Moderator', `<@${client.user.id}>`, true); + embed.addField('Old Tier -> New Tier', `${acc.tier} -> 3`, true); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + this.client.createMessage('580950455581147146', { embed }); client.getDMChannel(acc.userID).then((chan) => { chan.createMessage('***Your account has automatically been upgraded to Tier 3 since you are a Staff member.***'); }); diff --git a/src/intervals/memory.ts b/src/intervals/memory.ts index 87c5d60..b61451f 100644 --- a/src/intervals/memory.ts +++ b/src/intervals/memory.ts @@ -1,10 +1,11 @@ +/* eslint-disable no-continue */ /* eslint-disable no-await-in-loop */ import { Client } from '..'; import { RichEmbed } from '../class'; const channelID = '691824484230889546'; -export const memoryLimits = { +const memoryLimits = { TIER_1_SOFT: 200, TIER_1_HARD: 350, TIER_2_SOFT: 350, @@ -15,61 +16,76 @@ export const memoryLimits = { export default function memory(client: Client) { setInterval(async () => { - const accounts = await client.db.Account.find(); - for (const acc of accounts) { - if (acc.root) return; - // memory in bytes - const mem = Number(await client.util.exec(`memory ${acc.username}`)) * 1000; - // memory in megabytes - const memoryConversion = mem / 1024 / 1024; - let userLimits: { soft: number, hard: number }; - if (acc.tier === 1) { - userLimits = { soft: memoryLimits.TIER_1_SOFT, hard: memoryLimits.TIER_1_HARD }; - } else if (acc.tier === 2) { - userLimits = { soft: memoryLimits.TIER_2_SOFT, hard: memoryLimits.TIER_2_HARD }; - } else if (acc.tier === 3) { - userLimits = { soft: memoryLimits.TIER_3_SOFT, hard: memoryLimits.TIER_3_HARD }; - } - - /* if the user has exceeded their soft memory limit, which is the one described in the - resource limit guidelines, we'll inform staff. - */ - if (acc.tier === 1 && memoryConversion >= userLimits.soft) { - const embed = new RichEmbed(); - if (client.users.get(acc.userID)) embed.setThumbnail(client.users.get(acc.userID).avatarURL); - embed.addField('User', `${acc.username} | <@${acc.userID}> | ${acc.userID}`, true); - embed.addField('Tier', String(acc.tier), true); - embed.addField('Memory Usage', `${String(memoryConversion)} MB`, true); - embed.addField('Memory Limit', `${String(userLimits.soft)} MB`, true); - embed.setFooter(client.user.username, client.user.avatarURL); - embed.setTimestamp(); - - // if they exceed the hard limit, we'll kill all of their processes. - if (memoryConversion >= userLimits.hard) { - client.util.exec(`killall -9 -u ${acc.username}`); - embed.setTitle('Resource Enforcement Notification'); - embed.setDescription('Someone has reached the (hard) resource limit for their tier on RAM. The system has automatically killed all of their processes.'); - client.util.createModerationLog(acc.userID, client.guilds.get('446067825673633794').members.get(client.user.id), 1, '[AUTO] Exceeded resource limit for RAM.'); - client.util.transport.sendMail({ - to: acc.emailAddress, - from: 'Library of Code sp-us | Cloud Services ', - subject: 'Your account has been warned', - html: ` -

Library of Code sp-us | Cloud Services

-

Your account has received an official warning from a Moderator. Please get the underlying issue resolved to avoid possible moderative action.

-

Reason: [AUTO] Exceeded resource limit for RAM.

-

Moderator: ${client.user.username}

- - Library of Code sp-us | Support Team - `, - }); - } else { - embed.setTitle('Resource Limit Notification'); - embed.setDescription('Someone has reached the (soft) resource limit for their tier on RAM.'); + try { + const accounts = await client.db.Account.find(); + for (const acc of accounts) { + if (acc.root === true) continue; + // memory in bytes + const mem = Number(await client.util.exec(`memory ${acc.username}`)) * 1000; + // memory in megabytes + const memoryConversion = mem / 1024 / 1024; + const userLimits: { soft?: number, hard?: number } = {}; + switch (acc.tier) { + case 1: + userLimits.soft = memoryLimits.TIER_1_SOFT; + userLimits.hard = memoryLimits.TIER_1_HARD; + break; + case 2: + userLimits.soft = memoryLimits.TIER_2_SOFT; + userLimits.hard = memoryLimits.TIER_2_HARD; + break; + case 3: + userLimits.soft = memoryLimits.TIER_3_SOFT; + userLimits.hard = memoryLimits.TIER_3_HARD; + break; + default: + break; + } + + /* if the user has exceeded their soft memory limit, which is the one described in the + resource limit guidelines, we'll inform staff. + */ + if (memoryConversion >= userLimits.soft) { + client.signale.info(`RAM Soft Limit Reached | ${acc.username} | ${memoryConversion}/${userLimits.soft} MB`); + const embed = new RichEmbed(); + if (client.users.get(acc.userID)) embed.setThumbnail(client.users.get(acc.userID).avatarURL); + embed.addField('User', `${acc.username} | <@${acc.userID}> | ${acc.userID}`, true); + embed.addField('Tier', String(acc.tier), true); + embed.addField('Memory Usage', `${String(memoryConversion)} MB`, true); + embed.addField('Memory Limit', `${String(userLimits.soft)} MB`, true); + embed.setFooter(client.user.username, client.user.avatarURL); + embed.setTimestamp(); + + // if they exceed the hard limit, we'll kill all of their processes. + if (memoryConversion >= userLimits.hard) { + client.signale.info(`RAM Hard Limit Reached | ${acc.username} | ${memoryConversion}/${userLimits.hard} MB`); + client.util.exec(`killall -9 -u ${acc.username}`); + embed.setTitle('Resource Enforcement Notification'); + embed.setDescription('Someone has reached the (hard) resource limit for their tier on RAM. The system has automatically killed all of their processes.'); + client.util.createModerationLog(acc.userID, client.guilds.get('446067825673633794').members.get(client.user.id), 1, '[AUTO] Exceeded resource limit for RAM.'); + client.util.transport.sendMail({ + to: acc.emailAddress, + from: 'Library of Code sp-us | Cloud Services ', + subject: 'Your account has been warned', + html: ` +

Library of Code sp-us | Cloud Services

+

Your account has received an official warning from a Moderator. Please get the underlying issue resolved to avoid possible moderative action.

+

Reason: [AUTO] Exceeded resource limit for RAM.

+

Moderator: ${client.user.username}

+ + Library of Code sp-us | Support Team + `, + }); + } else { + embed.setTitle('Resource Limit Notification'); + embed.setDescription('Someone has reached the (soft) resource limit for their tier on RAM.'); + } + // @ts-ignore + client.createMessage(channelID, { embed }); } - // @ts-ignore - client.createMessage(channelID, { embed }); } + } catch (err) { + client.util.handleError(err); } - }, 300000); + }, 60000); } diff --git a/src/models/Account.ts b/src/models/Account.ts index 3f10dc2..7ba490b 100644 --- a/src/models/Account.ts +++ b/src/models/Account.ts @@ -8,7 +8,8 @@ export interface AccountInterface extends Document { createdBy: string, createdAt: Date, locked: boolean, - tier: number; + tier: number, + supportKey: string, permissions: { staff: boolean, sheriff: boolean, @@ -29,6 +30,7 @@ const Account: Schema = new Schema({ createdAt: Date, locked: Boolean, tier: Number, + supportKey: String, permissions: { staff: Boolean, sheriff: Boolean, diff --git a/src/static/nginx.conf b/src/static/nginx.conf index 327a416..4312ac5 100644 --- a/src/static/nginx.conf +++ b/src/static/nginx.conf @@ -24,19 +24,19 @@ server { limit_req zone=one burst=15; location / { - proxy_set_header Host $host; + proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $scheme; - proxy_pass http://localhost:[PORT]; + proxy_pass http://localhost:[PORT]; - proxy_read_timeout 90; + proxy_read_timeout 90; - proxy_redirect http://localhost:[PORT] https://[DOMAIN]; + proxy_redirect http://localhost:[PORT] https://[DOMAIN]; } } diff --git a/tsconfig.json b/tsconfig.json index 10493bf..febdc18 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "target": "ES2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "lib": ["ES2019.Object", "ES2020.Promise"], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */