Refactor emailAddresses.js to Typescript (#4925)
* convert breaches.js to ts * convert email_notifications to typescript * Refactor emailAddresses.js to Typescript * remove redundant type * more specific types * remove use of any for email hash function * remove use of any for email hash function * remove no-unsafe-return typescript linter exception * fix error
This commit is contained in:
Родитель
eaee8ffec9
Коммит
677eda250f
|
@ -15,7 +15,7 @@ import {
|
|||
getFxATokens,
|
||||
updateFxATokens,
|
||||
} from "../../../db/tables/subscribers.js";
|
||||
import { addSubscriber } from "../../../db/tables/emailAddresses.js";
|
||||
import { addSubscriber } from "../../../db/tables/emailAddresses";
|
||||
import { getBreaches } from "../../functions/server/getBreaches";
|
||||
import { getBreachesForEmail } from "../../../utils/hibp";
|
||||
import { getSha1, refreshOAuthTokens } from "../../../utils/fxa";
|
||||
|
@ -197,7 +197,7 @@ export const authOptions: AuthOptions = {
|
|||
});
|
||||
|
||||
const enabledFlags = await getEnabledFeatureFlags({
|
||||
email: verifiedSubscriber.primary_email,
|
||||
email: verifiedSubscriber ? verifiedSubscriber.primary_email : "",
|
||||
});
|
||||
|
||||
await initEmail(process.env.SMTP_URL);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { SubscriberRow } from "knex/types/tables";
|
||||
import { resetUnverifiedEmailAddress } from "../../../db/tables/emailAddresses.js";
|
||||
import { resetUnverifiedEmailAddress } from "../../../db/tables/emailAddresses";
|
||||
import { sendEmail, getVerificationUrl } from "../../../utils/email.js";
|
||||
import { getTemplate } from "../../../emails/email2022.js";
|
||||
import { verifyPartial } from "../../../emails/emailVerify.js";
|
||||
|
|
|
@ -7,7 +7,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|||
import AppConstants from "../../../../../appConstants";
|
||||
|
||||
import { getSubscriberByFxaUid } from "../../../../../db/tables/subscribers";
|
||||
import { addSubscriberUnverifiedEmailHash } from "../../../../../db/tables/emailAddresses.js";
|
||||
import { addSubscriberUnverifiedEmailHash } from "../../../../../db/tables/emailAddresses";
|
||||
|
||||
import { sendVerificationEmail } from "../../../utils/email";
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
import {
|
||||
removeOneSecondaryEmail,
|
||||
getEmailById,
|
||||
} from "../../../../../db/tables/emailAddresses.js";
|
||||
} from "../../../../../db/tables/emailAddresses";
|
||||
import { getL10n } from "../../../../functions/l10n/serverComponents";
|
||||
|
||||
interface EmailDeleteRequest {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { logger } from "../../../../functions/server/logging";
|
||||
import { verifyEmailHash } from "../../../../../db/tables/emailAddresses.js";
|
||||
import { verifyEmailHash } from "../../../../../db/tables/emailAddresses";
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const query = req.nextUrl.searchParams;
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
getAllBreachesFromDb,
|
||||
fetchHibpBreaches,
|
||||
} from "../../../utils/hibp";
|
||||
import { upsertBreaches } from "../../../db/tables/breaches.js";
|
||||
import { upsertBreaches } from "../../../db/tables/breaches";
|
||||
|
||||
let breaches: Array<HibpLikeDbBreach>;
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import { Session } from "next-auth";
|
||||
import { getSubscriberByFxaUid } from "../../../db/tables/subscribers.js";
|
||||
import { getUserEmails } from "../../../db/tables/emailAddresses.js";
|
||||
import { getUserEmails } from "../../../db/tables/emailAddresses";
|
||||
|
||||
/**
|
||||
* NOTE: new function to replace getUserBreaches
|
||||
|
|
|
@ -13,7 +13,7 @@ import { getSubscriberByFxaUid } from "../../../../src/db/tables/subscribers.js"
|
|||
import {
|
||||
BundledVerifiedEmails,
|
||||
getAllEmailsAndBreaches,
|
||||
} from "../../../../src/utils/breaches.js";
|
||||
} from "../../../../src/utils/breaches";
|
||||
import { SubscriberBreach } from "../../../utils/subscriberBreaches";
|
||||
import { HibpLikeDbBreach } from "../../../utils/hibp";
|
||||
|
||||
|
|
|
@ -25,4 +25,4 @@ export {
|
|||
}
|
||||
|
||||
export * from './tables/subscribers.js'
|
||||
export * from './tables/emailAddresses.js'
|
||||
export * from './tables/emailAddresses'
|
||||
|
|
|
@ -2,56 +2,45 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { formatDataClassesArray } from "../../utils/hibp";
|
||||
import { BreachRow } from "knex/types/tables";
|
||||
import {
|
||||
formatDataClassesArray,
|
||||
HibpGetBreachesResponse,
|
||||
} from "../../utils/hibp";
|
||||
import createDbConnection from "../connect.js";
|
||||
|
||||
const knex = createDbConnection();
|
||||
|
||||
/**
|
||||
* Get all records from "breaches" table
|
||||
*
|
||||
* @returns Array of all records from "breaches" table
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function getAllBreaches() {
|
||||
return knex('breaches')
|
||||
.returning("*")
|
||||
async function getAllBreaches(): Promise<BreachRow[]> {
|
||||
return knex("breaches").returning("*");
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* Get all count from "breaches" table
|
||||
*
|
||||
* @returns Count of all records from "breaches" table
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function getAllBreachesCount() {
|
||||
const breachesCount = await knex('breaches')
|
||||
async function getAllBreachesCount(): Promise<number> {
|
||||
const breachesCount = await knex("breaches")
|
||||
.count({ count: "id" })
|
||||
.first()
|
||||
.then(result => result?.count || "")
|
||||
.then((result) => result?.count || "");
|
||||
// Make sure we are returning a number.
|
||||
return parseInt(breachesCount.toString(), 10)
|
||||
return parseInt(breachesCount.toString(), 10);
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* Upsert breaches into "breaches" table
|
||||
* Skip inserting when 'name' field (unique) has a conflict
|
||||
*
|
||||
* @param {import("../../utils/hibp").HibpGetBreachesResponse} hibpBreaches breaches array from HIBP API
|
||||
* @returns
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function upsertBreaches(hibpBreaches) {
|
||||
console.debug('upsertBreaches', hibpBreaches[0])
|
||||
async function upsertBreaches(
|
||||
hibpBreaches: HibpGetBreachesResponse,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): Promise<any> {
|
||||
console.debug("upsertBreaches", hibpBreaches[0]);
|
||||
|
||||
return knex.transaction(async trx => {
|
||||
const queries = hibpBreaches.map(breach =>
|
||||
knex('breaches')
|
||||
return knex.transaction(async (trx) => {
|
||||
const queries = hibpBreaches.map((breach) =>
|
||||
knex("breaches")
|
||||
.insert({
|
||||
name: breach.Name,
|
||||
title: breach.Title,
|
||||
|
@ -61,7 +50,7 @@ async function upsertBreaches(hibpBreaches) {
|
|||
modified_date: breach.ModifiedDate,
|
||||
pwn_count: breach.PwnCount,
|
||||
description: breach.Description,
|
||||
logo_path: /** @type {RegExpExecArray} */(/[^/]*$/.exec(breach.LogoPath))[0],
|
||||
logo_path: breach.LogoPath,
|
||||
data_classes: formatDataClassesArray(breach.DataClasses),
|
||||
is_verified: breach.IsVerified,
|
||||
is_fabricated: breach.IsFabricated,
|
||||
|
@ -71,35 +60,26 @@ async function upsertBreaches(hibpBreaches) {
|
|||
is_malware: breach.IsMalware,
|
||||
favicon_url: null,
|
||||
})
|
||||
.onConflict('name')
|
||||
.onConflict("name")
|
||||
.merge()
|
||||
.transacting(trx)
|
||||
)
|
||||
.transacting(trx),
|
||||
);
|
||||
|
||||
try {
|
||||
const value = await Promise.all(queries)
|
||||
return trx.commit(value)
|
||||
const value = await Promise.all(queries);
|
||||
return trx.commit(value);
|
||||
} catch (error) {
|
||||
return trx.rollback(error)
|
||||
return trx.rollback(error);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* Update logo path of a breach by name
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {string | null} faviconUrl
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function updateBreachFaviconUrl(name, faviconUrl) {
|
||||
await knex('breaches')
|
||||
.where("name", name)
|
||||
.update({
|
||||
favicon_url: faviconUrl
|
||||
})
|
||||
async function updateBreachFaviconUrl(name: string, faviconUrl: string | null) {
|
||||
await knex("breaches").where("name", name).update({
|
||||
favicon_url: faviconUrl,
|
||||
});
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
|
@ -108,5 +88,5 @@ export {
|
|||
getAllBreachesCount,
|
||||
upsertBreaches,
|
||||
updateBreachFaviconUrl,
|
||||
knex
|
||||
}
|
||||
knex,
|
||||
};
|
|
@ -1,390 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import createDbConnection from "../connect.js";
|
||||
import { subscribeHash } from '../../utils/hibp'
|
||||
import { getSha1 } from '../../utils/fxa'
|
||||
import { getSubscriberByEmail, updateFxAData } from './subscribers.js'
|
||||
import {
|
||||
ForbiddenError,
|
||||
UnauthorizedError
|
||||
} from '../../utils/error'
|
||||
|
||||
const knex = createDbConnection();
|
||||
|
||||
/**
|
||||
* @param {string} token
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function getEmailByToken (token) {
|
||||
const res = await knex('email_addresses')
|
||||
.where('verification_token', '=', token)
|
||||
|
||||
return res[0]
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* @param {number} emailAddressId
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function getEmailById (emailAddressId) {
|
||||
const res = await knex('email_addresses')
|
||||
.where('id', '=', emailAddressId)
|
||||
|
||||
return res[0]
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* @param {string} email
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function getEmailAddressRecordByEmail (email) {
|
||||
const emailAddresses = await knex('email_addresses').where({
|
||||
email, verified: true
|
||||
})
|
||||
if (!emailAddresses) {
|
||||
return null
|
||||
}
|
||||
if (emailAddresses.length > 1) {
|
||||
// TODO: handle multiple emails in separate(?) subscriber accounts?
|
||||
console.warn('getEmailAddressRecordByEmail', { msg: 'found the same email multiple times' })
|
||||
}
|
||||
return emailAddresses[0]
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* @param {{ id: number; }} user
|
||||
* @param {string} email
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function addSubscriberUnverifiedEmailHash (user, email) {
|
||||
const lowerCaseEmail = email.toLowerCase()
|
||||
const res = await knex.transaction(trx => {
|
||||
return trx('email_addresses')
|
||||
.forUpdate()
|
||||
.select({
|
||||
subscriber_id: user.id
|
||||
})
|
||||
.insert({
|
||||
subscriber_id: user.id,
|
||||
email: lowerCaseEmail,
|
||||
sha1: getSha1(lowerCaseEmail),
|
||||
verification_token: uuidv4(),
|
||||
verified: false
|
||||
}).returning('*')
|
||||
})
|
||||
return res[0]
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* @param {number | string} emailAddressId
|
||||
* @param {import('knex/types/tables').SubscriberRow} subscriber
|
||||
* @param {import("@fluent/react").ReactLocalization} l10n
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function resetUnverifiedEmailAddress (emailAddressId, subscriber, l10n) {
|
||||
const newVerificationToken = uuidv4()
|
||||
|
||||
// Time in ms to require between verification reset.
|
||||
const verificationWait = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
const verificationRecentlyUpdated = await knex('email_addresses')
|
||||
.select('id')
|
||||
.whereRaw('"updated_at" > NOW() - INTERVAL \'1 MILLISECOND\' * ?', verificationWait)
|
||||
.andWhere('id', emailAddressId)
|
||||
.first()
|
||||
|
||||
if (verificationRecentlyUpdated?.id === (typeof emailAddressId === "number" ? emailAddressId : parseInt(emailAddressId, 10))) {
|
||||
throw new ForbiddenError(l10n.getString('error-email-validation-pending'))
|
||||
}
|
||||
|
||||
const res = await knex('email_addresses')
|
||||
.update({
|
||||
verification_token: newVerificationToken,
|
||||
// @ts-ignore knex.fn.now() results in it being set to a date,
|
||||
// even if it's not typed as a JS date object:
|
||||
updated_at: knex.fn.now()
|
||||
})
|
||||
.where('id', emailAddressId)
|
||||
.andWhere("subscriber_id", subscriber.id)
|
||||
.returning('*')
|
||||
return res[0]
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* @param {string} token
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function verifyEmailHash (token) {
|
||||
const unverifiedEmail = await getEmailByToken(token)
|
||||
if (!unverifiedEmail) {
|
||||
throw new UnauthorizedError('Error message for this verification email timed out or something went wrong.')
|
||||
}
|
||||
const verifiedEmail = await _verifyNewEmail(unverifiedEmail)
|
||||
return verifiedEmail[0]
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// TODO: refactor into an upsert? https://jaketrent.com/post/upsert-knexjs/
|
||||
// Used internally, ideally should not be called by consumers.
|
||||
/**
|
||||
* @param {string} sha1
|
||||
* @param {any} aFoundCallback
|
||||
* @param {any} aNotFoundCallback
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function _getSha1EntryAndDo (sha1, aFoundCallback, aNotFoundCallback) {
|
||||
const existingEntries = await knex('subscribers')
|
||||
.where('primary_sha1', sha1)
|
||||
|
||||
if (existingEntries.length && aFoundCallback) {
|
||||
return await aFoundCallback(existingEntries[0])
|
||||
}
|
||||
|
||||
if (!existingEntries.length && aNotFoundCallback) {
|
||||
return await aNotFoundCallback()
|
||||
}
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Used internally.
|
||||
/**
|
||||
* @param {string} sha1
|
||||
* @param {string} email
|
||||
* @param {string} signupLanguage
|
||||
* @param verified
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function _addEmailHash (sha1, email, signupLanguage, verified = false) {
|
||||
try {
|
||||
return await _getSha1EntryAndDo(sha1, async (/** @type {any} */ aEntry) => {
|
||||
// Entry existed, patch the email value if supplied.
|
||||
if (email) {
|
||||
const res = await knex('subscribers')
|
||||
.update({
|
||||
primary_email: email,
|
||||
primary_sha1: getSha1(email.toLowerCase()),
|
||||
primary_verified: verified,
|
||||
// @ts-ignore knex.fn.now() results in it being set to a date,
|
||||
// even if it's not typed as a JS date object:
|
||||
updated_at: knex.fn.now()
|
||||
})
|
||||
.where('id', '=', aEntry.id)
|
||||
.returning('*')
|
||||
return res[0]
|
||||
}
|
||||
|
||||
return aEntry
|
||||
}, async () => {
|
||||
// Always add a verification_token value
|
||||
const verificationToken = uuidv4()
|
||||
const res = await knex('subscribers')
|
||||
.insert({ primary_sha1: getSha1(email.toLowerCase()), primary_email: email, signup_language: signupLanguage, primary_verification_token: verificationToken, primary_verified: verified })
|
||||
.returning('*')
|
||||
return res[0]
|
||||
})
|
||||
} catch (e) {
|
||||
// @ts-ignore Log whatever, we don't care
|
||||
console.error(e)
|
||||
throw new Error('Could not add email address to database.')
|
||||
}
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* Add a subscriber:
|
||||
* 1. Add a record to subscribers
|
||||
* 2. Immediately call _verifySubscriber
|
||||
* 3. For FxA subscriber, add refresh token and profile data
|
||||
*
|
||||
* @param {string} email to add
|
||||
* @param {string} signupLanguage from Accept-Language
|
||||
* @param {string | null} fxaAccessToken from Firefox Account Oauth
|
||||
* @param {string | null} fxaRefreshToken from Firefox Account Oauth
|
||||
* @param {number} sessionExpiresAt from Firefox Account Oauth
|
||||
* @param {string | null} fxaProfileData from Firefox Account
|
||||
* @returns {Promise<import('knex/types/tables').SubscriberRow>} subscriber knex object added to DB
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function addSubscriber (email, signupLanguage, fxaAccessToken = null, fxaRefreshToken = null, sessionExpiresAt = 0, fxaProfileData = null) {
|
||||
const lowerCaseEmail = email.toLowerCase()
|
||||
const emailHash = await _addEmailHash(getSha1(lowerCaseEmail), lowerCaseEmail, signupLanguage, true)
|
||||
const verified = await _verifySubscriber(emailHash)
|
||||
const verifiedSubscriber = Array.isArray(verified) ? verified[0] : null
|
||||
if (fxaRefreshToken || fxaProfileData) {
|
||||
return updateFxAData(verifiedSubscriber, fxaAccessToken, fxaRefreshToken, sessionExpiresAt, fxaProfileData)
|
||||
}
|
||||
return verifiedSubscriber
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* When an email is verified, convert it into a subscriber:
|
||||
* 1. Subscribe the hash to HIBP
|
||||
* 2. Update our subscribers record to verified
|
||||
* 3. (if opted in) Subscribe the email to Fx newsletter
|
||||
*
|
||||
* @param {any} emailHash knex object in DB
|
||||
* @returns {Promise<any>} verified subscriber knex object in DB
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function _verifySubscriber (emailHash) {
|
||||
await subscribeHash(emailHash.primary_sha1)
|
||||
|
||||
const verifiedSubscriber = await knex('subscribers')
|
||||
.where('primary_email', '=', emailHash.primary_email)
|
||||
.update({
|
||||
primary_verified: true,
|
||||
// @ts-ignore knex.fn.now() results in it being set to a date,
|
||||
// even if it's not typed as a JS date object:
|
||||
updated_at: knex.fn.now()
|
||||
})
|
||||
.returning('*')
|
||||
|
||||
return verifiedSubscriber
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Verifies new emails added by existing users
|
||||
/**
|
||||
* @param {{ sha1: string; id: number; }} emailHash
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function _verifyNewEmail (emailHash) {
|
||||
await subscribeHash(emailHash.sha1)
|
||||
|
||||
const verifiedEmail = await knex('email_addresses')
|
||||
.where('id', '=', emailHash.id)
|
||||
.update({
|
||||
verified: true,
|
||||
// @ts-ignore knex.fn.now() results in it being set to a date,
|
||||
// even if it's not typed as a JS date object:
|
||||
updated_at: knex.fn.now(),
|
||||
})
|
||||
.returning('*')
|
||||
|
||||
return verifiedEmail
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* @param {number} userId
|
||||
* @returns {Promise<import('knex/types/tables').EmailAddressRow[]>}
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function getUserEmails (userId) {
|
||||
const userEmails = await knex('email_addresses')
|
||||
.where('subscriber_id', '=', userId)
|
||||
.returning('*')
|
||||
|
||||
return userEmails
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// This is used by SES callbacks to remove email addresses when recipients
|
||||
// perma-bounce or mark our emails as spam
|
||||
// Removes from either subscribers or email_addresses as necessary
|
||||
/**
|
||||
* @param {string} email
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function removeEmail (email) {
|
||||
const subscriber = await getSubscriberByEmail(email)
|
||||
if (!subscriber) {
|
||||
const emailAddress = await getEmailAddressRecordByEmail(email)
|
||||
if (!emailAddress) {
|
||||
console.warn('removed-subscriber-not-found')
|
||||
return
|
||||
}
|
||||
await knex('email_addresses')
|
||||
.where({
|
||||
email,
|
||||
verified: true
|
||||
})
|
||||
.del()
|
||||
return
|
||||
}
|
||||
// This can fail if a subscriber has more email_addresses and marks
|
||||
// a primary email as spam, but we should let it fail so we can see it
|
||||
// in the logs
|
||||
await knex('subscribers')
|
||||
.where({
|
||||
primary_verification_token: subscriber.primary_verification_token,
|
||||
primary_sha1: subscriber.primary_sha1
|
||||
})
|
||||
.del()
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* @param {number} emailId
|
||||
* @param {number} subscriberId
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function removeOneSecondaryEmail (emailId, subscriberId) {
|
||||
await knex('email_addresses')
|
||||
.where("id", emailId)
|
||||
.andWhere("subscriber_id", subscriberId)
|
||||
.del()
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* @param {string[]} hashes
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function getEmailAddressesByHashes (hashes) {
|
||||
return await knex('email_addresses')
|
||||
.join('subscribers', 'email_addresses.subscriber_id', '=', 'subscribers.id')
|
||||
.whereIn('email_addresses.sha1', hashes)
|
||||
.andWhere('email_addresses.verified', '=', true)
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* @param {string} uid
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function deleteEmailAddressesByUid (uid) {
|
||||
await knex('email_addresses').where("subscriber_id", uid).del()
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
export {
|
||||
getEmailByToken,
|
||||
getEmailById,
|
||||
getEmailAddressRecordByEmail,
|
||||
addSubscriberUnverifiedEmailHash,
|
||||
resetUnverifiedEmailAddress,
|
||||
verifyEmailHash,
|
||||
addSubscriber,
|
||||
getUserEmails,
|
||||
removeEmail,
|
||||
removeOneSecondaryEmail,
|
||||
getEmailAddressesByHashes,
|
||||
deleteEmailAddressesByUid,
|
||||
knex as knexEmailAddresses
|
||||
}
|
|
@ -0,0 +1,401 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import createDbConnection from "../connect.js";
|
||||
import { subscribeHash } from "../../utils/hibp";
|
||||
import { getSha1 } from "../../utils/fxa";
|
||||
import { getSubscriberByEmail, updateFxAData } from "./subscribers.js";
|
||||
import { ForbiddenError, UnauthorizedError } from "../../utils/error";
|
||||
import { EmailAddressRow, SubscriberRow } from "knex/types/tables";
|
||||
import { ReactLocalization } from "@fluent/react";
|
||||
|
||||
const knex = createDbConnection();
|
||||
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function getEmailByToken(token: string) {
|
||||
const res = await knex("email_addresses").where(
|
||||
"verification_token",
|
||||
"=",
|
||||
token,
|
||||
);
|
||||
|
||||
return res[0];
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function getEmailById(emailAddressId: number) {
|
||||
const res = await knex("email_addresses").where("id", "=", emailAddressId);
|
||||
|
||||
return res[0];
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function getEmailAddressRecordByEmail(email: string) {
|
||||
const emailAddresses = await knex("email_addresses").where({
|
||||
email,
|
||||
verified: true,
|
||||
});
|
||||
if (!emailAddresses) {
|
||||
return null;
|
||||
}
|
||||
if (emailAddresses.length > 1) {
|
||||
// TODO: handle multiple emails in separate(?) subscriber accounts?
|
||||
console.warn("getEmailAddressRecordByEmail", {
|
||||
msg: "found the same email multiple times",
|
||||
});
|
||||
}
|
||||
return emailAddresses[0];
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function addSubscriberUnverifiedEmailHash(
|
||||
user: SubscriberRow,
|
||||
email: string,
|
||||
) {
|
||||
const lowerCaseEmail = email.toLowerCase();
|
||||
const res = await knex.transaction((trx) => {
|
||||
return trx("email_addresses")
|
||||
.forUpdate()
|
||||
.select({
|
||||
subscriber_id: user.id,
|
||||
})
|
||||
.insert({
|
||||
subscriber_id: user.id,
|
||||
email: lowerCaseEmail,
|
||||
sha1: getSha1(lowerCaseEmail),
|
||||
verification_token: uuidv4(),
|
||||
verified: false,
|
||||
})
|
||||
.returning("*");
|
||||
});
|
||||
return res[0];
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function resetUnverifiedEmailAddress(
|
||||
emailAddressId: EmailAddressRow["id"],
|
||||
subscriber: SubscriberRow,
|
||||
l10n: ReactLocalization,
|
||||
) {
|
||||
const newVerificationToken = uuidv4();
|
||||
|
||||
// Time in ms to require between verification reset.
|
||||
const verificationWait = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
const verificationRecentlyUpdated = await knex("email_addresses")
|
||||
.select("id")
|
||||
.whereRaw(
|
||||
"\"updated_at\" > NOW() - INTERVAL '1 MILLISECOND' * ?",
|
||||
verificationWait,
|
||||
)
|
||||
.andWhere("id", emailAddressId)
|
||||
.first();
|
||||
|
||||
if (
|
||||
verificationRecentlyUpdated?.id ===
|
||||
(typeof emailAddressId === "number"
|
||||
? emailAddressId
|
||||
: parseInt(emailAddressId, 10))
|
||||
) {
|
||||
throw new ForbiddenError(l10n.getString("error-email-validation-pending"));
|
||||
}
|
||||
|
||||
const res = await knex("email_addresses")
|
||||
.update({
|
||||
verification_token: newVerificationToken,
|
||||
// @ts-ignore knex.fn.now() results in it being set to a date,
|
||||
// even if it's not typed as a JS date object:
|
||||
updated_at: knex.fn.now(),
|
||||
})
|
||||
.where("id", emailAddressId)
|
||||
.andWhere("subscriber_id", subscriber.id)
|
||||
.returning("*");
|
||||
return res[0];
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function verifyEmailHash(token: string) {
|
||||
const unverifiedEmail = await getEmailByToken(token);
|
||||
if (!unverifiedEmail) {
|
||||
throw new UnauthorizedError(
|
||||
"Error message for this verification email timed out or something went wrong.",
|
||||
);
|
||||
}
|
||||
const verifiedEmail = await _verifyNewEmail(unverifiedEmail);
|
||||
return verifiedEmail[0];
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// TODO: refactor into an upsert? https://jaketrent.com/post/upsert-knexjs/
|
||||
// Used internally, ideally should not be called by consumers.
|
||||
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function _getSha1EntryAndDo(
|
||||
sha1: string,
|
||||
aFoundCallback: (existingSubscriber: SubscriberRow) => Promise<SubscriberRow>,
|
||||
aNotFoundCallback: () => Promise<SubscriberRow>,
|
||||
) {
|
||||
const existingEntries = await knex("subscribers").where("primary_sha1", sha1);
|
||||
|
||||
if (existingEntries.length && aFoundCallback) {
|
||||
return await aFoundCallback(existingEntries[0]);
|
||||
}
|
||||
|
||||
if (!existingEntries.length && aNotFoundCallback) {
|
||||
return await aNotFoundCallback();
|
||||
}
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Used internally.
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
|
||||
async function _addEmailHash(
|
||||
sha1: string,
|
||||
email: string,
|
||||
signupLanguage: string,
|
||||
verified: boolean = false,
|
||||
): Promise<SubscriberRow | undefined> {
|
||||
try {
|
||||
return await _getSha1EntryAndDo(
|
||||
sha1,
|
||||
async (aEntry: SubscriberRow) => {
|
||||
// Entry existed, patch the email value if supplied.
|
||||
if (email) {
|
||||
const res = await knex("subscribers")
|
||||
.update({
|
||||
primary_email: email,
|
||||
primary_sha1: getSha1(email.toLowerCase()),
|
||||
primary_verified: verified,
|
||||
// @ts-ignore knex.fn.now() results in it being set to a date,
|
||||
// even if it's not typed as a JS date object:
|
||||
updated_at: knex.fn.now(),
|
||||
})
|
||||
.where("id", "=", aEntry.id)
|
||||
.returning("*");
|
||||
return res[0];
|
||||
}
|
||||
|
||||
return aEntry;
|
||||
},
|
||||
async () => {
|
||||
// Always add a verification_token value
|
||||
const verificationToken = uuidv4();
|
||||
const res = await knex("subscribers")
|
||||
.insert({
|
||||
primary_sha1: getSha1(email.toLowerCase()),
|
||||
primary_email: email,
|
||||
signup_language: signupLanguage,
|
||||
primary_verification_token: verificationToken,
|
||||
primary_verified: verified,
|
||||
})
|
||||
.returning("*");
|
||||
return res[0];
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// @ts-ignore Log whatever, we don't care
|
||||
console.error(e);
|
||||
throw new Error("Could not add email address to database.");
|
||||
}
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* Add a subscriber:
|
||||
* 1. Add a record to subscribers
|
||||
* 2. Immediately call _verifySubscriber
|
||||
* 3. For FxA subscriber, add refresh token and profile data
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
// subscriber knex object added to DB
|
||||
/* c8 ignore start */
|
||||
async function addSubscriber(
|
||||
email: string, // to add
|
||||
signupLanguage: string, // from Accept-Language
|
||||
fxaAccessToken: string | null = null, // from Firefox Account Oauth
|
||||
fxaRefreshToken: string | null = null, // from Firefox Account Oauth
|
||||
sessionExpiresAt: number = 0, // from Firefox Account Oauth
|
||||
fxaProfileData: string | null = null,
|
||||
): // from Firefox Account
|
||||
Promise<SubscriberRow | null> {
|
||||
// subscriber knex object added to DB
|
||||
const lowerCaseEmail = email.toLowerCase();
|
||||
const emailHash = await _addEmailHash(
|
||||
getSha1(lowerCaseEmail),
|
||||
lowerCaseEmail,
|
||||
signupLanguage,
|
||||
true,
|
||||
);
|
||||
|
||||
if (!emailHash) {
|
||||
throw new Error("Email hash undefined");
|
||||
}
|
||||
|
||||
const verified = await _verifySubscriber(emailHash);
|
||||
const verifiedSubscriber = Array.isArray(verified) ? verified[0] : null;
|
||||
if (fxaRefreshToken || fxaProfileData) {
|
||||
return updateFxAData(
|
||||
verifiedSubscriber,
|
||||
fxaAccessToken,
|
||||
fxaRefreshToken,
|
||||
sessionExpiresAt,
|
||||
fxaProfileData,
|
||||
) as Promise<SubscriberRow | null>;
|
||||
}
|
||||
return verifiedSubscriber;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* When an email is verified, convert it into a subscriber:
|
||||
* 1. Subscribe the hash to HIBP
|
||||
* 2. Update our subscribers record to verified
|
||||
* 3. (if opted in) Subscribe the email to Fx newsletter
|
||||
*/
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
|
||||
async function _verifySubscriber(emailHash: SubscriberRow) {
|
||||
await subscribeHash(emailHash.primary_sha1);
|
||||
|
||||
const verifiedSubscriber = await knex("subscribers")
|
||||
.where("primary_email", "=", emailHash.primary_email)
|
||||
.update({
|
||||
primary_verified: true,
|
||||
// @ts-ignore knex.fn.now() results in it being set to a date,
|
||||
// even if it's not typed as a JS date object:
|
||||
updated_at: knex.fn.now(),
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
return verifiedSubscriber;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Verifies new emails added by existing users
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
|
||||
type NewEmailType = {
|
||||
sha1: SubscriberRow["primary_sha1"];
|
||||
id: SubscriberRow["id"];
|
||||
};
|
||||
async function _verifyNewEmail(emailHash: NewEmailType) {
|
||||
await subscribeHash(emailHash.sha1);
|
||||
|
||||
const verifiedEmail = await knex("email_addresses")
|
||||
.where("id", "=", emailHash.id)
|
||||
.update({
|
||||
verified: true,
|
||||
// @ts-ignore knex.fn.now() results in it being set to a date,
|
||||
// even if it's not typed as a JS date object:
|
||||
updated_at: knex.fn.now(),
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
return verifiedEmail;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function getUserEmails(userId: number): Promise<EmailAddressRow[]> {
|
||||
const userEmails = await knex("email_addresses")
|
||||
.where("subscriber_id", "=", userId)
|
||||
.returning("*");
|
||||
|
||||
return userEmails;
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// This is used by SES callbacks to remove email addresses when recipients
|
||||
// perma-bounce or mark our emails as spam
|
||||
// Removes from either subscribers or email_addresses as necessary
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function removeEmail(email: string) {
|
||||
const subscriber = await getSubscriberByEmail(email);
|
||||
if (!subscriber) {
|
||||
const emailAddress = await getEmailAddressRecordByEmail(email);
|
||||
if (!emailAddress) {
|
||||
console.warn("removed-subscriber-not-found");
|
||||
return;
|
||||
}
|
||||
await knex("email_addresses")
|
||||
.where({
|
||||
email,
|
||||
verified: true,
|
||||
})
|
||||
.del();
|
||||
return;
|
||||
}
|
||||
// This can fail if a subscriber has more email_addresses and marks
|
||||
// a primary email as spam, but we should let it fail so we can see it
|
||||
// in the logs
|
||||
await knex("subscribers")
|
||||
.where({
|
||||
primary_verification_token: subscriber.primary_verification_token,
|
||||
primary_sha1: subscriber.primary_sha1,
|
||||
})
|
||||
.del();
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function removeOneSecondaryEmail(emailId: number, subscriberId: number) {
|
||||
await knex("email_addresses")
|
||||
.where("id", emailId)
|
||||
.andWhere("subscriber_id", subscriberId)
|
||||
.del();
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function getEmailAddressesByHashes(hashes: string[]) {
|
||||
return await knex("email_addresses")
|
||||
.join("subscribers", "email_addresses.subscriber_id", "=", "subscribers.id")
|
||||
.whereIn("email_addresses.sha1", hashes)
|
||||
.andWhere("email_addresses.verified", "=", true);
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
|
||||
/* c8 ignore start */
|
||||
async function deleteEmailAddressesByUid(uid: string) {
|
||||
await knex("email_addresses").where("subscriber_id", uid).del();
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
export {
|
||||
getEmailByToken,
|
||||
getEmailById,
|
||||
getEmailAddressRecordByEmail,
|
||||
addSubscriberUnverifiedEmailHash,
|
||||
resetUnverifiedEmailAddress,
|
||||
verifyEmailHash,
|
||||
addSubscriber,
|
||||
getUserEmails,
|
||||
removeEmail,
|
||||
removeOneSecondaryEmail,
|
||||
getEmailAddressesByHashes,
|
||||
deleteEmailAddressesByUid,
|
||||
knex as knexEmailAddresses,
|
||||
};
|
|
@ -2,83 +2,52 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { BreachRow } from "knex/types/tables";
|
||||
import createDbConnection from "../connect.js";
|
||||
|
||||
const knex = createDbConnection();
|
||||
|
||||
/**
|
||||
* @param {number} subscriberId
|
||||
*/
|
||||
async function getAllEmailNotificationsForSubscriber(subscriberId){
|
||||
/**
|
||||
* @param {number} subscriberId
|
||||
*/
|
||||
async function getAllEmailNotificationsForSubscriber(subscriberId: number) {
|
||||
console.info("getAllEmailNotificationsForSubscriber: ", subscriberId);
|
||||
return await knex.transaction(trx => {
|
||||
return trx('email_notifications')
|
||||
return await knex.transaction((trx) => {
|
||||
return trx("email_notifications")
|
||||
.forUpdate()
|
||||
.select()
|
||||
.where("subscriber_id", subscriberId)
|
||||
.orderBy("id");
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} subscriberId
|
||||
* @param {number} breachId
|
||||
* @param {string} email
|
||||
*/
|
||||
async function getEmailNotification(
|
||||
subscriberId,
|
||||
breachId,
|
||||
email
|
||||
){
|
||||
console.info(
|
||||
`getEmailNotification for subscriber: ${subscriberId}, breach: ${breachId}`,
|
||||
);
|
||||
const res = await knex.transaction(trx => {
|
||||
return trx('email_notifications')
|
||||
.forUpdate()
|
||||
.select()
|
||||
.where("subscriber_id", subscriberId)
|
||||
.andWhere("breach_id", breachId)
|
||||
.andWhere("email", email);
|
||||
})
|
||||
|
||||
if (res.length > 1) {
|
||||
console.error(
|
||||
"More than one entry for subscriber/breach email notification: ",
|
||||
res,
|
||||
);
|
||||
}
|
||||
|
||||
return res?.[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} breachId
|
||||
*/
|
||||
async function getNotifiedSubscribersForBreach(
|
||||
breachId
|
||||
){
|
||||
breachId: BreachRow["id"],
|
||||
): Promise<number[]> {
|
||||
console.info(
|
||||
`getEmailNotificationSubscribersForBreach for breach: ${breachId}`,
|
||||
);
|
||||
|
||||
const res = await knex.transaction(trx => {
|
||||
return trx('email_notifications')
|
||||
const res = await knex.transaction((trx) => {
|
||||
return trx("email_notifications")
|
||||
.forUpdate()
|
||||
.select("subscriber_id")
|
||||
.where("notified", true)
|
||||
.andWhere("breach_id", breachId);
|
||||
})
|
||||
});
|
||||
|
||||
return res.map((row) => row.subscriber_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ subscriberId: number; breachId: number; notified: boolean; email: string; notificationType: string; }} newNotification
|
||||
*/
|
||||
async function addEmailNotification(
|
||||
newNotification
|
||||
){
|
||||
type NewNotification = {
|
||||
breachId: number;
|
||||
subscriberId: number;
|
||||
notified: boolean;
|
||||
email: string;
|
||||
notificationType: string;
|
||||
};
|
||||
|
||||
async function addEmailNotification(newNotification: NewNotification) {
|
||||
console.info(`addEmailNotification: ${JSON.stringify(newNotification)}`);
|
||||
const emailNotificationDb = {
|
||||
subscriber_id: newNotification.subscriberId,
|
||||
|
@ -88,29 +57,24 @@ async function addEmailNotification(
|
|||
email: newNotification.email,
|
||||
notification_type: newNotification.notificationType,
|
||||
};
|
||||
|
||||
const res = await knex.transaction(trx => {
|
||||
return trx('email_notifications')
|
||||
.forUpdate()
|
||||
.insert(emailNotificationDb)
|
||||
.returning("*");
|
||||
});
|
||||
|
||||
const res = await knex.transaction((trx) => {
|
||||
return trx("email_notifications")
|
||||
.forUpdate()
|
||||
.insert(emailNotificationDb)
|
||||
.returning("*");
|
||||
});
|
||||
return res[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} subscriberId
|
||||
* @param {number} breachId
|
||||
* @param {string} email
|
||||
*/
|
||||
async function markEmailAsNotified(
|
||||
subscriberId,
|
||||
breachId,
|
||||
email
|
||||
subscriberId: number,
|
||||
breachId: BreachRow["id"],
|
||||
email: string,
|
||||
) {
|
||||
console.info(`markEmailAsNotified for breach: ${breachId}`);
|
||||
await knex.transaction(trx => {
|
||||
return trx('email_notifications')
|
||||
await knex.transaction((trx) => {
|
||||
return trx("email_notifications")
|
||||
.forUpdate()
|
||||
.where("subscriber_id", subscriberId)
|
||||
.andWhere("breach_id", breachId)
|
||||
|
@ -121,13 +85,12 @@ async function markEmailAsNotified(
|
|||
// even if it's not typed as a JS date object:
|
||||
updated_at: knex.fn.now(),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
getAllEmailNotificationsForSubscriber,
|
||||
getEmailNotification,
|
||||
getNotifiedSubscribersForBreach,
|
||||
addEmailNotification,
|
||||
markEmailAsNotified
|
||||
}
|
||||
markEmailAsNotified,
|
||||
};
|
|
@ -14,7 +14,7 @@ jest.mock("@sentry/nextjs", () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock("../../utils/email.js", () => {
|
||||
jest.mock("../../utils/email", () => {
|
||||
return {
|
||||
initEmail: jest.fn(),
|
||||
EmailTemplateType: jest.fn(),
|
||||
|
@ -43,13 +43,13 @@ jest.mock("../../db/tables/subscribers.js", () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock("../../db/tables/emailAddresses.js", () => {
|
||||
jest.mock("../../db/tables/emailAddresses", () => {
|
||||
return {
|
||||
getEmailAddressesByHashes: jest.fn(() => [""]),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock("../../db/tables/email_notifications.js", () => {
|
||||
jest.mock("../../db/tables/email_notifications", () => {
|
||||
return {
|
||||
getNotifiedSubscribersForBreach: jest.fn(() => [""]),
|
||||
addEmailNotification: jest.fn(),
|
||||
|
@ -368,13 +368,13 @@ test("skipping email when subscriber id exists in email_notifications table", as
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock("../../db/tables/emailAddresses.js", () => {
|
||||
jest.mock("../../db/tables/emailAddresses", () => {
|
||||
return {
|
||||
getEmailAddressesByHashes: jest.fn(() => []),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock("../../db/tables/email_notifications.js", () => {
|
||||
jest.mock("../../db/tables/email_notifications", () => {
|
||||
return {
|
||||
getNotifiedSubscribersForBreach: jest.fn(() => [1]),
|
||||
addEmailNotification: jest.fn(),
|
||||
|
@ -423,13 +423,13 @@ test("throws an error when addEmailNotification fails", async () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock("../../db/tables/emailAddresses.js", () => {
|
||||
jest.mock("../../db/tables/emailAddresses", () => {
|
||||
return {
|
||||
getEmailAddressesByHashes: jest.fn(() => [""]),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock("../../db/tables/email_notifications.js", () => {
|
||||
jest.mock("../../db/tables/email_notifications", () => {
|
||||
return {
|
||||
getNotifiedSubscribersForBreach: jest.fn(() => [2]),
|
||||
addEmailNotification: jest.fn().mockImplementationOnce(() => {
|
||||
|
@ -482,13 +482,13 @@ test("throws an error when markEmailAsNotified fails", async () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock("../../db/tables/emailAddresses.js", () => {
|
||||
jest.mock("../../db/tables/emailAddresses", () => {
|
||||
return {
|
||||
getEmailAddressesByHashes: jest.fn(() => [""]),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock("../../db/tables/email_notifications.js", () => {
|
||||
jest.mock("../../db/tables/email_notifications", () => {
|
||||
return {
|
||||
getNotifiedSubscribersForBreach: jest.fn(() => [2]),
|
||||
addEmailNotification: jest.fn(),
|
||||
|
|
|
@ -19,12 +19,12 @@ import {
|
|||
import {
|
||||
getEmailAddressesByHashes,
|
||||
knexEmailAddresses,
|
||||
} from "../../db/tables/emailAddresses.js";
|
||||
} from "../../db/tables/emailAddresses";
|
||||
import {
|
||||
getNotifiedSubscribersForBreach,
|
||||
addEmailNotification,
|
||||
markEmailAsNotified,
|
||||
} from "../../db/tables/email_notifications.js";
|
||||
} from "../../db/tables/email_notifications";
|
||||
import { getTemplate } from "../../emails/email2022.js";
|
||||
import { breachAlertEmailPartial } from "../../emails/emailBreachAlert.js";
|
||||
import {
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
getAllBreaches,
|
||||
upsertBreaches,
|
||||
updateBreachFaviconUrl,
|
||||
} from "../../db/tables/breaches.js";
|
||||
} from "../../db/tables/breaches";
|
||||
import { uploadToS3 } from "../s3.js";
|
||||
|
||||
const SENTRY_SLUG = "cron-sync-breaches";
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { getUserEmails } from "../db/tables/emailAddresses.js";
|
||||
import { getUserEmails } from "../db/tables/emailAddresses";
|
||||
import {
|
||||
getBreachesForEmail,
|
||||
getFilteredBreaches,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import AppConstants from "../appConstants.js";
|
||||
import { getAllBreaches, knex } from "../db/tables/breaches.js";
|
||||
import { getAllBreaches, knex } from "../db/tables/breaches";
|
||||
import { isUsingMockHIBPEndpoint } from "../app/functions/universal/mock.ts";
|
||||
import { BreachRow, EmailAddressRow, SubscriberRow } from "knex/types/tables";
|
||||
import { ISO8601DateString } from "./parse.js";
|
||||
|
|
|
@ -7,7 +7,7 @@ import { getBreachesForEmail, HibpLikeDbBreach } from "./hibp";
|
|||
import { getSubBreaches } from "./subscriberBreaches";
|
||||
import { getUserEmails } from "../db/tables/emailAddresses";
|
||||
|
||||
jest.mock("../db/tables/emailAddresses.js", () => ({
|
||||
jest.mock("../db/tables/emailAddresses", () => ({
|
||||
getUserEmails: jest.fn(),
|
||||
}));
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
SubscriberBreachResolution,
|
||||
SubscriberRow,
|
||||
} from "knex/types/tables";
|
||||
import { getUserEmails } from "../db/tables/emailAddresses.js";
|
||||
import { getUserEmails } from "../db/tables/emailAddresses";
|
||||
import { HibpLikeDbBreach, getBreachesForEmail } from "./hibp";
|
||||
import { getSha1 } from "./fxa";
|
||||
import { parseIso8601Datetime } from "./parse";
|
||||
|
|
Загрузка…
Ссылка в новой задаче