2025-01-21 09:38:14 +01:00
|
|
|
const { differenceInDays, addMinutes } = require("date-fns");
|
2025-01-01 14:27:04 +03:30
|
|
|
const { nanoid } = require("nanoid");
|
2024-08-11 18:41:03 +03:30
|
|
|
const passport = require("passport");
|
2025-01-21 09:38:14 +01:00
|
|
|
const { randomUUID } = require("node:crypto");
|
2024-08-11 18:41:03 +03:30
|
|
|
const bcrypt = require("bcryptjs");
|
|
|
|
|
2024-11-20 19:02:02 +03:30
|
|
|
const { ROLES } = require("../consts");
|
2024-08-11 18:41:03 +03:30
|
|
|
const query = require("../queries");
|
|
|
|
const utils = require("../utils");
|
|
|
|
const redis = require("../redis");
|
|
|
|
const mail = require("../mail");
|
|
|
|
const env = require("../env");
|
|
|
|
|
2024-09-12 14:26:39 +03:30
|
|
|
const CustomError = utils.CustomError;
|
|
|
|
|
|
|
|
function authenticate(type, error, isStrict, redirect) {
|
2024-08-11 18:41:03 +03:30
|
|
|
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
|
|
|
|
2024-09-12 17:38:00 +03:30
|
|
|
passport.authenticate(type, (err, user, info) => {
|
2020-01-30 18:51:52 +03:30
|
|
|
if (err) return next(err);
|
2020-01-11 17:40:25 +03:30
|
|
|
|
2024-09-12 14:26:39 +03:30
|
|
|
if (
|
2024-09-12 16:44:59 +03:30
|
|
|
req.isHTML &&
|
2024-09-12 14:26:39 +03:30
|
|
|
redirect &&
|
|
|
|
((!user && isStrict) ||
|
|
|
|
(user && isStrict && !user.verified) ||
|
|
|
|
(user && user.banned))
|
|
|
|
) {
|
|
|
|
if (redirect === "page") {
|
2024-11-20 19:02:02 +03:30
|
|
|
res.redirect("/logout");
|
2024-09-12 14:26:39 +03:30
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (redirect === "header") {
|
2024-11-20 19:02:02 +03:30
|
|
|
res.setHeader("HX-Redirect", "/logout");
|
2024-09-12 14:26:39 +03:30
|
|
|
res.send("NOT_AUTHENTICATED");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-11 17:40:25 +03:30
|
|
|
if (!user && isStrict) {
|
2024-08-21 21:22:59 +03:30
|
|
|
throw new CustomError(error, 401);
|
2020-01-11 17:40:25 +03:30
|
|
|
}
|
|
|
|
|
2024-11-19 07:58:57 +03:30
|
|
|
if (user && user.banned) {
|
|
|
|
throw new CustomError("You're banned from using this website.", 403);
|
|
|
|
}
|
|
|
|
|
2020-01-11 17:40:25 +03:30
|
|
|
if (user && isStrict && !user.verified) {
|
2024-08-21 21:22:59 +03:30
|
|
|
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) {
|
2024-11-19 07:58:57 +03:30
|
|
|
res.locals.isAdmin = utils.isAdmin(user);
|
2020-01-11 17:40:25 +03:30
|
|
|
req.user = {
|
|
|
|
...user,
|
2024-11-19 07:58:57 +03:30
|
|
|
admin: utils.isAdmin(user)
|
2020-01-11 17:40:25 +03:30
|
|
|
};
|
2024-09-12 17:38:00 +03:30
|
|
|
|
|
|
|
// renew token if it's been at least one day since the token has been created
|
|
|
|
// only do it for html page requests not api requests
|
|
|
|
if (info?.exp && req.isHTML && redirect === "page") {
|
|
|
|
const diff = Math.abs(differenceInDays(new Date(info.exp * 1000), new Date()));
|
|
|
|
if (diff < 6) {
|
|
|
|
const token = utils.signToken(user);
|
|
|
|
utils.deleteCurrentToken(res);
|
|
|
|
utils.setToken(res, token);
|
|
|
|
}
|
|
|
|
}
|
2020-01-11 17:40:25 +03:30
|
|
|
}
|
|
|
|
return next();
|
|
|
|
})(req, res, next);
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
|
|
|
}
|
2020-01-11 17:40:25 +03:30
|
|
|
|
2024-09-12 14:26:39 +03:30
|
|
|
const local = authenticate("local", "Login credentials are wrong.", true, null);
|
|
|
|
const jwt = authenticate("jwt", "Unauthorized.", true, "header");
|
|
|
|
const jwtPage = authenticate("jwt", "Unauthorized.", true, "page");
|
2024-09-12 16:44:59 +03:30
|
|
|
const jwtLoose = authenticate("jwt", "Unauthorized.", false, "header");
|
|
|
|
const jwtLoosePage = authenticate("jwt", "Unauthorized.", false, "page");
|
2024-09-12 14:26:39 +03:30
|
|
|
const apikey = authenticate("localapikey", "API key is not correct.", false, null);
|
2020-01-11 17:40:25 +03:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
function admin(req, res, next) {
|
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
|
|
|
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." });
|
|
|
|
}
|
|
|
|
|
2024-11-20 19:02:02 +03:30
|
|
|
async function createAdminUser(req, res) {
|
|
|
|
const isThereAUser = await query.user.findAny();
|
|
|
|
if (isThereAUser) {
|
|
|
|
throw new CustomError("Can not create the admin user because a user already exists.", 400);
|
|
|
|
}
|
|
|
|
|
|
|
|
const salt = await bcrypt.genSalt(12);
|
|
|
|
const password = await bcrypt.hash(req.body.password, salt);
|
|
|
|
|
|
|
|
const user = await query.user.add({
|
|
|
|
email: req.body.email,
|
|
|
|
password,
|
|
|
|
role: ROLES.ADMIN,
|
|
|
|
verified: true
|
|
|
|
});
|
|
|
|
|
|
|
|
const token = utils.signToken(user);
|
|
|
|
|
|
|
|
if (req.isHTML) {
|
|
|
|
utils.setToken(res, token);
|
|
|
|
res.render("partials/auth/welcome");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.status(201).send({ token });
|
|
|
|
}
|
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
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-09-12 14:26:39 +03:30
|
|
|
utils.setToken(res, token);
|
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
|
|
|
async function verify(req, res, next) {
|
2020-01-30 18:51:52 +03:30
|
|
|
if (!req.params.verificationToken) return next();
|
2024-09-25 15:44:55 +03:30
|
|
|
|
2024-11-24 14:56:58 +03:30
|
|
|
const user = await query.user.update(
|
|
|
|
{
|
|
|
|
verification_token: req.params.verificationToken,
|
|
|
|
verification_expires: [">", utils.dateToUTC(new Date())]
|
|
|
|
},
|
2020-01-30 18:51:52 +03:30
|
|
|
{
|
|
|
|
verified: true,
|
|
|
|
verification_token: null,
|
|
|
|
verification_expires: null
|
|
|
|
}
|
|
|
|
);
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2024-11-24 14:56:58 +03:30
|
|
|
if (user) {
|
2020-01-30 18:51:52 +03:30
|
|
|
const token = utils.signToken(user);
|
2024-09-12 14:26:39 +03:30
|
|
|
utils.deleteCurrentToken(res);
|
|
|
|
utils.setToken(res, token);
|
2024-09-08 14:10:02 +03:30
|
|
|
res.locals.token_verified = true;
|
|
|
|
req.cookies.token = token;
|
2020-01-30 18:51:52 +03:30
|
|
|
}
|
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
|
|
|
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-11-24 14:56:58 +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
|
|
|
|
|
|
|
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
|
|
|
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
|
|
|
|
2024-10-21 14:59:55 +03:30
|
|
|
if (env.REDIS_ENABLED) {
|
|
|
|
redis.remove.user(req.user);
|
|
|
|
}
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2024-11-24 14:56:58 +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
|
|
|
|
|
|
|
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-12-31 16:24:37 +03:30
|
|
|
async function resetPassword(req, res) {
|
2024-11-24 14:56:58 +03:30
|
|
|
const user = await query.user.update(
|
2020-01-30 18:51:52 +03:30
|
|
|
{ email: req.body.email },
|
|
|
|
{
|
2025-01-21 09:38:14 +01:00
|
|
|
reset_password_token: randomUUID(),
|
2024-10-07 09:08:40 +03:30
|
|
|
reset_password_expires: utils.dateToUTC(addMinutes(new Date(), 30))
|
2020-01-30 18:51:52 +03:30
|
|
|
}
|
|
|
|
);
|
2024-09-23 13:45:39 +03:30
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
if (user) {
|
2024-09-23 13:45:39 +03:30
|
|
|
mail.resetPasswordToken(user).catch(error => {
|
|
|
|
console.error("Send reset-password token email error:\n", error);
|
|
|
|
});
|
2024-09-08 14:10:02 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
if (req.isHTML) {
|
2024-12-31 16:24:37 +03:30
|
|
|
res.render("partials/reset_password/request_form", {
|
2024-09-08 14:10:02 +03:30
|
|
|
message: "If the email address exists, a reset password email will be sent to it."
|
|
|
|
});
|
|
|
|
return;
|
2020-01-30 18:51:52 +03:30
|
|
|
}
|
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-12-31 16:24:37 +03:30
|
|
|
async function newPassword(req, res) {
|
|
|
|
const { new_password, reset_password_token } = req.body;
|
2024-09-08 14:10:02 +03:30
|
|
|
|
2024-12-31 16:24:37 +03:30
|
|
|
const salt = await bcrypt.genSalt(12);
|
|
|
|
const password = await bcrypt.hash(req.body.new_password, salt);
|
|
|
|
|
|
|
|
const user = await query.user.update(
|
|
|
|
{
|
|
|
|
reset_password_token,
|
|
|
|
reset_password_expires: [">", utils.dateToUTC(new Date())]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
reset_password_expires: null,
|
|
|
|
reset_password_token: null,
|
|
|
|
password,
|
2020-01-30 18:51:52 +03:30
|
|
|
}
|
2024-12-31 16:24:37 +03:30
|
|
|
);
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
throw new CustomError("Could not set the password. Please try again later.");
|
2020-01-30 18:51:52 +03:30
|
|
|
}
|
2024-09-08 14:10:02 +03:30
|
|
|
|
2024-12-31 16:24:37 +03:30
|
|
|
res.render("partials/reset_password/new_password_success");
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
2020-08-09 15:25:18 +04:30
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
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
|
|
|
|
2024-11-24 14:56:58 +03:30
|
|
|
const user = await query.user.find({ email });
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2024-11-24 14:56:58 +03:30
|
|
|
if (user) {
|
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
|
|
|
|
2024-11-24 14:56:58 +03:30
|
|
|
const updatedUser = await query.user.update(
|
2020-09-19 18:02:32 +04:30
|
|
|
{ id: req.user.id },
|
|
|
|
{
|
|
|
|
change_email_address: email,
|
2025-01-21 09:38:14 +01:00
|
|
|
change_email_token: randomUUID(),
|
2024-10-07 09:08:40 +03:30
|
|
|
change_email_expires: utils.dateToUTC(addMinutes(new Date(), 30))
|
2020-09-19 18:02:32 +04:30
|
|
|
}
|
|
|
|
);
|
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
|
|
|
async function changeEmail(req, res, next) {
|
2024-09-08 14:10:02 +03:30
|
|
|
const changeEmailToken = req.params.changeEmailToken;
|
2024-08-11 18:41:03 +03:30
|
|
|
|
2020-09-19 18:02:32 +04:30
|
|
|
if (changeEmailToken) {
|
|
|
|
const foundUser = await query.user.find({
|
2024-09-25 15:44:55 +03:30
|
|
|
change_email_token: changeEmailToken,
|
2024-10-07 09:08:40 +03:30
|
|
|
change_email_expires: [">", utils.dateToUTC(new Date())]
|
2020-09-19 18:02:32 +04:30
|
|
|
});
|
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
|
|
|
|
2024-11-24 14:56:58 +03:30
|
|
|
const user = await query.user.update(
|
2024-09-25 15:44:55 +03:30
|
|
|
{ id: foundUser.id },
|
2020-09-19 18:02:32 +04:30
|
|
|
{
|
|
|
|
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
|
|
|
if (user) {
|
2024-08-11 18:41:03 +03:30
|
|
|
const token = utils.signToken(user);
|
2024-09-12 14:26:39 +03:30
|
|
|
utils.deleteCurrentToken(res);
|
|
|
|
utils.setToken(res, token);
|
2024-09-08 14:10:02 +03:30
|
|
|
res.locals.token_verified = true;
|
|
|
|
req.cookies.token = token;
|
2020-09-19 18:02:32 +04:30
|
|
|
}
|
|
|
|
}
|
|
|
|
return next();
|
2024-08-11 18:41:03 +03:30
|
|
|
}
|
|
|
|
|
2024-09-23 13:45:39 +03:30
|
|
|
function featureAccess(features, redirect) {
|
|
|
|
return function(req, res, next) {
|
|
|
|
for (let i = 0; i < features.length; ++i) {
|
|
|
|
if (!features[i]) {
|
|
|
|
if (redirect) {
|
|
|
|
return res.redirect("/");
|
|
|
|
} else {
|
|
|
|
throw new CustomError("Request is not allowed.", 400);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function featureAccessPage(features) {
|
|
|
|
return featureAccess(features, true);
|
|
|
|
}
|
|
|
|
|
2024-08-11 18:41:03 +03:30
|
|
|
module.exports = {
|
|
|
|
admin,
|
|
|
|
apikey,
|
|
|
|
changeEmail,
|
|
|
|
changeEmailRequest,
|
|
|
|
changePassword,
|
2024-11-20 19:02:02 +03:30
|
|
|
createAdminUser,
|
2024-09-23 13:45:39 +03:30
|
|
|
featureAccess,
|
|
|
|
featureAccessPage,
|
2024-08-11 18:41:03 +03:30
|
|
|
generateApiKey,
|
|
|
|
jwt,
|
|
|
|
jwtLoose,
|
2024-09-12 16:44:59 +03:30
|
|
|
jwtLoosePage,
|
2024-09-12 14:26:39 +03:30
|
|
|
jwtPage,
|
2024-08-11 18:41:03 +03:30
|
|
|
local,
|
|
|
|
login,
|
2024-12-31 16:24:37 +03:30
|
|
|
newPassword,
|
2024-08-11 18:41:03 +03:30
|
|
|
resetPassword,
|
|
|
|
signup,
|
|
|
|
verify,
|
|
|
|
}
|