1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-11-10 04:32:35 +01:00

Merge pull request #965 from Puyodead1/dev/mail

Email Support
This commit is contained in:
Madeline 2023-02-24 23:17:36 +11:00 committed by GitHub
commit 224e2c8374
45 changed files with 10611 additions and 850 deletions

View File

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html class="theme-dark" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no" name="viewport" />
<link rel="stylesheet" href="/assets/40532.bb53efb11e3ed4046082.css" integrity="" />
<link rel="icon" href="/assets/847541504914fd33810e70a0ea73177e.ico" />
<title>Fosscord Test Client</title>
<meta charset="utf-8" data-react-helmet="true" />
</head>
<body>
<div id="app-mount"></div>
<script>
window.__OVERLAY__ = /overlay/.test(location.pathname);
window.__BILLING_STANDALONE__ = /^\/billing/.test(location.pathname);
window.GLOBAL_ENV = {
API_ENDPOINT: "/api",
API_VERSION: 9,
GATEWAY_ENDPOINT: `${location.protocol === "https:" ? "wss://" : "ws://"}${location.host}`,
WEBAPP_ENDPOINT: "",
CDN_HOST: `${location.hostname}`,
ASSET_ENDPOINT: "",
MEDIA_PROXY_ENDPOINT: "https://media.discordapp.net",
WIDGET_ENDPOINT: `//${location.host}/widget`,
INVITE_HOST: `${location.hostname}/invite`,
GUILD_TEMPLATE_HOST: "${location.host}/template",
GIFT_CODE_HOST: "${location.hostname}/gift",
RELEASE_CHANNEL: "canary",
MARKETING_ENDPOINT: "//discord.com",
BRAINTREE_KEY: "production_5st77rrc_49pp2rp4phym7387",
STRIPE_KEY: "pk_live_CUQtlpQUF0vufWpnpUmQvcdi",
NETWORKING_ENDPOINT: "//router.discordapp.net",
RTC_LATENCY_ENDPOINT: "//${location.hostname}/rtc",
ACTIVITY_APPLICATION_HOST: "discordsays.com",
PROJECT_ENV: "production",
REMOTE_AUTH_ENDPOINT: "//localhost:3020",
SENTRY_TAGS: { buildId: "d5b97e42230075cb9634c419c0cf4d2f8f9ada53", buildType: "normal" },
MIGRATION_SOURCE_ORIGIN: "https://${location.hostname}",
MIGRATION_DESTINATION_ORIGIN: "https://${location.hostname}",
HTML_TIMESTAMP: Date.now(),
ALGOLIA_KEY: "aca0d7082e4e63af5ba5917d5e96bed0"
};
window.localStorage.setItem("gatewayURL", window.GLOBAL_ENV.GATEWAY_ENDPOINT);
window.localStorage.setItem(
"DeveloperOptionsStore",
`{"trace":false,"canary":true,"logGatewayEvents":true,"logOverlayEvents":true,"logAnalyticsEvents":true,"sourceMapsEnabled":false,"axeEnabled":false}`
);
</script>
<script src="/assets/33329b5e3fc4ba0db663.js"></script>
<script src="/assets/c31f5155b71da969ec04.js"></script>
<script src="/assets/bba695569f6557775f9c.js"></script>
<script src="/assets/867449841939756f0ab0.js"></script>
</body>
</html>

View File

