try {
const timestamp = new Date();
diff --git a/server/controllers/EmailVerificationController.ts b/server/controllers/EmailVerificationController.ts
index d389644..ecb89a0 100644
--- a/server/controllers/EmailVerificationController.ts
+++ b/server/controllers/EmailVerificationController.ts
@@ -1,60 +1,108 @@
-import { Op } from 'sequelize';
-import { EmailVerification } from '../models';
-import { MailController } from './MailController';
+import { Op } from "sequelize";
+import { EmailVerification } from "../models";
+import { MailController } from "./MailController";
+import crypto from "crypto";
export class EmailVerificationController {
static generateCode() {
return Math.floor(Math.random() * (999999 - 100000 + 1)) + 100000;
}
+ static generateToken() {
+ return crypto.randomBytes(32).toString("hex");
+ }
+
public async createVerification(uuid: string, email: string) {
const verifyExpiry = new Date();
verifyExpiry.setDate(verifyExpiry.getDate() + 1);
const verification = await EmailVerification.create({
user_id: uuid,
- email,
code: EmailVerificationController.generateCode(),
+ token: EmailVerificationController.generateToken(),
expires_at: verifyExpiry,
});
- return verification.get('code');
+ return {
+ code: verification.get("code"),
+ token: verification.get("token"),
+ };
+ }
+
+ public async checkVerificationCode(user_id: string, code: number) {
+ const now = new Date();
+ const foundVerification = await EmailVerification.findOne({
+ where: {
+ [Op.and]: [{ user_id }, { code }],
+ },
+ });
+ if (!foundVerification || foundVerification.expires_at < now) {
+ return null;
+ }
+
+ const response = {
+ uuid: foundVerification.get("user_id"),
+ };
+
+ await foundVerification.destroy();
+ return response;
}
- public async checkVerification(email: string, code: number) {
+ public async checkVerificationToken(token: string) {
const now = new Date();
const foundVerification = await EmailVerification.findOne({
where: {
- [Op.and]: [
- { email },
- { code },
- ],
+ token,
},
});
- if (!foundVerification || foundVerification.expires_at < now || foundVerification.code !== code) {
+
+ if (!foundVerification) {
return null;
}
+ // Token was found, but it's expired
+ const isExpired = foundVerification.expires_at < now;
+
const response = {
- uuid: foundVerification.get('user_id'),
- email: foundVerification.get('email'),
+ uuid: foundVerification.get("user_id"),
+ expired: isExpired,
};
await foundVerification.destroy();
return response;
}
- public async sendEmailVerificationMessage(mailSender: MailController, email: string, code: number) {
+ public async sendEmailVerificationMessage(
+ mailSender: MailController,
+ email: string,
+ code: number | null,
+ token: string,
+ ) {
if (!mailSender || !mailSender.isReady()) {
- throw new Error('No mail sender available to issue email verification!');
+ throw new Error("No mail sender available to issue email verification!");
+ }
+
+ let instructions = "";
+ if (code) {
+ instructions = `
+ Thank you for registering with LibreOne! To complete your registration, please use the verification code below or click the link to verify your email address:
+ ${code}
+ `;
+ } else {
+ instructions = `
+ Thank you for registering with LibreOne! To finish verifying your email address, please click the link below:
+ `;
}
const emailRes = await mailSender.send({
destination: { to: [email] },
- subject: `LibreOne Verification Code: ${code}`,
+ subject: `Your Verification ${code ? "Code" : "Link for LibreOne"}${code ? `: ${code}` : ""}`,
htmlContent: `
- Hello there,
- Please verify your email address by entering this code:
- ${code}
+ Hi there,
+ ${instructions}
+
+ Verify Email Address
+
+ This ${code ? `code and link` : `link`} will expire in 24 hours.
If this wasn't you, you can safely ignore this email.
Best,
The LibreTexts Team
diff --git a/server/models/EmailVerification.ts b/server/models/EmailVerification.ts
index 645678e..bc138bd 100644
--- a/server/models/EmailVerification.ts
+++ b/server/models/EmailVerification.ts
@@ -20,14 +20,14 @@ export class EmailVerification extends Model {
@Column(DataType.STRING)
declare user_id: string;
- @AllowNull(false)
- @Column(DataType.STRING)
- declare email: string;
-
@AllowNull(false)
@Column(DataType.INTEGER)
declare code: number;
+ @AllowNull(false)
+ @Column(DataType.STRING)
+ declare token: string;
+
@AllowNull(false)
@Column(DataType.DATE)
declare expires_at: Date;
diff --git a/server/models/User.ts b/server/models/User.ts
index 8e7cc96..5810cd3 100644
--- a/server/models/User.ts
+++ b/server/models/User.ts
@@ -28,10 +28,11 @@ import { VerificationRequest } from './VerificationRequest';
import { Session } from './Session';
import { UserNote } from './UserNote';
import { UserLicenseEntitlement } from './UserLicenseEntitlement';
+import { EmailVerification } from './EmailVerification';
@DefaultScope(() => ({
attributes: {
- exclude: ['password', 'ip_address', 'email_verify_code'],
+ exclude: ['password', 'ip_address', 'email_verifications'],
},
}))
@Table({
@@ -95,6 +96,9 @@ export class User extends Model {
@Column(DataType.DATE)
declare disabled_date: Date | null;
+ @Column(DataType.BOOLEAN)
+ declare email_verified: boolean;
+
@Default(false)
@Column(DataType.BOOLEAN)
declare expired: boolean;
@@ -172,6 +176,9 @@ export class User extends Model {
@HasMany(() => AccessRequest)
access_requests?: Array;
+ @HasMany(() => EmailVerification)
+ email_verifications?: Array;
+
@HasMany(() => LoginEvent)
login_events?: Array;
diff --git a/server/routes/auth.ts b/server/routes/auth.ts
index a9e53f4..d2b2c2a 100644
--- a/server/routes/auth.ts
+++ b/server/routes/auth.ts
@@ -27,9 +27,19 @@ authRouter.route('/register').post(
catchInternal((req, res) => controller.register(req, res)),
);
-authRouter.route('/verify-email').post(
- validate(AuthValidator.verifyEmailSchema, 'body'),
- catchInternal((req, res) => controller.verifyRegistrationEmail(req, res)),
+authRouter.route('/verify-email-code').post(
+ validate(AuthValidator.verifyEmailCodeSchema, 'body'),
+ catchInternal((req, res) => controller.verifyRegistrationEmailCode(req, res)),
+);
+
+authRouter.route('/verify-email-token').post(
+ validate(AuthValidator.verifyEmailTokenSchema, 'body'),
+ catchInternal((req, res) => controller.verifyRegistrationEmailToken(req, res)),
+);
+
+authRouter.route('/resend-verification-email').post(
+ validate(AuthValidator.resendVerificationEmailSchema, 'body'),
+ catchInternal((req, res) => controller.resendVerificationEmail(req, res)),
);
authRouter.route('/complete-registration').post(
diff --git a/server/tests/auth.spec.ts b/server/tests/auth.spec.ts
index 02c0ec9..71fdd94 100644
--- a/server/tests/auth.spec.ts
+++ b/server/tests/auth.spec.ts
@@ -92,7 +92,7 @@ describe('Authentication and Authorization', async () => {
);
const response = await request(server)
- .post('/api/v1/auth/verify-email')
+ .post('/api/v1/auth/verify-email-code')
.send({ email: 'info@libretexts.org', code: verifyCode });
expect(response.status).to.equal(200);
@@ -113,7 +113,7 @@ describe('Authentication and Authorization', async () => {
);
const response = await request(server)
- .post('/api/v1/auth/verify-email')
+ .post('/api/v1/auth/verify-email-code')
.send({ email: 'info@libretexts.org', code: 101101 });
expect(response.status).to.equal(400);
diff --git a/server/types/auth.ts b/server/types/auth.ts
index 1776b7d..21cdcef 100644
--- a/server/types/auth.ts
+++ b/server/types/auth.ts
@@ -15,11 +15,19 @@ export type RegisterBody = {
password: string;
};
-export type VerifyEmailBody = {
+export type VerifyEmailCodeBody = {
email: string;
code: number;
};
+export type VerifyEmailTokenBody = {
+ token: string;
+};
+
+export type ResendVerificationEmailBody = {
+ uuid: string;
+};
+
export type CreateUserFromExternalIdPBody = {
profileAttributes: CreateUserFromExternalIdPBodyProfileAttributes;
clientName: string;
diff --git a/server/validators/auth.ts b/server/validators/auth.ts
index 69fedfc..293d699 100644
--- a/server/validators/auth.ts
+++ b/server/validators/auth.ts
@@ -64,11 +64,19 @@ export const autoProvisionUserSchema = joi.object({
time_zone: timeZoneValidator.required(),
});
-export const verifyEmailSchema = joi.object({
+export const verifyEmailCodeSchema = joi.object({
email: joi.string().email().required(),
code: joi.number().integer().min(100000).max(999999).required(),
});
+export const verifyEmailTokenSchema = joi.object({
+ token: joi.string().length(64).required(),
+});
+
+export const resendVerificationEmailSchema = joi.object({
+ uuid: joi.string().uuid().required(),
+});
+
export const initLoginQuerySchema = joi.object({
redirectURI: joi.string().uri({ relativeOnly: true }),
redirectCASServiceURI: joi.string().uri(),