Initial commit.
This commit is contained in:
390
server/services/users.service.js
Normal file
390
server/services/users.service.js
Normal file
@@ -0,0 +1,390 @@
|
||||
"use strict";
|
||||
|
||||
const { MoleculerClientError } = require("moleculer").Errors;
|
||||
|
||||
//const crypto = require("crypto");
|
||||
const bcrypt = require("bcrypt");
|
||||
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}.*`);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user