import passport from "passport" import createError from "http-errors" import crypto from "crypto" import urlSafeBase64 from "urlsafe-base64" import url from "url" import util from "util" import autobind from "autobind-decorator" import config from "config" import { catchAll } from "." @autobind export class UserRoutes { constructor(container) { const app = container.app this.log = container.log this.db = container.db this.mq = container.mq this.ws = container.ws this.maxEmailTokenAgeInHours = config.get("email.maxEmailTokenAgeInHours") this.sendEmail = config.get("email.sendEmail") this.emailServiceName = config.get("serviceName.email") app .route("/users") .get( passport.authenticate("bearer", { session: false }), catchAll(this.listUsers) ) // Add a new user, send email confirmation email .post( passport.authenticate("bearer", { session: false }), catchAll(this.createUser) ) .put( passport.authenticate("bearer", { session: false }), catchAll(this.updateUser) ) app .route("/users/:_id([a-f0-9]{24})") .get( passport.authenticate("bearer", { session: false }), catchAll(this.getUser) ) .delete( passport.authenticate("bearer", { session: false }), catchAll(this.deleteUser) ) app .route("/users/enter-room/:roomName") .put( passport.authenticate("bearer", { session: false }), catchAll(this.enterRoom) ) app .route("/users/leave-room") .put( passport.authenticate("bearer", { session: false }), catchAll(this.leaveRoom) ) } async listUsers(req, res, next) { const User = this.db.User const limit = req.query.limit || 20 const skip = req.query.skip || 0 const partial = !!req.query.partial const isAdmin = !!req.user.administrator const team = req.query.team let query = {} if (team) { query = { team } } if (!isAdmin) { throw createError.Forbidden() } const total = await User.count({}) let users = [] let cursor = User.find(query) .limit(limit) .skip(skip) .cursor() .map((doc) => { return doc.toClient(req.user) }) cursor.on("data", (doc) => { users.push(doc) }) cursor.on("end", () => { res.json({ total: total, offset: skip, count: users.length, items: users, }) }) cursor.on("error", (err) => { throw err }) } async getUser(req, res, next) { let User = this.db.User const _id = req.params._id const isSelf = _id === req.user._id const isAdmin = req.user.administrator // User can see themselves, otherwise must be admin if (!isSelf && !isAdmin) { throw createError.Forbidden() } const user = await User.findById(_id) if (!user) { return Promise.reject( createError.NotFound(`User with _id ${_id} was not found`) ) } res.json(user.toClient(req.user)) } async createUser(req, res, next) { const isAdmin = req.user.administrator if (!isAdmin) { throw new createError.Forbidden() } let User = this.db.User let user = new User(req.body) // Add email confirmation required token const buf = await util.promisify(crypto.randomBytes)(32) user.emailToken = { value: urlSafeBase64.encode(buf), created: new Date(), } const savedUser = await user.save() const userFullName = `${savedUser.firstName} ${savedUser.lastName}` const senderFullName = `${req.user.firstName} ${req.user.lastName}` const siteUrl = url.parse(req.headers.referer) const msg = { toEmail: savedUser.email, templateName: "welcome", templateData: { recipientFullName: userFullName, senderFullName: senderFullName, confirmEmailLink: `${siteUrl.protocol}//${ siteUrl.host }/confirm-email?email-token%3D${savedUser.emailToken.value}`, confirmEmailLinkExpirationHours: this.maxEmailTokenAgeInHours, supportEmail: this.supportEmail, }, } res.json(savedUser.toClient()) if (this.sendEmail) { await this.mq.request(this.emailServiceName, "sendEmail", msg) } } async updateUser(req, res, next) { const isAdmin = req.user.administrator // Do this here because Mongoose will add it automatically otherwise if (!req.body._id) { throw createError.BadRequest("No user _id given in body") } const isSelf = req.body._id === req.user._id.toString() // User can change themselves, otherwise must be super user if (!isSelf && !isAdmin) { throw createError.Forbidden() } if (isSelf && isAdmin && !req.body.administrator) { throw createError.BadRequest("Cannot remove own administrator level") } const User = this.db.User const user = await User.findById(req.body._id) if (!user) { throw createError.NotFound(`User with _id ${req.body._id} was not found`) } // We don't allow direct updates to the email field so remove it if present const userUpdates = new User(req.body) userUpdates.email = undefined user.merge(userUpdates) const savedUser = await user.save() res.json(savedUser.toClient(req.user)) } enterRoom(req, res, next) { this.ws.enterRoom(req.user._id, req.params.roomName) res.json({}) } leaveRoom(req, res, next) { this.ws.enterRoom(req.user._id) res.json({}) } async deleteUser(req, res, next) { const isAdmin = req.user.administrator if (!isAdmin) { throw createError.Forbidden() } let User = this.db.User const _id = req.params._id const deletedUser = await User.remove({ _id }) if (!deletedUser) { throw createError.NotFound(`User with _id ${_id} was not found`) } const userFullName = `${deletedUser.firstName} ${deletedUser.lastName}` const senderFullName = `${req.user.firstName} ${req.user.lastName}` const msg = { toEmail: deletedUser.newEmail, templateName: "accountDeleted", templateData: { recipientFullName: userFullName, senderFullName: senderFullName, supportEmail: this.supportEmail, }, } res.json({}) if (this.sendEmail) { await this.mq.request(this.emailServiceName, "sendEmail", msg) } } }