зеркало из https://github.com/mozilla/fxa.git
Merge pull request #14368 from mozilla/FXA-5358
task(admin-panel): Unsubcribe users from newsletters
This commit is contained in:
Коммит
2d8807b6f8
|
@ -77,6 +77,12 @@ export const UNLINK_ACCOUNT = gql`
|
|||
}
|
||||
`;
|
||||
|
||||
export const UNSUBSCRIBE_FROM_MAILING_LISTS = gql`
|
||||
mutation unsubscribeFromMailingLists($uid: String!) {
|
||||
unsubscribeFromMailingLists(uid: $uid)
|
||||
}
|
||||
`;
|
||||
|
||||
export const LinkedAccount = ({
|
||||
uid,
|
||||
authAt,
|
||||
|
@ -189,7 +195,6 @@ export const DangerZone = ({
|
|||
window.alert('Error in unconfirming email');
|
||||
},
|
||||
});
|
||||
|
||||
const handleUnverify = () => {
|
||||
if (!window.confirm('Are you sure? This cannot be undone.')) {
|
||||
return;
|
||||
|
@ -197,6 +202,30 @@ export const DangerZone = ({
|
|||
unverify({ variables: { email: email.email } });
|
||||
};
|
||||
|
||||
const [unsubscribeFromMailingLists] = useMutation(
|
||||
UNSUBSCRIBE_FROM_MAILING_LISTS,
|
||||
{
|
||||
onCompleted: (data) => {
|
||||
if (data.unsubscribeFromMailingLists) {
|
||||
window.alert(
|
||||
"The user's email has been unsubscribed from mozilla mailing lists."
|
||||
);
|
||||
} else {
|
||||
window.alert('Unsubscribing was not successful.');
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
window.alert('Unexpected error encountered!');
|
||||
},
|
||||
}
|
||||
);
|
||||
const handleUnsubscribeFromMailingLists = () => {
|
||||
if (!window.confirm('Are you sure? This cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
unsubscribeFromMailingLists({ variables: { uid } });
|
||||
};
|
||||
|
||||
const [disableAccount] = useMutation(DISABLE_ACCOUNT, {
|
||||
onCompleted: () => {
|
||||
window.alert('The account has been disabled.');
|
||||
|
@ -265,6 +294,7 @@ export const DangerZone = ({
|
|||
AdminPanelFeature.UnverifyEmail,
|
||||
AdminPanelFeature.DisableAccount,
|
||||
AdminPanelFeature.EnableAccount,
|
||||
AdminPanelFeature.UnsubscribeFromMailingLists,
|
||||
]}
|
||||
>
|
||||
<h3 className="mt-0 mb-1 bg-red-600 font-medium h-8 pb-8 pl-1 pt-1 rounded-sm text-lg text-white">
|
||||
|
@ -348,6 +378,24 @@ export const DangerZone = ({
|
|||
</div>
|
||||
</Guard>
|
||||
)}
|
||||
<Guard features={[AdminPanelFeature.UnsubscribeFromMailingLists]}>
|
||||
<h2 className="text-lg account-header">
|
||||
Unsubscribe From Mailing Lists
|
||||
</h2>
|
||||
<div className="border-l-2 border-red-600 mb-4 pl-4">
|
||||
<p className="text-base leading-6">
|
||||
Unsubscribe user from <b>all</b> mozilla mailing lists.
|
||||
</p>
|
||||
<button
|
||||
className="bg-grey-10 border-2 border-grey-100 font-medium h-12 leading-6 mt-4 mr-4 rounded text-red-700 w-40 hover:border-2 hover:border-grey-10 hover:bg-grey-50 hover:text-red-700"
|
||||
type="button"
|
||||
data-testid="unsubscribe-from-mailing-lists"
|
||||
onClick={handleUnsubscribeFromMailingLists}
|
||||
>
|
||||
Unsubscribe
|
||||
</button>
|
||||
</div>
|
||||
</Guard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -706,6 +754,7 @@ export const Account = ({
|
|||
disabledAt: disabledAt!,
|
||||
email: primaryEmail, // only the primary for now
|
||||
onCleared: onCleared,
|
||||
unsubscribeToken: '<USER_TOKEN>',
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/* 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 { MockedResponse } from '@apollo/client/testing';
|
||||
import { UNSUBSCRIBE_FROM_MAILING_LISTS } from '.';
|
||||
|
||||
export const mockUnsubscribe = (success: boolean): MockedResponse => {
|
||||
const request = {
|
||||
query: UNSUBSCRIBE_FROM_MAILING_LISTS,
|
||||
};
|
||||
|
||||
let result = undefined;
|
||||
let error = undefined;
|
||||
if (success) {
|
||||
result = {
|
||||
data: {
|
||||
status: success,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
error = new Error('Unsubscribe failed.');
|
||||
}
|
||||
|
||||
return {
|
||||
request,
|
||||
result,
|
||||
error,
|
||||
};
|
||||
};
|
|
@ -10,6 +10,7 @@ import {
|
|||
CLEAR_BOUNCES_BY_EMAIL,
|
||||
EDIT_LOCALE,
|
||||
RECORD_ADMIN_SECURITY_EVENT,
|
||||
UNSUBSCRIBE_FROM_MAILING_LISTS,
|
||||
} from './Account/index';
|
||||
import { GET_ACCOUNT_BY_EMAIL, AccountSearch, GET_EMAILS_LIKE } from './index';
|
||||
import {
|
||||
|
@ -459,3 +460,64 @@ describe('Editing user locale', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsubscribe from mailing lists', () => {
|
||||
class Unsubscribe {
|
||||
static readonly ErrorMessage = 'Error unsubscribing';
|
||||
|
||||
static request(uid: string) {
|
||||
return {
|
||||
query: UNSUBSCRIBE_FROM_MAILING_LISTS,
|
||||
variables: {
|
||||
uid: uid,
|
||||
},
|
||||
};
|
||||
}
|
||||
static result(success: boolean) {
|
||||
return {
|
||||
data: { unsubscribeFromMailingLists: success },
|
||||
};
|
||||
}
|
||||
|
||||
static mock(uid: string, success: boolean) {
|
||||
return {
|
||||
request: this.request(uid),
|
||||
result: this.result(success),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let alertSpy: jest.SpyInstance;
|
||||
|
||||
async function renderAndClickUnSubscribe(success: boolean) {
|
||||
renderView([
|
||||
GetAccountsByEmail.mock(testEmail, false, true),
|
||||
Unsubscribe.mock('123', success),
|
||||
]);
|
||||
|
||||
jest.spyOn(window, 'confirm').mockImplementation(() => true);
|
||||
alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByTestId('email-input'), {
|
||||
target: { value: testEmail },
|
||||
});
|
||||
fireEvent.click(screen.getByTestId('search-button'));
|
||||
await waitFor(() => screen.getByTestId('account-section'));
|
||||
fireEvent.click(screen.getByTestId('unsubscribe-from-mailing-lists'));
|
||||
await waitFor(() => new Promise((r) => setTimeout(r, 100)));
|
||||
}
|
||||
|
||||
it('handles unsubscribe', async () => {
|
||||
await renderAndClickUnSubscribe(true);
|
||||
expect(alertSpy).toBeCalledWith(
|
||||
"The user's email has been unsubscribed from mozilla mailing lists."
|
||||
);
|
||||
});
|
||||
|
||||
it('handles unsubscribe failure', async () => {
|
||||
await renderAndClickUnSubscribe(false);
|
||||
expect(alertSpy).toHaveBeenCalledWith('Unsubscribing was not successful.');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -199,7 +199,7 @@ export const AccountSearch = () => {
|
|||
return filteredList.map((item) => (
|
||||
<a
|
||||
key={item}
|
||||
className="p-2 border-b border-grey-100 block hover:bg-grey-10 focus:bg-grey-10"
|
||||
className="p-2 border-b border-grey-100 block hover:bg-grey-10 focus:bg-grey-10 z-50"
|
||||
href="#suggestion"
|
||||
onClick={(e: any) => {
|
||||
e.preventDefault();
|
||||
|
@ -266,7 +266,7 @@ export const AccountSearch = () => {
|
|||
</button>
|
||||
{showSuggestion && filteredList.length > 0 && (
|
||||
<div
|
||||
className="suggestions-list absolute top-full w-full bg-white border border-grey-100 mt-3 shadow-sm rounded overflow-hidden"
|
||||
className="suggestions-list absolute top-full w-full bg-white border border-grey-100 mt-3 shadow-sm rounded overflow-hidden z-50"
|
||||
data-testid="email-suggestions"
|
||||
>
|
||||
{renderSuggestions()}
|
||||
|
|
|
@ -23,6 +23,7 @@ import { DatabaseService } from './database/database.service';
|
|||
import { EventLoggingModule } from './event-logging/event-logging.module';
|
||||
import { GqlModule } from './gql/gql.module';
|
||||
import { SubscriptionModule } from './subscriptions/subscriptions.module';
|
||||
import { NewslettersModule } from './newsletters/newsletters.module';
|
||||
import { BackendModule } from './backend/backend.module';
|
||||
|
||||
const version = getVersionInfo(__dirname);
|
||||
|
@ -37,6 +38,7 @@ const version = getVersionInfo(__dirname);
|
|||
DatabaseModule,
|
||||
EventLoggingModule,
|
||||
SubscriptionModule,
|
||||
NewslettersModule,
|
||||
GqlModule,
|
||||
GraphQLModule.forRootAsync({
|
||||
imports: [ConfigModule, SentryModule],
|
||||
|
|
|
@ -524,6 +524,26 @@ const conf = convict({
|
|||
default: path.resolve(__dirname, '../config/public-key.json'),
|
||||
env: 'PUBLIC_KEY_FILE',
|
||||
},
|
||||
newsletters: {
|
||||
basketHost: {
|
||||
doc: 'Host where basket api lives',
|
||||
format: String,
|
||||
default: 'basket.mozilla.org',
|
||||
env: 'BASKET_HOST',
|
||||
},
|
||||
basketApiKey: {
|
||||
doc: 'Api key for basket',
|
||||
format: String,
|
||||
default: '',
|
||||
env: 'BASKET_API_KEY',
|
||||
},
|
||||
newsletterHost: {
|
||||
doc: 'Host where newsletter api lives',
|
||||
format: String,
|
||||
default: 'www.mozilla.org',
|
||||
env: 'NEWSLETTER_HOST',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const configDir = __dirname;
|
||||
|
|
|
@ -12,6 +12,7 @@ export enum EventNames {
|
|||
DisableLogin = 'disable-login',
|
||||
AccountSearch = 'account-search',
|
||||
EditLocale = 'edit-locale',
|
||||
UnsubscribeFromMailingLists = 'unsubscribe-from-mailing-lists',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* 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 { Provider } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
@ -41,6 +42,7 @@ import { EventLoggingService } from '../../event-logging/event-logging.service';
|
|||
import { SubscriptionsService } from '../../subscriptions/subscriptions.service';
|
||||
import { AccountResolver } from './account.resolver';
|
||||
import { AuthClientService } from '../../backend/auth-client.service';
|
||||
import { BasketService } from '../../newsletters/basket.service';
|
||||
|
||||
export const chance = new Chance();
|
||||
|
||||
|
@ -91,6 +93,7 @@ describe('AccountResolver', () => {
|
|||
},
|
||||
};
|
||||
let authClient: any;
|
||||
let basketService: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
knex = await testDatabaseSetup();
|
||||
|
@ -150,7 +153,23 @@ describe('AccountResolver', () => {
|
|||
},
|
||||
};
|
||||
|
||||
basketService = {};
|
||||
const MockBasket: Provider = {
|
||||
provide: BasketService,
|
||||
useValue: basketService,
|
||||
};
|
||||
|
||||
const MockDb: Provider = {
|
||||
provide: DatabaseService,
|
||||
useValue: db,
|
||||
};
|
||||
|
||||
authClient = {};
|
||||
const MockAuthClient = {
|
||||
provide: AuthClientService,
|
||||
useValue: authClient,
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AccountResolver,
|
||||
|
@ -159,8 +178,9 @@ describe('AccountResolver', () => {
|
|||
MockConfig,
|
||||
MockMetricsFactory,
|
||||
MockSubscription,
|
||||
{ provide: DatabaseService, useValue: db },
|
||||
{ provide: AuthClientService, useValue: authClient },
|
||||
MockBasket,
|
||||
MockDb,
|
||||
MockAuthClient,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
@ -347,4 +367,55 @@ describe('AccountResolver', () => {
|
|||
expect(authClient.passwordForgotSendCode).toBeCalledTimes(1);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
describe('unsubscribes from mailing lists', () => {
|
||||
const fakeToken = '123';
|
||||
const uid = USER_1.uid;
|
||||
const badUid = uid.replace(/\d/g, '0');
|
||||
const email = USER_1.email;
|
||||
|
||||
beforeEach(() => {
|
||||
basketService.getUserToken = jest.fn().mockResolvedValue(fakeToken);
|
||||
basketService.unsubscribeAll = jest.fn().mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it('unsubscribes successfully', async () => {
|
||||
const result = await resolver.unsubscribeFromMailingLists(uid);
|
||||
|
||||
expect(basketService.getUserToken).toBeCalledWith(email);
|
||||
expect(basketService.unsubscribeAll).toBeCalledWith(fakeToken);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('handles bad request - unsubscribe', async () => {
|
||||
basketService.unsubscribeAll = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await resolver.unsubscribeFromMailingLists(uid);
|
||||
|
||||
expect(basketService.getUserToken).toBeCalledWith(USER_1.email);
|
||||
expect(basketService.unsubscribeAll).toBeCalledWith(fakeToken);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('handles bad request - invalid basket user', async () => {
|
||||
basketService.getUserToken = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await resolver.unsubscribeFromMailingLists(uid);
|
||||
|
||||
expect(basketService.getUserToken).toBeCalledWith(email);
|
||||
expect(basketService.unsubscribeAll).toBeCalledTimes(0);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('handles bad request - invalid fxa user', async () => {
|
||||
basketService.getUserToken = jest.fn().mockResolvedValue(undefined);
|
||||
basketService.unsubscribeAll = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await resolver.unsubscribeFromMailingLists(badUid);
|
||||
|
||||
expect(basketService.getUserToken).toBeCalledTimes(0);
|
||||
expect(basketService.unsubscribeAll).toBeCalledTimes(0);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -38,6 +38,7 @@ import { Email as EmailType } from '../../gql/model/emails.model';
|
|||
import { SubscriptionsService } from '../../subscriptions/subscriptions.service';
|
||||
import { AuthClientService } from '../../backend/auth-client.service';
|
||||
import AuthClient from 'fxa-auth-client';
|
||||
import { BasketService } from '../../newsletters/basket.service';
|
||||
|
||||
const ACCOUNT_COLUMNS = [
|
||||
'uid',
|
||||
|
@ -89,6 +90,7 @@ export class AccountResolver {
|
|||
private subscriptionsService: SubscriptionsService,
|
||||
private configService: ConfigService<AppConfig>,
|
||||
private eventLogging: EventLoggingService,
|
||||
private basketService: BasketService,
|
||||
@Inject(AuthClientService) private authAPI: AuthClient
|
||||
) {}
|
||||
|
||||
|
@ -373,4 +375,36 @@ export class AccountResolver {
|
|||
});
|
||||
return !!result;
|
||||
}
|
||||
|
||||
@Features(AdminPanelFeature.UnsubscribeFromMailingLists)
|
||||
@Mutation((returns) => Boolean)
|
||||
public async unsubscribeFromMailingLists(@Args('uid') uid: string) {
|
||||
// Look up email. This end point is protected, but using a uid would makes it harder
|
||||
// to abuse regardless.
|
||||
const account = await this.db.account
|
||||
.query()
|
||||
.select('email')
|
||||
.where({ uid: uuidTransformer.to(uid) })
|
||||
.first();
|
||||
|
||||
if (!account) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look up user token
|
||||
const token = await this.basketService.getUserToken(account.email);
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Request that user is unsubscribed from mailing list
|
||||
const success = await this.basketService.unsubscribeAll(token);
|
||||
|
||||
// Record an event if action was successful
|
||||
if (success) {
|
||||
this.eventLogging.onEvent(EventNames.UnsubscribeFromMailingLists);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { AccountResolver } from './account/account.resolver';
|
|||
import { EmailBounceResolver } from './email-bounce/email-bounce.resolver';
|
||||
import { RelyingPartyResolver } from './relying-party/relying-party.resolver';
|
||||
import { BackendModule } from '../backend/backend.module';
|
||||
import { NewslettersModule } from '../newsletters/newsletters.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -17,6 +18,7 @@ import { BackendModule } from '../backend/backend.module';
|
|||
SubscriptionModule,
|
||||
EventLoggingModule,
|
||||
BackendModule,
|
||||
NewslettersModule,
|
||||
],
|
||||
providers: [AccountResolver, EmailBounceResolver, RelyingPartyResolver],
|
||||
})
|
||||
|
|
|
@ -174,6 +174,7 @@ export interface IMutation {
|
|||
sendPasswordResetEmail(email: string): boolean | Promise<boolean>;
|
||||
recordAdminSecurityEvent(name: string, uid: string): boolean | Promise<boolean>;
|
||||
unlinkAccount(uid: string): boolean | Promise<boolean>;
|
||||
unsubscribeFromMailingLists(uid: string): boolean | Promise<boolean>;
|
||||
clearEmailBounce(email: string): boolean | Promise<boolean>;
|
||||
updateNotes(notes: string, id: string): boolean | Promise<boolean>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/* 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 { ConfigService } from '@nestjs/config';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppConfig } from '../config';
|
||||
import { MockConfig, mockConfigOverrides } from '../mocks';
|
||||
import { BasketService } from './basket.service';
|
||||
|
||||
describe('BasketService', () => {
|
||||
const validEmail = 'success@example.com';
|
||||
const invalidEmail = 'failure@example.com';
|
||||
let service: BasketService;
|
||||
let config: AppConfig['newsletters'];
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [BasketService, MockConfig],
|
||||
}).compile();
|
||||
service = module.get<BasketService>(BasketService);
|
||||
config = module.get<ConfigService>(ConfigService).get('').newsletters;
|
||||
});
|
||||
|
||||
it('has api key', () => {
|
||||
// Ensure basket api key is set in config/secrets.json or as env variable
|
||||
expect(config.basketApiKey).toBeDefined();
|
||||
});
|
||||
|
||||
it('looks up uid by valid email', async () => {
|
||||
if (!config.basketApiKey) {
|
||||
return;
|
||||
}
|
||||
const token = await service.getUserToken(validEmail);
|
||||
expect(token).toBeDefined();
|
||||
expect(token.length).toBeGreaterThan(8);
|
||||
});
|
||||
|
||||
it('looks up invalid email', async () => {
|
||||
if (!config.basketApiKey) {
|
||||
return;
|
||||
}
|
||||
const token = await service.getUserToken(invalidEmail);
|
||||
expect(token).toBeUndefined();
|
||||
});
|
||||
|
||||
it('unsubscribes from mailing lists', async () => {
|
||||
if (!config.basketApiKey) {
|
||||
return;
|
||||
}
|
||||
const token = await service.getUserToken(validEmail);
|
||||
const result = await service.unsubscribeAll(token);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('Missing api key', () => {
|
||||
beforeAll(async () => {
|
||||
mockConfigOverrides.newsletters = {
|
||||
newsletterHost: '',
|
||||
basketHost: '',
|
||||
basketApiKey: '',
|
||||
};
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [BasketService, MockConfig],
|
||||
}).compile();
|
||||
|
||||
service = module.get<BasketService>(BasketService);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete mockConfigOverrides.newsletters;
|
||||
});
|
||||
|
||||
it('throws on query', async () => {
|
||||
await expect(async () => {
|
||||
await service.getUserToken(validEmail);
|
||||
}).rejects.toThrowError('No API key configured!');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/* 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 { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import fetch from 'node-fetch';
|
||||
import { AppConfig } from '../config';
|
||||
|
||||
/**
|
||||
* A service for basket and newsletter APIs.
|
||||
*/
|
||||
@Injectable()
|
||||
export class BasketService {
|
||||
/** Basket config */
|
||||
private readonly config: AppConfig['newsletters'];
|
||||
|
||||
private get basketUrl() {
|
||||
return `https://${this.config.basketHost}`;
|
||||
}
|
||||
|
||||
private get newsLetterUrl() {
|
||||
return `https://${this.config.newsletterHost}`;
|
||||
}
|
||||
|
||||
constructor(configService: ConfigService<AppConfig>) {
|
||||
const config = configService.get('newsletters');
|
||||
if (!config) {
|
||||
throw new Error('No basket api key defined!');
|
||||
}
|
||||
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates a user token by email in basket
|
||||
* @param email
|
||||
* @returns User's token or undefined if no user is found.
|
||||
*/
|
||||
async getUserToken(email: string) {
|
||||
// Check that API key was configured.
|
||||
if (!this.config.basketApiKey) {
|
||||
throw new Error('No API key configured!');
|
||||
}
|
||||
|
||||
const url = getUrl(this.basketUrl, '/news/lookup-user/', {
|
||||
email,
|
||||
'api-key': this.config.basketApiKey,
|
||||
});
|
||||
const resp = await fetch(url);
|
||||
|
||||
if (resp.status === 200) {
|
||||
const body = await resp.json();
|
||||
return body?.token;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes a user from all email lists
|
||||
* @param userToken - A valid user token. Can be located with getUserToken method.
|
||||
* @returns True if request was successful.
|
||||
*/
|
||||
async unsubscribeAll(userToken: string) {
|
||||
const url = getUrl(
|
||||
this.newsLetterUrl,
|
||||
`/en-US/newsletter/existing/${userToken}/`,
|
||||
{}
|
||||
);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('form-TOTAL_FORMS', '0');
|
||||
params.append('form-INITIAL_FORMS', '0');
|
||||
params.append('form-MIN_NUM_FORMS', '0');
|
||||
params.append('country', 'us');
|
||||
params.append('lang', 'en');
|
||||
params.append('format', 'H');
|
||||
params.append('remove_all', 'on');
|
||||
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: params,
|
||||
});
|
||||
return resp?.status === 200;
|
||||
}
|
||||
}
|
||||
|
||||
function getUrl<T>(baseUrl: string, path: string, params: Record<string, any>) {
|
||||
const q = Object.entries(params)
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
||||
.join('&');
|
||||
return `${baseUrl}${path}?${q}`;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
/* 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 { Module } from '@nestjs/common';
|
||||
import { BasketService } from './basket.service';
|
||||
|
||||
@Module({
|
||||
providers: [BasketService],
|
||||
exports: [BasketService],
|
||||
})
|
||||
export class NewslettersModule {}
|
|
@ -169,6 +169,7 @@ type Mutation {
|
|||
sendPasswordResetEmail(email: String!): Boolean!
|
||||
recordAdminSecurityEvent(name: String!, uid: String!): Boolean!
|
||||
unlinkAccount(uid: String!): Boolean!
|
||||
unsubscribeFromMailingLists(uid: String!): Boolean!
|
||||
clearEmailBounce(email: String!): Boolean!
|
||||
updateNotes(notes: String!, id: String!): Boolean!
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ export enum PermissionLevel {
|
|||
}
|
||||
|
||||
/** Enum of known features */
|
||||
|
||||
export enum AdminPanelFeature {
|
||||
AccountSearch = 'AccountSearch',
|
||||
AccountHistory = 'AccountHistory',
|
||||
|
@ -32,6 +33,7 @@ export enum AdminPanelFeature {
|
|||
RelyingParties = 'RelyingParties',
|
||||
RelyingPartiesEditNotes = 'RelyingPartiesEditNotes',
|
||||
SendPasswordResetEmail = 'SendPasswordResetEmail',
|
||||
UnsubscribeFromMailingLists = 'UnsubscribeFromMailingLists',
|
||||
}
|
||||
|
||||
/** Enum of known user groups */
|
||||
|
@ -138,6 +140,10 @@ const defaultAdminPanelPermissions: Permissions = {
|
|||
name: 'Send Password Reset Email',
|
||||
level: PermissionLevel.Support,
|
||||
},
|
||||
[AdminPanelFeature.UnsubscribeFromMailingLists]: {
|
||||
name: 'Unsubscribe User From Mozilla Mailing Lists',
|
||||
level: PermissionLevel.Support,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
Загрузка…
Ссылка в новой задаче