diff --git a/src/api/routes/Root.ts b/src/api/routes/Root.ts index 061e57d..d12be02 100644 --- a/src/api/routes/Root.ts +++ b/src/api/routes/Root.ts @@ -57,6 +57,8 @@ export default class Root extends Route { try { res.setHeader('Access-Control-Allow-Origin', '*'); const token = jwt.verify(req.query.t.toString(), this.server.client.config.keyPair.privateKey); + const check = await this.server.storage.get(token); + if (!check) return res.sendStatus(401); const embed = new RichEmbed(); embed.setTitle('Referral Authorization'); embed.addField('Referred User', token.referredUserAndDiscrim, true); @@ -64,6 +66,7 @@ export default class Root extends Route { embed.addField('Referral Code', token.referralCode, true); const channel = this.server.client.guilds.get('446067825673633794').channels.get('580950455581147146'); res.sendStatus(200); + await this.server.storage.set(token, true); return channel.createMessage({ content: `<@${token.staffUserID}>`, embed }); } catch { return res.sendStatus(401); diff --git a/src/api/static/verify.html b/src/api/static/verify.html index ac819cd..206f039 100644 --- a/src/api/static/verify.html +++ b/src/api/static/verify.html @@ -14,7 +14,6 @@ if (response.status === 200) return alert('Request authorized. You may now close this tab.'); if (response.status === 401) return alert('Authorization Token incorrect, try again.'); if (response.status >= 500) return alert('INTERNAL SERVER ERROR'); - alert('Authentication Complete.'); } catch (err) { alert(err); } diff --git a/src/class/LocalStorage.ts b/src/class/LocalStorage.ts new file mode 100644 index 0000000..5dc83a3 --- /dev/null +++ b/src/class/LocalStorage.ts @@ -0,0 +1,154 @@ +/* eslint-disable no-constant-condition */ +import { promises as fs, accessSync, constants, writeFileSync } from 'fs'; +import { promisify } from 'util'; +import { gzip, gzipSync, unzip } from 'zlib'; + +type JSONData = [{key: string, value: any}?]; + + +/** + * Persistant local JSON-based storage. + * - auto-locking system to prevent corrupted data + * - uses gzip compression to keep DB storage space utilization low + * @author Matthew + */ +export default class LocalStorage { + protected readonly storagePath: string; + + protected locked: boolean = false; + + constructor(dbName: string, dir = `${__dirname}/../../localstorage`) { + this.storagePath = `${dir}/${dbName}.json.gz`; + this.init(); + } + + private init() { + try { + accessSync(this.storagePath, constants.F_OK); + } catch { + const setup = []; + const data = gzipSync(JSON.stringify(setup)); + writeFileSync(this.storagePath, data); + } + } + + /** + * Compresses data using gzip. + * @param data The data to be compressed. + * ```ts + * await LocalStorage.compress('hello!'); + * ``` + */ + static async compress(data: string): Promise { + const func = promisify(gzip); + const comp = await func(data); + return comp; + } + + /** + * Decompresses data using gzip. + * @param data The data to be decompressed. + * ```ts + * const compressed = await LocalStorage.compress('data'); + * const decompressed = await LocalStorage.decompress(compressed); + * console.log(decompressed); // logs 'data'; + * ``` + */ + static async decompress(data: Buffer): Promise { + const func = promisify(unzip); + const uncomp = await func(data); + return uncomp.toString(); + } + + /** + * Retrieves one data from the store. + * If the store has multiple entries for the same key, this function will only return the first entry. + * ```ts + * await LocalStorage.get('data-key'); + * ``` + * @param key The key for the data entry. + */ + public async get(key: string): Promise { + while (true) { + if (!this.locked) break; + } + this.locked = true; + + const file = await fs.readFile(this.storagePath); + const uncomp = await LocalStorage.decompress(file); + this.locked = false; + const json: JSONData = JSON.parse(uncomp); + const result = json.filter((data) => data.key === key); + if (!result[0]) return null; + return result[0].value; + } + + /** + * Retrieves multiple data keys/values from the store. + * This function will return all of the values matching the key you provided exactly. Use `LocalStorage.get();` if possible. + * ```ts + * await LocalStorage.get('data-key'); + * @param key The key for the data entry. + */ + public async getMany(key: string): Promise<{key: string, value: T}[]> { + while (true) { + if (!this.locked) break; + } + this.locked = true; + + const file = await fs.readFile(this.storagePath); + const uncomp = await LocalStorage.decompress(file); + this.locked = false; + const json: JSONData = JSON.parse(uncomp); + const result = json.filter((data) => data.key === key); + if (result.length < 1) return null; + return result; + } + + /** + * Sets a key/value pair and creates a new data entry. + * @param key The key for the data entry. + * @param value The value for the data entry, can be anything that is valid JSON. + * @param options.override [DEPRECATED] By default, this function will error if the key you're trying to set already exists. Set this option to true to override that setting. + * ```ts + * await LocalStorage.set('data-key', 'test'); + * ``` + */ + public async set(key: string, value: any): Promise { + while (true) { + if (!this.locked) break; + } + this.locked = true; + + const file = await fs.readFile(this.storagePath); + const uncomp = await LocalStorage.decompress(file); + const json: JSONData = JSON.parse(uncomp); + json.push({ key, value }); + const comp = await LocalStorage.compress(JSON.stringify(json)); + await fs.writeFile(this.storagePath, comp); + this.locked = false; + } + + /** + * Deletes the data for the specified key. + * **Warning:** This function will delete ALL matching entries. + * ```ts + * await LocalStorage.del('data-key'); + * ``` + * @param key The key for the data entry. + */ + public async del(key: string): Promise { + while (true) { + if (!this.locked) break; + } + this.locked = true; + + const file = await fs.readFile(this.storagePath); + const uncomp = await LocalStorage.decompress(file); + const json: JSONData = JSON.parse(uncomp); + const filtered = json.filter((data) => data.key !== key); + const comp = await LocalStorage.compress(JSON.stringify(filtered)); + await fs.writeFile(this.storagePath, comp); + this.locked = false; + } +} diff --git a/src/class/Server.ts b/src/class/Server.ts index 1db49ba..4cd98e0 100644 --- a/src/class/Server.ts +++ b/src/class/Server.ts @@ -3,7 +3,7 @@ import express from 'express'; import bodyParser from 'body-parser'; import helmet from 'helmet'; import fs from 'fs-extra'; -import { Client, Collection, Route } from '.'; +import { Client, Collection, LocalStorage, Route } from '.'; import { Security } from '../api'; @@ -16,6 +16,8 @@ export default class Server { public app: express.Express; + public storage: LocalStorage; + public options: { port: number } constructor(client: Client, options?: { port: number }) { @@ -24,6 +26,7 @@ export default class Server { this.client = client; this.security = new Security(this.client); this.app = express(); + this.storage = new LocalStorage('usedra', '/opt/CloudServices/localstorage'); this.connect(); this.loadRoutes(); } diff --git a/src/class/index.ts b/src/class/index.ts index ad33446..ea03741 100644 --- a/src/class/index.ts +++ b/src/class/index.ts @@ -3,6 +3,7 @@ export { default as Client } from './Client'; export { default as Collection } from './Collection'; export { default as Command } from './Command'; export { default as Event } from './Event'; +export { default as LocalStorage } from './LocalStorage'; export { default as RichEmbed } from './RichEmbed'; export { default as Route } from './Route'; export { default as Security } from './Security';