@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<title>Verify {instanceName} Login from New Location</title>
<style>
* {
font-size: 16px;
line-height: 24px;
font-family: Arial, Helvetica, sans-serif;
}
p {
color: white;
}
.ExternalClass {
width: 100%;
}
</style>
</head>
<body>
<div style="background-color: #202225;">
<img
src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
alt="Branding"
style="
width: 100%;
max-width: 200px;
margin: 0 auto;
display: block;
padding: 20px;
"
/>
<div
style="
width: 100%;
max-width: 500px;
margin: 0 auto;
padding: 40px 50px;
background-color: #32353b;
border-radius: 5px;
"
>
<p
style="
font-weight: 600;
font-size: 20px;
letter-spacing: 0.27px;
line-height: 24px;
"
>
Hey {userUsername},
</p>
<p>
It looks like someone tried to log into your {instanceName}
account from a new location. If this is you, follow the link
below to authorize logging in from this location on your
account. If this isn't you, we suggest changing your
password as soon as possible.
</p>
<p>
<strong>IP Address:</strong> {ipAddress}
<br />
<strong>Location:</strong> {locationCity}, {locationRegion},
{locationCountryName}
</p>
<div>
<div
style="
text-align: center;
justify-content: center;
padding-bottom: 10px;
"
>
<a
href="{verifyUrl}"
target="_blank"
style="
font-size: 15px;
border: none;
border-radius: 3px;
text-decoration: none;
color: white;
cursor: pointer;
padding: 15px 19px;
background-color: #ff5f00;
border-radius: 5px;
"
>Verify Login</a
>
</div>
<hr />
<div
style="
text-align: center;
justify-content: center;
padding-bottom: 10px;
"
>
<p>
Alternatively, you can directly paste this link into
your browser:
</p>
<a href="{verifyUrl}" target="_blank" style="word-wrap: break-word;">{verifyUrl}</a>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<title>{instanceName} Password Changed</title>
<style>
* {
font-size: 16px;
line-height: 24px;
font-family: Arial, Helvetica, sans-serif;
}
p {
color: white;
}
.ExternalClass {
width: 100%;
}
</style>
</head>
<body>
<div style="background-color: #202225;">
<img
src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
alt="Branding"
style="
width: 100%;
max-width: 200px;
margin: 0 auto;
display: block;
padding: 20px;
"
/>
<div
style="
width: 100%;
max-width: 500px;
margin: 0 auto;
padding: 40px 50px;
background-color: #32353b;
border-radius: 5px;
"
>
<p
style="
font-weight: 600;
font-size: 20px;
letter-spacing: 0.27px;
line-height: 24px;
"
>
Hey {userUsername},
</p>
<p>Your {instanceName} password has been changed.</p>
<p>
If this wasn't done by you, please immediately reset the
password to your {instanceName} account.
</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<title>Password Reset Request for {instanceName}</title>
<style>
* {
font-size: 16px;
line-height: 24px;
font-family: Arial, Helvetica, sans-serif;
}
p {
color: white;
}
.ExternalClass {
width: 100%;
}
</style>
</head>
<body>
<div style="background-color: #202225;">
<img
src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
alt="Branding"
style="
width: 100%;
max-width: 200px;
margin: 0 auto;
display: block;
padding: 20px;
"
/>
<div
style="
width: 100%;
max-width: 500px;
margin: 0 auto;
padding: 40px 50px;
background-color: #32353b;
border-radius: 5px;
"
>
<p
style="
font-weight: 600;
font-size: 20px;
letter-spacing: 0.27px;
line-height: 24px;
"
>
Hey {userUsername},
</p>
<p>
Your {instanceName} password can be reset by clicking the
button below. If you did not request a new password, please
ignore this email.
</p>
<div>
<div
style="
text-align: center;
justify-content: center;
padding-bottom: 10px;
"
>
<a
href="{passwordResetUrl}"
target="_blank"
style="
font-size: 15px;
border: none;
border-radius: 3px;
text-decoration: none;
color: white;
cursor: pointer;
padding: 15px 19px;
background-color: #ff5f00;
border-radius: 5px;
"
>Reset Password</a
>
</div>
<hr />
<div style="text-align: center">
<p>
Alternatively, you can directly paste this link into
your browser:
</p>
<a href="{passwordResetUrl}" target="_blank" style="word-wrap: break-word;"
>{passwordResetUrl}</a
>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<title>Phone Removed From {instanceName} Account</title>
<style>
* {
font-size: 16px;
line-height: 24px;
font-family: Arial, Helvetica, sans-serif;
}
p {
color: white;
}
.ExternalClass {
width: 100%;
}
</style>
</head>
<body>
<div style="background-color: #202225;">
<img
src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
alt="Branding"
style="
width: 100%;
max-width: 200px;
margin: 0 auto;
display: block;
padding: 20px;
"
/>
<div
style="
width: 100%;
max-width: 500px;
margin: 0 auto;
padding: 40px 50px;
background-color: #32353b;
border-radius: 5px;
"
>
<p
style="
font-weight: 600;
font-size: 20px;
letter-spacing: 0.27px;
line-height: 24px;
"
>
Hey {userUsername},
</p>
<p>
Your phone number ********{phoneNumber} was recently removed
from this account and added to a different {instanceName}
account.
</p>
<p>
Please note that your phone number can only be linked to one
{instanceName} account at a time.
</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<title>Verify Email Address for {instanceName}</title>
<style>
* {
font-size: 16px;
line-height: 24px;
font-family: Arial, Helvetica, sans-serif;
}
p {
color: white;
}
.ExternalClass {
width: 100%;
}
</style>
</head>
<body>
<div style="background-color: #202225;">
<img
src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
alt="Branding"
style="
width: 100%;
max-width: 200px;
margin: 0 auto;
display: block;
padding: 20px;
"
/>
<div
style="
width: 100%;
max-width: 500px;
margin: 0 auto;
padding: 40px 50px;
background-color: #32353b;
border-radius: 5px;
"
>
<p
style="
font-weight: 600;
font-size: 20px;
letter-spacing: 0.27px;
line-height: 24px;
"
>
Hey {userUsername},
</p>
<p>
Thanks for registering for an account on {instanceName}!
Before we get started, we just need to confirm that this is
you. Click below to verify your email address:
</p>
<div>
<div
style="
text-align: center;
justify-content: center;
padding-bottom: 10px;
"
>
<a
class="btn"
href="{emailVerificationUrl}"
target="_blank"
style="
font-size: 15px;
border: none;
border-radius: 3px;
text-decoration: none;
color: white;
cursor: pointer;
padding: 15px 19px;
background-color: #ff5f00;
border-radius: 5px;
"
>Verify Email</a
>
</div>
<hr />
<div style="text-align: center">
<p>
Alternatively, you can directly paste this link into
your browser:
</p>
<a href="{emailVerificationUrl}" target="_blank" style="word-wrap: break-word;"
>{emailVerificationUrl}</a
>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -16,5 +16,9 @@
"USERNAME_TOO_MANY_USERS": "Too many users have this username, please try another",
"GUESTS_DISABLED": "Guest users are disabled",
"TOO_MANY_REGISTRATIONS": "Too many registrations, please try again later"
},
"password_reset": {
"EMAIL_DOES_NOT_EXIST": "Email does not exist.",
"INVALID_TOKEN": "Invalid token."
}
}

