make sending emails optional

This commit is contained in:
Pouria Ezzati 2024-09-23 13:45:39 +03:30
parent ae98343cd3
commit ea888dcb1d
No known key found for this signature in database
11 changed files with 111 additions and 52 deletions

View File

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

View File

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

View File

@ -222,10 +222,11 @@ async function resetPasswordRequest(req, res) {
reset_password_expires: addMinutes(new Date(), 30).toISOString() reset_password_expires: addMinutes(new Date(), 30).toISOString()
} }
); );
if (user) { if (user) {
// TODO: handle error mail.resetPasswordToken(user).catch(error => {
mail.resetPasswordToken(user).catch(() => null); console.error("Send reset-password token email error:\n", error);
});
} }
if (req.isHTML) { if (req.isHTML) {
@ -264,11 +265,6 @@ async function resetPassword(req, res, next) {
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) { async function changeEmailRequest(req, res) {
const { email, password } = req.body; const { email, password } = req.body;
@ -352,6 +348,25 @@ async function changeEmail(req, res, next) {
return 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 = { module.exports = {
admin, admin,
apikey, apikey,
@ -359,6 +374,8 @@ module.exports = {
changeEmailRequest, changeEmailRequest,
changePassword, changePassword,
cooldown, cooldown,
featureAccess,
featureAccessPage,
generateApiKey, generateApiKey,
jwt, jwt,
jwtLoose, jwtLoose,
@ -369,6 +386,5 @@ module.exports = {
resetPassword, resetPassword,
resetPasswordRequest, resetPasswordRequest,
signup, signup,
signupAccess,
verify, verify,
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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