Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MNTOR-2945: add del subscription api and unsub before deleting an user #4303

Merged
merged 13 commits into from
Mar 9, 2024
18 changes: 18 additions & 0 deletions src/app/functions/server/deleteAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "../../../db/tables/subscribers";
import { deactivateProfile } from "./onerep";
import { SerializedSubscriber } from "../../../next-auth";
import { deleteSubscription } from "../../../utils/fxa";

export async function deleteAccount(
subscriber: SubscriberRow | SerializedSubscriber,
Expand All @@ -21,6 +22,7 @@ export async function deleteAccount(
// get profile id
const oneRepProfileId = await getOnerepProfileId(subscriber.id);
if (oneRepProfileId) {
// try to deactivate onerep profile
try {
await deactivateProfile(oneRepProfileId);
} catch (ex) {
Expand All @@ -37,6 +39,22 @@ export async function deleteAccount(
logger.info("deactivated_onerep_profile", {
subscriber_id: subscriber.id,
});

// try to unsubscribe from subplat
mansaj marked this conversation as resolved.
Show resolved Hide resolved
if (subscriber.fxa_access_token) {
try {
const isDeleted = await deleteSubscription(subscriber.fxa_access_token);
logger.info("unsubscribe_from_subplat", {
subscriber_id: subscriber.id,
success: isDeleted,
});
} catch (ex) {
logger.error("unsubscribe_from_subplat", {
subscriber_id: subscriber.id,
exception: ex,
});
}
}
}

// delete user events only have keys. Keys point to empty objects
Expand Down
3 changes: 2 additions & 1 deletion src/appConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ const optionalEnvVars = [
'RECRUITMENT_BANNER_LINK',
'RECRUITMENT_BANNER_TEXT',
'SENTRY_DSN_LEGACY',
'FALSE_DOOR_TEST_LINK_PHASE_ONE'
'FALSE_DOOR_TEST_LINK_PHASE_ONE',
'PREMIUM_PRODUCT_ID'
]

/** @type {Record<string, string>} */
Expand Down
97 changes: 85 additions & 12 deletions src/utils/fxa.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import AppConstants from '../appConstants.js'
// to abstract fetching API endpoints from the OAuth server (instead
// of specifying them in the environment) in the future.
const FxAOAuthUtils = {
get authorizationUri () { return AppConstants.OAUTH_AUTHORIZATION_URI },
get tokenUri () { return AppConstants.OAUTH_TOKEN_URI },
get authorizationUri() { return AppConstants.OAUTH_AUTHORIZATION_URI },
get tokenUri() { return AppConstants.OAUTH_TOKEN_URI },
// TODO: Add unit test when changing this code:
/* c8 ignore next */
get profileUri () { return AppConstants.OAUTH_PROFILE_URI }
get profileUri() { return AppConstants.OAUTH_PROFILE_URI }
}

const FxAOAuthClient = new ClientOAuth2({
Expand All @@ -35,7 +35,7 @@ const FxAOAuthClient = new ClientOAuth2({
*/
// TODO: Add unit test when changing this code:
/* c8 ignore start */
async function postTokenRequest (path, token) {
async function postTokenRequest(path, token) {
const fxaTokenOrigin = new URL(AppConstants.OAUTH_TOKEN_URI).origin
const tokenUrl = `${fxaTokenOrigin}${path}`
const tokenBody = (typeof token === 'object') ? token : { token }
Expand Down Expand Up @@ -65,7 +65,7 @@ async function postTokenRequest (path, token) {
*/
// TODO: Add unit test when changing this code:
/* c8 ignore start */
async function verifyOAuthToken (token) {
async function verifyOAuthToken(token) {
try {
const response = await postTokenRequest('/v1/verify', token)
return response
Expand All @@ -84,7 +84,7 @@ async function verifyOAuthToken (token) {
*/
// TODO: Add unit test when changing this code:
/* c8 ignore start */
async function destroyOAuthToken (token) {
async function destroyOAuthToken(token) {
const tokenBody = {
...token,
client_id: AppConstants.OAUTH_CLIENT_ID,
Expand All @@ -106,7 +106,7 @@ async function destroyOAuthToken (token) {
*/
// TODO: Add unit test when changing this code:
/* c8 ignore next 4 */
async function revokeOAuthTokens (subscriber) {
async function revokeOAuthTokens(subscriber) {
await destroyOAuthToken({ token: subscriber.fxa_access_token, token_type_hint: "access_token" })
await destroyOAuthToken({ token: subscriber.fxa_refresh_token, token_type_hint: "refresh_token" })
}
Expand All @@ -116,7 +116,7 @@ async function revokeOAuthTokens (subscriber) {
*/
// TODO: Add unit test when changing this code:
/* c8 ignore start */
async function getProfileData (accessToken) {
async function getProfileData(accessToken) {
try {
const response = await fetch(FxAOAuthUtils.profileUri, {
headers: { Authorization: `Bearer ${accessToken}` }
Expand All @@ -135,9 +135,9 @@ async function getProfileData (accessToken) {
/**
* @param {string} path
*/
// TODO: Add unit test when changing this code:
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
/* c8 ignore start */
async function sendMetricsFlowPing (path) {
async function sendMetricsFlowPing(path) {
const fxaMetricsFlowUrl = new URL(path, AppConstants.NEXT_PUBLIC_FXA_SETTINGS_URL)
try {
const response = await fetch(fxaMetricsFlowUrl, {
Expand All @@ -155,12 +155,83 @@ async function sendMetricsFlowPing (path) {
}
/* c8 ignore stop */

/**
* @param {string} bearerToken
* @returns {Promise<Array<any> | null>}
*/
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
/* c8 ignore start */
async function getSubscriptions(bearerToken) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Vinnl I also refactored the code a little bit so the GET call is now separate and reusable.. in case one day we do want to address the unsubscribe vs. resubscribe CTA in settings

const subscriptionIdUrl = `${AppConstants.OAUTH_ACCOUNT_URI}/oauth/subscriptions/active`
try {
const getResp = await fetch(subscriptionIdUrl, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${bearerToken}`
}
})

if (!getResp.ok) {
throw new InternalServerError(`bad response: ${getResp.status}`)
} else {
console.info(`get_fxa_subscriptions: success`)
return await getResp.json()
}
} catch (e) {
if (e instanceof Error) {
console.error('get_fxa_subscriptions', { stack: e.stack })
}
return null
}
}
/* c8 ignore stop */

/**
* @param {string} bearerToken
* @returns {Promise<boolean>}
*/
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
/* c8 ignore start */
async function deleteSubscription(bearerToken) {
try {
const subs = await getSubscriptions(bearerToken) ?? []
let subscriptionId;
for (const sub of subs) {
if (sub && sub.productId && sub.productId === AppConstants.PREMIUM_PRODUCT_ID) {
subscriptionId = sub.subscriptionId
}
}
if (subscriptionId) {
const deleteUrl = `${AppConstants.OAUTH_ACCOUNT_URI}/oauth/subscriptions/active/${subscriptionId}`
const response = await fetch(deleteUrl, {
method: "DELETE",
headers: {
Accept: 'application/json',
Authorization: `Bearer ${bearerToken}`
}
})
if (!response.ok) {
// throw new InternalServerError(`bad response: ${response.status}`)
} else {
console.info(`delete_fxa_subscription: success - ${JSON.stringify(await response.json())}`)
mansaj marked this conversation as resolved.
Show resolved Hide resolved
}
}
return true
} catch (e) {
if (e instanceof Error) {
console.error('delete_fxa_subscription', { stack: e.stack })
}
return false
}
}
/* c8 ignore stop */

/**
* @param {crypto.BinaryLike} email
*/
// TODO: Add unit test when changing this code:
/* c8 ignore next 3 */
function getSha1 (email) {
function getSha1(email) {
return crypto.createHash('sha1').update(email).digest('hex')
}

Expand All @@ -171,5 +242,7 @@ export {
revokeOAuthTokens,
getProfileData,
sendMetricsFlowPing,
getSha1
getSha1,
getSubscriptions,
deleteSubscription
}
Loading