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
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

View File

@ -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: "" })
});

View File

@ -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,
}

View File

@ -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();
}

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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>

View File

@ -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}}

View File

@ -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}}