authController.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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(
  82. "local",
  83. "Login email and/or password are wrong."
  84. );
  85. export const authJwt = authenticate("jwt", "Unauthorized.");
  86. export const authJwtLoose = authenticate("jwt", "Unauthorized.", false);
  87. export const authApikey = authenticate(
  88. "localapikey",
  89. "API key is not correct.",
  90. false
  91. );
  92. /* reCaptcha controller */
  93. export const recaptcha: Handler = async (req, res, next) => {
  94. if (process.env.NODE_ENV === "production" && !req.user) {
  95. const isReCaptchaValid = await axios({
  96. method: "post",
  97. url: "https://www.google.com/recaptcha/api/siteverify",
  98. headers: {
  99. "Content-type": "application/x-www-form-urlencoded"
  100. },
  101. params: {
  102. secret: process.env.RECAPTCHA_SECRET_KEY,
  103. response: req.body.reCaptchaToken,
  104. remoteip: req.realIP
  105. }
  106. });
  107. if (!isReCaptchaValid.data.success) {
  108. return res
  109. .status(401)
  110. .json({ error: "reCAPTCHA is not valid. Try again." });
  111. }
  112. }
  113. return next();
  114. };
  115. export const authAdmin: Handler = async (req, res, next) => {
  116. if (!req.user.admin) {
  117. return res.status(401).json({ error: "Unauthorized." });
  118. }
  119. return next();
  120. };
  121. export const signup: Handler = async (req, res) => {
  122. const { email, password } = req.body;
  123. if (password.length > 64) {
  124. return res.status(400).json({ error: "Maximum password length is 64." });
  125. }
  126. if (email.length > 255) {
  127. return res.status(400).json({ error: "Maximum email length is 255." });
  128. }
  129. const user = await getUser(email);
  130. if (user && user.verified) {
  131. return res.status(403).json({ error: "Email is already in use." });
  132. }
  133. const newUser = await createUser(email, password, user);
  134. try {
  135. const mail = await transporter.sendMail({
  136. from: process.env.MAIL_FROM || process.env.MAIL_USER,
  137. to: newUser.email,
  138. subject: "Verify your account",
  139. text: verifyMailText.replace(
  140. /{{verification}}/gim,
  141. newUser.verification_token
  142. ),
  143. html: verifyEmailTemplate.replace(
  144. /{{verification}}/gim,
  145. newUser.verification_token
  146. )
  147. });
  148. if (mail.accepted.length) {
  149. return res
  150. .status(201)
  151. .json({ email, message: "Verification email has been sent." });
  152. }
  153. return res
  154. .status(400)
  155. .json({ error: "Couldn't send verification email. Try again." });
  156. } catch (error) {
  157. console.log({ error });
  158. }
  159. };
  160. export const login: Handler = (req, res) => {
  161. const token = signToken(req.user);
  162. return res.status(200).json({ token });
  163. };
  164. export const renew: Handler = (req, res) => {
  165. const token = signToken(req.user);
  166. return res.status(200).json({ token });
  167. };
  168. export const verify: Handler = async (req, _res, next) => {
  169. const user = await verifyUser(req.params.verificationToken);
  170. if (user) {
  171. const token = signToken(user);
  172. req.token = token;
  173. }
  174. return next();
  175. };
  176. export const changeUserPassword: Handler = async (req, res) => {
  177. if (req.body.password.length < 8) {
  178. return res
  179. .status(400)
  180. .json({ error: "Password must be at least 8 chars long." });
  181. }
  182. if (req.body.password.length > 64) {
  183. return res.status(400).json({ error: "Maximum password length is 64." });
  184. }
  185. const changedUser = await changePassword(req.user.id, req.body.password);
  186. if (changedUser) {
  187. return res
  188. .status(200)
  189. .json({ message: "Your password has been changed successfully." });
  190. }
  191. return res
  192. .status(400)
  193. .json({ error: "Couldn't change the password. Try again later" });
  194. };
  195. export const generateUserApiKey = async (req, res) => {
  196. const apikey = await generateApiKey(req.user.id);
  197. if (apikey) {
  198. return res.status(201).json({ apikey });
  199. }
  200. return res
  201. .status(400)
  202. .json({ error: "Sorry, an error occured. Please try again later." });
  203. };
  204. export const userSettings: Handler = (req, res) =>
  205. res.status(200).json({
  206. apikey: req.user.apikey || "",
  207. customDomain: req.user.domain || "",
  208. homepage: req.user.homepage || ""
  209. });
  210. export const requestUserPasswordReset: Handler = async (req, res) => {
  211. const user = await requestPasswordReset(req.body.email);
  212. if (!user) {
  213. return res.status(400).json({ error: "Couldn't reset password." });
  214. }
  215. const mail = await transporter.sendMail({
  216. from: process.env.MAIL_USER,
  217. to: user.email,
  218. subject: "Reset your password",
  219. text: resetMailText
  220. .replace(/{{resetpassword}}/gm, user.reset_password_token)
  221. .replace(/{{domain}}/gm, process.env.DEFAULT_DOMAIN),
  222. html: resetEmailTemplate
  223. .replace(/{{resetpassword}}/gm, user.reset_password_token)
  224. .replace(/{{domain}}/gm, process.env.DEFAULT_DOMAIN)
  225. });
  226. if (mail.accepted.length) {
  227. return res.status(200).json({
  228. email: user.email,
  229. message: "Reset password email has been sent."
  230. });
  231. }
  232. return res.status(400).json({ error: "Couldn't reset password." });
  233. };
  234. export const resetUserPassword: Handler = async (req, _res, next) => {
  235. const user: UserJoined = await resetPassword(req.params.resetPasswordToken);
  236. if (user) {
  237. const token = signToken(user as UserJoined);
  238. req.token = token;
  239. }
  240. return next();
  241. };