kutt/server/handlers/auth.handler.js

374 lines
8.9 KiB
JavaScript
Raw Normal View History

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) {
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
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
}
2024-08-11 18:41:03 +03:30
/**
* @type {import("express").Handler}
*/
function signupAccess(req, res, next) {
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,
}