File diff suppressed because it is too large Load Diff

6584
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,7 @@
"@types/node": "^18.7.20",
"@types/node-fetch": "^2.6.2",
"@types/node-os-utils": "^1.3.0",
"@types/nodemailer": "^6.4.7",
"@types/probe-image-size": "^7.2.0",
"@types/sharp": "^0.31.0",
"@types/ws": "^8.5.3",
@ -95,6 +96,7 @@
"node-2fa": "^2.0.3",
"node-fetch": "^2.6.7",
"node-os-utils": "^1.3.7",
"nodemailer": "^6.9.0",
"picocolors": "^1.0.0",
"probe-image-size": "^7.2.3",
"proxy-agent": "^5.0.0",
@ -113,6 +115,9 @@
},
"optionalDependencies": {
"erlpack": "^0.1.4",
"nodemailer-mailgun-transport": "^2.1.5",
"nodemailer-mailjet-transport": "github:n0script22/nodemailer-mailjet-transport",
"nodemailer-sendgrid-transport": "github:Maria-Golomb/nodemailer-sendgrid-transport",
"sqlite3": "^5.1.4"
}
}

View File

@ -16,28 +16,29 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "missing-native-js-functions";
import { Server, ServerOptions } from "lambert-server";
import { Authentication, CORS } from "./middlewares/";
import {
Config,
Email,
initDatabase,
initEvent,
JSONReplacer,
registerRoutes,
Sentry,
WebAuthn,
} from "@fosscord/util";
import { ErrorHandler } from "./middlewares/ErrorHandler";
import { BodyParser } from "./middlewares/BodyParser";
import { Router, Request, Response } from "express";
import { Request, Response, Router } from "express";
import { Server, ServerOptions } from "lambert-server";
import "missing-native-js-functions";
import morgan from "morgan";
import path from "path";
import { red } from "picocolors";
import { Authentication, CORS } from "./middlewares/";
import { BodyParser } from "./middlewares/BodyParser";
import { ErrorHandler } from "./middlewares/ErrorHandler";
import { initRateLimits } from "./middlewares/RateLimit";
import TestClient from "./middlewares/TestClient";
import { initTranslation } from "./middlewares/Translation";
import morgan from "morgan";
import { initInstance } from "./util/handlers/Instance";
import { registerRoutes } from "@fosscord/util";
import { red } from "picocolors";
export type FosscordServerOptions = ServerOptions;
@ -63,6 +64,7 @@ export class FosscordServer extends Server {
await initDatabase();
await Config.init();
await initEvent();
await Email.init();
await initInstance();
await Sentry.init(this.app);
WebAuthn.init();

View File

@ -16,10 +16,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { NextFunction, Request, Response } from "express";
import { HTTPError } from "lambert-server";
import { checkToken, Config, Rights } from "@fosscord/util";
import * as Sentry from "@sentry/node";
import { NextFunction, Request, Response } from "express";
import { HTTPError } from "lambert-server";
export const NO_AUTHORIZATION_ROUTES = [
// Authentication routes
@ -28,6 +28,9 @@ export const NO_AUTHORIZATION_ROUTES = [
"/auth/location-metadata",
"/auth/mfa/totp",
"/auth/mfa/webauthn",
"/auth/verify",
"/auth/forgot",
"/auth/reset",
// Routes with a seperate auth system
"/webhooks/",
// Public information endpoints

View File

@ -0,0 +1,92 @@
import { getIpAdress, route, verifyCaptcha } from "@fosscord/api";
import {
Config,
Email,
FieldErrors,
ForgotPasswordSchema,
User,
} from "@fosscord/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router = Router();
router.post(
"/",
route({ body: "ForgotPasswordSchema" }),
async (req: Request, res: Response) => {
const { login, captcha_key } = req.body as ForgotPasswordSchema;
const config = Config.get();
if (
config.password_reset.requireCaptcha &&
config.security.captcha.enabled
) {
const { sitekey, service } = config.security.captcha;
if (!captcha_key) {
return res.status(400).json({
captcha_key: ["captcha-required"],
captcha_sitekey: sitekey,
captcha_service: service,
});
}
const ip = getIpAdress(req);
const verify = await verifyCaptcha(captcha_key, ip);
if (!verify.success) {
return res.status(400).json({
captcha_key: verify["error-codes"],
captcha_sitekey: sitekey,
captcha_service: service,
});
}
}
const user = await User.findOneOrFail({
where: [{ phone: login }, { email: login }],
select: ["username", "id", "disabled", "deleted", "email"],
relations: ["security_keys"],
}).catch(() => {
throw FieldErrors({
login: {
message: req.t("auth:password_reset.EMAIL_DOES_NOT_EXIST"),
code: "EMAIL_DOES_NOT_EXIST",
},
});
});
if (!user.email)
throw FieldErrors({
login: {
message:
"This account does not have an email address associated with it.",
code: "NO_EMAIL",
},
});
if (user.deleted)
return res.status(400).json({
message: "This account is scheduled for deletion.",
code: 20011,
});
if (user.disabled)
return res.status(400).json({
message: req.t("auth:login.ACCOUNT_DISABLED"),
code: 20013,
});
return await Email.sendResetPassword(user, user.email)
.then(() => {
return res.sendStatus(204);
})
.catch((e) => {
console.error(
`Failed to send password reset email to ${user.username}#${user.discriminator}: ${e}`,
);
throw new HTTPError("Failed to send password reset email", 500);
});
},
);
export default router;

View File

@ -77,6 +77,7 @@ router.post(
"mfa_enabled",
"webauthn_enabled",
"security_keys",
"verified",
],
relations: ["security_keys"],
}).catch(() => {
@ -102,6 +103,17 @@ router.post(
});
}
// return an error for unverified accounts if verification is required
if (config.login.requireVerification && !user.verified) {
throw FieldErrors({
login: {
code: "ACCOUNT_LOGIN_VERIFICATION_EMAIL",
message:
"Email verification is required, please check your email.",
},
});
}
if (user.mfa_enabled && !user.webauthn_enabled) {
// TODO: This is not a discord.com ticket. I'm not sure what it is but I'm lazy
const ticket = crypto.randomBytes(40).toString("hex");

View File

@ -278,6 +278,17 @@ router.post(
await Invite.joinGuild(user.id, body.invite);
}
// return an error for unverified accounts if verification is required
if (Config.get().login.requireVerification && !user.verified) {
throw FieldErrors({
login: {
code: "ACCOUNT_LOGIN_VERIFICATION_EMAIL",
message:
"Email verification is required, please check your email.",
},
});
}
return res.json({ token: await generateToken(user.id) });
},
);

