mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
Add the SheerID student verification module (#36704)
GitOrigin-RevId: a3673c09e185c8f857d5da3e706fa9a310fba6d4
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import mongoose from '../infrastructure/Mongoose.mjs'
|
||||
|
||||
const { Schema } = mongoose
|
||||
const { ObjectId } = Schema
|
||||
|
||||
/**
|
||||
* @typedef {'pending' | 'success' | 'failed'} StudentVerificationStatus
|
||||
*/
|
||||
|
||||
// Kept separate so retention, validity and the retry block can change
|
||||
// independently, though all three are 30 days at the moment.
|
||||
|
||||
// How long a success counts as verified. TODO: 30 days is a placeholder;
|
||||
// SheerID's own model is annual.
|
||||
export const VALIDITY_IN_SECONDS = 60 * 60 * 24 * 30
|
||||
|
||||
// How long a failure blocks starting again. TODO: decide whether to allow one
|
||||
// automatic retry first.
|
||||
export const BLOCK_DURATION_IN_SECONDS = 60 * 60 * 24 * 30
|
||||
|
||||
// How long the record is kept, via a Mongo TTL index.
|
||||
export const EXPIRY_IN_SECONDS = 60 * 60 * 24 * 30
|
||||
|
||||
// Runs at boot, since every path that touches a StudentVerification loads this
|
||||
// module. Fails loudly rather than letting a misconfiguration quietly delete
|
||||
// records that are still in use.
|
||||
if (
|
||||
EXPIRY_IN_SECONDS < Math.max(VALIDITY_IN_SECONDS, BLOCK_DURATION_IN_SECONDS)
|
||||
) {
|
||||
throw new Error(
|
||||
'StudentVerification: EXPIRY_IN_SECONDS must be >= VALIDITY_IN_SECONDS and BLOCK_DURATION_IN_SECONDS (a record must not be deleted by the TTL while it is still authoritative for the purchase gate or the retry block)'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} seconds
|
||||
*/
|
||||
function dateSecondsFromNow(seconds) {
|
||||
const timestamp = new Date()
|
||||
timestamp.setSeconds(timestamp.getSeconds() + seconds)
|
||||
return timestamp
|
||||
}
|
||||
|
||||
export const StudentVerificationSchema = new Schema(
|
||||
{
|
||||
// Unique, so it can be the sole lookup key everywhere.
|
||||
verificationId: { type: String, required: true, unique: true },
|
||||
userId: { type: ObjectId, required: true },
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'success', 'failed'],
|
||||
default: 'pending',
|
||||
},
|
||||
// Only ever set alongside a FAILED status. Distinguishes a permanent
|
||||
// SheerID block (verificationLimitExceeded) from an ordinary declined
|
||||
// result (docReviewLimitExceeded), so the frontend can show 'blocked'
|
||||
// rather than 'declined'. Not a status of its own.
|
||||
blocked: { type: Boolean, default: false },
|
||||
// Upper-case.
|
||||
country: { type: String, required: true },
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
updatedAt: { type: Date, default: Date.now },
|
||||
// Throttles the upstream fetch independently of how often the page polls
|
||||
// us. Absent means never refreshed.
|
||||
lastRefreshedAt: { type: Date },
|
||||
// When the status stopped being pending. Set once, alongside the move to
|
||||
// success or failed. Subtracting createdAt gives how long the verification
|
||||
// took, which updatedAt would not, since any later write moves it.
|
||||
resolvedAt: { type: Date },
|
||||
// Captured at the start so it survives the user leaving and coming back
|
||||
// later via email link, then reattached to the purchase once they are
|
||||
// verified.
|
||||
itm: {
|
||||
itm_campaign: { type: String, default: null },
|
||||
itm_content: { type: String, default: null },
|
||||
itm_referrer: { type: String, default: null },
|
||||
},
|
||||
// How long a SUCCESS counts as verified for the purchase gate. See
|
||||
// VALIDITY_IN_SECONDS above. The default is only a starting point:
|
||||
// StudentVerificationManager sets this from the moment SheerID resolved
|
||||
// the verification, so the window does not start ticking while a document
|
||||
// review is still going on.
|
||||
validUntil: {
|
||||
type: Date,
|
||||
default: () => dateSecondsFromNow(VALIDITY_IN_SECONDS),
|
||||
},
|
||||
// How long a FAILED record blocks starting a new verification. See
|
||||
// BLOCK_DURATION_IN_SECONDS above. Checked explicitly by the caller
|
||||
// (StudentVerificationController.startVerification) rather than
|
||||
// relying on the record ageing out via the TTL, so the block duration
|
||||
// can move independently of retention. Set from resolvedAt when the
|
||||
// verification fails, so the block runs from the failure rather than from
|
||||
// when the user started.
|
||||
blockedUntil: {
|
||||
type: Date,
|
||||
default: () => dateSecondsFromNow(BLOCK_DURATION_IN_SECONDS),
|
||||
},
|
||||
// How long the record is kept. Mongo deletes it when this passes. Set from
|
||||
// resolvedAt alongside the other two windows, so a record is never deleted
|
||||
// while it is still authoritative for the gate or the retry block.
|
||||
expires: {
|
||||
type: Date,
|
||||
default: () => dateSecondsFromNow(EXPIRY_IN_SECONDS),
|
||||
index: { expireAfterSeconds: 0 },
|
||||
},
|
||||
},
|
||||
{
|
||||
collection: 'studentVerifications',
|
||||
minimize: false,
|
||||
}
|
||||
)
|
||||
|
||||
// Not created automatically: infrastructure/Mongoose.mjs sets autoIndex: false
|
||||
// outside dev. Real environments get them from
|
||||
// tools/migrations/20260808093000_create_studentVerifications_indexes.mjs,
|
||||
// which says what each one serves. Keep the two in sync.
|
||||
StudentVerificationSchema.index({ userId: 1, status: 1, createdAt: -1 })
|
||||
StudentVerificationSchema.index({ status: 1, lastRefreshedAt: 1 })
|
||||
|
||||
export const StudentVerification = mongoose.model(
|
||||
'StudentVerification',
|
||||
StudentVerificationSchema
|
||||
)
|
||||
@@ -32,6 +32,7 @@
|
||||
"about_to_leave_project": "",
|
||||
"about_to_leave_projects": "",
|
||||
"about_to_remove_user_preamble": "",
|
||||
"about_to_switch_to_plan_x_for_price_y": "",
|
||||
"about_to_trash_projects": "",
|
||||
"abstract": "",
|
||||
"accept_all_cookies": "",
|
||||
@@ -157,7 +158,9 @@
|
||||
"all_these_experiments_are_available_exclusively": "",
|
||||
"allocate_license": "",
|
||||
"allows_to_search_by_author_title_etc_possible_to_pull_results_directly_from_your_reference_manager_if_connected": "",
|
||||
"already_changed_your_plan": "",
|
||||
"already_in_your_library": "",
|
||||
"already_purchased_this_plan": "",
|
||||
"an_email_has_already_been_sent_to": "",
|
||||
"an_error_occured_while_restoring_project": "",
|
||||
"and_much_more": "",
|
||||
@@ -303,6 +306,7 @@
|
||||
"chat_error": "",
|
||||
"checking_dropbox_status": "",
|
||||
"checking_project_github_status": "",
|
||||
"checking_your_student_status": "",
|
||||
"choose_a_custom_color": "",
|
||||
"choose_from_group_members": "",
|
||||
"choose_how_you_search_your_references": "",
|
||||
@@ -401,6 +405,7 @@
|
||||
"continue": "",
|
||||
"continue_github_merge": "",
|
||||
"continue_to": "",
|
||||
"continue_to_checkout": "",
|
||||
"continue_using_free_features": "",
|
||||
"continue_with_free_plan": "",
|
||||
"conversion_error_details": "",
|
||||
@@ -812,6 +817,7 @@
|
||||
"get_ready_for_overleaf_at_its_best_pro": "",
|
||||
"get_ready_for_overleaf_standard": "",
|
||||
"get_real_time_track_changes": "",
|
||||
"get_standard": "",
|
||||
"get_started_with_latex": "",
|
||||
"get_your_hands_on_the_ultimate_research_writing_ai_assistant": "",
|
||||
"git": "",
|
||||
@@ -1524,6 +1530,7 @@
|
||||
"please_confirm_secondary_email_or_edit": "",
|
||||
"please_confirm_your_email_before_making_it_default": "",
|
||||
"please_contact_support_to_makes_change_to_your_plan": "",
|
||||
"please_contact_us_if_you_think_this_is_in_error": "",
|
||||
"please_enter_confirmation_code": "",
|
||||
"please_get_in_touch": "",
|
||||
"please_link_before_making_primary": "",
|
||||
@@ -1659,6 +1666,8 @@
|
||||
"readcube_sync_description": "",
|
||||
"readcube_upgrade_prompt_content": "",
|
||||
"readcube_upgrade_prompt_title": "",
|
||||
"ready_to_complete_your_plan_change": "",
|
||||
"ready_to_complete_your_purchase": "",
|
||||
"ready_to_join_x": "",
|
||||
"ready_to_join_x_in_group_y": "",
|
||||
"ready_to_set_up": "",
|
||||
@@ -2066,6 +2075,18 @@
|
||||
"strongly_agree": "",
|
||||
"strongly_disagree": "",
|
||||
"student": "",
|
||||
"student_status_verified": "",
|
||||
"student_verification_blocked": "",
|
||||
"student_verification_blocked_info": "",
|
||||
"student_verification_confirm_email_to_continue": "",
|
||||
"student_verification_declined": "",
|
||||
"student_verification_declined_info": "",
|
||||
"student_verification_error": "",
|
||||
"student_verification_error_info": "",
|
||||
"student_verification_intro": "",
|
||||
"student_verification_rate_limited": "",
|
||||
"student_verification_rate_limited_info": "",
|
||||
"student_verification_required": "",
|
||||
"subject": "",
|
||||
"subject_area": "",
|
||||
"submit_title": "",
|
||||
@@ -2343,6 +2364,7 @@
|
||||
"turn_on": "",
|
||||
"turn_on_link_sharing": "",
|
||||
"type_a_command": "",
|
||||
"unable_to_load_current_plan_details": "",
|
||||
"unable_to_save_check_connection_or_download": "",
|
||||
"unarchive": "",
|
||||
"uncategorized": "",
|
||||
@@ -2584,6 +2606,7 @@
|
||||
"you_can_also_right_click_a_file_to_set_it_as_main": "",
|
||||
"you_can_buy_this_plan_but_not_as_a_trial": "",
|
||||
"you_can_link_x_in_your_account_settings": "",
|
||||
"you_can_now_buy_the_student_plan": "",
|
||||
"you_can_now_enable_sso": "",
|
||||
"you_can_now_log_in_sso": "",
|
||||
"you_can_now_search_and_add_references_from_your_rm_library_without_needing_to_import_files": "",
|
||||
@@ -2591,6 +2614,7 @@
|
||||
"you_can_restore_or_permanently_delete_them_at_any_time": "",
|
||||
"you_can_select_or_invite_collaborator": "",
|
||||
"you_can_select_or_invite_collaborator_plural": "",
|
||||
"you_can_still_get_the_features_you_need_on_our_standard_plan": "",
|
||||
"you_can_still_use_your_premium_features": "",
|
||||
"you_cant_add_or_change_password_due_to_sso": "",
|
||||
"you_cant_join_this_group_subscription": "",
|
||||
|
||||
@@ -334,6 +334,17 @@ export interface Meta {
|
||||
}>
|
||||
'ol-stripePublicKeyUK': string
|
||||
'ol-stripePublicKeyUS': string
|
||||
'ol-studentVerificationPage': {
|
||||
planCode: string
|
||||
origin: 'change-plan' | undefined
|
||||
alreadyVerified: boolean
|
||||
alreadyOnPlan: boolean
|
||||
// Already translated by the server, because the keys come from the plans
|
||||
// config and so are not picked up by the frontend translation build.
|
||||
studentPlanFeatures: string[]
|
||||
standardPlanFeatures: string[]
|
||||
ip: string | undefined
|
||||
}
|
||||
'ol-subscription': any // TODO: mixed types, split into two fields
|
||||
'ol-subscriptionChangePreview': SubscriptionChangePreview
|
||||
'ol-subscriptionCreationPreview': SubscriptionCreationPreview
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"about_to_leave_project": "You are about to leave this project.",
|
||||
"about_to_leave_projects": "You are about to leave the following projects:",
|
||||
"about_to_remove_user_preamble": "You’re about to remove __userName__ (__userEmail__). Doing this will mean:",
|
||||
"about_to_switch_to_plan_x_for_price_y": "You’re about to switch to <0>__planName__</0> for __price__.",
|
||||
"about_to_trash_projects": "You are about to trash the following projects:",
|
||||
"abstract": "Abstract",
|
||||
"accept": "Accept",
|
||||
@@ -196,9 +197,11 @@
|
||||
"all_these_experiments_are_available_exclusively": "All these experiments are available exclusively to members of the Labs program. If you sign up, you can choose which experiments you want to try.",
|
||||
"allocate_license": "Allocate license",
|
||||
"allows_to_search_by_author_title_etc_possible_to_pull_results_directly_from_your_reference_manager_if_connected": "Allows to search by author, title, etc. Possible to pull results directly from your reference manager (if connected).",
|
||||
"already_changed_your_plan": "You’ve already changed to this plan. <0>Go to your subscription</0>",
|
||||
"already_have_an_account": "Already have an account?",
|
||||
"already_have_sl_account": "Already have an __appName__ account?",
|
||||
"already_in_your_library": "Already in your library",
|
||||
"already_purchased_this_plan": "You’ve already purchased this plan. <0>Go to your subscription</0>",
|
||||
"also": "Also",
|
||||
"alternatively_create_new_institution_account": "Alternatively, you can create a <b>new account</b> with your institution email (<b>__email__</b>) by clicking <b>__clickText__</b>.",
|
||||
"an_email_has_already_been_sent_to": "An email has already been sent to <0>__email__</0>. Please wait and try again later.",
|
||||
@@ -387,6 +390,7 @@
|
||||
"checking": "Checking",
|
||||
"checking_dropbox_status": "Checking Dropbox status",
|
||||
"checking_project_github_status": "Checking project status in GitHub",
|
||||
"checking_your_student_status": "We’re checking your student status. We’ll email you if your document upload needs review.",
|
||||
"choose_a_custom_color": "Choose a custom color",
|
||||
"choose_a_new_password": "Choose a new password",
|
||||
"choose_from_group_members": "Choose from group members",
|
||||
@@ -523,6 +527,7 @@
|
||||
"continue": "Continue",
|
||||
"continue_github_merge": "I have manually merged. Continue",
|
||||
"continue_to": "Continue to __appName__",
|
||||
"continue_to_checkout": "Continue to checkout",
|
||||
"continue_using_free_features": "Continue using our free features",
|
||||
"continue_with_free_plan": "Continue with free plan",
|
||||
"continue_with_service": "Continue with __service__",
|
||||
@@ -2216,6 +2221,8 @@
|
||||
"readcube_sync_description": "With the ReadCube integration you can import your references from ReadCube into your __appName__ projects.",
|
||||
"readcube_upgrade_prompt_content": "Link your ReadCube account to search and add your references from ReadCube directly in your project—they’ll automatically be added to your .bib file. Or import them as a file into your __appName__ projects.",
|
||||
"readcube_upgrade_prompt_title": "Cite from ReadCube",
|
||||
"ready_to_complete_your_plan_change": "You’re ready to complete your plan change.",
|
||||
"ready_to_complete_your_purchase": "You’re ready to complete your purchase.",
|
||||
"ready_to_join_x": "You’re ready to join __inviterName__",
|
||||
"ready_to_join_x_in_group_y": "You’re ready to join __inviterName__ in __groupName__",
|
||||
"ready_to_set_up": "Ready to set up",
|
||||
@@ -2713,6 +2720,18 @@
|
||||
"strongly_disagree": "Strongly disagree",
|
||||
"student": "Student",
|
||||
"student_discount_tooltip": "Available to students currently enrolled at a degree-granting institution.",
|
||||
"student_status_verified": "Your student status has been verified",
|
||||
"student_verification_blocked": "You’ve already used your verification attempt",
|
||||
"student_verification_blocked_info": "We weren’t able to verify your student status last time, so we can’t try again automatically.",
|
||||
"student_verification_confirm_email_to_continue": "Please confirm your primary email address to continue. <0>Go to account settings</0>.",
|
||||
"student_verification_declined": "Your student status couldn’t be verified",
|
||||
"student_verification_declined_info": "We weren’t able to confirm that you’re a current student.",
|
||||
"student_verification_error": "Something went wrong",
|
||||
"student_verification_error_info": "We weren’t able to start student verification just now. This is a problem on our side, not with your details. Please try again in a few minutes.",
|
||||
"student_verification_intro": "We need to verify that you’re a current student before you can continue.",
|
||||
"student_verification_rate_limited": "Too many attempts",
|
||||
"student_verification_rate_limited_info": "We’ve had too many verification requests recently. Please wait a few minutes and try again.",
|
||||
"student_verification_required": "Student verification required",
|
||||
"students": "Students",
|
||||
"subject": "Subject",
|
||||
"subject_area": "Subject area",
|
||||
@@ -3038,6 +3057,7 @@
|
||||
"tutorials": "Tutorials",
|
||||
"type_a_command": "Type a command…",
|
||||
"unable_to_extract_the_supplied_zip_file": "Opening this content on Overleaf failed because the zip file could not be extracted. Please ensure that it is a valid zip file. If this keeps happening for links on a particular site, please report this to them.",
|
||||
"unable_to_load_current_plan_details": "We couldn’t load your current plan details. Please try again.",
|
||||
"unable_to_save_check_connection_or_download": "We couldn’t add this file to your project. Check your connection and try again, or <0>download the file</0> instead.",
|
||||
"unarchive": "Restore",
|
||||
"uncategorized": "Uncategorized",
|
||||
@@ -3314,6 +3334,7 @@
|
||||
"you_can_also_right_click_a_file_to_set_it_as_main": "You can also right-click a file to set it as main.",
|
||||
"you_can_buy_this_plan_but_not_as_a_trial": "You can buy this plan but not as a trial, as you’ve completed a trial before.",
|
||||
"you_can_link_x_in_your_account_settings": "You can link __managers__ in your <0>account settings</0>.",
|
||||
"you_can_now_buy_the_student_plan": "You can now buy the student plan",
|
||||
"you_can_now_enable_sso": "You can now enable SSO on your group settings page.",
|
||||
"you_can_now_log_in_sso": "You can now log in through your institution and if eligible you will receive <0>__appName__ premium features</0>.",
|
||||
"you_can_now_search_and_add_references_from_your_rm_library_without_needing_to_import_files": "You can now search and add references from your __referenceManager__ library without needing to import files—just type <code>\\cite{}</code> in your .tex file. <a>Learn more</a>",
|
||||
@@ -3323,6 +3344,7 @@
|
||||
"you_can_restore_or_permanently_delete_them_at_any_time": "You can restore or permanently delete them at any time.",
|
||||
"you_can_select_or_invite_collaborator": "You can select or invite __count__ collaborator on your current plan. Upgrade to add more editors or reviewers.",
|
||||
"you_can_select_or_invite_collaborator_plural": "You can select or invite __count__ collaborators on your current plan. Upgrade to add more editors or reviewers.",
|
||||
"you_can_still_get_the_features_you_need_on_our_standard_plan": "You can still get the features you need on our Standard plan",
|
||||
"you_can_still_use_your_premium_features": "You can still use your premium features until the pause becomes active.",
|
||||
"you_cant_add_or_change_password_due_to_sso": "You can’t add or change your password because your group or organization uses <0>single sign-on (SSO)</0>.",
|
||||
"you_cant_join_this_group_subscription": "You can’t join this group subscription",
|
||||
|
||||
@@ -200,6 +200,26 @@ module.exports = {
|
||||
price_in_cents: 1500,
|
||||
features: features.collaborator,
|
||||
},
|
||||
// The real student plans live in settings.overrides.saas.js, but
|
||||
// @overleaf/settings' merge replaces arrays wholesale rather than
|
||||
// merging them, so this list (not that one) is what acceptance tests
|
||||
// see. Without these, PlansLocator.findLocalPlanInSettings cannot
|
||||
// resolve 'student' and any test of the student-verification gate is
|
||||
// blocked before the gate is reached. Feature set is deliberately
|
||||
// borrowed from collaborator: the gate only needs the plan to resolve.
|
||||
{
|
||||
planCode: 'student',
|
||||
name: 'Student monthly',
|
||||
price_in_cents: 1000,
|
||||
features: features.collaborator,
|
||||
},
|
||||
{
|
||||
planCode: 'student-annual',
|
||||
name: 'Student annual',
|
||||
price_in_cents: 10000,
|
||||
annual: true,
|
||||
features: features.collaborator,
|
||||
},
|
||||
{
|
||||
planCode: 'professional',
|
||||
name: 'Pro monthly',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
@@ -88,3 +88,133 @@ describe('settings.defaults', function () {
|
||||
expect(haveSameStructure(pricings)).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
describe('settings.overrides.saas', function () {
|
||||
const originalEnv = {
|
||||
SHEERID_ENABLED: process.env.SHEERID_ENABLED,
|
||||
SHEERID_MOCK: process.env.SHEERID_MOCK,
|
||||
SHEERID_CLIENT_ID: process.env.SHEERID_CLIENT_ID,
|
||||
SHEERID_CLIENT_SECRET: process.env.SHEERID_CLIENT_SECRET,
|
||||
SHEERID_PROGRAM_ID: process.env.SHEERID_PROGRAM_ID,
|
||||
SHEERID_API_BASE_URL: process.env.SHEERID_API_BASE_URL,
|
||||
SHEERID_ENABLED_COUNTRIES: process.env.SHEERID_ENABLED_COUNTRIES,
|
||||
SHEERID_FALLBACK_COUNTRY: process.env.SHEERID_FALLBACK_COUNTRY,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
}
|
||||
|
||||
function setCredentials() {
|
||||
process.env.SHEERID_CLIENT_ID = 'client-id'
|
||||
process.env.SHEERID_CLIENT_SECRET = 'client-secret'
|
||||
process.env.SHEERID_PROGRAM_ID = 'program-id'
|
||||
}
|
||||
|
||||
afterEach(function () {
|
||||
for (const [key, value] of Object.entries(originalEnv)) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
clearSettingsCache()
|
||||
})
|
||||
|
||||
it('sets the verification page CSP frame-src from the configured SheerID host', function () {
|
||||
process.env.SHEERID_API_BASE_URL = 'https://sandbox.sheerid.com'
|
||||
clearSettingsCache()
|
||||
|
||||
const settings = require('../../../../config/settings.overrides.saas.js')
|
||||
|
||||
expect(
|
||||
settings.csp.viewDirectives[
|
||||
'modules/student-verification/app/views/student-verification'
|
||||
]
|
||||
).to.deep.equal(["frame-src 'self' https://sandbox.sheerid.com"])
|
||||
})
|
||||
|
||||
it('upper-cases the SheerID country settings', function () {
|
||||
process.env.SHEERID_ENABLED_COUNTRIES = 'gb, us ,De'
|
||||
process.env.SHEERID_FALLBACK_COUNTRY = 'gb'
|
||||
clearSettingsCache()
|
||||
|
||||
const settings = require('../../../../config/settings.overrides.saas.js')
|
||||
|
||||
expect(settings.sheerId.enabledCountries).to.deep.equal(['GB', 'US', 'DE'])
|
||||
expect(settings.sheerId.fallbackCountry).to.equal('GB')
|
||||
})
|
||||
|
||||
it('refuses to boot with SHEERID_ENABLED and the default mock client in production', function () {
|
||||
process.env.SHEERID_ENABLED = 'true'
|
||||
delete process.env.SHEERID_MOCK // defaults to mock: true
|
||||
process.env.NODE_ENV = 'production'
|
||||
clearSettingsCache()
|
||||
|
||||
expect(() =>
|
||||
require('../../../../config/settings.overrides.saas.js')
|
||||
).to.throw(/SHEERID_ENABLED/)
|
||||
})
|
||||
|
||||
it('boots with SHEERID_ENABLED, SHEERID_MOCK=false and credentials in production', function () {
|
||||
process.env.SHEERID_ENABLED = 'true'
|
||||
process.env.SHEERID_MOCK = 'false'
|
||||
process.env.NODE_ENV = 'production'
|
||||
setCredentials()
|
||||
clearSettingsCache()
|
||||
|
||||
expect(() =>
|
||||
require('../../../../config/settings.overrides.saas.js')
|
||||
).not.to.throw()
|
||||
})
|
||||
|
||||
it('refuses to boot with the real client and no credentials in production', function () {
|
||||
process.env.SHEERID_ENABLED = 'true'
|
||||
process.env.SHEERID_MOCK = 'false'
|
||||
process.env.NODE_ENV = 'production'
|
||||
delete process.env.SHEERID_CLIENT_ID
|
||||
delete process.env.SHEERID_CLIENT_SECRET
|
||||
delete process.env.SHEERID_PROGRAM_ID
|
||||
clearSettingsCache()
|
||||
|
||||
expect(() =>
|
||||
require('../../../../config/settings.overrides.saas.js')
|
||||
).to.throw(/SHEERID_CLIENT_ID, SHEERID_CLIENT_SECRET, SHEERID_PROGRAM_ID/)
|
||||
})
|
||||
|
||||
it('names only the credentials that are missing', function () {
|
||||
process.env.SHEERID_ENABLED = 'true'
|
||||
process.env.SHEERID_MOCK = 'false'
|
||||
process.env.NODE_ENV = 'production'
|
||||
setCredentials()
|
||||
delete process.env.SHEERID_PROGRAM_ID
|
||||
clearSettingsCache()
|
||||
|
||||
expect(() =>
|
||||
require('../../../../config/settings.overrides.saas.js')
|
||||
).to.throw(/requires SHEERID_PROGRAM_ID/)
|
||||
})
|
||||
|
||||
it('boots with the real client and no credentials outside production', function () {
|
||||
process.env.SHEERID_ENABLED = 'true'
|
||||
process.env.SHEERID_MOCK = 'false'
|
||||
process.env.NODE_ENV = 'test'
|
||||
delete process.env.SHEERID_CLIENT_ID
|
||||
delete process.env.SHEERID_CLIENT_SECRET
|
||||
delete process.env.SHEERID_PROGRAM_ID
|
||||
clearSettingsCache()
|
||||
|
||||
expect(() =>
|
||||
require('../../../../config/settings.overrides.saas.js')
|
||||
).not.to.throw()
|
||||
})
|
||||
|
||||
it('boots with SHEERID_ENABLED and the default mock client outside production', function () {
|
||||
process.env.SHEERID_ENABLED = 'true'
|
||||
delete process.env.SHEERID_MOCK
|
||||
process.env.NODE_ENV = 'test'
|
||||
clearSettingsCache()
|
||||
|
||||
expect(() =>
|
||||
require('../../../../config/settings.overrides.saas.js')
|
||||
).not.to.throw()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,6 +20,9 @@ declare module 'express' {
|
||||
userRestrictions?: Set
|
||||
oauth_user?: OAuth2Server.User
|
||||
logger: RequestLogger
|
||||
// Set by a body-parser `verify` callback on routes that check a webhook
|
||||
// signature, which needs the bytes exactly as received.
|
||||
rawBody?: Buffer | string
|
||||
i18n: {
|
||||
language: string
|
||||
translate(
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import Helpers from './lib/helpers.mjs'
|
||||
import { getCollectionInternal } from './lib/mongodb.mjs'
|
||||
|
||||
const tags = ['saas']
|
||||
|
||||
// Keep in sync with the indexes declared on the model
|
||||
// (services/web/app/src/models/StudentVerification.mjs): Mongoose runs with
|
||||
// autoIndex: false, so nothing creates these automatically.
|
||||
const indexes = [
|
||||
{
|
||||
// Every lookup of a single verification is by this id (status polling, the
|
||||
// webhook, and the mock outcome controls).
|
||||
key: { verificationId: 1 },
|
||||
unique: true,
|
||||
name: 'verificationId_1',
|
||||
},
|
||||
{
|
||||
// The purchase gate: the user's latest unexpired SUCCESS verification.
|
||||
key: { userId: 1, status: 1, createdAt: -1 },
|
||||
name: 'userId_1_status_1_createdAt_-1',
|
||||
},
|
||||
{
|
||||
// Retention: records carry their own `expires` date, so the TTL monitor
|
||||
// deletes each one at the time stored on it rather than after a fixed
|
||||
// interval.
|
||||
key: { expires: 1 },
|
||||
expireAfterSeconds: 0,
|
||||
name: 'expires_1',
|
||||
},
|
||||
{
|
||||
// The reconciliation poller
|
||||
// (modules/student-verification/scripts/reconcile_student_verifications.mjs)
|
||||
// sweeps pending records by refresh staleness, with no userId to narrow
|
||||
// on, so the {userId, status, createdAt} index cannot serve it and the DB
|
||||
// runs with notablescan.
|
||||
key: { status: 1, lastRefreshedAt: 1 },
|
||||
name: 'status_1_lastRefreshedAt_1',
|
||||
},
|
||||
]
|
||||
|
||||
const migrate = async () => {
|
||||
const studentVerifications = await getCollectionInternal(
|
||||
'studentVerifications'
|
||||
)
|
||||
await Helpers.addIndexesToCollection(studentVerifications, indexes)
|
||||
}
|
||||
|
||||
const rollback = async () => {
|
||||
const studentVerifications = await getCollectionInternal(
|
||||
'studentVerifications'
|
||||
)
|
||||
await Helpers.dropIndexesFromCollection(studentVerifications, indexes)
|
||||
}
|
||||
|
||||
export default {
|
||||
tags,
|
||||
migrate,
|
||||
rollback,
|
||||
}
|
||||
Reference in New Issue
Block a user