2024-08-11 18:41:03 +03:30
|
|
|
const { differenceInMinutes, addMinutes, subMinutes } = require("date-fns");
|
|
|
|
const passport = require("passport");
|
|
|
|
const { v4: uuid } = require("uuid");
|
|
|
|
const bcrypt = require("bcryptjs");
|
|
|
|
const nanoid = require("nanoid");
|
|
|
|
const axios = require("axios");
|
|
|
|
|
|
|
|
const { CustomError } = require("../utils");
|
|
|
|
const query = require("../queries");
|
|
|
|
const utils = require("../utils");
|
|
|
|
const redis = require("../redis");
|
|
|
|
const mail = require("../mail");
|
|
|
|
const env = require("../env");
|
|
|
|
|
|
|
|
function authenticate(type, error, isStrict) {
|
|
|
|
return function auth(req, res, next) {
|
2020-01-11 17:40:25 +03:30
|
|
|
if (req.user) return next();
|
2024-08-21 21:22:59 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
passport.authenticate(type, (err, user) => {
|
|
|
|
if (err) return next(err);
|
2024-08-11 18:41:03 +03:30
|
|
|
const accepts = req.accepts(["json", "html"]);
|
2020-01-11 17:40:25 +03:30
|
|
|
|
|
|
|
if (!user && isStrict) {
|
2024-08-21 21:22:59 +03:30
|
|
|
req.viewTemplate = "partials/auth/form";
|
|
|
|
throw new CustomError(error, 401);
|
2020-01-11 17:40:25 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
if (user && isStrict && !user.verified) {
|
2024-08-21 21:22:59 +03:30
|
|
|
req.viewTemplate = "partials/auth/form";
|
|
|
|
throw new CustomError("Your email address is not verified. " +
|
|
|
|
"Sign up to get the verification link again.", 400);
|
2020-01-11 17:40:25 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
if (user && user.banned) {
|
2024-08-21 21:22:59 +03:30
|
|
|
req.viewTemplate = "partials/auth/form";
|
|
|
|
throw new CustomError("You're banned from using this website.", 403);
|
2020-01-11 17:40:25 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
if (user) {
|
2024-08-31 12:19:39 +03:30
|
|
|
res.locals.isAdmin = utils.isAdmin(user.email);
|
2020-01-11 17:40:25 +03:30
|
|
|
req.user = {
|
|
|
|
...user,
|
2020-01-30 18:51:52 +03:30
|
|
|
admin: utils.isAdmin(user.email)
|
2020-01-11 17:40:25 +03:30
|
|
|
};
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
return next();
|
|
|
|
})(req, res, next);
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
|
|
|
}
|
2020-01-11 17:40:25 +03:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
const local = authenticate("local", "Login credentials are wrong.", true);
|
|
|
|
const jwt = authenticate("jwt", "Unauthorized.", true);
|
|
|
|
const jwtLoose = authenticate("jwt", "Unauthorized.", false);
|
|
|
|
const apikey = authenticate("localapikey", "API key is not correct.", false);
|
2020-01-11 17:40:25 +03:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
/**
|
|
|
|
* @type {import("express").Handler}
|
|
|
|
*/
|
|
|
|
async function cooldown(req, res, next) {
|
2020-08-09 15:25:18 +04:30
|
|
|
if (env.DISALLOW_ANONYMOUS_LINKS) return next();
|
2020-01-30 18:51:52 +03:30
|
|
|
const cooldownConfig = env.NON_USER_COOLDOWN;
|
2020-01-11 17:40:25 +03:30
|
|
|
if (req.user || !cooldownConfig) return next();
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
const ip = await query.ip.find({
|
2020-07-30 19:18:15 +04:30
|
|
|
ip: req.realIP.toLowerCase(),
|
|
|
|
created_at: [">", subMinutes(new Date(), cooldownConfig).toISOString()]
|
|
|
|
});
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-11 17:40:25 +03:30
|
|
|
if (ip) {
|
|
|
|
const timeToWait =
|
|
|
|
cooldownConfig - differenceInMinutes(new Date(), new Date(ip.created_at));
|
|
|
|
throw new CustomError(
|
|
|
|
`Non-logged in users are limited. Wait ${timeToWait} minutes or log in.`,
|
|
|
|
400
|
|
|
|
);
|
|
|
|
}
|
|
|
|
next();
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
2020-01-11 17:40:25 +03:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
/**
|
|
|
|
* @type {import("express").Handler}
|
|
|
|
*/
|
|
|
|
function admin(req, res, next) {
|
|
|
|
// FIXME: attaching to req is risky, find another way
|
2020-01-30 18:51:52 +03:30
|
|
|
if (req.user.admin) return next();
|
|
|
|
throw new CustomError("Unauthorized", 401);
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
2020-01-30 18:51:52 +03:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
/**
|
|
|
|
* @type {import("express").Handler}
|
|
|
|
*/
|
|
|
|
async function signup(req, res) {
|
2020-01-30 18:51:52 +03:30
|
|
|
const salt = await bcrypt.genSalt(12);
|
|
|
|
const password = await bcrypt.hash(req.body.password, salt);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
const user = await query.user.add(
|
|
|
|
{ email: req.body.email, password },
|
|
|
|
req.user
|
|
|
|
);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
await mail.verification(user);
|
|
|
|
|
2024-08-21 21:22:59 +03:30
|
|
|
if (req.isHTML) {
|
|
|
|
res.render("partials/auth/verify");
|
|
|
|
return;
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
return res.status(201).send({ message: "A verification email has been sent." });
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {import("express").Handler}
|
|
|
|
*/
|
|
|
|
function login(req, res) {
|
2020-01-30 18:51:52 +03:30
|
|
|
const token = utils.signToken(req.user);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2024-08-21 21:22:59 +03:30
|
|
|
if (req.isHTML) {
|
2024-08-11 18:41:03 +03:30
|
|
|
res.cookie("token", token, {
|
2024-08-21 21:22:59 +03:30
|
|
|
maxAge: 1000 * 60 * 60 * 24 * 7, // expire after seven days
|
2024-08-11 18:41:03 +03:30
|
|
|
httpOnly: true,
|
|
|
|
secure: env.isProd
|
|
|
|
});
|
2024-08-21 21:22:59 +03:30
|
|
|
res.render("partials/auth/welcome");
|
|
|
|
return;
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
return res.status(200).send({ token });
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
2020-01-30 18:51:52 +03:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
/**
|
|
|
|
* @type {import("express").Handler}
|
|
|
|
*/
|
|
|
|
async function verify(req, res, next) {
|
2020-01-30 18:51:52 +03:30
|
|
|
if (!req.params.verificationToken) return next();
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
const [user] = await query.user.update(
|
|
|
|
{
|
|
|
|
verification_token: req.params.verificationToken,
|
|
|
|
verification_expires: [">", new Date().toISOString()]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
verified: true,
|
|
|
|
verification_token: null,
|
|
|
|
verification_expires: null
|
|
|
|
}
|
|
|
|
);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
if (user) {
|
|
|
|
const token = utils.signToken(user);
|
|
|
|
req.token = token;
|
|
|
|
}
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
return next();
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
2020-01-30 18:51:52 +03:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
/**
|
|
|
|
* @type {import("express").Handler}
|
|
|
|
*/
|
|
|
|
async function changePassword(req, res) {
|
2024-08-31 12:19:39 +03:30
|
|
|
const isMatch = await bcrypt.compare(req.body.currentpassword, req.user.password);
|
|
|
|
if (!isMatch) {
|
|
|
|
const message = "Current password is not correct.";
|
|
|
|
res.locals.errors = { currentpassword: message };
|
|
|
|
throw new CustomError(message, 401);
|
|
|
|
}
|
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
const salt = await bcrypt.genSalt(12);
|
2024-08-31 12:19:39 +03:30
|
|
|
const newpassword = await bcrypt.hash(req.body.newpassword, salt);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2024-08-31 12:19:39 +03:30
|
|
|
const [user] = await query.user.update({ id: req.user.id }, { password: newpassword });
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
if (!user) {
|
|
|
|
throw new CustomError("Couldn't change the password. Try again later.");
|
|
|
|
}
|
2024-08-31 12:19:39 +03:30
|
|
|
|
|
|
|
await utils.sleep(1000);
|
|
|
|
|
|
|
|
if (req.isHTML) {
|
|
|
|
res.setHeader("HX-Trigger-After-Swap", "resetChangePasswordForm");
|
|
|
|
res.render("partials/settings/change_password", {
|
|
|
|
success: "Password has been changed."
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
return res
|
|
|
|
.status(200)
|
|
|
|
.send({ message: "Your password has been changed successfully." });
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
2020-01-30 18:51:52 +03:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
/**
|
|
|
|
* @type {import("express").Handler}
|
|
|
|
*/
|
|
|
|
async function generateApiKey(req, res) {
|
2020-01-30 18:51:52 +03:30
|
|
|
const apikey = nanoid(40);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 22:17:26 +03:30
|
|
|
redis.remove.user(req.user);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
const [user] = await query.user.update({ id: req.user.id }, { apikey });
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
if (!user) {
|
|
|
|
throw new CustomError("Couldn't generate API key. Please try again later.");
|
|
|
|
}
|
2024-08-31 12:19:39 +03:30
|
|
|
|
|
|
|
await utils.sleep(1000);
|
|
|
|
|
|
|
|
if (req.isHTML) {
|
|
|
|
res.render("partials/settings/apikey", {
|
|
|
|
user: { apikey },
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
return res.status(201).send({ apikey });
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
2020-01-30 18:51:52 +03:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
/**
|
|
|
|
* @type {import("express").Handler}
|
|
|
|
*/
|
|
|
|
async function resetPasswordRequest(req, res) {
|
2020-01-30 18:51:52 +03:30
|
|
|
const [user] = await query.user.update(
|
|
|
|
{ email: req.body.email },
|
|
|
|
{
|
|
|
|
reset_password_token: uuid(),
|
|
|
|
reset_password_expires: addMinutes(new Date(), 30).toISOString()
|
|
|
|
}
|
|
|
|
);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
if (user) {
|
|
|
|
await mail.resetPasswordToken(user);
|
|
|
|
}
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
return res.status(200).send({
|
|
|
|
message: "If email address exists, a reset password email has been sent."
|
2020-01-30 18:51:52 +03:30
|
|
|
});
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
2020-01-30 18:51:52 +03:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
/**
|
|
|
|
* @type {import("express").Handler}
|
|
|
|
*/
|
|
|
|
async function resetPassword(req, res, next) {
|
2020-01-30 18:51:52 +03:30
|
|
|
const { resetPasswordToken } = req.params;
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
if (resetPasswordToken) {
|
|
|
|
const [user] = await query.user.update(
|
|
|
|
{
|
|
|
|
reset_password_token: resetPasswordToken,
|
|
|
|
reset_password_expires: [">", new Date().toISOString()]
|
|
|
|
},
|
|
|
|
{ reset_password_expires: null, reset_password_token: null }
|
|
|
|
);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
if (user) {
|
2024-08-11 18:41:03 +03:30
|
|
|
const token = utils.signToken(user);
|
2020-01-30 18:51:52 +03:30
|
|
|
req.token = token;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return next();
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
2020-08-09 15:25:18 +04:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
/**
|
|
|
|
* @type {import("express").Handler}
|
|
|
|
*/
|
|
|
|
function signupAccess(req, res, next) {
|
2020-08-09 15:25:18 +04:30
|
|
|
if (!env.DISALLOW_REGISTRATION) return next();
|
|
|
|
return res.status(403).send({ message: "Registration is not allowed." });
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
2020-09-19 18:02:32 +04:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
/**
|
|
|
|
* @type {import("express").Handler}
|
|
|
|
*/
|
|
|
|
async function changeEmailRequest(req, res) {
|
2020-09-19 18:02:32 +04:30
|
|
|
const { email, password } = req.body;
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
const isMatch = await bcrypt.compare(password, req.user.password);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
if (!isMatch) {
|
2024-08-31 12:19:39 +03:30
|
|
|
const error = "Password is not correct.";
|
|
|
|
res.locals.errors = { password: error };
|
|
|
|
throw new CustomError(error, 401);
|
2020-09-19 18:02:32 +04:30
|
|
|
}
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
const currentUser = await query.user.find({ email });
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
if (currentUser) {
|
2024-08-31 12:19:39 +03:30
|
|
|
const error = "Can't use this email address.";
|
|
|
|
res.locals.errors = { email: error };
|
|
|
|
throw new CustomError(error, 400);
|
2020-09-19 18:02:32 +04:30
|
|
|
}
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
const [updatedUser] = await query.user.update(
|
|
|
|
{ id: req.user.id },
|
|
|
|
{
|
|
|
|
change_email_address: email,
|
|
|
|
change_email_token: uuid(),
|
|
|
|
change_email_expires: addMinutes(new Date(), 30).toISOString()
|
|
|
|
}
|
|
|
|
);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
redis.remove.user(updatedUser);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
if (updatedUser) {
|
|
|
|
await mail.changeEmail({ ...updatedUser, email });
|
|
|
|
}
|
2024-08-31 12:19:39 +03:30
|
|
|
|
|
|
|
const message = "A verification link has been sent to the requested email address."
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2024-08-31 12:19:39 +03:30
|
|
|
if (req.isHTML) {
|
|
|
|
res.setHeader("HX-Trigger-After-Swap", "resetChangeEmailForm");
|
|
|
|
res.render("partials/settings/change_email", {
|
|
|
|
success: message
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.status(200).send({ message });
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
2020-09-19 18:02:32 +04:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
/**
|
|
|
|
* @type {import("express").Handler}
|
|
|
|
*/
|
|
|
|
async function changeEmail(req, res, next) {
|
2020-09-19 18:02:32 +04:30
|
|
|
const { changeEmailToken } = req.params;
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
if (changeEmailToken) {
|
|
|
|
const foundUser = await query.user.find({
|
|
|
|
change_email_token: changeEmailToken
|
|
|
|
});
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
if (!foundUser) return next();
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
const [user] = await query.user.update(
|
|
|
|
{
|
|
|
|
change_email_token: changeEmailToken,
|
|
|
|
change_email_expires: [">", new Date().toISOString()]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
change_email_token: null,
|
|
|
|
change_email_expires: null,
|
|
|
|
change_email_address: null,
|
|
|
|
email: foundUser.change_email_address
|
|
|
|
}
|
|
|
|
);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
redis.remove.user(foundUser);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
if (user) {
|
2024-08-11 18:41:03 +03:30
|
|
|
const token = utils.signToken(user);
|
2020-09-19 18:02:32 +04:30
|
|
|
req.token = token;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return next();
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
admin,
|
|
|
|
apikey,
|
|
|
|
changeEmail,
|
|
|
|
changeEmailRequest,
|
|
|
|
changePassword,
|
|
|
|
cooldown,
|
|
|
|
generateApiKey,
|
|
|
|
jwt,
|
|
|
|
jwtLoose,
|
|
|
|
local,
|
|
|
|
login,
|
|
|
|
resetPassword,
|
|
|
|
resetPasswordRequest,
|
|
|
|
signup,
|
|
|
|
signupAccess,
|
|
|
|
verify,
|
|
|
|
}
|