From 71463b53896859881b9e9c760eb260f402114138 Mon Sep 17 00:00:00 2001 From: Mukhamediyar Kudaikulov Date: Wed, 10 Jul 2024 23:46:52 -0700 Subject: [PATCH 1/8] first ideas for better QA tool --- .../api/mock/hibp/mockData/mockOverwrite.json | 5 ++ .../hibp/range/search/[hashPrefix]/route.ts | 15 +++++- src/app/api/mock/onerep/config/config.ts | 34 ++++++++----- .../mock/onerep/mockData/mockOverwrite.json | 9 ++++ src/app/api/mock/resetTestData/reset.ts | 50 +++++++++++++++++++ .../{clearTestData => resetTestData}/route.ts | 34 ++++--------- src/app/api/utils/errorThrower.ts | 4 +- .../20240710214906_qa_custom_breaches.js | 15 ++++++ 8 files changed, 127 insertions(+), 39 deletions(-) create mode 100644 src/app/api/mock/hibp/mockData/mockOverwrite.json create mode 100644 src/app/api/mock/onerep/mockData/mockOverwrite.json create mode 100644 src/app/api/mock/resetTestData/reset.ts rename src/app/api/mock/{clearTestData => resetTestData}/route.ts (53%) create mode 100644 src/db/migrations/20240710214906_qa_custom_breaches.js diff --git a/src/app/api/mock/hibp/mockData/mockOverwrite.json b/src/app/api/mock/hibp/mockData/mockOverwrite.json new file mode 100644 index 00000000000..6f87ff9ed43 --- /dev/null +++ b/src/app/api/mock/hibp/mockData/mockOverwrite.json @@ -0,0 +1,5 @@ +{ + "doOvewrite": false, + "breachesOverwrite": [], + "breachesAdd": [] +} \ No newline at end of file diff --git a/src/app/api/mock/hibp/range/search/[hashPrefix]/route.ts b/src/app/api/mock/hibp/range/search/[hashPrefix]/route.ts index 3956d8a2230..e468ac61341 100644 --- a/src/app/api/mock/hibp/range/search/[hashPrefix]/route.ts +++ b/src/app/api/mock/hibp/range/search/[hashPrefix]/route.ts @@ -6,6 +6,7 @@ import { NextRequest, NextResponse } from "next/server"; import { logger } from "../../../../../../functions/server/logging"; import { errorIfProduction } from "../../../../../utils/errorThrower"; import { getBreachesForHash } from "../../../config/defaults"; +import mockOvewrite from "../../../mockData/mockOverwrite.json"; type BreachedAccountResponse = { hashSuffix: string; @@ -19,10 +20,20 @@ export function GET( const prodError = errorIfProduction(); if (prodError) return prodError; + const envIsLocal = process.env.APP_ENV === "local"; + logger.info("HIBP Mock endpoint: /range/search/"); - const hashPrefix = params.hashPrefix; - const breachesList = getBreachesForHash(hashPrefix); + let breachesList = []; + + if (envIsLocal && mockOvewrite.doOvewrite) { + breachesList = mockOvewrite.breachesOverwrite; + } else { + const hashPrefix = params.hashPrefix; + breachesList = getBreachesForHash(hashPrefix); + } + + if (envIsLocal) breachesList = breachesList.concat(mockOvewrite.breachesAdd); const data: BreachedAccountResponse = [ { diff --git a/src/app/api/mock/onerep/config/config.ts b/src/app/api/mock/onerep/config/config.ts index 8b5a0399b3d..13dcd955fd9 100644 --- a/src/app/api/mock/onerep/config/config.ts +++ b/src/app/api/mock/onerep/config/config.ts @@ -4,7 +4,8 @@ import { BinaryLike, createHash } from "crypto"; import { StateAbbr } from "../../../../../utils/states"; -import MockUser from "../mockData/mockUser.json"; +import mockUser from "../mockData/mockUser.json"; +import mockOverwrite from "../mockData/mockOverwrite.json"; import { computeSha1First6, hashToEmailKeyMap } from "../../../utils/mockUtils"; export interface Broker { @@ -78,41 +79,41 @@ export function MOCK_ONEREP_ID_START(profileId: number) { } export function MOCK_ONEREP_TIME() { - return MockUser.TIME; + return mockUser.TIME; } export function MOCK_ONEREP_FIRSTNAME() { - return MockUser.FIRSTNAME; + return mockUser.FIRSTNAME; } export function MOCK_ONEREP_LASTNAME() { - return MockUser.LASTNAME; + return mockUser.LASTNAME; } export function MOCK_ONEREP_BIRTHDATE() { - return MockUser.BIRTHDATE; + return mockUser.BIRTHDATE; } export function MOCK_ONEREP_EMAILS() { - return MockUser.EMAILS; + return mockUser.EMAILS; } export function MOCK_ONEREP_PHONES() { - return MockUser.PHONES; + return mockUser.PHONES; } export function MOCK_ONEREP_RELATIVES() { - return MockUser.RELATIVES; + return mockUser.RELATIVES; } export function MOCK_ONEREP_PROFILE_STATUS() { - return MockUser.STATUS as "active" | "inactive"; + return mockUser.STATUS as "active" | "inactive"; } export function MOCK_ONEREP_ADDRESSES() { type typeOfAddr = [{ city: string; state: StateAbbr }]; - return MockUser.ADDRESSES.map((address) => ({ + return mockUser.ADDRESSES.map((address) => ({ city: address.city, state: address.state as StateAbbr, })) as typeOfAddr; @@ -161,10 +162,19 @@ export function MOCK_ONEREP_BROKERS( const idStart = MOCK_ONEREP_ID_START(profileId); const idStartDataBroker = MOCK_ONEREP_DATABROKER_ID_START(profileId); + const envIsLocal = process.env.APP_ENV === "local"; const emailHash = computeSha1First6(email); - const brokersListMap = MockUser.BROKERS_LIST as BrokerMap; + const brokersListMap = mockUser.BROKERS_LIST as BrokerMap; const datasetKey = hashToEmailKeyMap[emailHash] || "default"; - const brokersList = brokersListMap[datasetKey]; + + let brokersList = []; + if (envIsLocal && mockOverwrite.doOverwrite) { + brokersList = mockOverwrite.brokersOverwrite; + } else { + brokersList = brokersListMap[datasetKey]; + } + + if (envIsLocal) brokersList = brokersList.concat(mockOverwrite.brokersAdd); const res = brokersList.map( (elem: BrokerOptionals, index: number) => diff --git a/src/app/api/mock/onerep/mockData/mockOverwrite.json b/src/app/api/mock/onerep/mockData/mockOverwrite.json new file mode 100644 index 00000000000..3be7b2107d5 --- /dev/null +++ b/src/app/api/mock/onerep/mockData/mockOverwrite.json @@ -0,0 +1,9 @@ +{ + "doOverwrite": false, + "brokersOverwrite": [ { + "relatives": ["Jon Jones"], + "link": "https://mockexample.com/link-to-databroker-e2e-test-1", + "data_broker": "mockexample-e2e-test-1.com" + }], + "brokersAdd": [] +} \ No newline at end of file diff --git a/src/app/api/mock/resetTestData/reset.ts b/src/app/api/mock/resetTestData/reset.ts new file mode 100644 index 00000000000..3db8276019e --- /dev/null +++ b/src/app/api/mock/resetTestData/reset.ts @@ -0,0 +1,50 @@ +/* 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 { + deleteScanResultsForProfile, + deleteSomeScansForProfile, +} from "../../../../db/tables/onerep_scans"; +import { + getOnerepProfileId, + unresolveAllBreaches, +} from "../../../../db/tables/subscribers"; +import { logger } from "../../../functions/server/logging"; + +export async function resetData( + subscriberId: number, + resetHibp: boolean, + resetOneRep: boolean, +): Promise<[success: boolean, msg?: string | undefined]> { + const onerepProfileId = await getOnerepProfileId(subscriberId); + if (!onerepProfileId) return [false, "Unable to fetch OneRep profile ID"]; + if (resetHibp) await unresolveAllBreaches(onerepProfileId); + if (resetOneRep) { + await deleteSomeScansForProfile(onerepProfileId, 1); + await deleteScanResultsForProfile(onerepProfileId); + } + const oneRepMsg = `${resetOneRep ? "attempted to delete all but 1 scans" : ""}`; + const hibpMsg = `${resetHibp ? "attempted to unresolve all breaches" : ""}`; + const loggerMsg = + `${oneRepMsg}${resetOneRep && resetHibp ? ", " : ""}${hibpMsg}` || + "no action done"; + logger.info(`Mock Data Reset endpoint: ${loggerMsg}`); + return [true]; +} + +export function brokerOverwriteOneRep() { + //modify json file +} + +export function breachOverwriteHibp() { + //modify json file +} + +export function brokerAddOneRep() { + //modify json file +} + +export function breachAddeHibp() { + //modify json file +} diff --git a/src/app/api/mock/clearTestData/route.ts b/src/app/api/mock/resetTestData/route.ts similarity index 53% rename from src/app/api/mock/clearTestData/route.ts rename to src/app/api/mock/resetTestData/route.ts index 3d6b5add05d..798a6f342fc 100644 --- a/src/app/api/mock/clearTestData/route.ts +++ b/src/app/api/mock/resetTestData/route.ts @@ -2,24 +2,16 @@ * 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 { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "../../../functions/server/getServerSession"; import { errorIfProduction, internalServerError, - unauthError, + unauthErrorResponse, } from "../../utils/errorThrower"; -import { - getOnerepProfileId, - unresolveAllBreaches, -} from "../../../../db/tables/subscribers"; -import { - deleteScanResultsForProfile, - deleteSomeScansForProfile, -} from "../../../../db/tables/onerep_scans"; -import { logger } from "../../../functions/server/logging"; +import { resetData } from "./reset"; -function isTestEmail(email: string) { +function isTestEmail(email: string | undefined) { if (!email) return false; const testEmailKeys = Object.keys(process.env).filter((key) => key.startsWith("E2E_TEST_ACCOUNT_EMAIL"), @@ -28,7 +20,7 @@ function isTestEmail(email: string) { return testEmails.includes(email); } -export async function GET() { +export async function GET(req: NextRequest) { const prodError = errorIfProduction(); if (prodError) return prodError; @@ -36,17 +28,13 @@ export async function GET() { const email = session?.user.email; const subscriberId = session?.user.subscriber?.id; if (!session || !email || !isTestEmail(email) || !subscriberId) - return unauthError(); + return unauthErrorResponse(); - const onerepProfileId = await getOnerepProfileId(subscriberId); - if (!onerepProfileId) - return internalServerError("Unable to fetch OneRep profile ID"); - await deleteScanResultsForProfile(onerepProfileId); - await deleteSomeScansForProfile(onerepProfileId, 1); - await unresolveAllBreaches(onerepProfileId); - logger.info( - "Mock OneRep endpoint: attempted to delete all but 1 scans, attempted to unresolve all breaches", - ); + const resetHibp = Boolean(req.nextUrl.searchParams.get("resetHibp")); + const resetOneRep = Boolean(req.nextUrl.searchParams.get("resetOneRep")); + + const [success, msg] = await resetData(subscriberId, resetHibp, resetOneRep); + if (!success) return internalServerError(msg); return NextResponse.json( { message: "Requested reached successfully" }, diff --git a/src/app/api/utils/errorThrower.ts b/src/app/api/utils/errorThrower.ts index 1fbcbbc34ea..aa6a88d8fd1 100644 --- a/src/app/api/utils/errorThrower.ts +++ b/src/app/api/utils/errorThrower.ts @@ -33,7 +33,7 @@ export function errorIfEnvCond(which: string, isEqualToWhich: boolean) { return null; } -export function unauthError() { +export function unauthErrorResponse() { return NextResponse.json( { error: "Unauthorized to access the endpoint" }, { status: 401 }, @@ -45,6 +45,6 @@ export function internalServerError( ) { return NextResponse.json( { error: `Internal server error: ${description}` }, - { status: 401 }, + { status: 500 }, ); } diff --git a/src/db/migrations/20240710214906_qa_custom_breaches.js b/src/db/migrations/20240710214906_qa_custom_breaches.js new file mode 100644 index 00000000000..b19c41cc79d --- /dev/null +++ b/src/db/migrations/20240710214906_qa_custom_breaches.js @@ -0,0 +1,15 @@ +/* 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/. */ + +exports.up = function(knex) { + +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + +}; From 449c963bdf19d92d3b4ddc7dd3b79def5ef58502 Mon Sep 17 00:00:00 2001 From: Mukhamediyar Kudaikulov Date: Mon, 15 Jul 2024 00:15:50 -0700 Subject: [PATCH 2/8] qa tool: backend - done --- .../20240710214906_qa_custom_breaches.js | 15 ---- .../20240710214906_qa_custom_brokers.js | 38 ++++++++++ src/db/tables/onerep_scans.ts | 38 ++++++---- src/db/tables/qa_customs.ts | 75 +++++++++++++++++++ 4 files changed, 137 insertions(+), 29 deletions(-) delete mode 100644 src/db/migrations/20240710214906_qa_custom_breaches.js create mode 100644 src/db/migrations/20240710214906_qa_custom_brokers.js create mode 100644 src/db/tables/qa_customs.ts diff --git a/src/db/migrations/20240710214906_qa_custom_breaches.js b/src/db/migrations/20240710214906_qa_custom_breaches.js deleted file mode 100644 index b19c41cc79d..00000000000 --- a/src/db/migrations/20240710214906_qa_custom_breaches.js +++ /dev/null @@ -1,15 +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/. */ - -exports.up = function(knex) { - -}; - -/** - * @param { import("knex").Knex } knex - * @returns { Promise } - */ -exports.down = function(knex) { - -}; diff --git a/src/db/migrations/20240710214906_qa_custom_brokers.js b/src/db/migrations/20240710214906_qa_custom_brokers.js new file mode 100644 index 00000000000..421f56be618 --- /dev/null +++ b/src/db/migrations/20240710214906_qa_custom_brokers.js @@ -0,0 +1,38 @@ +/* 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/. */ + + /** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export function up(knex) { + return knex.schema + .createTable('qa_custom_brokers', table => { + table.increments('onerep_scan_result_id').primary(); + table.integer('onerep_profile_id').notNullable(); + table.string("link").notNullable(); + table.integer("age").nullable(); + table.string("data_broker").notNullable(); + table.jsonb("emails").notNullable(); + table.jsonb("phones").notNullable(); + table.jsonb("addresses").notNullable(); + table.jsonb("relatives").notNullable(); + table.string("first_name").notNullable(); + table.string("middle_name").nullable(); + table.string("last_name").notNullable(); + table.string("status").notNullable(); + table.timestamp("created_at").defaultTo(knex.fn.now()); + table.timestamp("updated_at").defaultTo(knex.fn.now()); + }); +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +export function down(knex) { + return knex.schema.dropTableIfExists('qa_custom_brokers'); +} + + diff --git a/src/db/tables/onerep_scans.ts b/src/db/tables/onerep_scans.ts index 74480c41c8f..57b55b4b6f4 100644 --- a/src/db/tables/onerep_scans.ts +++ b/src/db/tables/onerep_scans.ts @@ -16,6 +16,7 @@ import { SubscriberRow, } from "knex/types/tables"; import { RemovalStatus } from "../../app/functions/universal/scanResult.js"; +import { getQaCustomBrokers } from "./qa_customs.ts"; const knex = createDbConnection(); @@ -139,25 +140,34 @@ async function getLatestOnerepScan( return scan ?? null; } +/* +Note: please, don't write the results of this function back to the database! +*/ async function getLatestOnerepScanResults( onerepProfileId: number | null, ): Promise { const scan = await getLatestOnerepScan(onerepProfileId); - const results = - typeof scan === "undefined" - ? [] - : ((await knex("onerep_scan_results") - .select("onerep_scan_results.*") - .distinctOn("link") - .where("onerep_profile_id", onerepProfileId) - .innerJoin( - "onerep_scans", - "onerep_scan_results.onerep_scan_id", - "onerep_scans.onerep_scan_id", - ) - .orderBy("link") - .orderBy("onerep_scan_result_id", "desc")) as OnerepScanResultRow[]); + let results: OnerepScanResultRow[] = []; + + if (typeof scan !== "undefined") { + // Fetch initial results from onerep_scan_results + const scanResults = await knex("onerep_scan_results") + .select("*") + .distinctOn("link") + .where("onerep_profile_id", onerepProfileId) + .innerJoin( + "onerep_scans", + "onerep_scan_results.onerep_scan_id", + "onerep_scans.onerep_scan_id", + ) + .orderBy("link") + .orderBy("onerep_scan_result_id", "desc"); + results = [ + ...scanResults, + ...(await getQaCustomBrokers(onerepProfileId, scan?.onerep_scan_id)), + ]; + } return { scan: scan ?? null, diff --git a/src/db/tables/qa_customs.ts b/src/db/tables/qa_customs.ts new file mode 100644 index 00000000000..9c12b57b2a4 --- /dev/null +++ b/src/db/tables/qa_customs.ts @@ -0,0 +1,75 @@ +/* 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 { OnerepScanResultRow } from "knex/types/tables"; +import { logger } from "../../app/functions/server/logging"; +import createDbConnection from "../connect"; + +const knex = createDbConnection(); + +interface QaBrokerData { + onerep_profile_id: number; + link: string; + age?: number; + data_broker: string; + emails: string; + phones: string; + addresses: string; + relatives: string; + first_name: string; + middle_name?: string; + last_name: string; + status: string; +} + +async function getQaCustomBrokers( + onerepProfileId: number | null, + onerepScanId: number | undefined | null, +) { + if (!onerepProfileId) { + logger.info("getQaCustomBrokers: onerepProfileId was not provided!"); + return []; + } + if (!onerepScanId) { + logger.info("getQaCustomBrokers: onerepScanId was not provided!"); + return []; + } + + let results: OnerepScanResultRow[] = []; + + // Fetch all results from qa_custom_brokers + const brokerResults = await knex("qa_custom_brokers") + .select("*") + .where("onerep_profile_id", onerepProfileId); + + if (brokerResults.length > 0) { + /* + Since these are fake records, their corresponding scanId will be some + existing id, and broker_id will match onerep_scan_result_id for uniqueness + */ + brokerResults.forEach((brokerResult) => { + brokerResult.onerep_scan_id = onerepScanId; + brokerResult.data_broker_id = brokerResult.onerep_scan_result_id; + }); + + results = [...results, ...brokerResults]; + } + return results; +} + +/** + * Inserts a new row into the qa_custom_brokers table. + * + * @param brokerData This object conforms to QaBrokerData, which is the same as + * OnerepScanResulsRow with some fields omitted due to them being automaticallty set. + */ +async function addQaCustomBroker(brokerData: QaBrokerData): Promise { + const [newBrokerId] = (await knex("qa_custom_brokers") + .insert(brokerData) + .returning("onerep_scan_result_id")) as [number]; + logger.info(`New broker added with ID: ${newBrokerId}`); + return newBrokerId; +} + +export { getQaCustomBrokers, addQaCustomBroker }; From 6e1169ae72a0f93b7f8194c9987ef14c699878d9 Mon Sep 17 00:00:00 2001 From: Mukhamediyar Kudaikulov Date: Mon, 15 Jul 2024 22:56:21 -0700 Subject: [PATCH 3/8] functional onerep qa tool, started working on hibp --- .../admin/qa-customs/ConfigPage.module.scss | 200 ++++++++++ .../admin/qa-customs/hibp/breachesLookup.tsx | 56 +++ .../admin/qa-customs/hibp/hibpConfig.tsx | 357 ++++++++++++++++++ .../admin/qa-customs/hibp/page.tsx | 36 ++ .../admin/qa-customs/onerep/onerepConfig.tsx | 355 +++++++++++++++++ .../admin/qa-customs/onerep/page.tsx | 34 ++ .../mock/hibp/mockData/mockAllBreaches.json | 74 ++-- .../api/mock/hibp/mockData/mockOverwrite.json | 2 +- .../mock/onerep/mockData/mockOverwrite.json | 14 +- src/app/api/v1/admin/qa-customs/hibp/route.ts | 152 ++++++++ .../api/v1/admin/qa-customs/onerep/route.ts | 147 ++++++++ src/app/api/v1/admin/qa-customs/route.ts | 59 +++ .../[onerepScanResultId]/resolution/route.ts | 13 + .../20240710214906_qa_custom_brokers.js | 9 +- .../20240715110621_qa_custom_toggles.js | 27 ++ .../20240715115031_qa_custom_breaches.js | 34 ++ src/db/tables/onerep_scans.ts | 1 - src/db/tables/qa_customs.ts | 183 ++++++++- 18 files changed, 1686 insertions(+), 67 deletions(-) create mode 100644 src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/ConfigPage.module.scss create mode 100644 src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/hibp/breachesLookup.tsx create mode 100644 src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/hibp/hibpConfig.tsx create mode 100644 src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/hibp/page.tsx create mode 100644 src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/onerep/onerepConfig.tsx create mode 100644 src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/onerep/page.tsx create mode 100644 src/app/api/v1/admin/qa-customs/hibp/route.ts create mode 100644 src/app/api/v1/admin/qa-customs/onerep/route.ts create mode 100644 src/app/api/v1/admin/qa-customs/route.ts create mode 100644 src/db/migrations/20240715110621_qa_custom_toggles.js create mode 100644 src/db/migrations/20240715115031_qa_custom_breaches.js diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/ConfigPage.module.scss b/src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/ConfigPage.module.scss new file mode 100644 index 00000000000..e5fe8ca3772 --- /dev/null +++ b/src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/ConfigPage.module.scss @@ -0,0 +1,200 @@ +@import "../../../../../tokens"; + +.wrapper { + display: grid; + grid-template-rows: 120px min-content; + gap: $spacing-md; + height: 100%; + padding: $layout-sm; + background-color: $color-grey-05; + align-items: center; + justify-content: center; +} + +.wrapperHibp { + grid-template-rows: 120px min-content 100px; + justify-content: center; +} + +.header { + font: $text-title-xs; + font-weight: normal; + margin: auto; + width: 100%; + b { + font-weight: bold; + } + text-align: center; +} + +.formAndListWrapper { + margin-top: 40px; + display: grid; + grid-template-rows: 1fr; + grid-template-columns: 3fr 2fr; + justify-content: center; +} + +.hibpFormAndListWrapper { + grid-template-columns: 1fr 1fr; +} + +.formWrapper { + display: grid; + grid-template-rows: 50px 4fr 100px; + justify-content: center; +} + +.h2 { + text-align: center; +} + +.allBreachesWrapper { + display: grid; + grid-template-rows: 50px 4fr; + justify-content: center; +} + +.listButtonsWrapper { + display: grid; + grid-template-rows: auto 100px; + justify-content: center; +} + +.listContainer { + max-height: 330px; + overflow-y: auto; +} + +.searchBox { + background-color: white; + border: 1px solid rgb(139, 139, 139); + border-radius: 3px; + height: 240px; + width: 100%; +} + +.buttonsWrapper { + height: auto; + display: grid; + grid-template-columns: 1fr 1fr; +} + +.buttonsStandalone { + height: 100px; + width: 100%; + display: flex; + justify-content: center; +} + +.listWrapper { + height: min-content; + display: grid; + grid-template-rows: 50px auto; + justify-content: center; + align-items: flex-start; +} + +.hibpSelectedBoxWrapper { + width: 100%; + grid-template-columns: 100%; + .listContainer { + margin-left: 10px; + margin-right: 10px; + margin-top: 8px; + height: 300px; + } + p { + margin-left: 10px; + } +} + +.button { + display: block; + margin: auto; + max-height: 100px; + max-width: 100px; + margin-top: 10px; + margin-bottom: 10px; +} + +.buttonUnderList { + margin-left: 10px; + margin-right: 10px; +} + +.buttonsHibp { + margin: 10px; + justify-self: center; +} + +@keyframes shake { + 0% { + transform: translateX(0); + } + 25% { + transform: translateX(-5px); + } + 75% { + transform: translateX(5px); + } + 100% { + transform: translateX(0); + } +} + +.error { + border-color: red; /* Example: Highlight border in red for error */ + animation: shake 0.5s ease-in-out; /* Example animation */ +} + +.form { + display: flex; + flex-direction: column; + gap: $spacing-sm; + align-items: center; + width: min-content; + + .userPicker { + flex: 1 0 auto; + align-items: center; + display: flex; + flex-wrap: wrap; + gap: $spacing-md; + min-height: $layout-md; + + label { + display: flex; + flex-direction: column; + gap: $spacing-sm; + min-width: 50%; + width: 350px; + } + + input { + padding: $spacing-sm; + font: $text-body-md; + transition: border-color 0.3s ease; + } + } +} + +.listItem:hover { + background-color: #f0f0f0; /* Light grey background on hover */ + cursor: pointer; /* Change cursor to indicate clickable item */ + text-decoration: line-through; +} + +.allBreachesItem:hover { + background-color: #f0f0f0; /* Light grey background on hover */ + cursor: pointer; /* Change cursor to indicate clickable item */ +} + +.selectedBox { + margin: 0; +} + +.breachName { + color: black; + margin-left: 10px; +} diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/hibp/breachesLookup.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/hibp/breachesLookup.tsx new file mode 100644 index 00000000000..eb392347514 --- /dev/null +++ b/src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/hibp/breachesLookup.tsx @@ -0,0 +1,56 @@ +/* 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 { getBreaches } from "../../../../../../functions/server/getBreaches.ts"; +import styles from "../ConfigPage.module.scss"; +import { Breach } from "../../../../../../functions/universal/breach.ts"; +import { HibpLikeDbBreach } from "../../../../../../../utils/hibp.js"; + +interface Props { + allBreaches: (Breach | HibpLikeDbBreach)[]; + filterWord?: string; + onClickFunc?: (elem: Breach | HibpLikeDbBreach) => void; + additionalStyles?: string; +} + +export default function BreachesLookup(props: Props) { + // const allBreaches = (await getBreaches()); + + const { + allBreaches, + filterWord = "", + onClickFunc = () => {}, + additionalStyles = "", + } = props; + + const getElementData = (breach: Breach | HibpLikeDbBreach) => { + const name = breach.Name; + const month = (breach.AddedDate as Date).toLocaleString("en-US", { + month: "long", + }); + const year = (breach.AddedDate as Date).getFullYear(); + return `${name} - ${month}, ${year}`; + }; + + return ( +
+ {allBreaches + .filter( + (elem) => + elem.Name.includes(filterWord) || + elem.Title.includes(filterWord) || + String(elem.AddedDate).includes(filterWord), + ) + .map((elem, index) => ( +

onClickFunc(elem)} + key={index} + > + {getElementData(elem)} +

+ ))} +
+ ); +} diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/hibp/hibpConfig.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/hibp/hibpConfig.tsx new file mode 100644 index 00000000000..dbde6b322e3 --- /dev/null +++ b/src/app/(proper_react)/(redesign)/(authenticated)/admin/qa-customs/hibp/hibpConfig.tsx @@ -0,0 +1,357 @@ +/* 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/. */ + +"use client"; + +import React, { useState, useEffect } from "react"; +import styles from "../ConfigPage.module.scss"; + +interface QaBreachData { + emailHashPrefix: string; + Id: number; + Name: string; + Title: string; + Domain: string; + ModifiedDate: Date | string; + PwnCount: number; + Description: string; + LogoPath: string; + DataClasses: string[]; + IsVerified: boolean; + IsFabricated: boolean; + IsSensitive: boolean; + IsRetired: boolean; + IsSpamList: boolean; + IsMalware: boolean; + FaviconUrl: string | null; +} + +const endpointBase = "/api/v1/admin/qa-customs/hibp"; + +interface Props { + emailHashPrefix: string; +} + +const ConfigPage = (props: Props) => { + const [breaches, setBreaches] = useState([]); + // Initialize a base breach template to reset form fields + const baseBreach: QaBreachData = { + emailHashPrefix: "", + Id: -1, + Name: "", + Title: "", + Domain: "", + ModifiedDate: new Date(), + PwnCount: 0, + Description: "", + LogoPath: "", + DataClasses: [], + IsVerified: true, + IsFabricated: false, + IsSensitive: false, + IsRetired: false, + IsSpamList: false, + IsMalware: false, + FaviconUrl: null, + }; + + // Temporary state to hold form input for a new breach + const [newBreach, setNewBreach] = useState(baseBreach); + + useEffect(() => { + void fetchBreaches(); + }, []); + + const fetchBreaches = async () => { + try { + const response = await fetch( + `${endpointBase}?emailHashPrefix=${props.emailHashPrefix}`, + ); + const data = await response.json(); + setBreaches(data); + } catch (error) { + console.error("Error fetching breaches:", error); + } + }; + + const handleChange = ( + e: + | React.ChangeEvent + | React.ChangeEvent + | React.ChangeEvent, + ) => { + setNewBreach({ ...newBreach, [e.target.name]: e.target.value }); + }; + + const handleAddBreach = async (event: React.FormEvent) => { + event.preventDefault(); + newBreach.emailHashPrefix = props.emailHashPrefix; + + if (newBreach.emailHashPrefix.length < 6) { + console.log("Invalid email hash!"); + } + + try { + const response = await fetch(endpointBase, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(newBreach), + }); + + if (response.ok) { + await fetchBreaches(); // Refresh the breaches list + setNewBreach(baseBreach); // Reset form fields + } else { + console.error("Error adding breach:", await response.json()); + } + } catch (error) { + console.error("Error adding breach:", error); + } + }; + + const handleDeleteBreach = async (id: number) => { + try { + const response = await fetch(`${endpointBase}?id=${id}`, { + method: "DELETE", + }); + + if (response.ok) { + await fetchBreaches(); // Refresh the breaches list + } else { + console.error("Error deleting breach:", await response.json()); + } + } catch (error) { + console.error("Error deleting breach:", error); + } + }; + + return ( +
+
+
+ Adding custom Breaches +
+
+
+ +
+
void handleAddBreach(e)} + > +

Input Breach Element

+
+
+ + + + + + + + + + +