@datawheel/canon-core
Version:
Reusable React environment and components for creating visualization engines.
317 lines (244 loc) • 10.3 kB
JavaScript
const Strategy = require("passport-local").Strategy;
const uuid = require("d3plus-common").uuid;
const BuildMail = require("buildmail"),
Mailgun = require("mailgun-js"),
bcrypt = require("bcrypt-nodejs"),
fs = require("fs"),
path = require("path"),
yn = require("yn");
const {name} = JSON.parse(require("shelljs").cat("package.json"));
const activationRoute = process.env.CANON_ACTIVATION_LINK || "/activate",
canonActivation = process.env.CANON_SIGNUP_ACTIVATION,
confirmEmailFilepath = process.env.CANON_ACTIVATION_HTML
? path.join(process.cwd(), process.env.CANON_ACTIVATION_HTML)
: path.join(__dirname, "emails/activation.html"),
mgApiKey = process.env.CANON_MAILGUN_API,
mgDomain = process.env.CANON_MAILGUN_DOMAIN,
mgEmail = process.env.CANON_MAILGUN_EMAIL,
mgName = process.env.CANON_MAILGUN_NAME || name,
resetEmailFilepath = process.env.CANON_RESET_HTML
? path.join(process.cwd(), process.env.CANON_RESET_HTML)
: path.join(__dirname, "emails/resetPassword.html"),
resetRoute = process.env.CANON_RESET_LINK || "/reset",
signupDisabled = yn(process.env.CANON_LOGINS_SIGNUP_DISABLE) || false;
const isAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) return next();
return res.status(401).send("not logged in");
};
module.exports = function(app) {
const {db, passport} = app.settings;
passport.use("local-login", new Strategy({usernameField: "email"},
(rawEmail, password, done) => {
const email = `${rawEmail}`.trim().toLowerCase();
db.users.findOne({where: {email}, raw: true})
.then(user => {
if (!user) done(null, false, {message: "Incorrect credentials."});
else {
const hashedPassword = bcrypt.hashSync(password, user.salt);
if (user.password === hashedPassword) done(null, user);
else done(null, false, {message: "Incorrect credentials."});
}
return user;
})
.catch(err => console.log(err));
}));
app.post("/auth/local/login", passport.authenticate("local-login", {
failureFlash: false
}), (req, res) => res.json({user: req.user}));
passport.use("local-signup", new Strategy({usernameField: "email", passReqToCallback: true},
(req, rawEmail, password, done) => {
const email = `${rawEmail}`.trim().toLowerCase();
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (!emailRegex.test(email)) {
done(null, false, {message: "Entered e-mail is not valid."});
return;
}
db.users.findOne({where: {email}, raw: true})
.then(user => {
if (user) return done(null, false, {message: "E-mail already exists."});
const username = req.body.username;
return db.users.findOne({where: {username}, raw: true})
.then(user => {
if (user) return done(null, false, {message: "Username already exists."});
const salt = bcrypt.genSaltSync(10);
const hashedPassword = bcrypt.hashSync(password, salt);
const newUser = {
id: uuid(),
email,
username,
salt,
password: hashedPassword
};
return db.users.create(newUser).then(user => {
if (canonActivation && mgApiKey && mgDomain && mgEmail) {
sendActivation(user, req, error => done(null, false, error), () => done(null, user));
}
else done(null, user);
});
})
.catch(err => console.log(err));
})
.catch(err => console.log(err));
}));
if (!signupDisabled) {
app.post("/auth/local/signup", passport.authenticate("local-signup", {
failureFlash: false
}), (req, res) => res.json({user: req.user}));
}
/** */
function sendActivation(user, req, err, done) {
const mailgun = new Mailgun({apiKey: mgApiKey, domain: mgDomain});
user.activateToken = bcrypt.genSaltSync();
const expiryHours = 48;
user.activateTokenExpiry = new Date(Date.now() + expiryHours * 60 * 60 * 1000);
return user.save()
.then(() => {
const {activateToken, email} = user;
fs.readFile(confirmEmailFilepath, "utf8", (error, template) => {
if (error) return err(error);
const url = `${ req.protocol }://${ req.headers.host }`;
const translationVariables = {
confirmLink: `${url}${activationRoute}?email=${email}&token=${activateToken}`,
site: mgName,
username: user.username,
url
};
const render = template
.replace(/t\(['|"]([A-z\.]+)['|"]\)/g, (match, str) => req.t(str, translationVariables))
.replace(/\{\{([A-z\.]+)\}\}/g, (match, str) => translationVariables[str]);
return new BuildMail("text/html")
.addHeader({from: mgEmail, subject: req.t("Activate.mailgun.title"), to: email})
.setContent(render).build((error, mail) => {
if (error) return err(error);
return mailgun.messages().sendMime({to: email, message: mail.toString("ascii")}, done);
});
});
})
.catch(err => console.log(err));
}
if (mgApiKey && mgDomain && mgEmail) {
app.set("mailgun", true);
app.get("/auth/sendActivation", isAuthenticated, (req, res) => {
const {email} = req.query;
db.users.findOne({where: {email}})
.then(user => {
if (user) {
return sendActivation(user, req, error => res.json({error}), () => res.json({success: true}));
}
else {
return res.json({error: true});
}
})
.catch(err => console.log(err));
});
app.get("/auth/activate", (req, res) => {
const {email, token} = req.query;
db.users.findOne({where: {email, activateToken: token}})
.then(user => {
if (user) {
if (user.activateTokenExpiry < new Date(Date.now())) {
user.activateTokenExpiry = null;
user.activateToken = null;
return user.save()
.then(() => res.json({success: false}))
.catch(err => console.log(err));
}
else {
user.activateTokenExpiry = null;
user.activateToken = null;
user.activated = true;
return user.save()
.then(() => res.json({success: true}))
.catch(err => console.log(err));
}
}
else {
return res.json({success: false});
}
})
.catch(err => console.log(err));
});
app.get("/auth/resetPassword", (req, res) => {
const {email} = req.query;
db.users.findOne({where: {email}})
.then(user => {
if (user) {
const mailgun = new Mailgun({apiKey: mgApiKey, domain: mgDomain});
user.resetToken = bcrypt.genSaltSync();
const expiryHours = 2;
user.resetTokenExpiry = new Date(Date.now() + expiryHours * 60 * 60 * 1000);
return user.save()
.then(() => {
fs.readFile(resetEmailFilepath, "utf8", (error, template) => {
if (error) return res.json({error});
const url = `${ req.protocol }://${ req.headers.host }`;
const translationVariables = {
resetLink: `${url}${resetRoute}?token=${user.resetToken}`,
site: mgName,
username: user.username,
url
};
const render = template
.replace(/t\(['|"]([A-z\.]+)['|"]\)/g, (match, str) => req.t(str, translationVariables))
.replace(/\{\{([A-z\.]+)\}\}/g, (match, str) => translationVariables[str]);
return new BuildMail("text/html")
.addHeader({from: mgEmail, subject: req.t("Reset.mailgun.title"), to: email})
.setContent(render).build((error, mail) => {
if (error) return res.json({error});
return mailgun.messages().sendMime({to: email, message: mail.toString("ascii")}, () => res.json({success: true}));
});
});
})
.catch(err => console.log(err));
}
else {
return res.json({error: true});
}
})
.catch(err => console.log(err));
});
app.get("/auth/validateReset", (req, res) => {
const {token} = req.query;
db.users.findOne({where: {resetToken: token}})
.then(user => {
if (user) {
if (user.resetTokenExpiry < new Date(Date.now())) {
user.resetTokenExpiry = null;
user.resetToken = null;
return user.save()
.then(() => res.json({success: false}))
.catch(err => console.log(err));
}
else {
return res.json({success: true});
}
}
else {
return res.json({success: false});
}
})
.catch(err => console.log(err));
});
app.post("/auth/changePassword", (req, res) => {
const {password, token} = req.body;
const newSalt = bcrypt.genSaltSync(10);
const newHashedPassword = bcrypt.hashSync(password, newSalt);
db.users.findOne({where: {resetToken: token}})
.then(user => {
if (user) {
user.resetToken = null;
user.resetTokenExpiry = null;
user.salt = newSalt;
user.password = newHashedPassword;
return user.save()
.then(() => res.json({success: true}))
.catch(err => console.log(err));
}
else {
return res.json({success: false});
}
})
.catch(err => console.log(err));
});
}
};