390 lines
10 KiB
JavaScript
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, bcrypt.genSaltSync(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 || "/assets/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}.*`);
|
|
}
|
|
}
|
|
}; |