diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts
new file mode 100644
index 00000000..eae938eb
--- /dev/null
+++ b/src/api/routes/auth/verify/index.ts
@@ -0,0 +1,45 @@
+import { route, verifyCaptcha } from "@fosscord/api";
+import { Config, FieldErrors, verifyToken } from "@fosscord/util";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+const router = Router();
+
+router.post(
+ "/",
+ route({ body: "VerifyEmailSchema" }),
+ async (req: Request, res: Response) => {
+ const { captcha_key, token } = req.body;
+
+ if (captcha_key) {
+ const { sitekey, service } = Config.get().security.captcha;
+ const verify = await verifyCaptcha(captcha_key);
+ if (!verify.success) {
+ return res.status(400).json({
+ captcha_key: verify["error-codes"],
+ captcha_sitekey: sitekey,
+ captcha_service: service,
+ });
+ }
+ }
+
+ try {
+ const { jwtSecret } = Config.get().security;
+
+ const { decoded, user } = await verifyToken(token, jwtSecret);
+ // toksn should last for 24 hours from the time they were issued
+ if (decoded.exp < Date.now() / 1000) {
+ throw FieldErrors({
+ token: {
+ code: "TOKEN_INVALID",
+ message: "Invalid token", // TODO: add translation
+ },
+ });
+ }
+ user.verified = true;
+ } catch (error: any) {
+ throw new HTTPError(error?.toString(), 400);
+ }
+ },
+);
+
+export default router;
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index 7b67c2ac..f39fc19b 100644
--- a/src/util/entities/User.ts
+++ b/src/util/entities/User.ts
@@ -31,7 +31,7 @@ import { ConnectedAccount } from "./ConnectedAccount";
import { Member } from "./Member";
import { UserSettings } from "./UserSettings";
import { Session } from "./Session";
-import { Config, FieldErrors, Snowflake, trimSpecial, adjustEmail } from "..";
+import { Config, FieldErrors, Snowflake, trimSpecial, adjustEmail, Email, generateToken } from "..";
import { Request } from "express";
import { SecurityKey } from "./SecurityKey";
@@ -383,6 +383,30 @@ export class User extends BaseClass {
user.validate();
await Promise.all([user.save(), settings.save()]);
+ // send verification email
+ if (Email.transporter && email) {
+ const token = (await generateToken(user.id, email)) as string;
+ const link = `http://localhost:3001/verify#token=${token}`;
+ const message = {
+ from:
+ Config.get().general.correspondenceEmail ||
+ "noreply@localhost",
+ to: email,
+ subject: `Verify Email Address for ${
+ Config.get().general.instanceName
+ }`,
+ html: `Please verify your email address by clicking the following link: <a href="${link}">Verify Email</a>`,
+ };
+
+ await Email.transporter
+ .sendMail(message)
+ .then((info) => {
+ console.log("Message sent: %s", info.messageId);
+ })
+ .catch((e) => {
+ console.error(`Failed to send email to ${email}: ${e}`);
+ });
+ }
setImmediate(async () => {
if (Config.get().guild.autoJoin.enabled) {
diff --git a/src/util/schemas/VerifyEmailSchema.ts b/src/util/schemas/VerifyEmailSchema.ts
new file mode 100644
index 00000000..ad170e84
--- /dev/null
+++ b/src/util/schemas/VerifyEmailSchema.ts
@@ -0,0 +1,4 @@
+export interface VerifyEmailSchema {
+ captcha_key: string | null;
+ token: string;
+}
diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts
index ca81eaaa..b3ebcc07 100644
--- a/src/util/util/Token.ts
+++ b/src/util/util/Token.ts
@@ -72,13 +72,34 @@ export function checkToken(
});
}
-export async function generateToken(id: string) {
+export function verifyToken(
+ token: string,
+ jwtSecret: string,
+): Promise<{ decoded: any; user: User }> {
+ return new Promise((res, rej) => {
+ jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded: any) => {
+ if (err || !decoded) return rej("Invalid Token");
+
+ const user = await User.findOne({
+ where: { id: decoded.id },
+ select: ["data", "bot", "disabled", "deleted", "rights"],
+ });
+ if (!user) return rej("Invalid Token");
+ if (user.disabled) return rej("User disabled");
+ if (user.deleted) return rej("User not found");
+
+ return res({ decoded, user });
+ });
+ });
+}
+
+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: id, email: email, iat },
Config.get().security.jwtSecret,
{
algorithm,
|