mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-11-04 01:15:14 +00:00 
			
		
		
		
	Backend
This commit is contained in:
		
							
								
								
									
										95
									
								
								src/backend/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/backend/app.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,95 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const path        = require('path');
 | 
				
			||||||
 | 
					const express     = require('express');
 | 
				
			||||||
 | 
					const bodyParser  = require('body-parser');
 | 
				
			||||||
 | 
					const compression = require('compression');
 | 
				
			||||||
 | 
					const log         = require('./logger').express;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * App
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const app = express();
 | 
				
			||||||
 | 
					app.use(bodyParser.json());
 | 
				
			||||||
 | 
					app.use(bodyParser.urlencoded({extended: true}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Gzip
 | 
				
			||||||
 | 
					app.use(compression());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * General Logging, BEFORE routes
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.disable('x-powered-by');
 | 
				
			||||||
 | 
					app.enable('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
 | 
				
			||||||
 | 
					app.enable('strict routing');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// pretty print JSON when not live
 | 
				
			||||||
 | 
					if (process.env.NODE_ENV !== 'production') {
 | 
				
			||||||
 | 
					    app.set('json spaces', 2);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// set the view engine to ejs
 | 
				
			||||||
 | 
					app.set('view engine', 'ejs');
 | 
				
			||||||
 | 
					app.set('views', path.join(__dirname, '/views'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CORS for everything
 | 
				
			||||||
 | 
					app.use(require('./lib/express/cors'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// General security/cache related headers + server header
 | 
				
			||||||
 | 
					app.use(function (req, res, next) {
 | 
				
			||||||
 | 
					    res.set({
 | 
				
			||||||
 | 
					        'Strict-Transport-Security': 'includeSubDomains; max-age=631138519; preload',
 | 
				
			||||||
 | 
					        'X-XSS-Protection':          '0',
 | 
				
			||||||
 | 
					        'X-Content-Type-Options':    'nosniff',
 | 
				
			||||||
 | 
					        'X-Frame-Options':           'DENY',
 | 
				
			||||||
 | 
					        'Cache-Control':             'no-cache, no-store, max-age=0, must-revalidate',
 | 
				
			||||||
 | 
					        Pragma:                      'no-cache',
 | 
				
			||||||
 | 
					        Expires:                     0
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    next();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ATTACH JWT value - FOR ANY RATE LIMITERS and JWT DECODE
 | 
				
			||||||
 | 
					app.use(require('./lib/express/jwt')());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Routes
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					app.use('/assets', express.static('dist/assets'));
 | 
				
			||||||
 | 
					app.use('/css', express.static('dist/css'));
 | 
				
			||||||
 | 
					app.use('/fonts', express.static('dist/fonts'));
 | 
				
			||||||
 | 
					app.use('/images', express.static('dist/images'));
 | 
				
			||||||
 | 
					app.use('/js', express.static('dist/js'));
 | 
				
			||||||
 | 
					app.use('/api', require('./routes/api/main'));
 | 
				
			||||||
 | 
					app.use('/', require('./routes/main'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// production error handler
 | 
				
			||||||
 | 
					// no stacktraces leaked to user
 | 
				
			||||||
 | 
					app.use(function (err, req, res, next) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let payload = {
 | 
				
			||||||
 | 
					        error: {
 | 
				
			||||||
 | 
					            code:    err.status,
 | 
				
			||||||
 | 
					            message: err.public ? err.message : 'Internal Error'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (process.env.NODE_ENV === 'development') {
 | 
				
			||||||
 | 
					        payload.debug = {
 | 
				
			||||||
 | 
					            stack:    typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null,
 | 
				
			||||||
 | 
					            previous: err.previous
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Not every error is worth logging - but this is good for now until it gets annoying.
 | 
				
			||||||
 | 
					    if (typeof err.stack !== 'undefined' && err.stack) {
 | 
				
			||||||
 | 
					        log.warn(err.stack);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res
 | 
				
			||||||
 | 
					        .status(err.status || 500)
 | 
				
			||||||
 | 
					        .send(payload);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = app;
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/backend/db.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/backend/db.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let config = require('config');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (!config.has('database')) {
 | 
				
			||||||
 | 
					    throw new Error('Database config does not exist! Read the README for instructions.');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let knex = require('knex')({
 | 
				
			||||||
 | 
					    client:     config.database.engine,
 | 
				
			||||||
 | 
					    connection: {
 | 
				
			||||||
 | 
					        host:     config.database.host,
 | 
				
			||||||
 | 
					        user:     config.database.user,
 | 
				
			||||||
 | 
					        password: config.database.password,
 | 
				
			||||||
 | 
					        database: config.database.name,
 | 
				
			||||||
 | 
					        port:     config.database.port
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    migrations: {
 | 
				
			||||||
 | 
					        tableName: 'migrations'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = knex;
 | 
				
			||||||
							
								
								
									
										45
									
								
								src/backend/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/backend/index.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env node
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const config       = require('config');
 | 
				
			||||||
 | 
					const app          = require('./app');
 | 
				
			||||||
 | 
					const logger       = require('./logger').global;
 | 
				
			||||||
 | 
					const migrate      = require('./migrate');
 | 
				
			||||||
 | 
					const setup        = require('./setup');
 | 
				
			||||||
 | 
					const apiValidator = require('./lib/validator/api');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let port = process.env.PORT || 81;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (config.has('port')) {
 | 
				
			||||||
 | 
					    port = config.get('port');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function appStart () {
 | 
				
			||||||
 | 
					    return migrate.latest()
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					            return setup();
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					            return apiValidator.loadSchemas;
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					            const server = app.listen(port, () => {
 | 
				
			||||||
 | 
					                logger.info('PID ' + process.pid + ' listening on port ' + port + ' ...');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                process.on('SIGTERM', () => {
 | 
				
			||||||
 | 
					                    logger.info('PID ' + process.pid + ' received SIGTERM');
 | 
				
			||||||
 | 
					                    server.close(() => {
 | 
				
			||||||
 | 
					                        logger.info('Stopping.');
 | 
				
			||||||
 | 
					                        process.exit(0);
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch(err => {
 | 
				
			||||||
 | 
					            logger.error(err.message);
 | 
				
			||||||
 | 
					            setTimeout(appStart, 1000);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					appStart();
 | 
				
			||||||
							
								
								
									
										166
									
								
								src/backend/internal/token.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								src/backend/internal/token.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,166 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const _          = require('lodash');
 | 
				
			||||||
 | 
					const error      = require('../lib/error');
 | 
				
			||||||
 | 
					const userModel  = require('../models/user');
 | 
				
			||||||
 | 
					const authModel  = require('../models/auth');
 | 
				
			||||||
 | 
					const helpers    = require('../lib/helpers');
 | 
				
			||||||
 | 
					const TokenModel = require('../models/token');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param   {Object} data
 | 
				
			||||||
 | 
					     * @param   {String} data.identity
 | 
				
			||||||
 | 
					     * @param   {String} data.secret
 | 
				
			||||||
 | 
					     * @param   {String} [data.scope]
 | 
				
			||||||
 | 
					     * @param   {String} [data.expiry]
 | 
				
			||||||
 | 
					     * @param   {String} [issuer]
 | 
				
			||||||
 | 
					     * @returns {Promise}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    getTokenFromEmail: (data, issuer) => {
 | 
				
			||||||
 | 
					        let Token = new TokenModel();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        data.scope  = data.scope || 'user';
 | 
				
			||||||
 | 
					        data.expiry = data.expiry || '30d';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return userModel
 | 
				
			||||||
 | 
					            .query()
 | 
				
			||||||
 | 
					            .where('email', data.identity)
 | 
				
			||||||
 | 
					            .andWhere('is_deleted', 0)
 | 
				
			||||||
 | 
					            .andWhere('is_disabled', 0)
 | 
				
			||||||
 | 
					            .first()
 | 
				
			||||||
 | 
					            .then(user => {
 | 
				
			||||||
 | 
					                if (user) {
 | 
				
			||||||
 | 
					                    // Get auth
 | 
				
			||||||
 | 
					                    return authModel
 | 
				
			||||||
 | 
					                        .query()
 | 
				
			||||||
 | 
					                        .where('user_id', '=', user.id)
 | 
				
			||||||
 | 
					                        .where('type', '=', 'password')
 | 
				
			||||||
 | 
					                        .first()
 | 
				
			||||||
 | 
					                        .then(auth => {
 | 
				
			||||||
 | 
					                            if (auth) {
 | 
				
			||||||
 | 
					                                return auth.verifyPassword(data.secret)
 | 
				
			||||||
 | 
					                                    .then(valid => {
 | 
				
			||||||
 | 
					                                        if (valid) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                            if (data.scope !== 'user' && _.indexOf(user.roles, data.scope) === -1) {
 | 
				
			||||||
 | 
					                                                // The scope requested doesn't exist as a role against the user,
 | 
				
			||||||
 | 
					                                                // you shall not pass.
 | 
				
			||||||
 | 
					                                                throw new error.AuthError('Invalid scope: ' + data.scope);
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                            // Create a moment of the expiry expression
 | 
				
			||||||
 | 
					                                            let expiry = helpers.parseDatePeriod(data.expiry);
 | 
				
			||||||
 | 
					                                            if (expiry === null) {
 | 
				
			||||||
 | 
					                                                throw new error.AuthError('Invalid expiry time: ' + data.expiry);
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                            return Token.create({
 | 
				
			||||||
 | 
					                                                iss:   issuer || 'api',
 | 
				
			||||||
 | 
					                                                attrs: {
 | 
				
			||||||
 | 
					                                                    id: user.id
 | 
				
			||||||
 | 
					                                                },
 | 
				
			||||||
 | 
					                                                scope: [data.scope]
 | 
				
			||||||
 | 
					                                            }, {
 | 
				
			||||||
 | 
					                                                expiresIn: expiry.unix()
 | 
				
			||||||
 | 
					                                            })
 | 
				
			||||||
 | 
					                                                .then(signed => {
 | 
				
			||||||
 | 
					                                                    return {
 | 
				
			||||||
 | 
					                                                        token:   signed.token,
 | 
				
			||||||
 | 
					                                                        expires: expiry.toISOString()
 | 
				
			||||||
 | 
					                                                    };
 | 
				
			||||||
 | 
					                                                });
 | 
				
			||||||
 | 
					                                        } else {
 | 
				
			||||||
 | 
					                                            throw new error.AuthError('Invalid password');
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    });
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                throw new error.AuthError('No password auth for user');
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        });
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    throw new error.AuthError('No relevant user found');
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {Access} access
 | 
				
			||||||
 | 
					     * @param {Object} [data]
 | 
				
			||||||
 | 
					     * @param {String} [data.expiry]
 | 
				
			||||||
 | 
					     * @param {String} [data.scope]   Only considered if existing token scope is admin
 | 
				
			||||||
 | 
					     * @returns {Promise}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    getFreshToken: (access, data) => {
 | 
				
			||||||
 | 
					        let Token = new TokenModel();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        data        = data || {};
 | 
				
			||||||
 | 
					        data.expiry = data.expiry || '30d';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (access && access.token.get('attrs').id) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Create a moment of the expiry expression
 | 
				
			||||||
 | 
					            let expiry = helpers.parseDatePeriod(data.expiry);
 | 
				
			||||||
 | 
					            if (expiry === null) {
 | 
				
			||||||
 | 
					                throw new error.AuthError('Invalid expiry time: ' + data.expiry);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let token_attrs = {
 | 
				
			||||||
 | 
					                id: access.token.get('attrs').id
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Only admins can request otherwise scoped tokens
 | 
				
			||||||
 | 
					            let scope = access.token.get('scope');
 | 
				
			||||||
 | 
					            if (data.scope && access.token.hasScope('admin')) {
 | 
				
			||||||
 | 
					                scope = [data.scope];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (data.scope === 'job-board' || data.scope === 'worker') {
 | 
				
			||||||
 | 
					                    token_attrs.id = 0;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return Token.create({
 | 
				
			||||||
 | 
					                iss:   'api',
 | 
				
			||||||
 | 
					                scope: scope,
 | 
				
			||||||
 | 
					                attrs: token_attrs
 | 
				
			||||||
 | 
					            }, {
 | 
				
			||||||
 | 
					                expiresIn: expiry.unix()
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					                .then(signed => {
 | 
				
			||||||
 | 
					                    return {
 | 
				
			||||||
 | 
					                        token:   signed.token,
 | 
				
			||||||
 | 
					                        expires: expiry.toISOString()
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            throw new error.AssertionFailedError('Existing token contained invalid user data');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param   {Object} user
 | 
				
			||||||
 | 
					     * @returns {Promise}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    getTokenFromUser: user => {
 | 
				
			||||||
 | 
					        let Token  = new TokenModel();
 | 
				
			||||||
 | 
					        let expiry = helpers.parseDatePeriod('1d');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Token.create({
 | 
				
			||||||
 | 
					            iss:   'api',
 | 
				
			||||||
 | 
					            attrs: {
 | 
				
			||||||
 | 
					                id: user.id
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            scope: ['user']
 | 
				
			||||||
 | 
					        }, {
 | 
				
			||||||
 | 
					            expiresIn: expiry.unix()
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					            .then(signed => {
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                    token:   signed.token,
 | 
				
			||||||
 | 
					                    expires: expiry.toISOString(),
 | 
				
			||||||
 | 
					                    user:    user
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										382
									
								
								src/backend/internal/user.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										382
									
								
								src/backend/internal/user.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,382 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const _             = require('lodash');
 | 
				
			||||||
 | 
					const error         = require('../lib/error');
 | 
				
			||||||
 | 
					const userModel     = require('../models/user');
 | 
				
			||||||
 | 
					const authModel     = require('../models/auth');
 | 
				
			||||||
 | 
					const gravatar      = require('gravatar');
 | 
				
			||||||
 | 
					const internalToken = require('./token');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function omissions () {
 | 
				
			||||||
 | 
					    return ['is_deleted'];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const internalUser = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param   {Access}  access
 | 
				
			||||||
 | 
					     * @param   {Object}  data
 | 
				
			||||||
 | 
					     * @returns {Promise}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    create: (access, data) => {
 | 
				
			||||||
 | 
					        let auth = data.auth;
 | 
				
			||||||
 | 
					        delete data.auth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        data.avatar = data.avatar || '';
 | 
				
			||||||
 | 
					        data.roles  = data.roles || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (typeof data.is_disabled !== 'undefined') {
 | 
				
			||||||
 | 
					            data.is_disabled = data.is_disabled ? 1 : 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return access.can('users:create', data)
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					                data.avatar = gravatar.url(data.email, {default: 'mm'});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return userModel
 | 
				
			||||||
 | 
					                    .query()
 | 
				
			||||||
 | 
					                    .omit(omissions())
 | 
				
			||||||
 | 
					                    .insertAndFetch(data);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then(user => {
 | 
				
			||||||
 | 
					                return authModel
 | 
				
			||||||
 | 
					                    .query()
 | 
				
			||||||
 | 
					                    .insert({
 | 
				
			||||||
 | 
					                        user_id: user.id,
 | 
				
			||||||
 | 
					                        type:    auth.type,
 | 
				
			||||||
 | 
					                        secret:  auth.secret,
 | 
				
			||||||
 | 
					                        meta:    {}
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .then(() => {
 | 
				
			||||||
 | 
					                        return internalUser.get(access, {id: user.id, expand: ['services']});
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param  {Access}  access
 | 
				
			||||||
 | 
					     * @param  {Object}  data
 | 
				
			||||||
 | 
					     * @param  {Integer} data.id
 | 
				
			||||||
 | 
					     * @param  {String}  [data.name]
 | 
				
			||||||
 | 
					     * @return {Promise}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    update: (access, data) => {
 | 
				
			||||||
 | 
					        if (typeof data.is_disabled !== 'undefined') {
 | 
				
			||||||
 | 
					            data.is_disabled = data.is_disabled ? 1 : 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return access.can('users:update', data.id)
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Make sure that the user being updated doesn't change their email to another user that is already using it
 | 
				
			||||||
 | 
					                // 1. get user we want to update
 | 
				
			||||||
 | 
					                return internalUser.get(access, {id: data.id})
 | 
				
			||||||
 | 
					                    .then(user => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // 2. if email is to be changed, find other users with that email
 | 
				
			||||||
 | 
					                        if (typeof data.email !== 'undefined') {
 | 
				
			||||||
 | 
					                            data.email = data.email.toLowerCase().trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            if (user.email !== data.email) {
 | 
				
			||||||
 | 
					                                return internalUser.isEmailAvailable(data.email, data.id)
 | 
				
			||||||
 | 
					                                    .then(available => {
 | 
				
			||||||
 | 
					                                        if (!available) {
 | 
				
			||||||
 | 
					                                            throw new error.ValidationError('Email address already in use - ' + data.email);
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        return user;
 | 
				
			||||||
 | 
					                                    });
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // No change to email:
 | 
				
			||||||
 | 
					                        return user;
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then(user => {
 | 
				
			||||||
 | 
					                if (user.id !== data.id) {
 | 
				
			||||||
 | 
					                    // Sanity check that something crazy hasn't happened
 | 
				
			||||||
 | 
					                    throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                data.avatar = gravatar.url(data.email || user.email, {default: 'mm'});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return userModel
 | 
				
			||||||
 | 
					                    .query()
 | 
				
			||||||
 | 
					                    .omit(omissions())
 | 
				
			||||||
 | 
					                    .patchAndFetchById(user.id, data)
 | 
				
			||||||
 | 
					                    .then(saved_user => {
 | 
				
			||||||
 | 
					                        return _.omit(saved_user, omissions());
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					                return internalUser.get(access, {id: data.id, expand: ['services']});
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param  {Access}   access
 | 
				
			||||||
 | 
					     * @param  {Object}   [data]
 | 
				
			||||||
 | 
					     * @param  {Integer}  [data.id]          Defaults to the token user
 | 
				
			||||||
 | 
					     * @param  {Array}    [data.expand]
 | 
				
			||||||
 | 
					     * @param  {Array}    [data.omit]
 | 
				
			||||||
 | 
					     * @return {Promise}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    get: (access, data) => {
 | 
				
			||||||
 | 
					        if (typeof data === 'undefined') {
 | 
				
			||||||
 | 
					            data = {};
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (typeof data.id === 'undefined' || !data.id) {
 | 
				
			||||||
 | 
					            data.id = access.token.get('attrs').id;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return access.can('users:get', data.id)
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					                let query = userModel
 | 
				
			||||||
 | 
					                    .query()
 | 
				
			||||||
 | 
					                    .where('is_deleted', 0)
 | 
				
			||||||
 | 
					                    .andWhere('id', data.id)
 | 
				
			||||||
 | 
					                    .first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Custom omissions
 | 
				
			||||||
 | 
					                if (typeof data.omit !== 'undefined' && data.omit !== null) {
 | 
				
			||||||
 | 
					                    query.omit(data.omit);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (typeof data.expand !== 'undefined' && data.expand !== null) {
 | 
				
			||||||
 | 
					                    query.eager('[' + data.expand.join(', ') + ']');
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return query;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then(row => {
 | 
				
			||||||
 | 
					                if (row) {
 | 
				
			||||||
 | 
					                    return _.omit(row, omissions());
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    throw new error.ItemNotFoundError(data.id);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Checks if an email address is available, but if a user_id is supplied, it will ignore checking
 | 
				
			||||||
 | 
					     * against that user.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param email
 | 
				
			||||||
 | 
					     * @param user_id
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    isEmailAvailable: (email, user_id) => {
 | 
				
			||||||
 | 
					        let query = userModel
 | 
				
			||||||
 | 
					            .query()
 | 
				
			||||||
 | 
					            .where('email', '=', email.toLowerCase().trim())
 | 
				
			||||||
 | 
					            .where('is_deleted', 0)
 | 
				
			||||||
 | 
					            .first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (typeof user_id !== 'undefined') {
 | 
				
			||||||
 | 
					            query.where('id', '!=', user_id);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return query
 | 
				
			||||||
 | 
					            .then(user => {
 | 
				
			||||||
 | 
					                return !user;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {Access}  access
 | 
				
			||||||
 | 
					     * @param {Object}  data
 | 
				
			||||||
 | 
					     * @param {Integer} data.id
 | 
				
			||||||
 | 
					     * @param {String}  [data.reason]
 | 
				
			||||||
 | 
					     * @returns {Promise}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    delete: (access, data) => {
 | 
				
			||||||
 | 
					        return access.can('users:delete', data.id)
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					                return internalUser.get(access, {id: data.id});
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then(user => {
 | 
				
			||||||
 | 
					                if (!user) {
 | 
				
			||||||
 | 
					                    throw new error.ItemNotFoundError(data.id);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Make sure user can't delete themselves
 | 
				
			||||||
 | 
					                if (user.id === access.token.get('attrs').id) {
 | 
				
			||||||
 | 
					                    throw new error.PermissionError('You cannot delete yourself.');
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return userModel
 | 
				
			||||||
 | 
					                    .query()
 | 
				
			||||||
 | 
					                    .where('id', user.id)
 | 
				
			||||||
 | 
					                    .patch({
 | 
				
			||||||
 | 
					                        is_deleted: 1
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * This will only count the users
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {Access}  access
 | 
				
			||||||
 | 
					     * @param {String}  [search_query]
 | 
				
			||||||
 | 
					     * @returns {*}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    getCount: (access, search_query) => {
 | 
				
			||||||
 | 
					        return access.can('users:list')
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					                let query = userModel
 | 
				
			||||||
 | 
					                    .query()
 | 
				
			||||||
 | 
					                    .count('id as count')
 | 
				
			||||||
 | 
					                    .where('is_deleted', 0)
 | 
				
			||||||
 | 
					                    .first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Query is used for searching
 | 
				
			||||||
 | 
					                if (typeof search_query === 'string') {
 | 
				
			||||||
 | 
					                    query.where(function () {
 | 
				
			||||||
 | 
					                        this.where('user.name', 'like', '%' + search_query + '%')
 | 
				
			||||||
 | 
					                            .orWhere('user.email', 'like', '%' + search_query + '%');
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return query;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then(row => {
 | 
				
			||||||
 | 
					                return parseInt(row.count, 10);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * All users
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param   {Access}  access
 | 
				
			||||||
 | 
					     * @param   {Integer} [start]
 | 
				
			||||||
 | 
					     * @param   {Integer} [limit]
 | 
				
			||||||
 | 
					     * @param   {Array}   [sort]
 | 
				
			||||||
 | 
					     * @param   {Array}   [expand]
 | 
				
			||||||
 | 
					     * @param   {String}  [search_query]
 | 
				
			||||||
 | 
					     * @returns {Promise}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    getAll: (access, start, limit, sort, expand, search_query) => {
 | 
				
			||||||
 | 
					        return access.can('users:list')
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					                let query = userModel
 | 
				
			||||||
 | 
					                    .query()
 | 
				
			||||||
 | 
					                    .where('is_deleted', 0)
 | 
				
			||||||
 | 
					                    .groupBy('id')
 | 
				
			||||||
 | 
					                    .limit(limit ? limit : 100)
 | 
				
			||||||
 | 
					                    .omit(['is_deleted']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (typeof start !== 'undefined' && start !== null) {
 | 
				
			||||||
 | 
					                    query.offset(start);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (typeof sort !== 'undefined' && sort !== null) {
 | 
				
			||||||
 | 
					                    _.map(sort, (item) => {
 | 
				
			||||||
 | 
					                        query.orderBy(item.field, item.dir);
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    query.orderBy('name', 'DESC');
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Query is used for searching
 | 
				
			||||||
 | 
					                if (typeof search_query === 'string') {
 | 
				
			||||||
 | 
					                    query.where(function () {
 | 
				
			||||||
 | 
					                        this.where('name', 'like', '%' + search_query + '%')
 | 
				
			||||||
 | 
					                            .orWhere('email', 'like', '%' + search_query + '%');
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (typeof expand !== 'undefined' && expand !== null) {
 | 
				
			||||||
 | 
					                    query.eager('[' + expand.join(', ') + ']');
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return query;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param   {Access} access
 | 
				
			||||||
 | 
					     * @param   {Integer} [id_requested]
 | 
				
			||||||
 | 
					     * @returns {[String]}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    getUserOmisionsByAccess: (access, id_requested) => {
 | 
				
			||||||
 | 
					        let response = []; // Admin response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!access.token.hasScope('admin') && access.token.get('attrs').id !== id_requested) {
 | 
				
			||||||
 | 
					            response = ['roles', 'is_deleted']; // Restricted response
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return response;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param  {Access}  access
 | 
				
			||||||
 | 
					     * @param  {Object}  data
 | 
				
			||||||
 | 
					     * @param  {Integer} data.id
 | 
				
			||||||
 | 
					     * @param  {String}  data.type
 | 
				
			||||||
 | 
					     * @param  {String}  data.secret
 | 
				
			||||||
 | 
					     * @return {Promise}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    setPassword: (access, data) => {
 | 
				
			||||||
 | 
					        return access.can('users:password', data.id)
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					                return internalUser.get(access, {id: data.id});
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then(user => {
 | 
				
			||||||
 | 
					                if (user.id !== data.id) {
 | 
				
			||||||
 | 
					                    // Sanity check that something crazy hasn't happened
 | 
				
			||||||
 | 
					                    throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (user.id === access.token.get('attrs').id) {
 | 
				
			||||||
 | 
					                    // they're setting their own password. Make sure their current password is correct
 | 
				
			||||||
 | 
					                    if (typeof data.current === 'undefined' || !data.current) {
 | 
				
			||||||
 | 
					                        throw new error.ValidationError('Current password was not supplied');
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return internalToken.getTokenFromEmail({
 | 
				
			||||||
 | 
					                        identity: user.email,
 | 
				
			||||||
 | 
					                        secret:   data.current
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                        .then(() => {
 | 
				
			||||||
 | 
					                            return user;
 | 
				
			||||||
 | 
					                        });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return user;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then(user => {
 | 
				
			||||||
 | 
					                return authModel
 | 
				
			||||||
 | 
					                    .query()
 | 
				
			||||||
 | 
					                    .where('user_id', user.id)
 | 
				
			||||||
 | 
					                    .andWhere('type', data.type)
 | 
				
			||||||
 | 
					                    .patch({
 | 
				
			||||||
 | 
					                        type:   data.type,
 | 
				
			||||||
 | 
					                        secret: data.secret
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .then(() => {
 | 
				
			||||||
 | 
					                        return true;
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {Access}   access
 | 
				
			||||||
 | 
					     * @param {Object}   data
 | 
				
			||||||
 | 
					     * @param {Integer}  data.id
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    loginAs: (access, data) => {
 | 
				
			||||||
 | 
					        return access.can('users:loginas', data.id)
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					                return internalUser.get(access, data);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then(user => {
 | 
				
			||||||
 | 
					                return internalToken.getTokenFromUser(user);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = internalUser;
 | 
				
			||||||
							
								
								
									
										256
									
								
								src/backend/lib/access.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								src/backend/lib/access.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,256 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const _          = require('lodash');
 | 
				
			||||||
 | 
					const validator  = require('ajv');
 | 
				
			||||||
 | 
					const error      = require('./error');
 | 
				
			||||||
 | 
					const userModel  = require('../models/user');
 | 
				
			||||||
 | 
					const TokenModel = require('../models/token');
 | 
				
			||||||
 | 
					const roleSchema = require('./access/roles.json');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = function (token_string) {
 | 
				
			||||||
 | 
					    let Token                 = new TokenModel();
 | 
				
			||||||
 | 
					    let token_data            = null;
 | 
				
			||||||
 | 
					    let initialised           = false;
 | 
				
			||||||
 | 
					    let object_cache          = {};
 | 
				
			||||||
 | 
					    let allow_internal_access = false;
 | 
				
			||||||
 | 
					    let user_roles            = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Loads the Token object from the token string
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns {Promise}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.init = () => {
 | 
				
			||||||
 | 
					        return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					            if (initialised) {
 | 
				
			||||||
 | 
					                resolve();
 | 
				
			||||||
 | 
					            } else if (!token_string) {
 | 
				
			||||||
 | 
					                reject(new error.PermissionError('Permission Denied'));
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                resolve(Token.load(token_string)
 | 
				
			||||||
 | 
					                    .then((data) => {
 | 
				
			||||||
 | 
					                        token_data = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // At this point we need to load the user from the DB and make sure they:
 | 
				
			||||||
 | 
					                        // - exist (and not soft deleted)
 | 
				
			||||||
 | 
					                        // - still have the appropriate scopes for this token
 | 
				
			||||||
 | 
					                        // This is only required when the User ID is supplied or if the token scope has `user`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (token_data.attrs.id || (typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'user') !== -1)) {
 | 
				
			||||||
 | 
					                            // Has token user id or token user scope
 | 
				
			||||||
 | 
					                            return userModel
 | 
				
			||||||
 | 
					                                .query()
 | 
				
			||||||
 | 
					                                .where('id', token_data.attrs.id)
 | 
				
			||||||
 | 
					                                .andWhere('is_deleted', 0)
 | 
				
			||||||
 | 
					                                .andWhere('is_disabled', 0)
 | 
				
			||||||
 | 
					                                .first('id')
 | 
				
			||||||
 | 
					                                .then((user) => {
 | 
				
			||||||
 | 
					                                    if (user) {
 | 
				
			||||||
 | 
					                                        // make sure user has all scopes of the token
 | 
				
			||||||
 | 
					                                        // The `user` role is not added against the user row, so we have to just add it here to get past this check.
 | 
				
			||||||
 | 
					                                        user.roles.push('user');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        let is_ok = true;
 | 
				
			||||||
 | 
					                                        _.forEach(token_data.scope, (scope_item) => {
 | 
				
			||||||
 | 
					                                            if (_.indexOf(user.roles, scope_item) === -1) {
 | 
				
			||||||
 | 
					                                                is_ok = false;
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        if (!is_ok) {
 | 
				
			||||||
 | 
					                                            throw new error.AuthError('Invalid token scope for User');
 | 
				
			||||||
 | 
					                                        } else {
 | 
				
			||||||
 | 
					                                            initialised = true;
 | 
				
			||||||
 | 
					                                            user_roles  = user.roles;
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    } else {
 | 
				
			||||||
 | 
					                                        throw new error.AuthError('User cannot be loaded for Token');
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                });
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            initialised = true;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Fetches the object ids from the database, only once per object type, for this token.
 | 
				
			||||||
 | 
					     * This only applies to USER token scopes, as all other tokens are not really bound
 | 
				
			||||||
 | 
					     * by object scopes
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param   {String} object_type
 | 
				
			||||||
 | 
					     * @returns {Promise}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.loadObjects = object_type => {
 | 
				
			||||||
 | 
					        return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					            if (Token.hasScope('user')) {
 | 
				
			||||||
 | 
					                if (typeof token_data.attrs.id === 'undefined' || !token_data.attrs.id) {
 | 
				
			||||||
 | 
					                    reject(new error.AuthError('User Token supplied without a User ID'));
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    let token_user_id = token_data.attrs.id ? token_data.attrs.id : 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (typeof object_cache[object_type] === 'undefined') {
 | 
				
			||||||
 | 
					                        switch (object_type) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            // USERS - should only return yourself
 | 
				
			||||||
 | 
					                            case 'users':
 | 
				
			||||||
 | 
					                                resolve(token_user_id ? [token_user_id] : []);
 | 
				
			||||||
 | 
					                                break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            // DEFAULT: null
 | 
				
			||||||
 | 
					                            default:
 | 
				
			||||||
 | 
					                                resolve(null);
 | 
				
			||||||
 | 
					                                break;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        resolve(object_cache[object_type]);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                resolve(null);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					            .then(objects => {
 | 
				
			||||||
 | 
					                object_cache[object_type] = objects;
 | 
				
			||||||
 | 
					                return objects;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {String} permission_label
 | 
				
			||||||
 | 
					     * @returns {Object}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getObjectSchema = permission_label => {
 | 
				
			||||||
 | 
					        let base_object_type = permission_label.split(':').shift();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let schema = {
 | 
				
			||||||
 | 
					            $id:                  'objects',
 | 
				
			||||||
 | 
					            $schema:              'http://json-schema.org/draft-07/schema#',
 | 
				
			||||||
 | 
					            description:          'Actor Properties',
 | 
				
			||||||
 | 
					            type:                 'object',
 | 
				
			||||||
 | 
					            additionalProperties: false,
 | 
				
			||||||
 | 
					            properties:           {
 | 
				
			||||||
 | 
					                user_id: {
 | 
				
			||||||
 | 
					                    anyOf: [
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            type: 'number',
 | 
				
			||||||
 | 
					                            enum: [Token.get('attrs').id]
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    ]
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                scope:   {
 | 
				
			||||||
 | 
					                    type:    'string',
 | 
				
			||||||
 | 
					                    pattern: '^' + Token.get('scope') + '$'
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.loadObjects(base_object_type)
 | 
				
			||||||
 | 
					            .then(object_result => {
 | 
				
			||||||
 | 
					                if (typeof object_result === 'object' && object_result !== null) {
 | 
				
			||||||
 | 
					                    schema.properties[base_object_type] = {
 | 
				
			||||||
 | 
					                        type:    'number',
 | 
				
			||||||
 | 
					                        enum:    object_result,
 | 
				
			||||||
 | 
					                        minimum: 1
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    schema.properties[base_object_type] = {
 | 
				
			||||||
 | 
					                        type:    'number',
 | 
				
			||||||
 | 
					                        minimum: 1
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return schema;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        token: Token,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         *
 | 
				
			||||||
 | 
					         * @param   {Boolean}  [allow_internal]
 | 
				
			||||||
 | 
					         * @returns {Promise}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        load: allow_internal => {
 | 
				
			||||||
 | 
					            return new Promise(function (resolve/*, reject*/) {
 | 
				
			||||||
 | 
					                if (token_string) {
 | 
				
			||||||
 | 
					                    resolve(Token.load(token_string));
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    allow_internal_access = allow_internal;
 | 
				
			||||||
 | 
					                    resolve(allow_internal_access || null);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         *
 | 
				
			||||||
 | 
					         * @param {String}  permission
 | 
				
			||||||
 | 
					         * @param {*}       [data]
 | 
				
			||||||
 | 
					         * @returns {Promise}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        can: (permission, data) => {
 | 
				
			||||||
 | 
					            if (allow_internal_access === true) {
 | 
				
			||||||
 | 
					                return Promise.resolve(true);
 | 
				
			||||||
 | 
					                //return true;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                return this.init()
 | 
				
			||||||
 | 
					                    .then(() => {
 | 
				
			||||||
 | 
					                        // Initialised, token decoded ok
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        return this.getObjectSchema(permission)
 | 
				
			||||||
 | 
					                            .then(objectSchema => {
 | 
				
			||||||
 | 
					                                let data_schema = {
 | 
				
			||||||
 | 
					                                    [permission]: {
 | 
				
			||||||
 | 
					                                        data:  data,
 | 
				
			||||||
 | 
					                                        scope: Token.get('scope'),
 | 
				
			||||||
 | 
					                                        roles: user_roles
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                let permissionSchema = {
 | 
				
			||||||
 | 
					                                    $schema:              'http://json-schema.org/draft-07/schema#',
 | 
				
			||||||
 | 
					                                    $async:               true,
 | 
				
			||||||
 | 
					                                    $id:                  'permissions',
 | 
				
			||||||
 | 
					                                    additionalProperties: false,
 | 
				
			||||||
 | 
					                                    properties:           {}
 | 
				
			||||||
 | 
					                                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                //console.log('objectSchema:', JSON.stringify(objectSchema, null, 2));
 | 
				
			||||||
 | 
					                                //console.log('permissionSchema:', JSON.stringify(permissionSchema, null, 2));
 | 
				
			||||||
 | 
					                                //console.log('data_schema:', JSON.stringify(data_schema, null, 2));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                let ajv = validator({
 | 
				
			||||||
 | 
					                                    verbose:      true,
 | 
				
			||||||
 | 
					                                    allErrors:    true,
 | 
				
			||||||
 | 
					                                    format:       'full',
 | 
				
			||||||
 | 
					                                    missingRefs:  'fail',
 | 
				
			||||||
 | 
					                                    breakOnError: true,
 | 
				
			||||||
 | 
					                                    coerceTypes:  true,
 | 
				
			||||||
 | 
					                                    schemas:      [
 | 
				
			||||||
 | 
					                                        roleSchema,
 | 
				
			||||||
 | 
					                                        objectSchema,
 | 
				
			||||||
 | 
					                                        permissionSchema
 | 
				
			||||||
 | 
					                                    ]
 | 
				
			||||||
 | 
					                                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                return ajv.validate('permissions', data_schema);
 | 
				
			||||||
 | 
					                            });
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .catch(err => {
 | 
				
			||||||
 | 
					                        //console.log(err.message);
 | 
				
			||||||
 | 
					                        //console.log(err.errors);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        throw new error.PermissionError('Permission Denied', err);
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										45
									
								
								src/backend/lib/access/roles.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/backend/lib/access/roles.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "$schema": "http://json-schema.org/draft-07/schema#",
 | 
				
			||||||
 | 
					    "$id": "roles",
 | 
				
			||||||
 | 
					    "definitions": {
 | 
				
			||||||
 | 
					        "admin": {
 | 
				
			||||||
 | 
					            "type": "object",
 | 
				
			||||||
 | 
					            "required": [
 | 
				
			||||||
 | 
					                "scope",
 | 
				
			||||||
 | 
					                "roles"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "properties": {
 | 
				
			||||||
 | 
					                "scope": {
 | 
				
			||||||
 | 
					                    "type": "array",
 | 
				
			||||||
 | 
					                    "contains": {
 | 
				
			||||||
 | 
					                        "type": "string",
 | 
				
			||||||
 | 
					                        "pattern": "^user$"
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                "roles": {
 | 
				
			||||||
 | 
					                    "type": "array",
 | 
				
			||||||
 | 
					                    "contains": {
 | 
				
			||||||
 | 
					                        "type": "string",
 | 
				
			||||||
 | 
					                        "pattern": "^admin$"
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "user": {
 | 
				
			||||||
 | 
					            "type": "object",
 | 
				
			||||||
 | 
					            "required": [
 | 
				
			||||||
 | 
					                "scope"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "properties": {
 | 
				
			||||||
 | 
					                "scope": {
 | 
				
			||||||
 | 
					                    "type": "array",
 | 
				
			||||||
 | 
					                    "contains": {
 | 
				
			||||||
 | 
					                        "type": "string",
 | 
				
			||||||
 | 
					                        "pattern": "^user$"
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/backend/lib/access/users-get.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/backend/lib/access/users-get.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "anyOf": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "$ref": "roles#/definitions/admin"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "type": "object",
 | 
				
			||||||
 | 
					      "required": ["data", "scope"],
 | 
				
			||||||
 | 
					      "properties": {
 | 
				
			||||||
 | 
					        "data": {
 | 
				
			||||||
 | 
					          "$ref": "objects#/properties/users"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "scope": {
 | 
				
			||||||
 | 
					          "type": "array",
 | 
				
			||||||
 | 
					          "contains": {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "pattern": "^user$"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										7
									
								
								src/backend/lib/access/users-list.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/backend/lib/access/users-list.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "anyOf": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "$ref": "roles#/definitions/admin"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										83
									
								
								src/backend/lib/error.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/backend/lib/error.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const _    = require('lodash');
 | 
				
			||||||
 | 
					const util = require('util');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PermissionError: function (message, previous) {
 | 
				
			||||||
 | 
					        Error.captureStackTrace(this, this.constructor);
 | 
				
			||||||
 | 
					        this.name     = this.constructor.name;
 | 
				
			||||||
 | 
					        this.previous = previous;
 | 
				
			||||||
 | 
					        this.message  = 'Permission Denied';
 | 
				
			||||||
 | 
					        this.public   = true;
 | 
				
			||||||
 | 
					        this.status   = 403;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ItemNotFoundError: function (id, previous) {
 | 
				
			||||||
 | 
					        Error.captureStackTrace(this, this.constructor);
 | 
				
			||||||
 | 
					        this.name     = this.constructor.name;
 | 
				
			||||||
 | 
					        this.previous = previous;
 | 
				
			||||||
 | 
					        this.message  = 'Item Not Found - ' + id;
 | 
				
			||||||
 | 
					        this.public   = true;
 | 
				
			||||||
 | 
					        this.status   = 404;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    AuthError: function (message, previous) {
 | 
				
			||||||
 | 
					        Error.captureStackTrace(this, this.constructor);
 | 
				
			||||||
 | 
					        this.name     = this.constructor.name;
 | 
				
			||||||
 | 
					        this.previous = previous;
 | 
				
			||||||
 | 
					        this.message  = message;
 | 
				
			||||||
 | 
					        this.public   = true;
 | 
				
			||||||
 | 
					        this.status   = 401;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    InternalError: function (message, previous) {
 | 
				
			||||||
 | 
					        Error.captureStackTrace(this, this.constructor);
 | 
				
			||||||
 | 
					        this.name     = this.constructor.name;
 | 
				
			||||||
 | 
					        this.previous = previous;
 | 
				
			||||||
 | 
					        this.message  = message;
 | 
				
			||||||
 | 
					        this.status   = 500;
 | 
				
			||||||
 | 
					        this.public   = false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    InternalValidationError: function (message, previous) {
 | 
				
			||||||
 | 
					        Error.captureStackTrace(this, this.constructor);
 | 
				
			||||||
 | 
					        this.name     = this.constructor.name;
 | 
				
			||||||
 | 
					        this.previous = previous;
 | 
				
			||||||
 | 
					        this.message  = message;
 | 
				
			||||||
 | 
					        this.status   = 400;
 | 
				
			||||||
 | 
					        this.public   = false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    CacheError: function (message, previous) {
 | 
				
			||||||
 | 
					        Error.captureStackTrace(this, this.constructor);
 | 
				
			||||||
 | 
					        this.name     = this.constructor.name;
 | 
				
			||||||
 | 
					        this.message  = message;
 | 
				
			||||||
 | 
					        this.previous = previous;
 | 
				
			||||||
 | 
					        this.status   = 500;
 | 
				
			||||||
 | 
					        this.public   = false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ValidationError: function (message, previous) {
 | 
				
			||||||
 | 
					        Error.captureStackTrace(this, this.constructor);
 | 
				
			||||||
 | 
					        this.name     = this.constructor.name;
 | 
				
			||||||
 | 
					        this.previous = previous;
 | 
				
			||||||
 | 
					        this.message  = message;
 | 
				
			||||||
 | 
					        this.public   = true;
 | 
				
			||||||
 | 
					        this.status   = 400;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    AssertionFailedError: function (message, previous) {
 | 
				
			||||||
 | 
					        Error.captureStackTrace(this, this.constructor);
 | 
				
			||||||
 | 
					        this.name     = this.constructor.name;
 | 
				
			||||||
 | 
					        this.previous = previous;
 | 
				
			||||||
 | 
					        this.message  = message;
 | 
				
			||||||
 | 
					        this.public   = false;
 | 
				
			||||||
 | 
					        this.status   = 400;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_.forEach(module.exports, function (error) {
 | 
				
			||||||
 | 
					    util.inherits(error, Error);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/backend/lib/express/cors.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/backend/lib/express/cors.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const validator = require('../validator');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = function (req, res, next) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (req.headers.origin) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // very relaxed validation....
 | 
				
			||||||
 | 
					        validator({
 | 
				
			||||||
 | 
					            type:    'string',
 | 
				
			||||||
 | 
					            pattern: '^[a-z\\-]+:\\/\\/(?:[\\w\\-\\.]+(:[0-9]+)?/?)?$'
 | 
				
			||||||
 | 
					        }, req.headers.origin)
 | 
				
			||||||
 | 
					            .then(function () {
 | 
				
			||||||
 | 
					                res.set({
 | 
				
			||||||
 | 
					                    'Access-Control-Allow-Origin':      req.headers.origin,
 | 
				
			||||||
 | 
					                    'Access-Control-Allow-Credentials': true,
 | 
				
			||||||
 | 
					                    'Access-Control-Allow-Methods':     'OPTIONS, GET, POST',
 | 
				
			||||||
 | 
					                    'Access-Control-Allow-Headers':     'Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit',
 | 
				
			||||||
 | 
					                    'Access-Control-Max-Age':           5 * 60,
 | 
				
			||||||
 | 
					                    'Access-Control-Expose-Headers':    'X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit'
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                next();
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(next);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        // No origin
 | 
				
			||||||
 | 
					        next();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										17
									
								
								src/backend/lib/express/jwt-decode.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/backend/lib/express/jwt-decode.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Access = require('../access');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = () => {
 | 
				
			||||||
 | 
					    return function (req, res, next) {
 | 
				
			||||||
 | 
					        res.locals.access = null;
 | 
				
			||||||
 | 
					        let access        = new Access(res.locals.token || null);
 | 
				
			||||||
 | 
					        access.load()
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					                res.locals.access = access;
 | 
				
			||||||
 | 
					                next();
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(next);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										15
									
								
								src/backend/lib/express/jwt.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/backend/lib/express/jwt.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = function () {
 | 
				
			||||||
 | 
					    return function (req, res, next) {
 | 
				
			||||||
 | 
					        if (req.headers.authorization) {
 | 
				
			||||||
 | 
					            let parts = req.headers.authorization.split(' ');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (parts && parts[0] === 'Bearer' && parts[1]) {
 | 
				
			||||||
 | 
					                res.locals.token = parts[1];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        next();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										57
									
								
								src/backend/lib/express/pagination.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/backend/lib/express/pagination.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let _ = require('lodash');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = function (default_sort, default_offset, default_limit, max_limit) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * This will setup the req query params with filtered data and defaults
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * sort    will be an array of fields and their direction
 | 
				
			||||||
 | 
					     * offset  will be an int, defaulting to zero if no other default supplied
 | 
				
			||||||
 | 
					     * limit   will be an int, defaulting to 50 if no other default supplied, and limited to the max if that was supplied
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return function (req, res, next) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        req.query.offset = typeof req.query.limit === 'undefined' ? default_offset || 0 : parseInt(req.query.offset, 10);
 | 
				
			||||||
 | 
					        req.query.limit  = typeof req.query.limit === 'undefined' ? default_limit || 50 : parseInt(req.query.limit, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (max_limit && req.query.limit > max_limit) {
 | 
				
			||||||
 | 
					            req.query.limit = max_limit;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Sorting
 | 
				
			||||||
 | 
					        let sort       = typeof req.query.sort === 'undefined' ? default_sort : req.query.sort;
 | 
				
			||||||
 | 
					        let myRegexp   = /.*\.(asc|desc)$/ig;
 | 
				
			||||||
 | 
					        let sort_array = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sort = sort.split(',');
 | 
				
			||||||
 | 
					        _.map(sort, function (val) {
 | 
				
			||||||
 | 
					            let matches = myRegexp.exec(val);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (matches !== null) {
 | 
				
			||||||
 | 
					                let dir = matches[1];
 | 
				
			||||||
 | 
					                sort_array.push({
 | 
				
			||||||
 | 
					                    field: val.substr(0, val.length - (dir.length + 1)),
 | 
				
			||||||
 | 
					                    dir:   dir.toLowerCase()
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                sort_array.push({
 | 
				
			||||||
 | 
					                    field: val,
 | 
				
			||||||
 | 
					                    dir:   'asc'
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Sort will now be in this format:
 | 
				
			||||||
 | 
					        // [
 | 
				
			||||||
 | 
					        //    { field: 'field1', dir: 'asc' },
 | 
				
			||||||
 | 
					        //    { field: 'field2', dir: 'desc' }
 | 
				
			||||||
 | 
					        // ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        req.query.sort = sort_array;
 | 
				
			||||||
 | 
					        next();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/backend/lib/express/user-id-from-me.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/backend/lib/express/user-id-from-me.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = (req, res, next) => {
 | 
				
			||||||
 | 
					    if (req.params.user_id === 'me' && res.locals.access) {
 | 
				
			||||||
 | 
					        req.params.user_id = res.locals.access.token.get('attrs').id;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        req.params.user_id = parseInt(req.params.user_id, 10);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    next();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										35
									
								
								src/backend/lib/helpers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/backend/lib/helpers.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const moment = require('moment');
 | 
				
			||||||
 | 
					const _      = require('lodash');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Takes an expression such as 30d and returns a moment object of that date in future
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * Key      Shorthand
 | 
				
			||||||
 | 
					     * ==================
 | 
				
			||||||
 | 
					     * years         y
 | 
				
			||||||
 | 
					     * quarters      Q
 | 
				
			||||||
 | 
					     * months        M
 | 
				
			||||||
 | 
					     * weeks         w
 | 
				
			||||||
 | 
					     * days          d
 | 
				
			||||||
 | 
					     * hours         h
 | 
				
			||||||
 | 
					     * minutes       m
 | 
				
			||||||
 | 
					     * seconds       s
 | 
				
			||||||
 | 
					     * milliseconds  ms
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {String}  expression
 | 
				
			||||||
 | 
					     * @returns {Object}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    parseDatePeriod: function (expression) {
 | 
				
			||||||
 | 
					        let matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m);
 | 
				
			||||||
 | 
					        if (matches) {
 | 
				
			||||||
 | 
					            return moment().add(matches[1], matches[2]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										57
									
								
								src/backend/lib/migrate_template.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/backend/lib/migrate_template.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const migrate_name = 'identifier_for_migrate';
 | 
				
			||||||
 | 
					const logger       = require('../logger').migrate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Migrate
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @see http://knexjs.org/#Schema
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {Object} knex
 | 
				
			||||||
 | 
					 * @param {Promise} Promise
 | 
				
			||||||
 | 
					 * @returns {Promise}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					exports.up = function (knex, Promise) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    logger.info('[' + migrate_name + '] Migrating Up...');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Create Table example:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /*return knex.schema.createTable('notification', (table) => {
 | 
				
			||||||
 | 
					         table.increments().primary();
 | 
				
			||||||
 | 
					         table.string('name').notNull();
 | 
				
			||||||
 | 
					         table.string('type').notNull();
 | 
				
			||||||
 | 
					         table.integer('created_on').notNull();
 | 
				
			||||||
 | 
					         table.integer('modified_on').notNull();
 | 
				
			||||||
 | 
					     })
 | 
				
			||||||
 | 
					     .then(function () {
 | 
				
			||||||
 | 
					        logger.info('[' + migrate_name + '] Notification Table created');
 | 
				
			||||||
 | 
					     });*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    logger.info('[' + migrate_name + '] Migrating Up Complete');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Promise.resolve(true);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Undo Migrate
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {Object} knex
 | 
				
			||||||
 | 
					 * @param {Promise} Promise
 | 
				
			||||||
 | 
					 * @returns {Promise}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					exports.down = function (knex, Promise) {
 | 
				
			||||||
 | 
					    logger.info('[' + migrate_name + '] Migrating Down...');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Drop table example:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /*return knex.schema.dropTable('notification')
 | 
				
			||||||
 | 
					     .then(() => {
 | 
				
			||||||
 | 
					        logger.info('[' + migrate_name + '] Notification Table dropped');
 | 
				
			||||||
 | 
					     });*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    logger.info('[' + migrate_name + '] Migrating Down Complete');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Promise.resolve(true);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										47
									
								
								src/backend/lib/validator/api.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/backend/lib/validator/api.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const error  = require('../error');
 | 
				
			||||||
 | 
					const path   = require('path');
 | 
				
			||||||
 | 
					const parser = require('json-schema-ref-parser');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ajv = require('ajv')({
 | 
				
			||||||
 | 
					    verbose:        true,
 | 
				
			||||||
 | 
					    validateSchema: true,
 | 
				
			||||||
 | 
					    allErrors:      false,
 | 
				
			||||||
 | 
					    format:         'full',
 | 
				
			||||||
 | 
					    coerceTypes:    true
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @param {Object} schema
 | 
				
			||||||
 | 
					 * @param {Object} payload
 | 
				
			||||||
 | 
					 * @returns {Promise}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function apiValidator (schema, payload/*, description*/) {
 | 
				
			||||||
 | 
					    return new Promise(function Promise_apiValidator (resolve, reject) {
 | 
				
			||||||
 | 
					        if (typeof payload === 'undefined') {
 | 
				
			||||||
 | 
					            reject(new error.ValidationError('Payload is undefined'));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let validate = ajv.compile(schema);
 | 
				
			||||||
 | 
					        let valid    = validate(payload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (valid && !validate.errors) {
 | 
				
			||||||
 | 
					            resolve(payload);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            let message = ajv.errorsText(validate.errors);
 | 
				
			||||||
 | 
					            let err     = new error.ValidationError(message);
 | 
				
			||||||
 | 
					            err.debug   = [validate.errors, payload];
 | 
				
			||||||
 | 
					            reject(err);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					apiValidator.loadSchemas = parser
 | 
				
			||||||
 | 
					    .dereference(path.resolve('src/backend/schema/index.json'))
 | 
				
			||||||
 | 
					    .then(schema => {
 | 
				
			||||||
 | 
					        ajv.addSchema(schema);
 | 
				
			||||||
 | 
					        return schema;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = apiValidator;
 | 
				
			||||||
							
								
								
									
										53
									
								
								src/backend/lib/validator/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/backend/lib/validator/index.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const _           = require('lodash');
 | 
				
			||||||
 | 
					const error       = require('../error');
 | 
				
			||||||
 | 
					const definitions = require('../../schema/definitions.json');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RegExp.prototype.toJSON = RegExp.prototype.toString;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ajv = require('ajv')({
 | 
				
			||||||
 | 
					    verbose:     true, //process.env.NODE_ENV === 'development',
 | 
				
			||||||
 | 
					    allErrors:   true,
 | 
				
			||||||
 | 
					    format:      'full',  // strict regexes for format checks
 | 
				
			||||||
 | 
					    coerceTypes: true,
 | 
				
			||||||
 | 
					    schemas:     [
 | 
				
			||||||
 | 
					        definitions
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {Object} schema
 | 
				
			||||||
 | 
					 * @param {Object} payload
 | 
				
			||||||
 | 
					 * @returns {Promise}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function validator (schema, payload) {
 | 
				
			||||||
 | 
					    return new Promise(function (resolve, reject) {
 | 
				
			||||||
 | 
					        if (!payload) {
 | 
				
			||||||
 | 
					            reject(new error.InternalValidationError('Payload is falsy'));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                let validate = ajv.compile(schema);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let valid = validate(payload);
 | 
				
			||||||
 | 
					                if (valid && !validate.errors) {
 | 
				
			||||||
 | 
					                    resolve(_.cloneDeep(payload));
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    //console.log('Validation failed:', schema, payload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    let message = ajv.errorsText(validate.errors);
 | 
				
			||||||
 | 
					                    reject(new error.InternalValidationError(message));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            } catch (err) {
 | 
				
			||||||
 | 
					                reject(err);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = validator;
 | 
				
			||||||
							
								
								
									
										7
									
								
								src/backend/logger.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/backend/logger.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					const {Signale} = require('signale');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					    global:  new Signale({scope: 'Global    '}),
 | 
				
			||||||
 | 
					    migrate: new Signale({scope: 'Migrate   '}),
 | 
				
			||||||
 | 
					    express: new Signale({scope: 'Express   '})
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										17
									
								
								src/backend/migrate.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/backend/migrate.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const db     = require('./db');
 | 
				
			||||||
 | 
					const logger = require('./logger').migrate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					    latest: function () {
 | 
				
			||||||
 | 
					        return db.migrate.currentVersion()
 | 
				
			||||||
 | 
					            .then(version => {
 | 
				
			||||||
 | 
					                logger.info('Current database version:', version);
 | 
				
			||||||
 | 
					                return db.migrate.latest({
 | 
				
			||||||
 | 
					                    tableName: 'migrations',
 | 
				
			||||||
 | 
					                    directory: 'src/backend/migrations'
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										60
									
								
								src/backend/migrations/20180618015850_initial.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/backend/migrations/20180618015850_initial.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const migrate_name = 'initial-schema';
 | 
				
			||||||
 | 
					const logger       = require('../logger').migrate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Migrate
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @see http://knexjs.org/#Schema
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param   {Object}  knex
 | 
				
			||||||
 | 
					 * @param   {Promise} Promise
 | 
				
			||||||
 | 
					 * @returns {Promise}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					exports.up = function (knex/*, Promise*/) {
 | 
				
			||||||
 | 
					    logger.info('[' + migrate_name + '] Migrating Up...');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return knex.schema.createTable('auth', table => {
 | 
				
			||||||
 | 
					        table.increments().primary();
 | 
				
			||||||
 | 
					        table.dateTime('created_on').notNull();
 | 
				
			||||||
 | 
					        table.dateTime('modified_on').notNull();
 | 
				
			||||||
 | 
					        table.integer('user_id').notNull().unsigned();
 | 
				
			||||||
 | 
					        table.string('type', 30).notNull();
 | 
				
			||||||
 | 
					        table.string('secret').notNull();
 | 
				
			||||||
 | 
					        table.json('meta').notNull();
 | 
				
			||||||
 | 
					        table.integer('is_deleted').notNull().unsigned().defaultTo(0);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					            logger.info('[' + migrate_name + '] auth Table created');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return knex.schema.createTable('user', table => {
 | 
				
			||||||
 | 
					                table.increments().primary();
 | 
				
			||||||
 | 
					                table.dateTime('created_on').notNull();
 | 
				
			||||||
 | 
					                table.dateTime('modified_on').notNull();
 | 
				
			||||||
 | 
					                table.integer('is_deleted').notNull().unsigned().defaultTo(0);
 | 
				
			||||||
 | 
					                table.integer('is_disabled').notNull().unsigned().defaultTo(0);
 | 
				
			||||||
 | 
					                table.string('email').notNull();
 | 
				
			||||||
 | 
					                table.string('name').notNull();
 | 
				
			||||||
 | 
					                table.string('nickname').notNull();
 | 
				
			||||||
 | 
					                table.string('avatar').notNull();
 | 
				
			||||||
 | 
					                table.json('roles').notNull();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					            logger.info('[' + migrate_name + '] user Table created');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Undo Migrate
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param   {Object}  knex
 | 
				
			||||||
 | 
					 * @param   {Promise} Promise
 | 
				
			||||||
 | 
					 * @returns {Promise}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					exports.down = function (knex, Promise) {
 | 
				
			||||||
 | 
					    logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.');
 | 
				
			||||||
 | 
					    return Promise.resolve(true);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										82
									
								
								src/backend/models/auth.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/backend/models/auth.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					// Objection Docs:
 | 
				
			||||||
 | 
					// http://vincit.github.io/objection.js/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const bcrypt = require('bcrypt-then');
 | 
				
			||||||
 | 
					const db     = require('../db');
 | 
				
			||||||
 | 
					const Model  = require('objection').Model;
 | 
				
			||||||
 | 
					const User   = require('./user');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Model.knex(db);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function encryptPassword () {
 | 
				
			||||||
 | 
					    /* jshint -W040 */
 | 
				
			||||||
 | 
					    let _this = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (_this.type === 'password' && _this.secret) {
 | 
				
			||||||
 | 
					        return bcrypt.hash(_this.secret, 13)
 | 
				
			||||||
 | 
					            .then(function (hash) {
 | 
				
			||||||
 | 
					                _this.secret = hash;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Auth extends Model {
 | 
				
			||||||
 | 
					    $beforeInsert (queryContext) {
 | 
				
			||||||
 | 
					        this.created_on  = Model.raw('NOW()');
 | 
				
			||||||
 | 
					        this.modified_on = Model.raw('NOW()');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return encryptPassword.apply(this, queryContext);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $beforeUpdate (queryContext) {
 | 
				
			||||||
 | 
					        this.modified_on = Model.raw('NOW()');
 | 
				
			||||||
 | 
					        return encryptPassword.apply(this, queryContext);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Verify a plain password against the encrypted password
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {String} password
 | 
				
			||||||
 | 
					     * @returns {Promise}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    verifyPassword (password) {
 | 
				
			||||||
 | 
					        return bcrypt.compare(password, this.secret);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static get name () {
 | 
				
			||||||
 | 
					        return 'Auth';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static get tableName () {
 | 
				
			||||||
 | 
					        return 'auth';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static get jsonAttributes () {
 | 
				
			||||||
 | 
					        return ['meta'];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static get relationMappings () {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            user: {
 | 
				
			||||||
 | 
					                relation:   Model.HasOneRelation,
 | 
				
			||||||
 | 
					                modelClass: User,
 | 
				
			||||||
 | 
					                join:       {
 | 
				
			||||||
 | 
					                    from: 'auth.user_id',
 | 
				
			||||||
 | 
					                    to:   'user.id'
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                filter:     {
 | 
				
			||||||
 | 
					                    is_deleted: 0
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                modify:     function (qb) {
 | 
				
			||||||
 | 
					                    qb.omit(['is_deleted']);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = Auth;
 | 
				
			||||||
							
								
								
									
										133
									
								
								src/backend/models/token.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/backend/models/token.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 NOTE: This is not a database table, this is a model of a Token object that can be created/loaded
 | 
				
			||||||
 | 
					 and then has abilities after that.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const _      = require('lodash');
 | 
				
			||||||
 | 
					const config = require('config');
 | 
				
			||||||
 | 
					const jwt    = require('jsonwebtoken');
 | 
				
			||||||
 | 
					const crypto = require('crypto');
 | 
				
			||||||
 | 
					const error  = require('../lib/error');
 | 
				
			||||||
 | 
					const ALGO   = 'RS256';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = function () {
 | 
				
			||||||
 | 
					    const public_key  = config.get('jwt.pub');
 | 
				
			||||||
 | 
					    const private_key = config.get('jwt.key');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let token_data = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * @param {Object}  payload
 | 
				
			||||||
 | 
					         * @param {Object}  [user_options]
 | 
				
			||||||
 | 
					         * @param {Integer} [user_options.expires]
 | 
				
			||||||
 | 
					         * @returns {Promise}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        create: (payload, user_options) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            user_options = user_options || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // sign with RSA SHA256
 | 
				
			||||||
 | 
					            let options = {
 | 
				
			||||||
 | 
					                algorithm: ALGO
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (typeof user_options.expires !== 'undefined' && user_options.expires) {
 | 
				
			||||||
 | 
					                options.expiresIn = user_options.expires;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            payload.jti = crypto.randomBytes(12)
 | 
				
			||||||
 | 
					                .toString('base64')
 | 
				
			||||||
 | 
					                .substr(-8);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					                jwt.sign(payload, private_key, options, (err, token) => {
 | 
				
			||||||
 | 
					                    if (err) {
 | 
				
			||||||
 | 
					                        reject(err);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        token_data = payload;
 | 
				
			||||||
 | 
					                        resolve({
 | 
				
			||||||
 | 
					                            token:   token,
 | 
				
			||||||
 | 
					                            payload: payload
 | 
				
			||||||
 | 
					                        });
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * @param {String} token
 | 
				
			||||||
 | 
					         * @returns {Promise}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        load: function (token) {
 | 
				
			||||||
 | 
					            return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    if (!token || token === null || token === 'null') {
 | 
				
			||||||
 | 
					                        reject('Empty token');
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        jwt.verify(token, public_key, {ignoreExpiration: false, algorithms: [ALGO]}, (err, result) => {
 | 
				
			||||||
 | 
					                            if (err) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                if (err.name === 'TokenExpiredError') {
 | 
				
			||||||
 | 
					                                    reject(new error.AuthError('Token has expired', err));
 | 
				
			||||||
 | 
					                                } else {
 | 
				
			||||||
 | 
					                                    reject(err);
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                token_data = result;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                // Hack: some tokens out in the wild have a scope of 'all' instead of 'user'.
 | 
				
			||||||
 | 
					                                // For 30 days at least, we need to replace 'all' with user.
 | 
				
			||||||
 | 
					                                if ((typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'all') !== -1)) {
 | 
				
			||||||
 | 
					                                    //console.log('Warning! Replacing "all" scope with "user"');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                    token_data.scope = ['user'];
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                resolve(token_data);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        });
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } catch (err) {
 | 
				
			||||||
 | 
					                    reject(err);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * Does the token have the specified scope?
 | 
				
			||||||
 | 
					         *
 | 
				
			||||||
 | 
					         * @param   {String}  scope
 | 
				
			||||||
 | 
					         * @returns {Boolean}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        hasScope: function (scope) {
 | 
				
			||||||
 | 
					            return typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, scope) !== -1;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * @param  {String}  key
 | 
				
			||||||
 | 
					         * @return {*}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        get: function (key) {
 | 
				
			||||||
 | 
					            if (typeof token_data[key] !== 'undefined') {
 | 
				
			||||||
 | 
					                return token_data[key];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * @param  {String}  key
 | 
				
			||||||
 | 
					         * @param  {*}       value
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        set: function (key, value) {
 | 
				
			||||||
 | 
					            token_data[key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										35
									
								
								src/backend/models/user.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/backend/models/user.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					// Objection Docs:
 | 
				
			||||||
 | 
					// http://vincit.github.io/objection.js/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const db    = require('../db');
 | 
				
			||||||
 | 
					const Model = require('objection').Model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Model.knex(db);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class User extends Model {
 | 
				
			||||||
 | 
					    $beforeInsert () {
 | 
				
			||||||
 | 
					        this.created_on  = Model.raw('NOW()');
 | 
				
			||||||
 | 
					        this.modified_on = Model.raw('NOW()');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $beforeUpdate () {
 | 
				
			||||||
 | 
					        this.modified_on = Model.raw('NOW()');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static get name () {
 | 
				
			||||||
 | 
					        return 'User';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static get tableName () {
 | 
				
			||||||
 | 
					        return 'user';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static get jsonAttributes () {
 | 
				
			||||||
 | 
					        return ['roles'];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = User;
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/backend/routes/api/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/backend/routes/api/main.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const express = require('express');
 | 
				
			||||||
 | 
					const pjson   = require('../../../../package.json');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let router = express.Router({
 | 
				
			||||||
 | 
					    caseSensitive: true,
 | 
				
			||||||
 | 
					    strict:        true,
 | 
				
			||||||
 | 
					    mergeParams:   true
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Health Check
 | 
				
			||||||
 | 
					 * GET /api
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					router.get('/', (req, res/*, next*/) => {
 | 
				
			||||||
 | 
					    let version = pjson.version.split('-').shift().split('.');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.status(200).send({
 | 
				
			||||||
 | 
					        status:  'OK',
 | 
				
			||||||
 | 
					        version: {
 | 
				
			||||||
 | 
					            major:    parseInt(version.shift(), 10),
 | 
				
			||||||
 | 
					            minor:    parseInt(version.shift(), 10),
 | 
				
			||||||
 | 
					            revision: parseInt(version.shift(), 10)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					router.use('/tokens', require('./tokens'));
 | 
				
			||||||
 | 
					router.use('/users', require('./users'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = router;
 | 
				
			||||||
							
								
								
									
										56
									
								
								src/backend/routes/api/tokens.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/backend/routes/api/tokens.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const express       = require('express');
 | 
				
			||||||
 | 
					const jwtdecode     = require('../../lib/express/jwt-decode');
 | 
				
			||||||
 | 
					const internalToken = require('../../internal/token');
 | 
				
			||||||
 | 
					const apiValidator  = require('../../lib/validator/api');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let router = express.Router({
 | 
				
			||||||
 | 
					    caseSensitive: true,
 | 
				
			||||||
 | 
					    strict:        true,
 | 
				
			||||||
 | 
					    mergeParams:   true
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					router
 | 
				
			||||||
 | 
					    .route('/')
 | 
				
			||||||
 | 
					    .options((req, res) => {
 | 
				
			||||||
 | 
					        res.sendStatus(204);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * GET /tokens
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * Get a new Token, given they already have a token they want to refresh
 | 
				
			||||||
 | 
					     * We also piggy back on to this method, allowing admins to get tokens
 | 
				
			||||||
 | 
					     * for services like Job board and Worker.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    .get(jwtdecode(), (req, res, next) => {
 | 
				
			||||||
 | 
					        internalToken.getFreshToken(res.locals.access, {
 | 
				
			||||||
 | 
					            expiry: (typeof req.query.expiry !== 'undefined' ? req.query.expiry : null),
 | 
				
			||||||
 | 
					            scope:  (typeof req.query.scope !== 'undefined' ? req.query.scope : null)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					            .then(data => {
 | 
				
			||||||
 | 
					                res.status(200)
 | 
				
			||||||
 | 
					                    .send(data);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(next);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * POST /tokens
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * Create a new Token
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    .post((req, res, next) => {
 | 
				
			||||||
 | 
					        apiValidator({$ref: 'endpoints/tokens#/links/0/schema'}, req.body)
 | 
				
			||||||
 | 
					            .then(payload => {
 | 
				
			||||||
 | 
					                return internalToken.getTokenFromEmail(payload);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then(data => {
 | 
				
			||||||
 | 
					                res.status(200)
 | 
				
			||||||
 | 
					                    .send(data);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(next);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = router;
 | 
				
			||||||
							
								
								
									
										256
									
								
								src/backend/routes/api/users.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								src/backend/routes/api/users.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,256 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const express      = require('express');
 | 
				
			||||||
 | 
					const validator    = require('../../lib/validator');
 | 
				
			||||||
 | 
					const jwtdecode    = require('../../lib/express/jwt-decode');
 | 
				
			||||||
 | 
					const pagination   = require('../../lib/express/pagination');
 | 
				
			||||||
 | 
					const userIdFromMe = require('../../lib/express/user-id-from-me');
 | 
				
			||||||
 | 
					const internalUser = require('../../internal/user');
 | 
				
			||||||
 | 
					const apiValidator = require('../../lib/validator/api');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let router = express.Router({
 | 
				
			||||||
 | 
					    caseSensitive: true,
 | 
				
			||||||
 | 
					    strict:        true,
 | 
				
			||||||
 | 
					    mergeParams:   true
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * /api/users
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					router
 | 
				
			||||||
 | 
					    .route('/')
 | 
				
			||||||
 | 
					    .options((req, res) => {
 | 
				
			||||||
 | 
					        res.sendStatus(204);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * GET /api/users
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * Retrieve all users
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    .get(pagination('name', 0, 50, 300), (req, res, next) => {
 | 
				
			||||||
 | 
					        validator({
 | 
				
			||||||
 | 
					            additionalProperties: false,
 | 
				
			||||||
 | 
					            required:             ['sort'],
 | 
				
			||||||
 | 
					            properties:           {
 | 
				
			||||||
 | 
					                sort:   {
 | 
				
			||||||
 | 
					                    $ref: 'definitions#/definitions/sort'
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                expand: {
 | 
				
			||||||
 | 
					                    $ref: 'definitions#/definitions/expand'
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                query:  {
 | 
				
			||||||
 | 
					                    $ref: 'definitions#/definitions/query'
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }, {
 | 
				
			||||||
 | 
					            sort:   req.query.sort,
 | 
				
			||||||
 | 
					            expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
 | 
				
			||||||
 | 
					            query:  (typeof req.query.query === 'string' ? req.query.query : null)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					            .then((data) => {
 | 
				
			||||||
 | 
					                return Promise.all([
 | 
				
			||||||
 | 
					                    internalUser.getCount(res.locals.access, data.query),
 | 
				
			||||||
 | 
					                    internalUser.getAll(res.locals.access, req.query.offset, req.query.limit, data.sort, data.expand, data.query)
 | 
				
			||||||
 | 
					                ]);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then((data) => {
 | 
				
			||||||
 | 
					                res.setHeader('X-Dataset-Total', data.shift());
 | 
				
			||||||
 | 
					                res.setHeader('X-Dataset-Offset', req.query.offset);
 | 
				
			||||||
 | 
					                res.setHeader('X-Dataset-Limit', req.query.limit);
 | 
				
			||||||
 | 
					                return data.shift();
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then((users) => {
 | 
				
			||||||
 | 
					                res.status(200)
 | 
				
			||||||
 | 
					                    .send(users);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(next);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * POST /api/users
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * Create a new User
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    .post((req, res, next) => {
 | 
				
			||||||
 | 
					        apiValidator({$ref: 'endpoints/users#/links/1/schema'}, req.body)
 | 
				
			||||||
 | 
					            .then((payload) => {
 | 
				
			||||||
 | 
					                return internalUser.create(res.locals.access, payload);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then((result) => {
 | 
				
			||||||
 | 
					                res.status(201)
 | 
				
			||||||
 | 
					                    .send(result);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(next);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Specific user
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * /api/users/123
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					router
 | 
				
			||||||
 | 
					    .route('/:user_id')
 | 
				
			||||||
 | 
					    .options((req, res) => {
 | 
				
			||||||
 | 
					        res.sendStatus(204);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
 | 
				
			||||||
 | 
					    .all(userIdFromMe)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * GET /users/123 or /users/me
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * Retrieve a specific user
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    .get((req, res, next) => {
 | 
				
			||||||
 | 
					        validator({
 | 
				
			||||||
 | 
					            required:             ['user_id'],
 | 
				
			||||||
 | 
					            additionalProperties: false,
 | 
				
			||||||
 | 
					            properties:           {
 | 
				
			||||||
 | 
					                user_id: {
 | 
				
			||||||
 | 
					                    $ref: 'definitions#/definitions/id'
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                expand:  {
 | 
				
			||||||
 | 
					                    $ref: 'definitions#/definitions/expand'
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }, {
 | 
				
			||||||
 | 
					            user_id: req.params.user_id,
 | 
				
			||||||
 | 
					            expand:  (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					            .then((data) => {
 | 
				
			||||||
 | 
					                return internalUser.get(res.locals.access, {
 | 
				
			||||||
 | 
					                    id:     data.user_id,
 | 
				
			||||||
 | 
					                    expand: data.expand,
 | 
				
			||||||
 | 
					                    omit:   internalUser.getUserOmisionsByAccess(res.locals.access, data.user_id)
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then((user) => {
 | 
				
			||||||
 | 
					                res.status(200)
 | 
				
			||||||
 | 
					                    .send(user);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(next);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * PUT /api/users/123
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * Update and existing user
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    .put((req, res, next) => {
 | 
				
			||||||
 | 
					        apiValidator({$ref: 'endpoints/users#/links/2/schema'}, req.body)
 | 
				
			||||||
 | 
					            .then((payload) => {
 | 
				
			||||||
 | 
					                payload.id = req.params.user_id;
 | 
				
			||||||
 | 
					                return internalUser.update(res.locals.access, payload);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then((result) => {
 | 
				
			||||||
 | 
					                res.status(200)
 | 
				
			||||||
 | 
					                    .send(result);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(next);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * DELETE /api/users/123
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * Update and existing user
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    .delete((req, res, next) => {
 | 
				
			||||||
 | 
					        internalUser.delete(res.locals.access, {id: req.params.user_id})
 | 
				
			||||||
 | 
					            .then((result) => {
 | 
				
			||||||
 | 
					                res.status(200)
 | 
				
			||||||
 | 
					                    .send(result);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(next);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Specific user auth
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * /api/users/123/auth
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					router
 | 
				
			||||||
 | 
					    .route('/:user_id/auth')
 | 
				
			||||||
 | 
					    .options((req, res) => {
 | 
				
			||||||
 | 
					        res.sendStatus(204);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
 | 
				
			||||||
 | 
					    .all(userIdFromMe)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * PUT /api/users/123/auth
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * Update password for a user
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    .put((req, res, next) => {
 | 
				
			||||||
 | 
					        apiValidator({$ref: 'endpoints/users#/links/4/schema'}, req.body)
 | 
				
			||||||
 | 
					            .then(payload => {
 | 
				
			||||||
 | 
					                payload.id = req.params.user_id;
 | 
				
			||||||
 | 
					                return internalUser.setPassword(res.locals.access, payload);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then(result => {
 | 
				
			||||||
 | 
					                res.status(201)
 | 
				
			||||||
 | 
					                    .send(result);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(next);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Specific user service settings
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * /api/users/123/services
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					router
 | 
				
			||||||
 | 
					    .route('/:user_id/services')
 | 
				
			||||||
 | 
					    .options((req, res) => {
 | 
				
			||||||
 | 
					        res.sendStatus(204);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
 | 
				
			||||||
 | 
					    .all(userIdFromMe)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * POST /api/users/123/services
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * Sets Service Settings for a user
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    .post((req, res, next) => {
 | 
				
			||||||
 | 
					        apiValidator({$ref: 'endpoints/users#/links/5/schema'}, req.body)
 | 
				
			||||||
 | 
					            .then((payload) => {
 | 
				
			||||||
 | 
					                payload.id = req.params.user_id;
 | 
				
			||||||
 | 
					                return internalUser.setServiceSettings(res.locals.access, payload);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then((result) => {
 | 
				
			||||||
 | 
					                res.status(200)
 | 
				
			||||||
 | 
					                    .send(result);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(next);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Specific user login as
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * /api/users/123/login
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					router
 | 
				
			||||||
 | 
					    .route('/:user_id/login')
 | 
				
			||||||
 | 
					    .options((req, res) => {
 | 
				
			||||||
 | 
					        res.sendStatus(204);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * POST /api/users/123/login
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * Log in as a user
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    .post((req, res, next) => {
 | 
				
			||||||
 | 
					        internalUser.loginAs(res.locals.access, {id: parseInt(req.params.user_id, 10)})
 | 
				
			||||||
 | 
					            .then(result => {
 | 
				
			||||||
 | 
					                res.status(201)
 | 
				
			||||||
 | 
					                    .send(result);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(next);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = router;
 | 
				
			||||||
							
								
								
									
										44
									
								
								src/backend/routes/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/backend/routes/main.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const express = require('express');
 | 
				
			||||||
 | 
					const fs      = require('fs');
 | 
				
			||||||
 | 
					const PACKAGE = require('../../../package.json');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const router = express.Router({
 | 
				
			||||||
 | 
					    caseSensitive: true,
 | 
				
			||||||
 | 
					    strict:        true,
 | 
				
			||||||
 | 
					    mergeParams:   true
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * GET /login
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					router.get('/login', function (req, res, next) {
 | 
				
			||||||
 | 
					    res.render('login', {
 | 
				
			||||||
 | 
					        version: PACKAGE.version
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * GET .*
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					router.get(/(.*)/, function (req, res, next) {
 | 
				
			||||||
 | 
					    req.params.page = req.params['0'];
 | 
				
			||||||
 | 
					    if (req.params.page === '/') {
 | 
				
			||||||
 | 
					        res.render('index', {
 | 
				
			||||||
 | 
					            version: PACKAGE.version
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        fs.readFile('dist' + req.params.page, 'utf8', function (err, data) {
 | 
				
			||||||
 | 
					            if (err) {
 | 
				
			||||||
 | 
					                res.render('index', {
 | 
				
			||||||
 | 
					                    version: PACKAGE.version
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                res.contentType('text/html').end(data);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = router;
 | 
				
			||||||
							
								
								
									
										139
									
								
								src/backend/schema/definitions.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								src/backend/schema/definitions.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "$schema": "http://json-schema.org/draft-07/schema#",
 | 
				
			||||||
 | 
					  "$id": "definitions",
 | 
				
			||||||
 | 
					  "definitions": {
 | 
				
			||||||
 | 
					    "id": {
 | 
				
			||||||
 | 
					      "description": "Unique identifier",
 | 
				
			||||||
 | 
					      "example": 123456,
 | 
				
			||||||
 | 
					      "readOnly": true,
 | 
				
			||||||
 | 
					      "type": "integer",
 | 
				
			||||||
 | 
					      "minimum": 1
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "token": {
 | 
				
			||||||
 | 
					      "type": "string",
 | 
				
			||||||
 | 
					      "minLength": 10
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "expand": {
 | 
				
			||||||
 | 
					      "anyOf": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "null"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "array",
 | 
				
			||||||
 | 
					          "minItems": 1,
 | 
				
			||||||
 | 
					          "items": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "sort": {
 | 
				
			||||||
 | 
					      "type": "array",
 | 
				
			||||||
 | 
					      "minItems": 1,
 | 
				
			||||||
 | 
					      "items": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "field",
 | 
				
			||||||
 | 
					          "dir"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "additionalProperties": false,
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "field": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "dir": {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "pattern": "^(asc|desc)$"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "query": {
 | 
				
			||||||
 | 
					      "anyOf": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "null"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "string",
 | 
				
			||||||
 | 
					          "minLength": 1,
 | 
				
			||||||
 | 
					          "maxLength": 255
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "criteria": {
 | 
				
			||||||
 | 
					      "anyOf": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "null"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "object"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "fields": {
 | 
				
			||||||
 | 
					      "anyOf": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "null"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "array",
 | 
				
			||||||
 | 
					          "minItems": 1,
 | 
				
			||||||
 | 
					          "items": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "omit": {
 | 
				
			||||||
 | 
					      "anyOf": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "null"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "array",
 | 
				
			||||||
 | 
					          "minItems": 1,
 | 
				
			||||||
 | 
					          "items": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "created_on": {
 | 
				
			||||||
 | 
					      "description": "Date and time of creation",
 | 
				
			||||||
 | 
					      "format": "date-time",
 | 
				
			||||||
 | 
					      "readOnly": true,
 | 
				
			||||||
 | 
					      "type": "string"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "modified_on": {
 | 
				
			||||||
 | 
					      "description": "Date and time of last update",
 | 
				
			||||||
 | 
					      "format": "date-time",
 | 
				
			||||||
 | 
					      "readOnly": true,
 | 
				
			||||||
 | 
					      "type": "string"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "user_id": {
 | 
				
			||||||
 | 
					      "description": "User ID",
 | 
				
			||||||
 | 
					      "example": 1234,
 | 
				
			||||||
 | 
					      "type": "integer",
 | 
				
			||||||
 | 
					      "minimum": 1
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "name": {
 | 
				
			||||||
 | 
					      "type": "string",
 | 
				
			||||||
 | 
					      "minLength": 1,
 | 
				
			||||||
 | 
					      "maxLength": 255
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "email": {
 | 
				
			||||||
 | 
					      "description": "Email Address",
 | 
				
			||||||
 | 
					      "example": "john@example.com",
 | 
				
			||||||
 | 
					      "format": "email",
 | 
				
			||||||
 | 
					      "type": "string",
 | 
				
			||||||
 | 
					      "minLength": 8,
 | 
				
			||||||
 | 
					      "maxLength": 100
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "password": {
 | 
				
			||||||
 | 
					      "description": "Password",
 | 
				
			||||||
 | 
					      "type": "string",
 | 
				
			||||||
 | 
					      "minLength": 8,
 | 
				
			||||||
 | 
					      "maxLength": 255
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										100
									
								
								src/backend/schema/endpoints/tokens.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/backend/schema/endpoints/tokens.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "$schema": "http://json-schema.org/draft-07/schema#",
 | 
				
			||||||
 | 
					  "$id": "endpoints/tokens",
 | 
				
			||||||
 | 
					  "title": "Token",
 | 
				
			||||||
 | 
					  "description": "Tokens are required to authenticate against the API",
 | 
				
			||||||
 | 
					  "stability": "stable",
 | 
				
			||||||
 | 
					  "type": "object",
 | 
				
			||||||
 | 
					  "definitions": {
 | 
				
			||||||
 | 
					    "identity": {
 | 
				
			||||||
 | 
					      "description": "Email Address or other 3rd party providers identifier",
 | 
				
			||||||
 | 
					      "example": "john@example.com",
 | 
				
			||||||
 | 
					      "type": "string"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "secret": {
 | 
				
			||||||
 | 
					      "description": "A password or key",
 | 
				
			||||||
 | 
					      "example": "correct horse battery staple",
 | 
				
			||||||
 | 
					      "type": "string"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "token": {
 | 
				
			||||||
 | 
					      "description": "JWT",
 | 
				
			||||||
 | 
					      "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk",
 | 
				
			||||||
 | 
					      "type": "string"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "expires": {
 | 
				
			||||||
 | 
					      "description": "Token expiry time",
 | 
				
			||||||
 | 
					      "format": "date-time",
 | 
				
			||||||
 | 
					      "type": "string"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "scope": {
 | 
				
			||||||
 | 
					      "description": "Scope of the Token, defaults to 'user'",
 | 
				
			||||||
 | 
					      "example": "user",
 | 
				
			||||||
 | 
					      "type": "string"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "links": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "title": "Create",
 | 
				
			||||||
 | 
					      "description": "Creates a new token.",
 | 
				
			||||||
 | 
					      "href": "/tokens",
 | 
				
			||||||
 | 
					      "access": "public",
 | 
				
			||||||
 | 
					      "method": "POST",
 | 
				
			||||||
 | 
					      "rel": "create",
 | 
				
			||||||
 | 
					      "schema": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "identity",
 | 
				
			||||||
 | 
					          "secret"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "identity": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/identity"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "secret": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/secret"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "scope": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/scope"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "targetSchema": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "token": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/token"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "expires": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/expires"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "title": "Refresh",
 | 
				
			||||||
 | 
					      "description": "Returns a new token.",
 | 
				
			||||||
 | 
					      "href": "/tokens",
 | 
				
			||||||
 | 
					      "access": "private",
 | 
				
			||||||
 | 
					      "method": "GET",
 | 
				
			||||||
 | 
					      "rel": "self",
 | 
				
			||||||
 | 
					      "http_header": {
 | 
				
			||||||
 | 
					        "$ref": "../examples.json#/definitions/auth_header"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "schema": {},
 | 
				
			||||||
 | 
					      "targetSchema": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "token": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/token"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "expires": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/expires"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "scope": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/scope"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										240
									
								
								src/backend/schema/endpoints/users.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								src/backend/schema/endpoints/users.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,240 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "$schema": "http://json-schema.org/draft-07/schema#",
 | 
				
			||||||
 | 
					  "$id": "endpoints/users",
 | 
				
			||||||
 | 
					  "title": "Users",
 | 
				
			||||||
 | 
					  "description": "Endpoints relating to Users",
 | 
				
			||||||
 | 
					  "stability": "stable",
 | 
				
			||||||
 | 
					  "type": "object",
 | 
				
			||||||
 | 
					  "definitions": {
 | 
				
			||||||
 | 
					    "id": {
 | 
				
			||||||
 | 
					      "$ref": "../definitions.json#/definitions/id"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "created_on": {
 | 
				
			||||||
 | 
					      "$ref": "../definitions.json#/definitions/created_on"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "modified_on": {
 | 
				
			||||||
 | 
					      "$ref": "../definitions.json#/definitions/modified_on"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "name": {
 | 
				
			||||||
 | 
					      "description": "Name",
 | 
				
			||||||
 | 
					      "example": "Jamie Curnow",
 | 
				
			||||||
 | 
					      "type": "string",
 | 
				
			||||||
 | 
					      "minLength": 2,
 | 
				
			||||||
 | 
					      "maxLength": 100
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "nickname": {
 | 
				
			||||||
 | 
					      "description": "Nickname",
 | 
				
			||||||
 | 
					      "example": "Jamie",
 | 
				
			||||||
 | 
					      "type": "string",
 | 
				
			||||||
 | 
					      "minLength": 2,
 | 
				
			||||||
 | 
					      "maxLength": 50
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "email": {
 | 
				
			||||||
 | 
					      "$ref": "../definitions.json#/definitions/email"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "avatar": {
 | 
				
			||||||
 | 
					      "description": "Avatar",
 | 
				
			||||||
 | 
					      "example": "http://somewhere.jpg",
 | 
				
			||||||
 | 
					      "type": "string",
 | 
				
			||||||
 | 
					      "minLength": 2,
 | 
				
			||||||
 | 
					      "maxLength": 150,
 | 
				
			||||||
 | 
					      "readOnly": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "roles": {
 | 
				
			||||||
 | 
					      "description": "Roles",
 | 
				
			||||||
 | 
					      "example": [
 | 
				
			||||||
 | 
					        "admin"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "type": "array"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "is_disabled": {
 | 
				
			||||||
 | 
					      "description": "Is Disabled",
 | 
				
			||||||
 | 
					      "example": false,
 | 
				
			||||||
 | 
					      "type": "boolean"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "links": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "title": "List",
 | 
				
			||||||
 | 
					      "description": "Returns a list of Users",
 | 
				
			||||||
 | 
					      "href": "/users",
 | 
				
			||||||
 | 
					      "access": "private",
 | 
				
			||||||
 | 
					      "method": "GET",
 | 
				
			||||||
 | 
					      "rel": "self",
 | 
				
			||||||
 | 
					      "http_header": {
 | 
				
			||||||
 | 
					        "$ref": "../examples.json#/definitions/auth_header"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "targetSchema": {
 | 
				
			||||||
 | 
					        "type": "array",
 | 
				
			||||||
 | 
					        "items": {
 | 
				
			||||||
 | 
					          "$ref": "#/properties"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "title": "Create",
 | 
				
			||||||
 | 
					      "description": "Creates a new User",
 | 
				
			||||||
 | 
					      "href": "/users",
 | 
				
			||||||
 | 
					      "access": "private",
 | 
				
			||||||
 | 
					      "method": "POST",
 | 
				
			||||||
 | 
					      "rel": "create",
 | 
				
			||||||
 | 
					      "http_header": {
 | 
				
			||||||
 | 
					        "$ref": "../examples.json#/definitions/auth_header"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "schema": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "name",
 | 
				
			||||||
 | 
					          "nickname",
 | 
				
			||||||
 | 
					          "email"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "name": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/name"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "nickname": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/nickname"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "email": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/email"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "roles": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/roles"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "is_disabled": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/is_disabled"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "auth": {
 | 
				
			||||||
 | 
					            "type": "object",
 | 
				
			||||||
 | 
					            "description": "Auth Credentials",
 | 
				
			||||||
 | 
					            "example": {
 | 
				
			||||||
 | 
					              "type": "password",
 | 
				
			||||||
 | 
					              "secret": "bigredhorsebanana"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "targetSchema": {
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "$ref": "#/properties"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "title": "Update",
 | 
				
			||||||
 | 
					      "description": "Updates a existing User",
 | 
				
			||||||
 | 
					      "href": "/users/{definitions.identity.example}",
 | 
				
			||||||
 | 
					      "access": "private",
 | 
				
			||||||
 | 
					      "method": "PUT",
 | 
				
			||||||
 | 
					      "rel": "update",
 | 
				
			||||||
 | 
					      "http_header": {
 | 
				
			||||||
 | 
					        "$ref": "../examples.json#/definitions/auth_header"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "schema": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "name": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/name"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "nickname": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/nickname"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "email": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/email"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "roles": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/roles"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "is_disabled": {
 | 
				
			||||||
 | 
					            "$ref": "#/definitions/is_disabled"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "targetSchema": {
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "$ref": "#/properties"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "title": "Delete",
 | 
				
			||||||
 | 
					      "description": "Deletes a existing User",
 | 
				
			||||||
 | 
					      "href": "/users/{definitions.identity.example}",
 | 
				
			||||||
 | 
					      "access": "private",
 | 
				
			||||||
 | 
					      "method": "DELETE",
 | 
				
			||||||
 | 
					      "rel": "delete",
 | 
				
			||||||
 | 
					      "http_header": {
 | 
				
			||||||
 | 
					        "$ref": "../examples.json#/definitions/auth_header"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "targetSchema": {
 | 
				
			||||||
 | 
					        "type": "boolean"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "title": "Set Password",
 | 
				
			||||||
 | 
					      "description": "Sets a password for an existing User",
 | 
				
			||||||
 | 
					      "href": "/users/{definitions.identity.example}/auth",
 | 
				
			||||||
 | 
					      "access": "private",
 | 
				
			||||||
 | 
					      "method": "PUT",
 | 
				
			||||||
 | 
					      "rel": "update",
 | 
				
			||||||
 | 
					      "http_header": {
 | 
				
			||||||
 | 
					        "$ref": "../examples.json#/definitions/auth_header"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "schema": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "type",
 | 
				
			||||||
 | 
					          "secret"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "type": {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "pattern": "^password$"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "current": {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "minLength": 1,
 | 
				
			||||||
 | 
					            "maxLength": 64
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "secret": {
 | 
				
			||||||
 | 
					            "type": "string",
 | 
				
			||||||
 | 
					            "minLength": 8,
 | 
				
			||||||
 | 
					            "maxLength": 64
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "targetSchema": {
 | 
				
			||||||
 | 
					        "type": "boolean"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "properties": {
 | 
				
			||||||
 | 
					    "id": {
 | 
				
			||||||
 | 
					      "$ref": "#/definitions/id"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "created_on": {
 | 
				
			||||||
 | 
					      "$ref": "#/definitions/created_on"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "modified_on": {
 | 
				
			||||||
 | 
					      "$ref": "#/definitions/modified_on"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "name": {
 | 
				
			||||||
 | 
					      "$ref": "#/definitions/name"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "nickname": {
 | 
				
			||||||
 | 
					      "$ref": "#/definitions/nickname"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "email": {
 | 
				
			||||||
 | 
					      "$ref": "#/definitions/email"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "avatar": {
 | 
				
			||||||
 | 
					      "$ref": "#/definitions/avatar"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "roles": {
 | 
				
			||||||
 | 
					      "$ref": "#/definitions/roles"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "is_disabled": {
 | 
				
			||||||
 | 
					      "$ref": "#/definitions/is_disabled"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/backend/schema/examples.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/backend/schema/examples.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "$schema": "http://json-schema.org/draft-07/schema#",
 | 
				
			||||||
 | 
					  "$id": "examples",
 | 
				
			||||||
 | 
					  "type": "object",
 | 
				
			||||||
 | 
					  "definitions": {
 | 
				
			||||||
 | 
					    "name": {
 | 
				
			||||||
 | 
					      "description": "Name",
 | 
				
			||||||
 | 
					      "example": "John Smith",
 | 
				
			||||||
 | 
					      "type": "string",
 | 
				
			||||||
 | 
					      "minLength": 1,
 | 
				
			||||||
 | 
					      "maxLength": 255
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "auth_header": {
 | 
				
			||||||
 | 
					      "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk",
 | 
				
			||||||
 | 
					      "X-API-Version": "next"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "token": {
 | 
				
			||||||
 | 
					      "type": "string",
 | 
				
			||||||
 | 
					      "description": "JWT",
 | 
				
			||||||
 | 
					      "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.O_frfYM8RzmRsUNigHtu0_jZ_utSejyr1axMGa8rlsk"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								src/backend/schema/index.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/backend/schema/index.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "$schema": "http://json-schema.org/draft-07/schema#",
 | 
				
			||||||
 | 
					  "$id": "root",
 | 
				
			||||||
 | 
					  "title": "Nginx Proxy Manager REST API",
 | 
				
			||||||
 | 
					  "description": "This is the Nginx Proxy Manager REST API",
 | 
				
			||||||
 | 
					  "version": "2.0.0",
 | 
				
			||||||
 | 
					  "links": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "href": "http://npm.example.com/api",
 | 
				
			||||||
 | 
					      "rel": "self"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "properties": {
 | 
				
			||||||
 | 
					    "tokens": {
 | 
				
			||||||
 | 
					      "$ref": "endpoints/tokens.json"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "users": {
 | 
				
			||||||
 | 
					      "$ref": "endpoints/users.json"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										87
									
								
								src/backend/setup.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/backend/setup.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const fs        = require('fs');
 | 
				
			||||||
 | 
					const NodeRSA   = require('node-rsa');
 | 
				
			||||||
 | 
					const config    = require('config');
 | 
				
			||||||
 | 
					const logger    = require('./logger').global;
 | 
				
			||||||
 | 
					const userModel = require('./models/user');
 | 
				
			||||||
 | 
					const authModel = require('./models/auth');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = function () {
 | 
				
			||||||
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					        // Now go and check if the jwt gpg keys have been created and if not, create them
 | 
				
			||||||
 | 
					        if (!config.has('jwt') || !config.has('jwt.key') || !config.has('jwt.pub')) {
 | 
				
			||||||
 | 
					            logger.info('Creating a new JWT key pair...');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // jwt keys are not configured properly
 | 
				
			||||||
 | 
					            const filename  = config.util.getEnv('NODE_CONFIG_DIR') + '/' + (config.util.getEnv('NODE_ENV') || 'default') + '.json';
 | 
				
			||||||
 | 
					            let config_data = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                config_data = require(filename);
 | 
				
			||||||
 | 
					            } catch (err) {
 | 
				
			||||||
 | 
					                // do nothing
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Now create the keys and save them in the config.
 | 
				
			||||||
 | 
					            let key = new NodeRSA({b: 2048});
 | 
				
			||||||
 | 
					            key.generateKeyPair();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            config_data.jwt = {
 | 
				
			||||||
 | 
					                key: key.exportKey('private').toString(),
 | 
				
			||||||
 | 
					                pub: key.exportKey('public').toString()
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Write config
 | 
				
			||||||
 | 
					            fs.writeFile(filename, JSON.stringify(config_data, null, 2), (err) => {
 | 
				
			||||||
 | 
					                if (err) {
 | 
				
			||||||
 | 
					                    logger.error('Could not write JWT key pair to config file: ' + filename);
 | 
				
			||||||
 | 
					                    reject(err);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    logger.info('Wrote JWT key pair to config file: ' + filename);
 | 
				
			||||||
 | 
					                    config.util.loadFileConfigs();
 | 
				
			||||||
 | 
					                    resolve();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // JWT key pair exists
 | 
				
			||||||
 | 
					            resolve();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					            return userModel
 | 
				
			||||||
 | 
					                .query()
 | 
				
			||||||
 | 
					                .select(userModel.raw('COUNT(`id`) as `count`'))
 | 
				
			||||||
 | 
					                .where('is_deleted', 0)
 | 
				
			||||||
 | 
					                .first('count')
 | 
				
			||||||
 | 
					                .then((row) => {
 | 
				
			||||||
 | 
					                    if (!row.count) {
 | 
				
			||||||
 | 
					                        // Create a new user and set password
 | 
				
			||||||
 | 
					                        logger.info('Creating a new user: admin@example.com with password: changeme');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        let data = {
 | 
				
			||||||
 | 
					                            is_deleted: 0,
 | 
				
			||||||
 | 
					                            email:      'admin@example.com',
 | 
				
			||||||
 | 
					                            name:       'Administrator',
 | 
				
			||||||
 | 
					                            nickname:   'Admin',
 | 
				
			||||||
 | 
					                            avatar:     '',
 | 
				
			||||||
 | 
					                            roles:      ['admin']
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        return userModel
 | 
				
			||||||
 | 
					                            .query()
 | 
				
			||||||
 | 
					                            .insertAndFetch(data)
 | 
				
			||||||
 | 
					                            .then(user => {
 | 
				
			||||||
 | 
					                                return authModel
 | 
				
			||||||
 | 
					                                    .query()
 | 
				
			||||||
 | 
					                                    .insert({
 | 
				
			||||||
 | 
					                                        user_id: user.id,
 | 
				
			||||||
 | 
					                                        type:    'password',
 | 
				
			||||||
 | 
					                                        secret:  'changeme',
 | 
				
			||||||
 | 
					                                        meta:    {}
 | 
				
			||||||
 | 
					                                    });
 | 
				
			||||||
 | 
					                            });
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/backend/views/index.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/backend/views/index.ejs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					<% var title = 'Nginx Proxy Manager' %>
 | 
				
			||||||
 | 
					<%- include partials/header.ejs %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div id="app">
 | 
				
			||||||
 | 
					    <span class="loader"></span>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script type="text/javascript" src="/js/main.js?v=<%= version %>"></script>
 | 
				
			||||||
 | 
					<%- include partials/footer.ejs %>
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/backend/views/login.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/backend/views/login.ejs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					<% var title = 'Login – Nginx Proxy Manager' %>
 | 
				
			||||||
 | 
					<%- include partials/header.ejs %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="page" id="login" data-version="<%= version %>">
 | 
				
			||||||
 | 
					    <span class="loader"></span>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script type="text/javascript" src="/js/login.js?v=<%= version %>"></script>
 | 
				
			||||||
 | 
					<%- include partials/footer.ejs %>
 | 
				
			||||||
							
								
								
									
										2
									
								
								src/backend/views/partials/footer.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/backend/views/partials/footer.ejs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					    </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										36
									
								
								src/backend/views/partials/header.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/backend/views/partials/header.ejs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					<!doctype html>
 | 
				
			||||||
 | 
					<html lang="en" dir="ltr">
 | 
				
			||||||
 | 
					    <head>
 | 
				
			||||||
 | 
					        <meta charset="utf-8">
 | 
				
			||||||
 | 
					        <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
 | 
				
			||||||
 | 
					        <meta http-equiv="X-UA-Compatible" content="ie=edge">
 | 
				
			||||||
 | 
					        <meta http-equiv="Content-Language" content="en">
 | 
				
			||||||
 | 
					        <meta name="msapplication-TileColor" content="#2d89ef">
 | 
				
			||||||
 | 
					        <meta name="theme-color" content="#4188c9">
 | 
				
			||||||
 | 
					        <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
 | 
				
			||||||
 | 
					        <meta name="apple-mobile-web-app-capable" content="yes">
 | 
				
			||||||
 | 
					        <meta name="mobile-web-app-capable" content="yes">
 | 
				
			||||||
 | 
					        <meta name="HandheldFriendly" content="True">
 | 
				
			||||||
 | 
					        <meta name="MobileOptimized" content="320">
 | 
				
			||||||
 | 
					        <title><%- title %></title>
 | 
				
			||||||
 | 
					        <link rel="apple-touch-icon" sizes="180x180" href="/images/favicons/apple-touch-icon.png?v=<%= version %>">
 | 
				
			||||||
 | 
					        <link rel="icon" type="image/png" sizes="32x32" href="/images/favicons/favicon-32x32.png?v=<%= version %>">
 | 
				
			||||||
 | 
					        <link rel="icon" type="image/png" sizes="16x16" href="/images/favicons/favicon-16x16.png?v=<%= version %>">
 | 
				
			||||||
 | 
					        <link rel="manifest" href="/images/favicons/site.webmanifest?v=<%= version %>">
 | 
				
			||||||
 | 
					        <link rel="mask-icon" href="/images/favicons/safari-pinned-tab.svg?v=<%= version %>" color="#5bbad5">
 | 
				
			||||||
 | 
					        <link rel="shortcut icon" href="/images/favicons/favicon.ico?v=<%= version %>">
 | 
				
			||||||
 | 
					        <meta name="msapplication-TileColor" content="#f5f5f5">
 | 
				
			||||||
 | 
					        <meta name="msapplication-config" content="/images/favicons/browserconfig.xml?v=<%= version %>">
 | 
				
			||||||
 | 
					        <meta name="theme-color" content="#ffffff">
 | 
				
			||||||
 | 
					        <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300i,400,400i,500,500i,600,600i,700,700i&subset=latin-ext">
 | 
				
			||||||
 | 
					        <link href="/css/main.css?v=<%= version %>" rel="stylesheet">
 | 
				
			||||||
 | 
					    </head>
 | 
				
			||||||
 | 
					    <body>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <noscript>
 | 
				
			||||||
 | 
					        <div class="container no-js-warning">
 | 
				
			||||||
 | 
					            <div class="alert alert-warning text-center">
 | 
				
			||||||
 | 
					                <strong>Warning!</strong> This application requires Javascript and your browser doesn't support it.
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </noscript>
 | 
				
			||||||
		Reference in New Issue
	
	Block a user