diff options
| author | Pitu <[email protected]> | 2018-09-16 00:55:30 -0300 |
|---|---|---|
| committer | Pitu <[email protected]> | 2018-09-16 00:55:30 -0300 |
| commit | 7268d24143dca10b75b64a6800cec9fdfa4e1d72 (patch) | |
| tree | a599710baa89521c9af042ca5384c10231081da4 /src/api/structures | |
| parent | New base (diff) | |
| download | host.fuwn.me-7268d24143dca10b75b64a6800cec9fdfa4e1d72.tar.xz host.fuwn.me-7268d24143dca10b75b64a6800cec9fdfa4e1d72.zip | |
Base structures
Diffstat (limited to 'src/api/structures')
| -rw-r--r-- | src/api/structures/Database.js | 110 | ||||
| -rw-r--r-- | src/api/structures/Route.js | 44 | ||||
| -rw-r--r-- | src/api/structures/Server.js | 78 |
3 files changed, 232 insertions, 0 deletions
diff --git a/src/api/structures/Database.js b/src/api/structures/Database.js new file mode 100644 index 0000000..dc26afe --- /dev/null +++ b/src/api/structures/Database.js @@ -0,0 +1,110 @@ +const log = require('../utils/Log'); +const { server } = require('../../../config'); +const db = require('knex')(server.database); +const bcrypt = require('bcrypt'); +const moment = require('moment'); +const randomstring = require('randomstring'); + +class Database { + constructor() { + this.createTables(); + } + + async createTables() { + if (!await db.schema.hasTable('users')) { + await db.schema.createTable('users', table => { + table.increments(); + table.string('username'); + table.string('password'); + table.boolean('enabled').defaultTo(true); + table.boolean('isAdmin').defaultTo(false); + table.string('apiKey'); + table.timestamp('passwordEditedAt'); + table.timestamp('apiKeyEditedAt'); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + } + + if (!await db.schema.hasTable('albums')) { + await db.schema.createTable('albums', table => { + table.increments(); + table.integer('userId'); + table.string('name'); + // table.string('identifier'); + // table.boolean('enabled'); + // table.boolean('enableDownload').defaultTo(true); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + } + + if (!await db.schema.hasTable('files')) { + await db.schema.createTable('files', table => { + table.increments(); + table.integer('userId'); + table.string('name'); + table.string('original'); + table.string('type'); + table.integer('size'); + table.string('hash'); + table.string('ip'); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + } + + if (!await db.schema.hasTable('links')) { + await db.schema.createTable('links', table => { + table.increments(); + table.integer('albumId'); + table.string('identifier'); + table.integer('views').defaultTo(0); + table.boolean('enabled').defaultTo(true); + table.boolean('enableDownload').defaultTo(true); + table.timestamp('expiresAt'); + table.timestamp('createdAt'); + table.timestamp('editedAt'); + }); + } + + if (!await db.schema.hasTable('albumsFiles')) { + await db.schema.createTable('albumsFiles', table => { + table.increments(); + table.integer('albumId'); + table.integer('fileId'); + }); + } + + if (!await db.schema.hasTable('albumsLinks')) { + await db.schema.createTable('albumsLinks', table => { + table.increments(); + table.integer('albumId'); + table.integer('linkId'); + }); + } + + const now = moment.utc().toDate(); + const user = await db.table('users').where({ username: 'root' }).first(); + if (user) return; + try { + const hash = await bcrypt.hash('root', 10); + await db.table('users').insert({ + username: 'root', + password: hash, + apiKey: randomstring.generate(64), + passwordEditedAt: now, + apiKeyEditedAt: now, + createdAt: now, + editedAt: now, + isAdmin: true + }); + log.success('Successfully created the root user with password "root". Make sure to log in and change it!'); + } catch (error) { + log.error(error); + if (error) log.error('Error generating password hash for root'); + } + } +} + +module.exports = Database; diff --git a/src/api/structures/Route.js b/src/api/structures/Route.js new file mode 100644 index 0000000..77ebd32 --- /dev/null +++ b/src/api/structures/Route.js @@ -0,0 +1,44 @@ +const JWT = require('jsonwebtoken'); +const { server } = require('../../../config'); +const db = require('knex')(server.database); +const moment = require('moment'); + +class Route { + constructor(path, method, options) { + if (!path) throw new Error('Every route needs a URL associated with it.'); + if (!method) throw new Error('Every route needs its method specified.'); + + this.path = path; + this.method = method; + this.options = options || {}; + } + + authorize(req, res) { + if (this.options.bypassAuth) return this.run(req, res); + if (!req.headers.authorization) return res.status(401).json({ message: 'No authorization header provided' }); + const token = req.headers.authorization.split(' ')[1]; + if (!token) return res.status(401).json({ message: 'No authorization header provided' }); + + return JWT.verify(token, server.secret, async (error, decoded) => { + if (error) { + console.log(error); + return res.status(401).json({ message: 'Your token appears to be invalid' }); + } + const id = decoded ? decoded.sub : ''; + const iat = decoded ? decoded.iat : ''; + + const user = await db.table('users').where({ id }).first(); + if (!user) return res.status(401).json({ message: 'Invalid authorization' }); + if (iat && iat < moment(user.passwordEditedAt).format('x')) return res.status(401).json({ message: 'Token expired' }); + if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' }); + + return this.run(req, res, user); + }); + } + + run(req, res, user) { // eslint-disable-line no-unused-vars + return; + } +} + +module.exports = Route; diff --git a/src/api/structures/Server.js b/src/api/structures/Server.js new file mode 100644 index 0000000..ae4b678 --- /dev/null +++ b/src/api/structures/Server.js @@ -0,0 +1,78 @@ +const config = require('../../../config'); +const log = require('../utils/Log'); +const express = require('express'); +const helmet = require('helmet'); +const cors = require('cors'); +const RateLimit = require('express-rate-limit'); +const bodyParser = require('body-parser'); +const jetpack = require('fs-jetpack'); +const path = require('path'); +const Database = require('./Database'); +const oneliner = require('one-liner'); + +const rateLimiter = new RateLimit({ + windowMs: config.server.rateLimits.window, + max: config.server.rateLimits.max, + delayMs: 0 +}); + +class Server { + constructor() { + this.port = config.server.ports.backend; + this.server = express(); + this.server.set('trust proxy', 1); + this.server.use(helmet()); + this.server.use(cors({ allowedHeaders: ['Accept', 'Authorization', 'Cache-Control', 'X-Requested-With', 'Content-Type', 'albumId'] })); + this.server.use((req, res, next) => { + if (req.headers.accept === 'application/vnd.lolisafe.json') return next(); + return res.status(405).json({ message: 'Incorrect `Accept` header provided' }); + }); + this.server.use(bodyParser.urlencoded({ extended: true })); + this.server.use(bodyParser.json()); + // this.server.use(rateLimiter); + this.routesFolder = path.join(__dirname, '..', 'routes'); + this.database = new Database(); + this.server.get('/config', (req, res) => res.json({ + baseURL: config.backendLocation, + serviceName: config.serviceName, + maxFileSize: config.uploads.uploadMaxSize, + chunkSize: config.uploads.chunkSize + })); + } + + registerAllTheRoutes() { + jetpack.find(this.routesFolder, { matching: '*.js' }).forEach(routeFile => { + const RouteClass = require(path.join('..', '..', '..', routeFile)); + let routes = [RouteClass]; + if (Array.isArray(RouteClass)) routes = RouteClass; + for (const File of routes) { + const route = new File(); + this.server[route.method](config.server.routePrefix + route.path, route.authorize.bind(route)); + log.info(`Found route ${route.method.toUpperCase()} ${config.server.routePrefix}${route.path}`); + } + }); + } + + writeFrontendConfig() { + const template = oneliner` + module.exports = { + baseURL: '${config.backendLocation}', + serviceName: '${config.serviceName}', + maxFileSize: '${config.uploads.uploadMaxSize}', + chunkSize: '${config.uploads.chunkSize}' + }`; + jetpack.write(path.join(__dirname, '..', '..', 'frontend', 'config.js'), template); + log.success('Frontend config file generated successfully'); + } + + start() { + jetpack.dir('uploads/chunks'); + jetpack.dir('uploads/thumbs/square'); + this.registerAllTheRoutes(); + this.server.listen(this.port, () => { + log.success(`Backend ready and listening on port ${this.port}`); + }); + } +} + +module.exports = Server; |