UNPKG

gypsum

Version:

Simple and easy lightweight typescript server side framework on Node.js.

586 lines 24.7 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; Object.defineProperty(exports, "__esModule", { value: true }); const path = require("path"); const fs = require("fs"); const MongoDB = require("mongodb"); const jwt = require("jsonwebtoken"); const crypt_1 = require("tools-box/crypt"); const string_1 = require("tools-box/string"); const unique_1 = require("tools-box/unique"); const Validall = require("validall"); const decorators_1 = require("../../decorators"); const models_1 = require("../../models"); const nodemailer_1 = require("nodemailer"); const state_1 = require("../../state"); const schema_1 = require("./schema"); const types_1 = require("../../types"); let Users = class Users extends models_1.MongoModel { constructor(app) { super(app); if (state_1.State.auth.transporter) { this.transporter = nodemailer_1.createTransport(state_1.State.auth.transporter); } else { nodemailer_1.createTestAccount((err, account) => { if (err) throw err; this.transporter = nodemailer_1.createTransport({ host: 'smtp.ethereal.email', port: 587, secure: false, auth: { user: account.user, pass: account.pass } }); }); } } getRootUser() { return new Promise((resolve, reject) => { this.$logger.info('getting root user: ' + state_1.State.auth.rootUserEmail); this.collection.findOne({ username: state_1.State.auth.rootUserEmail }) .then(doc => { resolve(doc); }) .catch(error => reject(error)); }); } createRootUser() { this.$logger.info('creating root user: ' + state_1.State.auth.rootUserEmail); return new Promise((resolve, reject) => { crypt_1.hash(state_1.State.auth.rootUserPassword) .then(results => { this.collection.insertOne({ email: state_1.State.auth.rootUserEmail, password: results[0], passwordSalt: results[1], verified: true }) .then(res => { if (res && res.ops.length) resolve(res.ops[0]); else reject('unable to create root user'); }) .catch(error => reject(error)); }) .catch(error => reject(error)); }); } getSockets(ids) { return new Promise((resolve, reject) => { if (ids && ids.length) { for (let i = 0; i < ids.length; i++) if (typeof ids[i] === "string") ids[i] = new MongoDB.ObjectID(ids[i]); this.collection.find({ _id: { $in: ids } }) .project({ sockets: 1 }) .toArray() .then(res => resolve(res .map(entry => entry.sockets) .reduce((a, b) => { a.push(...(b.filter(entry => !!entry))); return a; }, []))) .catch(err => reject(err)); } else { resolve([]); } }); } pushToken(ctx) { return new Promise((resolve, reject) => { let responseData = ctx.response.data; if (!responseData || !responseData._id) return resolve(); responseData.token = jwt.sign({ id: responseData._id, date: Date.now(), type: 'auth' }, state_1.State.auth.tokenSecret); resolve(); }); } pushNotification(ctx, id) { } authenticate(ctx) { return new Promise((resolve, reject) => { this.$logger.info(`authenticating user for ${ctx.service.__name} service...`); let token = ctx.getHeader('token') || ctx.body.token || ctx.query.token; if (!token) { return reject({ message: `[${this.name}]: user token is missing`, code: types_1.RESPONSE_CODES.UNAUTHORIZED }); } let data; try { data = jwt.verify(token, state_1.State.auth.tokenSecret); } catch (err) { return reject({ message: `[${this.name}]: invalid user token`, code: types_1.RESPONSE_CODES.UNAUTHORIZED }); } if (!data || !data.id) return reject({ message: `[${this.name}]: invalid user token`, code: types_1.RESPONSE_CODES.UNAUTHORIZED }); if (data.type !== 'verifyEmail' && data.type !== 'auth') { return reject({ message: `[${this.name}]: fake token`, code: types_1.RESPONSE_CODES.UNAUTHORIZED }); } if (data.type === 'auth' && (!data.date || ((Date.now() - data.date) > state_1.State.auth.tokenExpiry))) { return reject({ message: `[${this.name}]: out dated token`, code: types_1.RESPONSE_CODES.UNAUTHORIZED }); } ctx.set('tokenData', data); this.collection.findOne({ _id: new MongoDB.ObjectID(data.id) }) .then(doc => { if (!doc) return reject({ message: `[${this.name}]: user no longer exists`, code: types_1.RESPONSE_CODES.UNAUTHORIZED }); if (data.type === "verifyEmail") { ctx.user = doc; return resolve(); } let update = { $set: { lastVisit: Date.now() } }; if (ctx._socket && (doc.sockets.indexOf(ctx._socket.id) === -1)) update.$push = { sockets: ctx._socket.id }; this.collection.findOneAndUpdate({ _id: doc._id }, update, { returnOriginal: false }) .then((res) => { ctx.user = res.value; resolve(); }) .catch(err => reject({ message: `[${this.name}]: error updating user auth`, original: err, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR })); }) .catch(err => reject({ message: `[${this.name}]: error finding user`, original: err, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR })); }); } verificationEmail(ctx) { return new Promise((resolve, reject) => { let user = ctx.user || ctx.response.data; if (!user) return reject({ message: 'user_not_provided', code: types_1.RESPONSE_CODES.UNKNOWN_ERROR }); fs.readFile(path.join(__dirname, '../templates/activation-email-template.html'), (err, template) => { if (err) return reject({ message: 'error_reading_activation_email_template', original: err, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR }); let token = jwt.sign({ id: user._id, date: Date.now(), type: 'verifyEmail' }, state_1.State.auth.tokenSecret); let activationLink = `http${state_1.State.config.secure ? 's' : ''}://`; activationLink += `${this.app.name}.${state_1.State.config.domain}/`; activationLink += `${this.name}/activateUser?token=${token}`; this.$logger.info('activation link:', activationLink); let emailOptions = { from: state_1.State.auth.supportEmail || `${state_1.State.config.server_name} Administration`, to: user.email, subject: `${state_1.State.config.server_name} Account Verification`, html: string_1.compile(template.toString('utf-8'), { username: user.email.split('@')[0], activationLink }) }; this.transporter.sendMail(emailOptions, (sendEmailError, info) => { if (sendEmailError) return reject({ message: `error sending activation email`, original: sendEmailError, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR }); this.$logger.info('Message %s sent: %s', info.messageId, info.response); resolve(); }); }); }); } sendVerificationEmail(email, ctx) { return new Promise((resolve, reject) => { this.collection.findOne({ email: email }) .then(doc => { if (doc) { ctx.user = doc; resolve({ data: true }); } else { reject({ message: 'email_not_found', code: types_1.RESPONSE_CODES.UNAUTHORIZED }); } }) .catch(err => reject(err)); }); } activateUser(ctx) { return new Promise((resolve, reject) => { let user = ctx.user; let tokenData = ctx.get('tokenData'); if (tokenData.type !== "verifyEmail") { return reject({ message: `[${this.name}]: fake token`, code: types_1.RESPONSE_CODES.UNAUTHORIZED }); } if (!tokenData.date || ((Date.now() - tokenData.date) > state_1.State.auth.verificationEmailExpiry)) { return reject({ message: `[${this.name}]: out dated token`, code: types_1.RESPONSE_CODES.UNAUTHORIZED }); } this.collection .updateOne({ _id: new MongoDB.ObjectID(user._id) }, { $set: { verified: true } }) .then(doc => { fs.readFile(path.join(__dirname, '../templates/activation-page-template.html'), 'utf-8', (err, data) => { if (err) return reject({ message: `[${this.name}]: error reading activation page`, original: err, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR }); let message = 'Your account has been activated successfully'; let template = string_1.compile(data, { username: ctx.user.email.split('@')[0], message: message }); resolve({ data: template, type: 'html' }); }); }) .catch(error => reject({ message: `[${this.name}]: error activating account`, original: error, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR })); }); } forgetPassword(email) { return new Promise((resolve, reject) => { if (!email) return reject({ message: 'email_is_required', code: types_1.RESPONSE_CODES.BAD_REQUEST }); if (!Validall.Is.email('email')) return reject({ message: 'invalid_email', code: types_1.RESPONSE_CODES.BAD_REQUEST }); this.collection.findOne({ email: email }) .then(user => { if (!user) return reject({ message: 'email_is_not_registered', code: types_1.RESPONSE_CODES.BAD_REQUEST }); let newPassword = unique_1.Unique.Get(); let hashedPassword; let passwordSalt; crypt_1.hash(newPassword) .then((result) => { let hashedPassword = result[0]; let passwordSalt = result[1]; this.collection.findOneAndUpdate({ _id: user._id }, { $set: { password: hashedPassword, passwordSalt: passwordSalt, } }, { returnOriginal: false }) .then(doc => { fs.readFile(path.join(__dirname, '../templates/forget-password-email-template.html'), (err, template) => { if (err) return reject({ message: 'error reading forget password email template', original: err, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR }); let emailOptions = { from: state_1.State.auth.supportEmail || `${state_1.State.config.server_name} Administration`, to: user.email, subject: `${state_1.State.config.server_name} Reset Password`, html: string_1.compile(template.toString('utf-8'), { username: user.email.split('@')[0], password: newPassword }) }; this.transporter.sendMail(emailOptions, (sendEmailError, info) => { if (sendEmailError) return reject({ message: `error_sending_forget_password_email`, original: sendEmailError, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR }); this.$logger.info('Message %s sent: %s', info.messageId, info.response); resolve(true); }); }); }) .catch(err => reject({ message: 'error updaring user password', original: err, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR })); }) .catch(err => reject({ message: 'error hashing new password', original: err, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR })); }) .catch(err => reject({ message: 'error finding user', original: err, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR })); }); } verifyToken(token, ctx) { return new Promise((resolve, reject) => { if (ctx.user) return resolve({ data: ctx.user }); reject({ message: 'invalid token', code: types_1.RESPONSE_CODES.UNAUTHORIZED }); }); } signin(email, password, uuid, ctx) { return new Promise((resolve, reject) => { if (!email || !email.trim()) return reject({ message: 'email is required', code: types_1.RESPONSE_CODES.BAD_REQUEST }); if (!password || !password.trim()) return reject({ message: 'password is required', code: types_1.RESPONSE_CODES.BAD_REQUEST }); let query = {}; if (Validall.Is.email(email)) query.email = email; else return reject({ message: 'invalid email', code: types_1.RESPONSE_CODES.BAD_REQUEST }); this.collection .findOne(query) .then(doc => { if (!doc || !Object.keys(doc).length) return reject({ message: 'user is not found', code: types_1.RESPONSE_CODES.UNAUTHORIZED }); crypt_1.verify(password, doc.password, doc.passwordSalt) .then((match) => { if (match === true) { let update = { $set: { lastVisit: Date.now() } }; if (uuid && doc.uuids.indexOf(uuid) === -1) update.$push = { uuids: uuid }; this.updateById(doc._id, update) .then(res => resolve(res)); } else { reject({ message: 'wrong password', code: types_1.RESPONSE_CODES.UNAUTHORIZED }); } }) .catch(error => reject({ message: 'error verifying password', original: error, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR })); }) .catch(error => reject({ message: `[${this.name}] - findOne: unknown error`, original: error, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR })); }); } signup(document, ctx) { return new Promise((resolve, reject) => { try { let state = Validall(document, this.$get('schema')); if (!state) return reject({ message: Validall.error.message, original: Validall.error, code: types_1.RESPONSE_CODES.BAD_REQUEST }); this.collection.count({ email: document.email }) .then(count => { if (count) return reject({ message: 'document with same email already exists!', code: types_1.RESPONSE_CODES.BAD_REQUEST }); crypt_1.hash(document.password) .then(results => { if (results && results.length) { document.password = results[0]; document.passwordSalt = results[1]; this.insertOne(document) .then(doc => { resolve(doc); }) .catch(error => reject(error)); } else { reject({ message: 'Error hashing password', code: types_1.RESPONSE_CODES.UNKNOWN_ERROR }); } }) .catch(error => { reject({ message: 'Error hashing password', original: error, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR }); }); }) .catch(err => reject({ message: 'error checking email existance!', original: err, code: types_1.RESPONSE_CODES.BAD_REQUEST })); } catch (e) { console.trace(e); } }); } signout(uuid, ctx) { return new Promise((resolve, reject) => { let update = {}; if (ctx._socket) update.$pop = { sockets: ctx._socket }; if (uuid) update.$pop = { uuids: uuid }; if (!Object.keys(update).length) return resolve(true); this.collection.updateOne({ _id: ctx.user._id }, update) .then(res => { resolve({ data: true }); }) .catch(err => { this.$logger.error('error signing user out:'); this.$logger.error(err); reject({ message: 'error signing user out', original: err, code: types_1.RESPONSE_CODES.UNKNOWN_ERROR }); }); }); } }; __decorate([ decorators_1.HOOK() ], Users.prototype, "pushToken", null); __decorate([ decorators_1.HOOK() ], Users.prototype, "pushNotification", null); __decorate([ decorators_1.HOOK() ], Users.prototype, "authenticate", null); __decorate([ decorators_1.HOOK({ private: true }) ], Users.prototype, "verificationEmail", null); __decorate([ decorators_1.SERVICE({ secure: false, authorize: false, args: ['body.email'], method: 'post', after: ['-filter', 'auth.users.verificationEmail'] }) ], Users.prototype, "sendVerificationEmail", null); __decorate([ decorators_1.SERVICE({ secure: true, authorize: false, after: ['-filter'] }) ], Users.prototype, "activateUser", null); __decorate([ decorators_1.SERVICE({ secure: false, authorize: false, method: 'post', args: ['body.email'], after: ['-filter'] }) ], Users.prototype, "forgetPassword", null); __decorate([ decorators_1.SERVICE({ secure: true, authorize: false, args: ['body.token'], method: 'post', after: [`auth.users.pushToken`] }) ], Users.prototype, "verifyToken", null); __decorate([ decorators_1.SERVICE({ secure: false, authorize: false, args: ['body.email', 'body.password', 'body.uuid'], method: 'post', after: [`auth.users.pushToken`] }) ], Users.prototype, "signin", null); __decorate([ decorators_1.SERVICE({ secure: false, authorize: false, args: ['body.document'], method: 'post', after: [`auth.users.pushToken`, `auth.users.verificationEmail`] }) ], Users.prototype, "signup", null); __decorate([ decorators_1.SERVICE({ secure: true, authorize: false, args: ['body.uuid'] }) ], Users.prototype, "signout", null); Users = __decorate([ decorators_1.MODEL({ secure: true, authorize: true, schema: schema_1.usersSchema, servicesOptions: { FindById: { authorize: { match: '$params.id' } }, UpdateById: { authorize: { match: '$params.id' } } }, domain: types_1.RESPONSE_DOMAINS.SELF, after: ['filter:-password,passwordSalt,uuids,sockets,lastVisit'], indexes: [{ name: 'email', options: { unique: true } }] }) ], Users); exports.Users = Users; //# sourceMappingURL=index.js.map