import { RequestHandler } from 'express'; import fs from 'fs'; import path from 'path'; import passport from 'passport'; import JWT from 'jsonwebtoken'; import axios from 'axios'; import { isAdmin } from '../utils'; import transporter from '../mail/mail'; import { resetMailText, verifyMailText } from '../mail/text'; import { createUser, changePassword, generateApiKey, getUser, verifyUser, requestPasswordReset, resetPassword, } from '../db/user'; import { IUser } from '../models/user'; /* Read email template */ const resetEmailTemplatePath = path.join( __dirname, '../mail/template-reset.html' ); const verifyEmailTemplatePath = path.join( __dirname, '../mail/template-verify.html' ); const resetEmailTemplate = fs .readFileSync(resetEmailTemplatePath, { encoding: 'utf-8' }) .replace(/{{domain}}/gm, process.env.DEFAULT_DOMAIN); const verifyEmailTemplate = fs .readFileSync(verifyEmailTemplatePath, { encoding: 'utf-8' }) .replace(/{{domain}}/gm, process.env.DEFAULT_DOMAIN); /* Function to generate JWT */ const signToken = (user: IUser) => JWT.sign( { iss: 'ApiAuth', sub: () => user.email, domain: (user.domain && user.domain.name) || '', admin: isAdmin(user.email), iat: new Date().getTime(), exp: new Date().setDate(new Date().getDate() + 7), }, process.env.JWT_SECRET ); /* Passport.js authentication controller */ const authenticate = ( type: 'jwt' | 'local' | 'localapikey', error: string, isStrict: boolean = true ) => function auth(req, res, next) { if (req.user) return next(); return passport.authenticate(type, (err, user) => { if (err) return res.status(400); if (!user && isStrict) return res.status(401).json({ error }); if (user && isStrict && !user.verified) { return res.status(400).json({ error: 'Your email address is not verified.' + 'Click on signup to get the verification link again.', }); } if (user && user.banned) { return res .status(400) .json({ error: 'Your are banned from using this website.' }); } if (user) { req.user = { ...user, admin: isAdmin(user.email), }; return next(); } return next(); })(req, res, next); }; export const authLocal = authenticate( 'local', 'Login email and/or password are wrong.' ); export const authJwt = authenticate('jwt', 'Unauthorized.'); export const authJwtLoose = authenticate('jwt', 'Unauthorized.', false); export const authApikey = authenticate( 'localapikey', 'API key is not correct.', false ); /* reCaptcha controller */ export const recaptcha: RequestHandler = async (req, res, next) => { if (process.env.NODE_ENV === 'production' && !req.user) { const isReCaptchaValid = await axios({ method: 'post', url: 'https://www.google.com/recaptcha/api/siteverify', headers: { 'Content-type': 'application/x-www-form-urlencoded', }, params: { secret: process.env.RECAPTCHA_SECRET_KEY, response: req.body.reCaptchaToken, remoteip: req.realIP, }, }); if (!isReCaptchaValid.data.success) { return res .status(401) .json({ error: 'reCAPTCHA is not valid. Try again.' }); } } return next(); }; export const authAdmin: RequestHandler = async (req, res, next) => { if (!req.user.admin) { return res.status(401).json({ error: 'Unauthorized.' }); } return next(); }; export const signup: RequestHandler = async (req, res) => { const { email, password } = req.body; if (password.length > 64) { return res.status(400).json({ error: 'Maximum password length is 64.' }); } if (email.length > 64) { return res.status(400).json({ error: 'Maximum email length is 64.' }); } const user = await getUser(email); if (user && user.verified) return res.status(403).json({ error: 'Email is already in use.' }); const newUser = await createUser(email, password); const mail = await transporter.sendMail({ from: process.env.MAIL_FROM || process.env.MAIL_USER, to: newUser.email, subject: 'Verify your account', text: verifyMailText.replace( /{{verification}}/gim, newUser.verificationToken ), html: verifyEmailTemplate.replace( /{{verification}}/gim, newUser.verificationToken ), }); if (mail.accepted.length) { return res .status(201) .json({ email, message: 'Verification email has been sent.' }); } return res .status(400) .json({ error: "Couldn't send verification email. Try again." }); }; export const login: RequestHandler = (req, res) => { const token = signToken(req.user); return res.status(200).json({ token }); }; export const renew: RequestHandler = (req, res) => { const token = signToken(req.user); return res.status(200).json({ token }); }; export const verify: RequestHandler = async (req, _res, next) => { const user = await verifyUser(req.params.verificationToken); if (user) { const token = signToken(user); req.user = { token }; } return next(); }; export const changeUserPassword: RequestHandler = async (req, res) => { if (req.body.password.length < 8) { return res .status(400) .json({ error: 'Password must be at least 8 chars long.' }); } if (req.body.password.length > 64) { return res.status(400).json({ error: 'Maximum password length is 64.' }); } const changedUser = await changePassword(req.user._id, req.body.password); if (changedUser) { return res .status(200) .json({ message: 'Your password has been changed successfully.' }); } return res .status(400) .json({ error: "Couldn't change the password. Try again later" }); }; export const generateUserApiKey: RequestHandler = async (req, res) => { const user = await generateApiKey(req.user._id); if (user.apikey) { return res.status(201).json({ apikey: user.apikey }); } return res .status(400) .json({ error: 'Sorry, an error occured. Please try again later.' }); }; export const userSettings: RequestHandler = (req, res) => res.status(200).json({ apikey: req.user.apikey || '', customDomain: req.user.domain || '', homepage: req.user.homepage || '', }); export const requestUserPasswordReset: RequestHandler = async (req, res) => { const user = await requestPasswordReset(req.body.email); if (!user) { return res.status(400).json({ error: "Couldn't reset password." }); } const mail = await transporter.sendMail({ from: process.env.MAIL_USER, to: user.email, subject: 'Reset your password', text: resetMailText .replace(/{{resetpassword}}/gm, user.resetPasswordToken) .replace(/{{domain}}/gm, process.env.DEFAULT_DOMAIN), html: resetEmailTemplate .replace(/{{resetpassword}}/gm, user.resetPasswordToken) .replace(/{{domain}}/gm, process.env.DEFAULT_DOMAIN), }); if (mail.accepted.length) { return res.status(200).json({ email: user.email, message: 'Reset password email has been sent.', }); } return res.status(400).json({ error: "Couldn't reset password." }); }; export const resetUserPassword: RequestHandler = async (req, _res, next) => { const user = await resetPassword(req.params.resetPasswordToken); if (user) { const token = signToken(user); req.user = { token }; } return next(); };