refactor(models): use typegoose

refactor/models
Hiroyuki 2021-07-08 20:50:04 -04:00
parent a3d15b231c
commit b9d4a28c4f
No known key found for this signature in database
GPG Key ID: C15AC26538975A24
17 changed files with 217 additions and 173 deletions

View File

@ -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"
}
}

27
.vscode/settings.json vendored
View File

@ -1,10 +1,21 @@
{
"eslint.enable": true,
"eslint.validate": [
{
"language": "typescript",
"autoFix": true
}
],
"editor.tabSize": 2
"files.autoSave": "onFocusChange",
"editor.tabSize": 2,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"cSpell.language": "en-gb",
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"files.enableTrash": false,
"eslint.validate": ["typescript"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"typescript.preferences.importModuleSpecifier": "relative"
}

View File

@ -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"
},

View File

@ -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
}

View File

@ -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.');

View File

@ -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<Event>;
public db: { Account: mongoose.Model<AccountInterface>; Domain: mongoose.Model<DomainInterface>; Moderation: mongoose.Model<ModerationInterface>; Tier: mongoose.Model<TierInterface>; };
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<Command>();
this.events = new Collection<Event>();
this.functions = new Collection<Function>();
this.db = { Account, Domain, Moderation, Tier };
this.redis = new Redis();
this.stores = { emojis };
this.signale = signale;

View File

@ -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<null | AccountInterface> {
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 });

View File

@ -10,7 +10,7 @@ import moment from 'moment';
import fs from 'fs';
import { getUserByUid } from '../functions';
import { AccountUtil, Client, Command, RichEmbed } from '.';
import { ModerationInterface, AccountInterface, Account } from '../models';
import { Account, Moderation } from '../models';
export default class Util {
public client: Client;
@ -36,7 +36,7 @@ export default class Util {
public async exec(command: string, options: childProcess.ExecOptions = {}): Promise<string> {
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 +47,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 +99,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 +153,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 +198,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<AccountInterface> {
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 +222,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<Message> {
public async messageCollector(message: Message, question: string, timeout: number, shouldDelete = false, choices: string[] = null, filter = (msg: Message): boolean | void => { }): Promise<Message> {
const msg = await message.channel.createMessage(question);
return new Promise((res, rej) => {
const func = (Msg: Message) => {
@ -250,12 +250,12 @@ export default class Util {
*
* `4` - Delete
*/
public async createModerationLog(user: string, moderator: Member|User, type: number, reason?: string, duration?: number): Promise<ModerationInterface> {
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(),
};

View File

@ -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.`);

View File

@ -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<GuildTextableChannel>, 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}`),

View File

@ -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)) {

View File

@ -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<AccountInterface>({
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<AccountInterface>('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[];
}

View File

@ -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<DomainInterface>({
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<DomainInterface>('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;
}

View File

@ -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<ModerationInterface>({
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<ModerationInterface>('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;
}

View File

@ -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<TierInterface>({
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<TierInterface>('Tier', Tier);
@prop({ required: true, type: () => ResourceLimits })
resourceLimits: ResourceLimits;
}

View File

@ -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';

View File

@ -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. */
}
}