View File

@ -0,0 +1,56 @@
import { route } from "@fosscord/api";
import {
checkToken,
Config,
Email,
FieldErrors,
generateToken,
PasswordResetSchema,
User,
} from "@fosscord/util";
import bcrypt from "bcrypt";
import { Request, Response, Router } from "express";
const router = Router();
router.post(
"/",
route({ body: "PasswordResetSchema" }),
async (req: Request, res: Response) => {
const { password, token } = req.body as PasswordResetSchema;
const { jwtSecret } = Config.get().security;
let user;
try {
const userTokenData = await checkToken(token, jwtSecret, true);
user = userTokenData.user;
} catch {
throw FieldErrors({
password: {
message: req.t("auth:password_reset.INVALID_TOKEN"),
code: "INVALID_TOKEN",
},
});
}
// the salt is saved in the password refer to bcrypt docs
const hash = await bcrypt.hash(password, 12);
const data = {
data: {
hash,
valid_tokens_since: new Date(),
},
};
await User.update({ id: user.id }, data);
// come on, the user has to have an email to reset their password in the first place
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await Email.sendPasswordChanged(user, user.email!);
res.json({ token: await generateToken(user.id) });
},
);
export default router;

View File

@ -0,0 +1,93 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress, route, verifyCaptcha } from "@fosscord/api";
import {
checkToken,
Config,
FieldErrors,
generateToken,
User,
} from "@fosscord/util";
import { Request, Response, Router } from "express";
const router = Router();
async function getToken(user: User) {
const token = await generateToken(user.id);
// Notice this will have a different token structure, than discord
// Discord header is just the user id as string, which is not possible with npm-jsonwebtoken package
// https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png
return { token };
}
router.post(
"/",
route({ body: "VerifyEmailSchema" }),
async (req: Request, res: Response) => {
const { captcha_key, token } = req.body;
const config = Config.get();
if (config.register.requireCaptcha) {
const { sitekey, service } = config.security.captcha;
if (!captcha_key) {
return res.status(400).json({
captcha_key: ["captcha-required"],
captcha_sitekey: sitekey,
captcha_service: service,
});
}
const ip = getIpAdress(req);
const verify = await verifyCaptcha(captcha_key, ip);
if (!verify.success) {
return res.status(400).json({
captcha_key: verify["error-codes"],
captcha_sitekey: sitekey,
captcha_service: service,
});
}
}
const { jwtSecret } = Config.get().security;
let user;
try {
const userTokenData = await checkToken(token, jwtSecret, true);
user = userTokenData.user;
} catch {
throw FieldErrors({
password: {
message: req.t("auth:password_reset.INVALID_TOKEN"),
code: "INVALID_TOKEN",
},
});
}
if (user.verified) return res.json(await getToken(user));
await User.update({ id: user.id }, { verified: true });
return res.json(await getToken(user));
},
);
export default router;

View File

@ -0,0 +1,52 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { route } from "@fosscord/api";
import { Email, User } from "@fosscord/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router = Router();
router.post(
"/",
route({ right: "RESEND_VERIFICATION_EMAIL" }),
async (req: Request, res: Response) => {
const user = await User.findOneOrFail({
where: { id: req.user_id },
select: ["username", "email"],
});
if (!user.email) {
// TODO: whats the proper error response for this?
throw new HTTPError("User does not have an email address", 400);
}
await Email.sendVerifyEmail(user, user.email)
.then(() => {
return res.sendStatus(204);
})
.catch((e) => {
console.error(
`Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`,
);
throw new HTTPError("Failed to send verification email", 500);
});
},
);
export default router;

View File

