Files
elm-0.19-workshop/server/services/users.service.js
2018-05-04 19:46:36 -04:00

390 lines
10 KiB
JavaScript

"use strict";
const { MoleculerClientError } = require("moleculer").Errors;
//const crypto = require("crypto");
const bcrypt = require("bcrypt-nodejs");
const jwt = require("jsonwebtoken");
const DbService = require("../mixins/db.mixin");
module.exports = {
name: "users",
mixins: [DbService("users")],
/**
* Default settings
*/
settings: {
/** Secret for JWT */
JWT_SECRET: process.env.JWT_SECRET || "jwt-conduit-secret",
/** Public fields */
fields: ["_id", "username", "email", "bio", "image"],
/** Validator schema for entity */
entityValidator: {
username: { type: "string", min: 2, pattern: /^[a-zA-Z0-9]+$/ },
password: { type: "string", min: 6 },
email: { type: "email" },
bio: { type: "string", optional: true },
image: { type: "string", optional: true },
}
},
/**
* Actions
*/
actions: {
/**
* Register a new user
*
* @actions
* @param {Object} user - User entity
*
* @returns {Object} Created entity & token
*/
create: {
params: {
user: { type: "object" }
},
handler(ctx) {
let entity = ctx.params.user;
return this.validateEntity(entity)
.then(() => {
if (entity.username)
return this.adapter.findOne({ username: entity.username })
.then(found => {
if (found)
return Promise.reject(new MoleculerClientError("Username is exist!", 422, "", [{ field: "username", message: "is exist"}]));
});
})
.then(() => {
if (entity.email)
return this.adapter.findOne({ email: entity.email })
.then(found => {
if (found)
return Promise.reject(new MoleculerClientError("Email is exist!", 422, "", [{ field: "email", message: "is exist"}]));
});
})
.then(() => {
entity.password = bcrypt.hashSync(entity.password, 10);
entity.bio = entity.bio || "";
entity.image = entity.image || null;
entity.createdAt = new Date();
return this.adapter.insert(entity)
.then(doc => this.transformDocuments(ctx, {}, doc))
.then(user => this.transformEntity(user, true, ctx.meta.token))
.then(json => this.entityChanged("created", json, ctx).then(() => json));
});
}
},
/**
* Login with username & password
*
* @actions
* @param {Object} user - User credentials
*
* @returns {Object} Logged in user with token
*/
login: {
params: {
user: { type: "object", props: {
email: { type: "email" },
password: { type: "string", min: 1 }
}}
},
handler(ctx) {
const { email, password } = ctx.params.user;
return this.Promise.resolve()
.then(() => this.adapter.findOne({ email }))
.then(user => {
if (!user)
return this.Promise.reject(new MoleculerClientError("Email or password is invalid!", 422, "", [{ field: "email", message: "is not found"}]));
return bcrypt.compare(password, user.password).then(res => {
if (!res)
return Promise.reject(new MoleculerClientError("Wrong password!", 422, "", [{ field: "email", message: "is not found"}]));
// Transform user entity (remove password and all protected fields)
return this.transformDocuments(ctx, {}, user);
});
})
.then(user => this.transformEntity(user, true, ctx.meta.token));
}
},
/**
* Get user by JWT token (for API GW authentication)
*
* @actions
* @param {String} token - JWT token
*
* @returns {Object} Resolved user
*/
resolveToken: {
cache: {
keys: ["token"],
ttl: 60 * 60 // 1 hour
},
params: {
token: "string"
},
handler(ctx) {
return new this.Promise((resolve, reject) => {
jwt.verify(ctx.params.token, this.settings.JWT_SECRET, (err, decoded) => {
if (err)
return reject(err);
resolve(decoded);
});
})
.then(decoded => {
if (decoded.id)
return this.getById(decoded.id);
});
}
},
/**
* Get current user entity.
* Auth is required!
*
* @actions
*
* @returns {Object} User entity
*/
me: {
auth: "required",
cache: {
keys: ["#token"]
},
handler(ctx) {
return this.getById(ctx.meta.user._id)
.then(user => {
if (!user)
return this.Promise.reject(new MoleculerClientError("User not found!", 400));
return this.transformDocuments(ctx, {}, user);
})
.then(user => this.transformEntity(user, true, ctx.meta.token));
}
},
/**
* Update current user entity.
* Auth is required!
*
* @actions
*
* @param {Object} user - Modified fields
* @returns {Object} User entity
*/
updateMyself: {
auth: "required",
params: {
user: { type: "object", props: {
username: { type: "string", min: 2, optional: true, pattern: /^[a-zA-Z0-9]+$/ },
password: { type: "string", min: 6, optional: true },
email: { type: "email", optional: true },
bio: { type: "string", optional: true },
image: { type: "string", optional: true },
}}
},
handler(ctx) {
const newData = ctx.params.user;
return this.Promise.resolve()
.then(() => {
if (newData.username)
return this.adapter.findOne({ username: newData.username })
.then(found => {
if (found && found._id.toString() !== ctx.meta.user._id.toString())
return Promise.reject(new MoleculerClientError("Username is exist!", 422, "", [{ field: "username", message: "is exist"}]));
});
})
.then(() => {
if (newData.email)
return this.adapter.findOne({ email: newData.email })
.then(found => {
if (found && found._id.toString() !== ctx.meta.user._id.toString())
return Promise.reject(new MoleculerClientError("Email is exist!", 422, "", [{ field: "email", message: "is exist"}]));
});
})
.then(() => {
newData.updatedAt = new Date();
const update = {
"$set": newData
};
return this.adapter.updateById(ctx.meta.user._id, update);
})
.then(doc => this.transformDocuments(ctx, {}, doc))
.then(user => this.transformEntity(user, true, ctx.meta.token))
.then(json => this.entityChanged("updated", json, ctx).then(() => json));
}
},
/**
* Get a user profile.
*
* @actions
*
* @param {String} username - Username
* @returns {Object} User entity
*/
profile: {
cache: {
keys: ["#token", "username"]
},
params: {
username: { type: "string" }
},
handler(ctx) {
return this.adapter.findOne({ username: ctx.params.username })
.then(user => {
if (!user)
return this.Promise.reject(new MoleculerClientError("User not found!", 404));
return this.transformDocuments(ctx, {}, user);
})
.then(user => this.transformProfile(ctx, user, ctx.meta.user));
}
},
/**
* Follow a user
* Auth is required!
*
* @actions
*
* @param {String} username - Followed username
* @returns {Object} Current user entity
*/
follow: {
auth: "required",
params: {
username: { type: "string" }
},
handler(ctx) {
return this.adapter.findOne({ username: ctx.params.username })
.then(user => {
if (!user)
return this.Promise.reject(new MoleculerClientError("User not found!", 404));
return ctx.call("follows.add", { user: ctx.meta.user._id.toString(), follow: user._id.toString() })
.then(() => this.transformDocuments(ctx, {}, user));
})
.then(user => this.transformProfile(ctx, user, ctx.meta.user));
}
},
/**
* Unfollow a user
* Auth is required!
*
* @actions
*
* @param {String} username - Unfollowed username
* @returns {Object} Current user entity
*/
unfollow: {
auth: "required",
params: {
username: { type: "string" }
},
handler(ctx) {
return this.adapter.findOne({ username: ctx.params.username })
.then(user => {
if (!user)
return this.Promise.reject(new MoleculerClientError("User not found!", 404));
return ctx.call("follows.delete", { user: ctx.meta.user._id.toString(), follow: user._id.toString() })
.then(() => this.transformDocuments(ctx, {}, user));
})
.then(user => this.transformProfile(ctx, user, ctx.meta.user));
}
}
},
/**
* Methods
*/
methods: {
/**
* Generate a JWT token from user entity
*
* @param {Object} user
*/
generateJWT(user) {
const today = new Date();
const exp = new Date(today);
exp.setDate(today.getDate() + 60);
return jwt.sign({
id: user._id,
username: user.username,
exp: Math.floor(exp.getTime() / 1000)
}, this.settings.JWT_SECRET);
},
/**
* Transform returned user entity. Generate JWT token if neccessary.
*
* @param {Object} user
* @param {Boolean} withToken
*/
transformEntity(user, withToken, token) {
if (user) {
//user.image = user.image || "https://www.gravatar.com/avatar/" + crypto.createHash("md5").update(user.email).digest("hex") + "?d=robohash";
user.image = user.image || "";
if (withToken)
user.token = token || this.generateJWT(user);
}
return { user };
},
/**
* Transform returned user entity as profile.
*
* @param {Context} ctx
* @param {Object} user
* @param {Object?} loggedInUser
*/
transformProfile(ctx, user, loggedInUser) {
//user.image = user.image || "https://www.gravatar.com/avatar/" + crypto.createHash("md5").update(user.email).digest("hex") + "?d=robohash";
user.image = user.image || "https://static.productionready.io/images/smiley-cyrus.jpg";
if (loggedInUser) {
return ctx.call("follows.has", { user: loggedInUser._id.toString(), follow: user._id.toString() })
.then(res => {
user.following = res;
return { profile: user };
});
}
user.following = false;
return { profile: user };
}
},
events: {
"cache.clean.users"() {
if (this.broker.cacher)
this.broker.cacher.clean(`${this.name}.*`);
},
"cache.clean.follows"() {
if (this.broker.cacher)
this.broker.cacher.clean(`${this.name}.*`);
}
}
};