Skip to content

Commit

Permalink
adds setting to disable email/smtp
Browse files Browse the repository at this point in the history
  • Loading branch information
tdjsnelling committed Feb 21, 2024
1 parent 41155da commit e2ba154
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 69 deletions.
76 changes: 42 additions & 34 deletions api/src/controllers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const register = (mail) => async (req, res, next) => {
role,
invitedBy: invite?.invitingUser,
remainingInvites: 0,
emailVerified: false,
emailVerified: process.env.SQ_DISABLE_EMAIL,
bonusPoints: 0,
totp: {
enabled: false,
Expand All @@ -125,19 +125,21 @@ export const register = (mail) => async (req, res, next) => {

const createdUser = await newUser.save();

const emailVerificationValidUntil = created + 48 * 60 * 60 * 1000;
const emailVerificationToken = jwt.sign(
{
user: req.body.email,
validUntil: emailVerificationValidUntil,
},
process.env.SQ_JWT_SECRET
);
await sendVerificationEmail(
mail,
req.body.email,
emailVerificationToken
);
if (!process.env.SQ_DISABLE_EMAIL) {
const emailVerificationValidUntil = created + 48 * 60 * 60 * 1000;
const emailVerificationToken = jwt.sign(
{
user: req.body.email,
validUntil: emailVerificationValidUntil,
},
process.env.SQ_JWT_SECRET
);
await sendVerificationEmail(
mail,
req.body.email,
emailVerificationToken
);
}

if (createdUser) {
if (req.body.invite) {
Expand Down Expand Up @@ -293,14 +295,16 @@ export const generateInvite = (mail) => async (req, res, next) => {
const createdInvite = await invite.save();

if (createdInvite) {
await mail.sendMail({
from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`,
to: email,
subject: "Invite",
text: `You have been invited to join ${process.env.SQ_SITE_NAME}. Please follow the link below to register.
if (!process.env.SQ_DISABLE_EMAIL) {
await mail.sendMail({
from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`,
to: email,
subject: "Invite",
text: `You have been invited to join ${process.env.SQ_SITE_NAME}. Please follow the link below to register.
${process.env.SQ_BASE_URL}/register?token=${createdInvite.token}`,
});
});
}
res.send(createdInvite);
}
} else {
Expand Down Expand Up @@ -343,18 +347,20 @@ export const changePassword = (mail) => async (req, res, next) => {
{ $set: { password: hash } }
);

await mail.sendMail({
from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`,
to: user.email,
subject: "Your password was changed",
text: `Your password was updated successfully at ${new Date().toISOString()} from ${
req.ip
}.
if (!process.env.SQ_DISABLE_EMAIL) {
await mail.sendMail({
from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`,
to: user.email,
subject: "Your password was changed",
text: `Your password was updated successfully at ${new Date().toISOString()} from ${
req.ip
}.
If you did not perform this action, follow the link below immediately to reset your password. If this was you, no action is required.
${process.env.SQ_BASE_URL}/reset-password/initiate`,
});
});
}

res.sendStatus(200);
} catch (e) {
Expand Down Expand Up @@ -388,14 +394,16 @@ export const initiatePasswordReset = (mail) => async (req, res, next) => {
process.env.SQ_JWT_SECRET
);

await mail.sendMail({
from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`,
to: user.email,
subject: "Password reset",
text: `Please follow the link below to reset your password.
if (!process.env.SQ_DISABLE_EMAIL) {
await mail.sendMail({
from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`,
to: user.email,
subject: "Password reset",
text: `Please follow the link below to reset your password.
${process.env.SQ_BASE_URL}/reset-password/finalise?token=${token}`,
});
});
}

res.sendStatus(200);
} catch (e) {
Expand Down
22 changes: 13 additions & 9 deletions api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,19 @@ validateConfig(config).then(() => {
});
}

const mail = nodemailer.createTransport({
host: process.env.SQ_SMTP_HOST,
port: process.env.SQ_SMTP_PORT,
secure: process.env.SQ_SMTP_SECURE,
auth: {
user: process.env.SQ_SMTP_USER,
pass: process.env.SQ_SMTP_PASS,
},
});
let mail;

if (!process.env.SQ_DISABLE_EMAIL) {
mail = nodemailer.createTransport({
host: process.env.SQ_SMTP_HOST,
port: process.env.SQ_SMTP_PORT,
secure: process.env.SQ_SMTP_SECURE,
auth: {
user: process.env.SQ_SMTP_USER,
pass: process.env.SQ_SMTP_PASS,
},
});
}

const connectToDb = () => {
console.log("[sq] initiating db connection...");
Expand Down
29 changes: 16 additions & 13 deletions api/src/setup/createAdminUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const createAdminUser = async (mail) => {
password: hash,
created,
remainingInvites: Number.MAX_SAFE_INTEGER,
emailVerified: process.env.SQ_DISABLE_EMAIL,
});
adminUser.uid = crypto
.createHash("sha256")
Expand All @@ -35,19 +36,21 @@ const createAdminUser = async (mail) => {

await adminUser.save();

const emailVerificationValidUntil = created + 48 * 60 * 60 * 1000;
const emailVerificationToken = jwt.sign(
{
user: process.env.SQ_ADMIN_EMAIL,
validUntil: emailVerificationValidUntil,
},
process.env.SQ_JWT_SECRET
);
await sendVerificationEmail(
mail,
process.env.SQ_ADMIN_EMAIL,
emailVerificationToken
);
if (!process.env.SQ_DISABLE_EMAIL) {
const emailVerificationValidUntil = created + 48 * 60 * 60 * 1000;
const emailVerificationToken = jwt.sign(
{
user: process.env.SQ_ADMIN_EMAIL,
validUntil: emailVerificationValidUntil,
},
process.env.SQ_JWT_SECRET
);
await sendVerificationEmail(
mail,
process.env.SQ_ADMIN_EMAIL,
emailVerificationToken
);
}

console.log("[sq] created initial admin user");
}
Expand Down
41 changes: 35 additions & 6 deletions api/src/utils/validateConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,31 @@ const configSchema = yup
SQ_BASE_URL: yup.string().matches(httpRegex).required(),
SQ_API_URL: yup.string().matches(httpRegex).required(),
SQ_MONGO_URL: yup.string().matches(mongoRegex).required(),
SQ_MAIL_FROM_ADDRESS: yup.string().email().required(),
SQ_SMTP_HOST: yup.string().required(),
SQ_SMTP_PORT: yup.number().integer().min(1).max(65535).required(),
SQ_SMTP_SECURE: yup.boolean().required(),
SQ_DISABLE_EMAIL: yup.boolean(),
SQ_MAIL_FROM_ADDRESS: yup
.string()
.email()
.when("SQ_DISABLE_EMAIL", {
is: (val) => val !== true,
then: (schema) => schema.required(),
}),
SQ_SMTP_HOST: yup.string().when("SQ_DISABLE_EMAIL", {
is: (val) => val !== true,
then: (schema) => schema.required(),
}),
SQ_SMTP_PORT: yup
.number()
.integer()
.min(1)
.max(65535)
.when("SQ_DISABLE_EMAIL", {
is: (val) => val !== true,
then: (schema) => schema.required(),
}),
SQ_SMTP_SECURE: yup.boolean().when("SQ_DISABLE_EMAIL", {
is: (val) => val !== true,
then: (schema) => schema.required(),
}),
})
.strict()
.noUnknown()
Expand All @@ -77,8 +98,16 @@ const configSchema = yup
SQ_JWT_SECRET: yup.string().required(),
SQ_SERVER_SECRET: yup.string().required(),
SQ_ADMIN_EMAIL: yup.string().email().required(),
SQ_SMTP_USER: yup.string().required(),
SQ_SMTP_PASS: yup.string().required(),
SQ_SMTP_USER: yup.string(),
SQ_SMTP_PASS: yup.string(),
})
.when("envs.SQ_DISABLE_EMAIL", {
is: (val) => val !== true,
then: (schema) => {
schema.fields.SQ_SMTP_USER = yup.string().required();
schema.fields.SQ_SMTP_PASS = yup.string().required();
return schema;
},
})
.strict()
.noUnknown()
Expand Down
9 changes: 6 additions & 3 deletions client/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,24 @@
"accEnable2FA": "Enable 2FA",
"accEveryRequestYouFulfill": "for every request you fulfill, or",
"accForEveryGBYouUpload": "for every GB you upload",
"accIfYouAreAlsUploaderAcceptTorrent": "if you are als the uploader of the accepted torrent",
"accIfYouAreAlsUploaderAcceptTorrent": "if you are also the uploader of the accepted torrent",
"accInviteLinkCopiedClipboard": "Invite link copied to clipboard",
"accInvites": "Invites",
"accRemaining": "remaining",
"accRoleUser": "Role: user",
"accRoleAdmin": "Role: admin",
"accInviteSentSuccess": "Invite sent successfully",
"accInviteSentSuccessNoEmail": "Invite created successfully",
"accInviteText1": "Enter an email address to send an invite. The invited user will need to sign up with the same email address. Once the invite is generated, you can also copy a direct invite link.",
"accInviteText1NoEmail": "Enter an email address to generate an invite. The invited user will need to sign up with the same email address. This tracker has email sending disabled, so you will need to copy and send the invite link yourself.",
"accItemsPurchasedSuccess": "Items purchased successfully",
"accMyAccount": "My account",
"accPassChangedSuccess": "Password changed successfully",
"accPurchaseInvites": "Purchase invites",
"accPurchaseUpload1GB": "Purchase upload (1 GB)",
"accRole": "Role",
"accSendInvite": "Send invite",
"accSendInviteNoEmail": "Create invite",
"accThisIsAdminAcc": "This is an admin account.",
"accValidUntil": "Valid until",
"accYouCurrentlyHave": "You currently have",
Expand Down Expand Up @@ -279,8 +282,8 @@
"userUploaded": "Uploaded",
"userMyUploads": "My uploads",
"usernameRules": "Can only consist of letters, numbers, and “.”",
"userUnban": "unban",
"userBan": "ban",
"userUnban": "Unban",
"userBan": "Ban",
"userUnbanned": "unbanned",
"userBanned": "banned",
"userAdmin": "Admin",
Expand Down
26 changes: 22 additions & 4 deletions client/pages/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ const Account = ({ token, invites = [], user, userRole }) => {
SQ_BP_COST_PER_INVITE,
SQ_BP_COST_PER_GB,
SQ_ALLOW_REGISTER,
SQ_DISABLE_EMAIL,
},
} = getConfig();

Expand Down Expand Up @@ -133,7 +134,14 @@ const Account = ({ token, invites = [], user, userRole }) => {
return currentInvitesList;
});

addNotification("success", `${getLocaleString("accInviteSentSuccess")}`);
addNotification(
"success",
`${getLocaleString(
SQ_DISABLE_EMAIL
? "accInviteSentSuccessNoEmail"
: "accInviteSentSuccess"
)}`
);

setRemainingInvites((r) => r - 1);

Expand Down Expand Up @@ -425,7 +433,9 @@ const Account = ({ token, invites = [], user, userRole }) => {
onClick={() => setShowInviteModal(true)}
disabled={remainingInvites < 1}
>
{getLocaleString("accSendInvite")}
{getLocaleString(
SQ_DISABLE_EMAIL ? "accSendInviteNoEmail" : "accSendInvite"
)}
</Button>
</Box>
</Box>
Expand Down Expand Up @@ -650,7 +660,11 @@ const Account = ({ token, invites = [], user, userRole }) => {
)}
{showInviteModal && (
<Modal close={() => setShowInviteModal(false)}>
<Text mb={5}>{getLocaleString("accInviteText1")}</Text>
<Text mb={5}>
{getLocaleString(
SQ_DISABLE_EMAIL ? "accInviteText1NoEmail" : "accInviteText1"
)}
</Text>
<form onSubmit={handleGenerateInvite}>
<Input
name="email"
Expand All @@ -674,7 +688,11 @@ const Account = ({ token, invites = [], user, userRole }) => {
>
{getLocaleString("accCancel")}
</Button>
<Button>{getLocaleString("accSendInvite")}</Button>
<Button>
{getLocaleString(
SQ_DISABLE_EMAIL ? "accSendInviteNoEmail" : "accSendInvite"
)}
</Button>
</Box>
</form>
</Modal>
Expand Down
10 changes: 10 additions & 0 deletions config.example.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,26 @@ module.exports = {
// The URL of your MongoDB server. Under the recommended setup, it should be `mongodb://sq_mongodb/sqtracker`.
SQ_MONGO_URL: "mongodb://sq_mongodb/sqtracker",

// Disables sending of any emails and removes the need for an SMTP server.
// Fine for testing, not recommended in production as users will not be able to reset their passwords.
SQ_DISABLE_EMAIL: false,

// The email address that mail will be sent from.
// Not required if SQ_DISABLE_EMAIL=true.
SQ_MAIL_FROM_ADDRESS: "[email protected]",

// The hostname of your SMTP server.
// Not required if SQ_DISABLE_EMAIL=true.
SQ_SMTP_HOST: "smtp.example.com",

// The port of your SMTP server.
// Not required if SQ_DISABLE_EMAIL=true.
SQ_SMTP_PORT: 587,

// Whether to force SMTP TLS: if true the connection will use TLS when connecting to server.
// If false (the default) then TLS is used if server supports the STARTTLS extension.
// In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false.
// Not required if SQ_DISABLE_EMAIL=true.
SQ_SMTP_SECURE: false,
},
secrets: {
Expand All @@ -111,9 +119,11 @@ module.exports = {
SQ_ADMIN_EMAIL: "[email protected]",

// The username to authenticate with your SMTP server with.
// Not required if SQ_DISABLE_EMAIL=true.
SQ_SMTP_USER: "smtp_username",

// The password to authenticate with your SMTP server with.
// Not required if SQ_DISABLE_EMAIL=true.
SQ_SMTP_PASS: "smtp_password",
},
};

0 comments on commit e2ba154

Please sign in to comment.