diff --git a/.eslintrc.json b/.eslintrc.json index af3e032..5fe8e4e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -39,6 +39,7 @@ "import/prefer-default-export": "off", "no-useless-constructor": "off", "@typescript-eslint/no-useless-constructor": 2, - "import/extensions": "off" + "import/extensions": "off", + "max-classes-per-file": "off" } } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index eb10a0b..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "eslint.enable": true, - "eslint.validate": [ - { - "language": "typescript", - "autoFix": true - } - ], - "editor.tabSize": 2 -} \ No newline at end of file diff --git a/package.json b/package.json index f6707f3..ac500ba 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "mongoose": "^5.7.4", "nodemailer": "^6.3.1", "signale": "^1.4.0", + "@typegoose/typegoose": "^7.6.2", "uuid": "^3.3.3", "x509": "bsian03/node-x509" }, diff --git a/src/api/interfaces.ts b/src/api/interfaces.ts index 411ad80..36dfade 100644 --- a/src/api/interfaces.ts +++ b/src/api/interfaces.ts @@ -1,6 +1,6 @@ import express from 'express'; -import { AccountInterface } from '../models'; +import { Account } from '../models'; export interface Req extends express.Request { - account: AccountInterface + account: Account } diff --git a/src/class/AccountUtil.ts b/src/class/AccountUtil.ts index cb4d7c2..00a760f 100644 --- a/src/class/AccountUtil.ts +++ b/src/class/AccountUtil.ts @@ -1,7 +1,6 @@ import axios from 'axios'; import moment from 'moment'; import { randomBytes } from 'crypto'; -import { AccountInterface } from '../models'; import { Client } from '..'; export default class AccountUtil { @@ -19,7 +18,7 @@ export default class AccountUtil { * @param data.emailAddress The user's email address. * @param moderator The Discord user ID for the Staff member that created the account. */ - public async createAccount(data: { userID: string, username: string, emailAddress: string }, moderator: string): Promise<{ account: AccountInterface, tempPass: string }> { + public async createAccount(data: { userID: string, username: string, emailAddress: string }, moderator: string) { const moderatorMember = this.client.guilds.get('446067825673633794').members.get(moderator); const tempPass = this.client.util.randomPassword(); let passHash = await this.client.util.createHash(tempPass); passHash = passHash.replace(/[$]/g, '\\$').replace('\n', ''); @@ -74,15 +73,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\n' - + '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(); + + `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\n' + + '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 }; } - public async lock(username: string, moderatorID: string, data?: { reason?: string, time?: number}) { + public async lock(username: string, moderatorID: string, data?: { reason?: string, time?: number }) { const account = await this.client.db.Account.findOne({ username }); if (!account) throw new Error('Account does not exist.'); if (account.locked) throw new Error('Account is already locked.'); diff --git a/src/class/Client.ts b/src/class/Client.ts index 4f2b4fd..711b337 100644 --- a/src/class/Client.ts +++ b/src/class/Client.ts @@ -3,10 +3,11 @@ import Redis from 'ioredis'; import mongoose from 'mongoose'; import signale from 'signale'; import fs from 'fs-extra'; +import { getModelForClass } from '@typegoose/typegoose'; import config from '../config.json'; -import { Account, AccountInterface, Moderation, ModerationInterface, Domain, DomainInterface, Tier, TierInterface } from '../models'; +import { Account, Moderation, Domain, Tier } from '../models'; import { emojis } from '../stores'; -import { Command, CSCLI, Util, Collection, Server, Event } from '.'; +import { Command, Util, Collection, Server, Event } from '.'; export default class Client extends Eris.Client { @@ -18,7 +19,12 @@ export default class Client extends Eris.Client { public events: Collection; - public db: { Account: mongoose.Model; Domain: mongoose.Model; Moderation: mongoose.Model; Tier: mongoose.Model; }; + public db = { + Account: getModelForClass(Account), + Domain: getModelForClass(Domain), + Moderation: getModelForClass(Moderation), + Tier: getModelForClass(Tier), + } public redis: Redis.Redis; @@ -43,7 +49,6 @@ export default class Client extends Eris.Client { this.commands = new Collection(); this.events = new Collection(); this.functions = new Collection(); - this.db = { Account, Domain, Moderation, Tier }; this.redis = new Redis(); this.stores = { emojis }; this.signale = signale; diff --git a/src/class/Security.ts b/src/class/Security.ts index a59f9a2..59dfbc2 100644 --- a/src/class/Security.ts +++ b/src/class/Security.ts @@ -2,7 +2,6 @@ import jwt from 'jsonwebtoken'; import { Request } from 'express'; import { Client } from '.'; -import { AccountInterface } from '../models'; export default class Security { public client: Client; @@ -36,7 +35,7 @@ export default class Security { * If the bearer token is valid, will return the Account, else will return null. * @param bearer The bearer token provided. */ - public async checkBearer(bearer: string): Promise { + public async checkBearer(bearer: string) { try { const res: any = jwt.verify(bearer, this.keys.key, { issuer: 'Library of Code sp-us | CSD' }); const account = await this.client.db.Account.findOne({ _id: res.id }); diff --git a/src/class/Util.ts b/src/class/Util.ts index d732137..5d5d0a4 100644 --- a/src/class/Util.ts +++ b/src/class/Util.ts @@ -10,7 +10,6 @@ import moment from 'moment'; import fs from 'fs'; import { getUserByUid } from '../functions'; import { AccountUtil, Client, Command, RichEmbed } from '.'; -import { ModerationInterface, AccountInterface, Account } from '../models'; export default class Util { public client: Client; @@ -36,7 +35,7 @@ export default class Util { public async exec(command: string, options: childProcess.ExecOptions = {}): Promise { return new Promise((res, rej) => { let output = ''; - const writeFunction = (data: string|Buffer|Error) => { + const writeFunction = (data: string | Buffer | Error) => { output += `${data}`; }; const cmd = childProcess.exec(command, options); @@ -47,7 +46,7 @@ export default class Util { cmd.stdout.off('data', writeFunction); cmd.stderr.off('data', writeFunction); cmd.off('error', writeFunction); - setTimeout(() => {}, 1000); + setTimeout(() => { }, 1000); if (code !== 0) rej(new Error(`Command failed: ${command}\n${output}`)); res(output); }); @@ -99,7 +98,7 @@ export default class Util { * @param query Command input * @param message Only used to check for errors */ - public resolveCommand(query: string | string[], message?: Message): Promise<{cmd: Command, args: string[] }> { + public resolveCommand(query: string | string[], message?: Message): Promise<{ cmd: Command, args: string[] }> { try { let resolvedCommand: Command; if (typeof query === 'string') query = query.split(' '); @@ -153,7 +152,7 @@ export default class Util { public splitFields(fields: { name: string, value: string, inline?: boolean }[]): { name: string, value: string, inline?: boolean }[][] { let index = 0; - const array: {name: string, value: string, inline?: boolean}[][] = [[]]; + const array: { name: string, value: string, inline?: boolean }[][] = [[]]; while (fields.length) { if (array[index].length >= 25) { index += 1; array[index] = []; } array[index].push(fields[0]); fields.shift(); @@ -198,7 +197,7 @@ export default class Util { return tempPass; } - public async createAccount(hash: string, etcPasswd: string, username: string, userID: string, emailAddress: string, moderatorID: string, code: string): Promise { + public async createAccount(hash: string, etcPasswd: string, username: string, userID: string, emailAddress: string, moderatorID: string, code: string) { await this.exec(`useradd -m -p ${hash} -c ${etcPasswd} -s /bin/bash ${username}`); await this.exec(`chage -d0 ${username}`); const tier = await this.client.db.Tier.findOne({ id: 1 }); @@ -222,7 +221,7 @@ export default class Util { await Promise.all(tasks); } - public async messageCollector(message: Message, question: string, timeout: number, shouldDelete = false, choices: string[] = null, filter = (msg: Message): boolean|void => {}): Promise { + public async messageCollector(message: Message, question: string, timeout: number, shouldDelete = false, choices: string[] = null, filter = (msg: Message): boolean | void => { }): Promise { const msg = await message.channel.createMessage(question); return new Promise((res, rej) => { const func = (Msg: Message) => { @@ -250,12 +249,12 @@ export default class Util { * * `4` - Delete */ - public async createModerationLog(user: string, moderator: Member|User, type: number, reason?: string, duration?: number): Promise { + public async createModerationLog(user: string, moderator: Member | User, type: number, reason?: string, duration?: number) { const moderatorID = moderator.id; const account = await this.client.db.Account.findOne({ $or: [{ username: user }, { userID: user }] }); if (!account) return Promise.reject(new Error(`Account ${user} not found`)); const { username, userID } = account; - const logInput: { username: string, userID: string, logID: string, moderatorID: string, reason?: string, type: number, date: Date, expiration?: { date: Date, processed: boolean }} = { + const logInput: { username: string, userID: string, logID: string, moderatorID: string, reason?: string, type: number, date: Date, expiration?: { date: Date, processed: boolean } } = { username, userID, logID: uuid(), moderatorID, type, date: new Date(), }; diff --git a/src/commands/cwg_create.ts b/src/commands/cwg_create.ts index 39209b3..2ded99a 100644 --- a/src/commands/cwg_create.ts +++ b/src/commands/cwg_create.ts @@ -2,7 +2,7 @@ import fs, { writeFile, unlink } from 'fs-extra'; import axios from 'axios'; import { randomBytes } from 'crypto'; import { Message } from 'eris'; -import { AccountInterface } from '../models'; +import { Account } from '../models'; import { Client, Command, RichEmbed } from '../class'; import { parseCertificate } from '../functions'; @@ -144,7 +144,7 @@ export default class CWG_Create extends Command { * @param x509Certificate The contents the certificate and key files. * @example await CWG.createDomain(account, 'mydomain.cloud.libraryofcode.org', 6781); */ - public async createDomain(account: AccountInterface, domain: string, port: number, x509Certificate: { cert?: string, key?: string }) { + public async createDomain(account: Account, domain: string, port: number, x509Certificate: { cert?: string, key?: string }) { try { if (port <= 1024 || port >= 65535) throw new RangeError(`Port range must be between 1024 and 65535, received ${port}.`); if (await this.client.db.Domain.exists({ domain })) throw new Error(`Domain ${domain} already exists in the database.`); diff --git a/src/commands/whois.ts b/src/commands/whois.ts index 30fc824..71d1b0e 100644 --- a/src/commands/whois.ts +++ b/src/commands/whois.ts @@ -1,8 +1,8 @@ import moment from 'moment'; import { Message, GuildTextableChannel, Member, Role } from 'eris'; -import { Client, Command, Report, RichEmbed } from '../class'; +import { Client, Command, RichEmbed } from '../class'; import { dataConversion } from '../functions'; -import { AccountInterface } from '../models'; +import { Account } from '../models'; export default class Whois extends Command { constructor(client: Client) { @@ -21,7 +21,7 @@ export default class Whois extends Command { public async run(message: Message, args: string[]) { try { let full = false; - let account: AccountInterface; + let account: Account; if (args[1] === '--full' && this.fullRoles.some((r) => message.member.roles.includes(r) || message.author.id === '554168666938277889')) full = true; const user = args[0] || message.author.id; @@ -68,7 +68,7 @@ export default class Whois extends Command { } } - public async full(account: AccountInterface, embed: RichEmbed, member: Member) { + public async full(account: Account, embed: RichEmbed, member: Member) { const [cpuUsage, data, fingerInformation, chage, memory] = await Promise.all([ this.client.util.exec(`top -b -n 1 -u ${account.username} | awk 'NR>7 { sum += $9; } END { print sum; }'`), this.client.redis.get(`storage-${account.username}`), @@ -92,7 +92,7 @@ export default class Whois extends Command { embed.addField('Storage', data ? dataConversion(Number(data)) : 'N/A', true); } - public async default(account: AccountInterface, embed: RichEmbed) { + public async default(account: Account, embed: RichEmbed) { const [cpuUsage, data, memory] = await Promise.all([ this.client.util.exec(`top -b -n 1 -u ${account.username} | awk 'NR>7 { sum += $9; } END { print sum; }'`), this.client.redis.get(`storage-${account.username}`), diff --git a/src/intervals/memory.ts b/src/intervals/memory.ts index 92ba296..66bff03 100644 --- a/src/intervals/memory.ts +++ b/src/intervals/memory.ts @@ -2,7 +2,6 @@ /* eslint-disable no-continue */ /* eslint-disable no-await-in-loop */ import { Client, RichEmbed } from '../class'; -import { Tiers } from '../models'; const channelID = '691824484230889546'; @@ -18,8 +17,7 @@ export default function memory(client: Client) { // memory in megabytes const memoryConversion = mem / 1024 / 1024; const userLimits: { soft?: number, hard?: number } = {}; - // @ts-ignore - const tier: Tiers = await client.db.Tier.findOne({ id: acc.tier }).lean().exec(); + const tier = await client.db.Tier.findOne({ id: acc.tier }).lean().exec(); userLimits.soft = acc.ramLimitNotification; userLimits.hard = tier.resourceLimits.ram; if ((memoryConversion <= userLimits.soft) && (acc.ramLimitNotification !== 0)) { diff --git a/src/models/Account.ts b/src/models/Account.ts index 49e83ee..fb8439b 100644 --- a/src/models/Account.ts +++ b/src/models/Account.ts @@ -1,53 +1,72 @@ -import { Document, Schema, model } from 'mongoose'; +import { modelOptions, prop } from '@typegoose/typegoose'; +import { Base } from '@typegoose/typegoose/lib/defaultClasses'; -export interface AccountInterface extends Document { - username: string, - userID: string, - homepath: string, - emailAddress: string, - createdBy: string, - createdAt: Date, - locked: boolean, - tier: number, - supportKey: string, - referralCode: string, - totalReferrals: number, - permissions: { - staff: boolean, - technician: boolean, - director: boolean, - }, - ramLimitNotification: number, - root: boolean, - hash: boolean, - salt: string, - authTag: Buffer - revokedBearers: string[], +export type Tier = 1 | 2 | 3; + +class Permissions { + @prop() + staff?: boolean; + + @prop() + technician?: boolean; + + @prop() + director?: boolean; } -const Account = new Schema({ - username: String, - userID: String, - homepath: String, - emailAddress: String, - createdBy: String, - createdAt: Date, - locked: Boolean, - tier: Number, - supportKey: String, - referralCode: String, - totalReferrals: Number, - permissions: { - staff: Boolean, - technician: Boolean, - director: Boolean, - }, - ramLimitNotification: Number, - root: Boolean, - hash: Boolean, - salt: String, - authTag: Buffer, - revokedBearers: Array, -}); +@modelOptions({ schemaOptions: { collection: 'Account' } }) +export default class Account extends Base { + @prop({ required: true, unique: true }) + username: string; -export default model('Account', Account); + @prop({ required: true, unique: true }) + userID: string; + + @prop({ required: true, unique: true }) + homepath: string; + + @prop({ required: true }) + emailAddress: string; + + @prop({ required: true }) + createdBy: string; + + @prop({ required: true }) + createdAt: Date; + + @prop({ required: true, default: false }) + locked: boolean; + + @prop({ required: true, default: 1 }) + tier: Tier; + + @prop({ required: true }) + supportKey: string; + + @prop({ required: true, unique: true }) + referralCode: string; + + @prop({ required: true, default: 0 }) + totalReferrals: number; + + @prop() + permissions?: Permissions; + + @prop() + ramLimitNotification: number; + + @prop() + root: boolean; + + @prop() + hash: boolean; + + @prop() + salt: string; + + @prop() + authTag: Buffer; + + @prop({ type: () => String }) + revokedBearers: string[]; +} diff --git a/src/models/Domain.ts b/src/models/Domain.ts index de6add9..4a220f4 100644 --- a/src/models/Domain.ts +++ b/src/models/Domain.ts @@ -1,24 +1,28 @@ -import { Document, Schema, model } from 'mongoose'; -import { AccountInterface } from './Account'; +import { modelOptions, prop } from '@typegoose/typegoose'; +import { Account } from '.'; -export interface DomainInterface extends Document { - account: AccountInterface, - domain: string, - port: number, - // Below is the full absolute path to the location of the x509 certificate and key files. - x509: { - cert: string, - key: string - }, - enabled: true +class X509 { + @prop({ required: true }) + cert: string; + + @prop({ required: true }) + key: string; } -const Domain = new Schema({ - account: Object, - domain: String, - port: Number, - x509: { cert: String, key: String }, - enabled: Boolean, -}); +@modelOptions({ schemaOptions: { collection: 'Domain' } }) +export default class Domain { + @prop({ type: () => Account, required: true }) + account: Account; -export default model('Domain', Domain); + @prop({ required: true, unique: true }) + domain: string; + + @prop({ required: true }) + port: number; + + @prop({ required: true, type: () => X509 }) + x509: X509; + + @prop({ required: true }) + enabled: boolean; +} diff --git a/src/models/Moderation.ts b/src/models/Moderation.ts index 43d5a3c..79cfdc1 100644 --- a/src/models/Moderation.ts +++ b/src/models/Moderation.ts @@ -1,38 +1,45 @@ -import { Document, Schema, model } from 'mongoose'; +import { modelOptions, prop } from '@typegoose/typegoose'; -export interface ModerationInterface extends Document { - username: string, - userID: string, - logID: string, - moderatorID: string, - reason: string, - /** - * @field 0 - Create - * @field 1 - Warn - * @field 2 - Lock - * @field 3 - Unlock - * @field 4 - Delete - */ - type: 0 | 1 | 2 | 3 | 4 - date: Date, - expiration: { - date: Date, - processed: boolean - } + +class Expiration { + @prop({ required: true }) + date: Date; + + @prop({ required: true, default: false }) + processed: boolean; } -const Moderation = new Schema({ - username: String, - userID: String, - logID: String, - moderatorID: String, - reason: String, - type: Number, - date: Date, - expiration: { - date: Date, - processed: Boolean, - }, -}); +enum Type { + Create, + Warn, + Lock, + Unlock, + Delete +} -export default model('Moderation', Moderation); +@modelOptions({ schemaOptions: { collection: 'Moderation' } }) +export default class Moderation { + @prop({ required: true }) + username: string; + + @prop({ required: true }) + userID: string; + + @prop({ required: true, unique: true }) + logID: string; + + @prop({ required: true }) + moderatorID: string; + + @prop() + reason?: string; + + @prop({ enum: Type, required: true }) + type: Type; + + @prop({ required: true }) + date: Date; + + @prop() + expiration?: Expiration; +} diff --git a/src/models/Tier.ts b/src/models/Tier.ts index 4bf81ed..a867271 100644 --- a/src/models/Tier.ts +++ b/src/models/Tier.ts @@ -1,23 +1,23 @@ -import { Document, Schema, model } from 'mongoose'; +import { modelOptions, prop } from '@typegoose/typegoose'; -export interface Tiers { - id: number, - resourceLimits: { - // In MB - ram: number, storage: number - } +class ResourceLimits { + @prop({ required: true }) + ram: number; + + @prop({ required: true }) + storage: number; } -export interface TierInterface extends Tiers, Document { - id: number; -} - -const Tier = new Schema({ - id: Number, - resourceLimits: { - ram: Number, - storage: Number, +@modelOptions({ + schemaOptions: { + _id: false, + collection: 'Tier', }, -}, { id: false }); +}) +export default class Tier { + @prop({ required: true, unique: true }) + id: number; -export default model('Tier', Tier); + @prop({ required: true, type: () => ResourceLimits }) + resourceLimits: ResourceLimits; +} diff --git a/src/models/index.ts b/src/models/index.ts index f49b1df..64aabef 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,4 +1,4 @@ -export { default as Account, AccountInterface } from './Account'; -export { default as Domain, DomainInterface } from './Domain'; -export { default as Moderation, ModerationInterface } from './Moderation'; -export { default as Tier, TierInterface, Tiers } from './Tier'; +export { default as Account } from './Account'; +export { default as Domain } from './Domain'; +export { default as Moderation } from './Moderation'; +export { default as Tier } from './Tier'; diff --git a/tsconfig.json b/tsconfig.json index febdc18..6f68545 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -47,7 +47,7 @@ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ @@ -58,7 +58,7 @@ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ } }