Jelajahi Sumber

make sending emails optional

Pouria Ezzati 1 tahun lalu
induk
melakukan
ea888dcb1d

+ 5 - 3
.example.env

@@ -61,9 +61,11 @@ ADMIN_EMAILS=
 # Get it from https://developers.google.com/safe-browsing/v4/get-started
 GOOGLE_SAFE_BROWSING_KEY=
 
-# Your email host details to use to send verification emails.
-# More info on http://nodemailer.com/
-# Mail from example "Kutt <support@kutt.it>". Leave empty to use MAIL_USER
+# Optional - Email is used to verify or change email address, reset password, and send reports.
+# If it's disabled, all the above functionality would be disabled as well.
+# MAIL_FROM example: "Kutt <support@kutt.it>". Leave it empty to use MAIL_USER.
+# More info on the configuration on http://nodemailer.com/.
+MAIL_ENABLED=false
 MAIL_HOST=
 MAIL_PORT=
 MAIL_SECURE=true

+ 5 - 4
server/env.js

@@ -28,12 +28,13 @@ const env = cleanEnv(process.env, {
   JWT_SECRET: str(),
   ADMIN_EMAILS: str({ default: "" }),
   GOOGLE_SAFE_BROWSING_KEY: str({ default: "" }),
-  MAIL_HOST: str(),
-  MAIL_PORT: num(),
+  MAIL_ENABLED: bool({ default: false }),
+  MAIL_HOST: str({ default: "" }),
+  MAIL_PORT: num({ default: 587 }),
   MAIL_SECURE: bool({ default: false }),
-  MAIL_USER: str(),
+  MAIL_USER: str({ default: "" }),
   MAIL_FROM: str({ default: "", example: "Kutt <support@kutt.it>" }),
-  MAIL_PASSWORD: str(),
+  MAIL_PASSWORD: str({ default: "" }),
   REPORT_EMAIL: str({ default: "" }),
   CONTACT_EMAIL: str({ default: "" })
 });

+ 25 - 9
server/handlers/auth.handler.js

@@ -222,10 +222,11 @@ async function resetPasswordRequest(req, res) {
       reset_password_expires: addMinutes(new Date(), 30).toISOString()
     }
   );
