mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-09-14 10:52:34 +00:00
506 lines
12 KiB
JavaScript
506 lines
12 KiB
JavaScript
import gravatar from "gravatar";
|
|
import _ from "lodash";
|
|
import errs from "../lib/error.js";
|
|
import utils from "../lib/utils.js";
|
|
import authModel from "../models/auth.js";
|
|
import userModel from "../models/user.js";
|
|
import userPermissionModel from "../models/user_permission.js";
|
|
import internalAuditLog from "./audit-log.js";
|
|
import internalToken from "./token.js";
|
|
|
|
const omissions = () => {
|
|
return ["is_deleted"];
|
|
}
|
|
|
|
const DEFAULT_AVATAR = 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=200&d=mp&r=g';
|
|
|
|
const internalUser = {
|
|
/**
|
|
* @param {Access} access
|
|
* @param {Object} data
|
|
* @returns {Promise}
|
|
*/
|
|
create: (access, data) => {
|
|
const 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;
|
|
});
|
|
}
|
|
return user;
|
|
})
|
|
.then((user) => {
|
|
// Create permissions row as well
|
|
const 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 errs.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 errs.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) => {
|
|
const thisData = data || {};
|
|
|
|
if (typeof thisData.id === "undefined" || !thisData.id) {
|
|
thisData.id = access.token.getUserId(0);
|
|
}
|
|
|
|
return access
|
|
.can("users:get", thisData.id)
|
|
.then(() => {
|
|
const query = userModel
|
|
.query()
|
|
.where("is_deleted", 0)
|
|
.andWhere("id", thisData.id)
|
|
.allowGraph("[permissions]")
|
|
.first();
|
|
|
|
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
|
|
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
|
|
}
|
|
|
|
return query.then(utils.omitRow(omissions()));
|
|
})
|
|
.then((row) => {
|
|
if (!row || !row.id) {
|
|
throw new errs.ItemNotFoundError(thisData.id);
|
|
}
|
|
// Custom omissions
|
|
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
|
|
return _.omit(row, thisData.omit);
|
|
}
|
|
|
|
if (row.avatar === "") {
|
|
row.avatar = DEFAULT_AVATAR;
|
|
}
|
|
|
|
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) => {
|
|
const 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 errs.ItemNotFoundError(data.id);
|
|
}
|
|
|
|
// Make sure user can't delete themselves
|
|
if (user.id === access.token.getUserId(0)) {
|
|
throw new errs.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(() => {
|
|
const 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 Number.parseInt(row.count, 10);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* All users
|
|
*
|
|
* @param {Access} access
|
|
* @param {Array} [expand]
|
|
* @param {String} [search_query]
|
|
* @returns {Promise}
|
|
*/
|
|
getAll: async (access, expand, search_query) => {
|
|
await access.can("users:list");
|
|
const 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(", ")}]`);
|
|
}
|
|
|
|
const res = await query;
|
|
return utils.omitRows(omissions())(res);
|
|
},
|
|
|
|
/**
|
|
* @param {Access} access
|
|
* @param {Integer} [id_requested]
|
|
* @returns {[String]}
|
|
*/
|
|
getUserOmisionsByAccess: (access, idRequested) => {
|
|
let response = []; // Admin response
|
|
|
|
if (!access.token.hasScope("admin") && access.token.getUserId(0) !== idRequested) {
|
|
response = ["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 errs.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 errs.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,
|
|
});
|
|
}
|
|
// 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 errs.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));
|
|
}
|
|
// 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);
|
|
});
|
|
},
|
|
};
|
|
|
|
export default internalUser;
|