kutt/server/handlers/auth.handler.js

394 lines
9.9 KiB
JavaScript
Raw Normal View History

2024-09-12 17:38:00 +03:30
const { differenceInDays, differenceInMinutes, addMinutes, subMinutes } = require("date-fns");
2024-08-11 18:41:03 +03:30
const passport = require("passport");
const { v4: uuid } = require("uuid");
const bcrypt = require("bcryptjs");
const nanoid = require("nanoid");
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") {
res.redirect("/logout");
2024-09-12 14:26:39 +03:30
return;
}
if (redirect === "header") {
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." });
}
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-08-11 18:41:03 +03:30
async function resetPasswordRequest(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 },
{
reset_password_token: uuid(),
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) {
res.render("partials/reset_password/form", {
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-08-11 18:41:03 +03:30
async function resetPassword(req, res, next) {
2024-09-08 14:10:02 +03:30
const resetPasswordToken = req.params.resetPasswordToken;
2020-01-30 18:51:52 +03:30
if (resetPasswordToken) {
2024-11-24 14:56:58 +03:30
const user = await query.user.update(
2020-01-30 18:51:52 +03:30
{
reset_password_token: resetPasswordToken,
2024-10-07 09:08:40 +03:30
reset_password_expires: [">", utils.dateToUTC(new Date())]
2020-01-30 18:51:52 +03:30
},
{ reset_password_expires: null, reset_password_token: null }
);
2024-11-24 14:56:58 +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);
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-09-08 14:10:02 +03:30
next();
2024-08-11 18:41:03 +03: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,
change_email_token: uuid(),
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,
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,
resetPassword,
resetPasswordRequest,
signup,
verify,
}