From 18f2aa5a0c217fb2af305c6a501468aed2c244d9 Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Fri, 2 May 2025 09:35:34 +0100 Subject: [PATCH] Merge pull request #25190 from overleaf/mj-survey-signup-limits [web] Add options to limit survey exposure based on signup date GitOrigin-RevId: 5719997339b5040d5cc42ffe7bee6d7b66bff12d --- .../app/src/Features/Survey/SurveyHandler.mjs | 20 +++++++++++ .../app/src/Features/Survey/SurveyManager.js | 36 +++++++++++++++++++ services/web/app/src/models/Survey.js | 6 ++++ .../web/test/acceptance/src/helpers/User.mjs | 9 ++++- 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/services/web/app/src/Features/Survey/SurveyHandler.mjs b/services/web/app/src/Features/Survey/SurveyHandler.mjs index 009bc12a95..7ce89594c6 100644 --- a/services/web/app/src/Features/Survey/SurveyHandler.mjs +++ b/services/web/app/src/Features/Survey/SurveyHandler.mjs @@ -4,6 +4,7 @@ import crypto from 'node:crypto' import SurveyCache from './SurveyCache.mjs' import SubscriptionLocator from '../Subscription/SubscriptionLocator.js' import { callbackify } from '@overleaf/promise-utils' +import UserGetter from '../User/UserGetter.js' /** * @import { Survey } from '../../../../types/project/dashboard/survey' @@ -33,6 +34,25 @@ async function getSurvey(userId) { return } + const { earliestSignupDate, latestSignupDate } = survey.options || {} + if (earliestSignupDate || latestSignupDate) { + const user = await UserGetter.promises.getUser(userId, { signUpDate: 1 }) + if (!user) { + return + } + const { signUpDate } = user + if (latestSignupDate) { + // Make the check inclusive + latestSignupDate.setHours(23, 59, 59, 999) + if (signUpDate > latestSignupDate) { + return + } + } + if (earliestSignupDate && signUpDate < earliestSignupDate) { + return + } + } + return { name, preText, linkText, url } } } diff --git a/services/web/app/src/Features/Survey/SurveyManager.js b/services/web/app/src/Features/Survey/SurveyManager.js index abaee90f13..3d6f225e10 100644 --- a/services/web/app/src/Features/Survey/SurveyManager.js +++ b/services/web/app/src/Features/Survey/SurveyManager.js @@ -10,6 +10,7 @@ async function getSurvey() { } async function updateSurvey({ name, preText, linkText, url, options }) { + validateOptions(options) let survey = await getSurvey() if (!survey) { survey = new Survey() @@ -23,6 +24,41 @@ async function updateSurvey({ name, preText, linkText, url, options }) { return survey } +function validateOptions(options) { + if (!options) { + return + } + if (typeof options !== 'object') { + throw new Error('options must be an object') + } + const { earliestSignupDate, latestSignupDate } = options + + const earliestDate = parseDate(earliestSignupDate) + const latestDate = parseDate(latestSignupDate) + if (earliestDate && latestDate) { + if (earliestDate > latestDate) { + throw new Error('earliestSignupDate must be before latestSignupDate') + } + } +} + +function parseDate(date) { + if (date) { + if (typeof date !== 'string') { + throw new Error('Date must be a string') + } + if (date.match(/^\d{4}-\d{2}-\d{2}$/) === null) { + throw new Error('Date must be in YYYY-MM-DD format') + } + const asDate = new Date(date) + if (isNaN(asDate.getTime())) { + throw new Error('Date must be a valid date') + } + return asDate + } + return null +} + async function deleteSurvey() { const survey = await getSurvey() if (survey) { diff --git a/services/web/app/src/models/Survey.js b/services/web/app/src/models/Survey.js index 5d56a20b54..deb5d60454 100644 --- a/services/web/app/src/models/Survey.js +++ b/services/web/app/src/models/Survey.js @@ -36,6 +36,12 @@ const SurveySchema = new Schema( type: Boolean, default: false, }, + earliestSignupDate: { + type: Date, + }, + latestSignupDate: { + type: Date, + }, rolloutPercentage: { type: Number, default: 100, diff --git a/services/web/test/acceptance/src/helpers/User.mjs b/services/web/test/acceptance/src/helpers/User.mjs index 5f8ac2903f..361466e259 100644 --- a/services/web/test/acceptance/src/helpers/User.mjs +++ b/services/web/test/acceptance/src/helpers/User.mjs @@ -36,6 +36,7 @@ class User { this.request = request.defaults({ jar: this.jar, }) + this.signUpDate = options.signUpDate ?? new Date() } getSession(options, callback) { @@ -425,7 +426,13 @@ class User { UserModel.findOneAndUpdate( filter, - { $set: { hashedPassword, emails: this.emails } }, + { + $set: { + hashedPassword, + emails: this.emails, + signUpDate: this.signUpDate, + }, + }, options ) .then(user => {