const vnModel = require('vn-loopback/common/models/vn-model'); const {Email} = require('vn-print'); const ForbiddenError = require('vn-loopback/util/forbiddenError'); const LoopBackContext = require('loopback-context'); const UserError = require('vn-loopback/util/user-error'); module.exports = function(Self) { vnModel(Self); require('../methods/vn-user/sign-in')(Self); require('../methods/vn-user/acl')(Self); require('../methods/vn-user/recover-password')(Self); require('../methods/vn-user/privileges')(Self); require('../methods/vn-user/validate-auth')(Self); require('../methods/vn-user/renew-token')(Self); require('../methods/vn-user/update-user')(Self); Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create'); // Validations Self.validatesFormatOf('email', { message: 'Invalid email', allowNull: true, allowBlank: false, with: /^[\w|.|-]+@[\w|-]+(\.[\w|-]+)*(,[\w|.|-]+@[\w|-]+(\.[\w|-]+)*)*$/ }); Self.validatesUniquenessOf('name', { message: `A client with that Web User name already exists` }); Self.remoteMethod('getCurrentUserData', { description: 'Gets the current user data', accepts: [ { arg: 'ctx', type: 'Object', http: {source: 'context'} } ], returns: { type: 'Object', root: true }, http: { verb: 'GET', path: '/getCurrentUserData' } }); Self.getCurrentUserData = async function(ctx) { let userId = ctx.req.accessToken.userId; return await Self.findById(userId, { fields: ['id', 'name', 'nickname'] }); }; /** * Checks if user has a role. * * @param {Integer} userId The user id * @param {String} name The role name * @param {Object} options Options * @return {Boolean} %true if user has the role, %false otherwise */ Self.hasRole = async function(userId, name, options) { const roles = await Self.getRoles(userId, options); return roles.some(role => role == name); }; /** * Get all user roles. * * @param {Integer} userId The user id * @param {Object} options Options * @return {Object} User role list */ Self.getRoles = async(userId, options) => { const result = await Self.rawSql( `SELECT r.name FROM account.user u JOIN account.roleRole rr ON rr.role = u.role JOIN account.role r ON r.id = rr.inheritsFrom WHERE u.id = ?`, [userId], options); const roles = []; for (const role of result) roles.push(role.name); return roles; }; Self.on('resetPasswordRequest', async function(info) { const loopBackContext = LoopBackContext.getCurrentContext(); const httpCtx = {req: loopBackContext.active}; const httpRequest = httpCtx.req.http.req; const headers = httpRequest.headers; const origin = headers.origin; const defaultHash = '/reset-password?access_token=$token$'; const recoverHashes = { hedera: 'verificationToken=$token$' }; const app = info.options?.app; let recoverHash = app ? recoverHashes[app] : defaultHash; recoverHash = recoverHash.replace('$token$', info.accessToken.id); const user = await Self.app.models.VnUser.findById(info.user.id); const params = { recipient: info.email, lang: user.lang, url: origin + '/#!' + recoverHash }; const options = Object.assign({}, info.options); for (const param in options) params[param] = options[param]; const email = new Email(options.emailTemplate, params); return email.send(); }); /** * Sign-in validate * @param {String} user The user * @param {Object} userToken Options * @param {Object} token accessToken * @param {Object} ctx context */ Self.signInValidate = async(user, userToken, token, ctx) => { const [[key, value]] = Object.entries(Self.userUses(user)); const isOwner = Self.rawSql(`SELECT ? = ? `, [userToken[key], value]); if (!isOwner) { await Self.app.models.SignInLog.create({ userName: user, token: token.id, userFk: userToken.id, ip: ctx.req.ip, owner: isOwner }); throw new UserError('Try again'); } }; /** * Validate login params * @param {String} user The user * @param {String} password * @param {Object} ctx context */ Self.validateLogin = async function(user, password, ctx) { const loginInfo = Object.assign({password}, Self.userUses(user)); const token = await Self.login(loginInfo, 'user'); const userToken = await token.user.get(); if (ctx) await Self.signInValidate(user, userToken, token, ctx); try { await Self.app.models.Account.sync(userToken.name, password); } catch (err) { console.warn(err); } return {token: token.id, ttl: token.ttl}; }; Self.userUses = function(user) { return user.indexOf('@') !== -1 ? {email: user} : {username: user}; }; const _setPassword = Self.prototype.setPassword; Self.prototype.setPassword = async function(newPassword, options, cb) { if (cb === undefined && typeof options === 'function') { cb = options; options = undefined; } const myOptions = {}; let tx; if (typeof options == 'object') Object.assign(myOptions, options); if (!myOptions.transaction) { tx = await Self.beginTransaction({}); myOptions.transaction = tx; } options = myOptions; try { await Self.rawSql(`CALL account.user_checkPassword(?)`, [newPassword], options); await _setPassword.call(this, newPassword, options); await this.updateAttribute('passExpired', null, options); await Self.app.models.Account.sync(this.name, newPassword, null, options); tx && await tx.commit(); cb && cb(); } catch (err) { tx && await tx.rollback(); if (cb) cb(err); else throw err; } }; Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls = Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls .filter(acl => acl.property != 'changePassword'); Self.userSecurity = async(ctx, userId, options) => { const models = Self.app.models; const accessToken = ctx?.options?.accessToken || LoopBackContext.getCurrentContext().active.accessToken; const ctxToken = {req: {accessToken}}; if (userId === accessToken.userId) return; const myOptions = {}; if (typeof options == 'object') Object.assign(myOptions, options); const hasHigherPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'higherPrivileges', myOptions); if (hasHigherPrivileges) return; const hasMediumPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'mediumPrivileges', myOptions); const user = await models.VnUser.findById(userId, {fields: ['id', 'emailVerified']}, myOptions); if (!user.emailVerified && hasMediumPrivileges) return; throw new ForbiddenError(); }; Self.observe('after save', async ctx => { const instance = ctx?.instance; const newEmail = instance?.email; const oldEmail = ctx?.hookState?.oldInstance?.email; if (!ctx.isNewInstance && (!newEmail || !oldEmail || newEmail == oldEmail)) return; const loopBackContext = LoopBackContext.getCurrentContext(); const httpCtx = {req: loopBackContext.active}; const httpRequest = httpCtx.req.http.req; const headers = httpRequest.headers; const origin = headers.origin; const url = origin.split(':'); const env = process.env.NODE_ENV; const liliumUrl = await Self.app.models.Url.findOne({ where: { and: [ {appName: 'lilium'}, {environment: env} ] } }); class Mailer { async send(verifyOptions, cb) { try { const url = new URL(verifyOptions.verifyHref); if (process.env.NODE_ENV) url.port = ''; const email = new Email('email-verify', { url: url.href, recipient: verifyOptions.to }); await email.send(); cb(null, verifyOptions.to); } catch (err) { cb(err); } } } const options = { type: 'email', to: newEmail, from: {}, redirect: `${liliumUrl.url}verifyEmail?userId=${instance.id}`, template: false, mailer: new Mailer, host: url[1].split('/')[2], port: url[2], protocol: url[0], user: Self }; await instance.verify(options, ctx.options); }); };