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:
Barry Chen 2023-03-13 08:50:33 -05:00
Родитель 21fcfb61cf
Коммит 2b2fada774
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 228DB2785954A0D0
41 изменённых файлов: 7 добавлений и 3790 удалений

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

@ -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"
}
]
}

41
packages/fxa-support-panel/.vscode/tasks.json поставляемый
Просмотреть файл

@ -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>

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

@ -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: