From a244d98806774381c8280802a6d345d405198b3c Mon Sep 17 00:00:00 2001 From: Matthieu Bessat Date: Fri, 10 Jul 2020 22:44:01 +0200 Subject: [PATCH] feat: add api support and auth --- .env.example | 18 +++ package.json | 10 ++ src/EmailService.ts | 18 +++ src/IconService.ts | 51 +++++++ src/RedisService.ts | 13 ++ src/app.ts | 64 +++++++-- .../AdminOrganizationController.ts | 71 +++++++++- src/controllers/AdminTagController.ts | 45 ++++-- src/controllers/DefaultController.ts | 90 +++++++----- src/controllers/DelegateController.ts | 21 +++ src/controllers/PublicController.ts | 27 ++++ src/middlewares/AdminAuthMiddleware.ts | 17 +++ src/middlewares/DelegateAuthMiddleware.ts | 23 +++ src/models/Organization.ts | 1 - src/models/Tag.ts | 9 +- static/fa_icons | 1 + templates/about.html | 0 templates/home.html | 0 templates/legals.html | 37 +++++ templates/organization.html | 0 yarn.lock | 132 +++++++++++++++++- 21 files changed, 585 insertions(+), 63 deletions(-) create mode 100644 .env.example create mode 100644 src/EmailService.ts create mode 100644 src/IconService.ts create mode 100644 src/RedisService.ts create mode 100644 src/controllers/DelegateController.ts create mode 100644 src/controllers/PublicController.ts create mode 100644 src/middlewares/AdminAuthMiddleware.ts create mode 100644 src/middlewares/DelegateAuthMiddleware.ts create mode 120000 static/fa_icons create mode 100644 templates/about.html create mode 100644 templates/home.html create mode 100644 templates/legals.html create mode 100644 templates/organization.html diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..19ab42b --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +S3_ORGANIZATION_ID= +S3_ACCESS_KEY= +S3_SECRET_KEY= + +MONGO_URI=mongodb://root:root@127.0.0.1:27017/forumvirt?authSource=admin + +SMTP_HOST= +SMTP_PORT= +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_SECURE=false + +ADMIN_TOKEN=pass + +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD=root + diff --git a/package.json b/package.json index 5af5c30..57e34f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,12 @@ { "dependencies": { + "@fortawesome/fontawesome-svg-core": "^1.2.29", + "@fortawesome/free-brands-svg-icons": "^5.13.1", + "@fortawesome/free-regular-svg-icons": "^5.13.1", + "@fortawesome/free-solid-svg-icons": "^5.13.1", + "@types/cors": "^2.8.6", "@types/express": "^4.17.6", + "@types/ioredis": "^4.17.0", "@types/mongoose": "^5.7.28", "@types/multer": "^1.4.3", "@types/multer-s3": "^2.7.7", @@ -8,13 +14,17 @@ "@types/nodemailer": "^6.4.0", "aws-sdk": "^2.706.0", "body-parser": "^1.19.0", + "cors": "^2.8.5", + "crypto-random-string": "^3.2.0", "dotenv": "^8.2.0", "express": "^4.17.1", + "ioredis": "^4.17.3", "mongoose": "^5.9.20", "multer": "^1.4.2", "multer-s3": "^2.9.0", "multer-s3-v2": "^2.2.1", "mustache": "^4.0.1", + "nanoid": "^3.1.10", "nodemailer": "^6.4.10", "typescript": "^3.9.5" }, diff --git a/src/EmailService.ts b/src/EmailService.ts new file mode 100644 index 0000000..b9d607b --- /dev/null +++ b/src/EmailService.ts @@ -0,0 +1,18 @@ +import nodemailer from 'nodemailer' + +export default class EmailService { + static getTransporter() { + if (process.env.SMTP_HOST == undefined || process.env.SMTP_PORT == undefined) { + throw new Error("Invalid SMTP config") + } + return nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: parseInt(process.env.SMTP_PORT), + secure: process.env.SMTP_SECURE == 'true', + auth: { + user: process.env.SMTP_USERNAME, + pass: process.env.SMTP_PASSWORD, + } + }) + } +} \ No newline at end of file diff --git a/src/IconService.ts b/src/IconService.ts new file mode 100644 index 0000000..e115352 --- /dev/null +++ b/src/IconService.ts @@ -0,0 +1,51 @@ +import * as faSolid from '@fortawesome/free-solid-svg-icons' +import * as faBrands from '@fortawesome/free-brands-svg-icons' +import * as faRegular from '@fortawesome/free-regular-svg-icons' +import { IconDefinition } from '@fortawesome/fontawesome-common-types' + +export interface IconInterface { + id: string, + width: number, + height: number, + path: string +} + +export class IconService { + + static snakeToCamel(str: string): string { + return str.replace( + /([-_][a-z])/g, + group => group.toUpperCase().replace('-', '').replace('_', '') + ) + } + + static getIcon(id: string): IconInterface|null { + let components: string[] = id.split(id.indexOf('_') !== -1 ? '_' : ' ') + if (components.length < 2) { + return null + } + let iconSet: any = {} + switch (components[0]) { + case 'fas': + iconSet = faSolid + break + case 'fab': + iconSet = faBrands + break + case 'far': + iconSet = faRegular + break + } + let iconName: string = this.snakeToCamel(components[1]) + let iconVal: IconDefinition|undefined = iconSet[iconName] + if (iconVal === undefined) { + return null + } + return { + id: components[0] + ' ' + components[1], + width: iconVal.icon[0], + height: iconVal.icon[1], + path: iconVal.icon[4] as string + } + } +} \ No newline at end of file diff --git a/src/RedisService.ts b/src/RedisService.ts new file mode 100644 index 0000000..6efdad2 --- /dev/null +++ b/src/RedisService.ts @@ -0,0 +1,13 @@ +import Redis from 'ioredis' +import IORedis from 'ioredis' + +export default class RedisService { + + static getClient(): IORedis.Redis { + return new IORedis( + parseInt(process.env.REDIS_PORT === undefined ? '6379' : process.env.REDIS_PORT), + process.env.REDIS_HOST === undefined ? "127.0.0.1" : process.env.REDIS_HOST, + { password: process.env.REDIS_PASSWORD } + ) + } +} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index ebe5a8f..763a974 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,9 +3,15 @@ import bodyParser from 'body-parser' import express from 'express' import mongoose from 'mongoose' import AdminTagController from './controllers/AdminTagController' +import AdminOrganizationController from './controllers/AdminOrganizationController' import DefaultController from './controllers/DefaultController' import MediaController from './controllers/MediaController' import dotenv from 'dotenv' +import DelegateController from './controllers/DelegateController' +import AdminAuthMiddleware from './middlewares/AdminAuthMiddleware' +import DelegateAuthMiddleware from './middlewares/DelegateAuthMiddleware' +import PublicController from './controllers/PublicController' +import cors from 'cors' console.log('WOOWOWO') @@ -32,20 +38,62 @@ let main = async () => { console.log('> Connected to mongodb') }) + app.use(cors()) app.use(bodyParser.json()) - - app.get('/', DefaultController.home) - app.get('/association/:slug', DefaultController.publicOrganization) + + app.use((err: any, req: express.Request, res: express.Response, next: express.RequestHandler) => { + console.error(err.stack) + res.status(res.statusCode).json({ success: false, error: err.stack }) + }) + + app.get('/', PublicController.home) + app.get('/association/:slug', PublicController.organization) + + app.get('/icon/:id', DefaultController.viewIcon) app.get('/email', DefaultController.sendEmail) - app.get('/api/tags', AdminTagController.getTags) - app.put('/api/tags/:id', AdminTagController.updateTag) - app.post('/api/tags', AdminTagController.storeTag) - app.delete('/api/tags/:id', AdminTagController.destroyTag) + app.use('/admin', express.Router() + .use(AdminAuthMiddleware.handle) + .get('/', DefaultController.success) + .use('/tags', express.Router() + .get('/', AdminTagController.getMany) + .post('/', AdminTagController.store) + .put('/:id', AdminTagController.update) + .delete('/:id', AdminTagController.destroy) + ) + .use('/organizations', express.Router() + .get('/', AdminOrganizationController.getMany) + .get('/:id', AdminOrganizationController.getOne) + .post('', AdminOrganizationController.store) + .put('/:id', AdminOrganizationController.update) + .delete('/:id', AdminOrganizationController.destroy) + .post('/:id/reset-token', AdminOrganizationController.resetToken) + .post('/:id/approve', AdminOrganizationController.approve) + .post('/:id/reject', AdminOrganizationController.reject) + ) + ) + + app.use('/delegate', express.Router() + .use(DelegateAuthMiddleware.handle) + .get('/', DelegateController.get) + .put('/', DelegateController.update) + .put('/submit', DelegateController.submit) + .delete('/', DelegateController.destroy) + ) + + /* + + .put('/tags/:id', AdminTagController.update) + .put('/tags/:id', AdminTagController.destroy) + .use('/organizations', () => express.Router() + .get('/', AdminOrganizationController.getMany) + ) + */ + app.post('/api/upload', MediaController.uploadRoute()) - app.listen(port, host, () => { + app.listen(port, host, () => { console.log(`API listening on ${host}:${port}`) }) } diff --git a/src/controllers/AdminOrganizationController.ts b/src/controllers/AdminOrganizationController.ts index eaf6f8d..4552008 100644 --- a/src/controllers/AdminOrganizationController.ts +++ b/src/controllers/AdminOrganizationController.ts @@ -1,10 +1,79 @@ import * as express from 'express' import Organization from '../models/Organization' +import cryptoRandomString from 'crypto-random-string' export default class AdminOrganizationController { - static getOrganizations(req: express.Request, res: express.Response) { + static getMany(req: express.Request, res: express.Response) { Organization.find().then(data => { res.json(data) }) } + + static getOne(req: express.Request, res: express.Response) { + Organization.findById(req.params.id).then(data => { + res.json(data) + }) + } + + static store(req: express.Request, res: express.Response) { + Organization.create({ + token: cryptoRandomString({ length: 10, type: 'distinguishable' }), + ...req.body + }).then(data => { + res.json({ + success: true, + data + }) + }).catch(err => { + res.status(400).json({ + success: false, + errors: err.errors + }) + }) + } + + static update(req: express.Request, res: express.Response) { + Organization.updateOne({ _id: req.params.id }, req.body).then(data => { + res.json({ + success: true, + data + }) + }).catch(err => { + res.status(400).json({ + success: false, + errors: err.errors !== undefined ? err.errors : err + }) + }) + } + + static destroy(req: express.Request, res: express.Response) { + Organization.deleteOne({ _id: req.params.id }).then(data => { + let isSuccess = data.deletedCount !== undefined && data.deletedCount > 0 + res.status(isSuccess ? 200 : 400).json({ + success: isSuccess, + data + }) + }).catch(err => { + res.status(400).json({ + success: false, + errors: err.errors + }) + }) + } + + static sendEmailToken() { + + } + + static resetToken(req: express.Request, res: express.Response) { + + } + + static approve(req: express.Request, res: express.Response) { + + } + + static reject(req: express.Request, res: express.Response) { + + } } \ No newline at end of file diff --git a/src/controllers/AdminTagController.ts b/src/controllers/AdminTagController.ts index f15e5d4..32f2ba6 100644 --- a/src/controllers/AdminTagController.ts +++ b/src/controllers/AdminTagController.ts @@ -1,14 +1,26 @@ import Tag from '../models/Tag' import express from 'express' +import { IconService, IconInterface } from '../IconService' export default class AdminTagController { - static getTags(_: any, res: express.Response) { + static getMany(_: any, res: express.Response) { Tag.find().then(data => { - res.json(data) + res.json({ success: true, data }) }) } - static storeTag(req: express.Request, res: express.Response) { + static store(req: express.Request, res: express.Response) { + // verify the icon + let icon: IconInterface|null = IconService.getIcon(req.body.icon) + if (icon === null) { + return res.status(400).json({ + success: false, + errors: [ + {code: 'invalid-icon', message: 'Invalid font awesome icon code'} + ] + }) + } + req.body.icon = icon Tag.create(req.body).then(data => { res.json({ success: true, @@ -22,30 +34,35 @@ export default class AdminTagController { }) } - static updateTag(req: express.Request, res: express.Response) { + static update(req: express.Request, res: express.Response) { + let icon: IconInterface|null = IconService.getIcon(req.body.icon) + if (icon === null) { + return res.status(400).json({ + success: false, + errors: [ + {code: 'invalid-icon', message: 'Invalid font awesome icon code'} + ] + }) + } + req.body.icon = icon Tag.updateOne({ _id: req.params.id }, req.body).then(data => { res.json({ success: true, data }) }).catch(err => { - let errors - if (err.errors !== undefined) { - errors = err.errors - } else { - errors = err - } res.status(400).json({ success: false, - errors + errors: err.errors !== undefined ? err.errors : err }) }) } - static destroyTag(req: express.Request, res: express.Response) { + static destroy(req: express.Request, res: express.Response) { Tag.deleteOne({ _id: req.params.id }).then(data => { - res.json({ - success: true, + let isSuccess = data.deletedCount !== undefined && data.deletedCount > 0 + res.status(isSuccess ? 200 : 400).json({ + success: isSuccess, data }) }).catch(err => { diff --git a/src/controllers/DefaultController.ts b/src/controllers/DefaultController.ts index 1201180..178c101 100644 --- a/src/controllers/DefaultController.ts +++ b/src/controllers/DefaultController.ts @@ -3,45 +3,65 @@ import * as express from 'express' import nodemailer from 'nodemailer' import Mustache from 'mustache' import fs from 'fs' +import { IconService, IconInterface } from '../IconService' +import EmailService from '../EmailService' export default class DefaultController { - static home(req: express.Request, res: express.Response) { - // Organization.create({ - // admin_name: "hello", - // email: 'spamfree@matthieubessat.fr', - // token: 'dqsdsqdsq', - // validationState: "ndsqqdsd" - // }).then(data => { - // console.log(data) - // res.json({ - // success: true - // }) - // }).catch(err => { - // res.json({ - // success: false, - // errors: err.errors - // }) - // }) + static success(req: express.Request, res: express.Response) { res.json({ success: true }) } + static viewIcon(req: express.Request, res: express.Response) { + // let components: string[] = req.params.id.split('_') + // if (components.length < 2) { + // return res + // .status(400) + // .json({ success: false, errors: [{ code: 'invalid-input', message: 'Invalid Input icon' }] }) + // } + // components = [components[0].replace(' ', ''), components[1].replace(' ', '')] + // let iconSet: any = {} + // console.log(components[0], components[1]) + // if (components[0] === "fas") { + // iconSet = faSolid + // } + // if (components[0] === "fab") { + // iconSet = faBrands + // } + // if (components[0] === "far") { + // iconSet = faRegular + // } + // let iconName: string = snakeToCamel(components[1]) + // let iconVal: IconDefinition|undefined = iconSet[iconName] + let icon: IconInterface|null = IconService.getIcon(req.params.id) + if (icon === null) { + return res + .status(400) + .json({ success: false, errors: [{ code: 'invalid-icon', message: 'The icon is invalid' }] }) + } + res.json({ success: true, data: icon }) + // res.setHeader('Content-Type', 'text/html') + // return res.send(Mustache.render(` + // `, {...icon})) + } + static async sendEmail(req: express.Request, res: express.Response) { // create reusable transporter object using the default SMTP transport - let transporter = nodemailer.createTransport({ - host: "node02.cluster.stantabcorp.net", - port: 587, - secure: false, - auth: { - user: "mail@matthieubessat.fr", - pass: "", - }, - }); - - transporter.sendMail({ - from: '"Matthieu Bessat" ', - to: "matthieu.bessat.27@gmail.com", + EmailService.getTransporter().sendMail({ + from: '"Some Sender" ', + to: "spamfree@matthieubessat.fr", subject: "Hello ✔", text: "Hello world? Comment va tu Earum facilis libero excepturi sunt fuga eveniet autem. Illo odit quae aperiam et praesentium. Error dignissimos atque omnis. Ea iste in doloribus praesentium corrupti. Ut consequatur eius eveniet quia aut. Nam a rerum quis. Repudiandae sit nobis esse. Eaque ipsum qui enim. Expedita laudantium officia omnis maxime. Odio exercitationem recusandae quis consequatur voluptatum.", html: "

