2020-01-30 18:51:52 +03:30
|
|
|
import { differenceInMinutes, addMinutes, subMinutes } from "date-fns";
|
2020-01-11 17:40:25 +03:30
|
|
|
import { Handler } from "express";
|
|
|
|
import passport from "passport";
|
2020-01-30 18:51:52 +03:30
|
|
|
import bcrypt from "bcryptjs";
|
|
|
|
import nanoid from "nanoid";
|
|
|
|
import uuid from "uuid/v4";
|
2020-01-11 17:40:25 +03:30
|
|
|
import axios from "axios";
|
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
import { CustomError } from "../utils";
|
|
|
|
import * as utils from "../utils";
|
2020-07-30 19:18:15 +04:30
|
|
|
import * as redis from "../redis";
|
|
|
|
import queries from "../queries";
|
2020-01-30 18:51:52 +03:30
|
|
|
import * as mail from "../mail";
|
|
|
|
import query from "../queries";
|
|
|
|
import env from "../env";
|
2020-01-11 17:40:25 +03:30
|
|
|
|
|
|
|
const authenticate = (
|
|
|
|
type: "jwt" | "local" | "localapikey",
|
|
|
|
error: string,
|
|
|
|
isStrict = true
|
|
|
|
) =>
|
|
|
|
async function auth(req, res, next) {
|
|
|
|
if (req.user) return next();
|
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
passport.authenticate(type, (err, user) => {
|
|
|
|
if (err) return next(err);
|
2020-01-11 17:40:25 +03:30
|
|
|
|
|
|
|
if (!user && isStrict) {
|
|
|
|
throw new CustomError(error, 401);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (user && isStrict && !user.verified) {
|
|
|
|
throw new CustomError(
|
|
|
|
"Your email address is not verified. " +
|
|
|
|
"Click on signup to get the verification link again.",
|
|
|
|
400
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (user && user.banned) {
|
2020-06-30 01:13:45 +05:30
|
|
|
throw new CustomError("You're banned from using this website.", 403);
|
2020-01-11 17:40:25 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
if (user) {
|
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
|
|
|
export const local = authenticate("local", "Login credentials are wrong.");
|
|
|
|
export const jwt = authenticate("jwt", "Unauthorized.");
|
|
|
|
export const jwtLoose = authenticate("jwt", "Unauthorized.", false);
|
|
|
|
export const apikey = authenticate(
|
|
|
|
"localapikey",
|
|
|
|
"API key is not correct.",
|
|
|
|
false
|
|
|
|
);
|
|
|
|
|
|
|
|
export const cooldown: Handler = async (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();
|
|
|
|
|
2020-07-30 19:18:15 +04:30
|
|
|
const ip = await queries.ip.find({
|
|
|
|
ip: req.realIP.toLowerCase(),
|
|
|
|
created_at: [">", subMinutes(new Date(), cooldownConfig).toISOString()]
|
|
|
|
});
|
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();
|
|
|
|
};
|
|
|
|
|
|
|
|
export const recaptcha: Handler = async (req, res, next) => {
|
2020-01-30 18:51:52 +03:30
|
|
|
if (env.isDev || req.user) return next();
|
2020-08-09 15:25:18 +04:30
|
|
|
if (env.DISALLOW_ANONYMOUS_LINKS) return next();
|
2020-07-29 19:16:57 +04:30
|
|
|
if (!env.RECAPTCHA_SECRET_KEY) return next();
|
2020-01-11 17:40:25 +03:30
|
|
|
|
|
|
|
const isReCaptchaValid = await axios({
|
|
|
|
method: "post",
|
|
|
|
url: "https://www.google.com/recaptcha/api/siteverify",
|
|
|
|
headers: {
|
|
|
|
"Content-type": "application/x-www-form-urlencoded"
|
|
|
|
},
|
|
|
|
params: {
|
2020-01-30 18:51:52 +03:30
|
|
|
secret: env.RECAPTCHA_SECRET_KEY,
|
2020-01-11 17:40:25 +03:30
|
|
|
response: req.body.reCaptchaToken,
|
|
|
|
remoteip: req.realIP
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!isReCaptchaValid.data.success) {
|
|
|
|
throw new CustomError("reCAPTCHA is not valid. Try again.", 401);
|
|
|
|
}
|
|
|
|
|
|
|
|
return next();
|
|
|
|
};
|
2020-01-30 18:51:52 +03:30
|
|
|
|
|
|
|
export const admin: Handler = async (req, res, next) => {
|
|
|
|
if (req.user.admin) return next();
|
|
|
|
throw new CustomError("Unauthorized", 401);
|
|
|
|
};
|
|
|
|
|
|
|
|
export const signup: Handler = async (req, res) => {
|
|
|
|
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 },
|
|
|
|
req.user
|
|
|
|
);
|
|
|
|
|
|
|
|
await mail.verification(user);
|
|
|
|
|
|
|
|
return res.status(201).send({ message: "Verification email has been sent." });
|
|
|
|
};
|
|
|
|
|
|
|
|
export const token: Handler = async (req, res) => {
|
|
|
|
const token = utils.signToken(req.user);
|
|
|
|
return res.status(200).send({ token });
|
|
|
|
};
|
|
|
|
|
|
|
|
export const verify: Handler = async (req, res, next) => {
|
|
|
|
if (!req.params.verificationToken) return next();
|
|
|
|
|
|
|
|
const [user] = await query.user.update(
|
|
|
|
{
|
|
|
|
verification_token: req.params.verificationToken,
|
|
|
|
verification_expires: [">", new Date().toISOString()]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
verified: true,
|
|
|
|
verification_token: null,
|
|
|
|
verification_expires: null
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
if (user) {
|
|
|
|
const token = utils.signToken(user);
|
|
|
|
req.token = token;
|
|
|
|
}
|
|
|
|
|
|
|
|
return next();
|
|
|
|
};
|
|
|
|
|
|
|
|
export const changePassword: Handler = async (req, res) => {
|
|
|
|
const salt = await bcrypt.genSalt(12);
|
|
|
|
const password = await bcrypt.hash(req.body.password, salt);
|
|
|
|
|
|
|
|
const [user] = await query.user.update({ id: req.user.id }, { password });
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
throw new CustomError("Couldn't change the password. Try again later.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return res
|
|
|
|
.status(200)
|
|
|
|
.send({ message: "Your password has been changed successfully." });
|
|
|
|
};
|
|
|
|
|
2020-08-09 15:25:18 +04:30
|
|
|
export const generateApiKey: Handler = async (req, res) => {
|
2020-01-30 18:51:52 +03:30
|
|
|
const apikey = nanoid(40);
|
|
|
|
|
2020-01-30 22:17:26 +03:30
|
|
|
redis.remove.user(req.user);
|
|
|
|
|
2020-01-30 18:51:52 +03:30
|
|
|
const [user] = await query.user.update({ id: req.user.id }, { apikey });
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
throw new CustomError("Couldn't generate API key. Please try again later.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.status(201).send({ apikey });
|
|
|
|
};
|
|
|
|
|
2020-08-09 15:25:18 +04:30
|
|
|
export const resetPasswordRequest: Handler = async (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()
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
if (user) {
|
|
|
|
await mail.resetPasswordToken(user);
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.status(200).json({
|
|
|
|
error: "If email address exists, a reset password email has been sent."
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-08-09 15:25:18 +04:30
|
|
|
export const resetPassword: Handler = async (req, res, next) => {
|
2020-01-30 18:51:52 +03:30
|
|
|
const { resetPasswordToken } = req.params;
|
|
|
|
|
|
|
|
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 }
|
|
|
|
);
|
|
|
|
|
|
|
|
if (user) {
|
|
|
|
const token = utils.signToken(user as UserJoined);
|
|
|
|
req.token = token;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return next();
|
|
|
|
};
|
2020-08-09 15:25:18 +04:30
|
|
|
|
|
|
|
export const signupAccess: Handler = (req, res, next) => {
|
|
|
|
if (!env.DISALLOW_REGISTRATION) return next();
|
|
|
|
return res.status(403).send({ message: "Registration is not allowed." });
|
|
|
|
};
|