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

implement password reset

This commit is contained in:
Puyodead1 2023-02-24 01:54:10 -05:00
parent dc48a74373
commit 05453ec148
18 changed files with 2218 additions and 734 deletions

View File

@ -104,7 +104,7 @@
Alternatively, you can directly paste this link into
your browser:
</p>
<a href="{verifyUrl}" target="_blank">{verifyUrl}</a>
<a href="{verifyUrl}" target="_blank" style="word-wrap: break-word;">{verifyUrl}</a>
</div>
</div>
</div>

View File

@ -90,7 +90,7 @@
Alternatively, you can directly paste this link into
your browser:
</p>
<a href="{passwordResetUrl}" target="_blank"
<a href="{passwordResetUrl}" target="_blank" style="word-wrap: break-word;"
>{passwordResetUrl}</a
>
</div>

View File

@ -91,7 +91,7 @@
Alternatively, you can directly paste this link into
your browser:
</p>
<a href="{emailVerificationUrl}" target="_blank"
<a href="{emailVerificationUrl}" target="_blank" style="word-wrap: break-word;"
>{emailVerificationUrl}</a
>
</div>

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

View File

@ -29,6 +29,8 @@ export const NO_AUTHORIZATION_ROUTES = [
"/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

@ -0,0 +1,57 @@
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";
import { HTTPError } from "lambert-server";
const router = Router();
router.post(
"/",
route({ body: "PasswordResetSchema" }),
async (req: Request, res: Response) => {
const { password, token } = req.body as PasswordResetSchema;
try {
const { jwtSecret } = Config.get().security;
const { user } = await checkToken(token, jwtSecret, true);
// 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) });
} catch (e) {
if ((e as Error).toString() === "Invalid Token")
throw FieldErrors({
password: {
message: req.t("auth:password_reset.INVALID_TOKEN"),
code: "INVALID_TOKEN",
},
});
throw new HTTPError((e as Error).toString(), 400);
}
},
);
export default router;

View File

@ -36,7 +36,7 @@ router.post(
throw new HTTPError("User does not have an email address", 400);
}
await Email.sendVerificationEmail(user, user.email)
await Email.sendVerifyEmail(user, user.email)
.then(() => {
return res.sendStatus(204);
})

View File

@ -31,6 +31,7 @@ import {
LimitsConfiguration,
LoginConfiguration,
MetricsConfiguration,
PasswordResetConfiguration,
RabbitMQConfiguration,
RegionConfiguration,
RegisterConfiguration,
@ -60,4 +61,6 @@ export class ConfigValue {
defaults: DefaultsConfiguration = new DefaultsConfiguration();
external: ExternalTokensConfiguration = new ExternalTokensConfiguration();
email: EmailConfiguration = new EmailConfiguration();
password_reset: PasswordResetConfiguration =
new PasswordResetConfiguration();
}

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

@ -30,6 +30,7 @@ export * from "./KafkaConfiguration";
export * from "./LimitConfigurations";
export * from "./LoginConfiguration";
export * from "./MetricsConfiguration";
export * from "./PasswordResetConfiguration";
export * from "./RabbitMQConfiguration";
export * from "./RegionConfiguration";
export * from "./RegisterConfiguration";

View File

@ -393,7 +393,7 @@ export class User extends BaseClass {
// send verification email if users aren't verified by default and we have an email
if (!Config.get().defaults.user.verified && email) {
await Email.sendVerificationEmail(user, email).catch((e) => {
await Email.sendVerifyEmail(user, email).catch((e) => {
console.error(
`Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`,
);

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

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

@ -194,8 +194,14 @@ const transporters = {
export const Email: {
transporter: Transporter | null;
init: () => Promise<void>;
generateVerificationLink: (id: string, email: string) => Promise<string>;
sendVerificationEmail: (
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>;
@ -231,10 +237,10 @@ export const Email: {
* Replaces all placeholders in an email template with the correct values
*/
doReplacements: function (
template: string,
user: User,
emailVerificationUrl?: string,
passwordResetUrl?: string,
template,
user,
emailVerificationUrl?,
passwordResetUrl?,
ipInfo?: {
ip: string;
city: string;
@ -285,23 +291,22 @@ export const Email: {
*
* @param id user id
* @param email user email
* @returns a verification link for the user
*/
generateVerificationLink: async function (id: string, email: string) {
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}/verify#token=${token}`;
const link = `${instanceUrl}/${type}#token=${token}`;
return link;
},
sendVerificationEmail: async function (user: User, email: string) {
/**
* 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 verificationLink = await this.generateVerificationLink(
user.id,
email,
);
const link = await this.generateLink("verify", user.id, email);
// load the email template
const rawTemplate = fs.readFileSync(
@ -314,7 +319,78 @@ export const Email: {
);
// replace email template placeholders
const html = this.doReplacements(rawTemplate, user, verificationLink);
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 = fs.readFileSync(
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 = fs.readFileSync(
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] || "";

View File

@ -38,6 +38,15 @@ async function checkEmailToken(
where: {
email: decoded.email,
},
select: [
"email",
"id",
"verified",
"deleted",
"disabled",
"username",
"data",
],
});
if (!user) return rej("Invalid Token");