Hello world? Comment va tu Earum facilis libero excepturi sunt fuga eveniet autem. Illo odit quae aperiam et praesentium. Error dignissimos atque omnis. Ea iste in doloribus praesentium corrupti. Ut consequatur eius eveniet quia aut. Nam a rerum quis. Repudiandae sit nobis esse. Eaque ipsum qui enim. Expedita laudantium officia omnis maxime. Odio exercitationem recusandae quis consequatur voluptatum.

", @@ -55,9 +75,9 @@ export default class DefaultController { return res.json({ success: true }) } - static async publicOrganization(req: express.Request, res: express.Response) { - res.setHeader('Content-Type', 'text/html') - let path: string = __dirname + '/../../templates/organization.html' - return res.send(Mustache.render(fs.readFileSync(path).toString(), {})) - } + // static async publicOrganization(req: express.Request, res: express.Response) { + // res.setHeader('Content-Type', 'text/html') + // let path: string = __dirname + '/../../templates/organization.html' + // return res.send(Mustache.render(fs.readFileSync(path).toString(), {})) + // } } \ No newline at end of file diff --git a/src/controllers/DelegateController.ts b/src/controllers/DelegateController.ts new file mode 100644 index 0000000..0a2fb5d --- /dev/null +++ b/src/controllers/DelegateController.ts @@ -0,0 +1,21 @@ +import Organization from '../models/Organization' +import * as express from 'express' + +export default class DelegateController { + + static get(req: express.Request, res: express.Response) { + res.json({ success: true, data: res.locals.organization }) + } + + static update(req: express.Request, res: express.Response) { + res.json({ success: true }) + } + + static submit(req: express.Request, res: express.Response) { + res.json({ success: true }) + } + + static destroy(req: express.Request, res: express.Response) { + res.json({ success: true }) + } +} \ No newline at end of file diff --git a/src/controllers/PublicController.ts b/src/controllers/PublicController.ts new file mode 100644 index 0000000..9c99a68 --- /dev/null +++ b/src/controllers/PublicController.ts @@ -0,0 +1,27 @@ + +import RedisService from '../RedisService' +import Organization from '../models/Organization' +import * as express from 'express' +import nodemailer from 'nodemailer' +import Mustache from 'mustache' +import fs from 'fs' +import { IconService, IconInterface } from '../IconService' +import IORedis from 'ioredis' + +export default class PublicController { + + + static async home(req: express.Request, res: express.Response) { + let client: IORedis.Redis = RedisService.getClient() + await client.set('hello', 'world') + res.json({ + data: await client.get('hello') + }) + } + + static async organization(req: express.Request, res: express.Response) { + res.setHeader('Content-Type', 'text/html') + let path: string = __dirname + '/../../templates/organization.html' + return res.send(Mustache.render(fs.readFileSync(path).toString(), {})) + } +} \ No newline at end of file diff --git a/src/middlewares/AdminAuthMiddleware.ts b/src/middlewares/AdminAuthMiddleware.ts new file mode 100644 index 0000000..7dd3c96 --- /dev/null +++ b/src/middlewares/AdminAuthMiddleware.ts @@ -0,0 +1,17 @@ +import express from 'express' + +export default class AdminAuthMiddleware { + static handle(req: express.Request, res: express.Response, next: any) { + let auth: string | undefined = req.get('Authorization') + if (auth === undefined || auth.replace('Bearer ', '') !== process.env.ADMIN_TOKEN) { + res + .status(400) + .json({ + success: false, + errors: { code: 'invalid-auth', message: 'Invalid admin Authorization header' } + }) + return + } + next() + } +} \ No newline at end of file diff --git a/src/middlewares/DelegateAuthMiddleware.ts b/src/middlewares/DelegateAuthMiddleware.ts new file mode 100644 index 0000000..7f198e6 --- /dev/null +++ b/src/middlewares/DelegateAuthMiddleware.ts @@ -0,0 +1,23 @@ +import express from 'express' +import Organization from '../models/Organization' + +export default class DelegateAuthMiddleware { + static async handle(req: express.Request, res: express.Response, next: express.NextFunction) { + let token: string | undefined = req.get('Authorization') + // fetch the token + if (token !== undefined) { + let data = await Organization.findOne({ token: token.replace('Bearer ', '') }) + if (data !== null) { + res.locals.organization = data + next() + return + } + } + res + .status(400) + .json({ + success: false, + errors: { code: 'invalid-auth', message: 'Invalid admin Authorization header' } + }) + } +} \ No newline at end of file diff --git a/src/models/Organization.ts b/src/models/Organization.ts index 2ec219a..36ae545 100644 --- a/src/models/Organization.ts +++ b/src/models/Organization.ts @@ -17,7 +17,6 @@ class AllowedString extends mongoose.SchemaType { } cast(value: any) { - console.log(value + ' is being validated') if (this.allowedValues.indexOf(value) === -1) { let validationStr: string = 'AllowedString: ' + value + ' must be one of which: [' + this.allowedValues.join(', ') + ']' console.log(validationStr) diff --git a/src/models/Tag.ts b/src/models/Tag.ts index d546d9b..c46d658 100644 --- a/src/models/Tag.ts +++ b/src/models/Tag.ts @@ -1,8 +1,15 @@ import mongoose, { Schema } from 'mongoose' +const Icon = new Schema({ + id: { type: String, required: true }, + width: { type: Number, required: true }, + height: { type: Number, required: true }, + path: { type: String, required: true } +}) + const _Tag = new Schema({ name: { type: String, required: true }, - icon: { type: String, required: true }, + icon: { type: Icon, required: true }, description: { type: String } }) diff --git a/static/fa_icons b/static/fa_icons new file mode 120000 index 0000000..ccdb5bc --- /dev/null +++ b/static/fa_icons @@ -0,0 +1 @@ +/home/mbess/font_awesome_icons/ \ No newline at end of file diff --git a/templates/about.html b/templates/about.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/legals.html b/templates/legals.html new file mode 100644 index 0000000..2c67a3c --- /dev/null +++ b/templates/legals.html @@ -0,0 +1,37 @@ +

