Skip to content

Commit

Permalink
MNTOR-3435 - Convert Breaches.js to Typescript (#4876)
Browse files Browse the repository at this point in the history
* Migrate utils/hibp.js to TypeScript

This removes the `Breach` type in functions/universal/breaches,
which was created when first introducing TypeScript and the flow
of data was still unclear, but by now had overlap with other types
and no clear provenance.

Instead, there are now three breach-related types, that represent
where the data came from:

- HibpGetBreachesResponse: this is an array of breach elements as
                           returned from the HIBP API, unprocessed.
                           Properties are in PascalCase, so are a
                           breach's data classes.
- BreachRow: this is a breach's data as stored in our database,
             along with some data we added to it, such as a favicon
             URL. Properties are snake_case, and data classes are
             lowercased and kebab-cased by the
             formatDataClassesArray function.
- HibpLikeDbBreach: this is a breach's data fetched from the
                    database, but stored in an object meant to look
                    like the ones in HibpGetBreachesResponse. In
                    other words, it contains the same data as
                    BreachRow (including lowercased, kebab-cased
                    data classes), but on PascalCase properties.

The latter is somewhat of a historical artefact, because we used
to try to load breaches from our database, then if our database
didn't contain any breaches yet, fetch them live from the HIBP API
and continue working with that.

We no longer do that: now, even after fetching them from the HIBP
API, we do a new query to get them from the database and process
them into HibpLikeDbBreach, so that we can assume a consisent data
structure everywhere we work with breaches.

* MNTOR-3435 - breaches js to ts

* remove old typedef

* explicitly type breach as any

* update path

* update breaches path

* Fix mistaken conflict resolutions

* remove recency code

* update fxa path link

* remove comments

---------

Co-authored-by: Vincent <git@vincenttunru.com>
  • Loading branch information
codemist and Vinnl authored Aug 5, 2024
1 parent 73a4ab7 commit 324cebb
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 165 deletions.
4 changes: 1 addition & 3 deletions src/app/functions/server/breachResolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { getL10n } from "../l10n/serverComponents";
import AppConstants from "../../../appConstants.js";
import { BreachDataTypes } from "../universal/breach";
import { AllEmailsAndBreaches } from "../../../utils/breaches.js";
import { AllEmailsAndBreaches } from "../../../utils/breaches";

/**
* TODO: Map from google doc: https://docs.google.com/document/d/1KoItFsTYVIBInIG2YmA7wSxkKS4vti_X0A0td_yaHVM/edit#
Expand Down Expand Up @@ -99,8 +99,6 @@ function appendBreachResolutionChecklist(
const { verifiedEmails } = userBreachData;

for (const { breaches } of verifiedEmails) {
// Old untyped code, adding type defitions now isn't worth the effort:
/* eslint-disable @typescript-eslint/no-explicit-any */
breaches.forEach((b) => {
const dataClasses = b.DataClasses;
const blockList = (AppConstants.HIBP_BREACH_DOMAIN_BLOCKLIST ?? "").split(
Expand Down
2 changes: 1 addition & 1 deletion src/app/functions/server/getBreaches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function getBreaches(): Promise<HibpLikeDbBreach[]> {
return breaches;
}
breaches = await getAllBreachesFromDb();
logger.debug("loaded breaches from database", {
logger.debug("loaded_breaches_from_database", {
breachesLength: breaches.length,
});

Expand Down
2 changes: 1 addition & 1 deletion src/knex-tables.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ declare module "knex/types/tables" {
fxa_uid: null | string;
// TODO: Find unknown type
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
breaches_last_shown: null | unknown;
breaches_last_shown: Date;
// NOTE: this field is inherited from an older version of the product, it only applies to instant alerts
all_emails_to_primary: boolean | null; // added null in MNTOR-1368
// TODO: Find unknown type
Expand Down
160 changes: 0 additions & 160 deletions src/utils/breaches.js

This file was deleted.

166 changes: 166 additions & 0 deletions src/utils/breaches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/* 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 { getUserEmails } from "../db/tables/emailAddresses.js";
import {
getBreachesForEmail,
getFilteredBreaches,
HibpLikeDbBreach,
} from "./hibp";
import { getSha1 } from "./fxa";
import { captureMessage } from "@sentry/node";
import { EmailAddressRow, SubscriberRow } from "knex/types/tables";

export type BundledVerifiedEmails = {
email: string;
breaches: HibpLikeDbBreach[];
id: number;
primary: boolean;
verified: boolean;
hasNewBreaches?: number;
};

export type AllEmailsAndBreaches = {
unverifiedEmails: EmailAddressRow[];
verifiedEmails: BundledVerifiedEmails[];
};

type userType =
| ({
email_addresses: Array<{
id: EmailAddressRow["id"];
email: EmailAddressRow["email"];
}>;
} & SubscriberRow)
| undefined;

async function getAllEmailsAndBreaches(
user: userType,
allBreaches: HibpLikeDbBreach[],
): Promise<AllEmailsAndBreaches> {
const verifiedEmails: BundledVerifiedEmails[] = [];
const unverifiedEmails: EmailAddressRow[] = [];

if (!user) {
const errMsg = "getAllEmailsAndBreaches: subscriber cannot be undefined";
console.error(errMsg);
captureMessage(errMsg);

return { verifiedEmails, unverifiedEmails };
}
if (!allBreaches || allBreaches.length === 0) {
const errMsg =
"getAllEmailsAndBreaches: allBreaches object cannot be empty";
console.error(errMsg);
captureMessage(errMsg);

return { verifiedEmails, unverifiedEmails };
}

const monitoredEmails = await getUserEmails(user.id);
verifiedEmails.push(
await bundleVerifiedEmails({
user,
email: user.primary_email,
recordId: user.id,
recordVerified: user.primary_verified,
allBreaches,
}),
);
for (const email of monitoredEmails) {
if (email.verified) {
verifiedEmails.push(
await bundleVerifiedEmails({
user,
email: user.primary_email,
recordId: email.id,
recordVerified: email.verified,
allBreaches,
}),
);
} else {
unverifiedEmails.push(email);
}
}

// get new breaches since last shown
for (const emailEntry of verifiedEmails) {
const newBreachesForEmail = emailEntry.breaches.filter(
(breach) => breach.AddedDate >= user.breaches_last_shown,
);

for (const newBreachForEmail of newBreachesForEmail) {
newBreachForEmail.NewBreach = true; // add "NewBreach" property to the new breach.
emailEntry.hasNewBreaches = newBreachesForEmail.length; // add the number of new breaches to the email
}
}

return { verifiedEmails, unverifiedEmails };
}

function addRecencyIndex(foundBreaches: HibpLikeDbBreach[]) {
const annotatedBreaches: HibpLikeDbBreach[] = [];
// slice() the array to make a copy so before reversing so we don't
// reverse foundBreaches in-place
const oldestToNewestFoundBreaches = foundBreaches.slice().reverse();
oldestToNewestFoundBreaches.forEach((annotatingBreach, index) => {
const foundBreach = foundBreaches.find(
(foundBreach) => foundBreach.Name === annotatingBreach.Name,
);
annotatedBreaches.push(Object.assign({ recencyIndex: index }, foundBreach));
});
return annotatedBreaches.reverse();
}

type options = {
user: userType;
email: string;
recordId: number;
recordVerified: boolean;
allBreaches: HibpLikeDbBreach[];
};
async function bundleVerifiedEmails(
options: options,
): Promise<BundledVerifiedEmails> {
const { user, email, recordId, recordVerified, allBreaches } = options;
const lowerCaseEmailSha = getSha1(email.toLowerCase());

// find all breaches relevant to the current email
const foundBreaches = await getBreachesForEmail(
lowerCaseEmailSha,
allBreaches,
true,
false,
);

// TODO: remove after migration MNTOR-978
// adding index to breaches based on recency
const foundBreachesWithRecency = addRecencyIndex(foundBreaches);

if (!user) {
const errMsg = "breachResolutionV2: subscriber cannot be undefined";
console.error(errMsg);
captureMessage(errMsg);

// @ts-ignore: function will be deprecated
return { verifiedEmails, unverifiedEmails };
}

// filter out irrelevant breaches based on HIBP
const filteredAnnotatedFoundBreaches = getFilteredBreaches(
foundBreachesWithRecency,
);

const emailEntry: BundledVerifiedEmails = {
email: email,
breaches: filteredAnnotatedFoundBreaches,
primary: email === user.primary_email,
id: recordId,
verified: recordVerified,
};

return emailEntry;
}

export { getAllEmailsAndBreaches };
1 change: 1 addition & 0 deletions src/utils/hibp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ export type HibpLikeDbBreach = {
IsSpamList: BreachRow["is_spam_list"];
IsMalware: BreachRow["is_malware"];
FaviconUrl?: BreachRow["favicon_url"];
NewBreach?: boolean;
};

/**
Expand Down

0 comments on commit 324cebb

Please sign in to comment.