authController.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import { Handler } from "express";
  2. import fs from "fs";
  3. import path from "path";
  4. import passport from "passport";
  5. import JWT from "jsonwebtoken";
  6. import axios from "axios";
  7. import { addDays } from "date-fns";
  8. import { isAdmin } from "../utils";
  9. import transporter from "../mail/mail";
  10. import { resetMailText, verifyMailText } from "../mail/text";
  11. import {
  12. createUser,
  13. changePassword,
  14. generateApiKey,
  15. getUser,
  16. verifyUser,
  17. requestPasswordReset,
  18. resetPassword
  19. } from "../db/user";
  20. /* Read email template */
  21. const resetEmailTemplatePath = path.join(
  22. __dirname,
  23. "../mail/template-reset.html"
  24. );
  25. const verifyEmailTemplatePath = path.join(
  26. __dirname,
  27. "../mail/template-verify.html"
  28. );
  29. const resetEmailTemplate = fs
  30. .readFileSync(resetEmailTemplatePath, { encoding: "utf-8" })
  31. .replace(/{{domain}}/gm, process.env.DEFAULT_DOMAIN);
  32. const verifyEmailTemplate = fs
  33. .readFileSync(verifyEmailTemplatePath, { encoding: "utf-8" })
  34. .replace(/{{domain}}/gm, process.env.DEFAULT_DOMAIN);
  35. /* Function to generate JWT */
  36. const signToken = (user: UserJoined) =>
  37. JWT.sign(
  38. {
  39. iss: "ApiAuth",
  40. sub: user.email,
  41. domain: user.domain || "",
  42. admin: isAdmin(user.email),
  43. iat: parseInt((new Date().getTime() / 1000).toFixed(0)),
  44. exp: parseInt((addDays(new Date(), 7).getTime() / 1000).toFixed(0))
  45. } as Record<string, any>,
  46. process.env.JWT_SECRET
  47. );
  48. /* Passport.js authentication controller */
  49. const authenticate = (
  50. type: "jwt" | "local" | "localapikey",
  51. error: string,
  52. isStrict = true
  53. ) =>
  54. function auth(req, res, next) {
  55. if (req.user) return next();
  56. return passport.authenticate(type, (err, user) => {
  57. if (err) return res.status(400);
  58. if (!user && isStrict) return res.status(401).json({ error });
  59. if (user && isStrict && !user.verified) {
  60. return res.status(400).json({
  61. error:
  62. "Your email address is not verified. " +
  63. "Click on signup to get the verification link again."
  64. });
  65. }
  66. if (user && user.banned) {
  67. return res
  68. .status(400)
  69. .json({ error: "Your are banned from using this website." });
  70. }
  71. if (user) {
  72. req.user = {
  73. ...user,
  74. admin: isAdmin(user.email)
  75. };
  76. return next();
  77. }
  78. return next();
  79. })(req, res, next);
  80. };
  81. export const authLocal = authenticate("local", "Login credentials are wrong.");
  82. export const authJwt = authenticate("jwt", "Unauthorized.");
  83. export const authJwtLoose = authenticate("jwt", "Unauthorized.", false);
  84. export const authApikey = authenticate(
  85. "localapikey",
  86. "API key is not correct.",
  87. false
  88. );
  89. /* reCaptcha controller */
  90. export const recaptcha: Handler = async (req, res, next) => {
  91. if (process.env.NODE_ENV === "production" && !req.user) {
  92. const isReCaptchaValid = await axios({
  93. method: "post",
  94. url: "https://www.google.com/recaptcha/api/siteverify",
  95. headers: {
  96. "Content-type": "application/x-www-form-urlencoded"
  97. },
  98. params: {
  99. secret: process.env.RECAPTCHA_SECRET_KEY,
  100. response: req.body.reCaptchaToken,
  101. remoteip: req.realIP
  102. }
  103. });
  104. if (!isReCaptchaValid.data.success) {
  105. return res
  106. .status(401)
  107. .json({ error: "reCAPTCHA is not valid. Try again." });
  108. }
  109. }
  110. return next();
  111. };
  112. export const authAdmin: Handler = async (req, res, next) => {
  113. if (!req.user.admin) {
  114. return res.status(401).json({ error: "Unauthorized." });
  115. }
  116. return next();
  117. };
  118. export const signup: Handler = async (req, res) => {
  119. const { email, password } = req.body;
  120. if (password.length > 64) {
  121. return res.status(400).json({ error: "Maximum password length is 64." });
  122. }
  123. if (email.length > 255) {
  124. return res.status(400).json({ error: "Maximum email length is 255." });
  125. }
  126. const user = await getUser(email);
  127. if (user && user.verified) {
  128. return res.status(403).json({ error: "Email is already in use." });
  129. }
  130. const newUser = await createUser(email, password, user);
  131. const mail = await transporter.sendMail({
  132. from: process.env.MAIL_FROM || process.env.MAIL_USER,
  133. to: newUser.email,
  134. subject: "Verify your account",
  135. text: verifyMailText.replace(
  136. /{{verification}}/gim,
  137. newUser.verification_token
  138. ),
  139. html: verifyEmailTemplate.replace(
  140. /{{verification}}/gim,
  141. newUser.verification_token
  142. )
  143. });
  144. if (mail.accepted.length) {
  145. return res
  146. .status(201)
  147. .json({ email, message: "Verification email has been sent." });
  148. }
  149. return res
  150. .status(400)
  151. .json({ error: "Couldn't send verification email. Try again." });
  152. };
  153. export const login: Handler = (req, res) => {
  154. const token = signToken(req.user);
  155. return res.status(200).json({ token });
  156. };
  157. export const renew: Handler = (req, res) => {
  158. const token = signToken(req.user);
  159. return res.status(200).json({ token });
  160. };
  161. export const verify: Handler = async (req, _res, next) => {
  162. const { verificationToken } = req.params;
  163. if (!verificationToken) return next();
  164. const user = await verifyUser(req.params.verificationToken);
  165. if (user) {
  166. const token = signToken(user);
  167. req.token = token;
  168. }
  169. return next();
  170. };
  171. export const changeUserPassword: Handler = async (req, res) => {
  172. if (req.body.password.length < 8) {
  173. return res
  174. .status(400)
  175. .json({ error: "Password must be at least 8 chars long." });
  176. }
  177. if (req.body.password.length > 64) {
  178. return res.status(400).json({ error: "Maximum password length is 64." });
  179. }
  180. const changedUser = await changePassword(req.user.id, req.body.password);
  181. if (changedUser) {
  182. return res
  183. .status(200)
  184. .json({ message: "Your password has been changed successfully." });
  185. }
  186. return res
  187. .status(400)
  188. .json({ error: "Couldn't change the password. Try again later" });
  189. };
  190. export const generateUserApiKey = async (req, res) => {
  191. const apikey = await generateApiKey(req.user.id);
  192. if (apikey) {
  193. return res.status(201).json({ apikey });
  194. }
  195. return res
  196. .status(400)
  197. .json({ error: "Sorry, an error occured. Please try again later." });
  198. };
  199. export const userSettings: Handler = (req, res) =>
  200. res.status(200).json({
  201. apikey: req.user.apikey || "",
  202. customDomain: req.user.domain || "",
  203. homepage: req.user.homepage || ""
  204. });
  205. export const requestUserPasswordReset: Handler = async (req, res) => {
  206. const user = await requestPasswordReset(req.body.email);
  207. if (!user) {
  208. return res.status(400).json({ error: "Couldn't reset password." });
  209. }
  210. const mail = await transporter.sendMail({
  211. from: process.env.MAIL_USER,
  212. to: user.email,
  213. subject: "Reset your password",
  214. text: resetMailText
  215. .replace(/{{resetpassword}}/gm, user.reset_password_token)
  216. .replace(/{{domain}}/gm, process.env.DEFAULT_DOMAIN),
  217. html: resetEmailTemplate
  218. .replace(/{{resetpassword}}/gm, user.reset_password_token)
  219. .replace(/{{domain}}/gm, process.env.DEFAULT_DOMAIN)
  220. });
  221. if (mail.accepted.length) {
  222. return res.status(200).json({
  223. email: user.email,
  224. message: "Reset password email has been sent."
  225. });
  226. }
  227. return res.status(400).json({ error: "Couldn't reset password." });
  228. };
  229. export const resetUserPassword: Handler = async (req, _res, next) => {
  230. const { resetPasswordToken } = req.params;
  231. if (resetPasswordToken) {
  232. const user: UserJoined = await resetPassword(resetPasswordToken);
  233. if (user) {
  234. const token = signToken(user as UserJoined);
  235. req.token = token;
  236. }
  237. }
  238. return next();
  239. };