зеркало из https://github.com/mozilla/fxa.git
chore(auth-server): enable typescript-eslint
Because: * We aren't currently linting TS files in auth-server This commit: * Enables and fixes linting issues in auth-server Closes FXA-6136
This commit is contained in:
Родитель
6941fd4d64
Коммит
76ef507b26
|
@ -121,6 +121,27 @@ jobs:
|
||||||
name: Reporting code coverage...
|
name: Reporting code coverage...
|
||||||
command: bash <(curl -s https://codecov.io/bash) -F << parameters.package >> -X gcov
|
command: bash <(curl -s https://codecov.io/bash) -F << parameters.package >> -X gcov
|
||||||
|
|
||||||
|
lint:
|
||||||
|
resource_class: medium+
|
||||||
|
docker:
|
||||||
|
- image: cimg/node:16.13
|
||||||
|
environment:
|
||||||
|
TRACING_SERVICE_NAME: ci-lint
|
||||||
|
TRACING_CONSOLE_EXPORTER_ENABLED: true
|
||||||
|
steps:
|
||||||
|
- base-install
|
||||||
|
- run:
|
||||||
|
name: Linting
|
||||||
|
command: |
|
||||||
|
PACKAGES=(\
|
||||||
|
'fxa-shared' \
|
||||||
|
'fxa-auth-server' \
|
||||||
|
)
|
||||||
|
for p in "${PACKAGES[@]}"; do
|
||||||
|
(cd packages/$p && yarn lint)
|
||||||
|
done
|
||||||
|
- jira/notify
|
||||||
|
|
||||||
test-many:
|
test-many:
|
||||||
resource_class: medium+
|
resource_class: medium+
|
||||||
docker:
|
docker:
|
||||||
|
@ -308,6 +329,12 @@ jobs:
|
||||||
workflows:
|
workflows:
|
||||||
test_pull_request:
|
test_pull_request:
|
||||||
jobs:
|
jobs:
|
||||||
|
- lint:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: main
|
||||||
|
tags:
|
||||||
|
ignore: /.*/
|
||||||
- test-many:
|
- test-many:
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
|
@ -395,6 +422,12 @@ workflows:
|
||||||
ignore: /.*/
|
ignore: /.*/
|
||||||
test_and_deploy_tag:
|
test_and_deploy_tag:
|
||||||
jobs:
|
jobs:
|
||||||
|
- lint:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: /.*/
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
- test-many:
|
- test-many:
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
|
|
|
@ -54,11 +54,14 @@
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.js": [
|
"*.js": [
|
||||||
"prettier --config _dev/.prettierrc --write",
|
"prettier --config _dev/.prettierrc --write",
|
||||||
"eslint -c _dev/.eslintrc"
|
"eslint"
|
||||||
],
|
],
|
||||||
"*.{ts,tsx}": [
|
"*.{ts,tsx}": [
|
||||||
"prettier --config _dev/.prettierrc --write"
|
"prettier --config _dev/.prettierrc --write"
|
||||||
],
|
],
|
||||||
|
"packages/fxa-auth-server/**/*.{ts,tsx}": [
|
||||||
|
"eslint"
|
||||||
|
],
|
||||||
"*.css": [
|
"*.css": [
|
||||||
"prettier --config _dev/.prettierrc --write"
|
"prettier --config _dev/.prettierrc --write"
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
{
|
{
|
||||||
"rules": {
|
"rules": {
|
||||||
"require-atomic-updates": "off"
|
"require-atomic-updates": "off",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", {"vars": "all", "args": "none"}],
|
||||||
|
"no-redeclare": "off",
|
||||||
|
"@typescript-eslint/no-redeclare": "error"
|
||||||
},
|
},
|
||||||
"extends": ["plugin:fxa/server"],
|
"extends": ["plugin:fxa/server"],
|
||||||
"plugins": ["fxa"],
|
"plugins": ["@typescript-eslint", "fxa"],
|
||||||
"parserOptions": {
|
"parser": "@typescript-eslint/parser",
|
||||||
"ecmaVersion": "2020",
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"root": true,
|
"root": true,
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|
|
@ -86,6 +86,14 @@ Use the following as a template, and fill in your own values:
|
||||||
|
|
||||||
The sandbox PayPal business account API credentials above can be found in the PayPal developer dashboard under ["Sandbox" > "Accounts"](https://developer.paypal.com/developer/accounts/). You may need to create a business account if one doesn't exist.
|
The sandbox PayPal business account API credentials above can be found in the PayPal developer dashboard under ["Sandbox" > "Accounts"](https://developer.paypal.com/developer/accounts/). You may need to create a business account if one doesn't exist.
|
||||||
|
|
||||||
|
## Linting
|
||||||
|
|
||||||
|
Run lint with:
|
||||||
|
|
||||||
|
yarn lint
|
||||||
|
|
||||||
|
Linting will also be run for staged files automatically via Husky when you attempt to commit.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Run tests with:
|
Run tests with:
|
||||||
|
|
|
@ -15,7 +15,7 @@ const FIVE_MINUTES = 1000 * 60 * 5;
|
||||||
convict.addFormats(require('convict-format-with-moment'));
|
convict.addFormats(require('convict-format-with-moment'));
|
||||||
convict.addFormats(require('convict-format-with-validator'));
|
convict.addFormats(require('convict-format-with-validator'));
|
||||||
|
|
||||||
const conf = convict({
|
const convictConf = convict({
|
||||||
env: {
|
env: {
|
||||||
doc: 'The current node.js environment',
|
doc: 'The current node.js environment',
|
||||||
default: 'prod',
|
default: 'prod',
|
||||||
|
@ -1572,7 +1572,7 @@ const conf = convict({
|
||||||
format: RegExp,
|
format: RegExp,
|
||||||
// eslint-disable-next-line no-useless-escape
|
// eslint-disable-next-line no-useless-escape
|
||||||
default:
|
default:
|
||||||
/^https:\/\/[a-zA-Z0-9._-]+(\.services\.mozilla\.com|autopush\.dev\.mozaws\.net|autopush\.stage\.mozaws\.net)(?:\:\d+)?(\/.*)?$/,
|
/^https:\/\/[a-zA-Z0-9._-]+(\.services\.mozilla\.com|autopush\.dev\.mozaws\.net|autopush\.stage\.mozaws\.net)(?::\d+)?(\/.*)?$/,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pushbox: {
|
pushbox: {
|
||||||
|
@ -1953,55 +1953,64 @@ const conf = convict({
|
||||||
// files to process, which will be overlayed in order, in the CONFIG_FILES
|
// files to process, which will be overlayed in order, in the CONFIG_FILES
|
||||||
// environment variable.
|
// environment variable.
|
||||||
|
|
||||||
let envConfig = path.join(__dirname, `${conf.get('env')}.json`);
|
let envConfig = path.join(__dirname, `${convictConf.get('env')}.json`);
|
||||||
envConfig = `${envConfig},${process.env.CONFIG_FILES || ''}`;
|
envConfig = `${envConfig},${process.env.CONFIG_FILES || ''}`;
|
||||||
const files = envConfig.split(',').filter(fs.existsSync);
|
const files = envConfig.split(',').filter(fs.existsSync);
|
||||||
conf.loadFile(files);
|
convictConf.loadFile(files);
|
||||||
conf.validate();
|
convictConf.validate();
|
||||||
|
|
||||||
// set the public url as the issuer domain for assertions
|
// set the public url as the issuer domain for assertions
|
||||||
conf.set('domain', url.parse(conf.get('publicUrl')).host);
|
convictConf.set('domain', url.parse(convictConf.get('publicUrl')).host);
|
||||||
|
|
||||||
// derive fxa-auth-mailer configuration from our content-server url
|
// derive fxa-auth-mailer configuration from our content-server url
|
||||||
const baseUri = conf.get('contentServer.url');
|
const baseUri = convictConf.get('contentServer.url');
|
||||||
conf.set('smtp.accountSettingsUrl', `${baseUri}/settings`);
|
convictConf.set('smtp.accountSettingsUrl', `${baseUri}/settings`);
|
||||||
conf.set(
|
convictConf.set(
|
||||||
'smtp.accountRecoveryCodesUrl',
|
'smtp.accountRecoveryCodesUrl',
|
||||||
`${baseUri}/settings/two_step_authentication/replace_codes`
|
`${baseUri}/settings/two_step_authentication/replace_codes`
|
||||||
);
|
);
|
||||||
conf.set('smtp.verificationUrl', `${baseUri}/verify_email`);
|
convictConf.set('smtp.verificationUrl', `${baseUri}/verify_email`);
|
||||||
conf.set('smtp.pushVerificationUrl', `${baseUri}/push/confirm_login`);
|
convictConf.set('smtp.pushVerificationUrl', `${baseUri}/push/confirm_login`);
|
||||||
conf.set('smtp.passwordResetUrl', `${baseUri}/complete_reset_password`);
|
convictConf.set('smtp.passwordResetUrl', `${baseUri}/complete_reset_password`);
|
||||||
conf.set('smtp.initiatePasswordResetUrl', `${baseUri}/reset_password`);
|
convictConf.set('smtp.initiatePasswordResetUrl', `${baseUri}/reset_password`);
|
||||||
conf.set(
|
convictConf.set(
|
||||||
'smtp.initiatePasswordChangeUrl',
|
'smtp.initiatePasswordChangeUrl',
|
||||||
`${baseUri}/settings/change_password`
|
`${baseUri}/settings/change_password`
|
||||||
);
|
);
|
||||||
conf.set('smtp.verifyLoginUrl', `${baseUri}/complete_signin`);
|
convictConf.set('smtp.verifyLoginUrl', `${baseUri}/complete_signin`);
|
||||||
conf.set(
|
convictConf.set(
|
||||||
'smtp.accountFinishSetupUrl',
|
'smtp.accountFinishSetupUrl',
|
||||||
`${baseUri}/post_verify/finish_account_setup/set_password`
|
`${baseUri}/post_verify/finish_account_setup/set_password`
|
||||||
);
|
);
|
||||||
conf.set('smtp.reportSignInUrl', `${baseUri}/report_signin`);
|
convictConf.set('smtp.reportSignInUrl', `${baseUri}/report_signin`);
|
||||||
conf.set('smtp.revokeAccountRecoveryUrl', `${baseUri}/settings#recovery-key`);
|
convictConf.set(
|
||||||
conf.set(
|
'smtp.revokeAccountRecoveryUrl',
|
||||||
|
`${baseUri}/settings#recovery-key`
|
||||||
|
);
|
||||||
|
convictConf.set(
|
||||||
'smtp.createAccountRecoveryUrl',
|
'smtp.createAccountRecoveryUrl',
|
||||||
`${baseUri}/settings/account_recovery`
|
`${baseUri}/settings/account_recovery`
|
||||||
);
|
);
|
||||||
conf.set('smtp.verifyPrimaryEmailUrl', `${baseUri}/verify_primary_email`);
|
convictConf.set(
|
||||||
conf.set('smtp.verifySecondaryEmailUrl', `${baseUri}/verify_secondary_email`);
|
'smtp.verifyPrimaryEmailUrl',
|
||||||
conf.set('smtp.subscriptionSettingsUrl', `${baseUri}/subscriptions`);
|
`${baseUri}/verify_primary_email`
|
||||||
conf.set('smtp.subscriptionSupportUrl', `${baseUri}/support`);
|
);
|
||||||
conf.set('smtp.syncUrl', `${baseUri}/connect_another_device`);
|
convictConf.set(
|
||||||
|
'smtp.verifySecondaryEmailUrl',
|
||||||
|
`${baseUri}/verify_secondary_email`
|
||||||
|
);
|
||||||
|
convictConf.set('smtp.subscriptionSettingsUrl', `${baseUri}/subscriptions`);
|
||||||
|
convictConf.set('smtp.subscriptionSupportUrl', `${baseUri}/support`);
|
||||||
|
convictConf.set('smtp.syncUrl', `${baseUri}/connect_another_device`);
|
||||||
|
|
||||||
conf.set('isProduction', conf.get('env') === 'prod');
|
convictConf.set('isProduction', convictConf.get('env') === 'prod');
|
||||||
|
|
||||||
//sns endpoint is not to be set in production
|
//sns endpoint is not to be set in production
|
||||||
if (conf.has('snsTopicEndpoint') && conf.get('env') !== 'dev') {
|
if (convictConf.has('snsTopicEndpoint') && convictConf.get('env') !== 'dev') {
|
||||||
throw new Error('snsTopicEndpoint is only allowed in dev env');
|
throw new Error('snsTopicEndpoint is only allowed in dev env');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conf.get('env') === 'dev') {
|
if (convictConf.get('env') === 'dev') {
|
||||||
if (!process.env.AWS_ACCESS_KEY_ID) {
|
if (!process.env.AWS_ACCESS_KEY_ID) {
|
||||||
process.env.AWS_ACCESS_KEY_ID = 'DEV_KEY_ID';
|
process.env.AWS_ACCESS_KEY_ID = 'DEV_KEY_ID';
|
||||||
}
|
}
|
||||||
|
@ -2010,65 +2019,71 @@ if (conf.get('env') === 'dev') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conf.get('oauthServer.openid.keyFile')) {
|
if (convictConf.get('oauthServer.openid.keyFile')) {
|
||||||
const keyFile = path.resolve(
|
const keyFile = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'..',
|
'..',
|
||||||
conf.get('oauthServer.openid.keyFile')
|
convictConf.get('oauthServer.openid.keyFile')
|
||||||
);
|
);
|
||||||
conf.set('oauthServer.openid.keyFile', keyFile);
|
convictConf.set('oauthServer.openid.keyFile', keyFile);
|
||||||
// If the file doesnt exist, or contains an empty object, then there's no active key.
|
// If the file doesnt exist, or contains an empty object, then there's no active key.
|
||||||
conf.set('oauthServer.openid.key', null);
|
convictConf.set('oauthServer.openid.key', null);
|
||||||
if (fs.existsSync(keyFile)) {
|
if (fs.existsSync(keyFile)) {
|
||||||
const key = JSON.parse(fs.readFileSync(keyFile, 'utf-8'));
|
const key = JSON.parse(fs.readFileSync(keyFile, 'utf-8'));
|
||||||
if (key && Object.keys(key).length > 0) {
|
if (key && Object.keys(key).length > 0) {
|
||||||
conf.set('oauthServer.openid.key', key);
|
convictConf.set('oauthServer.openid.key', key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (Object.keys(conf.get('oauthServer.openid.key')).length === 0) {
|
} else if (
|
||||||
conf.set('oauthServer.openid.key', null);
|
Object.keys(convictConf.get('oauthServer.openid.key')).length === 0
|
||||||
|
) {
|
||||||
|
convictConf.set('oauthServer.openid.key', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conf.get('oauthServer.openid.newKeyFile')) {
|
if (convictConf.get('oauthServer.openid.newKeyFile')) {
|
||||||
const newKeyFile = path.resolve(
|
const newKeyFile = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'..',
|
'..',
|
||||||
conf.get('oauthServer.openid.newKeyFile')
|
convictConf.get('oauthServer.openid.newKeyFile')
|
||||||
);
|
);
|
||||||
conf.set('oauthServer.openid.newKeyFile', newKeyFile);
|
convictConf.set('oauthServer.openid.newKeyFile', newKeyFile);
|
||||||
// If the file doesnt exist, or contains an empty object, then there's no new key.
|
// If the file doesnt exist, or contains an empty object, then there's no new key.
|
||||||
conf.set('oauthServer.openid.newKey', null);
|
convictConf.set('oauthServer.openid.newKey', null);
|
||||||
if (fs.existsSync(newKeyFile)) {
|
if (fs.existsSync(newKeyFile)) {
|
||||||
const newKey = JSON.parse(fs.readFileSync(newKeyFile, 'utf-8'));
|
const newKey = JSON.parse(fs.readFileSync(newKeyFile, 'utf-8'));
|
||||||
if (newKey && Object.keys(newKey).length > 0) {
|
if (newKey && Object.keys(newKey).length > 0) {
|
||||||
conf.set('oauthServer.openid.newKey', newKey);
|
convictConf.set('oauthServer.openid.newKey', newKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (Object.keys(conf.get('oauthServer.openid.newKey')).length === 0) {
|
} else if (
|
||||||
conf.set('oauthServer.openid.newKey', null);
|
Object.keys(convictConf.get('oauthServer.openid.newKey')).length === 0
|
||||||
|
) {
|
||||||
|
convictConf.set('oauthServer.openid.newKey', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conf.get('oauthServer.openid.oldKeyFile')) {
|
if (convictConf.get('oauthServer.openid.oldKeyFile')) {
|
||||||
const oldKeyFile = path.resolve(
|
const oldKeyFile = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'..',
|
'..',
|
||||||
conf.get('oauthServer.openid.oldKeyFile')
|
convictConf.get('oauthServer.openid.oldKeyFile')
|
||||||
);
|
);
|
||||||
conf.set('oauthServer.openid.oldKeyFile', oldKeyFile);
|
convictConf.set('oauthServer.openid.oldKeyFile', oldKeyFile);
|
||||||
// If the file doesnt exist, or contains an empty object, then there's no old key.
|
// If the file doesnt exist, or contains an empty object, then there's no old key.
|
||||||
conf.set('oauthServer.openid.oldKey', null);
|
convictConf.set('oauthServer.openid.oldKey', null);
|
||||||
if (fs.existsSync(oldKeyFile)) {
|
if (fs.existsSync(oldKeyFile)) {
|
||||||
const oldKey = JSON.parse(fs.readFileSync(oldKeyFile, 'utf-8'));
|
const oldKey = JSON.parse(fs.readFileSync(oldKeyFile, 'utf-8'));
|
||||||
if (oldKey && Object.keys(oldKey).length > 0) {
|
if (oldKey && Object.keys(oldKey).length > 0) {
|
||||||
conf.set('oauthServer.openid.oldKey', oldKey);
|
convictConf.set('oauthServer.openid.oldKey', oldKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (Object.keys(conf.get('oauthServer.openid.oldKey')).length === 0) {
|
} else if (
|
||||||
conf.set('oauthServer.openid.oldKey', null);
|
Object.keys(convictConf.get('oauthServer.openid.oldKey')).length === 0
|
||||||
|
) {
|
||||||
|
convictConf.set('oauthServer.openid.oldKey', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure secrets are not set to their default values in production.
|
// Ensure secrets are not set to their default values in production.
|
||||||
if (conf.get('isProduction')) {
|
if (convictConf.get('isProduction')) {
|
||||||
const SECRET_SETTINGS = [
|
const SECRET_SETTINGS = [
|
||||||
'pushbox.key',
|
'pushbox.key',
|
||||||
'metrics.flow_id_key',
|
'metrics.flow_id_key',
|
||||||
|
@ -2078,13 +2093,13 @@ if (conf.get('isProduction')) {
|
||||||
'supportPanel.secretBearerToken',
|
'supportPanel.secretBearerToken',
|
||||||
];
|
];
|
||||||
for (const key of SECRET_SETTINGS) {
|
for (const key of SECRET_SETTINGS) {
|
||||||
if (conf.get(key) === conf.default(key)) {
|
if (convictConf.get(key) === convictConf.default(key)) {
|
||||||
throw new Error(`Config '${key}' must be set in production`);
|
throw new Error(`Config '${key}' must be set in production`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type conf = typeof conf;
|
export type conf = typeof convictConf;
|
||||||
export type ConfigType = ReturnType<conf['getProperties']>;
|
export type ConfigType = ReturnType<conf['getProperties']>;
|
||||||
|
|
||||||
module.exports = conf;
|
module.exports = convictConf;
|
||||||
|
|
|
@ -25,8 +25,8 @@ class Localizer {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async fetchMessages(currentLocales: string[]) {
|
protected async fetchMessages(currentLocales: string[]) {
|
||||||
let fetchedPending: Record<string, Promise<string>> = {};
|
const fetchedPending: Record<string, Promise<string>> = {};
|
||||||
let fetched: Record<string, string> = {};
|
const fetched: Record<string, string> = {};
|
||||||
for (const locale of currentLocales) {
|
for (const locale of currentLocales) {
|
||||||
fetchedPending[locale] = this.fetchTranslatedMessages(locale);
|
fetchedPending[locale] = this.fetchTranslatedMessages(locale);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,10 @@ const { AuthLogger } = require('../../types');
|
||||||
const { Container } = require('typedi');
|
const { Container } = require('typedi');
|
||||||
|
|
||||||
// These are used only in type declarations.
|
// These are used only in type declarations.
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const AccessToken = require('./accessToken');
|
const AccessToken = require('./accessToken');
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const RefreshTokenMetadata = require('./refreshTokenMetadata');
|
const RefreshTokenMetadata = require('./refreshTokenMetadata');
|
||||||
|
|
||||||
function resolveLogger() {
|
function resolveLogger() {
|
||||||
|
|
|
@ -12,8 +12,8 @@ export class CurrencyHelper {
|
||||||
this.payPalEnabled = config.subscriptions?.paypalNvpSigCredentials?.enabled;
|
this.payPalEnabled = config.subscriptions?.paypalNvpSigCredentials?.enabled;
|
||||||
// Validate currencyMap
|
// Validate currencyMap
|
||||||
const currencyMap = new Map(Object.entries(config.currenciesToCountries));
|
const currencyMap = new Map(Object.entries(config.currenciesToCountries));
|
||||||
let allListedCountries: string[] = [];
|
const allListedCountries: string[] = [];
|
||||||
for (let [currency, countries] of currencyMap) {
|
for (const [currency, countries] of currencyMap) {
|
||||||
// Is currency acceptable
|
// Is currency acceptable
|
||||||
if (!CurrencyHelper.validCurrencyCodes.includes(currency)) {
|
if (!CurrencyHelper.validCurrencyCodes.includes(currency)) {
|
||||||
throw new Error(`Currency code ${currency} is invalid.`);
|
throw new Error(`Currency code ${currency} is invalid.`);
|
||||||
|
@ -25,7 +25,7 @@ export class CurrencyHelper {
|
||||||
throw new Error(`Currency code ${currency} is invalid.`);
|
throw new Error(`Currency code ${currency} is invalid.`);
|
||||||
}
|
}
|
||||||
// Are countries acceptable
|
// Are countries acceptable
|
||||||
for (let country of countries) {
|
for (const country of countries) {
|
||||||
if (!CurrencyHelper.validCountryCodes.includes(country)) {
|
if (!CurrencyHelper.validCountryCodes.includes(country)) {
|
||||||
throw new Error(`Country code ${country} is invalid.`);
|
throw new Error(`Country code ${country} is invalid.`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,10 @@ import { IapConfig } from './types';
|
||||||
export function getIapPurchaseType(
|
export function getIapPurchaseType(
|
||||||
purchase: PlayStoreSubscriptionPurchase | AppStoreSubscriptionPurchase
|
purchase: PlayStoreSubscriptionPurchase | AppStoreSubscriptionPurchase
|
||||||
): Omit<SubscriptionType, typeof MozillaSubscriptionTypes.WEB> {
|
): Omit<SubscriptionType, typeof MozillaSubscriptionTypes.WEB> {
|
||||||
if (purchase.hasOwnProperty('purchaseToken')) {
|
if ('purchaseToken' in purchase) {
|
||||||
return MozillaSubscriptionTypes.IAP_GOOGLE;
|
return MozillaSubscriptionTypes.IAP_GOOGLE;
|
||||||
}
|
}
|
||||||
if (purchase.hasOwnProperty('originalTransactionId')) {
|
if ('originalTransactionId' in purchase) {
|
||||||
return MozillaSubscriptionTypes.IAP_APPLE;
|
return MozillaSubscriptionTypes.IAP_APPLE;
|
||||||
}
|
}
|
||||||
throw new Error('Purchase is not recognized as either Google or Apple IAP.');
|
throw new Error('Purchase is not recognized as either Google or Apple IAP.');
|
||||||
|
|
|
@ -40,7 +40,7 @@ export function appStoreSubscriptionPurchaseToAppStoreSubscriptionDTO(
|
||||||
// TODO: Should this always be present or just for TransactionType of
|
// TODO: Should this always be present or just for TransactionType of
|
||||||
// "Auto-Renewable Subscription"? See https://developer.apple.com/forums/thread/705730
|
// "Auto-Renewable Subscription"? See https://developer.apple.com/forums/thread/705730
|
||||||
...(purchase.expiresDate && { expiry_time_millis: purchase.expiresDate }),
|
...(purchase.expiresDate && { expiry_time_millis: purchase.expiresDate }),
|
||||||
...(purchase.hasOwnProperty('isInBillingRetry') && {
|
...('isInBillingRetry' in purchase && {
|
||||||
is_in_billing_retry_period: purchase.isInBillingRetry,
|
is_in_billing_retry_period: purchase.isInBillingRetry,
|
||||||
}),
|
}),
|
||||||
price_id: purchase.price_id,
|
price_id: purchase.price_id,
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
import { CollectionReference, Firestore } from '@google-cloud/firestore';
|
import { CollectionReference, Firestore } from '@google-cloud/firestore';
|
||||||
import { ACTIVE_SUBSCRIPTION_STATUSES } from 'fxa-shared/subscriptions/stripe';
|
|
||||||
import { StripeFirestore as StripeFirestoreBase } from 'fxa-shared/payments/stripe-firestore';
|
import { StripeFirestore as StripeFirestoreBase } from 'fxa-shared/payments/stripe-firestore';
|
||||||
import { Stripe } from 'stripe';
|
import { Stripe } from 'stripe';
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,6 @@ import { AppStoreSubscriptionPurchase } from './iap/apple-app-store/subscription
|
||||||
import { PlayStoreSubscriptionPurchase } from './iap/google-play/subscription-purchase';
|
import { PlayStoreSubscriptionPurchase } from './iap/google-play/subscription-purchase';
|
||||||
import { getIapPurchaseType } from './iap/iap-config';
|
import { getIapPurchaseType } from './iap/iap-config';
|
||||||
import { FirestoreStripeError, StripeFirestore } from './stripe-firestore';
|
import { FirestoreStripeError, StripeFirestore } from './stripe-firestore';
|
||||||
import { stripeInvoiceToFirstInvoicePreviewDTO } from './stripe-formatter';
|
|
||||||
import { generateIdempotencyKey } from './utils';
|
import { generateIdempotencyKey } from './utils';
|
||||||
|
|
||||||
// Maintains backwards compatibility. Some type defs hoisted to fxa-shared/payments/stripe
|
// Maintains backwards compatibility. Some type defs hoisted to fxa-shared/payments/stripe
|
||||||
|
@ -129,7 +128,7 @@ export type BillingAddressOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PaymentBillingDetails = Awaited<
|
export type PaymentBillingDetails = Awaited<
|
||||||
ReturnType<StripeHelper['extractBillingDetails']>
|
ReturnType<StripeHelper['extractBillingDetails']> // eslint-disable-line no-use-before-define
|
||||||
> & {
|
> & {
|
||||||
paypal_payment_error?: PaypalPaymentError;
|
paypal_payment_error?: PaypalPaymentError;
|
||||||
billing_agreement_id?: string;
|
billing_agreement_id?: string;
|
||||||
|
@ -2633,7 +2632,7 @@ export class StripeHelper extends StripeHelperBase {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
let formattedSubscriptions = [];
|
const formattedSubscriptions = [];
|
||||||
|
|
||||||
for (const subscription of customer.subscriptions.data) {
|
for (const subscription of customer.subscriptions.data) {
|
||||||
if (ACTIVE_SUBSCRIPTION_STATUSES.includes(subscription.status)) {
|
if (ACTIVE_SUBSCRIPTION_STATUSES.includes(subscription.status)) {
|
||||||
|
@ -3181,13 +3180,20 @@ export class StripeHelper extends StripeHelperBase {
|
||||||
* Process a webhook event from Stripe and if needed, save it to Firestore.
|
* Process a webhook event from Stripe and if needed, save it to Firestore.
|
||||||
*/
|
*/
|
||||||
async processWebhookEventToFirestore(event: Stripe.Event) {
|
async processWebhookEventToFirestore(event: Stripe.Event) {
|
||||||
const { type, data } = event;
|
const { type } = event;
|
||||||
|
|
||||||
|
// Stripe does not include the card_automatically_updated event
|
||||||
|
// despite this being a valid event for Stripe webhook registration
|
||||||
|
type StripeEnabledEvent =
|
||||||
|
| Stripe.WebhookEndpointUpdateParams.EnabledEvent
|
||||||
|
| 'payment_method.card_automatically_updated';
|
||||||
|
|
||||||
// Note that we must insert before any event handled by the general
|
// Note that we must insert before any event handled by the general
|
||||||
// webhook code to ensure the object is up to date in Firestore before
|
// webhook code to ensure the object is up to date in Firestore before
|
||||||
// our code handles the event.
|
// our code handles the event.
|
||||||
let handled = true;
|
let handled = true;
|
||||||
try {
|
try {
|
||||||
switch (type as Stripe.WebhookEndpointUpdateParams.EnabledEvent) {
|
switch (type as StripeEnabledEvent) {
|
||||||
case 'invoice.created':
|
case 'invoice.created':
|
||||||
case 'invoice.finalized':
|
case 'invoice.finalized':
|
||||||
case 'invoice.paid':
|
case 'invoice.paid':
|
||||||
|
@ -3207,7 +3213,6 @@ export class StripeHelper extends StripeHelperBase {
|
||||||
await this.processSubscriptionEventToFirestore(event);
|
await this.processSubscriptionEventToFirestore(event);
|
||||||
break;
|
break;
|
||||||
case 'payment_method.attached':
|
case 'payment_method.attached':
|
||||||
// @ts-ignore
|
|
||||||
case 'payment_method.card_automatically_updated':
|
case 'payment_method.card_automatically_updated':
|
||||||
case 'payment_method.updated':
|
case 'payment_method.updated':
|
||||||
await this.processPaymentMethodEventToFirestore(event);
|
await this.processPaymentMethodEventToFirestore(event);
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
export function generateIdempotencyKey(params: string[]) {
|
export function generateIdempotencyKey(params: string[]) {
|
||||||
let sha = createHash('sha256');
|
const sha = createHash('sha256');
|
||||||
sha.update(params.join(''));
|
sha.update(params.join(''));
|
||||||
return sha.digest('base64url');
|
return sha.digest('base64url');
|
||||||
}
|
}
|
||||||
|
|
|
@ -463,7 +463,7 @@ export class AccountHandler {
|
||||||
|
|
||||||
this.setMetricsFlowCompleteSignal(request, service);
|
this.setMetricsFlowCompleteSignal(request, service);
|
||||||
|
|
||||||
let { account, password } = await this.createAccount({
|
const { account, password } = await this.createAccount({
|
||||||
authPW,
|
authPW,
|
||||||
authSalt,
|
authSalt,
|
||||||
email,
|
email,
|
||||||
|
@ -613,7 +613,6 @@ export class AccountHandler {
|
||||||
form.uid = payload.uid;
|
form.uid = payload.uid;
|
||||||
const account = await this.db.account(uid);
|
const account = await this.db.account(uid);
|
||||||
await this.setPasswordOnStubAccount(account, authPW);
|
await this.setPasswordOnStubAccount(account, authPW);
|
||||||
const metricsContext = await request.gatherMetricsContext({});
|
|
||||||
await this.signupUtils.verifyAccount(request, account, {});
|
await this.signupUtils.verifyAccount(request, account, {});
|
||||||
const sessionToken = await this.createSessionToken({
|
const sessionToken = await this.createSessionToken({
|
||||||
account,
|
account,
|
||||||
|
@ -635,7 +634,7 @@ export class AccountHandler {
|
||||||
|
|
||||||
// if it errored out after verifiying the account
|
// if it errored out after verifiying the account
|
||||||
// remove the uid from the list of accounts to send reminders to.
|
// remove the uid from the list of accounts to send reminders to.
|
||||||
if (!!uid) {
|
if (uid) {
|
||||||
const account = await this.db.account(uid);
|
const account = await this.db.account(uid);
|
||||||
if (account.verifierSetAt > 0) {
|
if (account.verifierSetAt > 0) {
|
||||||
await this.subscriptionAccountReminders.delete(uid);
|
await this.subscriptionAccountReminders.delete(uid);
|
||||||
|
@ -844,6 +843,87 @@ export class AccountHandler {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const forceTokenVerification = (request: AuthRequest, account: any) => {
|
||||||
|
// If there was anything suspicious about the request,
|
||||||
|
// we should force token verification.
|
||||||
|
if (request.app.isSuspiciousRequest) {
|
||||||
|
return 'suspect';
|
||||||
|
}
|
||||||
|
if (this.config.signinConfirmation?.forceGlobally) {
|
||||||
|
return 'global';
|
||||||
|
}
|
||||||
|
// If it's an email address used for testing etc,
|
||||||
|
// we should force token verification.
|
||||||
|
if (
|
||||||
|
this.config.signinConfirmation?.forcedEmailAddresses?.test(
|
||||||
|
account.primaryEmail.email
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return 'email';
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const skipTokenVerification = (request: AuthRequest, account: any) => {
|
||||||
|
// If they're logging in from an IP address on which they recently did
|
||||||
|
// another, successfully-verified login, then we can consider this one
|
||||||
|
// verified as well without going through the loop again.
|
||||||
|
const allowedRecency =
|
||||||
|
this.config.securityHistory.ipProfiling.allowedRecency || 0;
|
||||||
|
if (securityEventVerified && securityEventRecency < allowedRecency) {
|
||||||
|
this.log.info('Account.ipprofiling.seenAddress', {
|
||||||
|
uid: account.uid,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the account was recently created, don't make the user
|
||||||
|
// confirm sign-in for a configurable amount of time. This will reduce
|
||||||
|
// the friction of a user adding a second device.
|
||||||
|
const skipForNewAccounts =
|
||||||
|
this.config.signinConfirmation.skipForNewAccounts;
|
||||||
|
if (skipForNewAccounts?.enabled) {
|
||||||
|
const accountAge = requestNow - account.createdAt;
|
||||||
|
if (accountAge <= (skipForNewAccounts.maxAge as unknown as number)) {
|
||||||
|
this.log.info('account.signin.confirm.bypass.age', {
|
||||||
|
uid: account.uid,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certain accounts have the ability to *always* skip sign-in confirmation
|
||||||
|
// regardless of account age or device. This is for internal use where we need
|
||||||
|
// to guarantee the login experience.
|
||||||
|
const lowerCaseEmail = account.primaryEmail.normalizedEmail.toLowerCase();
|
||||||
|
const alwaysSkip =
|
||||||
|
this.skipConfirmationForEmailAddresses?.includes(lowerCaseEmail);
|
||||||
|
if (alwaysSkip) {
|
||||||
|
this.log.info('account.signin.confirm.bypass.always', {
|
||||||
|
uid: account.uid,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const forcePasswordChange = (account: any) => {
|
||||||
|
// If it's an email address used for testing etc,
|
||||||
|
// we should force password change.
|
||||||
|
if (
|
||||||
|
this.config.forcePasswordChange?.forcedEmailAddresses?.test(
|
||||||
|
account.primaryEmail.email
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otw only force if account lockAt flag set
|
||||||
|
return accountRecord.lockedAt > 0;
|
||||||
|
};
|
||||||
|
|
||||||
const createSessionToken = async () => {
|
const createSessionToken = async () => {
|
||||||
// All sessions are considered unverified by default.
|
// All sessions are considered unverified by default.
|
||||||
let needsVerificationId = true;
|
let needsVerificationId = true;
|
||||||
|
@ -922,87 +1002,6 @@ export class AccountHandler {
|
||||||
sessionToken = await this.db.createSessionToken(sessionTokenOptions);
|
sessionToken = await this.db.createSessionToken(sessionTokenOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
const forcePasswordChange = (account: any) => {
|
|
||||||
// If it's an email address used for testing etc,
|
|
||||||
// we should force password change.
|
|
||||||
if (
|
|
||||||
this.config.forcePasswordChange?.forcedEmailAddresses?.test(
|
|
||||||
account.primaryEmail.email
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// otw only force if account lockAt flag set
|
|
||||||
return accountRecord.lockedAt > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const forceTokenVerification = (request: AuthRequest, account: any) => {
|
|
||||||
// If there was anything suspicious about the request,
|
|
||||||
// we should force token verification.
|
|
||||||
if (request.app.isSuspiciousRequest) {
|
|
||||||
return 'suspect';
|
|
||||||
}
|
|
||||||
if (this.config.signinConfirmation?.forceGlobally) {
|
|
||||||
return 'global';
|
|
||||||
}
|
|
||||||
// If it's an email address used for testing etc,
|
|
||||||
// we should force token verification.
|
|
||||||
if (
|
|
||||||
this.config.signinConfirmation?.forcedEmailAddresses?.test(
|
|
||||||
account.primaryEmail.email
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return 'email';
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const skipTokenVerification = (request: AuthRequest, account: any) => {
|
|
||||||
// If they're logging in from an IP address on which they recently did
|
|
||||||
// another, successfully-verified login, then we can consider this one
|
|
||||||
// verified as well without going through the loop again.
|
|
||||||
const allowedRecency =
|
|
||||||
this.config.securityHistory.ipProfiling.allowedRecency || 0;
|
|
||||||
if (securityEventVerified && securityEventRecency < allowedRecency) {
|
|
||||||
this.log.info('Account.ipprofiling.seenAddress', {
|
|
||||||
uid: account.uid,
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the account was recently created, don't make the user
|
|
||||||
// confirm sign-in for a configurable amount of time. This will reduce
|
|
||||||
// the friction of a user adding a second device.
|
|
||||||
const skipForNewAccounts =
|
|
||||||
this.config.signinConfirmation.skipForNewAccounts;
|
|
||||||
if (skipForNewAccounts?.enabled) {
|
|
||||||
const accountAge = requestNow - account.createdAt;
|
|
||||||
if (accountAge <= (skipForNewAccounts.maxAge as unknown as number)) {
|
|
||||||
this.log.info('account.signin.confirm.bypass.age', {
|
|
||||||
uid: account.uid,
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Certain accounts have the ability to *always* skip sign-in confirmation
|
|
||||||
// regardless of account age or device. This is for internal use where we need
|
|
||||||
// to guarantee the login experience.
|
|
||||||
const lowerCaseEmail = account.primaryEmail.normalizedEmail.toLowerCase();
|
|
||||||
const alwaysSkip =
|
|
||||||
this.skipConfirmationForEmailAddresses?.includes(lowerCaseEmail);
|
|
||||||
if (alwaysSkip) {
|
|
||||||
this.log.info('account.signin.confirm.bypass.always', {
|
|
||||||
uid: account.uid,
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendSigninNotifications = async () => {
|
const sendSigninNotifications = async () => {
|
||||||
await this.signinUtils.sendSigninNotifications(
|
await this.signinUtils.sendSigninNotifications(
|
||||||
request,
|
request,
|
||||||
|
|
|
@ -30,7 +30,7 @@ export class LinkedAccountHandler {
|
||||||
private db: any,
|
private db: any,
|
||||||
private config: ConfigType,
|
private config: ConfigType,
|
||||||
private mailer: any,
|
private mailer: any,
|
||||||
private profile: ProfileClient,
|
private profile: ProfileClient
|
||||||
) {
|
) {
|
||||||
if (config.googleAuthConfig && config.googleAuthConfig.clientId) {
|
if (config.googleAuthConfig && config.googleAuthConfig.clientId) {
|
||||||
this.googleAuthClient = new OAuth2Client(
|
this.googleAuthClient = new OAuth2Client(
|
||||||
|
@ -163,7 +163,10 @@ export class LinkedAccountHandler {
|
||||||
const name = idToken.name;
|
const name = idToken.name;
|
||||||
|
|
||||||
let accountRecord;
|
let accountRecord;
|
||||||
let linkedAccountRecord = await this.db.getLinkedAccount(userid, provider);
|
const linkedAccountRecord = await this.db.getLinkedAccount(
|
||||||
|
userid,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
|
||||||
if (!linkedAccountRecord) {
|
if (!linkedAccountRecord) {
|
||||||
try {
|
try {
|
||||||
|
@ -303,7 +306,7 @@ export const linkedAccountRoutes = (
|
||||||
db: any,
|
db: any,
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
mailer: any,
|
mailer: any,
|
||||||
profile: ProfileClient,
|
profile: ProfileClient
|
||||||
) => {
|
) => {
|
||||||
const handler = new LinkedAccountHandler(log, db, config, mailer, profile);
|
const handler = new LinkedAccountHandler(log, db, config, mailer, profile);
|
||||||
|
|
||||||
|
|
|
@ -19,79 +19,6 @@ import { AuthLogger, AuthRequest } from '../../types';
|
||||||
import validators from '../validators';
|
import validators from '../validators';
|
||||||
import { handleAuth } from './utils';
|
import { handleAuth } from './utils';
|
||||||
|
|
||||||
export const mozillaSubscriptionRoutes = ({
|
|
||||||
log,
|
|
||||||
db,
|
|
||||||
customs,
|
|
||||||
stripeHelper,
|
|
||||||
playSubscriptions,
|
|
||||||
appStoreSubscriptions,
|
|
||||||
capabilityService,
|
|
||||||
}: {
|
|
||||||
log: AuthLogger;
|
|
||||||
db: any;
|
|
||||||
customs: any;
|
|
||||||
stripeHelper: StripeHelper;
|
|
||||||
playSubscriptions?: PlaySubscriptions;
|
|
||||||
appStoreSubscriptions?: AppStoreSubscriptions;
|
|
||||||
capabilityService?: CapabilityService;
|
|
||||||
}): ServerRoute[] => {
|
|
||||||
if (!playSubscriptions) {
|
|
||||||
playSubscriptions = Container.get(PlaySubscriptions);
|
|
||||||
}
|
|
||||||
if (!appStoreSubscriptions) {
|
|
||||||
appStoreSubscriptions = Container.get(AppStoreSubscriptions);
|
|
||||||
}
|
|
||||||
if (!capabilityService) {
|
|
||||||
capabilityService = Container.get(CapabilityService);
|
|
||||||
}
|
|
||||||
const mozillaSubscriptionHandler = new MozillaSubscriptionHandler(
|
|
||||||
log,
|
|
||||||
db,
|
|
||||||
customs,
|
|
||||||
stripeHelper,
|
|
||||||
playSubscriptions,
|
|
||||||
appStoreSubscriptions,
|
|
||||||
capabilityService
|
|
||||||
);
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
path: '/oauth/mozilla-subscriptions/customer/billing-and-subscriptions',
|
|
||||||
options: {
|
|
||||||
...SUBSCRIPTIONS_DOCS.OAUTH_MOZILLA_SUBSCRIPTIONS_CUSTOMER_BILLING_AND_SUBSCRIPTIONS_GET,
|
|
||||||
auth: {
|
|
||||||
payload: false,
|
|
||||||
strategy: 'oauthToken',
|
|
||||||
},
|
|
||||||
response: {
|
|
||||||
schema: validators.subscriptionsMozillaSubscriptionsValidator as any,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
handler: (request: AuthRequest) =>
|
|
||||||
mozillaSubscriptionHandler.getBillingDetailsAndSubscriptions(request),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
path: '/oauth/mozilla-subscriptions/customer/plan-eligibility/{planId}',
|
|
||||||
options: {
|
|
||||||
...SUBSCRIPTIONS_DOCS.OAUTH_MOZILLA_SUBSCRIPTIONS_CUSTOMER_PLAN_ELIGIBILITY,
|
|
||||||
auth: {
|
|
||||||
payload: false,
|
|
||||||
strategy: 'oauthToken',
|
|
||||||
},
|
|
||||||
validate: {
|
|
||||||
params: {
|
|
||||||
planId: validators.subscriptionsPlanId.required(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
handler: (request: AuthRequest) =>
|
|
||||||
mozillaSubscriptionHandler.getPlanEligibility(request),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export class MozillaSubscriptionHandler {
|
export class MozillaSubscriptionHandler {
|
||||||
constructor(
|
constructor(
|
||||||
protected log: AuthLogger,
|
protected log: AuthLogger,
|
||||||
|
@ -173,3 +100,76 @@ export class MozillaSubscriptionHandler {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mozillaSubscriptionRoutes = ({
|
||||||
|
log,
|
||||||
|
db,
|
||||||
|
customs,
|
||||||
|
stripeHelper,
|
||||||
|
playSubscriptions,
|
||||||
|
appStoreSubscriptions,
|
||||||
|
capabilityService,
|
||||||
|
}: {
|
||||||
|
log: AuthLogger;
|
||||||
|
db: any;
|
||||||
|
customs: any;
|
||||||
|
stripeHelper: StripeHelper;
|
||||||
|
playSubscriptions?: PlaySubscriptions;
|
||||||
|
appStoreSubscriptions?: AppStoreSubscriptions;
|
||||||
|
capabilityService?: CapabilityService;
|
||||||
|
}): ServerRoute[] => {
|
||||||
|
if (!playSubscriptions) {
|
||||||
|
playSubscriptions = Container.get(PlaySubscriptions);
|
||||||
|
}
|
||||||
|
if (!appStoreSubscriptions) {
|
||||||
|
appStoreSubscriptions = Container.get(AppStoreSubscriptions);
|
||||||
|
}
|
||||||
|
if (!capabilityService) {
|
||||||
|
capabilityService = Container.get(CapabilityService);
|
||||||
|
}
|
||||||
|
const mozillaSubscriptionHandler = new MozillaSubscriptionHandler(
|
||||||
|
log,
|
||||||
|
db,
|
||||||
|
customs,
|
||||||
|
stripeHelper,
|
||||||
|
playSubscriptions,
|
||||||
|
appStoreSubscriptions,
|
||||||
|
capabilityService
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
path: '/oauth/mozilla-subscriptions/customer/billing-and-subscriptions',
|
||||||
|
options: {
|
||||||
|
...SUBSCRIPTIONS_DOCS.OAUTH_MOZILLA_SUBSCRIPTIONS_CUSTOMER_BILLING_AND_SUBSCRIPTIONS_GET,
|
||||||
|
auth: {
|
||||||
|
payload: false,
|
||||||
|
strategy: 'oauthToken',
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
schema: validators.subscriptionsMozillaSubscriptionsValidator as any,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: (request: AuthRequest) =>
|
||||||
|
mozillaSubscriptionHandler.getBillingDetailsAndSubscriptions(request),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
path: '/oauth/mozilla-subscriptions/customer/plan-eligibility/{planId}',
|
||||||
|
options: {
|
||||||
|
...SUBSCRIPTIONS_DOCS.OAUTH_MOZILLA_SUBSCRIPTIONS_CUSTOMER_PLAN_ELIGIBILITY,
|
||||||
|
auth: {
|
||||||
|
payload: false,
|
||||||
|
strategy: 'oauthToken',
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
params: {
|
||||||
|
planId: validators.subscriptionsPlanId.required(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: (request: AuthRequest) =>
|
||||||
|
mozillaSubscriptionHandler.getPlanEligibility(request),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
|
@ -86,7 +86,7 @@ export class PayPalNotificationHandler extends PayPalHandler {
|
||||||
|
|
||||||
if (invoice.status == null || !['draft', 'open'].includes(invoice.status)) {
|
if (invoice.status == null || !['draft', 'open'].includes(invoice.status)) {
|
||||||
if (
|
if (
|
||||||
invoice.status == 'uncollectible' &&
|
invoice.status === 'uncollectible' &&
|
||||||
['Completed', 'Processed'].includes(message.payment_status)
|
['Completed', 'Processed'].includes(message.payment_status)
|
||||||
) {
|
) {
|
||||||
// we need to refund the user since the invoice was cancelled
|
// we need to refund the user since the invoice was cancelled
|
||||||
|
@ -108,7 +108,7 @@ export class PayPalNotificationHandler extends PayPalHandler {
|
||||||
case 'Failed':
|
case 'Failed':
|
||||||
case 'Voided':
|
case 'Voided':
|
||||||
case 'Expired':
|
case 'Expired':
|
||||||
if (message.custom.length == 0) {
|
if (message.custom.length === 0) {
|
||||||
this.log.error('handleMerchPayment', {
|
this.log.error('handleMerchPayment', {
|
||||||
message: 'No idempotency key on PayPal transaction',
|
message: 'No idempotency key on PayPal transaction',
|
||||||
ipnMessage: message,
|
ipnMessage: message,
|
||||||
|
|
|
@ -97,7 +97,7 @@ export class PayPalHandler extends StripeWebhookHandler {
|
||||||
try {
|
try {
|
||||||
await this.customs.check(request, email, 'createSubscriptionWithPaypal');
|
await this.customs.check(request, email, 'createSubscriptionWithPaypal');
|
||||||
|
|
||||||
let customer = await this.stripeHelper.fetchCustomer(uid, [
|
const customer = await this.stripeHelper.fetchCustomer(uid, [
|
||||||
'subscriptions',
|
'subscriptions',
|
||||||
'tax',
|
'tax',
|
||||||
]);
|
]);
|
||||||
|
@ -135,7 +135,7 @@ export class PayPalHandler extends StripeWebhookHandler {
|
||||||
throw error.billingAgreementExists(customer.id);
|
throw error.billingAgreementExists(customer.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sourceCountry, subscription } = !!token
|
const { sourceCountry, subscription } = token
|
||||||
? await this._createPaypalBillingAgreementAndSubscription({
|
? await this._createPaypalBillingAgreementAndSubscription({
|
||||||
request,
|
request,
|
||||||
uid,
|
uid,
|
||||||
|
@ -370,7 +370,7 @@ export class PayPalHandler extends StripeWebhookHandler {
|
||||||
const { uid, email } = await handleAuth(this.db, request.auth, true);
|
const { uid, email } = await handleAuth(this.db, request.auth, true);
|
||||||
await this.customs.check(request, email, 'updatePaypalBillingAgreement');
|
await this.customs.check(request, email, 'updatePaypalBillingAgreement');
|
||||||
|
|
||||||
let customer = await this.stripeHelper.fetchCustomer(uid, [
|
const customer = await this.stripeHelper.fetchCustomer(uid, [
|
||||||
'subscriptions',
|
'subscriptions',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -476,7 +476,7 @@ export class PayPalHandler extends StripeWebhookHandler {
|
||||||
// copy bill to address information to Customer
|
// copy bill to address information to Customer
|
||||||
const accountCustomer = await getAccountCustomerByUid(uid);
|
const accountCustomer = await getAccountCustomerByUid(uid);
|
||||||
if (accountCustomer.stripeCustomerId) {
|
if (accountCustomer.stripeCustomerId) {
|
||||||
let locationDetails = {} as any;
|
const locationDetails = {} as any;
|
||||||
if (agreementDetails.countryCode === options.location?.countryCode) {
|
if (agreementDetails.countryCode === options.location?.countryCode) {
|
||||||
// Record the state (short name) if needed
|
// Record the state (short name) if needed
|
||||||
const state = options.location?.state;
|
const state = options.location?.state;
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
reportValidationError,
|
reportValidationError,
|
||||||
} from '../../../lib/sentry';
|
} from '../../../lib/sentry';
|
||||||
import error from '../../error';
|
import error from '../../error';
|
||||||
import { CapabilityService } from '../../payments/capability';
|
|
||||||
import { PayPalHelper, RefusedError } from '../../payments/paypal';
|
import { PayPalHelper, RefusedError } from '../../payments/paypal';
|
||||||
import {
|
import {
|
||||||
CUSTOMER_RESOURCE,
|
CUSTOMER_RESOURCE,
|
||||||
|
@ -792,7 +791,7 @@ export class StripeWebhookHandler extends StripeHandler {
|
||||||
(plan) => (plan.product as Stripe.Product).id !== product.id
|
(plan) => (plan.product as Stripe.Product).id !== product.id
|
||||||
);
|
);
|
||||||
const latestStripePlansForProduct = latestStripePlans.filter(
|
const latestStripePlansForProduct = latestStripePlans.filter(
|
||||||
(plan) => (plan.product as Stripe.Product).id == product.id
|
(plan) => (plan.product as Stripe.Product).id === product.id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (event.type !== 'product.deleted') {
|
if (event.type !== 'product.deleted') {
|
||||||
|
|
|
@ -283,7 +283,7 @@ export class StripeHandler {
|
||||||
const customer = await this.stripeHelper.fetchCustomer(uid);
|
const customer = await this.stripeHelper.fetchCustomer(uid);
|
||||||
const planCurrency = (await this.stripeHelper.findAbbrevPlanById(planId))
|
const planCurrency = (await this.stripeHelper.findAbbrevPlanById(planId))
|
||||||
.currency;
|
.currency;
|
||||||
if (customer && customer.currency != planCurrency) {
|
if (customer && customer.currency !== planCurrency) {
|
||||||
throw error.currencyCurrencyMismatch(customer.currency, planCurrency);
|
throw error.currencyCurrencyMismatch(customer.currency, planCurrency);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,37 @@ import { StripeHelper } from '../payments/stripe';
|
||||||
import { AuthLogger, AuthRequest } from '../types';
|
import { AuthLogger, AuthRequest } from '../types';
|
||||||
import validators from './validators';
|
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 = ({
|
export const supportPanelRoutes = ({
|
||||||
log,
|
log,
|
||||||
config,
|
config,
|
||||||
|
@ -74,36 +105,5 @@ export const supportPanelRoutes = ({
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = supportPanelRoutes;
|
module.exports = supportPanelRoutes;
|
||||||
module.exports.supportPanelRoutes = supportPanelRoutes;
|
module.exports.supportPanelRoutes = supportPanelRoutes;
|
||||||
|
|
|
@ -48,7 +48,7 @@ export function transformMjIncludeTags(mjml: string): string {
|
||||||
function extractMjIncludeTags(mjml: string): MjIncludeTag[] {
|
function extractMjIncludeTags(mjml: string): MjIncludeTag[] {
|
||||||
let chomp = false;
|
let chomp = false;
|
||||||
let include = '';
|
let include = '';
|
||||||
let includes = [];
|
const includes = [];
|
||||||
mjml
|
mjml
|
||||||
.replace(/<mj-include/g, ' <mj-include')
|
.replace(/<mj-include/g, ' <mj-include')
|
||||||
.split(/\n|\s/g)
|
.split(/\n|\s/g)
|
||||||
|
@ -102,7 +102,7 @@ function parseMjIncludeTag(include: string): MjIncludeTag {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMjStyle(tag: MjIncludeTag) {
|
function toMjStyle(tag: MjIncludeTag) {
|
||||||
let { inline, path, type } = tag;
|
const { inline, path, type } = tag;
|
||||||
|
|
||||||
if (type !== 'css') return '';
|
if (type !== 'css') return '';
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
// NOTE: This file handled with browser ESLint bindings
|
||||||
|
// instead of NodeJS for DOM typings support
|
||||||
|
/* eslint-env browser */
|
||||||
|
|
||||||
import { Story } from '@storybook/html';
|
import { Story } from '@storybook/html';
|
||||||
import Renderer from '../renderer';
|
import Renderer from '../renderer';
|
||||||
import { BrowserRendererBindings } from '../renderer/bindings-browser';
|
import { BrowserRendererBindings } from '../renderer/bindings-browser';
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
// NOTE: This file handled with browser ESLint bindings
|
||||||
|
// instead of NodeJS for DOM typings support
|
||||||
|
/* eslint-env browser */
|
||||||
|
|
||||||
import { RendererBindings, RendererOpts, TemplateContext } from './bindings';
|
import { RendererBindings, RendererOpts, TemplateContext } from './bindings';
|
||||||
|
|
||||||
// When rendering templates in storybook, use the mjml-browser implementation
|
// When rendering templates in storybook, use the mjml-browser implementation
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
// NOTE: This file handled with browser ESLint bindings
|
||||||
|
// instead of NodeJS for DOM typings support
|
||||||
|
/* eslint-env browser */
|
||||||
|
|
||||||
import { ILocalizerBindings } from '../../l10n/interfaces/ILocalizerBindings';
|
import { ILocalizerBindings } from '../../l10n/interfaces/ILocalizerBindings';
|
||||||
import { LocalizerOpts } from '../../l10n/models/LocalizerOpts';
|
import { LocalizerOpts } from '../../l10n/models/LocalizerOpts';
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,7 @@
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import { DOMLocalization, Localization } from '@fluent/dom';
|
import { DOMLocalization, Localization } from '@fluent/dom';
|
||||||
import {
|
import { RendererBindings, TemplateContext, RendererContext } from './bindings';
|
||||||
RendererBindings,
|
|
||||||
TemplateContext,
|
|
||||||
RendererContext,
|
|
||||||
TemplateValues,
|
|
||||||
} from './bindings';
|
|
||||||
import Localizer, { FtlIdMsg } from '../../l10n';
|
import Localizer, { FtlIdMsg } from '../../l10n';
|
||||||
|
|
||||||
const RTL_LOCALES = [
|
const RTL_LOCALES = [
|
||||||
|
@ -135,9 +130,13 @@ class Renderer extends Localizer {
|
||||||
): Promise<GlobalTemplateValues> {
|
): Promise<GlobalTemplateValues> {
|
||||||
// We must use 'require' here, 'import' causes an 'unknown file extension .ts'
|
// We must use 'require' here, 'import' causes an 'unknown file extension .ts'
|
||||||
// error. Might be a config option to make it work?
|
// error. Might be a config option to make it work?
|
||||||
|
// eslint-disable-next-line no-useless-catch
|
||||||
try {
|
try {
|
||||||
// make this a switch statement on 'template' if more cases arise?
|
// make this a switch statement on 'template' if more cases arise?
|
||||||
if (context.template === 'lowRecoveryCodes' || context.template === 'postConsumeRecoveryCode') {
|
if (
|
||||||
|
context.template === 'lowRecoveryCodes' ||
|
||||||
|
context.template === 'postConsumeRecoveryCode'
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
await require(`../emails/templates/${context.template}/includes`)
|
await require(`../emails/templates/${context.template}/includes`)
|
||||||
).getIncludes(context.numberRemaining);
|
).getIncludes(context.numberRemaining);
|
||||||
|
@ -159,7 +158,7 @@ class Renderer extends Localizer {
|
||||||
const ftlContext = flattenNestedObjects(context);
|
const ftlContext = flattenNestedObjects(context);
|
||||||
|
|
||||||
const plainTextArr = text.split('\n');
|
const plainTextArr = text.split('\n');
|
||||||
for (let i in plainTextArr) {
|
for (const i in plainTextArr) {
|
||||||
// match the lines that are of format key = "value" since we will be extracting the key
|
// match the lines that are of format key = "value" since we will be extracting the key
|
||||||
// to pass down to fluent
|
// to pass down to fluent
|
||||||
const { key, val } = splitPlainTextLine(plainTextArr[i]);
|
const { key, val } = splitPlainTextLine(plainTextArr[i]);
|
||||||
|
|
|
@ -15,18 +15,6 @@ import { ConfigType } from '../config';
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface AuthLogger extends Logger {
|
|
||||||
(tags: string | string[], data?: string | object): void;
|
|
||||||
|
|
||||||
begin(location: string, request: AuthRequest): void;
|
|
||||||
|
|
||||||
notifyAttachedServices(
|
|
||||||
serviceName: string,
|
|
||||||
request: AuthRequest,
|
|
||||||
data: Record<string, any>
|
|
||||||
): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthApp extends RequestApplicationState {
|
export interface AuthApp extends RequestApplicationState {
|
||||||
devices: Promise<any>;
|
devices: Promise<any>;
|
||||||
locale: String;
|
locale: String;
|
||||||
|
@ -56,6 +44,7 @@ export interface AuthApp extends RequestApplicationState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthRequest extends Request {
|
export interface AuthRequest extends Request {
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
log: AuthLogger;
|
log: AuthLogger;
|
||||||
app: AuthApp;
|
app: AuthApp;
|
||||||
validateMetricsContext: any;
|
validateMetricsContext: any;
|
||||||
|
@ -71,8 +60,22 @@ export interface ProfileClient {
|
||||||
updateDisplayName(uid: string, name: string): Promise<void>;
|
updateDisplayName(uid: string, name: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuthLogger extends Logger {
|
||||||
|
(tags: string | string[], data?: string | object): void;
|
||||||
|
|
||||||
|
begin(location: string, request: AuthRequest): void;
|
||||||
|
|
||||||
|
notifyAttachedServices(
|
||||||
|
serviceName: string,
|
||||||
|
request: AuthRequest,
|
||||||
|
data: Record<string, any>
|
||||||
|
): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
// Container token types
|
// Container token types
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||||
export const AuthLogger = new Token<AuthLogger>('AUTH_LOGGER');
|
export const AuthLogger = new Token<AuthLogger>('AUTH_LOGGER');
|
||||||
export const AuthFirestore = new Token<Firestore>('AUTH_FIRESTORE');
|
export const AuthFirestore = new Token<Firestore>('AUTH_FIRESTORE');
|
||||||
export const AppConfig = new Token<ConfigType>('APP_CONFIG');
|
export const AppConfig = new Token<ConfigType>('APP_CONFIG');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||||
export const ProfileClient = new Token<ProfileClient>('PROFILE_CLIENT');
|
export const ProfileClient = new Token<ProfileClient>('PROFILE_CLIENT');
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
"create-mock-iap": "NODE_ENV=dev FIRESTORE_EMULATOR_HOST=localhost:9090 node -r esbuild-register ./scripts/create-mock-iap-subscriptions.ts",
|
"create-mock-iap": "NODE_ENV=dev FIRESTORE_EMULATOR_HOST=localhost:9090 node -r esbuild-register ./scripts/create-mock-iap-subscriptions.ts",
|
||||||
"bump-template-versions": "node scripts/template-version-bump",
|
"bump-template-versions": "node scripts/template-version-bump",
|
||||||
"audit": "npm audit --json | audit-filter --nsp-config=.nsprc --audit=-",
|
"audit": "npm audit --json | audit-filter --nsp-config=.nsprc --audit=-",
|
||||||
"lint": "eslint . .storybook --ignore-pattern 'dist'",
|
"lint": "eslint . .storybook --ignore-pattern 'dist' --ext .js,.ts",
|
||||||
"postinstall": "grunt merge-ftl && ../../_scripts/clone-l10n.sh fxa-auth-server",
|
"postinstall": "grunt merge-ftl && ../../_scripts/clone-l10n.sh fxa-auth-server",
|
||||||
"start": "yarn emails-scss && scripts/start-local.sh",
|
"start": "yarn emails-scss && scripts/start-local.sh",
|
||||||
"stop": "pm2 stop pm2.config.js",
|
"stop": "pm2 stop pm2.config.js",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import program from 'commander';
|
import program from 'commander';
|
||||||
import { StatsD } from 'hot-shots';
|
import { StatsD } from 'hot-shots';
|
||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import Redlock, { Lock, RedlockAbortSignal } from 'redlock';
|
import Redlock, { RedlockAbortSignal } from 'redlock';
|
||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
@ -90,6 +90,7 @@ export async function init() {
|
||||||
[program.lockName],
|
[program.lockName],
|
||||||
lockDuration,
|
lockDuration,
|
||||||
async (signal: RedlockAbortSignal) => {
|
async (signal: RedlockAbortSignal) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
for await (const _ of processor.processInvoices()) {
|
for await (const _ of processor.processInvoices()) {
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
throw signal.error;
|
throw signal.error;
|
||||||
|
@ -101,6 +102,7 @@ export async function init() {
|
||||||
throw new Error(`Cannot acquire lock to run: ${err.message}`);
|
throw new Error(`Cannot acquire lock to run: ${err.message}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
for await (const _ of processor.processInvoices()) {
|
for await (const _ of processor.processInvoices()) {
|
||||||
// no need to do anything between invoices since we are not extending a lock
|
// no need to do anything between invoices since we are not extending a lock
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { PayPalClient } from 'fxa-auth-server/lib/payments/paypal/client';
|
//import { PayPalClient } from 'fxa-auth-server/lib/payments/paypal/client';
|
||||||
import {
|
//import {
|
||||||
deleteAllPayPalBAs,
|
//deleteAllPayPalBAs,
|
||||||
getAllPayPalBAByUid,
|
//getAllPayPalBAByUid,
|
||||||
} from 'fxa-shared/db/models/auth';
|
//} from 'fxa-shared/db/models/auth';
|
||||||
import { Account } from 'fxa-shared/db/models/auth/account';
|
//import { Account } from 'fxa-shared/db/models/auth/account';
|
||||||
import Stripe from 'stripe';
|
//import Stripe from 'stripe';
|
||||||
import Container from 'typedi';
|
//import Container from 'typedi';
|
||||||
import { promisify } from 'util';
|
//import { promisify } from 'util';
|
||||||
import { StatsD } from 'hot-shots';
|
//import { StatsD } from 'hot-shots';
|
||||||
import { PayPalHelper } from '../lib/payments/paypal';
|
//import { PayPalHelper } from '../lib/payments/paypal';
|
||||||
import { setupProcessingTaskObjects } from '../lib/payments/processing-tasks-setup';
|
//import { setupProcessingTaskObjects } from '../lib/payments/processing-tasks-setup';
|
||||||
import { StripeHelper } from '../lib/payments/stripe';
|
//import { StripeHelper } from '../lib/payments/stripe';
|
||||||
import { reportSentryError } from '../lib/sentry';
|
//import { reportSentryError } from '../lib/sentry';
|
||||||
const config = require('../config').getProperties();
|
//const config = require('../config').getProperties();
|
||||||
|
|
||||||
// export async function retreiveUnverifiedAccounts(
|
// export async function retreiveUnverifiedAccounts(
|
||||||
// database: any
|
// database: any
|
||||||
|
|
|
@ -106,7 +106,7 @@ const handleEnglishPlan = (plan: Partial<Stripe.Plan>) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lang = findLocaleInTitle('en', plan.nickname!);
|
const lang = findLocaleInTitle('en', plan.nickname!);
|
||||||
|
|
||||||
if (lang === 'en') {
|
if (lang === 'en') {
|
||||||
// the plan's en strings are different than the product's, so we save
|
// the plan's en strings are different than the product's, so we save
|
||||||
|
|
|
@ -28,7 +28,6 @@ import {
|
||||||
} from './plan-language-tags-guesser';
|
} from './plan-language-tags-guesser';
|
||||||
import { promises as fsPromises, constants } from 'fs';
|
import { promises as fsPromises, constants } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { string } from 'joi';
|
|
||||||
|
|
||||||
const DEFAULT_LOCALE = 'en';
|
const DEFAULT_LOCALE = 'en';
|
||||||
|
|
||||||
|
@ -135,7 +134,7 @@ export class StripeProductsAndPlansConverter {
|
||||||
key.toLowerCase().startsWith('capabilities')
|
key.toLowerCase().startsWith('capabilities')
|
||||||
)) {
|
)) {
|
||||||
// Parse the key to determine if it's an 'all RP' or single RP capability
|
// Parse the key to determine if it's an 'all RP' or single RP capability
|
||||||
const [_, clientId] = oldKey.split(':');
|
const [, clientId] = oldKey.split(':');
|
||||||
const newKey = clientId ?? '*';
|
const newKey = clientId ?? '*';
|
||||||
capabilities[newKey] = commaSeparatedListToArray(
|
capabilities[newKey] = commaSeparatedListToArray(
|
||||||
stripeObject.metadata![oldKey]
|
stripeObject.metadata![oldKey]
|
||||||
|
|
|
@ -13,7 +13,7 @@ const through = require('through');
|
||||||
let clientCount = 2;
|
let clientCount = 2;
|
||||||
const pathStats = {};
|
const pathStats = {};
|
||||||
let requests = 0;
|
let requests = 0;
|
||||||
let pass = 0; // eslint-disable-line no-unused-vars
|
let pass = 0; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
let fail = 0;
|
let fail = 0;
|
||||||
let start = null;
|
let start = null;
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ server.stderr
|
||||||
}
|
}
|
||||||
requests++;
|
requests++;
|
||||||
if (json.code === 200) {
|
if (json.code === 200) {
|
||||||
pass++;
|
pass++; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
} else {
|
} else {
|
||||||
fail++;
|
fail++;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { Localization } from '@fluent/dom';
|
||||||
import chai, { assert } from 'chai';
|
import chai, { assert } from 'chai';
|
||||||
import chaiAsPromised from 'chai-as-promised';
|
import chaiAsPromised from 'chai-as-promised';
|
||||||
import Localizer from '../../../lib/l10n';
|
import Localizer from '../../../lib/l10n';
|
||||||
import { parseAcceptLanguage } from 'fxa-shared/l10n/parseAcceptLanguage';
|
|
||||||
import { LocalizerBindings } from '../../../lib/l10n/bindings';
|
import { LocalizerBindings } from '../../../lib/l10n/bindings';
|
||||||
|
|
||||||
chai.use(chaiAsPromised);
|
chai.use(chaiAsPromised);
|
||||||
|
@ -24,6 +23,7 @@ describe('Localizer', () => {
|
||||||
basePath: '/not/a/apth',
|
basePath: '/not/a/apth',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
new Localizer(localizerBindings);
|
new Localizer(localizerBindings);
|
||||||
}, 'Invalid ftl translations basePath');
|
}, 'Invalid ftl translations basePath');
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,7 +11,7 @@ describe('converts <mj-include> to <mj-style> tag', () => {
|
||||||
return mjml
|
return mjml
|
||||||
.replace(/\n/g, '')
|
.replace(/\n/g, '')
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
.replace(/\>\s*</g, '/><')
|
.replace(/>\s*</g, '/><')
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,12 @@ chai.use(chaiAsPromised);
|
||||||
describe('Renderer', () => {
|
describe('Renderer', () => {
|
||||||
it('fails with a bad localizer ftl basePath', () => {
|
it('fails with a bad localizer ftl basePath', () => {
|
||||||
assert.throws(() => {
|
assert.throws(() => {
|
||||||
let LocalizerBindings = new NodeRendererBindings({
|
const LocalizerBindings = new NodeRendererBindings({
|
||||||
translations: {
|
translations: {
|
||||||
basePath: '/not/a/apth',
|
basePath: '/not/a/apth',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
new Renderer(LocalizerBindings);
|
new Renderer(LocalizerBindings);
|
||||||
}, 'Invalid ftl translations basePath');
|
}, 'Invalid ftl translations basePath');
|
||||||
});
|
});
|
||||||
|
@ -53,7 +54,7 @@ describe('Renderer', () => {
|
||||||
|
|
||||||
it('handles escaped quote format', () => {
|
it('handles escaped quote format', () => {
|
||||||
const { key, val } = splitPlainTextLine(
|
const { key, val } = splitPlainTextLine(
|
||||||
`${pair.key}="${pair.val} \"baz\" "`
|
`${pair.key}="${pair.val} "baz" "`
|
||||||
);
|
);
|
||||||
assert.equal(key, pair.key);
|
assert.equal(key, pair.key);
|
||||||
assert.equal(val, pair.val + ' "baz" ');
|
assert.equal(val, pair.val + ' "baz" ');
|
||||||
|
|
|
@ -126,12 +126,8 @@ describe('remote account create', function () {
|
||||||
it('create account with service identifier and resume', () => {
|
it('create account with service identifier and resume', () => {
|
||||||
const email = server.uniqueEmail();
|
const email = server.uniqueEmail();
|
||||||
const password = 'allyourbasearebelongtous';
|
const password = 'allyourbasearebelongtous';
|
||||||
let client = null; // eslint-disable-line no-unused-vars
|
|
||||||
const options = { service: 'abcdef', resume: 'foo' };
|
const options = { service: 'abcdef', resume: 'foo' };
|
||||||
return Client.create(config.publicUrl, email, password, options)
|
return Client.create(config.publicUrl, email, password, options)
|
||||||
.then((x) => {
|
|
||||||
client = x;
|
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return server.mailbox.waitForEmail(email);
|
return server.mailbox.waitForEmail(email);
|
||||||
})
|
})
|
||||||
|
|
|
@ -60,15 +60,11 @@ describe('remote recovery email verify', function () {
|
||||||
it('verification email link', () => {
|
it('verification email link', () => {
|
||||||
const email = server.uniqueEmail();
|
const email = server.uniqueEmail();
|
||||||
const password = 'something';
|
const password = 'something';
|
||||||
let client = null; // eslint-disable-line no-unused-vars
|
|
||||||
const options = {
|
const options = {
|
||||||
redirectTo: `https://sync.${config.smtp.redirectDomain}/`,
|
redirectTo: `https://sync.${config.smtp.redirectDomain}/`,
|
||||||
service: 'sync',
|
service: 'sync',
|
||||||
};
|
};
|
||||||
return Client.create(config.publicUrl, email, password, options)
|
return Client.create(config.publicUrl, email, password, options)
|
||||||
.then((c) => {
|
|
||||||
client = c;
|
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return server.mailbox.waitForEmail(email);
|
return server.mailbox.waitForEmail(email);
|
||||||
})
|
})
|
||||||
|
|
Загрузка…
Ссылка в новой задаче