MNTOR-2224 - allow updating FxA profile info from front-end without sign-out (#3474)

* MNTOR-2224 - refresh FxA profile when update() from useSession is called
---------
Co-authored-by: Vincent <Vinnl@users.noreply.github.com>
This commit is contained in:
Robert Helmer 2023-09-29 09:20:28 -07:00 коммит произвёл GitHub
Родитель 36f9bce11b
Коммит c29b4a7d81
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 66 добавлений и 219 удалений

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

@ -517,3 +517,6 @@ leaked-security-questions-steps-subtitle = This requires access to your account,
# $breach_name is the name of the breach where the security questions were found.
leaked-security-questions-step-one = Update your security questions on <link_to_breach_site>{ $breach_name }</link_to_breach_site>.
leaked-security-questions-step-two = Update them on any other site where you used the same security questions. Be sure to use different security questions for every account.
# Subscription
subscription-check-loading = Loading, please wait…

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

@ -16,6 +16,7 @@ import {
getExposureReduction,
} from "../../../../../../../../functions/server/dashboard";
import { Button } from "../../../../../../../../components/server/Button";
import { hasPremium } from "../../../../../../../../functions/universal/user";
export default async function WelcomeToPremium() {
const l10n = getL10n();
@ -25,6 +26,11 @@ export default async function WelcomeToPremium() {
redirect("/redesign/user/dashboard/");
}
// The user may have subscribed and just need their session updated - they will be redirected back to try again if it looks valid.
if (!hasPremium(session.user)) {
redirect(`${process.env.NEXTAUTH_URL}/redesign/user/dashboard/subscribed`);
}
const result = await getOnerepProfileId(session.user.subscriber.id);
const profileId = result[0]["onerep_profile_id"] ?? -1;
const scanResultItems =

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

@ -2,77 +2,50 @@
* 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 { getServerSession } from "next-auth";
import { getOnerepProfileId } from "../../../../../../../db/tables/subscribers";
import { authOptions } from "../../../../../../api/utils/auth";
import {
activateProfile,
getAllScanResults,
isEligibleForPremium,
optoutProfile,
} from "../../../../../../functions/server/onerep";
import {
getLatestOnerepScanResults,
addOnerepScanResults,
} from "../../../../../../../db/tables/onerep_scans";
import { getCountryCode } from "../../../../../../functions/server/getCountryCode";
import { headers } from "next/headers";
"use client";
export default async function Subscribed() {
const session = await getServerSession(authOptions);
if (!session?.user?.subscriber) {
throw new Error("No session");
}
import { useSession } from "next-auth/react";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
if (!(await isEligibleForPremium(session.user, getCountryCode(headers())))) {
throw new Error("Not eligible for premium");
}
import { hasPremium } from "../../../../../../functions/universal/user";
import { captureException } from "@sentry/browser";
import { useL10n } from "../../../../../../hooks/l10n";
const result = await getOnerepProfileId(session.user.subscriber.id);
const profileId = result[0]["onerep_profile_id"] as number;
/**
* Client-side page to update session info.
*
* Next-Auth does not have a simple way to do this purely from the server-side, so we
* use this page to check and redirect appropriately.
*
* NOTE: this does not replace doing server-side `hasPremium` checks! This is just
* a convenience so users do not need to sign out and back in to refresh their session
* after subscribing.
*/
export default function Subscribed() {
const l10n = useL10n();
const { update } = useSession();
const router = useRouter();
try {
await activateProfile(profileId);
await optoutProfile(profileId);
} catch (ex) {
console.debug(ex); // TODO handle
}
useEffect(() => {
async function updateSession() {
try {
const result = await update();
if (hasPremium(result?.user)) {
router.replace(
`/redesign/user/dashboard/fix/data-broker-profiles/welcome-to-premium`
);
} else {
router.replace(`/`);
}
} catch (ex) {
console.error(ex);
captureException(ex);
router.replace(`/`);
}
}
void updateSession();
}, [update, router]);
const dev =
process.env.NODE_ENV === "development" || process.env.APP_ENV === "heroku";
const latestScan = await getLatestOnerepScanResults(profileId);
if (!latestScan.scan) {
throw new Error("Must have performed manual scan");
}
const scans = await getAllScanResults(profileId);
// In dev mode, record scans every time this page is reloaded.
// The webhook does this in production.
if (dev) {
await addOnerepScanResults(
profileId,
latestScan.scan.onerep_scan_id,
scans,
"initial",
latestScan.scan.onerep_scan_status
);
}
return (
<div>
<div>
<h3>You are now subscribed</h3>
{dev ? (
<div>
<h3>Dev mode enabled</h3>
<p>Reload this page to update scan</p>
<pre>{JSON.stringify(scans, null, 2)}</pre>
</div>
) : (
""
)}
</div>
</div>
);
return <div>{l10n.getString("subscription-check-loading")}</div>;
}

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

@ -1,41 +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 { getServerSession } from "next-auth";
import { getOnerepProfileId } from "../../../../../../../db/tables/subscribers";
import { authOptions } from "../../../../../../api/utils/auth";
import {
deactivateProfile,
isEligibleForPremium,
} from "../../../../../../functions/server/onerep";
import { getCountryCode } from "../../../../../../functions/server/getCountryCode";
import { headers } from "next/headers";
export default async function Unsubscribed() {
const session = await getServerSession(authOptions);
if (!session?.user?.subscriber) {
throw new Error("No session");
}
if (!(await isEligibleForPremium(session.user, getCountryCode(headers())))) {
throw new Error("Not eligible for premium");
}
const result = await getOnerepProfileId(session.user.subscriber.id);
const profileId = result[0]["onerep_profile_id"] as number;
try {
await deactivateProfile(profileId);
} catch (ex) {
console.debug(ex); // TODO handle
}
return (
<div>
<div>
<h3>You are now unsubscribed</h3>
</div>
</div>
);
}

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

@ -1,43 +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 { getOnerepProfileId } from "../../../../../../../db/tables/subscribers";
import { getServerSession } from "next-auth";
import { authOptions } from "../../../../../../api/utils/auth";
import {
activateProfile,
optoutProfile,
} from "../../../../../../functions/server/onerep";
import Script from "next/script";
import { isUserSubscribed } from "../../../../../../functions/server/isUserSubscribed";
export function generateMetadata() {
return {
title: "Welcome - Results",
};
}
export default async function UserWelcomeRemove() {
const session = await getServerSession(authOptions);
if (!session?.user?.subscriber?.id) {
throw new Error("No session");
}
if (!(await isUserSubscribed())) {
return (
<Script id="redirect">window.location = &quot;results&quot;;</Script>
);
}
const result = await getOnerepProfileId(session.user.subscriber.id);
const profileId = result[0]["onerep_profile_id"] as number;
await activateProfile(profileId);
await optoutProfile(profileId);
return (
<main>
<h2>Profile activated and removal has started</h2>
</main>
);
}

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

@ -63,15 +63,8 @@ export const authOptions: AuthOptions = {
},
token: AppConstants.OAUTH_TOKEN_URI,
userinfo: {
request: async (context) => {
const response = await fetch(AppConstants.OAUTH_PROFILE_URI, {
headers: {
Authorization: `Bearer ${context.tokens.access_token ?? ""}`,
},
});
const userInfo = (await response.json()) as Profile;
return userInfo;
},
request: async (context) =>
fetchUserInfo(context.tokens.access_token ?? ""),
},
clientId: AppConstants.OAUTH_CLIENT_ID,
clientSecret: AppConstants.OAUTH_CLIENT_SECRET,
@ -86,14 +79,16 @@ export const authOptions: AuthOptions = {
twoFactorAuthentication: profile.twoFactorAuthentication,
metricsEnabled: profile.metricsEnabled,
locale: profile.locale,
};
} as Profile;
},
},
],
callbacks: {
// Unused arguments also listed to show what's available:
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async jwt({ token, account, profile, trigger }) {
if (trigger === "update") {
profile = await fetchUserInfo(token.subscriber?.fxa_access_token ?? "");
}
if (profile) {
token.fxa = {
locale: profile.locale,
@ -200,6 +195,16 @@ export const authOptions: AuthOptions = {
},
};
async function fetchUserInfo(accessToken: string) {
const response = await fetch(AppConstants.OAUTH_PROFILE_URI, {
headers: {
Authorization: `Bearer ${accessToken ?? ""}`,
},
});
const userInfo = (await response.json()) as Profile;
return userInfo;
}
export function bearerToken(req: NextRequest) {
const requestHeaders = new Headers(req.headers);
requestHeaders.get("authorization");

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

@ -1,56 +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 { getServerSession } from "next-auth";
import { authOptions } from "../../api/utils/auth";
type FxaSubscriptionResponse = {
subscriptions: Array<{
product_id: string;
plan_id: string;
status: "active";
}>;
};
export const isUserSubscribed = async () => {
// Fetch list of subscriptions.
let subscriptions: FxaSubscriptionResponse;
const session = await getServerSession(authOptions);
if (!session?.user?.subscriber?.id) {
throw new Error("No session");
}
const bearerToken = session?.user.subscriber?.fxa_access_token;
if (bearerToken) {
const url = `${
process.env.OAUTH_API_URI ?? ""
}/oauth/mozilla-subscriptions/customer/billing-and-subscriptions`;
const result = await fetch(url, {
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${bearerToken}`,
},
});
if (result.ok) {
subscriptions = await result.json();
for (const subscription of subscriptions.subscriptions) {
if (
subscription.product_id ===
process.env.NEXT_PUBLIC_PREMIUM_PRODUCT_ID &&
(subscription.plan_id ===
process.env.NEXT_PUBLIC_PREMIUM_PLAN_ID_MONTHLY_US ||
subscription.plan_id ===
process.env.NEXT_PUBLIC_PREMIUM_PLAN_ID_YEARLY_US) &&
subscription.status === "active"
) {
return true;
}
}
return false;
}
} else {
console.error("User has no bearer token");
}
};