-  
+
   if (user) {
-    // TODO: handle error
-    mail.resetPasswordToken(user).catch(() => null);
+    mail.resetPasswordToken(user).catch(error => {
+      console.error("Send reset-password token email error:\n", error);
+    });
   }
 
   if (req.isHTML) {
@@ -264,11 +265,6 @@ async function resetPassword(req, res, next) {
   next();
 }
 
-function signupAccess(req, res, next) {
-  if (!env.DISALLOW_REGISTRATION) return next();
-  throw new CustomError("Registration is not allowed.");
-}
-
 async function changeEmailRequest(req, res) {
   const { email, password } = req.body;
   
@@ -352,6 +348,25 @@ async function changeEmail(req, res, next) {
   return next();
 }
 
+function featureAccess(features, redirect) {
+  return function(req, res, next) {
+    for (let i = 0; i < features.length; ++i) {
+      if (!features[i]) {
+        if (redirect) {
+          return res.redirect("/");
+        } else {
+          throw new CustomError("Request is not allowed.", 400);
+        }
+      } 
+    }
+    next();
+  }
+}
+
+function featureAccessPage(features) {
+  return featureAccess(features, true);
+}
+
 module.exports = {
   admin,
   apikey,
@@ -359,6 +374,8 @@ module.exports = {
   changeEmailRequest,
   changePassword,
   cooldown,
+  featureAccess,
+  featureAccessPage,
   generateApiKey,
   jwt,
   jwtLoose,
@@ -369,6 +386,5 @@ module.exports = {
   resetPassword,
   resetPasswordRequest,
   signup,
-  signupAccess,
   verify,
 }

+ 1 - 0
server/handlers/locals.handler.js

@@ -26,6 +26,7 @@ function config(req, res, next) {
   res.locals.contact_email = env.CONTACT_EMAIL;
   res.locals.server_ip_address = env.SERVER_IP_ADDRESS;
   res.locals.disallow_registration = env.DISALLOW_REGISTRATION;
+  res.locals.mail_enabled = env.MAIL_ENABLED;
   next();
 }
 

+ 38 - 13
server/mail/mail.js

@@ -24,20 +24,33 @@ const transporter = nodemailer.createTransport(mailConfig);
 const resetEmailTemplatePath = path.join(__dirname, "template-reset.html");
 const verifyEmailTemplatePath = path.join(__dirname, "template-verify.html");
 const changeEmailTemplatePath = path.join(__dirname,"template-change-email.html");
-const resetEmailTemplate = fs
-  .readFileSync(resetEmailTemplatePath, { encoding: "utf-8" })
-  .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
-  .replace(/{{site_name}}/gm, env.SITE_NAME);
-const verifyEmailTemplate = fs
-  .readFileSync(verifyEmailTemplatePath, { encoding: "utf-8" })
-  .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
-  .replace(/{{site_name}}/gm, env.SITE_NAME);
-const changeEmailTemplate = fs
-  .readFileSync(changeEmailTemplatePath, { encoding: "utf-8" })
-  .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
-  .replace(/{{site_name}}/gm, env.SITE_NAME);
+
+
+let resetEmailTemplate, 
+    verifyEmailTemplate,
+    changeEmailTemplate;
+
+// only read email templates if email is enabled
+if (env.MAIL_ENABLED) {
+  resetEmailTemplate = fs
+    .readFileSync(resetEmailTemplatePath, { encoding: "utf-8" })
+    .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+    .replace(/{{site_name}}/gm, env.SITE_NAME);
+  verifyEmailTemplate = fs
+    .readFileSync(verifyEmailTemplatePath, { encoding: "utf-8" })
+    .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+    .replace(/{{site_name}}/gm, env.SITE_NAME);
+  changeEmailTemplate = fs
+    .readFileSync(changeEmailTemplatePath, { encoding: "utf-8" })
+    .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
+    .replace(/{{site_name}}/gm, env.SITE_NAME);
+}
 
 async function verification(user) {
+  if (!env.MAIL_ENABLED) {
+    throw new Error("Attempting to send verification email but email is not enabled.");
+  };
+
   const mail = await transporter.sendMail({
     from: env.MAIL_FROM || env.MAIL_USER,
     to: user.email,
@@ -58,6 +71,10 @@ async function verification(user) {
 }
 
 async function changeEmail(user) {
+  if (!env.MAIL_ENABLED) {
+    throw new Error("Attempting to send change email token but email is not enabled.");
+  };
+  
   const mail = await transporter.sendMail({
     from: env.MAIL_FROM || env.MAIL_USER,
     to: user.change_email_address,
@@ -78,6 +95,10 @@ async function changeEmail(user) {
 }
 
 async function resetPasswordToken(user) {
+  if (!env.MAIL_ENABLED) {
+    throw new Error("Attempting to send reset password email but email is not enabled.");
+  };
+
   const mail = await transporter.sendMail({
     from: env.MAIL_FROM || env.MAIL_USER,
     to: user.email,
@@ -89,7 +110,7 @@ async function resetPasswordToken(user) {
       .replace(/{{resetpassword}}/gm, user.reset_password_token)
       .replace(/{{domain}}/gm, env.DEFAULT_DOMAIN)
   });
-  
+
   if (!mail.accepted.length) {
     throw new CustomError(
       "Couldn't send reset password email. Try again later."
@@ -98,6 +119,10 @@ async function resetPasswordToken(user) {
 }
 
 async function sendReportEmail(link) {
+  if (!env.MAIL_ENABLED) {
+    throw new Error("Attempting to send report email but email is not enabled.");
+  };
+
   const mail = await transporter.sendMail({
     from: env.MAIL_FROM || env.MAIL_USER,
     to: env.REPORT_EMAIL,

+ 4 - 1
server/routes/auth.routes.js

@@ -6,6 +6,7 @@ const asyncHandler = require("../utils/asyncHandler");
 const locals = require("../handlers/locals.handler");
 const auth = require("../handlers/auth.handler");
 const utils = require("../utils");
+const env = require("../env");
 
 const router = Router();
 
@@ -21,7 +22,7 @@ router.post(
 router.post(
   "/signup",
   locals.viewTemplate("partials/auth/form"),
-  auth.signupAccess,
+  auth.featureAccess([!env.DISALLOW_REGISTRATION, env.MAIL_ENABLED]),
   validators.signup,
   asyncHandler(helpers.verify),
   asyncHandler(auth.signup)
@@ -40,6 +41,7 @@ router.post(
   "/change-email",
   locals.viewTemplate("partials/settings/change_email"),
   asyncHandler(auth.jwt),
+  auth.featureAccess([env.MAIL_ENABLED]),
   validators.changeEmail,
   asyncHandler(helpers.verify),
   asyncHandler(auth.changeEmailRequest)
@@ -55,6 +57,7 @@ router.post(
 router.post(
   "/reset-password",
   locals.viewTemplate("partials/reset_password/form"),
+  auth.featureAccess([env.MAIL_ENABLED]),
   validators.resetPassword,
   asyncHandler(helpers.verify),
   asyncHandler(auth.resetPasswordRequest)

+ 1 - 0
server/routes/link.routes.js

@@ -88,6 +88,7 @@ router.post(
 router.post(
   "/report",
   locals.viewTemplate("partials/report/form"),
+  auth.featureAccess([env.MAIL_ENABLED]),
   validators.reportLink,
   asyncHandler(helpers.verify),
   asyncHandler(link.report)

+ 2 - 0
server/routes/renders.routes.js

@@ -5,6 +5,7 @@ const renders = require("../handlers/renders.handler");
 const asyncHandler = require("../utils/asyncHandler");
 const locals = require("../handlers/locals.handler");
 const auth = require("../handlers/auth.handler");
+const env = require("../env");
 
 const router = Router();
 
@@ -64,6 +65,7 @@ router.get(
 
 router.get(
   "/reset-password",
+  auth.featureAccessPage([env.MAIL_ENABLED]),
   asyncHandler(auth.jwtLoosePage),
   asyncHandler(locals.user),
   asyncHandler(renders.resetPassword)

+ 21 - 17
server/views/partials/auth/form.hbs

@@ -29,25 +29,29 @@
       Log in
     </button>
     {{#unless disallow_registration}}
-      <button 
-        type="button"
-        class="secondary signup" 
-        hx-post="/api/auth/signup" 
-        hx-target="#login-signup" 
-        hx-trigger="click" 
-        hx-indicator="#login-signup" 
-        hx-swap="outerHTML"
-        hx-sync="closest form"
-        hx-on:htmx:before-request="htmx.addClass('#login-signup', 'signup')"
-        hx-on:htmx:after-request="htmx.removeClass('#login-signup', 'signup')"
-      >
-          <span>{{> icons/new_user}}</span>
-          <span>{{> icons/spinner}}</span>
-          Sign up
-      </button>
+      {{#if mail_enabled}}
+        <button 
+          type="button"
+          class="secondary signup" 
+          hx-post="/api/auth/signup" 
+          hx-target="#login-signup" 
+          hx-trigger="click" 
+          hx-indicator="#login-signup" 
+          hx-swap="outerHTML"
+          hx-sync="closest form"
+          hx-on:htmx:before-request="htmx.addClass('#login-signup', 'signup')"
+          hx-on:htmx:after-request="htmx.removeClass('#login-signup', 'signup')"
+        >
+            <span>{{> icons/new_user}}</span>
+            <span>{{> icons/spinner}}</span>
+            Sign up
+        </button>
+      {{/if}}
     {{/unless}}
   </div>
-  <a class="forgot-password" href="/reset-password" title="Reset password">Forgot your password?</a>
+  {{#if mail_enabled}}
+    <a class="forgot-password" href="/reset-password" title="Reset password">Forgot your password?</a>
+  {{/if}}
   {{#unless errors}}
     {{#if error}}
       <p class="error">{{error}}</p>

+ 5 - 3
server/views/report.hbs

@@ -4,10 +4,12 @@
     Report abuse.
   </h2>
   <p>
-    Report abuses, malware and phishing links to the email address below
-    or use the form. We will review as soon as we can.
+    Report abuses, malware and phishing links to the email address below {{#if mail_enabled}}or use the form{{/if}}.
+    We will review as soon as we can.
   </p>
   {{> report/email}}
-  {{> report/form}}
+  {{#if mail_enabled}}
+    {{> report/form}}
+  {{/if}}
 </section>
 {{> footer}}

+ 4 - 2
server/views/settings.hbs

@@ -10,8 +10,10 @@
   <hr />
   {{> settings/change_password}}
   <hr />
-  {{> settings/change_email}}
-  <hr />
+  {{#if mail_enabled}}
+    {{> settings/change_email}}
+    <hr />
+  {{/if}}
   {{> settings/delete_account}}
 </section>
 {{> footer}}