Informations légalesw

+ +

Siège social

+ +

+Espace Condorcet Centre Social
+12 rue Jean Moulin,
+27600 GAILLON +

+ +

Contact

+ +

+Téléphone : 02 32 77 50 80
+Fax : 02 32 77 50 99 +

+ +

Représentant légal

+ +

Liliane COQUET

+ + +

Immatriculation

+ +

+Numéro SIREN : 338 248 206 000 25
+ +Code APE 88 99 B +

+ +

Hébergement

+ +

+Scaleway
+ +ONLINE SAS BP 438 75366 PARIS CEDEX 08 FRANCE +

diff --git a/templates/organization.html b/templates/organization.html new file mode 100644 index 0000000..e69de29 diff --git a/yarn.lock b/yarn.lock index 9415feb..27edcb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,39 @@ # yarn lockfile v1 +"@fortawesome/fontawesome-common-types@^0.2.29": + version "0.2.29" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.29.tgz#e1a456b643237462d390304cab6975ff3fd68397" + integrity sha512-cY+QfDTbZ7XVxzx7jxbC98Oxr/zc7R2QpTLqTxqlfyXDrAJjzi/xUIqAUsygELB62JIrbsWxtSRhayKFkGI7MA== + +"@fortawesome/fontawesome-svg-core@^1.2.29": + version "1.2.29" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.29.tgz#34ef32824664534f9e4ef37982ebf286b899a189" + integrity sha512-xmPmP2t8qrdo8RyKihTkGb09RnZoc+7HFBCnr0/6ZhStdGDSLeEd7ajV181+2W29NWIFfylO13rU+s3fpy3cnA== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.29" + +"@fortawesome/free-brands-svg-icons@^5.13.1": + version "5.13.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.13.1.tgz#fef07a4b957dc1708f2191a94cdfcd6901c35383" + integrity sha512-dKwF+NpIV2LVCNBA7hibH53k+ChF4Wu59P2z35gu3zwRBZpmpLVhS9k1/RiSqUqkyXUQvA2rSv48GY6wp5axZQ== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.29" + +"@fortawesome/free-regular-svg-icons@^5.13.1": + version "5.13.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.13.1.tgz#02ac0c2fd52948d2139eca3ca42b267f17e3d725" + integrity sha512-sSeaqqmv2ovA5LKcrbh3VnEDZHVhaxijWKm4R0AdT0eG21pgxNsJbStD8lW9z6bgSuWXRNHhbhOmARuRCLS8tw== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.29" + +"@fortawesome/free-solid-svg-icons@^5.13.1": + version "5.13.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.13.1.tgz#010a846b718a0f110b3cd137d072639b4e8bd41a" + integrity sha512-LQH/0L1p4+rqtoSHa9qFYR84hpuRZKqaQ41cfBQx8b68p21zoWSekTAeA54I/2x9VlCHDLFlG74Nmdg4iTPQOg== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.29" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -41,6 +74,13 @@ dependencies: "@types/node" "*" +"@types/cors@^2.8.6": + version "2.8.6" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.6.tgz#cfaab33c49c15b1ded32f235111ce9123009bd02" + integrity sha512-invOmosX0DqbpA+cE2yoHGUlF/blyf7nB0OGYBBiH27crcVm5NmFaZkLP4Ta1hGaesckCi5lVLlydNJCxkTOSg== + dependencies: + "@types/express" "*" + "@types/express-serve-static-core@*": version "4.17.8" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.8.tgz#b8f7b714138536742da222839892e203df569d1c" @@ -60,6 +100,13 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/ioredis@^4.17.0": + version "4.17.0" + resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.17.0.tgz#25fea62388f546958d53e31bbb6284b60c793e60" + integrity sha512-2wmT+lB9JAHSYlBrmJGeYDQ4cGlIgxmaC3Bv85LJws/G/OkoFoxPezhMqtTp0JMCgsMadIoI9c43QvRz8WKLSw== + dependencies: + "@types/node" "*" + "@types/mime@*": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.2.tgz#857a118d8634c84bba7ae14088e4508490cd5da5" @@ -370,6 +417,11 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +cluster-key-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" + integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -453,11 +505,26 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +crypto-random-string@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-3.2.0.tgz#d513ef0c2ac6ff7cad5769de585d9bf2ad5a2b4d" + integrity sha512-8vPu5bsKaq2uKRy3OL7h1Oo7RayAWB8sYexLKAqvCXVib8SxgbmoF1IN4QMKjBv8uI8mp5gPPMbiRah25GMrVQ== + dependencies: + type-fest "^0.8.1" + debug@2.6.9, debug@^2.2.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -479,6 +546,13 @@ debug@^3.1.0, debug@^3.2.6: dependencies: ms "^2.1.1" +debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" @@ -501,7 +575,7 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -denque@^1.4.1: +denque@^1.1.0, denque@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== @@ -822,6 +896,21 @@ ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== +ioredis@^4.17.3: + version "4.17.3" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.17.3.tgz#9938c60e4ca685f75326337177bdc2e73ae9c9dc" + integrity sha512-iRvq4BOYzNFkDnSyhx7cmJNOi1x/HWYe+A4VXHBu4qpwJaGT1Mp+D2bVGJntH9K/Z/GeOM/Nprb8gB3bmitz1Q== + dependencies: + cluster-key-slot "^1.1.0" + debug "^4.1.1" + denque "^1.1.0" + lodash.defaults "^4.2.0" + lodash.flatten "^4.4.0" + redis-commands "1.5.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.0.1" + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -947,6 +1036,16 @@ latest-version@^5.0.0: dependencies: package-json "^6.3.0" +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -1128,6 +1227,11 @@ mustache@^4.0.1: resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.0.1.tgz#d99beb031701ad433338e7ea65e0489416c854a2" integrity sha512-yL5VE97+OXn4+Er3THSmTdCFCtx5hHWzrolvH+JObZnUYwuaG7XV+Ch4fR2cIrcYI0tFHxS7iyFYl14bW8y2sA== +nanoid@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.10.tgz#69a8a52b77892de0d11cede96bc9762852145bc4" + integrity sha512-iZFMXKeXWkxzlfmMfM91gw7YhN2sdJtixY+eZh9V6QWJWTOiurhpKhBMgr82pfzgSqglQgqYSCowEYsz8D++6w== + negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -1171,7 +1275,7 @@ normalize-url@^4.1.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== -object-assign@^4.1.1: +object-assign@^4, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -1333,6 +1437,23 @@ readdirp@~3.4.0: dependencies: picomatch "^2.2.1" +redis-commands@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785" + integrity sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg== + +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + dependencies: + redis-errors "^1.0.0" + regexp-clone@1.0.0, regexp-clone@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" @@ -1482,6 +1603,11 @@ sparse-bitfield@^3.0.3: dependencies: memory-pager "^1.0.2" +standard-as-callback@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.0.1.tgz#ed8bb25648e15831759b6023bdb87e6b60b38126" + integrity sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg== + "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" @@ -1706,7 +1832,7 @@ uuid@3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -vary@~1.1.2: +vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=