Merge branch 'main' into mntor-3492-e2e
This commit is contained in:
Коммит
0d3493d5c4
Двоичные данные
public/images/email/footer-bg-shapes.png
Двоичные данные
public/images/email/footer-bg-shapes.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 14 KiB |
Двоичные данные
public/images/email/hero-bg-gradient.png
Двоичные данные
public/images/email/hero-bg-gradient.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 846 B |
|
@ -10,6 +10,7 @@ import { getServerSession } from "../../../../../functions/server/getServerSessi
|
|||
import { isAdmin } from "../../../../../api/utils/auth";
|
||||
import { logger } from "@sentry/utils";
|
||||
import { captureException } from "@sentry/node";
|
||||
import { getSubscriberByFxaUid } from "../../../../../../db/tables/subscribers";
|
||||
|
||||
export async function getAttachedClientsAction() {
|
||||
const session = await getServerSession();
|
||||
|
@ -17,14 +18,23 @@ export async function getAttachedClientsAction() {
|
|||
if (
|
||||
!session?.user?.email ||
|
||||
!isAdmin(session.user.email) ||
|
||||
process.env.APP_ENV === "production"
|
||||
process.env.APP_ENV === "production" ||
|
||||
typeof session?.user?.subscriber?.fxa_uid !== "string"
|
||||
) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const subscriber = await getSubscriberByFxaUid(
|
||||
session.user.subscriber.fxa_uid,
|
||||
);
|
||||
if (!subscriber) {
|
||||
logger.error("admin_fxa_no_subscriber_found");
|
||||
return notFound();
|
||||
}
|
||||
|
||||
try {
|
||||
const attachedClients = await getAttachedClients(
|
||||
session?.user.subscriber?.fxa_access_token ?? "",
|
||||
subscriber.fxa_access_token ?? "",
|
||||
);
|
||||
return attachedClients;
|
||||
} catch (error) {
|
||||
|
|
|
@ -176,7 +176,20 @@ export async function onDeleteAccount() {
|
|||
};
|
||||
}
|
||||
|
||||
await deleteAccount(session.user.subscriber);
|
||||
const subscriber = await getSubscriberByFxaUid(
|
||||
session.user.subscriber.fxa_uid,
|
||||
);
|
||||
if (!subscriber) {
|
||||
logger.error(
|
||||
`Tried to delete an account with a session that could not be linked to a subscriber.`,
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: "delete-account-with-invalid-session",
|
||||
errorMessage: `User tried to delete their account, but we could not find it.`,
|
||||
};
|
||||
}
|
||||
await deleteAccount(subscriber);
|
||||
|
||||
// Tell the front page to display an "account deleted" notification:
|
||||
cookies().set("justDeletedAccount", "justDeletedAccount", {
|
||||
|
@ -202,7 +215,20 @@ export async function onApplyCouponCode() {
|
|||
};
|
||||
}
|
||||
|
||||
const result = await applyCurrentCouponCode(session.user.subscriber);
|
||||
const subscriber = await getSubscriberByFxaUid(
|
||||
session.user.subscriber.fxa_uid,
|
||||
);
|
||||
if (!subscriber) {
|
||||
logger.error(
|
||||
`Tried to apply a coupon code with a session that could not be linked to a subscriber.`,
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: "apply-coupon-code-with-invalid-session",
|
||||
errorMessage: `User tried to apply a coupon code, but we could not find their account.`,
|
||||
};
|
||||
}
|
||||
const result = await applyCurrentCouponCode(subscriber);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
import { NextRequest } from "next/server";
|
||||
import { AuthOptions, Profile as FxaProfile, User } from "next-auth";
|
||||
import { SubscriberRow } from "knex/types/tables";
|
||||
import { logger } from "../../functions/server/logging";
|
||||
|
||||
import {
|
||||
|
@ -26,6 +25,7 @@ import { record } from "../../functions/server/glean";
|
|||
import { renderEmail } from "../../../emails/renderEmail";
|
||||
import { SignupReportEmail } from "../../../emails/templates/signupReport/SignupReportEmail";
|
||||
import { getEnvVarsOrThrow } from "../../../envVars";
|
||||
import { sanitizeSubscriberRow } from "../../functions/server/sanitize";
|
||||
|
||||
const envVars = getEnvVarsOrThrow([
|
||||
"OAUTH_AUTHORIZATION_URI",
|
||||
|
@ -117,14 +117,11 @@ export const authOptions: AuthOptions = {
|
|||
);
|
||||
|
||||
if (subscriberFromDb) {
|
||||
profile = subscriberFromDb.fxa_profile_json as FxaProfile;
|
||||
const sanitizedSubscriber = sanitizeSubscriberRow(subscriberFromDb);
|
||||
profile = sanitizedSubscriber.fxa_profile_json as FxaProfile;
|
||||
|
||||
// MNTOR-2599 The breach_resolution object can get pretty big,
|
||||
// causing the session token cookie to balloon in size,
|
||||
// eventually resulting in a 400 Bad Request due to headers being too large.
|
||||
delete (subscriberFromDb as Partial<SubscriberRow>).breach_resolution;
|
||||
token.subscriber =
|
||||
subscriberFromDb as unknown as SerializedSubscriber;
|
||||
sanitizedSubscriber as unknown as SerializedSubscriber;
|
||||
}
|
||||
}
|
||||
if (profile) {
|
||||
|
@ -153,11 +150,9 @@ export const authOptions: AuthOptions = {
|
|||
const existingUser = await getSubscriberByFxaUid(profile.uid);
|
||||
|
||||
if (existingUser) {
|
||||
// MNTOR-2599 The breach_resolution object can get pretty big,
|
||||
// causing the session token cookie to balloon in size,
|
||||
// eventually resulting in a 400 Bad Request due to headers being too large.
|
||||
delete (existingUser as Partial<SubscriberRow>).breach_resolution;
|
||||
token.subscriber = existingUser as unknown as SerializedSubscriber;
|
||||
const sanitizedSubscriber = sanitizeSubscriberRow(existingUser);
|
||||
token.subscriber =
|
||||
sanitizedSubscriber as unknown as SerializedSubscriber;
|
||||
if (account.access_token && account.refresh_token) {
|
||||
const updatedUser = await updateFxAData(
|
||||
existingUser,
|
||||
|
@ -166,13 +161,13 @@ export const authOptions: AuthOptions = {
|
|||
account.expires_at ?? 0,
|
||||
profile,
|
||||
);
|
||||
// MNTOR-2599 The breach_resolution object can get pretty big,
|
||||
// causing the session token cookie to balloon in size,
|
||||
// eventually resulting in a 400 Bad Request due to headers being too large.
|
||||
delete (updatedUser as Partial<SubscriberRow>).breach_resolution;
|
||||
// Next-Auth implicitly converts `updatedUser` to a SerializedSubscriber,
|
||||
// hence the type assertion:
|
||||
token.subscriber = updatedUser as unknown as SerializedSubscriber;
|
||||
if (updatedUser) {
|
||||
const sanitizedUpdatedUser = sanitizeSubscriberRow(updatedUser);
|
||||
// Next-Auth implicitly converts `updatedUser` to a SerializedSubscriber,
|
||||
// hence the type assertion:
|
||||
token.subscriber =
|
||||
sanitizedUpdatedUser as unknown as SerializedSubscriber;
|
||||
}
|
||||
}
|
||||
} else if (!existingUser && profile.email) {
|
||||
const verifiedSubscriber = await addSubscriber(
|
||||
|
@ -183,10 +178,14 @@ export const authOptions: AuthOptions = {
|
|||
account.expires_at,
|
||||
profile,
|
||||
);
|
||||
// The date fields of `verifiedSubscriber` get converted to an ISO 8601
|
||||
// date string when serialised in the token, hence the type assertion:
|
||||
token.subscriber =
|
||||
verifiedSubscriber as unknown as SerializedSubscriber;
|
||||
if (verifiedSubscriber) {
|
||||
const sanitizedSubscriber =
|
||||
sanitizeSubscriberRow(verifiedSubscriber);
|
||||
// The date fields of `verifiedSubscriber` get converted to an ISO 8601
|
||||
// date string when serialised in the token, hence the type assertion:
|
||||
token.subscriber =
|
||||
sanitizedSubscriber as unknown as SerializedSubscriber;
|
||||
}
|
||||
|
||||
const allBreaches = await getBreaches();
|
||||
const unsafeBreachesForEmail = await getBreachesForEmail(
|
||||
|
@ -276,13 +275,13 @@ export const authOptions: AuthOptions = {
|
|||
Date.now() + responseTokens.expires_in * 1000,
|
||||
);
|
||||
|
||||
// MNTOR-2599 The breach_resolution object can get pretty big,
|
||||
// causing the session token cookie to balloon in size,
|
||||
// eventually resulting in a 400 Bad Request due to headers being too large.
|
||||
delete (updatedUser as Partial<SubscriberRow>).breach_resolution;
|
||||
// Next-Auth implicitly converts `updatedUser` to a SerializedSubscriber,
|
||||
// hence the type assertion:
|
||||
token.subscriber = updatedUser as unknown as SerializedSubscriber;
|
||||
if (updatedUser) {
|
||||
const sanitizedUpdatedUser = sanitizeSubscriberRow(updatedUser);
|
||||
// Next-Auth implicitly converts `updatedUser` to a SerializedSubscriber,
|
||||
// hence the type assertion:
|
||||
token.subscriber =
|
||||
sanitizedUpdatedUser as unknown as SerializedSubscriber;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("refresh_access_token", error);
|
||||
// The error property can be used client-side to handle the refresh token error
|
||||
|
|
|
@ -11,9 +11,7 @@ import {
|
|||
} from "../../../db/tables/subscriber_coupons";
|
||||
import { applyCoupon } from "../../../utils/fxa";
|
||||
|
||||
export async function applyCurrentCouponCode(
|
||||
subscriber: SubscriberRow | SerializedSubscriber,
|
||||
) {
|
||||
export async function applyCurrentCouponCode(subscriber: SubscriberRow) {
|
||||
logger.info("fxa_apply_coupon_code", {
|
||||
subscriber: subscriber.id,
|
||||
});
|
||||
|
|
|
@ -9,13 +9,10 @@ import {
|
|||
getOnerepProfileId,
|
||||
} from "../../../db/tables/subscribers";
|
||||
import { deactivateProfile } from "./onerep";
|
||||
import { SerializedSubscriber } from "../../../next-auth";
|
||||
import { deleteSubscription } from "../../../utils/fxa";
|
||||
import { record } from "./glean";
|
||||
|
||||
export async function deleteAccount(
|
||||
subscriber: SubscriberRow | SerializedSubscriber,
|
||||
) {
|
||||
export async function deleteAccount(subscriber: SubscriberRow) {
|
||||
logger.info("fxa_delete_user", {
|
||||
subscriber: subscriber.id,
|
||||
});
|
||||
|
|
|
@ -48,6 +48,7 @@ export type SanitizedSubscriberRow = SanitizationMarker &
|
|||
| "id"
|
||||
| "primary_email"
|
||||
| "primary_verified"
|
||||
| "primary_sha1"
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "fx_newsletter"
|
||||
|
@ -77,6 +78,7 @@ export function sanitizeSubscriberRow(
|
|||
id: subscriber.id,
|
||||
primary_email: subscriber.primary_email,
|
||||
primary_verified: subscriber.primary_verified,
|
||||
primary_sha1: subscriber.primary_sha1,
|
||||
created_at: subscriber.created_at,
|
||||
updated_at: subscriber.updated_at,
|
||||
fx_newsletter: subscriber.fx_newsletter,
|
||||
|
|
|
@ -4,10 +4,16 @@
|
|||
|
||||
import { Session } from "next-auth";
|
||||
import { getBillingAndSubscriptions } from "../../../utils/fxa";
|
||||
import { getSubscriberByFxaUid } from "../../../db/tables/subscribers";
|
||||
|
||||
/* c8 ignore start */
|
||||
export async function checkUserHasMonthlySubscription(user: Session["user"]) {
|
||||
if (!user.subscriber?.fxa_access_token) {
|
||||
if (!user.subscriber?.fxa_uid) {
|
||||
console.error("FXA UID not set");
|
||||
return false;
|
||||
}
|
||||
const subscriber = await getSubscriberByFxaUid(user.subscriber.fxa_uid);
|
||||
if (!subscriber || !subscriber.fxa_access_token) {
|
||||
console.error("FXA token not set");
|
||||
return false;
|
||||
}
|
||||
|
@ -18,7 +24,7 @@ export async function checkUserHasMonthlySubscription(user: Session["user"]) {
|
|||
}
|
||||
|
||||
const billingAndSubscriptionInfo = await getBillingAndSubscriptions(
|
||||
user.subscriber.fxa_access_token,
|
||||
subscriber.fxa_access_token,
|
||||
);
|
||||
|
||||
if (billingAndSubscriptionInfo === null) {
|
||||
|
|
|
@ -23,8 +23,8 @@ export const EmailHero = (props: Props) => {
|
|||
<mj-wrapper padding="24px 16px">
|
||||
<mj-section
|
||||
padding="10px 12px"
|
||||
css-class="hero_background"
|
||||
border-radius="16px 16px 0 0"
|
||||
background-color="#E7DFFF"
|
||||
>
|
||||
<mj-group>
|
||||
<mj-column
|
||||
|
@ -58,7 +58,7 @@ export const EmailHero = (props: Props) => {
|
|||
</mj-column>
|
||||
</mj-group>
|
||||
</mj-section>
|
||||
<mj-section css-class="hero_background" border-radius="0 0 16px 16px">
|
||||
<mj-section background-color="#E7DFFF" border-radius="0 0 16px 16px">
|
||||
<mj-column>
|
||||
<mj-text font-size="20px">
|
||||
<h2>{props.heading}</h2>
|
||||
|
|
|
@ -120,7 +120,7 @@ export const RedesignedEmailFooter = (props: Props) => {
|
|||
<mj-wrapper
|
||||
full-width="full-width"
|
||||
padding="50px 32px"
|
||||
css-class="footer_background"
|
||||
background-color="#F9F9FA"
|
||||
>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
|
|
|
@ -13,26 +13,12 @@ export const MetaTags = () => {
|
|||
};
|
||||
|
||||
export const HeaderStyles = () => {
|
||||
const hideBgImageOnDarkMode = `
|
||||
const enforceLightMode = `
|
||||
:root {
|
||||
color-scheme: light only;
|
||||
supported-color-schemes: light only;
|
||||
}
|
||||
|
||||
.footer_background {
|
||||
background-image: url(${process.env.SERVER_URL}/images/email/footer-bg-shapes.png);
|
||||
background-position: center bottom;
|
||||
background-repeat: no-repeat;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.hero_background {
|
||||
background-image: url(${process.env.SERVER_URL}/images/email/hero-bg-gradient.png);
|
||||
background-repeat: repeat;
|
||||
background-position-x: 0;
|
||||
color: #000000 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
return <mj-style>{hideBgImageOnDarkMode}</mj-style>;
|
||||
return <mj-style>{enforceLightMode}</mj-style>;
|
||||
};
|
||||
|
|
|
@ -3,10 +3,13 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { DefaultSession } from "next-auth";
|
||||
import { SubscriberRow } from "knex/types/tables";
|
||||
import { ISO8601DateString } from "./utils/parse";
|
||||
import { SanitizedSubscriberRow } from "./app/functions/server/sanitize";
|
||||
|
||||
export type SerializedSubscriber = Omit<SubscriberRow, "created_at"> & {
|
||||
export type SerializedSubscriber = Omit<
|
||||
SanitizedSubscriberRow,
|
||||
"created_at"
|
||||
> & {
|
||||
created_at: ISO8601DateString;
|
||||
};
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче