chore: Remove deprecated directory

This commit is contained in:
Florian Zia 2024-03-12 13:59:27 +01:00
Родитель 7a19a8c4d2
Коммит 88e04dabe7
Не найден ключ, соответствующий данной подписи
42 изменённых файлов: 53 добавлений и 2651 удалений

Просмотреть файл

@ -129,7 +129,6 @@
"src/views/**/*.js",
"src/middleware/**/*.js",
"src/controllers/**/*.js",
"src/app/deprecated/**/*",
"src/app/(migration_remnants)/**/*",
"src/app/functions/server/breachResolution.ts"
],
@ -149,7 +148,6 @@
// to avoid a barrage of warnings for older code:
"files": ["**/*.{ts,tsx}"],
"excludedFiles": [
"./src/app/deprecated/**/*",
"./src/app/(migration_remnants)/**/*"
],
"extends": [

Просмотреть файл

@ -1,3 +1,2 @@
src/client/
src/app/(nextjs_migration)/
src/app/deprecated/

Просмотреть файл

@ -10,7 +10,6 @@ import { HandleFalseDoorTest } from "../deprecated/components/client/FalseDoorBa
import { getCountryCode } from "../functions/server/getCountryCode";
import { headers } from "next/headers";
import AppConstants from "../../appConstants";
import { getNonce } from "../deprecated/functions/server/getNonce";
import { getEnabledFeatureFlags } from "../../db/tables/featureFlags";
export default async function MigrationLayout({

Просмотреть файл

@ -22,8 +22,10 @@ import {
} from "../../../../../../../../functions/server/getRelevantGuidedSteps";
import { getGuidedExperienceBreaches } from "../../../../../../../../functions/universal/guidedExperienceBreaches";
import { hasPremium } from "../../../../../../../../functions/universal/user";
import { HighRiskDataTypes } from "../../../../../../../../functions/universal/breach";
import { BreachBulkResolutionRequest } from "../../../../../../../../deprecated/(authenticated)/user/breaches/breaches";
import {
BreachBulkResolutionRequest,
HighRiskDataTypes,
} from "../../../../../../../../functions/universal/breach";
import { TelemetryButton } from "../../../../../../../../components/client/TelemetryButton";
import { TelemetryLink } from "../../../../../../../../components/client/TelemetryLink";

Просмотреть файл

@ -10,12 +10,12 @@ import { GuidedExperienceBreaches } from "../../../../../../../../functions/serv
import { ExtendedReactLocalization } from "../../../../../../../../hooks/l10n";
import { Button } from "../../../../../../../../components/client/Button";
import { StepLink } from "../../../../../../../../functions/server/getRelevantGuidedSteps";
import { getLocale } from "../../../../../../../../functions/universal/getLocale";
import { TelemetryButton } from "../../../../../../../../components/client/TelemetryButton";
import {
BreachResolutionRequest,
HibpBreachDataTypes,
} from "../../../../../../../../deprecated/(authenticated)/user/breaches/breaches";
import { getLocale } from "../../../../../../../../functions/universal/getLocale";
import { TelemetryButton } from "../../../../../../../../components/client/TelemetryButton";
} from "../../../../../../../../functions/universal/breach";
export const leakedPasswordTypes = [
"passwords",

Просмотреть файл

@ -23,8 +23,10 @@ import {
} from "../../../../../../../../functions/server/getRelevantGuidedSteps";
import { getGuidedExperienceBreaches } from "../../../../../../../../functions/universal/guidedExperienceBreaches";
import { hasPremium } from "../../../../../../../../functions/universal/user";
import { SecurityRecommendationDataTypes } from "../../../../../../../../functions/universal/breach";
import { BreachBulkResolutionRequest } from "../../../../../../../../deprecated/(authenticated)/user/breaches/breaches";
import {
BreachBulkResolutionRequest,
SecurityRecommendationDataTypes,
} from "../../../../../../../../functions/universal/breach";
export interface SecurityRecommendationsLayoutProps {
type: SecurityRecommendationTypes;

Просмотреть файл

@ -9,7 +9,7 @@ import { getBreachesForEmail } from "../../../../utils/hibp";
import { getSha1 } from "../../../../utils/fxa";
import { getL10n } from "../../../functions/server/l10n";
import { getBreachLogo } from "../../../../utils/breachLogo";
import { Breach } from "../../../deprecated/(authenticated)/user/breaches/breaches";
import { Breach } from "../../../functions/universal/breach";
export interface RequestBreachScanErrorResponse {
success: false;

Просмотреть файл

@ -5,13 +5,13 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "../../../../../functions/server/getServerSession";
import { logger } from "../../../../../functions/server/logging";
import { BreachBulkResolutionRequest } from "../../../../../deprecated/(authenticated)/user/breaches/breaches.js";
import { getBreaches } from "../../../../../functions/server/getBreaches";
import { getAllEmailsAndBreaches } from "../../../../../../utils/breaches";
import {
getSubscriberByFxaUid,
setBreachResolution,
} from "../../../../../../db/tables/subscribers";
import { BreachBulkResolutionRequest } from "../../../../../functions/universal/breach";
export async function PUT(req: NextRequest): Promise<NextResponse> {
const session = await getServerSession();

Просмотреть файл

@ -6,7 +6,6 @@ import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
import { logger } from "../../../../functions/server/logging";
import { BreachResolutionRequest } from "../../../../deprecated/(authenticated)/user/breaches/breaches.js";
import { getBreaches } from "../../../../functions/server/getBreaches";
import { getAllEmailsAndBreaches } from "../../../../../utils/breaches";
import {
@ -14,6 +13,7 @@ import {
setBreachResolution,
} from "../../../../../db/tables/subscribers";
import appConstants from "../../../../../appConstants";
import { BreachResolutionRequest } from "../../../../functions/universal/breach";
// Get breaches data
export async function GET(req: NextRequest) {

Просмотреть файл

@ -9,7 +9,7 @@ import { OnerepScanResultRow } from "knex/types/tables";
import styles from "./ExposureCard.module.scss";
import { useL10n } from "../../../hooks/l10n";
import { Exposure, isScanResult } from "./ExposureCard";
import { HibpBreachDataTypes } from "../../../deprecated/(authenticated)/user/breaches/breaches";
import { HibpBreachDataTypes } from "../../../functions/universal/breach";
type OnerepScanResultSerializedColumns = Extract<
keyof OnerepScanResultRow,

Просмотреть файл

@ -1,172 +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 { redirect } from "next/navigation";
import Script from "next/script";
import appConstants from "../../../../../../appConstants";
import {
EmailTemplateType,
getMonthlyDummyData,
getNotificationDummyData,
getSignupReportDummyData,
getVerificationDummyData,
} from "../../../../../../utils/email";
import { getPreviewTemplate } from "../../../../../../views/emails/email2022";
import { verifyPartial } from "../../../../../../views/emails/emailVerify";
import { breachAlertEmailPartial } from "../../../../../../views/emails/emailBreachAlert";
import { monthlyUnresolvedEmailPartial } from "../../../../../../views/emails/emailMonthlyUnresolved";
import { signupReportEmailPartial } from "../../../../../../views/emails/emailSignupReport";
import { getL10n } from "../../../../../functions/server/l10n";
import { ReactLocalization } from "@fluent/react";
import { getNonce } from "../../../../functions/server/getNonce";
import { getServerSession } from "../../../../../functions/server/getServerSession";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
interface IntrinsicElements {
"custom-select": {
name: string;
dangerouslySetInnerHTML: { __html: string };
};
}
}
}
export default async function EmailTemplatePage(props: {
params: { template: string };
}) {
const session = await getServerSession();
if (!session?.user.email) {
return redirect("/");
}
const admins = process.env.ADMINS?.split(",") ?? [];
// Note: ../layout.tsx currently hides this page for non-admins.
// Not sure if we actually want to have this available to non-admins as well.
const isAdminPreview = admins.includes(session.user.email);
const chosenTemplate =
Object.values(EmailTemplateType).find((t) => t === props.params.template) ??
EmailTemplateType.Verification;
const l10n = getL10n();
const templates = getTemplatesData(l10n);
const selectedPreviewTemplate = templates[chosenTemplate].template;
const recipients = [session.user.email];
if (appConstants.EMAIL_TEST_RECIPIENT) {
recipients.push(appConstants.EMAIL_TEST_RECIPIENT);
}
return (
<div data-partial="emailPreview">
<Script
type="module"
src="/nextjs_migration/client/js/customSelect.js"
nonce={getNonce()}
/>
<Script
type="module"
src="/nextjs_migration/client/js/emailPreview.js"
nonce={getNonce()}
/>
<section className="email-preview js-email">
<h1>Email preview</h1>
<div className="email-preview-controls">
<custom-select
name="email-template"
dangerouslySetInnerHTML={{
__html: getPreviewOptions(chosenTemplate, templates),
}}
/>
{isAdminPreview ? (
<form className="js-email-preview-form email-preview-form">
{getRecipientInputs(recipients)}
<button className="primary" type="submit">
Send test email
</button>
</form>
) : null}
</div>
<hr className="monitor-gradient" />
<div dangerouslySetInnerHTML={{ __html: selectedPreviewTemplate }} />
</section>
</div>
);
}
function getTemplatesData(l10n: ReactLocalization) {
return {
[EmailTemplateType.Verification]: {
label: "Email verification",
template: getPreviewTemplate(
getVerificationDummyData(appConstants.EMAIL_TEST_RECIPIENT, l10n),
verifyPartial,
l10n,
),
},
[EmailTemplateType.Notification]: {
label: "Breach notification",
template: getPreviewTemplate(
getNotificationDummyData(appConstants.EMAIL_TEST_RECIPIENT, l10n),
breachAlertEmailPartial,
l10n,
),
},
[EmailTemplateType.Monthly]: {
label: "Monthly unresolved breaches",
template: getPreviewTemplate(
getMonthlyDummyData(appConstants.EMAIL_TEST_RECIPIENT, l10n),
monthlyUnresolvedEmailPartial,
l10n,
),
},
[EmailTemplateType.SignupReport]: {
label: "Signup report",
template: getPreviewTemplate(
getSignupReportDummyData(appConstants.EMAIL_TEST_RECIPIENT, l10n),
signupReportEmailPartial,
l10n,
),
},
};
}
// Transitioning from untyped JS:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getPreviewOptions(currentTemplateKey: string, data: any) {
const optionsElements = Object.keys(data)
.map(
(templateKey) => `
<option
value='${templateKey}'
${currentTemplateKey === templateKey ? "selected" : ""}
>
${data[templateKey].label as string}
</option>
`,
)
.join("");
return optionsElements;
}
// Transitioning from untyped JS:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getRecipientInputs(recipients: any[]) {
const recipientInputElements = recipients.map((recipient, index) => {
return (
<label key={recipient}>
<input
name="email-recipient-option"
type="radio"
value={recipient}
checked={index === 0}
/>
{recipient}
</label>
);
});
return <fieldset>{recipientInputElements}</fieldset>;
}

Просмотреть файл

@ -1,24 +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 { notFound, redirect } from "next/navigation";
import { getServerSession } from "../../../../functions/server/getServerSession";
import { EmailTemplateType } from "../../../../../utils/email";
export default async function EmailRootPage() {
const session = await getServerSession();
if (!session?.user.email) {
return notFound();
}
const admins = process.env.ADMINS?.split(",") ?? [];
const isAdmin = admins.includes(session.user.email);
if (!isAdmin) {
return notFound();
}
return redirect("./emails/" + EmailTemplateType.Verification);
}

Просмотреть файл

@ -1,28 +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 { ReactNode } from "react";
import { notFound } from "next/navigation";
import { getServerSession } from "../../../functions/server/getServerSession";
import "../../../../client/css/index.css";
export type Props = {
children: ReactNode;
};
const AdminLayout = async (props: Props) => {
const session = await getServerSession();
const admins = process.env.ADMINS?.split(",") ?? [];
if (
!session ||
typeof session.user.email !== "string" ||
!admins.includes(session.user.email)
) {
return notFound();
}
return <>{props.children}</>;
};
export default AdminLayout;

Просмотреть файл

@ -1,20 +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/. */
export default function AdminPage() {
return (
<section>
<h1>Admin</h1>
<ul>
<li>
<a href="/admin/emails">Email preview</a>
</li>
<li>
<a href="/admin/location-search">Location search</a>
</li>
</ul>
</section>
);
}

Просмотреть файл

@ -1,121 +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 { NextRequest, NextResponse } from "next/server";
import AppConstants from "../../../../../appConstants.js";
import { logger } from "../../../../functions/server/logging";
import {
EmailTemplateType,
getMonthlyDummyData,
getSignupReportDummyData,
getVerificationDummyData,
initEmail,
sendEmail,
} from "../../../../../utils/email.js";
import { getTemplate } from "../../../../../views/emails/email2022.js";
import { getL10n } from "../../../../functions/server/l10n";
import { verifyPartial } from "../../../../../views/emails/emailVerify.js";
import { monthlyUnresolvedEmailPartial } from "../../../../../views/emails/emailMonthlyUnresolved.js";
import { signupReportEmailPartial } from "../../../../../views/emails/emailSignupReport.js";
export async function POST(req: NextRequest) {
const { emailId, recipient } = (await req.json()) as {
emailId: string;
recipient: string;
};
const l10n = getL10n();
switch (emailId) {
case EmailTemplateType.Verification: {
// Send test verification email
const emailTemplate = getTemplate(
getVerificationDummyData(recipient, l10n),
verifyPartial,
l10n,
);
await initEmail(process.env.SMTP_URL);
await sendEmail(
recipient,
l10n.getString("email-subject-verify"),
emailTemplate,
);
break;
}
case EmailTemplateType.Notification: {
// Send test breach notification email
await sendTestNotification(req, new NextResponse());
break;
}
case EmailTemplateType.Monthly: {
// Send test monthly unresolved breaches email
const emailTemplate = getTemplate(
getMonthlyDummyData(AppConstants.EMAIL_TEST_RECIPIENT, l10n),
monthlyUnresolvedEmailPartial,
l10n,
);
await initEmail(process.env.SMTP_URL);
await sendEmail(
recipient,
l10n.getString("email-unresolved-heading"),
emailTemplate,
);
break;
}
case EmailTemplateType.SignupReport: {
// Send test sign-up report email
const emailTemplate = getTemplate(
getSignupReportDummyData(AppConstants.EMAIL_TEST_RECIPIENT, l10n),
signupReportEmailPartial,
l10n,
);
await initEmail(process.env.SMTP_URL);
await sendEmail(
recipient,
l10n.getString("email-subject-found-breaches"),
emailTemplate,
);
break;
}
default: {
throw new Error(`No test email found for ${emailId}`);
}
}
logger.info(`Sent test email: ${emailId}`);
// The notify function has its own response
if (emailId !== EmailTemplateType.Notification) {
return NextResponse.json(
{ success: true, message: `Sent test ${emailId} email` },
{ status: 200 },
);
}
}
// Leaving the `async` for now because of the commented-out `await`:
// eslint-disable-next-line @typescript-eslint/require-await
async function sendTestNotification(_req: NextRequest, _res: NextResponse) {
// The test breach notification can be viewed in the public Mailinator inbox
// as documented in the README:
// https://github.com/mozilla/blurts-server#trigger-breach-alert-email
const _breachNotificationData = {
breachName: "Adobe",
// Hash for dummy email `localmonitor20200827@mailinator.com`
hashPrefix: "365050",
hashSuffixes: ["53cbb89874fc738c0512daf12bc4d91765"],
};
// TODO Disabled for now; not sure yet how to trigger this with the functionality
// moved to a Cloud Function.
// const notifyReq = {
// app: req.app,
// body: {
// ...req.body,
// ...breachNotificationData,
// },
// token: AppConstants.HIBP_NOTIFY_TOKEN,
// };
// await notify(notifyReq, res);
}

Просмотреть файл

@ -1,139 +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 { BreachDataTypes } from "../../../../functions/universal/breach.js";
export type BreachResolutionTypes = Record<
keyof BreachDataTypes,
BreachResolution
>;
export interface CircleChartProps
extends React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
> {
class?: string;
title?: string;
"data-txt-other"?: string;
"data-txt-none"?: string;
data?: string;
}
export type HibpBreachDataTypes = typeof BreachDataTypes;
export interface BreachResolution {
priority: number;
header: string;
body?: string;
applicableCountryCodes?: Array<string>;
}
export interface SubscriberEmail {
email: string;
id: number;
}
export interface BreachStats {
monitoredEmails: {
count: number;
};
numBreaches: {
count: number;
numResolved: number;
numUnresolved: number;
};
passwords: {
count: number;
numResolved: number;
};
}
export interface SubscriberBreachResolution {
useBreachId?: boolean;
[email: string]: {
[id: number]: {
resolutionsChecked: Array<string>;
};
};
}
export interface Breach {
AddedDate: string;
BreachDate: string;
DataClasses: Array<string>;
Description: string;
Domain: string;
Id: number;
IsFabricated: boolean;
IsMalware: boolean;
IsResolved?: boolean;
IsRetired: boolean;
IsSensitive: boolean;
IsSpamList: boolean;
IsVerified: boolean;
LogoPath: string;
ModifiedDate: string;
Name: string;
PwnCount: number;
recencyIndex: number;
ResolutionsChecked: Array<string>;
Title: string;
}
/**
* @deprecated Use {@see SubscriberRow} instead
*/
export interface Subscriber {
id: number;
primary_sha1: string;
primary_email: string;
primary_verification_token: string;
primary_verified: boolean;
created_at: Date;
updated_at: Date;
fx_newsletter: boolean;
signup_language: string;
fxa_refresh_token: string;
fxa_profile_json: {
uid: string;
email: string;
avatar: string;
locale: string;
amrValues: Array<string>;
avatarDefault: boolean;
metricsEnabled: boolean;
twoFactorAuthentication: boolean;
};
fxa_uid: string;
breaches_last_shown: Date;
all_emails_to_primary: boolean;
fxa_access_token: string;
breaches_resolved: {
[email: string]: Array<Breach>;
};
waitlists_joined: boolean | null;
breach_stats: BreachStats;
monthly_email_at: Date | null;
monthly_email_optout: boolean | null;
breach_resolution: SubscriberBreachResolution;
email_addresses: Array<SubscriberEmail>;
}
export interface VerifiedEmail {
breaches: Array<Breach>;
email: string;
id: number;
primary: boolean;
verified: boolean;
}
export interface BreachResolutionRequest {
affectedEmail: string;
breachId: number;
resolutionsChecked: Array<HibpBreachDataTypes[keyof HibpBreachDataTypes]>;
}
export interface BreachBulkResolutionRequest {
dataType: BreachDataTypes;
}

Просмотреть файл

@ -1,187 +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 Image from "next/image";
import Script from "next/script";
import { headers } from "next/headers";
import { getServerSession } from "../../../../functions/server/getServerSession";
import { CircleChartProps } from "./breaches.d";
import { getL10n } from "../../../../functions/server/l10n";
import {
getUserBreaches,
type UserBreaches as UserBreachesType,
} from "../../../../functions/server/getUserBreaches";
import "../../../../../client/css/partials/breaches.css";
import ImageIconEmail from "../../../../../client/images/icon-email.svg";
import { BreachesTable } from "../../../components/server/BreachesTable";
import { getComponentAsString } from "../../../functions/server/getComponentAsString";
import { getCountryCode } from "../../../../functions/server/getCountryCode";
import { getNonce } from "../../../functions/server/getNonce";
import { SignInButton } from "../../../components/client/SignInButton";
import { CONST_MAX_NUM_ADDRESSES } from "../../../../../constants";
export function generateMetadata() {
const l10n = getL10n();
return {
title: l10n.getString("breach-meta-title"),
twitter: {
card: "summary_large_image",
title: l10n.getString("brand-mozilla-monitor"),
description: l10n.getString("meta-desc-2"),
images: ["/images/og-image.webp"],
},
openGraph: {
title: l10n.getString("brand-mozilla-monitor"),
description: l10n.getString("meta-desc-2"),
siteName: l10n.getString("brand-mozilla-monitor"),
type: "website",
url: process.env.SERVER_URL,
images: ["/images/og-image.webp"],
},
};
}
function createEmailOptions({
breachesData,
emailSelectIndex,
}: UserBreachesType) {
const emails = breachesData.verifiedEmails.map((obj) => obj.email);
const optionElements = emails.map(
(email, index) =>
`<option ${
emailSelectIndex === index ? "selected" : ""
}>${email}</option>`,
);
return optionElements.join("");
}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
interface IntrinsicElements {
"circle-chart": CircleChartProps;
}
}
}
export default async function UserBreaches() {
const session = await getServerSession();
if (!session?.user?.subscriber) {
return <SignInButton autoSignIn />;
}
const l10n = getL10n();
const headerList = headers();
const userBreachesData: UserBreachesType = await getUserBreaches({
user: session.user,
options: {
countryCode: getCountryCode(headerList),
},
});
return (
<>
{/* These scripts predate the use of React and thus shouldnt wait for
hydration to adjust the layout. */}
{/* eslint-disable @next/next/no-sync-scripts */}
<script
type="module"
src="/nextjs_migration/client/js/customSelect.js"
rel="preload"
crossOrigin="anonymous"
nonce={getNonce()}
/>
<script
type="module"
src="/nextjs_migration/client/js/circleChart.js"
rel="preload"
crossOrigin="anonymous"
nonce={getNonce()}
/>
<script
type="module"
src="/nextjs_migration/client/js/breaches.js"
rel="preload"
crossOrigin="anonymous"
nonce={getNonce()}
/>
{/* eslint-enable @next/next/no-sync-scripts */}
<Script
type="module"
src="/nextjs_migration/client/js/dialog.js"
nonce={getNonce()}
/>
<main data-partial="breaches">
<section>
<header className="breaches-header">
<h1
// The DOM for this element is modified by regular JavaScript
// files that predate our migration to Next.js. We dont use any
// React-specific features here, so hydration errors should
// not be a problem.
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: l10n.getString("breach-heading-email", {
"email-select": `<custom-select name="email-account">${createEmailOptions(
userBreachesData,
)}</custom-select>`,
}),
}}
/>
<circle-chart
// The DOM for this element is modified by regular JavaScript
// files that predate our migration to Next.js. We dont use any
// React-specific features here, so hydration errors should
// not be a problem.
suppressHydrationWarning
class="breach-chart"
title={l10n.getString("breach-chart-title")}
data-txt-other={l10n.getString("other-data-class")}
data-txt-none={l10n.getString("none-data-class")}
></circle-chart>
<figure
className="email-stats"
data-count={userBreachesData.emailTotalCount}
data-total={CONST_MAX_NUM_ADDRESSES}
>
<Image src={ImageIconEmail} alt="" width={55} height={30} />
<figcaption>
<strong>
{l10n.getString("emails-monitored", {
count: userBreachesData.emailVerifiedCount,
total: CONST_MAX_NUM_ADDRESSES,
})}
</strong>
<a href="/user/settings">
{l10n.getString("manage-emails-link")}
</a>
</figcaption>
</figure>
</header>
</section>
<div
// The DOM for this element is modified by regular JavaScript
// files that predate our migration to Next.js. We dont use any
// React-specific features here, so hydration errors should
// not be a problem.
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: await getComponentAsString({
component: <BreachesTable userBreaches={userBreachesData} />,
}),
}}
/>
</main>
</>
);
}

Просмотреть файл

@ -1,9 +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 { redirect } from "next/navigation";
export default function UserDashboard() {
redirect("/user/breaches");
}

Просмотреть файл

@ -1,152 +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 { ReactNode } from "react";
import Image from "next/image";
import Script from "next/script";
import { logger } from "../../../functions/server/logging";
import "../../../../client/css/index.css";
import { UserMenu } from "../../components/client/UserMenu";
import { SignInButton } from "../../components/client/SignInButton";
import { SiteNavigation } from "../../components/client/SiteNavigation";
import AppConstants from "../../../../appConstants.js";
import MonitorLogo from "../../../../client/images/monitor-logo-transparent@2x.webp";
import MozillaLogo from "../../../../client/images/moz-logo-1color-white-rgb-01.svg";
import { getL10n } from "../../../functions/server/l10n";
import { getNonce } from "../../functions/server/getNonce";
import { PageLoadEvent } from "../../../components/client/PageLoadEvent";
import { getExperiments } from "../../../functions/server/getExperiments";
import { getEnabledFeatureFlags } from "../../../../db/tables/featureFlags";
import { getServerSession } from "../../../functions/server/getServerSession";
export type Props = {
children: ReactNode;
};
const MainLayout = async (props: Props) => {
const session = await getServerSession();
if (!session?.user?.subscriber) {
return <SignInButton autoSignIn />;
}
const userId = session.user.subscriber.fxa_uid ?? "";
if (!userId) {
logger.error("No user ID for telemetry");
}
try {
// TODO For initial A/A testing `features` is unused. https://mozilla-hub.atlassian.net/browse/MNTOR-2182
const features = await getExperiments(userId);
// TODO remove debug for A/A testing https://mozilla-hub.atlassian.net/browse/MNTOR-2182
logger.debug("Nimbus features in authenticated session:", features);
} catch (ex) {
logger.error("Could not fetch Nimbus features:", ex);
}
const l10n = getL10n();
const enabledFeatureFlags = await getEnabledFeatureFlags({
email: session.user.email,
});
return (
<>
<Script
type="module"
src="/nextjs_migration/client/js/nav.js"
nonce={getNonce()}
/>
<PageLoadEvent userId={userId} enabledFlags={enabledFeatureFlags} />
<header>
<div className="header-wrapper">
<a href="/user/breaches">
<Image
className="monitor-logo"
src={MonitorLogo}
width="213"
height="33"
alt={l10n.getString("brand-mozilla-monitor")}
priority
/>
</a>
<div className="nav-wrapper">
<button className="nav-toggle">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 10 8"
width="20"
>
<path
d="M1 1h8M1 4h8M1 7h8"
stroke="#000"
strokeWidth="1"
strokeLinecap="round"
/>
</svg>
</button>
<UserMenu
session={session}
fxaSettingsUrl={AppConstants.FXA_SETTINGS_URL}
nonce={getNonce()}
enabledFeatureFlags={enabledFeatureFlags}
/>
</div>
</div>
</header>
<SiteNavigation />
{props.children}
<footer className="site-footer">
<a href="https://www.mozilla.org" target="_blank">
<Image
src={MozillaLogo}
width="100"
height="29"
loading="lazy"
alt={l10n.getString("mozilla")}
/>
</a>
<menu>
<li>
<a href="/breaches">{l10n.getString("footer-nav-all-breaches")}</a>
</li>
<li>
<a
href="https://support.mozilla.org/kb/firefox-monitor-faq"
target="_blank"
>
FAQ
</a>
</li>
<li>
<a
href="https://www.mozilla.org/about/legal/terms/subscription-services/"
target="_blank"
>
{l10n.getString("terms-of-service")}
</a>
</li>
<li>
<a
href="https://www.mozilla.org/privacy/subscription-services/"
target="_blank"
>
{l10n.getString("privacy-notice")}
</a>
</li>
<li>
<a href="https://github.com/mozilla/blurts-server" target="_blank">
{l10n.getString("github")}
</a>
</li>
</menu>
</footer>
</>
);
};
export default MainLayout;

