mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-30 23:33:34 +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