зеркало из https://github.com/mozilla/fxa.git
chore(repo): delete support panel from fxa
Because: - the support panel has been replaced by features in the admin panel This commit: - deletes the support panel package
This commit is contained in:
Родитель
21fcfb61cf
Коммит
2b2fada774
|
@ -23,5 +23,4 @@ yarn workspaces foreach \
|
|||
--exclude=fxa-graphql-api \
|
||||
--exclude=fxa-payments-server \
|
||||
--exclude=fxa-settings \
|
||||
--exclude=fxa-support-panel \
|
||||
run lint;
|
||||
|
|
|
@ -50,7 +50,6 @@ npx yarn workspaces focus --production \
|
|||
fxa-profile-server \
|
||||
fxa-react \
|
||||
fxa-settings \
|
||||
fxa-shared \
|
||||
fxa-support-panel
|
||||
fxa-shared
|
||||
npx yarn cache clean --all
|
||||
rm -rf artifacts
|
||||
|
|
|
@ -20,7 +20,7 @@ echo "checking for affected services..."
|
|||
ORIGINAL_IFS=$IFS
|
||||
IFS=$'\n'
|
||||
# We don't need to worry about front-end services that use TS, since they must build successfully for CI to pass.
|
||||
BACKEND_PACKAGES=( "fxa-admin-server" "fxa-auth-server" "fxa-event-broker" "fxa-graphql-api" "fxa-shared" "fxa-auth-client" "fxa-support-panel" )
|
||||
BACKEND_PACKAGES=( "fxa-admin-server" "fxa-auth-server" "fxa-event-broker" "fxa-graphql-api" "fxa-shared" "fxa-auth-client" )
|
||||
INCLUDE_ARGS=''
|
||||
AFFECTED_PACKAGES=''
|
||||
for package_modified in $PACKAGES_MODIFIED; do
|
||||
|
|
|
@ -1942,14 +1942,6 @@ const convictConf = convict({
|
|||
env: 'OTP_SIGNUP_DIGIT',
|
||||
},
|
||||
},
|
||||
supportPanel: {
|
||||
secretBearerToken: {
|
||||
default: 'YOU MUST CHANGE ME',
|
||||
doc: 'Shared secret to access certain endpoints. Please only use for GET. No state mutation allowed!',
|
||||
env: 'SUPPORT_PANEL_AUTH_SECRET_BEARER_TOKEN',
|
||||
format: 'String',
|
||||
},
|
||||
},
|
||||
syncTokenserverUrl: {
|
||||
default: 'http://localhost:5000/token',
|
||||
doc: 'The url of the Firefox Sync tokenserver',
|
||||
|
@ -2122,7 +2114,6 @@ if (convictConf.get('isProduction')) {
|
|||
'oauth.jwtSecretKeys',
|
||||
'oauth.secretKey',
|
||||
'profileServer.secretBearerToken',
|
||||
'supportPanel.secretBearerToken',
|
||||
];
|
||||
for (const key of SECRET_SETTINGS) {
|
||||
if (convictConf.get(key) === convictConf.default(key)) {
|
||||
|
|
|
@ -588,11 +588,11 @@ module.exports = (
|
|||
) {
|
||||
throw new error.featureNotEnabled();
|
||||
}
|
||||
|
||||
|
||||
// If this request is using a session token we bump the last access time
|
||||
if (credentials.id) {
|
||||
credentials.lastAccessTime = Date.now();
|
||||
await db.touchSessionToken(credentials, {} , true);
|
||||
await db.touchSessionToken(credentials, {}, true);
|
||||
}
|
||||
|
||||
const deviceArray = await request.app.devices;
|
||||
|
@ -795,53 +795,5 @@ module.exports = (
|
|||
return {};
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/account/sessions/locations',
|
||||
options: {
|
||||
...DEVICES_AND_SERVICES_DOCS.ACCOUNT_SESSIONS_LOCATIONS_GET,
|
||||
auth: {
|
||||
payload: false,
|
||||
strategy: 'supportPanelSecret',
|
||||
},
|
||||
validate: {
|
||||
query: {
|
||||
uid: isA.string().required(),
|
||||
},
|
||||
},
|
||||
response: {
|
||||
schema: isA.array().items(
|
||||
isA.object({
|
||||
city: isA.string().required().allow(null),
|
||||
state: isA.string().required().allow(null),
|
||||
stateCode: isA.string().required().allow(null),
|
||||
country: isA.string().required().allow(null),
|
||||
countryCode: isA.string().required().allow(null),
|
||||
lastAccessTime: isA.number().required(),
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
handler: async function (request) {
|
||||
log.begin('Account.sessionsLocations', request);
|
||||
const { uid } = request.query;
|
||||
|
||||
try {
|
||||
const tokenMetaData = await redis.getSessionTokens(uid);
|
||||
return Object.entries(tokenMetaData)
|
||||
.filter(([_, v]) => v.location)
|
||||
.map(([_, v]) => ({
|
||||
...v.location,
|
||||
lastAccessTime: v.lastAccessTime,
|
||||
}));
|
||||
} catch (err) {
|
||||
log.error('Account.sessionsLocations', {
|
||||
uid: uid,
|
||||
error: err,
|
||||
});
|
||||
throw error.backendServiceFailure();
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
@ -172,12 +172,6 @@ module.exports = function (
|
|||
stripeHelper,
|
||||
zendeskClient
|
||||
);
|
||||
const supportPanel = require('./support-panel')({
|
||||
log,
|
||||
db,
|
||||
config,
|
||||
stripeHelper,
|
||||
});
|
||||
const newsletters = require('./newsletters')(log, db);
|
||||
const util = require('./util')(log, config, config.smtp.redirectDomain);
|
||||
|
||||
|
@ -205,7 +199,6 @@ module.exports = function (
|
|||
util,
|
||||
recoveryKey,
|
||||
subscriptions,
|
||||
supportPanel,
|
||||
newsletters,
|
||||
linkedAccounts
|
||||
);
|
||||
|
|
|
@ -1,109 +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 { ServerRoute } from '@hapi/hapi';
|
||||
import isA from 'joi';
|
||||
import { MozillaSubscriptionTypes } from 'fxa-shared/subscriptions/types';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
import { ConfigType } from '../../config';
|
||||
import SUBSCRIPTIONS_DOCS from '../../docs/swagger/subscriptions-api';
|
||||
import { PlaySubscriptions } from '../../lib/payments/iap/google-play/subscriptions';
|
||||
import { AppStoreSubscriptions } from '../../lib/payments/iap/apple-app-store/subscriptions';
|
||||
import {
|
||||
playStoreSubscriptionPurchaseToPlayStoreSubscriptionDTO,
|
||||
appStoreSubscriptionPurchaseToAppStoreSubscriptionDTO,
|
||||
} from '../payments/iap/iap-formatter';
|
||||
import { StripeHelper } from '../payments/stripe';
|
||||
import { AuthLogger, AuthRequest } from '../types';
|
||||
import validators from './validators';
|
||||
|
||||
export class SupportPanelHandler {
|
||||
constructor(
|
||||
protected log: AuthLogger,
|
||||
protected stripeHelper: StripeHelper,
|
||||
protected playSubscriptions: PlaySubscriptions,
|
||||
protected appStoreSubscriptions: AppStoreSubscriptions
|
||||
) {}
|
||||
|
||||
async getSubscriptions(request: AuthRequest) {
|
||||
this.log.begin('supportPanelGetSubscriptions', request);
|
||||
const { uid } = request.query as Record<string, string>;
|
||||
const iapPlaySubscriptions = (
|
||||
await this.playSubscriptions.getSubscriptions(uid)
|
||||
).map(playStoreSubscriptionPurchaseToPlayStoreSubscriptionDTO);
|
||||
const iapAppStoreSubscriptions = (
|
||||
await this.appStoreSubscriptions.getSubscriptions(uid)
|
||||
).map(appStoreSubscriptionPurchaseToAppStoreSubscriptionDTO);
|
||||
const customer = await this.stripeHelper.fetchCustomer(uid);
|
||||
const webSubscriptions = customer?.subscriptions;
|
||||
const formattedWebSubscriptions = webSubscriptions
|
||||
? await this.stripeHelper.formatSubscriptionsForSupport(webSubscriptions)
|
||||
: [];
|
||||
|
||||
return {
|
||||
[MozillaSubscriptionTypes.WEB]: formattedWebSubscriptions,
|
||||
[MozillaSubscriptionTypes.IAP_GOOGLE]: iapPlaySubscriptions,
|
||||
[MozillaSubscriptionTypes.IAP_APPLE]: iapAppStoreSubscriptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const supportPanelRoutes = ({
|
||||
log,
|
||||
config,
|
||||
stripeHelper,
|
||||
playSubscriptions,
|
||||
appStoreSubscriptions,
|
||||
}: {
|
||||
log: AuthLogger;
|
||||
config: ConfigType;
|
||||
stripeHelper: StripeHelper;
|
||||
playSubscriptions?: PlaySubscriptions;
|
||||
appStoreSubscriptions?: AppStoreSubscriptions;
|
||||
}): ServerRoute[] => {
|
||||
if (!config.subscriptions.enabled || !stripeHelper) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!playSubscriptions) {
|
||||
playSubscriptions = Container.get(PlaySubscriptions);
|
||||
}
|
||||
|
||||
if (!appStoreSubscriptions) {
|
||||
appStoreSubscriptions = Container.get(AppStoreSubscriptions);
|
||||
}
|
||||
|
||||
const supportPanelHandler = new SupportPanelHandler(
|
||||
log,
|
||||
stripeHelper,
|
||||
playSubscriptions,
|
||||
appStoreSubscriptions
|
||||
);
|
||||
return [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/oauth/support-panel/subscriptions',
|
||||
options: {
|
||||
...SUBSCRIPTIONS_DOCS.OAUTH_SUPPORTPANEL_SUBSCRIPTIONS_GET,
|
||||
auth: {
|
||||
payload: false,
|
||||
strategy: 'supportPanelSecret',
|
||||
},
|
||||
response: {
|
||||
schema: validators.subscriptionsSubscriptionSupportValidator as any,
|
||||
},
|
||||
validate: {
|
||||
query: {
|
||||
uid: isA.string().required(),
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: (request: AuthRequest) =>
|
||||
supportPanelHandler.getSubscriptions(request),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
module.exports = supportPanelRoutes;
|
||||
module.exports.supportPanelRoutes = supportPanelRoutes;
|
|
@ -422,12 +422,6 @@ async function create(log, error, config, routes, db, statsd) {
|
|||
);
|
||||
server.auth.strategy('subscriptionsSecret', 'subscriptionsSecret');
|
||||
|
||||
server.auth.scheme(
|
||||
'supportPanelSecret',
|
||||
sharedSecretAuth.strategy(`Bearer ${config.supportPanel.secretBearerToken}`)
|
||||
);
|
||||
server.auth.strategy('supportPanelSecret', 'supportPanelSecret');
|
||||
|
||||
server.auth.scheme(
|
||||
'supportSecret',
|
||||
sharedSecretAuth.strategy(`Bearer ${config.support.secretBearerToken}`, {
|
||||
|
|
|
@ -43,10 +43,6 @@ describe('Config', () => {
|
|||
'PROFILE_SERVER_AUTH_SECRET_BEARER_TOKEN',
|
||||
'production secret here'
|
||||
);
|
||||
mockEnv(
|
||||
'SUPPORT_PANEL_AUTH_SECRET_BEARER_TOKEN',
|
||||
'production secret here'
|
||||
);
|
||||
assert.doesNotThrow(() => {
|
||||
proxyquire(`${ROOT_DIR}/config`, {});
|
||||
});
|
||||
|
|
|
@ -2065,67 +2065,3 @@ describe('/account/sessions', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /account/sessions/locations', () => {
|
||||
let redis;
|
||||
const uid = 'de2437db3aee4b3b9830133f0f71bbec';
|
||||
let mockRequest, mockLog, accountRoutes, route;
|
||||
const tokenMetadata = Array(2).fill({
|
||||
location: {
|
||||
city: 'Heapolandia',
|
||||
state: 'Memory Palace',
|
||||
stateCode: 'MP',
|
||||
country: 'United Devices of von Neumann',
|
||||
countryCode: 'UVN',
|
||||
},
|
||||
lastAccessTime: Date.now(),
|
||||
});
|
||||
tokenMetadata.push({ nolocation: 'fact' });
|
||||
|
||||
beforeEach(() => {
|
||||
redis = {};
|
||||
mockRequest = mocks.mockRequest({
|
||||
query: { uid },
|
||||
auth: { strategy: 'supportPanelSecret' },
|
||||
});
|
||||
mockLog = mocks.mockLog();
|
||||
accountRoutes = makeRoutes({
|
||||
config: {},
|
||||
devices: {},
|
||||
log: mockLog,
|
||||
redis,
|
||||
});
|
||||
route = getRoute(accountRoutes, '/account/sessions/locations');
|
||||
});
|
||||
|
||||
it('should return a list of locations from redis', () => {
|
||||
redis.getSessionTokens = sinon.fake.returns(tokenMetadata);
|
||||
return runTest(route, mockRequest, (response) => {
|
||||
assert.isTrue(redis.getSessionTokens.calledOnceWith(uid));
|
||||
assert.deepEqual(
|
||||
response,
|
||||
tokenMetadata
|
||||
.filter((x) => x.location)
|
||||
.map((x) => ({ ...x.location, lastAccessTime: x.lastAccessTime }))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw and log an error if fetching locations from redis failed', () => {
|
||||
redis.getSessionTokens = sinon.fake.throws(error.backendServiceFailure());
|
||||
return runTest(
|
||||
route,
|
||||
mockRequest,
|
||||
() => {
|
||||
assert.fail('error expected');
|
||||
},
|
||||
(err) => {
|
||||
mockLog.error.calledOnceWith('Account.sessionsLocations', {
|
||||
uid,
|
||||
error: err,
|
||||
});
|
||||
assert.deepEqual(err, error.backendServiceFailure());
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,190 +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 strict';
|
||||
|
||||
const chai = require('chai');
|
||||
const mocks = require('../../mocks');
|
||||
const sinon = require('sinon');
|
||||
const uuid = require('uuid');
|
||||
const { getRoute } = require('../../routes_helpers');
|
||||
const { supportPanelRoutes } = require('../../../lib/routes/support-panel');
|
||||
const { MozillaSubscriptionTypes } = require('fxa-shared/subscriptions/types');
|
||||
const { OAUTH_SCOPE_SUBSCRIPTIONS } = require('fxa-shared/oauth/constants');
|
||||
|
||||
const assert = { ...sinon.assert, ...chai.assert };
|
||||
const sandbox = sinon.createSandbox();
|
||||
const UID = uuid.v4({}, Buffer.alloc(16)).toString('hex');
|
||||
const TEST_EMAIL = 'testo@example.gg';
|
||||
const ACCOUNT_LOCALE = 'en-US';
|
||||
const MOCK_SCOPES = ['profile:email', OAUTH_SCOPE_SUBSCRIPTIONS];
|
||||
const VALID_REQUEST = {
|
||||
auth: {
|
||||
credentials: {
|
||||
scope: MOCK_SCOPES,
|
||||
user: `${UID}`,
|
||||
email: `${TEST_EMAIL}`,
|
||||
},
|
||||
},
|
||||
query: {
|
||||
uid: `${UID}`,
|
||||
},
|
||||
};
|
||||
const mockConfig = {
|
||||
subscriptions: { enabled: true },
|
||||
};
|
||||
const mockCustomer = { id: 'cus_testo', subscriptions: { data: {} } };
|
||||
const mockFormattedWebSubscription = {
|
||||
created: 1588972390,
|
||||
current_period_end: 1591650790,
|
||||
current_period_start: 1588972390,
|
||||
plan_changed: null,
|
||||
previous_product: null,
|
||||
product_name: 'Amazing Product',
|
||||
status: 'active',
|
||||
subscription_id: 'sub_12345',
|
||||
};
|
||||
const mockPlayStoreSubscriptionPurchase = {
|
||||
kind: 'androidpublisher#subscriptionPurchase',
|
||||
startTimeMillis: `${Date.now() - 10000}`,
|
||||
expiryTimeMillis: `${Date.now() + 10000}`,
|
||||
autoRenewing: true,
|
||||
priceCurrencyCode: 'JPY',
|
||||
priceAmountMicros: '99000000',
|
||||
countryCode: 'JP',
|
||||
developerPayload: '',
|
||||
paymentState: 1,
|
||||
orderId: 'GPA.3313-5503-3858-32549',
|
||||
packageName: 'testPackage',
|
||||
purchaseToken: 'testToken',
|
||||
sku: 'sku',
|
||||
verifiedAt: Date.now(),
|
||||
isEntitlementActive: sinon.fake.returns(true),
|
||||
};
|
||||
|
||||
const mockAppStoreSubscriptionPurchase = {
|
||||
productId: 'wow',
|
||||
autoRenewing: false,
|
||||
bundleId: 'hmm',
|
||||
isEntitlementActive: sinon.fake.returns(true),
|
||||
};
|
||||
|
||||
const mockExtraStripeInfo = {
|
||||
price_id: 'price_lol',
|
||||
product_id: 'prod_lol',
|
||||
product_name: 'LOL Product',
|
||||
};
|
||||
|
||||
const mockAppendedPlayStoreSubscriptionPurchase = {
|
||||
...mockPlayStoreSubscriptionPurchase,
|
||||
...mockExtraStripeInfo,
|
||||
_subscription_type: MozillaSubscriptionTypes.IAP_GOOGLE,
|
||||
};
|
||||
|
||||
const mockAppendedAppStoreSubscriptionPurchase = {
|
||||
...mockAppStoreSubscriptionPurchase,
|
||||
...mockExtraStripeInfo,
|
||||
_subscription_type: MozillaSubscriptionTypes.IAP_APPLE,
|
||||
};
|
||||
|
||||
const mockFormattedPlayStoreSubscription = {
|
||||
auto_renewing: mockPlayStoreSubscriptionPurchase.autoRenewing,
|
||||
expiry_time_millis: mockPlayStoreSubscriptionPurchase.expiryTimeMillis,
|
||||
package_name: mockPlayStoreSubscriptionPurchase.packageName,
|
||||
sku: mockPlayStoreSubscriptionPurchase.sku,
|
||||
...mockExtraStripeInfo,
|
||||
_subscription_type: MozillaSubscriptionTypes.IAP_GOOGLE,
|
||||
};
|
||||
|
||||
const mockFormattedAppStoreSubscription = {
|
||||
app_store_product_id: mockAppStoreSubscriptionPurchase.productId,
|
||||
auto_renewing: mockAppStoreSubscriptionPurchase.autoRenewing,
|
||||
bundle_id: mockAppStoreSubscriptionPurchase.bundleId,
|
||||
...mockExtraStripeInfo,
|
||||
_subscription_type: MozillaSubscriptionTypes.IAP_APPLE,
|
||||
};
|
||||
|
||||
const log = mocks.mockLog();
|
||||
const db = mocks.mockDB({
|
||||
uid: UID,
|
||||
email: TEST_EMAIL,
|
||||
locale: ACCOUNT_LOCALE,
|
||||
});
|
||||
let stripeHelper;
|
||||
|
||||
describe('support-panel', () => {
|
||||
beforeEach(() => {
|
||||
stripeHelper = {
|
||||
addPriceInfoToIapPurchases: sandbox
|
||||
.stub()
|
||||
.resolves([
|
||||
mockAppendedPlayStoreSubscriptionPurchase,
|
||||
mockAppendedAppStoreSubscriptionPurchase,
|
||||
]),
|
||||
fetchCustomer: sandbox.stub().resolves(mockCustomer),
|
||||
formatSubscriptionsForSupport: sandbox
|
||||
.stub()
|
||||
.resolves([mockFormattedWebSubscription]),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.reset();
|
||||
});
|
||||
|
||||
describe('GET /oauth/support-panel/subscriptions', () => {
|
||||
it('returns an empty list of routes when subscriptions is disabled', () => {
|
||||
const routes = supportPanelRoutes({
|
||||
log,
|
||||
db,
|
||||
config: { subscriptions: { enabled: false } },
|
||||
stripeHelper,
|
||||
});
|
||||
assert.deepEqual(routes, []);
|
||||
});
|
||||
|
||||
it('gets the expected subscriptions', async () => {
|
||||
const playSubscriptions = {
|
||||
getSubscriptions: sandbox
|
||||
.stub()
|
||||
.resolves([mockAppendedPlayStoreSubscriptionPurchase]),
|
||||
};
|
||||
const appStoreSubscriptions = {
|
||||
getSubscriptions: sandbox
|
||||
.stub()
|
||||
.resolves([mockAppendedAppStoreSubscriptionPurchase]),
|
||||
};
|
||||
const routes = supportPanelRoutes({
|
||||
log,
|
||||
db,
|
||||
config: mockConfig,
|
||||
stripeHelper,
|
||||
playSubscriptions,
|
||||
appStoreSubscriptions,
|
||||
});
|
||||
const route = getRoute(
|
||||
routes,
|
||||
'/oauth/support-panel/subscriptions',
|
||||
'GET'
|
||||
);
|
||||
const request = mocks.mockRequest(VALID_REQUEST);
|
||||
const resp = await route.handler(request);
|
||||
|
||||
assert.deepEqual(resp, {
|
||||
[MozillaSubscriptionTypes.WEB]: [mockFormattedWebSubscription],
|
||||
[MozillaSubscriptionTypes.IAP_GOOGLE]: [
|
||||
mockFormattedPlayStoreSubscription,
|
||||
],
|
||||
[MozillaSubscriptionTypes.IAP_APPLE]: [
|
||||
mockFormattedAppStoreSubscription,
|
||||
],
|
||||
});
|
||||
assert.calledOnceWithExactly(stripeHelper.fetchCustomer, UID);
|
||||
assert.calledOnceWithExactly(
|
||||
stripeHelper.formatSubscriptionsForSupport,
|
||||
mockCustomer.subscriptions
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"comment_1179": "1179 is prototype pollution in minimist, used by tslint, mocha, handlebars. Doesn't affect us, as this library is only used by support agents, so untrusted external inputs aren't passed to handlebars.",
|
||||
"comment_1179": "1179 is prototype pollution in yargs-parser, used by convict, mocha. Doesn't affect us, as this library is only used by support agents, so untrusted external inputs aren't passed in via CLI.",
|
||||
"exceptions": [
|
||||
"https://npmjs.com/advisories/1179",
|
||||
"https://npmjs.com/advisories/1500"
|
||||
]
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible Node.js debug attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Mocha All",
|
||||
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
|
||||
"args": [
|
||||
"--timeout",
|
||||
"999999",
|
||||
"--colors",
|
||||
"-r",
|
||||
"esbuild-register",
|
||||
"${workspaceFolder}/test/**/*.spec.ts",
|
||||
"${workspaceFolder}/test/**/**/*.spec.ts"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Mocha Current File",
|
||||
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
|
||||
"args": [
|
||||
"--timeout",
|
||||
"999999",
|
||||
"--colors",
|
||||
"-r",
|
||||
"esbuild-register",
|
||||
"${workspaceFolder}/${relativeFile}"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "tsc-watch",
|
||||
"command": "npm",
|
||||
"args": ["run", "watch"],
|
||||
"type": "shell",
|
||||
"isBackground": true,
|
||||
"group": "build",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"dependsOn": "Stop Support Panel Server"
|
||||
},
|
||||
{
|
||||
"label": "Run Current Test",
|
||||
"type": "shell",
|
||||
"command": "./node_modules/mocha/bin/mocha.js",
|
||||
"args": ["-r", "esbuild-register", "${relativeFile}"],
|
||||
"group": "test",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Stop Support Panel Server",
|
||||
"type": "shell",
|
||||
"command": "pm2 stop 'support admin panel PORT 7100'"
|
||||
},
|
||||
{
|
||||
"label": "Start Support Panel Server",
|
||||
"type": "shell",
|
||||
"command": "pm2 start 'support admin panel PORT 7100'"
|
||||
}
|
||||
]
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,44 +0,0 @@
|
|||
# fxa-support-panel
|
||||
|
||||
The Firefox Accounts Support Panel is a small web service that is intended for use as
|
||||
an embedded iframe in the Zendesk Support UX. It's intended to show Subscription Support
|
||||
Agents relevant data about a users Firefox Account so that they may assist the users
|
||||
support request.
|
||||
|
||||
## Software Architecture
|
||||
|
||||
The primary source of truth on FxA user data is the fxa-auth-server. That service is
|
||||
configured to use OAuth for user and service access to account data, and has write access
|
||||
to the user data. Since this view is intended purely for read-only access to user data for
|
||||
assisting a support agent, this service instead uses read-restricted mysql credentials.
|
||||
|
||||
Read-only access is enforced on the database by using a MySQL user restricted to the stored
|
||||
procedures needed to run the queries that fetch basic profile information.
|
||||
|
||||
## Local Development
|
||||
|
||||
In order to make API calls, the support panel needs a shared secret bearer token with the auth server. This can be done by setting the corresponding environment variables for each server to match to the same string value:
|
||||
|
||||
- fxa-support-panel: `AUTH_SECRET_BEARER_TOKEN`
|
||||
- fxa-auth-server: `SUPPORT_PANEL_AUTH_SECRET_BEARER_TOKEN`
|
||||
|
||||
Note: the default config for each server should already have this set up.
|
||||
|
||||
The support panel can be viewed locally by going to `http://localhost:${port}/?uid=${uid}`, where:
|
||||
|
||||
- `port` is the port as defined in `./pm2.config.js`
|
||||
- `uid` is a local FxA user ID
|
||||
|
||||
## Testing
|
||||
|
||||
This package uses [Jest](https://mochajs.org/) to test its code. By default `yarn test` will test all files ending in `.spec.ts`.
|
||||
|
||||
Test commands:
|
||||
|
||||
```bash
|
||||
# Test with coverage
|
||||
yarn test:cov
|
||||
|
||||
# Test on file change
|
||||
yarn test:watch
|
||||
```
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src"
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
{
|
||||
"name": "fxa-support-panel",
|
||||
"version": "1.252.4",
|
||||
"description": "Small app to help customer support access FxA details",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "nest build",
|
||||
"compile": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"audit": "npm audit --json | audit-filter --nsp-config=.nsprc --audit=-",
|
||||
"watch": "tsc -w",
|
||||
"start": "pm2 start pm2.config.js",
|
||||
"stop": "pm2 stop pm2.config.js",
|
||||
"restart": "pm2 restart pm2.config.js",
|
||||
"delete": "pm2 delete pm2.config.js",
|
||||
"test": "jest --runInBand --logHeapUsage && yarn test:e2e",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r esbuild-register node_modules/.bin/jest --runInBand --logHeapUsage",
|
||||
"test:e2e": "jest --runInBand --config ./test/jest-e2e.json",
|
||||
"test:unit": "echo No unit tests present for $npm_package_name",
|
||||
"test:integration": "yarn test"
|
||||
},
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mozilla/fxa.git"
|
||||
},
|
||||
"bugs": "https://github.com/mozilla/fxa/issues/",
|
||||
"homepage": "https://github.com/mozilla/fxa/",
|
||||
"license": "MPL-2.0",
|
||||
"author": "Mozilla (https://mozilla.org/)",
|
||||
"readmeFilename": "README.md",
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^9.1.2",
|
||||
"@nestjs/config": "^2.3.1",
|
||||
"@nestjs/core": "^9.2.0",
|
||||
"@nestjs/mapped-types": "^1.2.0",
|
||||
"@nestjs/platform-express": "^9.2.0",
|
||||
"@sentry/integrations": "^6.19.1",
|
||||
"@sentry/node": "^6.19.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"convict": "^6.2.4",
|
||||
"convict-format-with-moment": "^6.2.0",
|
||||
"convict-format-with-validator": "^6.2.0",
|
||||
"express": "^4.17.3",
|
||||
"fxa-shared": "workspace:*",
|
||||
"handlebars": "^4.7.7",
|
||||
"hbs": "^4.2.0",
|
||||
"helmet": "^6.0.0",
|
||||
"hot-shots": "^10.0.0",
|
||||
"mozlog": "^3.0.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.8.0",
|
||||
"semver": "^7.3.5",
|
||||
"superagent": "^8.0.0",
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.1.3",
|
||||
"@types/convict": "^5.2.2",
|
||||
"@types/eslint": "7.2.13",
|
||||
"@types/hbs": "^4",
|
||||
"@types/node": "^16.11.3",
|
||||
"@types/superagent": "4.1.11",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"audit-filter": "^0.5.0",
|
||||
"esbuild": "^0.14.2",
|
||||
"esbuild-register": "^3.2.0",
|
||||
"eslint": "^7.32.0",
|
||||
"jest": "27.5.1",
|
||||
"pm2": "^5.2.2",
|
||||
"prettier": "^2.3.1",
|
||||
"supertest": "^6.3.0",
|
||||
"ts-jest": "^29.0.0",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": [
|
||||
"ts-jest",
|
||||
{
|
||||
"isolatedModules": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"coverageDirectory": "./coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
|
@ -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/. */
|
||||
|
||||
const PATH = process.env.PATH.split(':')
|
||||
.filter((p) => !p.includes(process.env.TMPDIR))
|
||||
.join(':');
|
||||
|
||||
const nest = require.resolve('@nestjs/cli/bin/nest.js');
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'support',
|
||||
cwd: __dirname,
|
||||
script: `${nest} start --debug=9190 --watch`,
|
||||
max_restarts: '1',
|
||||
min_uptime: '2m',
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
TS_NODE_TRANSPILE_ONLY: 'true',
|
||||
TS_NODE_FILES: 'true',
|
||||
PORT: '7100',
|
||||
PATH,
|
||||
SENTRY_ENV: 'local',
|
||||
SENTRY_DSN: process.env.SENTRY_DSN_SUPPPORT_PANEL,
|
||||
TRACING_SERVICE_NAME: 'fxa-support-panel',
|
||||
},
|
||||
filter_env: ['npm_'],
|
||||
watch: ['src'],
|
||||
time: true,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -1,51 +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 { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { HealthModule } from 'fxa-shared/nestjs/health/health.module';
|
||||
import { LoggerModule } from 'fxa-shared/nestjs/logger/logger.module';
|
||||
import { SentryModule } from 'fxa-shared/nestjs/sentry/sentry.module';
|
||||
import { getVersionInfo } from 'fxa-shared/nestjs/version';
|
||||
|
||||
import { AppController } from './app/app.controller';
|
||||
import Config, { AppConfig } from './config';
|
||||
import { RemoteLookupService } from './remote-lookup/remote-lookup.service';
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
import { DatabaseService } from './database/database.service';
|
||||
import { MetricsFactory } from 'fxa-shared/nestjs/metrics.service';
|
||||
import { MozLoggerService } from 'fxa-shared/nestjs/logger/logger.service';
|
||||
|
||||
const version = getVersionInfo(__dirname);
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
load: [(): AppConfig => Config.getProperties()],
|
||||
isGlobal: true,
|
||||
}),
|
||||
DatabaseModule,
|
||||
HealthModule.forRootAsync({
|
||||
imports: [DatabaseModule],
|
||||
inject: [DatabaseService],
|
||||
useFactory: async (db: DatabaseService) => ({
|
||||
version,
|
||||
extraHealthData: () => db.dbHealthCheck(),
|
||||
}),
|
||||
}),
|
||||
LoggerModule,
|
||||
SentryModule.forRootAsync({
|
||||
imports: [ConfigModule, LoggerModule],
|
||||
inject: [ConfigService, MozLoggerService],
|
||||
useFactory: (configService: ConfigService<AppConfig>) => ({
|
||||
sentryConfig: {
|
||||
sentry: configService.get('sentry'),
|
||||
version: version.version,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [RemoteLookupService, MetricsFactory],
|
||||
})
|
||||
export class AppModule {}
|
|
@ -1,14 +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 { IsHexadecimal, IsInt, IsOptional, Length } from 'class-validator';
|
||||
|
||||
export class AccountQuery {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
requestTicket?: number;
|
||||
|
||||
@IsHexadecimal()
|
||||
@Length(32, 32)
|
||||
uid!: string;
|
||||
}
|
|
@ -1,99 +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 { ConfigService } from '@nestjs/config';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Account, Device, TotpToken } from 'fxa-shared/db/models/auth';
|
||||
import { MozLoggerService } from 'fxa-shared/nestjs/logger/logger.service';
|
||||
import { MozillaSubscriptionTypes } from 'fxa-shared/subscriptions/types';
|
||||
|
||||
import { RemoteLookupService } from '../remote-lookup/remote-lookup.service';
|
||||
import {
|
||||
formattedSigninLocations,
|
||||
formattedSubscriptions,
|
||||
MOCKDATA,
|
||||
} from '../remote-lookup/remote-lookup.service.spec';
|
||||
import { AppController } from './app.controller';
|
||||
|
||||
describe('AppController', () => {
|
||||
let controller: AppController;
|
||||
let mockRemote: Partial<RemoteLookupService>;
|
||||
let mockLog: Partial<MozLoggerService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfig = jest.fn().mockReturnValue({});
|
||||
mockRemote = {};
|
||||
mockLog = {
|
||||
info: jest.fn(),
|
||||
};
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: RemoteLookupService,
|
||||
useValue: mockRemote,
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: { get: mockConfig },
|
||||
},
|
||||
{
|
||||
provide: MozLoggerService,
|
||||
useValue: mockLog,
|
||||
},
|
||||
],
|
||||
controllers: [AppController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('index', () => {
|
||||
it('returns successfully', async () => {
|
||||
(jest.spyOn(Account, 'findByUid') as jest.Mock).mockReturnValue(
|
||||
MOCKDATA.account
|
||||
);
|
||||
(jest.spyOn(Device, 'findByUid') as jest.Mock).mockReturnValue(
|
||||
MOCKDATA.devices
|
||||
);
|
||||
(jest.spyOn(TotpToken, 'findByUid') as jest.Mock).mockReturnValue(
|
||||
MOCKDATA.totp
|
||||
);
|
||||
mockRemote.subscriptions = jest
|
||||
.fn()
|
||||
.mockResolvedValue(formattedSubscriptions);
|
||||
mockRemote.signinLocations = jest
|
||||
.fn()
|
||||
.mockResolvedValue(formattedSigninLocations);
|
||||
|
||||
expect(
|
||||
await controller.root('testuser', { uid: 'testuid' })
|
||||
).toStrictEqual({
|
||||
created: String(new Date(MOCKDATA.account.createdAt)),
|
||||
devices: [
|
||||
{
|
||||
created: String(new Date(MOCKDATA.devices[0].createdAt)),
|
||||
name: 'desktop',
|
||||
type: 'browser',
|
||||
},
|
||||
],
|
||||
email: MOCKDATA.account.email,
|
||||
emailVerified: !!MOCKDATA.account.emailVerified,
|
||||
locale: MOCKDATA.account.locale,
|
||||
signinLocations: formattedSigninLocations,
|
||||
subscriptionStatus: true,
|
||||
webSubscriptions: formattedSubscriptions[MozillaSubscriptionTypes.WEB],
|
||||
playSubscriptions:
|
||||
formattedSubscriptions[MozillaSubscriptionTypes.IAP_GOOGLE],
|
||||
appSubscriptions:
|
||||
formattedSubscriptions[MozillaSubscriptionTypes.IAP_APPLE],
|
||||
twoFactorAuth: true,
|
||||
uid: 'testuid',
|
||||
});
|
||||
expect(mockLog.info).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,70 +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 {
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
Render,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { MozLoggerService } from 'fxa-shared/nestjs/logger/logger.service';
|
||||
import { Account, Device, TotpToken } from 'fxa-shared/db/models/auth';
|
||||
|
||||
import { CurrentUser } from '../auth/auth-header.decorator';
|
||||
import { AuthHeaderGuard } from '../auth/auth-header.guard';
|
||||
import { RemoteLookupService } from '../remote-lookup/remote-lookup.service';
|
||||
import { AccountQuery } from './account-query.dto';
|
||||
import { SubscriptionResponse } from '../remote-lookup/remote-responses.dto';
|
||||
import { MozillaSubscriptionTypes } from 'fxa-shared/subscriptions/types';
|
||||
|
||||
@Controller()
|
||||
@UseGuards(AuthHeaderGuard)
|
||||
export class AppController {
|
||||
constructor(
|
||||
private log: MozLoggerService,
|
||||
private remote: RemoteLookupService
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@UsePipes(new ValidationPipe())
|
||||
@Render('index')
|
||||
async root(@CurrentUser() user: string, @Query() query: AccountQuery) {
|
||||
const uid = query.uid;
|
||||
const requestTicket = query.requestTicket || 'ticket-unknown';
|
||||
|
||||
// This is the user who is asking for the information:
|
||||
this.log.info('infoRequest', { authUser: user, requestTicket, uid });
|
||||
|
||||
const [account, devices, signinLocations, totp] = await Promise.all([
|
||||
Account.findByUid(uid),
|
||||
Device.findByUid(uid),
|
||||
this.remote.signinLocations(uid),
|
||||
TotpToken.findByUid(uid),
|
||||
]);
|
||||
const subscriptions = await this.remote.subscriptions(uid, account!.email);
|
||||
|
||||
return {
|
||||
created: String(new Date(account!.createdAt)),
|
||||
devices: devices.map((d) => ({
|
||||
created: String(new Date(d.createdAt!)),
|
||||
name: d.name,
|
||||
type: d.type,
|
||||
})),
|
||||
email: account!.email,
|
||||
emailVerified: account!.emailVerified,
|
||||
locale: account!.locale,
|
||||
signinLocations,
|
||||
subscriptionStatus: Object.keys(subscriptions).some(
|
||||
(k) => subscriptions[k as keyof SubscriptionResponse].length > 0
|
||||
),
|
||||
webSubscriptions: subscriptions[MozillaSubscriptionTypes.WEB],
|
||||
playSubscriptions: subscriptions[MozillaSubscriptionTypes.IAP_GOOGLE],
|
||||
appSubscriptions: subscriptions[MozillaSubscriptionTypes.IAP_APPLE],
|
||||
twoFactorAuth: !!totp,
|
||||
uid,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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 { ROUTE_ARGS_METADATA } from '@nestjs/common/constants';
|
||||
|
||||
import { CurrentUser } from './auth-header.decorator';
|
||||
|
||||
class Test {
|
||||
public test(@CurrentUser() user: string) {}
|
||||
}
|
||||
|
||||
describe('CurrentUser', () => {
|
||||
it('returns the user', () => {
|
||||
const args = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
|
||||
const factory = args[Object.keys(args)[0]].factory;
|
||||
const mockContext = {
|
||||
switchToHttp: jest.fn().mockReturnValue({
|
||||
getRequest: jest.fn().mockReturnValue({ user: 'testuser' }),
|
||||
}),
|
||||
};
|
||||
const result = factory(null, mockContext);
|
||||
expect(result).toBe('testuser');
|
||||
});
|
||||
});
|
|
@ -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 { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: unknown, context: ExecutionContext) => {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
return req.user;
|
||||
}
|
||||
);
|
|
@ -1,52 +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 { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { AuthHeaderGuard } from './auth-header.guard';
|
||||
|
||||
describe('AuthHeaderGuard', () => {
|
||||
let guard: AuthHeaderGuard;
|
||||
let mockConfig: Partial<ConfigService>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig = {
|
||||
get: jest.fn().mockReturnValue({ authHeader: 'test' }),
|
||||
};
|
||||
guard = new AuthHeaderGuard(mockConfig as any);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(guard).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns true in dev', async () => {
|
||||
(guard as any).dev = true;
|
||||
expect(await guard.canActivate({} as any)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false with no username', async () => {
|
||||
const mockContext = {
|
||||
switchToHttp: jest.fn().mockReturnValue({
|
||||
getRequest: jest.fn().mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(undefined),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
expect(await guard.canActivate(mockContext as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('sets the username on the request', async () => {
|
||||
const mockRequest = {
|
||||
user: null,
|
||||
get: jest.fn().mockReturnValue('testuser'),
|
||||
};
|
||||
const mockContext = {
|
||||
switchToHttp: jest.fn().mockReturnValue({
|
||||
getRequest: jest.fn().mockReturnValue(mockRequest),
|
||||
}),
|
||||
};
|
||||
expect(await guard.canActivate(mockContext as any)).toBe(true);
|
||||
expect(mockRequest.user).toBe('testuser');
|
||||
});
|
||||
});
|
|
@ -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 { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import Config, { AppConfig } from '../config';
|
||||
|
||||
@Injectable()
|
||||
export class AuthHeaderGuard implements CanActivate {
|
||||
private authHeader: string;
|
||||
private dev: boolean;
|
||||
|
||||
constructor(configService: ConfigService<AppConfig>) {
|
||||
this.authHeader = configService.get('authHeader') as string;
|
||||
this.dev = Config.get('env') === 'development';
|
||||
}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
if (this.dev) {
|
||||
return true;
|
||||
}
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const username = request.get(this.authHeader);
|
||||
if (username) {
|
||||
(request as any).user = username;
|
||||
}
|
||||
return !!username;
|
||||
}
|
||||
}
|
|
@ -1,176 +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 convict from 'convict';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { makeMySQLConfig } from 'fxa-shared/db/config';
|
||||
import { tracingConfig } from 'fxa-shared/tracing/config';
|
||||
|
||||
convict.addFormats(require('convict-format-with-moment'));
|
||||
convict.addFormats(require('convict-format-with-validator'));
|
||||
|
||||
const conf = convict({
|
||||
authHeader: {
|
||||
default: 'oidc-claim-id-token-email',
|
||||
doc: 'Authentication header that should be logged for the user',
|
||||
env: 'AUTH_HEADER',
|
||||
format: String,
|
||||
},
|
||||
authServer: {
|
||||
secretBearerToken: {
|
||||
default: 'YOU MUST CHANGE ME',
|
||||
doc: 'Shared secret for accessing certain auth server endpoints',
|
||||
env: 'AUTH_SECRET_BEARER_TOKEN',
|
||||
format: 'String',
|
||||
},
|
||||
signinLocationsSearchPath: {
|
||||
default: '/v1/account/sessions/locations',
|
||||
doc: 'Auth server path session token metadata locations',
|
||||
env: 'AUTH_SERVER_LOCATIONS_SEARCH_PATH',
|
||||
format: String,
|
||||
},
|
||||
subscriptionsSearchPath: {
|
||||
default: '/v1/oauth/support-panel/subscriptions',
|
||||
doc: 'Auth server path for subscriptions',
|
||||
env: 'AUTH_SERVER_SUBS_SEARCH_PATH',
|
||||
format: String,
|
||||
},
|
||||
url: {
|
||||
default: 'http://localhost:9000',
|
||||
doc: 'URL for auth server',
|
||||
env: 'AUTH_SERVER_URL',
|
||||
format: String,
|
||||
},
|
||||
},
|
||||
database: {
|
||||
mysql: {
|
||||
auth: makeMySQLConfig('AUTH', 'fxa'),
|
||||
},
|
||||
},
|
||||
env: {
|
||||
default: 'production',
|
||||
doc: 'The current node.js environment',
|
||||
env: 'NODE_ENV',
|
||||
format: ['development', 'test', 'stage', 'production'],
|
||||
},
|
||||
log: {
|
||||
app: { default: 'fxa-support-panel' },
|
||||
fmt: {
|
||||
default: 'heka',
|
||||
env: 'LOGGING_FORMAT',
|
||||
format: ['heka', 'pretty'],
|
||||
},
|
||||
level: {
|
||||
default: 'info',
|
||||
env: 'LOG_LEVEL',
|
||||
},
|
||||
},
|
||||
listen: {
|
||||
host: {
|
||||
default: '0.0.0.0',
|
||||
doc: 'The ip address the server should bind',
|
||||
env: 'IP_ADDRESS',
|
||||
format: String,
|
||||
},
|
||||
port: {
|
||||
default: 7100,
|
||||
doc: 'The port the server should bind',
|
||||
env: 'PORT',
|
||||
format: 'port',
|
||||
},
|
||||
publicUrl: {
|
||||
default: 'http://localhost:3031',
|
||||
env: 'PUBLIC_URL',
|
||||
format: 'url',
|
||||
},
|
||||
},
|
||||
metrics: {
|
||||
host: {
|
||||
default: '',
|
||||
doc: 'Metrics host to report to',
|
||||
env: 'METRIC_HOST',
|
||||
format: String,
|
||||
},
|
||||
port: {
|
||||
default: 8125,
|
||||
doc: 'Metric port to report to',
|
||||
env: 'METRIC_PORT',
|
||||
format: Number,
|
||||
},
|
||||
prefix: {
|
||||
default: 'fxa-support-panel.',
|
||||
doc: 'Metric prefix for statsD',
|
||||
env: 'METRIC_PREFIX',
|
||||
format: String,
|
||||
},
|
||||
telegraf: {
|
||||
default: true,
|
||||
doc: 'Whether to use telegraf formatted metrics',
|
||||
env: 'METRIC_USE_TELEGRAF',
|
||||
format: Boolean,
|
||||
},
|
||||
},
|
||||
sentry: {
|
||||
dsn: {
|
||||
default: '',
|
||||
doc: 'Sentry DSN for error and log reporting',
|
||||
env: 'SENTRY_DSN',
|
||||
format: String,
|
||||
},
|
||||
env: {
|
||||
doc: 'Environment name to report to sentry',
|
||||
default: 'local',
|
||||
format: ['local', 'ci', 'dev', 'stage', 'prod'],
|
||||
env: 'SENTRY_ENV',
|
||||
},
|
||||
sampleRate: {
|
||||
default: 1.0,
|
||||
doc: 'Rate at which errors are sampled.',
|
||||
env: 'SENTRY_SAMPLE_RATE',
|
||||
format: 'Number',
|
||||
},
|
||||
serverName: {
|
||||
doc: 'Name used by sentry to identify the server.',
|
||||
default: 'fxa-support-panel',
|
||||
format: 'String',
|
||||
env: 'SENTRY_SERVER_NAME',
|
||||
},
|
||||
},
|
||||
tracing: tracingConfig,
|
||||
hstsEnabled: {
|
||||
default: true,
|
||||
doc: 'Send a Strict-Transport-Security header',
|
||||
env: 'HSTS_ENABLED',
|
||||
format: Boolean,
|
||||
},
|
||||
hstsMaxAge: {
|
||||
default: 31536000, // a year
|
||||
doc: 'Max age of the STS directive in seconds',
|
||||
// Note: This format is a number because the value needs to be in seconds
|
||||
format: Number,
|
||||
},
|
||||
csp: {
|
||||
frameAncestors: {
|
||||
default: 'none',
|
||||
doc: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors',
|
||||
env: 'CSP_FRAME_ANCESTORS',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// handle configuration files. you can specify a CSV list of configuration
|
||||
// files to process, which will be overlayed in order, in the CONFIG_FILES
|
||||
// environment variable.
|
||||
|
||||
// Need to move two dirs up as we're in the compiled directory now
|
||||
const configDir = path.dirname(path.dirname(__dirname));
|
||||
let envConfig = path.join(configDir, 'config', `${conf.get('env')}.json`);
|
||||
envConfig = `${envConfig},${process.env.CONFIG_FILES || ''}`;
|
||||
const files = envConfig.split(',').filter(fs.existsSync);
|
||||
conf.loadFile(files);
|
||||
conf.validate({ allowed: 'strict' });
|
||||
const Config = conf;
|
||||
|
||||
export type AppConfig = ReturnType<typeof Config['getProperties']>;
|
||||
export default Config;
|
|
@ -1,9 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { DatabaseService } from './database.service';
|
||||
import { MetricsFactory } from 'fxa-shared/nestjs/metrics.service';
|
||||
|
||||
@Module({
|
||||
providers: [DatabaseService, MetricsFactory],
|
||||
exports: [DatabaseService],
|
||||
})
|
||||
export class DatabaseModule {}
|
|
@ -1,75 +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 { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { DatabaseService } from './database.service';
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Account } from 'fxa-shared/db/models/auth';
|
||||
import { MozLoggerService } from 'fxa-shared/nestjs/logger/logger.service';
|
||||
|
||||
describe('DatabaseService', () => {
|
||||
let service: DatabaseService;
|
||||
let logger: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dbConfig = {
|
||||
database: 'testAdmin',
|
||||
host: 'localhost',
|
||||
password: '',
|
||||
port: 3306,
|
||||
user: 'root',
|
||||
};
|
||||
const MockConfig: Provider = {
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
get: jest.fn().mockReturnValue({
|
||||
mysql: {
|
||||
auth: dbConfig,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
logger = { debug: jest.fn(), error: jest.fn(), info: jest.fn() };
|
||||
const MockMozLogger: Provider = {
|
||||
provide: MozLoggerService,
|
||||
useValue: logger,
|
||||
};
|
||||
const MockMetricsFactory: Provider = {
|
||||
provide: 'METRICS',
|
||||
useFactory: () => undefined,
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DatabaseService,
|
||||
MockConfig,
|
||||
MockMozLogger,
|
||||
MockMetricsFactory,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DatabaseService>(DatabaseService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns success', async () => {
|
||||
(jest.spyOn(Account, 'query') as jest.Mock).mockReturnValue({
|
||||
limit: jest.fn().mockResolvedValue({}),
|
||||
});
|
||||
const result = await service.dbHealthCheck();
|
||||
expect(result).toStrictEqual({ db: { status: 'ok' } });
|
||||
});
|
||||
|
||||
it('returns error', async () => {
|
||||
(jest.spyOn(Account, 'query') as jest.Mock).mockReturnValue({
|
||||
limit: jest.fn().mockRejectedValue({}),
|
||||
});
|
||||
const result = await service.dbHealthCheck();
|
||||
expect(result).toStrictEqual({ db: { status: 'error' } });
|
||||
});
|
||||
});
|
|
@ -1,38 +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 { Inject, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { setupAuthDatabase } from 'fxa-shared/db';
|
||||
import { StatsD } from 'hot-shots';
|
||||
import { Account } from 'fxa-shared/db/models/auth';
|
||||
import { MozLoggerService } from 'fxa-shared/nestjs/logger/logger.service';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
import { AppConfig } from '../config';
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseService {
|
||||
public authKnex: Knex;
|
||||
|
||||
constructor(
|
||||
configService: ConfigService<AppConfig>,
|
||||
logger: MozLoggerService,
|
||||
@Inject('METRICS') metrics: StatsD
|
||||
) {
|
||||
const dbConfig = configService.get('database') as AppConfig['database'];
|
||||
this.authKnex = setupAuthDatabase(dbConfig.mysql.auth, logger, metrics);
|
||||
}
|
||||
|
||||
async dbHealthCheck(): Promise<Record<string, any>> {
|
||||
let status = 'ok';
|
||||
try {
|
||||
await Account.query().limit(1);
|
||||
} catch (err) {
|
||||
status = 'error';
|
||||
}
|
||||
return {
|
||||
db: { status },
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,68 +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 { NestApplicationOptions } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { SentryInterceptor } from 'fxa-shared/nestjs/sentry/sentry.interceptor';
|
||||
import { init as initTracing } from 'fxa-shared/tracing/node-tracing';
|
||||
|
||||
import helmet from 'helmet';
|
||||
import mozLog from 'mozlog';
|
||||
import { join } from 'path';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
import Config, { AppConfig } from './config';
|
||||
|
||||
async function bootstrap() {
|
||||
// Initialize tracing first
|
||||
initTracing(
|
||||
Config.getProperties().tracing,
|
||||
mozLog(Config.getProperties().log)(Config.getProperties().log.app)
|
||||
);
|
||||
|
||||
const nestConfig: NestApplicationOptions = {};
|
||||
if (Config.getProperties().env !== 'development') {
|
||||
nestConfig.logger = false;
|
||||
}
|
||||
const app = await NestFactory.create<NestExpressApplication>(
|
||||
AppModule,
|
||||
nestConfig
|
||||
);
|
||||
const config: ConfigService<AppConfig> = app.get(ConfigService);
|
||||
|
||||
// Setup application security
|
||||
const cspConfig = config.get('csp') as AppConfig['csp'];
|
||||
const frameAncestors = cspConfig.frameAncestors;
|
||||
app.use(
|
||||
helmet.contentSecurityPolicy({
|
||||
directives: { frameAncestors, defaultSrc: 'self' },
|
||||
})
|
||||
);
|
||||
app.use(helmet.xssFilter());
|
||||
if (config.get<boolean>('hstsEnabled')) {
|
||||
const maxAge = config.get<number>('hstsMaxAge');
|
||||
app.use(helmet.hsts({ includeSubDomains: true, maxAge }));
|
||||
}
|
||||
|
||||
// We run behind a proxy when deployed, include the express middleware
|
||||
// to extract the X-Forwarded-For header.
|
||||
if (Config.getProperties().env !== 'development') {
|
||||
app.set('trust proxy', true);
|
||||
}
|
||||
|
||||
// Add sentry as error reporter
|
||||
app.useGlobalInterceptors(new SentryInterceptor());
|
||||
|
||||
// Starts listening for shutdown hooks
|
||||
app.enableShutdownHooks();
|
||||
|
||||
// Setup handlebars template rendering
|
||||
app.setBaseViewsDir(join(__dirname, '..', 'views'));
|
||||
app.setViewEngine('hbs');
|
||||
|
||||
const listenConfig = config.get('listen') as AppConfig['listen'];
|
||||
await app.listen(listenConfig.port, listenConfig.host);
|
||||
}
|
||||
bootstrap();
|
|
@ -1,241 +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 { ConfigService } from '@nestjs/config';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { MozillaSubscriptionTypes } from 'fxa-shared/subscriptions/types';
|
||||
import superagent from 'superagent';
|
||||
|
||||
import { RemoteLookupService } from './remote-lookup.service';
|
||||
|
||||
const now = new Date().getTime();
|
||||
const MS_IN_SEC = 1000;
|
||||
|
||||
export const MOCKDATA = {
|
||||
account: {
|
||||
createdAt: now,
|
||||
email: 'test+quux@example.com',
|
||||
emailVerified: true,
|
||||
locale: 'en-us',
|
||||
},
|
||||
devices: [
|
||||
{
|
||||
createdAt: 1578414423827,
|
||||
name: 'desktop',
|
||||
type: 'browser',
|
||||
},
|
||||
],
|
||||
signinLocations: [
|
||||
{
|
||||
city: 'Heapolandia',
|
||||
country: 'United Devices of von Neumann',
|
||||
countryCode: 'UVN',
|
||||
lastAccessTime: 1578414423827,
|
||||
state: 'Memory Palace',
|
||||
stateCode: 'MP',
|
||||
},
|
||||
{
|
||||
city: 'Boring',
|
||||
country: 'United States',
|
||||
countryCode: 'US',
|
||||
lastAccessTime: 1578498222026,
|
||||
state: 'Oregon',
|
||||
stateCode: 'OR',
|
||||
},
|
||||
],
|
||||
subscriptions: {
|
||||
[MozillaSubscriptionTypes.WEB]: [
|
||||
{
|
||||
created: 1555354567,
|
||||
current_period_end: 1579716673,
|
||||
current_period_start: 1579630273,
|
||||
plan_changed: 1579630273,
|
||||
previous_product: 'Old Product',
|
||||
product_name: 'Example Product',
|
||||
status: 'active',
|
||||
subscription_id: 'sub_GZ7WKEJp1YGZ86',
|
||||
},
|
||||
{
|
||||
created: 1588972390,
|
||||
current_period_end: 1591650790,
|
||||
current_period_start: 1588972390,
|
||||
plan_changed: null,
|
||||
previous_product: null,
|
||||
product_name: 'Amazing Product',
|
||||
status: 'active',
|
||||
subscription_id: 'sub_12345',
|
||||
},
|
||||
],
|
||||
[MozillaSubscriptionTypes.IAP_GOOGLE]: [
|
||||
{
|
||||
auto_renewing: false,
|
||||
expiry_time_millis: 1591650790000,
|
||||
package_name: 'club.foxkeh',
|
||||
sku: 'LOL.daily',
|
||||
product_id: 'prod_testo',
|
||||
product_name: 'LOL Daily',
|
||||
},
|
||||
],
|
||||
[MozillaSubscriptionTypes.IAP_APPLE]: [
|
||||
{
|
||||
app_store_product_id: 'wow',
|
||||
auto_renewing: false,
|
||||
bundle_id: 'hmm',
|
||||
expiry_time_millis: 1591650790000,
|
||||
product_id: 'prod_123',
|
||||
product_name: 'Cooking with Foxkeh',
|
||||
},
|
||||
],
|
||||
},
|
||||
totp: {
|
||||
enabled: true,
|
||||
epoch: now,
|
||||
sharedSecret: '',
|
||||
verified: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const formattedSubscriptions = {
|
||||
[MozillaSubscriptionTypes.WEB]: [
|
||||
{
|
||||
created: String(new Date(1555354567 * MS_IN_SEC)),
|
||||
current_period_end: String(new Date(1579716673 * MS_IN_SEC)),
|
||||
current_period_start: String(new Date(1579630273 * MS_IN_SEC)),
|
||||
plan_changed: String(new Date(1579630273 * MS_IN_SEC)),
|
||||
previous_product: 'Old Product',
|
||||
product_name: 'Example Product',
|
||||
status: 'active',
|
||||
subscription_id: 'sub_GZ7WKEJp1YGZ86',
|
||||
},
|
||||
{
|
||||
created: String(new Date(1588972390 * MS_IN_SEC)),
|
||||
current_period_end: String(new Date(1591650790 * MS_IN_SEC)),
|
||||
current_period_start: String(new Date(1588972390 * MS_IN_SEC)),
|
||||
plan_changed: 'N/A',
|
||||
previous_product: 'N/A',
|
||||
product_name: 'Amazing Product',
|
||||
status: 'active',
|
||||
subscription_id: 'sub_12345',
|
||||
},
|
||||
],
|
||||
[MozillaSubscriptionTypes.IAP_GOOGLE]: [
|
||||
{
|
||||
auto_renewing: false,
|
||||
expiry_time_millis: 1591650790000,
|
||||
expiry: String(new Date(1591650790 * MS_IN_SEC)),
|
||||
package_name: 'club.foxkeh',
|
||||
sku: 'LOL.daily',
|
||||
product_id: 'prod_testo',
|
||||
product_name: 'LOL Daily',
|
||||
},
|
||||
],
|
||||
[MozillaSubscriptionTypes.IAP_APPLE]: [
|
||||
{
|
||||
app_store_product_id: 'wow',
|
||||
auto_renewing: false,
|
||||
bundle_id: 'hmm',
|
||||
expiry_time_millis: 1591650790000,
|
||||
expiry: String(new Date(1591650790 * MS_IN_SEC)),
|
||||
product_id: 'prod_123',
|
||||
product_name: 'Cooking with Foxkeh',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const formattedSigninLocations = [
|
||||
{
|
||||
city: 'Heapolandia',
|
||||
country: 'United Devices of von Neumann',
|
||||
countryCode: 'UVN',
|
||||
lastAccessTime: new Date(1578414423827),
|
||||
state: 'Memory Palace',
|
||||
stateCode: 'MP',
|
||||
},
|
||||
{
|
||||
city: 'Boring',
|
||||
country: 'United States',
|
||||
countryCode: 'US',
|
||||
lastAccessTime: new Date(1578498222026),
|
||||
state: 'Oregon',
|
||||
stateCode: 'OR',
|
||||
},
|
||||
];
|
||||
|
||||
describe('RemoteLookupService', () => {
|
||||
let service: RemoteLookupService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfig = jest.fn().mockReturnValue({});
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
RemoteLookupService,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: { get: mockConfig },
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<RemoteLookupService>(RemoteLookupService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('calls a URL with headers set', async () => {
|
||||
(jest.spyOn(superagent, 'get') as jest.Mock).mockReturnValue({
|
||||
set: jest.fn().mockReturnValue({
|
||||
set: jest.fn().mockResolvedValue({
|
||||
body: MOCKDATA.account,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
expect(await service.authServerGetBody('url')).toStrictEqual(
|
||||
MOCKDATA.account
|
||||
);
|
||||
});
|
||||
|
||||
describe('subscriptions', () => {
|
||||
it('returns successfully', async () => {
|
||||
service.authServerGetBody = jest
|
||||
.fn()
|
||||
.mockResolvedValue(MOCKDATA.subscriptions);
|
||||
expect(await service.subscriptions('test', 'email')).toStrictEqual(
|
||||
formattedSubscriptions
|
||||
);
|
||||
});
|
||||
|
||||
it('handles subscriptions not found', async () => {
|
||||
service.authServerGetBody = jest
|
||||
.fn()
|
||||
.mockRejectedValue({ status: 500, response: { body: { errno: 998 } } });
|
||||
expect(await service.subscriptions('test', 'email')).toStrictEqual({
|
||||
[MozillaSubscriptionTypes.WEB]: [],
|
||||
[MozillaSubscriptionTypes.IAP_GOOGLE]: [],
|
||||
[MozillaSubscriptionTypes.IAP_APPLE]: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('re-throws other errors', async () => {
|
||||
expect.assertions(1);
|
||||
service.authServerGetBody = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('unknown'));
|
||||
try {
|
||||
await service.subscriptions('test', 'email');
|
||||
} catch (err) {
|
||||
expect(err).toStrictEqual(new Error('unknown'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('returns signin locations', async () => {
|
||||
service.authServerGetBody = jest
|
||||
.fn()
|
||||
.mockResolvedValue(MOCKDATA.signinLocations);
|
||||
expect(await service.signinLocations('test')).toStrictEqual(
|
||||
formattedSigninLocations
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,103 +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 { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { MozillaSubscriptionTypes } from 'fxa-shared/subscriptions/types';
|
||||
import superagent from 'superagent';
|
||||
|
||||
import { AppConfig } from '../config';
|
||||
import {
|
||||
SubscriptionResponse,
|
||||
SigninLocationResponse,
|
||||
} from './remote-responses.dto';
|
||||
|
||||
const MS_IN_SEC = 1000;
|
||||
|
||||
@Injectable()
|
||||
export class RemoteLookupService {
|
||||
private authServer: AppConfig['authServer'];
|
||||
|
||||
constructor(configService: ConfigService<AppConfig>) {
|
||||
this.authServer = configService.get(
|
||||
'authServer'
|
||||
) as AppConfig['authServer'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to include bearer token for auth server calls and extract the body.
|
||||
*
|
||||
* @param url
|
||||
*/
|
||||
authServerGetBody(url: string): Promise<any> {
|
||||
return superagent
|
||||
.get(url)
|
||||
.set('Authorization', `Bearer ${this.authServer.secretBearerToken}`)
|
||||
.set('accept', 'json')
|
||||
.then((response) => response.body);
|
||||
}
|
||||
|
||||
async subscriptions(
|
||||
uid: string,
|
||||
email: string
|
||||
): Promise<SubscriptionResponse> {
|
||||
try {
|
||||
const subscriptions = await this.authServerGetBody(
|
||||
`${this.authServer.url}${
|
||||
this.authServer.subscriptionsSearchPath
|
||||
}?uid=${uid}&email=${encodeURIComponent(email)}`
|
||||
);
|
||||
return {
|
||||
[MozillaSubscriptionTypes.WEB]: subscriptions[
|
||||
MozillaSubscriptionTypes.WEB
|
||||
].map((s: any) => ({
|
||||
...s,
|
||||
created: String(new Date(s.created * MS_IN_SEC)),
|
||||
current_period_end: String(
|
||||
new Date(s.current_period_end * MS_IN_SEC)
|
||||
),
|
||||
current_period_start: String(
|
||||
new Date(s.current_period_start * MS_IN_SEC)
|
||||
),
|
||||
plan_changed: s.plan_changed
|
||||
? String(new Date(s.plan_changed * MS_IN_SEC))
|
||||
: 'N/A',
|
||||
previous_product: s.previous_product || 'N/A',
|
||||
})),
|
||||
[MozillaSubscriptionTypes.IAP_GOOGLE]: subscriptions[
|
||||
MozillaSubscriptionTypes.IAP_GOOGLE
|
||||
].map((s: any) => ({
|
||||
...s,
|
||||
expiry: String(new Date(parseInt(s.expiry_time_millis))),
|
||||
})),
|
||||
[MozillaSubscriptionTypes.IAP_APPLE]: subscriptions[
|
||||
MozillaSubscriptionTypes.IAP_APPLE
|
||||
].map((s: any) => ({
|
||||
...s,
|
||||
expiry: String(new Date(parseInt(s.expiry_time_millis))),
|
||||
})),
|
||||
};
|
||||
} catch (err) {
|
||||
// A lack of subscriptions results in a errno 998 for invalid user
|
||||
if (err.status === 500 && err.response?.body.errno === 998) {
|
||||
return {
|
||||
[MozillaSubscriptionTypes.WEB]: [],
|
||||
[MozillaSubscriptionTypes.IAP_GOOGLE]: [],
|
||||
[MozillaSubscriptionTypes.IAP_APPLE]: [],
|
||||
};
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async signinLocations(uid: string): Promise<SigninLocationResponse> {
|
||||
const locations = await this.authServerGetBody(
|
||||
`${this.authServer.url}${this.authServer.signinLocationsSearchPath}?uid=${uid}`
|
||||
);
|
||||
return locations.map((v: any) => ({
|
||||
...v,
|
||||
lastAccessTime: new Date(v.lastAccessTime),
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -1,73 +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 { MozillaSubscriptionTypes } from 'fxa-shared/subscriptions/types';
|
||||
|
||||
// Note that these `*.Response` interfaces are purely for access to known
|
||||
// response keys and not an attempt to validate the return payloads from
|
||||
// the database.
|
||||
|
||||
interface WebSubscription {
|
||||
created: string;
|
||||
current_period_end: string;
|
||||
current_period_start: string;
|
||||
plan_changed: string;
|
||||
previous_product: string;
|
||||
product_name: string;
|
||||
status: string;
|
||||
subscription_id: string;
|
||||
}
|
||||
|
||||
type PlaySubscription = {
|
||||
auto_renewing: boolean;
|
||||
cancel_reason?: number;
|
||||
package_name: string;
|
||||
sku: string;
|
||||
product_id: string;
|
||||
expiry: string;
|
||||
};
|
||||
|
||||
type AppStoreSubscription = {
|
||||
app_store_product_id: string;
|
||||
auto_renewing: boolean;
|
||||
bundle_id: string;
|
||||
is_in_billing_retry_period: boolean;
|
||||
product_id: string;
|
||||
expiry: string;
|
||||
};
|
||||
|
||||
export type SubscriptionResponse = {
|
||||
[MozillaSubscriptionTypes.WEB]: WebSubscription[];
|
||||
[MozillaSubscriptionTypes.IAP_GOOGLE]: PlaySubscription[];
|
||||
[MozillaSubscriptionTypes.IAP_APPLE]: AppStoreSubscription[];
|
||||
};
|
||||
|
||||
interface SigninLocation {
|
||||
city: string;
|
||||
state: string;
|
||||
stateCode: string;
|
||||
country: string;
|
||||
countryCode: string;
|
||||
lastAccessTime: number | Date;
|
||||
}
|
||||
|
||||
export interface SigninLocationResponse extends Array<SigninLocation> {}
|
||||
|
||||
export interface TotpTokenResponse {
|
||||
sharedSecret: string;
|
||||
epoch: number;
|
||||
verified: boolean;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
/** SupportController configuration */
|
||||
export type SupportConfig = {
|
||||
authHeader: string;
|
||||
authServer: {
|
||||
secretBearerToken: string;
|
||||
signinLocationsSearchPath: string;
|
||||
subscriptionsSearchPath: string;
|
||||
url: string;
|
||||
};
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/__version__ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/__lbheartbeat__')
|
||||
.expect(200)
|
||||
.expect('{}');
|
||||
});
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": [ "ts-jest", { "isolatedModules": true } ]
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
},
|
||||
"references": [{ "path": "../fxa-shared" }],
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"extends": "../../_dev/tsconfig.node.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"noEmitHelpers": true,
|
||||
"importHelpers": true,
|
||||
"types": ["mozlog", "jest"]
|
||||
},
|
||||
"references": [{ "path": "../fxa-shared" }],
|
||||
"include": ["src"]
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<h3>User {{ uid }}</h3>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Email:</th>
|
||||
<td>{{email}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Email confirmed:</th>
|
||||
<td>
|
||||
{{#emailVerified}}yes{{/emailVerified}}
|
||||
{{^emailVerified}}no{{/emailVerified}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Account created:</th>
|
||||
<td>{{created}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Locale:</th>
|
||||
<td>{{locale}}</td>
|
||||
</tr>
|
||||
{{#signinLocations}}
|
||||
<tr>
|
||||
<th>Signin Location:</th>
|
||||
<td>
|
||||
{{city}}, {{stateCode}}, {{countryCode}} on {{lastAccessTime}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/signinLocations}}
|
||||
{{#devices}}
|
||||
<tr>
|
||||
<th>Device:</th>
|
||||
<td>name: {{name}}; type: {{type}}; created: {{created}}</td>
|
||||
</tr>
|
||||
{{/devices}}
|
||||
<tr>
|
||||
<th>2FA enabled?</th>
|
||||
<td>
|
||||
{{#twoFactorAuth}}yes{{/twoFactorAuth}}
|
||||
{{^twoFactorAuth}}no{{/twoFactorAuth}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>FxA subscription status:</th>
|
||||
<td>{{subscriptionStatus}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{#subscriptionStatus}}
|
||||
<h3>Current Subscriptions</h3>
|
||||
|
||||
{{#webSubscriptions}}
|
||||
<h4>Web Subscriptions</h4>
|
||||
<table>
|
||||
|
||||
<tr>
|
||||
<th>Subscription:</th>
|
||||
<td>{{ product_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created:</th>
|
||||
<td>{{ created }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Last Payment:</th>
|
||||
<td>{{ current_period_start }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Next Payment:</th>
|
||||
<td>{{ current_period_end }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Previous Product:</th>
|
||||
<td>{{ previous_product }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Plan Changed:</th>
|
||||
<td>{{ plan_changed }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/>
|
||||
{{/webSubscriptions}}
|
||||
|
||||
{{#playSubscriptions}}
|
||||
<h4>Google Play Subscriptions</h4>
|
||||
<table>
|
||||
|
||||
<tr>
|
||||
<th>Subscription:</th>
|
||||
<td>{{ product_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Auto Renewing:</th>
|
||||
<td>{{ auto_renewing }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Next Payment:</th>
|
||||
<td>{{ expiry }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/>
|
||||
{{/playSubscriptions}}
|
||||
|
||||
{{#appSubscriptions}}
|
||||
<h4>App Store Subscriptions</h4>
|
||||
<table>
|
||||
|
||||
<tr>
|
||||
<th>Subscription:</th>
|
||||
<td>{{ product_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Auto Renewing:</th>
|
||||
<td>{{ auto_renewing }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Next Payment:</th>
|
||||
<td>{{ expiry }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/>
|
||||
{{/appSubscriptions}}
|
||||
{{/subscriptionStatus}}
|
||||
</body>
|
||||
</html>
|
74
yarn.lock
74
yarn.lock
|
@ -11744,7 +11744,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/eslint@npm:*, @types/eslint@npm:7.2.13, @types/eslint@npm:^7.2.6":
|
||||
"@types/eslint@npm:*, @types/eslint@npm:^7.2.6":
|
||||
version: 7.2.13
|
||||
resolution: "@types/eslint@npm:7.2.13"
|
||||
dependencies:
|
||||
|
@ -11983,15 +11983,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/hbs@npm:^4":
|
||||
version: 4.0.1
|
||||
resolution: "@types/hbs@npm:4.0.1"
|
||||
dependencies:
|
||||
handlebars: ^4.1.0
|
||||
checksum: 28eae02b0a4133b0864096a59ca7e4573bcacebd9a0412c56428d06fe6ed42aa041d0cc57124551aecb841e08533ca3bdca7998d13cf44ba98ffdf32bff67fa5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/helmet@npm:4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "@types/helmet@npm:4.0.0"
|
||||
|
@ -26787,55 +26778,6 @@ fsevents@~2.1.1:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"fxa-support-panel@workspace:packages/fxa-support-panel":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "fxa-support-panel@workspace:packages/fxa-support-panel"
|
||||
dependencies:
|
||||
"@nestjs/cli": ^9.1.3
|
||||
"@nestjs/common": ^9.1.2
|
||||
"@nestjs/config": ^2.3.1
|
||||
"@nestjs/core": ^9.2.0
|
||||
"@nestjs/mapped-types": ^1.2.0
|
||||
"@nestjs/platform-express": ^9.2.0
|
||||
"@sentry/integrations": ^6.19.1
|
||||
"@sentry/node": ^6.19.1
|
||||
"@types/convict": ^5.2.2
|
||||
"@types/eslint": 7.2.13
|
||||
"@types/hbs": ^4
|
||||
"@types/node": ^16.11.3
|
||||
"@types/superagent": 4.1.11
|
||||
"@types/supertest": ^2.0.11
|
||||
audit-filter: ^0.5.0
|
||||
class-transformer: ^0.5.1
|
||||
class-validator: ^0.14.0
|
||||
convict: ^6.2.4
|
||||
convict-format-with-moment: ^6.2.0
|
||||
convict-format-with-validator: ^6.2.0
|
||||
esbuild: ^0.14.2
|
||||
esbuild-register: ^3.2.0
|
||||
eslint: ^7.32.0
|
||||
express: ^4.17.3
|
||||
fxa-shared: "workspace:*"
|
||||
handlebars: ^4.7.7
|
||||
hbs: ^4.2.0
|
||||
helmet: ^6.0.0
|
||||
hot-shots: ^10.0.0
|
||||
jest: 27.5.1
|
||||
mozlog: ^3.0.2
|
||||
pm2: ^5.2.2
|
||||
prettier: ^2.3.1
|
||||
reflect-metadata: ^0.1.13
|
||||
rimraf: ^3.0.2
|
||||
rxjs: ^7.8.0
|
||||
semver: ^7.3.5
|
||||
superagent: ^8.0.0
|
||||
supertest: ^6.3.0
|
||||
ts-jest: ^29.0.0
|
||||
tslib: ^2.5.0
|
||||
typescript: ^4.9.3
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"fxa@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "fxa@workspace:."
|
||||
|
@ -28379,7 +28321,7 @@ fsevents@~2.1.1:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"handlebars@npm:4.7.7, handlebars@npm:^4.1.0, handlebars@npm:^4.7.7":
|
||||
"handlebars@npm:^4.7.7":
|
||||
version: 4.7.7
|
||||
resolution: "handlebars@npm:4.7.7"
|
||||
dependencies:
|
||||
|
@ -28814,16 +28756,6 @@ fsevents@~2.1.1:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hbs@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "hbs@npm:4.2.0"
|
||||
dependencies:
|
||||
handlebars: 4.7.7
|
||||
walk: 2.3.15
|
||||
checksum: 811259d1bc6ecf94fca824df438167306a5f7e122b82aca59a3c593067c50175ce81c3de4d258b9cb19ae9ef5b91e8486282b5791191940bb5d8a298304965b5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"he@npm:1.2.0, he@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "he@npm:1.2.0"
|
||||
|
@ -49552,7 +49484,7 @@ resolve@^2.0.0-next.3:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"walk@npm:2.3.15, walk@npm:^2.3.15, walk@npm:^2.3.9":
|
||||
"walk@npm:^2.3.15, walk@npm:^2.3.9":
|
||||
version: 2.3.15
|
||||
resolution: "walk@npm:2.3.15"
|
||||
dependencies:
|
||||
|
|
Загрузка…
Ссылка в новой задаче