Просмотреть файл

@ -1,262 +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 { redirect } from "next/navigation";
import Image from "next/image";
import Script from "next/script";
import { EmailAddressRow } from "knex/types/tables";
import AppConstants from "../../../../../appConstants";
import { getL10n } from "../../../../functions/server/l10n";
import ImageIconDelete from "../../../../../client/images/icon-delete.svg";
import "../../../../../client/css/partials/settings.css";
import React from "react";
import { getUserEmails } from "../../../../../db/tables/emailAddresses";
import { getBreaches } from "../../../../functions/server/getBreaches";
import { getBreachesForEmail } from "../../../../../utils/hibp";
import { getSha1 } from "../../../../../utils/fxa";
import { getSubscriberById } from "../../../../../db/tables/subscribers";
import { getNonce } from "../../../functions/server/getNonce";
import { CONST_MAX_NUM_ADDRESSES } from "../../../../../constants";
import { getServerSession } from "../../../../functions/server/getServerSession";
const emailNeedsVerificationSub = (email: EmailAddressRow) => {
const l10n = getL10n();
return (
<>
<span className="verification-required">
{l10n.getString("settings-email-verification-callout")}
</span>
<a className="js-settings-resend-email" data-email-id={email.id} href="#">
{l10n.getString("settings-resend-email-verification-link")}
</a>
</>
);
};
const deleteButton = (email: EmailAddressRow) => {
const l10n = getL10n();
return (
<button
aria-label={l10n.getString("settings-delete-email-button")}
data-subscriber-id={email.subscriber_id}
data-email-id={email.id}
className="settings-email-remove-button js-remove-email-button"
>
<Image
src={ImageIconDelete}
alt={l10n.getString("settings-delete-email-button")}
/>
</button>
);
};
const createEmailItem = (
email: EmailAddressRow & { primary?: boolean },
breachCounts: Map<string, number>,
) => {
const l10n = getL10n();
return (
<li className="settings-email-item">
<strong>
{email.primary
? l10n.getString("settings-email-label-primary", {
email: email.email,
})
: email.email}
</strong>
{email.verified
? l10n.getString("settings-email-number-of-breaches-info", {
breachCount: breachCounts.get(email.email)!,
})
: emailNeedsVerificationSub(email)}
{email.primary ? null : deleteButton(email)}
</li>
);
};
// Moves the primary email to the front and sorts the rest alphabeticaly.
const getSortedEmails = (
emails: Array<EmailAddressRow & { primary?: boolean }>,
) =>
[...emails].sort((a, b) => {
if (a.primary) {
return -1;
}
if (b.primary) {
return 1;
}
return a.email.localeCompare(b.email);
});
const createEmailList = (
emails: EmailAddressRow[],
breachCounts: Map<string, number>,
) => {
return (
<ul className="settings-email-list">
{getSortedEmails(emails).map((email) =>
createEmailItem(email, breachCounts),
)}
</ul>
);
};
const alertOptions = ({
allEmailsToPrimary,
}: {
allEmailsToPrimary: boolean;
}) => {
const l10n = getL10n();
return (
<div className="settings-alert-options">
<label className="settings-radio-input">
<input
defaultChecked={!allEmailsToPrimary}
className="js-settings-alert-options-input"
data-alert-option={0}
name="settings-alert-options"
type="radio"
/>
<span className="settings-radio-label">
{l10n.getString("settings-alert-preferences-option-one")}
</span>
</label>
<label className="settings-radio-input">
<input
defaultChecked={allEmailsToPrimary}
className="js-settings-alert-options-input"
data-alert-option={1}
name="settings-alert-options"
type="radio"
/>
<span className="settings-radio-label">
{l10n.getString("settings-alert-preferences-option-two")}
</span>
</label>
</div>
);
};
export default async function Settings() {
const l10n = getL10n();
const session = await getServerSession();
if (!session || !session.user?.subscriber) {
return redirect("/");
}
// Re-fetch the subscriber every time, rather than reading it from `session`
// - if the user changes their preferences on this page, the JSON web token
// containing the subscriber data won't be updated until the next sign-in.
// (Possibly we shouldn't store subscriber data in that token in the first
// place, other than their ID?)
const subscriber = await getSubscriberById(session.user.subscriber.id);
const emails = await getUserEmails(session.user.subscriber.id);
// Add primary subscriber email to the list
emails.push({
email: session.user.subscriber.primary_email,
sha1: session.user.subscriber.primary_sha1,
primary: true,
verified: true,
// This is old code that mixed objects with different structure in the same
// array, hence the need to assert `any`:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
const breachCounts = new Map();
const allBreaches = await getBreaches();
for (const email of emails) {
const breaches = await getBreachesForEmail(
getSha1(email.email),
allBreaches,
true,
);
breachCounts.set(email.email, breaches?.length || 0);
}
return (
<>
<Script
type="module"
src="/nextjs_migration/client/js/settings.js"
nonce={getNonce()}
/>
<Script
type="module"
src="/nextjs_migration/client/js/dialog.js"
nonce={getNonce()}
/>
<main data-partial="settings">
<div className="settings js-settings">
<h2 className="settings-title">
{l10n.getString("settings-page-title")}
</h2>
<div className="settings-content">
{/* Monitored email addresses */}
<section>
<h3 className="settings-section-title">
{l10n.getString("settings-email-list-title")}
</h3>
<p className="settings-section-info">
{l10n.getString("settings-email-limit-info", {
limit: CONST_MAX_NUM_ADDRESSES,
})}
</p>
{createEmailList(emails, breachCounts)}
<button
aria-label={l10n.getString("settings-add-email-button")}
className="primary settings-add-email-button"
data-dialog="addEmail"
disabled={emails.length >= CONST_MAX_NUM_ADDRESSES}
>
{l10n.getString("settings-add-email-button")}
</button>
</section>
<hr />
{/* Breach alert preferences */}
<section>
<h3 className="settings-section-title">
{l10n.getString("settings-alert-preferences-title")}
</h3>
{alertOptions({
allEmailsToPrimary: subscriber.all_emails_to_primary,
})}
</section>
<hr />
{/* Deactivate account */}
<section>
<h3 className="settings-section-title">
{l10n.getString("settings-deactivate-account-title")}
</h3>
<p className="settings-section-info">
{l10n.getString("settings-deactivate-account-info-2")}
</p>
<a
className="settings-link-fxa"
href={AppConstants.FXA_SETTINGS_URL}
target="_blank"
rel="noopener noreferrer"
>
{l10n.getString("settings-fxa-link-label-3")}
</a>
</section>
</div>
</div>
</main>
</>
);
}

Просмотреть файл

@ -1,20 +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 { NextResponse } from "next/server";
import { getComponentAsString } from "../../../../functions/server/getComponentAsString";
import AddEmailDialog from "../../../../components/server/AddEmailDialog";
export async function GET() {
try {
const dialogContentString = await getComponentAsString({
component: AddEmailDialog(),
});
return new NextResponse(dialogContentString, { status: 200 });
} catch (e) {
return NextResponse.json({ success: false }, { status: 500 });
}
}

Просмотреть файл

@ -1,121 +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 { v5 as uuidv5 } from "uuid";
import { ReactNode } from "react";
import { logger } from "../../functions/server/logging";
import "../../../client/css/index.css";
import Image from "next/image";
import MonitorLogo from "../../../client/images/monitor-logo-transparent@2x.webp";
import MozillaLogo from "../../../client/images/moz-logo-1color-white-rgb-01.svg";
import { SignInButton } from "../components/client/SignInButton";
import { getL10n } from "../../functions/server/l10n";
import { getServerSession } from "../../functions/server/getServerSession";
import { PageLoadEvent } from "../../components/client/PageLoadEvent";
import { getExperiments } from "../../functions/server/getExperiments";
import { getEnabledFeatureFlags } from "../../../db/tables/featureFlags";
import { getUserId } from "../../functions/server/getUserId";
export type Props = {
children: ReactNode;
};
const GuestLayout = async (props: Props) => {
const l10n = getL10n();
// If the user is logged in, use UUID derived from FxA UID as Nimbus user ID.
const session = await getServerSession();
const userId = getUserId(session);
if (!userId) {
logger.error("No user ID for Nimbus telemetry");
}
try {
// TODO For initial A/A testing `features` is unused. https://mozilla-hub.atlassian.net/browse/MNTOR-2182
const features = await getExperiments(userId);
// TODO remove debug for A/A testing https://mozilla-hub.atlassian.net/browse/MNTOR-2182
logger.debug("Nimbus features in guest session:", features);
} catch (ex) {
logger.error("Could not fetch Nimbus features:", ex);
}
const enabledFlags = await getEnabledFeatureFlags({
email: session?.user.email ?? "",
});
return (
<>
<PageLoadEvent userId={userId} enabledFlags={enabledFlags} />
<header>
<div className="header-wrapper">
<a href="/">
<Image
className="monitor-logo"
src={MonitorLogo}
width="213"
height="33"
alt={l10n.getString("brand-mozilla-monitor")}
priority
/>
</a>
<menu>
<li>
<SignInButton />
</li>
</menu>
</div>
</header>
<main>{props.children}</main>
<footer className="site-footer">
<a href="https://www.mozilla.org" target="_blank">
<Image
src={MozillaLogo}
width="100"
height="29"
loading="lazy"
alt={l10n.getString("mozilla")}
/>
</a>
<menu>
<li>
<a href="/breaches">{l10n.getString("footer-nav-all-breaches")}</a>
</li>
<li>
<a
href="https://support.mozilla.org/kb/firefox-monitor-faq"
target="_blank"
>
FAQ
</a>
</li>
<li>
<a
href="https://www.mozilla.org/about/legal/terms/subscription-services/"
target="_blank"
>
{l10n.getString("terms-of-service")}
</a>
</li>
<li>
<a
href="https://www.mozilla.org/privacy/subscription-services/"
target="_blank"
>
{l10n.getString("privacy-notice")}
</a>
</li>
<li>
<a href="https://github.com/mozilla/blurts-server" target="_blank">
{l10n.getString("github")}
</a>
</li>
</menu>
</footer>
</>
);
};
export default GuestLayout;

Просмотреть файл

@ -1,168 +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 Image from "next/image";
import Script from "next/script";
import "../../../client/css/partials/landing.css";
import { getL10n } from "../../functions/server/l10n";
import HeroImage from "../../../client/images/landing-hero@2x.webp";
import LaptopImage from "../../../client/images/landing-laptop@2x.webp";
import LockImage from "../../../client/images/landing-lock@2x.webp";
import MailImage from "../../../client/images/landing-mail@2x.webp";
import NaturePhoneImage from "../../../client/images/landing-nature-phone@2x.webp";
import { getNonce } from "../functions/server/getNonce";
export default function Home() {
const l10n = getL10n();
return (
<div data-partial="landing">
{/* These scripts predate the use of React and thus shouldnt wait for
hydration to adjust the layout. */}
{/* eslint-disable @next/next/no-sync-scripts */}
<script
type="module"
src="/nextjs_migration/client/js/transitionObserver.js"
rel="preload"
crossOrigin="anonymous"
nonce={getNonce()}
/>
<Script
type="module"
src="/nextjs_migration/client/js/landing.js"
nonce={getNonce()}
/>
<section className="hero">
<div>
<h1>{l10n.getString("exposure-landing-hero-heading")}</h1>
<p>{l10n.getString("exposure-landing-hero-lead")}</p>
<form hidden className="exposure-scan">
<label htmlFor="scan-email-address" className="visually-hidden">
{l10n.getString("exposure-landing-hero-email-label")}
</label>
<input
id="scan-email-address"
name="email"
type="email"
placeholder={l10n.getString(
"exposure-landing-hero-email-placeholder",
)}
required
/>
<button
type="submit"
className="button primary"
data-cta-id="exposure-landing-1"
>
{l10n.getString("exposure-landing-hero-cta-label")}
</button>
</form>
</div>
<figure>
<Image alt="" src={HeroImage} priority />
</figure>
</section>
<section className="why-use-monitor" data-enter-transition>
<h2>{l10n.getString("why-use-monitor")}</h2>
<p>{l10n.getString("identifying-breaches")}</p>
<ul>
<li>
<h3>{l10n.getString("protect-account")}</h3>
<p>{l10n.getString("protect-account-prevent-hackers")}</p>
</li>
<li>
<h3>{l10n.getString("prevent-fraud")}</h3>
<p>{l10n.getString("prevent-fraud-keep-info")}</p>
</li>
<li>
<h3>{l10n.getString("get-alerts")}</h3>
<p>{l10n.getString("get-alerts-find-out")}</p>
</li>
</ul>
</section>
<section className="how-it-works" data-enter-transition>
<h2>{l10n.getString("how-it-works")}</h2>
<ol>
<li>
<Image alt="" src={LaptopImage} />
<h3>{l10n.getString("check-for-breaches")}</h3>
<p>{l10n.getString("check-for-breaches-we-search")}</p>
</li>
<li>
<Image alt="" src={LockImage} />
<h3>{l10n.getString("protect-accounts")}</h3>
<p>{l10n.getString("protect-accounts-clear-steps")}</p>
</li>
<li>
<Image alt="" src={MailImage} />
<h3>{l10n.getString("alerts-for-breaches")}</h3>
<p>{l10n.getString("alerts-for-breaches-monitor-new")}</p>
</li>
</ol>
</section>
<section className="safe-with-us" data-enter-transition>
<div>
<h2>{l10n.getString("safe-with-us")}</h2>
<p>{l10n.getString("parent-company")}</p>
<p>{l10n.getString("our-mission")}</p>
<p>
<a href="https://www.mozilla.org/mission/" target="_blank">
{l10n.getString("learn-more-mission")}
</a>
</p>
</div>
<figure>
<Image alt="" src={NaturePhoneImage} />
</figure>
</section>
<section className="top-questions-about-monitor" data-enter-transition>
<div>
<h2>{l10n.getString("top-questions-about-monitor")}</h2>
<a
href="https://support.mozilla.org/kb/firefox-monitor-faq"
target="_blank"
>
{l10n.getString("see-all-faq")}
</a>
</div>
<div>
<details>
<summary>{l10n.getString("what-is-breach")}</summary>
<p>{l10n.getString("when-info-exposed")}</p>
</details>
<details>
<summary>{l10n.getString("what-do-i-do")}</summary>
<p>{l10n.getString("visit-monitor-to-learn")}</p>
</details>
<details>
<summary>{l10n.getString("what-gets-exposed")}</summary>
<p>{l10n.getString("depends-on-hackers")}</p>
</details>
</div>
</section>
<section className="see-if-data-breach" data-enter-transition>
<h2>{l10n.getString("see-if-data-breach")}</h2>
<a
className="button primary"
data-cta-id="landing-2"
href="/user/breaches"
>
{l10n.getString("get-started")}
</a>
<p
className="hibp-attribution"
dangerouslySetInnerHTML={{
__html: l10n.getString("hibp-footer-attribution"),
}}
/>
</section>
</div>
);
}

Просмотреть файл

@ -1,214 +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 Script from "next/script";
import "../../../../client/css/partials/exposureScan.css";
import Image from "next/image";
import HeroImage from "../../../../client/images/exposure-scan-hero.svg";
import NoBreachesImage from "../../../../client/images/breaches-none.svg";
import { getL10n } from "../../../functions/server/l10n";
import { getNonce } from "../../functions/server/getNonce";
export function generateMetadata() {
const l10n = getL10n();
return {
title: l10n.getString("breach-scan-meta-title"),
};
}
export default function PublicScan() {
const l10n = getL10n();
return (
<div data-partial="exposureScan">
<Script
type="module"
src="/nextjs_migration/client/js/scan.js"
nonce={getNonce()}
/>
<div hidden id="data"></div>
<div id="exposure-scan-loading">
{l10n.getString("exposure-landing-result-loading")}
</div>
<div hidden id="exposure-scan-error" aria-live="polite">
{l10n.getString("exposure-landing-result-error")}
</div>
<template
id="breach-template"
dangerouslySetInnerHTML={{
__html: `
<li class="exposure-scan-breach">
<h3>
${
/* The company logo and name will be added client-side, after running the scan */ ""
}
<span class="exposure-scan-breach-company-logo"></span>
<span class="exposure-scan-breach-company-name"></span>
</h3>
<div class="exposure-scan-breach-main">
<dl>
<div class="exposure-scan-breach-added">
<dt>${l10n.getString(
"exposure-landing-result-card-added",
)}</dt>
${
/* The added date will be added client-side, after running the scan */ ""
}
<dd></dd>
</div>
<div class="exposure-scan-breach-data">
<dt>${l10n.getString(
"exposure-landing-result-card-data",
)}</dt>
${
/* The breached data will be added client-side, after running the scan */ ""
}
<dd></dd>
</div>
</dl>
</div>
</li>
`,
}}
/>
<div
hidden
id="exposure-scan-results-overflow"
className="exposure-scan-results"
aria-live="polite"
>
<header className="exposure-scan-hero">
<div className="exposure-scan-hero-content">
<p>
{l10n.getString("exposure-landing-result-overflow-hero-lead")}
</p>
<a
href="/user/breaches"
data-cta-id="exposure-landing-result-overflow-cta-hero"
className="button primary"
>
{l10n.getString(
"exposure-landing-result-overflow-hero-cta-label",
)}
</a>
</div>
<Image alt="" src={HeroImage} />
</header>
<ul className="exposure-scan-breaches"></ul>
<footer className="exposure-scan-footer">
<a
href="/user/breaches"
data-cta-id="exposure-landing-result-overflow-cta-footer"
className="button primary"
>
{l10n.getString(
"exposure-landing-result-overflow-footer-cta-label",
)}
</a>
<p
className="hibp-attribution"
dangerouslySetInnerHTML={{
__html: l10n
.getString("exposure-landing-result-footer-attribution")
.replace(
"<hibp-link>",
'<a href="https://haveibeenpwned.com/" target="_blank">',
)
.replace("</hibp-link>", "</a>"),
}}
/>
</footer>
</div>
<div
hidden
id="exposure-scan-results-some"
className="exposure-scan-results"
aria-live="polite"
>
<header className="exposure-scan-hero">
<div className="exposure-scan-hero-content">
<p>{l10n.getString("exposure-landing-result-some-hero-lead")}</p>
<a
href="/user/breaches"
data-cta-id="exposure-landing-result-some-cta-hero"
className="button primary"
>
{l10n.getString("exposure-landing-result-some-hero-cta-label")}
</a>
</div>
<Image alt="" src={HeroImage} />
</header>
<ul className="exposure-scan-breaches"></ul>
<footer className="exposure-scan-footer">
<a
href="/user/breaches"
data-cta-id="exposure-landing-result-some-cta-footer"
className="button primary"
>
{l10n.getString("exposure-landing-result-some-footer-cta-label")}
</a>
<p
className="hibp-attribution"
dangerouslySetInnerHTML={{
__html: l10n
.getString("exposure-landing-result-footer-attribution")
.replace(
"<hibp-link>",
'<a href="https://haveibeenpwned.com/" target="_blank">',
)
.replace("</hibp-link>", "</a>"),
}}
/>
</footer>
</div>
<div
hidden
id="exposure-scan-results-none"
className="exposure-scan-results"
aria-live="polite"
>
<header className="exposure-scan-hero">
<div className="exposure-scan-hero-content">
<p>{l10n.getString("exposure-landing-result-none-hero-lead")}</p>
<a
href="/user/breaches"
data-cta-id="exposure-landing-result-none-cta-hero"
className="button primary"
>
{l10n.getString("exposure-landing-result-none-hero-cta-label")}
</a>
</div>
<Image alt="" src={HeroImage} />
</header>
<div className="exposure-scan-breaches">
<div className="exposure-scan-breach">
<Image alt="" src={NoBreachesImage} />
{l10n.getString("exposure-landing-result-card-nothing")}
</div>
</div>
<footer className="exposure-scan-footer">
<a
href="/user/breaches"
data-cta-id="exposure-landing-result-none-cta-footer"
className="button primary"
>
{l10n.getString("exposure-landing-result-none-footer-cta-label")}
</a>
<p
className="hibp-attribution"
dangerouslySetInnerHTML={{
__html: l10n
.getString("exposure-landing-result-footer-attribution")
.replace(
"<hibp-link>",
'<a href="https://haveibeenpwned.com/" target="_blank">',
)
.replace("</hibp-link>", "</a>"),
}}
/>
</footer>
</div>
</div>
);
}

Просмотреть файл

@ -1,34 +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 { redirect } from "next/navigation";
import { updateMonthlyEmailOptout } from "../../../../../db/tables/subscribers";
import { getL10n } from "../../../../functions/server/l10n";
export default async function UnsubscribeMonthly(props: {
searchParams: { [key: string]: string | string[] | undefined };
}) {
const token = props.searchParams.token;
if (typeof token !== "string") {
return redirect("/");
}
try {
await updateMonthlyEmailOptout(token);
} catch (error) {
console.log(error);
return redirect("/");
}
const l10n = getL10n();
return (
<div data-partial="unsubscribeMonthly">
<section className="unsubscribe">
<h1
dangerouslySetInnerHTML={{ __html: l10n.getString("unsub-headline") }}
/>
<p>{l10n.getString("changes-saved")}</p>
</section>
</div>
);
}

Просмотреть файл

@ -1,116 +0,0 @@
@import "../../../tokens";
.falseDoorTestWrapper {
font: $text-body-md;
position: fixed;
bottom: 0;
left: 0;
// Overlap all regular content on the site with a z-index,
// except for ModalOverlay which, at the time of writing, has a z-index of 2
z-index: 1;
width: 100%;
background-color: #ffdff0;
display: flex;
flex-direction: row;
justify-content: space-between;
gap: $spacing-xl;
padding-inline: $spacing-sm;
padding-block: $spacing-md;
@media screen and (min-width: $screen-lg) {
padding-inline: $spacing-2xl;
padding-block: $spacing-md;
}
.dismiss {
right: $spacing-sm;
position: absolute;
color: $color-grey-40;
@media screen and (min-width: $screen-lg) {
align-self: flex-start;
margin-left: auto;
position: static;
right: 0;
}
&:focus {
border: 1px solid $color-informational-active;
outline: $border-focus-width solid $color-informational-focus;
}
}
.content {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: $spacing-md;
width: 100%;
justify-content: center;
padding-block: $spacing-sm;
padding-inline: $spacing-sm;
text-align: center;
@media screen and (min-width: $screen-sm) {
padding-inline: $spacing-xl;
}
@media screen and (min-width: $screen-md) {
display: grid;
text-align: start;
grid-template-columns: 1fr 3fr 1fr;
padding-inline: $spacing-lg;
@media screen and (min-width: $screen-xl) {
gap: $spacing-lg;
padding: $spacing-sm $layout-2xl;
}
}
.logoWrapper {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
@media screen and (min-width: $screen-md) {
width: auto;
}
@media screen and (min-width: $screen-lg) {
padding-inline: $spacing-xl;
}
.logo {
max-width: 200px; // Width of the Mozilla Monitor logo
}
}
p {
margin: 0;
}
.cta {
text-decoration: none;
padding-inline: $spacing-xl;
padding-block: $spacing-sm;
background: $color-blue-50;
font-weight: 600;
justify-content: center;
color: $color-white;
border-radius: $border-radius-xs;
cursor: pointer;
text-align: center;
width: 150px; // Width of the CTA
&:focus {
outline: $border-focus-width solid $color-blue-30;
}
@media screen and (min-width: $screen-lg) {
margin-left: auto;
width: 200px; // Width of the CTA
}
}
}
}

Просмотреть файл

@ -1,28 +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 type { Meta, StoryObj } from "@storybook/react";
import { FalseDoorBanner } from "./FalseDoorBanner";
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta<typeof FalseDoorBanner> = {
title: "False Door Banner",
component: FalseDoorBanner,
};
export default meta;
type Story = StoryObj<typeof FalseDoorBanner>;
export const FalseDoorBannerDashboard: Story = {
args: {
checkIsOnDashboard: true,
link: "example.com",
},
};
export const FalseDoorBannerDefault: Story = {
args: {
checkIsOnDashboard: false,
link: "example.com",
},
};

Просмотреть файл

@ -1,33 +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 { render, screen } from "@testing-library/react";
import { it, expect } from "@jest/globals";
import { composeStory } from "@storybook/react";
import Meta, {
FalseDoorBannerDashboard,
FalseDoorBannerDefault,
} from "./FalseDoorBanner.stories";
it("shows false door test variant on dashboard", () => {
const ComposedFalseDoorBanner = composeStory(FalseDoorBannerDashboard, Meta);
render(<ComposedFalseDoorBanner />);
// substring match https://testing-library.com/docs/queries/about/#textmatch
const bannerElem = screen.getByText(
"Automatically remove data from broker sites with",
{ exact: false },
);
expect(bannerElem).toBeInTheDocument();
});
it("shows false door test", () => {
const ComposedFalseDoorBanner = composeStory(FalseDoorBannerDefault, Meta);
render(<ComposedFalseDoorBanner />);
// substring match https://testing-library.com/docs/queries/about/#textmatch
const bannerElem = screen.getByText(
"Automatically remove data from sketchy sites with",
{ exact: false },
);
expect(bannerElem).toBeInTheDocument();
});

Просмотреть файл

@ -1,138 +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/. */
"use client";
import Image from "next/image";
import MozillaMonitorLogo from "../../../(proper_react)/images/monitor-logo.webp";
import styles from "./FalseDoorBanner.module.scss";
import { CloseBtn } from "../../../components/server/Icons";
import { useEffect, useState } from "react";
import { useCookies } from "react-cookie";
import { useL10n } from "../../../hooks/l10n";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { appendUtmParams } from "../../../../utils/utmParams";
type HandleFalseDoorBanner = {
link: string;
};
// Ignoring full coverage here because we'll be creating a custom cookies hook
// TODO: MNTOR-2043
/* c8 ignore start */
export const HandleFalseDoorTest = (props: HandleFalseDoorBanner) => {
const [cookies, setCookie] = useCookies(["falseDoorDismissedPhase3"]);
const [shouldShowFalseDoor, setShouldShowFalseDoor] = useState(false);
const pathname = usePathname();
const isOnDashboard = pathname === "/user/breaches";
const waitlistLink = appendUtmParams({
linkUrl: props.link,
utmParams: {
utm_source: "monitor",
utm_medium: "monitor-product",
utm_content: `banner-phase-3-us_${pathname}`,
},
});
const handleDismiss = (event?: Event) => {
setCookie("falseDoorDismissedPhase3", "true", { path: "/" });
if (event && event.target && "id" in event.target) {
let action;
if (event?.target.id === "close-button") {
action = "dismissed";
} else {
action = "opened";
}
window.gtag("event", "clicked_false_door", {
action,
result: "success",
page_location: location.href,
});
}
};
useEffect(() => {
setShouldShowFalseDoor(!cookies.falseDoorDismissedPhase3);
}, [cookies.falseDoorDismissedPhase3]);
return (
<>
{shouldShowFalseDoor && (
<FalseDoorBanner
checkIsOnDashboard={isOnDashboard}
link={waitlistLink}
onDismiss={handleDismiss}
/>
)}
</>
);
};
type FalseDoorBanner = {
onDismiss?: () => void;
checkIsOnDashboard: boolean;
link: string;
};
export const FalseDoorBanner = (props: FalseDoorBanner) => {
const l10n = useL10n();
const [isLargeScreen, setIsLargeScreen] = useState(false);
const handleResize = () => {
// 1024px is equivalent to $screen-lg
if (window.innerWidth > 1024) {
setIsLargeScreen(true);
} else {
setIsLargeScreen(false);
}
};
useEffect(() => {
window.addEventListener("resize", handleResize);
});
const content = (
<p>
{l10n.getString("false-door-test-phase-3-content-part-one")}
{isLargeScreen ? <br /> : " "}
{props.checkIsOnDashboard
? l10n.getString("false-door-test-phase-3-content-part-two-dashboard")
: l10n.getString("false-door-test-phase-3-content-part-two")}
</p>
);
/* c8 ignore stop */
return (
<div className={styles.falseDoorTestWrapper}>
<div className={styles.content}>
<span className={styles.logoWrapper}>
<Image src={MozillaMonitorLogo} alt="" className={styles.logo} />
</span>
{content}
<Link
id="open-button"
className={styles.cta}
target="_blank"
href={props.link}
data-cta-id="false-door"
onClick={props.onDismiss}
>
{l10n.getString("false-door-test-cta")}
</Link>
</div>
<button
id="close-button"
className={styles.dismiss}
onClick={props.onDismiss}
>
<CloseBtn
alt={l10n.getString("false-door-test-popup-close")}
width="15"
height="15"
/>
</button>
</div>
);
};

Просмотреть файл

@ -1,44 +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/. */
"use client";
import { signIn } from "next-auth/react";
import { usePathname } from "next/navigation";
import { useL10n } from "../../../hooks/l10n";
import { useEffect } from "react";
export type Props = {
autoSignIn?: boolean;
};
function initSignIn(callbackUrl: string) {
void signIn("fxa", { callbackUrl });
}
export const SignInButton = ({ autoSignIn }: Props) => {
const l10n = useL10n();
const pathname = usePathname();
const callbackUrl = pathname === "/" ? "/user/breaches" : pathname;
useEffect(() => {
if (autoSignIn) {
initSignIn(callbackUrl);
}
}, [autoSignIn, callbackUrl]);
if (autoSignIn) {
return null;
}
return (
<button
onClick={() => initSignIn(callbackUrl)}
data-cta-id="sign-in-1"
className="button secondary"
>
{l10n.getString("sign-in")}
</button>
);
};

Просмотреть файл

@ -1,110 +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/. */
"use client";
import Image from "next/image";
import { usePathname } from "next/navigation";
import { useL10n } from "../../../hooks/l10n";
import RelayLogo from "../../../../client/images/logo-relay.svg";
import VPNLogo from "../../../../client/images/logo-vpn.svg";
export const SiteNavigation = () => {
const l10n = useL10n();
const pathname = usePathname();
const isBreachesPage = pathname === "/user/breaches";
const isSettingsPage = pathname === "/user/settings";
return (
<nav className="site-nav">
<div className="pages-nav">
<a
href="/user/breaches"
className={`nav-item ${isBreachesPage ? "current" : ""}`}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.5942 20.049C9.87439 21.3816 10.8394 22.9996 12.3539 22.9996H19.657C21.1692 22.9996 22.1344 21.3862 21.4193 20.0538L17.7796 13.2724C17.0264 11.8689 15.0148 11.8662 14.2577 13.2676L10.5942 20.049Z"
fill="white"
stroke="currentcolor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 21C16.5523 21 17 20.5523 17 20C17 19.4477 16.5523 19 16 19C15.4477 19 15 19.4477 15 20C15 20.5523 15.4477 21 16 21Z"
fill="currentcolor"
/>
<path
d="M16 17V16"
stroke="currentcolor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M7 22H5C3.89543 22 3 21.1046 3 20V11C3 9.89543 3.89543 9 5 9H19C20.1046 9 21 9.89543 21 11V13"
stroke="currentcolor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7 9V7C7 4.23858 9.23858 2 12 2C14.7614 2 17 4.23858 17 7V9"
stroke="currentcolor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
{l10n.getString("site-nav-breaches-link")}
</a>
</div>
<div className="meta-nav">
<a
href="/user/settings"
className={`nav-item ${isSettingsPage ? "current" : ""}`}
>
{l10n.getString("site-nav-settings-link")}
</a>
<a
target="_blank"
href="https://support.mozilla.org/kb/firefox-monitor"
className="nav-item"
>
{l10n.getString("site-nav-help-link")}
</a>
</div>
<div className="callouts">
<p>{l10n.getString("site-nav-ad-callout")}</p>
<a
href="https://relay.firefox.com/?utm_medium=mozilla-websites&utm_source=monitor&utm_campaign=&utm_content=nav-bar-global"
target="_blank"
>
<Image alt={l10n.getString("brand-relay")} src={RelayLogo} />
</a>
<a
href="https://www.mozilla.org/products/vpn?utm_medium=mozilla-websites&utm_source=monitor&utm_campaign=&utm_content=nav-bar-global"
target="_blank"
>
<Image alt={l10n.getString("brand-mozilla-vpn")} src={VPNLogo} />
</a>
</div>
</nav>
);
};

Просмотреть файл

@ -1,104 +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/. */
"use client";
import { Session } from "next-auth";
import Image from "next/image";
import { signOut } from "next-auth/react";
import Script from "next/script";
import { useL10n } from "../../../hooks/l10n";
import OpenInIcon from "../../../../client/images/icon-open-in.svg";
import SettingsIcon from "../../../../client/images/icon-settings.svg";
import HelpIcon from "../../../../client/images/icon-help.svg";
import SignOutIcon from "../../../../client/images/icon-signout.svg";
import { FeatureFlagName } from "../../../../db/tables/featureFlags";
export type Props = {
session: Session;
fxaSettingsUrl: string;
nonce: string | undefined;
enabledFeatureFlags: FeatureFlagName[];
};
export const UserMenu = ({
session,
fxaSettingsUrl,
nonce,
enabledFeatureFlags,
}: Props) => {
const l10n = useL10n();
if (!session) {
return null;
}
return (
<div className="user-menu-wrapper" tabIndex={-1}>
<Script
type="module"
src="/nextjs_migration/client/js/userMenu.js"
nonce={nonce}
/>
<button
aria-expanded="false"
aria-haspopup="true"
className="user-menu-button"
title={l10n.getString("menu-button-title")}
>
{/* The avatar is an SVG, which next/image doesn't process: https://nextjs.org/docs/pages/api-reference/components/image#dangerouslyallowsvg */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
// The avatar should always be provided by FxA
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
src={session.user?.fxa?.avatar}
alt={l10n.getString("menu-button-alt")}
height={46}
/>
</button>
<menu
aria-label={l10n.getString("menu-list-accessible-label")}
className="user-menu-container user-menu-popover"
role="navigation"
hidden
>
<li tabIndex={1}>
<a href={fxaSettingsUrl} target="_blank" className="user-menu-header">
<b className="user-menu-email">{session.user?.email}</b>
<div className="user-menu-subtitle">
{l10n.getString("menu-item-fxa-2")}
<Image alt="" src={OpenInIcon} />
</div>
</a>
<hr />
</li>
<li>
<a href="/user/settings" className="user-menu-link">
<Image alt="" src={SettingsIcon} />
{l10n.getString("menu-item-settings")}
</a>
</li>
<li>
<a
href="https://support.mozilla.org/kb/firefox-monitor"
target="_blank"
className="user-menu-link"
>
<Image alt="" src={HelpIcon} />
{l10n.getString("menu-item-help")}
</a>
</li>
<li>
<button
onClick={() => void signOut({ callbackUrl: "/" })}
className="user-menu-link"
>
<Image alt="" src={SignOutIcon} />
{l10n.getString("menu-item-logout")}
</button>
</li>
</menu>
</div>
);
};

Просмотреть файл

@ -1,5 +0,0 @@
<svg width="70" height="72" viewBox="0 0 70 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5288 16.8559C15.113 27.7003 15.7061 36.4495 18.3751 43.3568C21.0322 50.2333 25.8731 55.5922 34.8519 59.2261C43.8307 55.5922 48.6715 50.2333 51.3287 43.3568C53.9976 36.4495 54.5907 27.7003 54.175 16.8559L34.8519 11.8552L15.5288 16.8559ZM34.8519 7.74902L11.7785 13.7203C10.4967 38.2691 13.3168 55.5196 34.8519 63.4814C56.387 55.5196 59.2071 38.2691 57.9253 13.7203L34.8519 7.74902Z" fill="#E21587"/>
<path d="M15.4043 16.3722L15.0442 16.4654L15.03 16.8371C14.6137 27.6957 15.2021 36.5305 17.9095 43.5373C20.619 50.5495 25.565 56.0069 34.6651 59.6899L34.8527 59.7658L35.0403 59.6899C44.1404 56.0069 49.0863 50.5495 51.7959 43.5373C54.5033 36.5305 55.0917 27.6957 54.6754 16.8371L54.6612 16.4654L54.3011 16.3722L34.978 11.3714L34.8527 11.339L34.7274 11.3714L15.4043 16.3722Z" fill="#E21587" stroke="#E21587"/>
<path d="M27.1211 32.8427L32.7565 38.4781C33.1892 38.9109 33.9056 38.8569 34.2687 38.3642L44.8798 23.9634" stroke="#FFDFF0" stroke-width="2.5" stroke-linecap="round"/>
</svg>

До

Ширина:  |  Высота:  |  Размер: 1.1 KiB

Просмотреть файл

@ -1,4 +0,0 @@
<svg width="70" height="69" viewBox="0 0 70 69" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5288 16.1067C15.113 26.4691 15.7061 34.8295 18.3751 41.4297C21.0322 48.0007 25.8731 53.1213 34.8519 56.5938C43.8307 53.1213 48.6715 48.0007 51.3287 41.4297C53.9976 34.8295 54.5907 26.4691 54.175 16.1067L34.8519 11.3282L15.5288 16.1067ZM34.8519 7.40454L11.7785 13.1105C10.4967 36.5682 13.3168 53.052 34.8519 60.6599C56.387 53.052 59.2071 36.5682 57.9253 13.1105L34.8519 7.40454Z" fill="#E21587"/>
<path d="M27.1211 33.2614L32.7565 38.8968C33.1892 39.3296 33.9056 39.2756 34.2687 38.7829L44.8798 24.3821" stroke="#E21587" stroke-width="2.5" stroke-linecap="round"/>
</svg>

До

Ширина:  |  Высота:  |  Размер: 720 B

Просмотреть файл

@ -1,63 +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 React from "react";
import cloudImage from "../../../../client/images/dialog-email-clouds.svg";
import AppConstants from "../../../../appConstants";
import { getL10n } from "../../../functions/server/l10n";
import { CONST_MAX_NUM_ADDRESSES } from "../../../../constants";
export default function AddEmailDialog() {
const l10n = getL10n();
const emailLimit = CONST_MAX_NUM_ADDRESSES;
return (
<>
{/* Styles need to be included manually when fetching the component via API */}
{/* eslint-disable-next-line @next/next/no-css-tags */}
<link rel="stylesheet" href="/nextjs_migration/client/css/addEmail.css" />
<header>
<button className="close"></button>
{/* We cant use next/image when we fetch the component via API */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={cloudImage.src} alt="" />
<h2>{l10n.getString("add-email-add-another-heading")}</h2>
</header>
<form>
<p>
{l10n.getString("add-email-your-account-includes", {
total: emailLimit,
})}
</p>
<label className="text-field">
<span>{l10n.getString("add-email-address-input-label")}</span>
<input type="email" name="email-address" required />
</label>
<button className="primary" type="submit" name="email-submit">
{l10n.getString("add-email-send-verification-button")}
</button>
</form>
<template
data-success
dangerouslySetInnerHTML={{
__html: `
<p class="add-email-success">
${l10n
.getString("add-email-verify-the-link", {
email: '<b class="current-email"></b>',
"settings-href": 'href="/user/settings"',
})
// The following are special characters inserted by Fluent,
// which break the link when inserted into the tag.
// (For future strings, we can just `getElement` to properly insert
// tags into localised strings.)
.replaceAll("", "")
.replaceAll("", "")}
</p>
`,
}}
/>
</>
);
}

Просмотреть файл

@ -1,228 +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 type { UserBreaches } from "../../../functions/server/getUserBreaches";
import { getL10n } from "../../../functions/server/l10n";
import { BreachLogo } from "../../../components/server/BreachLogo";
import { getLocale } from "../../../../utils/fluent.js";
import ImageBreachesNone from "../../../../client/images/breaches-none.svg";
import ImageBreachesAllResolved from "../../../../client/images/breaches-all-resolved.svg";
import { HibpLikeDbBreach } from "../../../../utils/hibp";
import { Breach } from "../../(authenticated)/user/breaches/breaches";
function createResolveSteps(breach: any) {
const checkedArr = breach.ResolutionsChecked || [];
const resolveStepsHTML = Object.entries(breach.breachChecklist).map(
([key, value]: [string, any]) => `
<li class="resolve-list-item">
<input name="${breach.Id as string}" value="${key}" type="checkbox" ${
checkedArr.includes(key) ? "checked" : ""
}>
<p>${value.header as string}<br><i>${value.body as string}</i></p>
</li>
`,
);
return resolveStepsHTML.join("");
}
export type Props = {
userBreaches: UserBreaches;
};
export const BreachesTable = ({ userBreaches }: Props) => {
const l10n = getL10n();
function createBreachRows({ breachesData }: UserBreaches) {
const locale = getLocale();
const shortDate = new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "2-digit",
day: "2-digit",
timeZone: "UTC",
});
const shortList = new Intl.ListFormat(locale, { style: "narrow" });
const longDate = new Intl.DateTimeFormat(locale, {
dateStyle: "long",
timeZone: "UTC",
});
const longList = new Intl.ListFormat(locale, { style: "long" });
const breachRowsHTML = breachesData.verifiedEmails.flatMap((account) => {
return account.breaches.map((breach: HibpLikeDbBreach | Breach) => {
const isHidden = !account.primary || (breach as Breach).IsResolved; // initial breach hidden state
const status = (breach as Breach).IsResolved
? "resolved"
: "unresolved";
const breachDate = Date.parse(breach.BreachDate);
const addedDate = Date.parse((breach as Breach).AddedDate);
const dataClassesTranslated = breach.DataClasses.map((item) =>
l10n.getString(item),
);
const description = l10n.getString("breach-description", {
companyName: breach.Title,
breachDate: longDate.format(breachDate),
addedDate: longDate.format(addedDate),
dataClasses: longList.format(dataClassesTranslated),
});
return (
<details
key={breach.Name + account.email}
className="breach-row"
data-status={status}
data-email={account.email}
data-classes={dataClassesTranslated}
hidden={isHidden}
>
<summary>
<span className="breach-company">
<BreachLogo breach={breach as HibpLikeDbBreach} htmlTags />{" "}
{breach.Title}
</span>
<span>{shortList.format(dataClassesTranslated)}</span>
<span>
<span className="resolution-badge is-resolved">
{l10n.getString("column-status-badge-resolved")}
</span>
<span className="resolution-badge is-active">
{l10n.getString("column-status-badge-active")}
</span>
</span>
<span>{shortDate.format(addedDate)}</span>
</summary>
<article>
<p>{description}</p>
<p>
<strong>{l10n.getString("breaches-resolve-heading")}</strong>
</p>
<ol
className="resolve-list"
dangerouslySetInnerHTML={{ __html: createResolveSteps(breach) }}
/>
</article>
</details>
);
});
});
return breachRowsHTML;
}
return (
<>
<fieldset className="breaches-filter">
<input
id="breaches-unresolved"
type="radio"
name="breaches-status"
value="unresolved"
autoComplete="off"
/>
<label htmlFor="breaches-unresolved">
{/* The DOM for this element is modified by regular JavaScript files
that predate our migration to Next.js. We dont use any React-specific
features here, so hydration errors should not be a problem. */}
<output suppressHydrationWarning>&nbsp;</output>
{l10n.getString("filter-label-unresolved")}
</label>
<input
id="breaches-resolved"
type="radio"
name="breaches-status"
value="resolved"
autoComplete="off"
/>
<label htmlFor="breaches-resolved">
{/* The DOM for this element is modified by regular JavaScript files
that predate our migration to Next.js. We dont use any React-specific
features here, so hydration errors should not be a problem. */}
<output suppressHydrationWarning>&nbsp;</output>
{l10n.getString("filter-label-resolved")}
</label>
</fieldset>
{/* The DOM for this element is modified by regular JavaScript files
that predate our migration to Next.js. We dont use any React-specific
features here, so hydration errors should not be a problem. */}
<section suppressHydrationWarning className="breaches-table">
<header>
<span>{l10n.getString("column-company")}</span>
<span>{l10n.getString("column-breached-data")}</span>
{/* The active/resolved badge does not have a column header, but by
including an empty <span>, we can re-use the `nth-child`-based
selectors for the content columns. */}
<span></span>
<span>{l10n.getString("column-detected")}</span>
</header>
<div>{createBreachRows(userBreaches)}</div>
<template
className="no-breaches"
dangerouslySetInnerHTML={{
__html: `
<div class="zero-state no-breaches-message">
<img src="${
ImageBreachesNone.src as string
}" alt="" width="136" height="102" />
<h2>${l10n.getString("breaches-none-headline")}</h2>
<p>
${l10n.getString("breaches-none-copy", {
email: '<b class="current-email"></b>',
})}
</p>
<p class="add-email-cta">
<span>${l10n.getString("breaches-none-cta-blurb")}</span>
<button
class="primary"
data-cta-id="breaches-none"
data-dialog="addEmail"
>
${l10n.getString("breaches-none-cta-button")}
</button>
</p>
</div>
`,
}}
/>
<template
// The DOM for this element is modified by regular JavaScript
// files that predate our migration to Next.js. We dont use any
// React-specific features here, so hydration errors should
// not be a problem.
suppressHydrationWarning
className="all-breaches-resolved"
dangerouslySetInnerHTML={{
__html: `
<div class="zero-state all-breaches-resolved-message">
<img
src="${ImageBreachesAllResolved.src as string}"
alt=""
width="136"
height="102"
/>
<h2>${l10n.getString("breaches-all-resolved-headline")}</h2>
<p>
${l10n.getString("breaches-all-resolved-copy", {
email: '<b class="current-email"></b>',
})}
</p>
<p class="add-email-cta">
<span>${l10n.getString(
"breaches-all-resolved-cta-blurb",
)}</span>
<button
class="primary"
data-cta-id="breaches-all-resolved"
data-dialog="addEmail"
>
${l10n.getString("breaches-all-resolved-cta-button")}
</button>
</p>
</div>
`,
}}
/>
</section>
</>
);
};

Просмотреть файл

@ -1,16 +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 { ReactElement } from "react";
export type Props = {
component: ReactElement;
};
// If a component imports `react-dom/server` directly Next.js throws a
// compilation error `ReactServerComponentsError` for perf and security reasons.
export const getComponentAsString = async ({ component }: Props) => {
const ReactDOMServer = (await import("react-dom/server")).default;
return ReactDOMServer.renderToString(component);
};

Просмотреть файл

@ -1,11 +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 { headers } from "next/headers";
export function getNonce() {
// This header gets set in /src/middleware.ts:
const nonce = headers().get("x-nonce") ?? undefined;
return nonce;
}

Просмотреть файл

@ -1,61 +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 { ReactNode } from "react";
import Script from "next/script";
import { L10nProvider } from "../../contextProviders/localization";
import { getL10nBundles } from "../functions/server/l10n";
import { HandleFalseDoorTest } from "./components/client/FalseDoorBanner";
import { getCountryCode } from "../functions/server/getCountryCode";
import { headers } from "next/headers";
import AppConstants from "../../appConstants";
import { getNonce } from "./functions/server/getNonce";
import { getEnabledFeatureFlags } from "../../db/tables/featureFlags";
export default async function MigrationLayout({
children,
}: {
children: ReactNode;
}) {
const headersList = headers();
const l10nBundles = getL10nBundles();
const countryCode = getCountryCode(headersList);
const enabledFlags = await getEnabledFeatureFlags({ ignoreAllowlist: true });
const waitlistLink = AppConstants.FALSE_DOOR_TEST_LINK_PHASE_ONE;
const acceptLanguage = headersList.get("Accept-Language");
let locale = "";
if (acceptLanguage) {
const acceptedLocales = acceptLanguage.split(",");
const primaryLocale = acceptedLocales[0];
locale = primaryLocale.split(";")[0];
}
return (
<L10nProvider bundleSources={l10nBundles}>
{/* This script predates the use of React and thus shouldnt wait for
hydration to adjust the layout. */}
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
<script
type="module"
src="/nextjs_migration/client/js/resizeObserver.js"
rel="preload"
crossOrigin="anonymous"
nonce={getNonce()}
/>
<Script
type="module"
src="/nextjs_migration/client/js/analytics.js"
nonce={getNonce()}
/>
{children}
{enabledFlags.includes("FalseDoorTest") &&
waitlistLink &&
countryCode.toLowerCase() === "us" &&
["en", "en-GB", "en-US", "en-CA"].includes(locale) && (
<HandleFalseDoorTest link={waitlistLink} />
)}
</L10nProvider>
);
}

Просмотреть файл

@ -10,7 +10,7 @@ import {
req,
} from "../../../utils/hibp.js";
import { upsertBreaches } from "../../../db/tables/breaches.js";
import { Breach } from "../../deprecated/(authenticated)/user/breaches/breaches.js";
import { Breach } from "../universal/breach.js";
let breaches: Array<Breach | HibpLikeDbBreach>;

Просмотреть файл

@ -3,10 +3,32 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { DataClassEffected } from "../../../utils/subscriberBreaches";
import { HibpBreachDataTypes } from "../../deprecated/(authenticated)/user/breaches/breaches";
// TODO: Move pure functions that operate on breaches to this file
export interface Breach {
AddedDate: string;
BreachDate: string;
DataClasses: Array<string>;
Description: string;
Domain: string;
Id: number;
IsFabricated: boolean;
IsMalware: boolean;
IsResolved?: boolean;
IsRetired: boolean;
IsSensitive: boolean;
IsSpamList: boolean;
IsVerified: boolean;
LogoPath: string;
ModifiedDate: string;
Name: string;
PwnCount: number;
recencyIndex: number;
ResolutionsChecked: Array<string>;
Title: string;
}
export const BreachDataTypes = {
Passwords: "passwords",
Email: "email-addresses",
@ -53,6 +75,18 @@ export const SecurityRecommendationDataTypes = {
IP: BreachDataTypes.IP,
} as const;
export type HibpBreachDataTypes = typeof BreachDataTypes;
export interface BreachBulkResolutionRequest {
dataType: HibpBreachDataTypes;
}
export interface BreachResolutionRequest {
affectedEmail: string;
breachId: number;
resolutionsChecked: Array<HibpBreachDataTypes[keyof HibpBreachDataTypes]>;
}
export function isBreachResolved(
dataClassesAffected: DataClassEffected[],
resolvedDataClasses: Array<HibpBreachDataTypes[keyof HibpBreachDataTypes]>,

Просмотреть файл

@ -15,7 +15,6 @@ import { metropolis } from "./fonts/Metropolis/metropolis";
import { CONST_GA4_MEASUREMENT_ID } from "../constants";
import { headers } from "next/headers";
import { GoogleAnalyticsWorkaround } from "./components/client/GoogleAnalyticsWorkaround";
import { getNonce } from "./deprecated/functions/server/getNonce";
import StripeScript from "./components/client/StripeScript";
// DO NOT ADD SECRETS: Env variables added here become public.
@ -56,6 +55,7 @@ export default async function RootLayout({
}: {
children: ReactNode;
}) {
const nonce = headers().get("x-nonce") ?? "";
const currentLocale = getLocale(getL10nBundles());
const session = await getServerSession();
@ -76,7 +76,7 @@ export default async function RootLayout({
{headers().get("DNT") !== "1" && (
<GoogleAnalyticsWorkaround
gaId={CONST_GA4_MEASUREMENT_ID}
nonce={getNonce()}
nonce={nonce}
debugMode={
process.env.NEXT_PUBLIC_GA4_DEBUG_MODE === "true" &&
process.env.NODE_ENV !== "test"