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:
Julian Poyourow 2022-11-01 13:17:53 -07:00
Родитель 6941fd4d64
Коммит 76ef507b26
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: EA0570ABC73D47D3
39 изменённых файлов: 406 добавлений и 333 удалений

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

@ -121,6 +121,27 @@ jobs:
name: Reporting code coverage...
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:
resource_class: medium+
docker:
@ -308,6 +329,12 @@ jobs:
workflows:
test_pull_request:
jobs:
- lint:
filters:
branches:
ignore: main
tags:
ignore: /.*/
- test-many:
filters:
branches:
@ -395,6 +422,12 @@ workflows:
ignore: /.*/
test_and_deploy_tag:
jobs:
- lint:
filters:
branches:
ignore: /.*/
tags:
only: /.*/
- test-many:
filters:
branches:

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

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

@ -54,11 +54,14 @@
"lint-staged": {
"*.js": [
"prettier --config _dev/.prettierrc --write",
"eslint -c _dev/.eslintrc"
"eslint"
],
"*.{ts,tsx}": [
"prettier --config _dev/.prettierrc --write"
],
"packages/fxa-auth-server/**/*.{ts,tsx}": [
"eslint"
],
"*.css": [
"prettier --config _dev/.prettierrc --write"
],

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

@ -1,13 +1,14 @@
{
"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"],
"plugins": ["fxa"],
"parserOptions": {
"ecmaVersion": "2020",
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "fxa"],
"parser": "@typescript-eslint/parser",
"root": true,
"ignorePatterns": [
"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.
## Linting
Run lint with:
yarn lint
Linting will also be run for staged files automatically via Husky when you attempt to commit.
## Testing
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-validator'));
const conf = convict({
const convictConf = convict({
env: {
doc: 'The current node.js environment',
default: 'prod',
@ -1572,7 +1572,7 @@ const conf = convict({
format: RegExp,
// eslint-disable-next-line no-useless-escape
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: {
@ -1953,55 +1953,64 @@ const conf = convict({
// files to process, which will be overlayed in order, in the CONFIG_FILES
// 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 || ''}`;
const files = envConfig.split(',').filter(fs.existsSync);
conf.loadFile(files);
conf.validate();
convictConf.loadFile(files);
convictConf.validate();
// 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
const baseUri = conf.get('contentServer.url');
conf.set('smtp.accountSettingsUrl', `${baseUri}/settings`);
conf.set(
const baseUri = convictConf.get('contentServer.url');
convictConf.set('smtp.accountSettingsUrl', `${baseUri}/settings`);
convictConf.set(
'smtp.accountRecoveryCodesUrl',
`${baseUri}/settings/two_step_authentication/replace_codes`
);
conf.set('smtp.verificationUrl', `${baseUri}/verify_email`);
conf.set('smtp.pushVerificationUrl', `${baseUri}/push/confirm_login`);
conf.set('smtp.passwordResetUrl', `${baseUri}/complete_reset_password`);
conf.set('smtp.initiatePasswordResetUrl', `${baseUri}/reset_password`);
conf.set(
convictConf.set('smtp.verificationUrl', `${baseUri}/verify_email`);
convictConf.set('smtp.pushVerificationUrl', `${baseUri}/push/confirm_login`);
convictConf.set('smtp.passwordResetUrl', `${baseUri}/complete_reset_password`);
convictConf.set('smtp.initiatePasswordResetUrl', `${baseUri}/reset_password`);
convictConf.set(
'smtp.initiatePasswordChangeUrl',
`${baseUri}/settings/change_password`
);
conf.set('smtp.verifyLoginUrl', `${baseUri}/complete_signin`);
conf.set(
convictConf.set('smtp.verifyLoginUrl', `${baseUri}/complete_signin`);
convictConf.set(
'smtp.accountFinishSetupUrl',
`${baseUri}/post_verify/finish_account_setup/set_password`
);
conf.set('smtp.reportSignInUrl', `${baseUri}/report_signin`);
conf.set('smtp.revokeAccountRecoveryUrl', `${baseUri}/settings#recovery-key`);
conf.set(
convictConf.set('smtp.reportSignInUrl', `${baseUri}/report_signin`);
convictConf.set(
'smtp.revokeAccountRecoveryUrl',
`${baseUri}/settings#recovery-key`
);
convictConf.set(
'smtp.createAccountRecoveryUrl',
`${baseUri}/settings/account_recovery`
);
conf.set('smtp.verifyPrimaryEmailUrl', `${baseUri}/verify_primary_email`);
conf.set('smtp.verifySecondaryEmailUrl', `${baseUri}/verify_secondary_email`);
conf.set('smtp.subscriptionSettingsUrl', `${baseUri}/subscriptions`);
conf.set('smtp.subscriptionSupportUrl', `${baseUri}/support`);
conf.set('smtp.syncUrl', `${baseUri}/connect_another_device`);
convictConf.set(
'smtp.verifyPrimaryEmailUrl',
`${baseUri}/verify_primary_email`
);
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
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');
}
if (conf.get('env') === 'dev') {
if (convictConf.get('env') === 'dev') {
if (!process.env.AWS_ACCESS_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(
__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.
conf.set('oauthServer.openid.key', null);
convictConf.set('oauthServer.openid.key', null);
if (fs.existsSync(keyFile)) {
const key = JSON.parse(fs.readFileSync(keyFile, 'utf-8'));
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) {
conf.set('oauthServer.openid.key', null);
} else if (
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(
__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.
conf.set('oauthServer.openid.newKey', null);
convictConf.set('oauthServer.openid.newKey', null);
if (fs.existsSync(newKeyFile)) {
const newKey = JSON.parse(fs.readFileSync(newKeyFile, 'utf-8'));
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) {
conf.set('oauthServer.openid.newKey', null);
} else if (
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(
__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.
conf.set('oauthServer.openid.oldKey', null);
convictConf.set('oauthServer.openid.oldKey', null);
if (fs.existsSync(oldKeyFile)) {
const oldKey = JSON.parse(fs.readFileSync(oldKeyFile, 'utf-8'));
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) {
conf.set('oauthServer.openid.oldKey', null);
} else if (
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.
if (conf.get('isProduction')) {
if (convictConf.get('isProduction')) {
const SECRET_SETTINGS = [
'pushbox.key',
'metrics.flow_id_key',
@ -2078,13 +2093,13 @@ if (conf.get('isProduction')) {
'supportPanel.secretBearerToken',
];
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`);
}
}
}
export type conf = typeof conf;
export type conf = typeof convictConf;
export type ConfigType = ReturnType<conf['getProperties']>;
module.exports = conf;
module.exports = convictConf;

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

@ -25,8 +25,8 @@ class Localizer {
}
protected async fetchMessages(currentLocales: string[]) {
let fetchedPending: Record<string, Promise<string>> = {};
let fetched: Record<string, string> = {};
const fetchedPending: Record<string, Promise<string>> = {};
const fetched: Record<string, string> = {};
for (const locale of currentLocales) {
fetchedPending[locale] = this.fetchTranslatedMessages(locale);
}

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

@ -9,10 +9,10 @@ const { AuthLogger } = require('../../types');
const { Container } = require('typedi');
// 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');
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const RefreshTokenMetadata = require('./refreshTokenMetadata');
function resolveLogger() {

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

@ -12,8 +12,8 @@ export class CurrencyHelper {
this.payPalEnabled = config.subscriptions?.paypalNvpSigCredentials?.enabled;
// Validate currencyMap
const currencyMap = new Map(Object.entries(config.currenciesToCountries));
let allListedCountries: string[] = [];
for (let [currency, countries] of currencyMap) {
const allListedCountries: string[] = [];
for (const [currency, countries] of currencyMap) {
// Is currency acceptable
if (!CurrencyHelper.validCurrencyCodes.includes(currency)) {
throw new Error(`Currency code ${currency} is invalid.`);
@ -25,7 +25,7 @@ export class CurrencyHelper {
throw new Error(`Currency code ${currency} is invalid.`);
}
// Are countries acceptable
for (let country of countries) {
for (const country of countries) {
if (!CurrencyHelper.validCountryCodes.includes(country)) {
throw new Error(`Country code ${country} is invalid.`);
}

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

@ -20,10 +20,10 @@ import { IapConfig } from './types';
export function getIapPurchaseType(
purchase: PlayStoreSubscriptionPurchase | AppStoreSubscriptionPurchase
): Omit<SubscriptionType, typeof MozillaSubscriptionTypes.WEB> {
if (purchase.hasOwnProperty('purchaseToken')) {
if ('purchaseToken' in purchase) {
return MozillaSubscriptionTypes.IAP_GOOGLE;
}
if (purchase.hasOwnProperty('originalTransactionId')) {
if ('originalTransactionId' in purchase) {
return MozillaSubscriptionTypes.IAP_APPLE;
}
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
// "Auto-Renewable Subscription"? See https://developer.apple.com/forums/thread/705730
...(purchase.expiresDate && { expiry_time_millis: purchase.expiresDate }),
...(purchase.hasOwnProperty('isInBillingRetry') && {
...('isInBillingRetry' in purchase && {
is_in_billing_retry_period: purchase.isInBillingRetry,
}),
price_id: purchase.price_id,

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

@ -2,7 +2,6 @@
* 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 { 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 { Stripe } from 'stripe';

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

@ -71,7 +71,6 @@ import { AppStoreSubscriptionPurchase } from './iap/apple-app-store/subscription
import { PlayStoreSubscriptionPurchase } from './iap/google-play/subscription-purchase';
import { getIapPurchaseType } from './iap/iap-config';
import { FirestoreStripeError, StripeFirestore } from './stripe-firestore';
import { stripeInvoiceToFirstInvoicePreviewDTO } from './stripe-formatter';
import { generateIdempotencyKey } from './utils';
// Maintains backwards compatibility. Some type defs hoisted to fxa-shared/payments/stripe
@ -129,7 +128,7 @@ export type BillingAddressOptions = {
};
export type PaymentBillingDetails = Awaited<
ReturnType<StripeHelper['extractBillingDetails']>
ReturnType<StripeHelper['extractBillingDetails']> // eslint-disable-line no-use-before-define
> & {
paypal_payment_error?: PaypalPaymentError;
billing_agreement_id?: string;
@ -2633,7 +2632,7 @@ export class StripeHelper extends StripeHelperBase {
return [];
}
let formattedSubscriptions = [];
const formattedSubscriptions = [];
for (const subscription of customer.subscriptions.data) {
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.
*/
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
// webhook code to ensure the object is up to date in Firestore before
// our code handles the event.
let handled = true;
try {
switch (type as Stripe.WebhookEndpointUpdateParams.EnabledEvent) {
switch (type as StripeEnabledEvent) {
case 'invoice.created':
case 'invoice.finalized':
case 'invoice.paid':
@ -3207,7 +3213,6 @@ export class StripeHelper extends StripeHelperBase {
await this.processSubscriptionEventToFirestore(event);
break;
case 'payment_method.attached':
// @ts-ignore
case 'payment_method.card_automatically_updated':
case 'payment_method.updated':
await this.processPaymentMethodEventToFirestore(event);

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

@ -4,7 +4,7 @@
import { createHash } from 'crypto';
export function generateIdempotencyKey(params: string[]) {
let sha = createHash('sha256');
const sha = createHash('sha256');
sha.update(params.join(''));
return sha.digest('base64url');
}

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

@ -463,7 +463,7 @@ export class AccountHandler {
this.setMetricsFlowCompleteSignal(request, service);
let { account, password } = await this.createAccount({
const { account, password } = await this.createAccount({
authPW,
authSalt,
email,
@ -613,7 +613,6 @@ export class AccountHandler {
form.uid = payload.uid;
const account = await this.db.account(uid);
await this.setPasswordOnStubAccount(account, authPW);
const metricsContext = await request.gatherMetricsContext({});
await this.signupUtils.verifyAccount(request, account, {});
const sessionToken = await this.createSessionToken({
account,
@ -635,7 +634,7 @@ export class AccountHandler {
// if it errored out after verifiying the account
// remove the uid from the list of accounts to send reminders to.
if (!!uid) {
if (uid) {
const account = await this.db.account(uid);
if (account.verifierSetAt > 0) {
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 () => {
// All sessions are considered unverified by default.
let needsVerificationId = true;
@ -922,87 +1002,6 @@ export class AccountHandler {
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 () => {
await this.signinUtils.sendSigninNotifications(
request,

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

@ -30,7 +30,7 @@ export class LinkedAccountHandler {
private db: any,
private config: ConfigType,
private mailer: any,
private profile: ProfileClient,
private profile: ProfileClient
) {
if (config.googleAuthConfig && config.googleAuthConfig.clientId) {
this.googleAuthClient = new OAuth2Client(
@ -163,7 +163,10 @@ export class LinkedAccountHandler {
const name = idToken.name;
let accountRecord;
let linkedAccountRecord = await this.db.getLinkedAccount(userid, provider);
const linkedAccountRecord = await this.db.getLinkedAccount(
userid,
provider
);
if (!linkedAccountRecord) {
try {
@ -303,7 +306,7 @@ export const linkedAccountRoutes = (
db: any,
config: ConfigType,
mailer: any,
profile: ProfileClient,
profile: ProfileClient
) => {
const handler = new LinkedAccountHandler(log, db, config, mailer, profile);

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

@ -19,79 +19,6 @@ import { AuthLogger, AuthRequest } from '../../types';
import validators from '../validators';
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 {
constructor(
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 == 'uncollectible' &&
invoice.status === 'uncollectible' &&
['Completed', 'Processed'].includes(message.payment_status)
) {
// we need to refund the user since the invoice was cancelled
@ -108,7 +108,7 @@ export class PayPalNotificationHandler extends PayPalHandler {
case 'Failed':
case 'Voided':
case 'Expired':
if (message.custom.length == 0) {
if (message.custom.length === 0) {
this.log.error('handleMerchPayment', {
message: 'No idempotency key on PayPal transaction',
ipnMessage: message,

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

@ -97,7 +97,7 @@ export class PayPalHandler extends StripeWebhookHandler {
try {
await this.customs.check(request, email, 'createSubscriptionWithPaypal');
let customer = await this.stripeHelper.fetchCustomer(uid, [
const customer = await this.stripeHelper.fetchCustomer(uid, [
'subscriptions',
'tax',
]);
@ -135,7 +135,7 @@ export class PayPalHandler extends StripeWebhookHandler {
throw error.billingAgreementExists(customer.id);
}
const { sourceCountry, subscription } = !!token
const { sourceCountry, subscription } = token
? await this._createPaypalBillingAgreementAndSubscription({
request,
uid,
@ -370,7 +370,7 @@ export class PayPalHandler extends StripeWebhookHandler {
const { uid, email } = await handleAuth(this.db, request.auth, true);
await this.customs.check(request, email, 'updatePaypalBillingAgreement');
let customer = await this.stripeHelper.fetchCustomer(uid, [
const customer = await this.stripeHelper.fetchCustomer(uid, [
'subscriptions',
]);
@ -476,7 +476,7 @@ export class PayPalHandler extends StripeWebhookHandler {
// copy bill to address information to Customer
const accountCustomer = await getAccountCustomerByUid(uid);
if (accountCustomer.stripeCustomerId) {
let locationDetails = {} as any;
const locationDetails = {} as any;
if (agreementDetails.countryCode === options.location?.countryCode) {
// Record the state (short name) if needed
const state = options.location?.state;

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

@ -17,7 +17,6 @@ import {
reportValidationError,
} from '../../../lib/sentry';
import error from '../../error';
import { CapabilityService } from '../../payments/capability';
import { PayPalHelper, RefusedError } from '../../payments/paypal';
import {
CUSTOMER_RESOURCE,
@ -792,7 +791,7 @@ export class StripeWebhookHandler extends StripeHandler {
(plan) => (plan.product as Stripe.Product).id !== product.id
);
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') {

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

@ -283,7 +283,7 @@ export class StripeHandler {
const customer = await this.stripeHelper.fetchCustomer(uid);
const planCurrency = (await this.stripeHelper.findAbbrevPlanById(planId))
.currency;
if (customer && customer.currency != planCurrency) {
if (customer && customer.currency !== planCurrency) {
throw error.currencyCurrencyMismatch(customer.currency, planCurrency);
}

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

@ -18,6 +18,37 @@ 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,
@ -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 = supportPanelRoutes;

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

@ -48,7 +48,7 @@ export function transformMjIncludeTags(mjml: string): string {
function extractMjIncludeTags(mjml: string): MjIncludeTag[] {
let chomp = false;
let include = '';
let includes = [];
const includes = [];
mjml
.replace(/<mj-include/g, ' <mj-include')
.split(/\n|\s/g)
@ -102,7 +102,7 @@ function parseMjIncludeTag(include: string): MjIncludeTag {
}
function toMjStyle(tag: MjIncludeTag) {
let { inline, path, type } = tag;
const { inline, path, type } = tag;
if (type !== 'css') return '';
if (!path) return '';

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

@ -2,6 +2,10 @@
* 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/. */
// NOTE: This file handled with browser ESLint bindings
// instead of NodeJS for DOM typings support
/* eslint-env browser */
import { Story } from '@storybook/html';
import Renderer from '../renderer';
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
* 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';
// 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
* 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 { LocalizerOpts } from '../../l10n/models/LocalizerOpts';

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

@ -3,12 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { DOMLocalization, Localization } from '@fluent/dom';
import {
RendererBindings,
TemplateContext,
RendererContext,
TemplateValues,
} from './bindings';
import { RendererBindings, TemplateContext, RendererContext } from './bindings';
import Localizer, { FtlIdMsg } from '../../l10n';
const RTL_LOCALES = [
@ -135,9 +130,13 @@ class Renderer extends Localizer {
): Promise<GlobalTemplateValues> {
// We must use 'require' here, 'import' causes an 'unknown file extension .ts'
// error. Might be a config option to make it work?
// eslint-disable-next-line no-useless-catch
try {
// 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 (
await require(`../emails/templates/${context.template}/includes`)
).getIncludes(context.numberRemaining);
@ -159,7 +158,7 @@ class Renderer extends Localizer {
const ftlContext = flattenNestedObjects(context);
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
// to pass down to fluent
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 {
devices: Promise<any>;
locale: String;
@ -56,6 +44,7 @@ export interface AuthApp extends RequestApplicationState {
}
export interface AuthRequest extends Request {
// eslint-disable-next-line no-use-before-define
log: AuthLogger;
app: AuthApp;
validateMetricsContext: any;
@ -71,8 +60,22 @@ export interface ProfileClient {
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
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const AuthLogger = new Token<AuthLogger>('AUTH_LOGGER');
export const AuthFirestore = new Token<Firestore>('AUTH_FIRESTORE');
export const AppConfig = new Token<ConfigType>('APP_CONFIG');
// eslint-disable-next-line @typescript-eslint/no-redeclare
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",
"bump-template-versions": "node scripts/template-version-bump",
"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",
"start": "yarn emails-scss && scripts/start-local.sh",
"stop": "pm2 stop pm2.config.js",

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

@ -4,7 +4,7 @@
import program from 'commander';
import { StatsD } from 'hot-shots';
import Redis from 'ioredis';
import Redlock, { Lock, RedlockAbortSignal } from 'redlock';
import Redlock, { RedlockAbortSignal } from 'redlock';
import Container from 'typedi';
import { promisify } from 'util';
@ -90,6 +90,7 @@ export async function init() {
[program.lockName],
lockDuration,
async (signal: RedlockAbortSignal) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const _ of processor.processInvoices()) {
if (signal.aborted) {
throw signal.error;
@ -101,6 +102,7 @@ export async function init() {
throw new Error(`Cannot acquire lock to run: ${err.message}`);
}
} else {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const _ of processor.processInvoices()) {
// 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 {
deleteAllPayPalBAs,
getAllPayPalBAByUid,
} from 'fxa-shared/db/models/auth';
import { Account } from 'fxa-shared/db/models/auth/account';
import Stripe from 'stripe';
import Container from 'typedi';
import { promisify } from 'util';
import { StatsD } from 'hot-shots';
import { PayPalHelper } from '../lib/payments/paypal';
import { setupProcessingTaskObjects } from '../lib/payments/processing-tasks-setup';
import { StripeHelper } from '../lib/payments/stripe';
import { reportSentryError } from '../lib/sentry';
const config = require('../config').getProperties();
//import { PayPalClient } from 'fxa-auth-server/lib/payments/paypal/client';
//import {
//deleteAllPayPalBAs,
//getAllPayPalBAByUid,
//} from 'fxa-shared/db/models/auth';
//import { Account } from 'fxa-shared/db/models/auth/account';
//import Stripe from 'stripe';
//import Container from 'typedi';
//import { promisify } from 'util';
//import { StatsD } from 'hot-shots';
//import { PayPalHelper } from '../lib/payments/paypal';
//import { setupProcessingTaskObjects } from '../lib/payments/processing-tasks-setup';
//import { StripeHelper } from '../lib/payments/stripe';
//import { reportSentryError } from '../lib/sentry';
//const config = require('../config').getProperties();
// export async function retreiveUnverifiedAccounts(
// database: any

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

@ -106,7 +106,7 @@ const handleEnglishPlan = (plan: Partial<Stripe.Plan>) => {
return;
}
let lang = findLocaleInTitle('en', plan.nickname!);
const lang = findLocaleInTitle('en', plan.nickname!);
if (lang === 'en') {
// the plan's en strings are different than the product's, so we save

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

@ -28,7 +28,6 @@ import {
} from './plan-language-tags-guesser';
import { promises as fsPromises, constants } from 'fs';
import path from 'path';
import { string } from 'joi';
const DEFAULT_LOCALE = 'en';
@ -135,7 +134,7 @@ export class StripeProductsAndPlansConverter {
key.toLowerCase().startsWith('capabilities')
)) {
// 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 ?? '*';
capabilities[newKey] = commaSeparatedListToArray(
stripeObject.metadata![oldKey]

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

@ -13,7 +13,7 @@ const through = require('through');
let clientCount = 2;
const pathStats = {};
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 start = null;
@ -41,7 +41,7 @@ server.stderr
}
requests++;
if (json.code === 200) {
pass++;
pass++; // eslint-disable-line @typescript-eslint/no-unused-vars
} else {
fail++;
}

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

@ -7,7 +7,6 @@ import { Localization } from '@fluent/dom';
import chai, { assert } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import Localizer from '../../../lib/l10n';
import { parseAcceptLanguage } from 'fxa-shared/l10n/parseAcceptLanguage';
import { LocalizerBindings } from '../../../lib/l10n/bindings';
chai.use(chaiAsPromised);
@ -24,6 +23,7 @@ describe('Localizer', () => {
basePath: '/not/a/apth',
},
});
// eslint-disable-next-line no-new
new Localizer(localizerBindings);
}, 'Invalid ftl translations basePath');
});

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

@ -11,7 +11,7 @@ describe('converts <mj-include> to <mj-style> tag', () => {
return mjml
.replace(/\n/g, '')
.replace(/\s+/g, ' ')
.replace(/\>\s*</g, '/><')
.replace(/>\s*</g, '/><')
.trim();
}

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

@ -15,11 +15,12 @@ chai.use(chaiAsPromised);
describe('Renderer', () => {
it('fails with a bad localizer ftl basePath', () => {
assert.throws(() => {
let LocalizerBindings = new NodeRendererBindings({
const LocalizerBindings = new NodeRendererBindings({
translations: {
basePath: '/not/a/apth',
},
});
// eslint-disable-next-line no-new
new Renderer(LocalizerBindings);
}, 'Invalid ftl translations basePath');
});
@ -53,7 +54,7 @@ describe('Renderer', () => {
it('handles escaped quote format', () => {
const { key, val } = splitPlainTextLine(
`${pair.key}="${pair.val} \"baz\" "`
`${pair.key}="${pair.val} "baz" "`
);
assert.equal(key, pair.key);
assert.equal(val, pair.val + ' "baz" ');

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

@ -126,12 +126,8 @@ describe('remote account create', function () {
it('create account with service identifier and resume', () => {
const email = server.uniqueEmail();
const password = 'allyourbasearebelongtous';
let client = null; // eslint-disable-line no-unused-vars
const options = { service: 'abcdef', resume: 'foo' };
return Client.create(config.publicUrl, email, password, options)
.then((x) => {
client = x;
})
.then(() => {
return server.mailbox.waitForEmail(email);
})

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

@ -60,15 +60,11 @@ describe('remote recovery email verify', function () {
it('verification email link', () => {
const email = server.uniqueEmail();
const password = 'something';
let client = null; // eslint-disable-line no-unused-vars
const options = {
redirectTo: `https://sync.${config.smtp.redirectDomain}/`,
service: 'sync',
};
return Client.create(config.publicUrl, email, password, options)
.then((c) => {
client = c;
})
.then(() => {
return server.mailbox.waitForEmail(email);
})