@ -21,6 +21,7 @@ import {
CdnConfiguration,
ClientConfiguration,
DefaultsConfiguration,
EmailConfiguration,
EndpointConfiguration,
ExternalTokensConfiguration,
GeneralConfiguration,
@ -30,6 +31,7 @@ import {
LimitsConfiguration,
LoginConfiguration,
MetricsConfiguration,
PasswordResetConfiguration,
RabbitMQConfiguration,
RegionConfiguration,
RegisterConfiguration,
@ -58,4 +60,7 @@ export class ConfigValue {
sentry: SentryConfiguration = new SentryConfiguration();
defaults: DefaultsConfiguration = new DefaultsConfiguration();
external: ExternalTokensConfiguration = new ExternalTokensConfiguration();
email: EmailConfiguration = new EmailConfiguration();
password_reset: PasswordResetConfiguration =
new PasswordResetConfiguration();
}

View File

@ -0,0 +1,32 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
MailGunConfiguration,
MailJetConfiguration,
SMTPConfiguration,
} from "./subconfigurations/email";
import { SendGridConfiguration } from "./subconfigurations/email/SendGrid";
export class EmailConfiguration {
provider: string | null = null;
smtp: SMTPConfiguration = new SMTPConfiguration();
mailgun: MailGunConfiguration = new MailGunConfiguration();
mailjet: MailJetConfiguration = new MailJetConfiguration();
sendgrid: SendGridConfiguration = new SendGridConfiguration();
}

View File

@ -18,4 +18,5 @@
export class LoginConfiguration {
requireCaptcha: boolean = false;
requireVerification: boolean = false;
}

View File

@ -0,0 +1,21 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export class PasswordResetConfiguration {
requireCaptcha: boolean = false;
}

View File

@ -18,12 +18,13 @@
import {
DateOfBirthConfiguration,
EmailConfiguration,
PasswordConfiguration,
RegistrationEmailConfiguration,
} from ".";
export class RegisterConfiguration {
email: EmailConfiguration = new EmailConfiguration();
email: RegistrationEmailConfiguration =
new RegistrationEmailConfiguration();
dateOfBirth: DateOfBirthConfiguration = new DateOfBirthConfiguration();
password: PasswordConfiguration = new PasswordConfiguration();
disabled: boolean = false;
@ -34,5 +35,5 @@ export class RegisterConfiguration {
allowMultipleAccounts: boolean = true;
blockProxies: boolean = true;
incrementingDiscriminators: boolean = false; // random otherwise
defaultRights: string = "312119568366592"; // See `npm run generate:rights`
defaultRights: string = "875069521787904"; // See `npm run generate:rights`
}

View File

@ -20,6 +20,7 @@ export * from "./ApiConfiguration";
export * from "./CdnConfiguration";
export * from "./ClientConfiguration";
export * from "./DefaultsConfiguration";
export * from "./EmailConfiguration";
export * from "./EndpointConfiguration";
export * from "./ExternalTokensConfiguration";
export * from "./GeneralConfiguration";
@ -29,10 +30,11 @@ export * from "./KafkaConfiguration";
export * from "./LimitConfigurations";
export * from "./LoginConfiguration";
export * from "./MetricsConfiguration";
export * from "./PasswordResetConfiguration";
export * from "./RabbitMQConfiguration";
export * from "./RegionConfiguration";
export * from "./RegisterConfiguration";
export * from "./SecurityConfiguration";
export * from "./SentryConfiguration";
export * from "./TemplateConfiguration";
export * from "./subconfigurations";
export * from "./TemplateConfiguration";

View File

@ -0,0 +1,22 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export class MailGunConfiguration {
apiKey: string | null = null;
domain: string | null = null;
}

View File

@ -0,0 +1,22 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export class MailJetConfiguration {
apiKey: string | null = null;
apiSecret: string | null = null;
}

View File

@ -0,0 +1,25 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export class SMTPConfiguration {
host: string | null = null;
port: number | null = null;
secure: boolean | null = null;
username: string | null = null;
password: string | null = null;
}

View File

@ -0,0 +1,21 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export class SendGridConfiguration {
apiKey: string | null = null;
}

View File

@ -0,0 +1,21 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./MailGun";
export * from "./MailJet";
export * from "./SMTP";

View File

@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export class EmailConfiguration {
export class RegistrationEmailConfiguration {
required: boolean = false;
allowlist: boolean = false;
blocklist: boolean = true;

View File

@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Request } from "express";
import {
Column,
Entity,
@ -24,16 +25,22 @@ import {
OneToMany,
OneToOne,
} from "typeorm";
import { BaseClass } from "./BaseClass";
import {
adjustEmail,
Config,
Email,
FieldErrors,
Snowflake,
trimSpecial,
} from "..";
import { BitField } from "../util/BitField";
import { Relationship } from "./Relationship";
import { BaseClass } from "./BaseClass";
import { ConnectedAccount } from "./ConnectedAccount";
import { Member } from "./Member";
import { UserSettings } from "./UserSettings";
import { Session } from "./Session";
import { Config, FieldErrors, Snowflake, trimSpecial, adjustEmail } from "..";
import { Request } from "express";
import { Relationship } from "./Relationship";
import { SecurityKey } from "./SecurityKey";
import { Session } from "./Session";
import { UserSettings } from "./UserSettings";
export enum PublicUserEnum {
username,
@ -384,6 +391,15 @@ export class User extends BaseClass {
user.validate();
await Promise.all([user.save(), settings.save()]);
// send verification email if users aren't verified by default and we have an email
if (!Config.get().defaults.user.verified && email) {
await Email.sendVerifyEmail(user, email).catch((e) => {
console.error(
`Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`,
);
});
}
setImmediate(async () => {
if (Config.get().guild.autoJoin.enabled) {
for (const guild of Config.get().guild.autoJoin.guilds || []) {

View File

@ -0,0 +1,22 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export interface ForgotPasswordSchema {
login: string;
captcha_key?: string;
}

View File

@ -0,0 +1,22 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export interface PasswordResetSchema {
password: string;
token: string;
}

View File

@ -0,0 +1,22 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export interface VerifyEmailSchema {
captcha_key?: string | null;
token: string;
}

View File

@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./AckBulkSchema";
export * from "./ActivitySchema";
export * from "./ApplicationAuthorizeSchema";
export * from "./ApplicationCreateSchema";
@ -32,6 +33,7 @@ export * from "./CodesVerificationSchema";
export * from "./DmChannelCreateSchema";
export * from "./EmojiCreateSchema";
export * from "./EmojiModifySchema";
export * from "./ForgotPasswordSchema";
export * from "./GatewayPayloadSchema";
export * from "./GuildCreateSchema";
export * from "./GuildTemplateCreateSchema";
@ -45,8 +47,10 @@ export * from "./MemberChangeProfileSchema";
export * from "./MemberChangeSchema";
export * from "./MessageAcknowledgeSchema";
export * from "./MessageCreateSchema";
export * from "./MessageEditSchema";
export * from "./MfaCodesSchema";
export * from "./ModifyGuildStickerSchema";
export * from "./PasswordResetSchema";
export * from "./PurgeSchema";
export * from "./RegisterSchema";
export * from "./RelationshipPostSchema";
@ -69,22 +73,6 @@ export * from "./VanityUrlSchema";
export * from "./VoiceIdentifySchema";
export * from "./VoiceStateUpdateSchema";
export * from "./VoiceVideoSchema";
export * from "./IdentifySchema";
export * from "./ActivitySchema";
export * from "./LazyRequestSchema";
export * from "./GuildUpdateSchema";
export * from "./ChannelPermissionOverwriteSchema";
export * from "./UserGuildSettingsSchema";
export * from "./GatewayPayloadSchema";
export * from "./RolePositionUpdateSchema";
export * from "./ChannelReorderSchema";
export * from "./UserSettingsSchema";
export * from "./BotModifySchema";
export * from "./ApplicationModifySchema";
export * from "./ApplicationCreateSchema";
export * from "./ApplicationAuthorizeSchema";
export * from "./AckBulkSchema";
export * from "./WebAuthnSchema";
export * from "./WebhookCreateSchema";
export * from "./WidgetModifySchema";
export * from "./MessageEditSchema";

View File

@ -1,45 +0,0 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export function adjustEmail(email?: string): string | undefined {
if (!email) return email;
// body parser already checked if it is a valid email
const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
if (!parts || parts.length < 5) return undefined;
return email;
// // TODO: The below code doesn't actually do anything.
// const domain = parts[5];
// const user = parts[1];
// // TODO: check accounts with uncommon email domains
// if (domain === "gmail.com" || domain === "googlemail.com") {
// // replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator
// const v = user.replace(/[.]|(\+.*)/g, "") + "@gmail.com";
// }
// if (domain === "google.com") {
// // replace .dots and +alternatives -> Google Staff GMail Dot Trick
// const v = user.replace(/[.]|(\+.*)/g, "") + "@google.com";
// }
// return email;
}

View File

@ -93,6 +93,7 @@ export class Rights extends BitField {
EDIT_FLAGS: BitFlag(46), // can set others' flags
MANAGE_GROUPS: BitFlag(47), // can manage others' groups
VIEW_SERVER_STATS: BitFlag(48), // added per @chrischrome's request — can view server stats)
RESEND_VERIFICATION_EMAIL: BitFlag(49), // can resend verification emails (/auth/verify/resend)
};
any(permission: RightResolvable, checkOperator = true) {

View File

@ -27,9 +27,43 @@ export type UserTokenData = {
decoded: { id: string; iat: number };
};
async function checkEmailToken(
decoded: jwt.JwtPayload,
): Promise<UserTokenData> {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (res, rej) => {
if (!decoded.iat) return rej("Invalid Token"); // will never happen, just for typings.
const user = await User.findOne({
where: {
email: decoded.email,
},
select: [
"email",
"id",
"verified",
"deleted",
"disabled",
"username",
"data",
],
});
if (!user) return rej("Invalid Token");
if (new Date().getTime() > decoded.iat * 1000 + 86400 * 1000)
return rej("Invalid Token");
// Using as here because we assert `id` and `iat` are in decoded.
// TS just doesn't want to assume its there, though.
return res({ decoded, user } as UserTokenData);
});
}
export function checkToken(
token: string,
jwtSecret: string,
isEmailVerification = false,
): Promise<UserTokenData> {
return new Promise((res, rej) => {
token = token.replace("Bot ", "");
@ -48,6 +82,8 @@ export function checkToken(
)
return rej("Invalid Token"); // will never happen, just for typings.
if (isEmailVerification) return res(checkEmailToken(decoded));
const user = await User.findOne({
where: { id: decoded.id },
select: ["data", "bot", "disabled", "deleted", "rights"],
@ -72,13 +108,13 @@ export function checkToken(
});
}
export async function generateToken(id: string) {
export async function generateToken(id: string, email?: string) {
const iat = Math.floor(Date.now() / 1000);
const algorithm = "HS256";
return new Promise((res, rej) => {
jwt.sign(
{ id: id, iat },
{ id, iat, email },
Config.get().security.jwtSecret,
{
algorithm,

View File

@ -0,0 +1,269 @@
/*
Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Fosscord and Fosscord Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import fs from "node:fs";
import path from "node:path";
import { SentMessageInfo, Transporter } from "nodemailer";
import { User } from "../../entities";
import { Config } from "../Config";
import { generateToken } from "../Token";
import MailGun from "./transports/MailGun";
import MailJet from "./transports/MailJet";
import SendGrid from "./transports/SendGrid";
import SMTP from "./transports/SMTP";
const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets");
export const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export function adjustEmail(email?: string): string | undefined {
if (!email) return email;
// body parser already checked if it is a valid email
const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
if (!parts || parts.length < 5) return undefined;
return email;
// // TODO: The below code doesn't actually do anything.
// const domain = parts[5];
// const user = parts[1];
// // TODO: check accounts with uncommon email domains
// if (domain === "gmail.com" || domain === "googlemail.com") {
// // replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator
// const v = user.replace(/[.]|(\+.*)/g, "") + "@gmail.com";
// }
// if (domain === "google.com") {
// // replace .dots and +alternatives -> Google Staff GMail Dot Trick
// const v = user.replace(/[.]|(\+.*)/g, "") + "@google.com";
// }
// return email;
}
const transporters: {
[key: string]: () => Promise<Transporter<unknown> | void>;
} = {
smtp: SMTP,
mailgun: MailGun,
mailjet: MailJet,
sendgrid: SendGrid,
};
export const Email: {
transporter: Transporter | null;
init: () => Promise<void>;
generateLink: (
type: "verify" | "reset",
id: string,
email: string,
) => Promise<string>;
sendVerifyEmail: (user: User, email: string) => Promise<SentMessageInfo>;
sendResetPassword: (user: User, email: string) => Promise<SentMessageInfo>;
sendPasswordChanged: (
user: User,
email: string,
) => Promise<SentMessageInfo>;
doReplacements: (
template: string,
user: User,
emailVerificationUrl?: string,
passwordResetUrl?: string,
ipInfo?: {
ip: string;
city: string;
region: string;
country_name: string;
},
) => string;
} = {
transporter: null,
init: async function () {
const { provider } = Config.get().email;
if (!provider) return;
const transporterFn = transporters[provider];
if (!transporterFn)
return console.error(`[Email] Invalid provider: ${provider}`);
console.log(`[Email] Initializing ${provider} transport...`);
const transporter = await transporterFn();
if (!transporter) return;
this.transporter = transporter;
console.log(`[Email] ${provider} transport initialized.`);
},
/**
* Replaces all placeholders in an email template with the correct values
*/
doReplacements: function (
template,
user,
emailVerificationUrl?,
passwordResetUrl?,
ipInfo?: {
ip: string;
city: string;
region: string;
country_name: string;
},
) {
const { instanceName } = Config.get().general;
const replacements = [
["{instanceName}", instanceName],
["{userUsername}", user.username],
["{userDiscriminator}", user.discriminator],
["{userId}", user.id],
["{phoneNumber}", user.phone?.slice(-4)],
["{userEmail}", user.email],
["{emailVerificationUrl}", emailVerificationUrl],
["{passwordResetUrl}", passwordResetUrl],
["{ipAddress}", ipInfo?.ip],
["{locationCity}", ipInfo?.city],
["{locationRegion}", ipInfo?.region],
["{locationCountryName}", ipInfo?.country_name],
];
// loop through all replacements and replace them in the template
for (const [key, value] of Object.values(replacements)) {
if (!value) continue;
template = template.replace(key as string, value);
}
return template;
},
/**
*
* @param id user id
* @param email user email
*/
generateLink: async function (type, id, email) {
const token = (await generateToken(id, email)) as string;
const instanceUrl =
Config.get().general.frontPage || "http://localhost:3001";
const link = `${instanceUrl}/${type}#token=${token}`;
return link;
},
/**
* Sends an email to the user with a link to verify their email address
*/
sendVerifyEmail: async function (user, email) {
if (!this.transporter) return;
// generate a verification link for the user
const link = await this.generateLink("verify", user.id, email);
// load the email template
const rawTemplate = fs.readFileSync(
path.join(
ASSET_FOLDER_PATH,
"email_templates",
"verify_email.html",
),
{ encoding: "utf-8" },
);
// replace email template placeholders
const html = this.doReplacements(rawTemplate, user, link);
// extract the title from the email template to use as the email subject
const subject = html.match(/<title>(.*)<\/title>/)?.[1] || "";
// construct the email
const message = {
from:
Config.get().general.correspondenceEmail || "noreply@localhost",
to: email,
subject,
html,
};
// send the email
return this.transporter.sendMail(message);
},
/**
* Sends an email to the user with a link to reset their password
*/
sendResetPassword: async function (user, email) {
if (!this.transporter) return;
// generate a password reset link for the user
const link = await this.generateLink("reset", user.id, email);
// load the email template
const rawTemplate = await fs.promises.readFile(
path.join(
ASSET_FOLDER_PATH,
"email_templates",
"password_reset_request.html",
),
{ encoding: "utf-8" },
);
// replace email template placeholders
const html = this.doReplacements(rawTemplate, user, undefined, link);
// extract the title from the email template to use as the email subject
const subject = html.match(/<title>(.*)<\/title>/)?.[1] || "";
// construct the email
const message = {
from:
Config.get().general.correspondenceEmail || "noreply@localhost",
to: email,
subject,
html,
};
// send the email
return this.transporter.sendMail(message);
},
/**
* Sends an email to the user notifying them that their password has been changed
*/
sendPasswordChanged: async function (user, email) {
if (!this.transporter) return;
// load the email template
const rawTemplate = await fs.promises.readFile(
path.join(
ASSET_FOLDER_PATH,
"email_templates",
"password_changed.html",
),
{ encoding: "utf-8" },
);
// replace email template placeholders
const html = this.doReplacements(rawTemplate, user);
// extract the title from the email template to use as the email subject
const subject = html.match(/<title>(.*)<\/title>/)?.[1] || "";
// construct the email
const message = {
from:
Config.get().general.correspondenceEmail || "noreply@localhost",
to: email,
subject,
html,
};
// send the email
return this.transporter.sendMail(message);
},
};

View File

@ -0,0 +1,36 @@
import { Config } from "@fosscord/util";
import nodemailer from "nodemailer";
export default async function () {
// get configuration
const { apiKey, domain } = Config.get().email.mailgun;
// ensure all required configuration values are set
if (!apiKey || !domain)
return console.error(
"[Email] Mailgun has not been configured correctly.",
);
let mg;
try {
// try to import the transporter package
mg = require("nodemailer-mailgun-transport");
} catch {
// if the package is not installed, log an error and return void so we don't set the transporter
console.error(
"[Email] Mailgun transport is not installed. Please run `npm install nodemailer-mailgun-transport --save-optional` to install it.",
);
return;
}
// create the transporter configuration object
const auth = {
auth: {
api_key: apiKey,
domain: domain,
},
};
// create the transporter and return it
return nodemailer.createTransport(mg(auth));
}

View File

@ -0,0 +1,36 @@
import { Config } from "@fosscord/util";
import nodemailer from "nodemailer";
export default async function () {
// get configuration
const { apiKey, apiSecret } = Config.get().email.mailjet;
// ensure all required configuration values are set
if (!apiKey || !apiSecret)
return console.error(
"[Email] Mailjet has not been configured correctly.",
);
let mj;
try {
// try to import the transporter package
mj = require("nodemailer-mailjet-transport");
} catch {
// if the package is not installed, log an error and return void so we don't set the transporter
console.error(
"[Email] Mailjet transport is not installed. Please run `npm install n0script22/nodemailer-mailjet-transport --save-optional` to install it.",
);
return;
}
// create the transporter configuration object
const auth = {
auth: {
apiKey: apiKey,
apiSecret: apiSecret,
},
};
// create the transporter and return it
return nodemailer.createTransport(mj(auth));
}

View File

@ -0,0 +1,38 @@
import { Config } from "@fosscord/util";
import nodemailer from "nodemailer";
export default async function () {
// get configuration
const { host, port, secure, username, password } = Config.get().email.smtp;
// ensure all required configuration values are set
if (!host || !port || secure === null || !username || !password)
return console.error("[Email] SMTP has not been configured correctly.");
if (!Config.get().general.correspondenceEmail)
return console.error(
"[Email] Correspondence email has not been configured! This is used as the sender email address.",
);
// construct the transporter
const transporter = nodemailer.createTransport({
host,
port,
secure,
auth: {
user: username,
pass: password,
},
});
// verify connection configuration
const verified = await transporter.verify().catch((err) => {
console.error("[Email] SMTP verification failed:", err);
return;
});
// if verification failed, return void and don't set transporter
if (!verified) return;
return transporter;
}

View File

@ -0,0 +1,35 @@
import { Config } from "@fosscord/util";
import nodemailer from "nodemailer";
export default async function () {
// get configuration
const { apiKey } = Config.get().email.sendgrid;
// ensure all required configuration values are set
if (!apiKey)
return console.error(
"[Email] SendGrid has not been configured correctly.",
);
let sg;
try {
// try to import the transporter package
sg = require("nodemailer-sendgrid-transport");
} catch {
// if the package is not installed, log an error and return void so we don't set the transporter
console.error(
"[Email] SendGrid transport is not installed. Please run `npm install Maria-Golomb/nodemailer-sendgrid-transport --save-optional` to install it.",
);
return;
}
// create the transporter configuration object
const auth = {
auth: {
api_key: apiKey,
},
};
// create the transporter and return it
return nodemailer.createTransport(sg(auth));
}

View File

@ -0,0 +1 @@
export * from "./SMTP";

View File

@ -17,27 +17,27 @@
*/
export * from "./ApiError";
export * from "./Array";
export * from "./BitField";
export * from "./Token";
//export * from "./Categories";
export * from "./cdn";
export * from "./Config";
export * from "./Constants";
export * from "./Database";
export * from "./Email";
export * from "./email";
export * from "./Event";
export * from "./FieldError";
export * from "./Intents";
export * from "./InvisibleCharacters";
export * from "./JSON";
export * from "./MessageFlags";
export * from "./Permissions";
export * from "./RabbitMQ";
export * from "./Regex";
export * from "./Rights";
export * from "./Sentry";
export * from "./Snowflake";
export * from "./String";
export * from "./Array";
export * from "./Token";
export * from "./TraverseDirectory";
export * from "./InvisibleCharacters";
export * from "./Sentry";
export * from "./WebAuthn";
export * from "./JSON";