You've already forked nginx-proxy-manager
							
							
				mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-11-04 04:11:42 +03:00 
			
		
		
		
	- /schema now returns full openapi/swagger schema - That schema is used to validate incoming requests - And used as a contract in future integration tests - Moved route files up one level - Fixed incorrect 404 reponses when getting objects - Fixed saving new objects and passing jsonschemavalidation
		
			
				
	
	
		
			514 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			514 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const _                   = require('lodash');
 | 
						|
const error               = require('../lib/error');
 | 
						|
const utils               = require('../lib/utils');
 | 
						|
const userModel           = require('../models/user');
 | 
						|
const userPermissionModel = require('../models/user_permission');
 | 
						|
const authModel           = require('../models/auth');
 | 
						|
const gravatar            = require('gravatar');
 | 
						|
const internalToken       = require('./token');
 | 
						|
const internalAuditLog    = require('./audit-log');
 | 
						|
 | 
						|
function omissions () {
 | 
						|
	return ['is_deleted'];
 | 
						|
}
 | 
						|
 | 
						|
const internalUser = {
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param   {Access}  access
 | 
						|
	 * @param   {Object}  data
 | 
						|
	 * @returns {Promise}
 | 
						|
	 */
 | 
						|
	create: (access, data) => {
 | 
						|
		let auth = data.auth || null;
 | 
						|
		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()
 | 
						|
					.insertAndFetch(data)
 | 
						|
					.then(utils.omitRow(omissions()));
 | 
						|
			})
 | 
						|
			.then((user) => {
 | 
						|
				if (auth) {
 | 
						|
					return authModel
 | 
						|
						.query()
 | 
						|
						.insert({
 | 
						|
							user_id: user.id,
 | 
						|
							type:    auth.type,
 | 
						|
							secret:  auth.secret,
 | 
						|
							meta:    {}
 | 
						|
						})
 | 
						|
						.then(() => {
 | 
						|
							return user;
 | 
						|
						});
 | 
						|
				} else {
 | 
						|
					return user;
 | 
						|
				}
 | 
						|
			})
 | 
						|
			.then((user) => {
 | 
						|
				// Create permissions row as well
 | 
						|
				let is_admin = data.roles.indexOf('admin') !== -1;
 | 
						|
 | 
						|
				return userPermissionModel
 | 
						|
					.query()
 | 
						|
					.insert({
 | 
						|
						user_id:           user.id,
 | 
						|
						visibility:        is_admin ? 'all' : 'user',
 | 
						|
						proxy_hosts:       'manage',
 | 
						|
						redirection_hosts: 'manage',
 | 
						|
						dead_hosts:        'manage',
 | 
						|
						streams:           'manage',
 | 
						|
						access_lists:      'manage',
 | 
						|
						certificates:      'manage'
 | 
						|
					})
 | 
						|
					.then(() => {
 | 
						|
						return internalUser.get(access, {id: user.id, expand: ['permissions']});
 | 
						|
					});
 | 
						|
			})
 | 
						|
			.then((user) => {
 | 
						|
				// Add to audit log
 | 
						|
				return internalAuditLog.add(access, {
 | 
						|
					action:      'created',
 | 
						|
					object_type: 'user',
 | 
						|
					object_id:   user.id,
 | 
						|
					meta:        user
 | 
						|
				})
 | 
						|
					.then(() => {
 | 
						|
						return user;
 | 
						|
					});
 | 
						|
			});
 | 
						|
	},
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param  {Access}  access
 | 
						|
	 * @param  {Object}  data
 | 
						|
	 * @param  {Integer} data.id
 | 
						|
	 * @param  {String}  [data.email]
 | 
						|
	 * @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()
 | 
						|
					.patchAndFetchById(user.id, data)
 | 
						|
					.then(utils.omitRow(omissions()));
 | 
						|
			})
 | 
						|
			.then(() => {
 | 
						|
				return internalUser.get(access, {id: data.id});
 | 
						|
			})
 | 
						|
			.then((user) => {
 | 
						|
				// Add to audit log
 | 
						|
				return internalAuditLog.add(access, {
 | 
						|
					action:      'updated',
 | 
						|
					object_type: 'user',
 | 
						|
					object_id:   user.id,
 | 
						|
					meta:        data
 | 
						|
				})
 | 
						|
					.then(() => {
 | 
						|
						return user;
 | 
						|
					});
 | 
						|
			});
 | 
						|
	},
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @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.getUserId(0);
 | 
						|
		}
 | 
						|
 | 
						|
		return access.can('users:get', data.id)
 | 
						|
			.then(() => {
 | 
						|
				let query = userModel
 | 
						|
					.query()
 | 
						|
					.where('is_deleted', 0)
 | 
						|
					.andWhere('id', data.id)
 | 
						|
					.allowGraph('[permissions]')
 | 
						|
					.first();
 | 
						|
 | 
						|
				if (typeof data.expand !== 'undefined' && data.expand !== null) {
 | 
						|
					query.withGraphFetched('[' + data.expand.join(', ') + ']');
 | 
						|
				}
 | 
						|
 | 
						|
				return query.then(utils.omitRow(omissions()));
 | 
						|
			})
 | 
						|
			.then((row) => {
 | 
						|
				if (!row || !row.id) {
 | 
						|
					throw new error.ItemNotFoundError(data.id);
 | 
						|
				}
 | 
						|
				// Custom omissions
 | 
						|
				if (typeof data.omit !== 'undefined' && data.omit !== null) {
 | 
						|
					row = _.omit(row, data.omit);
 | 
						|
				}
 | 
						|
				return row;
 | 
						|
			});
 | 
						|
	},
 | 
						|
 | 
						|
	/**
 | 
						|
	 * 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.getUserId(0)) {
 | 
						|
					throw new error.PermissionError('You cannot delete yourself.');
 | 
						|
				}
 | 
						|
 | 
						|
				return userModel
 | 
						|
					.query()
 | 
						|
					.where('id', user.id)
 | 
						|
					.patch({
 | 
						|
						is_deleted: 1
 | 
						|
					})
 | 
						|
					.then(() => {
 | 
						|
						// Add to audit log
 | 
						|
						return internalAuditLog.add(access, {
 | 
						|
							action:      'deleted',
 | 
						|
							object_type: 'user',
 | 
						|
							object_id:   user.id,
 | 
						|
							meta:        _.omit(user, omissions())
 | 
						|
						});
 | 
						|
					});
 | 
						|
			})
 | 
						|
			.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   {Array}   [expand]
 | 
						|
	 * @param   {String}  [search_query]
 | 
						|
	 * @returns {Promise}
 | 
						|
	 */
 | 
						|
	getAll: (access, expand, search_query) => {
 | 
						|
		return access.can('users:list')
 | 
						|
			.then(() => {
 | 
						|
				let query = userModel
 | 
						|
					.query()
 | 
						|
					.where('is_deleted', 0)
 | 
						|
					.groupBy('id')
 | 
						|
					.allowGraph('[permissions]')
 | 
						|
					.orderBy('name', 'ASC');
 | 
						|
 | 
						|
				// 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.withGraphFetched('[' + expand.join(', ') + ']');
 | 
						|
				}
 | 
						|
 | 
						|
				return query.then(utils.omitRows(omissions()));
 | 
						|
			});
 | 
						|
	},
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param   {Access} access
 | 
						|
	 * @param   {Integer} [id_requested]
 | 
						|
	 * @returns {[String]}
 | 
						|
	 */
 | 
						|
	getUserOmisionsByAccess: (access, id_requested) => {
 | 
						|
		let response = []; // Admin response
 | 
						|
 | 
						|
		if (!access.token.hasScope('admin') && access.token.getUserId(0) !== 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.getUserId(0)) {
 | 
						|
					// 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) => {
 | 
						|
				// Get auth, patch if it exists
 | 
						|
				return authModel
 | 
						|
					.query()
 | 
						|
					.where('user_id', user.id)
 | 
						|
					.andWhere('type', data.type)
 | 
						|
					.first()
 | 
						|
					.then((existing_auth) => {
 | 
						|
						if (existing_auth) {
 | 
						|
							// patch
 | 
						|
							return authModel
 | 
						|
								.query()
 | 
						|
								.where('user_id', user.id)
 | 
						|
								.andWhere('type', data.type)
 | 
						|
								.patch({
 | 
						|
									type:   data.type, // This is required for the model to encrypt on save
 | 
						|
									secret: data.secret
 | 
						|
								});
 | 
						|
						} else {
 | 
						|
							// insert
 | 
						|
							return authModel
 | 
						|
								.query()
 | 
						|
								.insert({
 | 
						|
									user_id: user.id,
 | 
						|
									type:    data.type,
 | 
						|
									secret:  data.secret,
 | 
						|
									meta:    {}
 | 
						|
								});
 | 
						|
						}
 | 
						|
					})
 | 
						|
					.then(() => {
 | 
						|
						// Add to Audit Log
 | 
						|
						return internalAuditLog.add(access, {
 | 
						|
							action:      'updated',
 | 
						|
							object_type: 'user',
 | 
						|
							object_id:   user.id,
 | 
						|
							meta:        {
 | 
						|
								name:             user.name,
 | 
						|
								password_changed: true,
 | 
						|
								auth_type:        data.type
 | 
						|
							}
 | 
						|
						});
 | 
						|
					});
 | 
						|
			})
 | 
						|
			.then(() => {
 | 
						|
				return true;
 | 
						|
			});
 | 
						|
	},
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param  {Access}  access
 | 
						|
	 * @param  {Object}  data
 | 
						|
	 * @return {Promise}
 | 
						|
	 */
 | 
						|
	setPermissions: (access, data) => {
 | 
						|
		return access.can('users:permissions', 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);
 | 
						|
				}
 | 
						|
 | 
						|
				return user;
 | 
						|
			})
 | 
						|
			.then((user) => {
 | 
						|
				// Get perms row, patch if it exists
 | 
						|
				return userPermissionModel
 | 
						|
					.query()
 | 
						|
					.where('user_id', user.id)
 | 
						|
					.first()
 | 
						|
					.then((existing_auth) => {
 | 
						|
						if (existing_auth) {
 | 
						|
							// patch
 | 
						|
							return userPermissionModel
 | 
						|
								.query()
 | 
						|
								.where('user_id', user.id)
 | 
						|
								.patchAndFetchById(existing_auth.id, _.assign({user_id: user.id}, data));
 | 
						|
						} else {
 | 
						|
							// insert
 | 
						|
							return userPermissionModel
 | 
						|
								.query()
 | 
						|
								.insertAndFetch(_.assign({user_id: user.id}, data));
 | 
						|
						}
 | 
						|
					})
 | 
						|
					.then((permissions) => {
 | 
						|
						// Add to Audit Log
 | 
						|
						return internalAuditLog.add(access, {
 | 
						|
							action:      'updated',
 | 
						|
							object_type: 'user',
 | 
						|
							object_id:   user.id,
 | 
						|
							meta:        {
 | 
						|
								name:        user.name,
 | 
						|
								permissions: permissions
 | 
						|
							}
 | 
						|
						});
 | 
						|
 | 
						|
					});
 | 
						|
			})
 | 
						|
			.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;
 |