diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index ad1acb1a6..a7194308e 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,5 +1,5 @@ const PORT = process.env.PORT || 4000; -const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400'; +const EMAIL_TOKEN_LIFETIME = parseInt(process.env.EMAIL_TOKEN_LIFETIME! || '86400'); const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; @@ -24,7 +24,7 @@ const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!; const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!; const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!; -const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!; +const CLIENT_SLUG_VERCEL = process.env.CLIENT_SLUG_VERCEL!; const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com'; const POSTHOG_PROJECT_API_KEY = process.env.POSTHOG_PROJECT_API_KEY! || diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index 0040b583e..324be6c2e 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import crypto from 'crypto'; -import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config'; +import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, EMAIL_TOKEN_LIFETIME } from '../../config'; import { MembershipOrg, Organization, User, Token } from '../../models'; import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg'; import { checkEmailVerification } from '../../helpers/signup'; @@ -113,14 +113,14 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { if (!membershipOrg) { throw new Error('Failed to validate organization membership'); } - + invitee = await User.findOne({ email: inviteeEmail }).select('+publicKey'); if (invitee) { // case: invitee is an existing user - + inviteeMembershipOrg = await MembershipOrg.findOne({ user: invitee._id, organization: organizationId @@ -170,7 +170,8 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { { email: inviteeEmail, token, - createdAt: new Date() + createdAt: new Date(), + ttl: Math.floor(+new Date() / 1000) + EMAIL_TOKEN_LIFETIME // time in seconds, i.e unix }, { upsert: true, new: true } ); @@ -241,7 +242,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { message: 'Successfully verified email', user, }); - } + } if (!user) { // initialize user account diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index 0c5530f58..25529cd10 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -8,7 +8,7 @@ import { User, Token, BackupPrivateKey, LoginSRPDetail } from '../../models'; import { checkEmailVerification } from '../../helpers/signup'; import { createToken } from '../../helpers/auth'; import { sendMail } from '../../helpers/nodemailer'; -import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config'; +import { EMAIL_TOKEN_LIFETIME, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config'; import { BadRequestError } from '../../utils/errors'; /** @@ -39,7 +39,8 @@ export const emailPasswordReset = async (req: Request, res: Response) => { { email, token, - createdAt: new Date() + createdAt: new Date(), + ttl: Math.floor(+new Date() / 1000) + EMAIL_TOKEN_LIFETIME // time in seconds, i.e unix }, { upsert: true, new: true } ); diff --git a/backend/src/helpers/signup.ts b/backend/src/helpers/signup.ts index 1ef1219f8..95b67ba6c 100644 --- a/backend/src/helpers/signup.ts +++ b/backend/src/helpers/signup.ts @@ -7,6 +7,7 @@ import { createWorkspace } from './workspace'; import { addMemberships } from './membership'; import { OWNER, ADMIN, ACCEPTED } from '../variables'; import { sendMail } from '../helpers/nodemailer'; +import { EMAIL_TOKEN_LIFETIME } from '../config'; /** * Send magic link to verify email to [email] @@ -25,7 +26,8 @@ const sendEmailVerification = async ({ email }: { email: string }) => { { email, token, - createdAt: new Date() + createdAt: new Date(), + ttl: Math.floor(+new Date() / 1000) + EMAIL_TOKEN_LIFETIME // time in seconds, i.e unix }, { upsert: true, new: true } ); @@ -62,11 +64,20 @@ const checkEmailVerification = async ({ code: string; }) => { try { - const token = await Token.findOneAndDelete({ + const token = await Token.findOne({ email, token: code }); + if (token && Math.floor(Date.now() / 1000) > token.ttl) { + await Token.deleteOne({ + email, + token: code + }); + + throw new Error('Verification token has expired') + } + if (!token) throw new Error('Failed to find email verification token'); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/models/token.ts b/backend/src/models/token.ts index 9569aee0b..2da62d813 100644 --- a/backend/src/models/token.ts +++ b/backend/src/models/token.ts @@ -5,6 +5,7 @@ export interface IToken { email: string; token: string; createdAt: Date; + ttl: Number; } const tokenSchema = new Schema({ @@ -19,14 +20,13 @@ const tokenSchema = new Schema({ createdAt: { type: Date, default: Date.now + }, + ttl: { + type: Number, } }); -tokenSchema.index({ - createdAt: 1 -}, { - expireAfterSeconds: parseInt(EMAIL_TOKEN_LIFETIME) -}); +tokenSchema.index({ email: 1 }); const Token = model('Token', tokenSchema);