A guide for styling your node.js / JavaScript code. Fork & adjust to your taste.
mozilla/fathom 1966
A framework for extracting meaning from web pages
Keep your email safe from hackers and trackers. Make an email alias with 1 click, and keep your address to yourself.
Facebook Container isolates your Facebook activity from the rest of your web activity in order to prevent Facebook from tracking you outside of the Facebook website via third party cookies.
Firefox Monitor arms you with tools to keep your personal information safe. Find out what hackers already know about you and learn how to stay a step ahead of them.
A refreshed "new tab page" for Firefox
An add-on to let you snooze your tabs for a while.
Documentation and implementation of telemetry ingestion on Google Cloud Platform
mozilla/blok 24
Web Extension implementation of Firefox tracking protection for experimental development
The platform that powers the Firefox Public Data Report :violin: :trumpet: :musical_keyboard:
Pull request review commentmozilla/fxa
feat(apple): Add apple user migrations script
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++'use strict';++// This script is used to import users from Apple into FxA. It consumes a CSV+// containing the Apple `transfer_sub` id and exchanges it for their profile information.+// It then creates/updates the user and links the Apple account to the FxA account.+//+// Usage: node scripts/apple-transfer-users.js -i <input file> -o <output file>++const fs = require('fs');+const path = require('path');+const program = require('commander');++const axios = require('axios');+const random = require('../../lib/crypto/random');+const uuid = require('uuid');++const config = require('../../config').config.getProperties();++const GRANT_TYPE = 'client_credentials';+const SCOPE = 'user.migration';+const USER_MIGRATION_ENDPOINT = 'https://appleid.apple.com/auth/usermigrationinfo';++const APPLE_PROVIDER = 2;++export class AppleUser {+ constructor(email, transferSub, uid, alternateEmails, db) {+ this.email = email;+ this.transferSub = transferSub;+ this.uid = uid;+ this.alternateEmails = alternateEmails || [];+ this.db = db;+ }++ // Exchanges the Apple `transfer_sub` for the user's profile information and+ // moves the user to the new team.+ // Ref: https://developer.apple.com/documentation/sign_in_with_apple/bringing_new_apps_and_users_into_your_team#3559300+ async exchangeIdentifiers(accessToken) {+ try {+ const options = {+ transfer_sub: this.transferSub,+ client_id: config.appleAuthConfig.clientId,+ client_secret: config.appleAuthConfig.clientSecret,+ };+ const res = await axios.post(USER_MIGRATION_ENDPOINT,+ new URLSearchParams(options).toString(),+ {+ headers: {+ Authorization: `Bearer ${accessToken}`,+ },+ },+ );++ // Data here contains `sub`, `email` and `is_private_email`+ this.appleUserInfo = res.data;+ return res.data;+ } catch (err) {+ this.setFailure(err);+ if (err.response && err.response.status === 429) {+ console.error(`Rate limit exceeded, try again later: ${this.transferSub}`);+ } else {+ console.error(`Something went wrong with transfer: ${this.transferSub} ${err}`);+ }+ }+ }++ setSuccess(accountRecord) {+ this.success = true;+ this.accountRecord = accountRecord;+ }++ setFailure(err) {+ this.success = false;+ this.err = err;+ }++ async createLinkedAccount(accountRecord, sub) {+ // If the user already has a linked account, delete it and create a new one.+ await this.db.deleteLinkedAccount(accountRecord.uid, APPLE_PROVIDER);+ await this.db.createLinkedAccount(accountRecord.uid, sub, APPLE_PROVIDER);+ }++ async createUpdateFxAUser() {+ const sub = this.appleUserInfo.sub; // The recipient team-scoped identifier for the user.+ const appleEmail = this.appleUserInfo.email; // The private email address specific to the recipient team. + + // TODO, maybe we should mark this failure+ // const isPrivateEmail = this.appleUserInfo.is_private_email; // Boolean if email is private+ // if (isPrivateEmail) {+ // this.setFailure({ message: 'Apple email is private' });+ // }++ // 1. Check if user exists in FxA via the uid value from Pocket. We should expect+ // the uid to be valid, but if it isn't error out.+ try {+ if (this.uid) {+ const accountRecord = await this.db.account(this.uid);+ await this.createLinkedAccount(accountRecord, sub);+ this.setSuccess(accountRecord);+ return;+ }+ } catch (err) {+ const msg = `Uid not found: ${this.uid}`;+ console.error(msg);+ this.setFailure(err);+ return;+ }++ // 2. Check all emails to see if there exists a match in FxA, link Apple account+ // to the FxA account.+ let accountRecord;+ // Insert email into the front of alternateEmails array, this will be the+ // most likely to exist in FxA.+ this.alternateEmails.unshift(appleEmail);+ this.alternateEmails.unshift(this.email);+ if (this.alternateEmails) {+ for (const email of this.alternateEmails) {+ try {+ accountRecord = await this.db.accountRecord(email);+ break;+ } catch (err) {+ // Ignore+ }+ }+ }+ // There was a match! Link the Apple account to the FxA account.+ if (accountRecord) {+ await this.createLinkedAccount(accountRecord, sub);+ this.setSuccess(accountRecord);+ return;+ }++ // 3. No matches mean this is a completely new FxA user, create the user and+ // link the Apple account to the FxA account.+ try {+ const emailCode = await random.hex(16);+ const authSalt = await random.hex(32);+ const [kA, wrapWrapKb] = await random.hex(32, 32);+ accountRecord = await this.db.createAccount({+ uid: uuid.v4({}, Buffer.alloc(16)).toString('hex'),+ createdAt: Date.now(),+ email: appleEmail,+ emailCode,+ emailVerified: true,+ kA,+ wrapWrapKb,+ authSalt,+ verifierVersion: config.verifierVersion,+ verifyHash: Buffer.alloc(32).toString('hex'),+ verifierSetAt: 0,+ });+ await this.createLinkedAccount(accountRecord, sub);+ this.setSuccess(accountRecord);+ } catch (err) {+ this.setFailure(err);+ }+ }++ async transferUser(accessToken) {+ await this.exchangeIdentifiers(accessToken);+ await this.createUpdateFxAUser(this.appleUserInfo);+ }+}++export class ApplePocketFxAMigration {+ constructor(filename, config, db) {+ this.users = [];+ this.db = db;+ this.filename = filename;+ this.config = config;+ }++ printUsers() {+ console.table(this.users);+ }+ + saveResults() {+ const output = this.users.map((user) => {+ const appleEmail = user.appleUserInfo.email;+ const fxaEmail = user.accountRecord.email;+ const uid = user.accountRecord.uid; // Newly created uid+ const transferSub = user.transferSub;+ const success = user.success;+ const err = (user.err && user.err.message) || '';+ return `${transferSub},${uid},${fxaEmail},${appleEmail},${success},${err}`;+ }).join('\n');+ fs.writeFileSync(path.resolve(`${Date.now()}_output.csv`), output);+ }++ parseCSV() {+ try {+ const input = fs+ .readFileSync(path.resolve(this.filename))+ .toString('utf8');++ if (!input.length) {+ return [];+ }++ // Parse the input file CSV style+ return input.split(/\n/).map((s, index) => {+ if (index === 0) return;++ const delimiter = program.delimiter || ',';+ const tokens = s.split(delimiter);+ const transferSub = tokens[0];+ const uid = tokens[1];+ const email = tokens[2];+ let alternateEmails = [];++ if (tokens[3]) {+ alternateEmails = tokens[3].replaceAll('"', '').split(':');+ }+ return new AppleUser(email, transferSub, uid, alternateEmails, this.db);+ }).filter((user) => user);+ } catch (err) {+ console.error('No such file or directory');+ process.exit(1);+ }+ }++ async transferUsers() {+ const accessToken = await this.generateAccessToken();+ for (const user of this.users) {+ await user.transferUser(accessToken);+ }+ }++ async load() {+ this.db = await this.db.connect(config);+ this.users = this.parseCSV();+ console.info(+ '%s accounts loaded from %s',+ this.users.length,+ this.filename,+ );+ }++ async close() {+ await this.db.close();+ }++ async generateAccessToken() {+ const tokenOptions = {+ grant_type: GRANT_TYPE,+ scope: SCOPE,+ client_id: config.appleAuthConfig.clientId,+ client_secret: config.appleAuthConfig.clientSecret,+ };+ const tokenRes = await axios.post(config.appleAuthConfig.tokenEndpoint,+ new URLSearchParams(tokenOptions).toString(),+ );
Q: does this need a try..catch
in the unlikely event of a server/fetch error?
comment created time in 9 hours
Pull request review commentmozilla/fxa
feat(apple): Add apple user migrations script
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++'use strict';++// This script is used to import users from Apple into FxA. It consumes a CSV+// containing the Apple `transfer_sub` id and exchanges it for their profile information.+// It then creates/updates the user and links the Apple account to the FxA account.+// Example input file: /tests/fixtures/users-apple.csv+//+// Usage: node scripts/apple-transfer-users.js -i <input file> -o <output file>++const {ApplePocketFxAMigration} = require('./apple-transfer-users/apple-transfer-users');++const program = require('commander');++program+ .option('-d, --delimiter [delimiter]', 'Delimiter for input file', ',')+ .option('-o, --output <filename>', 'Output filename to save results to')+ .option(+ '-i, --input <filename>',+ 'Input filename from which to read input if not specified on the command line',+ )+ .parse(process.argv);++if (!program.input) {+ console.error('input file must be specified');+ process.exit(1);+}++async function main() {+ const migration = new ApplePocketFxAMigration(program.input);++ await migration.load();+ await migration.printUsers();+ await migration.transferUsers();+ await migration.saveResults();
Same w/ non-async saveResults()
:
https://github.com/mozilla/fxa/blob/ea9bb536bc17407e2965d130e2c88f112166f2bb/packages/fxa-auth-server/scripts/apple-transfer-users/apple-transfer-users.js#L180-L181
comment created time in 8 hours
Pull request review commentmozilla/fxa
feat(apple): Add apple user migrations script
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++'use strict';++// This script is used to import users from Apple into FxA. It consumes a CSV+// containing the Apple `transfer_sub` id and exchanges it for their profile information.+// It then creates/updates the user and links the Apple account to the FxA account.+//+// Usage: node scripts/apple-transfer-users.js -i <input file> -o <output file>++const fs = require('fs');+const path = require('path');+const program = require('commander');++const axios = require('axios');+const random = require('../../lib/crypto/random');+const uuid = require('uuid');++const config = require('../../config').config.getProperties();++const GRANT_TYPE = 'client_credentials';+const SCOPE = 'user.migration';+const USER_MIGRATION_ENDPOINT = 'https://appleid.apple.com/auth/usermigrationinfo';++const APPLE_PROVIDER = 2;++export class AppleUser {+ constructor(email, transferSub, uid, alternateEmails, db) {+ this.email = email;+ this.transferSub = transferSub;+ this.uid = uid;+ this.alternateEmails = alternateEmails || [];+ this.db = db;+ }++ // Exchanges the Apple `transfer_sub` for the user's profile information and+ // moves the user to the new team.+ // Ref: https://developer.apple.com/documentation/sign_in_with_apple/bringing_new_apps_and_users_into_your_team#3559300+ async exchangeIdentifiers(accessToken) {+ try {+ const options = {+ transfer_sub: this.transferSub,+ client_id: config.appleAuthConfig.clientId,+ client_secret: config.appleAuthConfig.clientSecret,+ };+ const res = await axios.post(USER_MIGRATION_ENDPOINT,+ new URLSearchParams(options).toString(),+ {+ headers: {+ Authorization: `Bearer ${accessToken}`,+ },+ },+ );++ // Data here contains `sub`, `email` and `is_private_email`+ this.appleUserInfo = res.data;+ return res.data;+ } catch (err) {+ this.setFailure(err);+ if (err.response && err.response.status === 429) {+ console.error(`Rate limit exceeded, try again later: ${this.transferSub}`);+ } else {+ console.error(`Something went wrong with transfer: ${this.transferSub} ${err}`);+ }+ }+ }++ setSuccess(accountRecord) {+ this.success = true;+ this.accountRecord = accountRecord;+ }++ setFailure(err) {+ this.success = false;+ this.err = err;+ }++ async createLinkedAccount(accountRecord, sub) {+ // If the user already has a linked account, delete it and create a new one.+ await this.db.deleteLinkedAccount(accountRecord.uid, APPLE_PROVIDER);+ await this.db.createLinkedAccount(accountRecord.uid, sub, APPLE_PROVIDER);+ }++ async createUpdateFxAUser() {+ const sub = this.appleUserInfo.sub; // The recipient team-scoped identifier for the user.+ const appleEmail = this.appleUserInfo.email; // The private email address specific to the recipient team. + + // TODO, maybe we should mark this failure+ // const isPrivateEmail = this.appleUserInfo.is_private_email; // Boolean if email is private+ // if (isPrivateEmail) {+ // this.setFailure({ message: 'Apple email is private' });+ // }++ // 1. Check if user exists in FxA via the uid value from Pocket. We should expect+ // the uid to be valid, but if it isn't error out.+ try {+ if (this.uid) {+ const accountRecord = await this.db.account(this.uid);+ await this.createLinkedAccount(accountRecord, sub);+ this.setSuccess(accountRecord);+ return;+ }+ } catch (err) {+ const msg = `Uid not found: ${this.uid}`;+ console.error(msg);+ this.setFailure(err);+ return;+ }++ // 2. Check all emails to see if there exists a match in FxA, link Apple account+ // to the FxA account.+ let accountRecord;+ // Insert email into the front of alternateEmails array, this will be the+ // most likely to exist in FxA.+ this.alternateEmails.unshift(appleEmail);+ this.alternateEmails.unshift(this.email);+ if (this.alternateEmails) {+ for (const email of this.alternateEmails) {+ try {+ accountRecord = await this.db.accountRecord(email);+ break;+ } catch (err) {+ // Ignore+ }+ }+ }+ // There was a match! Link the Apple account to the FxA account.+ if (accountRecord) {+ await this.createLinkedAccount(accountRecord, sub);+ this.setSuccess(accountRecord);+ return;+ }++ // 3. No matches mean this is a completely new FxA user, create the user and+ // link the Apple account to the FxA account.+ try {+ const emailCode = await random.hex(16);+ const authSalt = await random.hex(32);+ const [kA, wrapWrapKb] = await random.hex(32, 32);+ accountRecord = await this.db.createAccount({+ uid: uuid.v4({}, Buffer.alloc(16)).toString('hex'),+ createdAt: Date.now(),+ email: appleEmail,+ emailCode,+ emailVerified: true,+ kA,+ wrapWrapKb,+ authSalt,+ verifierVersion: config.verifierVersion,+ verifyHash: Buffer.alloc(32).toString('hex'),+ verifierSetAt: 0,+ });+ await this.createLinkedAccount(accountRecord, sub);+ this.setSuccess(accountRecord);+ } catch (err) {+ this.setFailure(err);+ }+ }++ async transferUser(accessToken) {+ await this.exchangeIdentifiers(accessToken);+ await this.createUpdateFxAUser(this.appleUserInfo);+ }+}++export class ApplePocketFxAMigration {+ constructor(filename, config, db) {+ this.users = [];+ this.db = db;+ this.filename = filename;+ this.config = config;+ }++ printUsers() {+ console.table(this.users);+ }+ + saveResults() {+ const output = this.users.map((user) => {+ const appleEmail = user.appleUserInfo.email;+ const fxaEmail = user.accountRecord.email;+ const uid = user.accountRecord.uid; // Newly created uid+ const transferSub = user.transferSub;+ const success = user.success;+ const err = (user.err && user.err.message) || '';+ return `${transferSub},${uid},${fxaEmail},${appleEmail},${success},${err}`;
Q: Would there be any potential issues if the ${err}
string had a comma (thus throwing off column counts, etc)?
comment created time in 9 hours
Pull request review commentmozilla/fxa
feat(apple): Add apple user migrations script
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++'use strict';++// This script is used to import users from Apple into FxA. It consumes a CSV+// containing the Apple `transfer_sub` id and exchanges it for their profile information.+// It then creates/updates the user and links the Apple account to the FxA account.+// Example input file: /tests/fixtures/users-apple.csv+//+// Usage: node scripts/apple-transfer-users.js -i <input file> -o <output file>++const {ApplePocketFxAMigration} = require('./apple-transfer-users/apple-transfer-users');++const program = require('commander');++program+ .option('-d, --delimiter [delimiter]', 'Delimiter for input file', ',')+ .option('-o, --output <filename>', 'Output filename to save results to')+ .option(+ '-i, --input <filename>',+ 'Input filename from which to read input if not specified on the command line',+ )+ .parse(process.argv);++if (!program.input) {+ console.error('input file must be specified');+ process.exit(1);+}++async function main() {+ const migration = new ApplePocketFxAMigration(program.input);++ await migration.load();+ await migration.printUsers();
I don't think printUsers()
is an async method:
https://github.com/mozilla/fxa/blob/ea9bb536bc17407e2965d130e2c88f112166f2bb/packages/fxa-auth-server/scripts/apple-transfer-users/apple-transfer-users.js#L176-L178
comment created time in 8 hours
Pull request review commentmozilla/fxa
feat(apple): Add apple user migrations script
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++'use strict';++// This script is used to import users from Apple into FxA. It consumes a CSV+// containing the Apple `transfer_sub` id and exchanges it for their profile information.+// It then creates/updates the user and links the Apple account to the FxA account.+//+// Usage: node scripts/apple-transfer-users.js -i <input file> -o <output file>++const fs = require('fs');+const path = require('path');+const program = require('commander');++const axios = require('axios');+const random = require('../../lib/crypto/random');+const uuid = require('uuid');++const config = require('../../config').config.getProperties();++const GRANT_TYPE = 'client_credentials';+const SCOPE = 'user.migration';+const USER_MIGRATION_ENDPOINT = 'https://appleid.apple.com/auth/usermigrationinfo';++const APPLE_PROVIDER = 2;++export class AppleUser {+ constructor(email, transferSub, uid, alternateEmails, db) {+ this.email = email;+ this.transferSub = transferSub;+ this.uid = uid;+ this.alternateEmails = alternateEmails || [];+ this.db = db;+ }++ // Exchanges the Apple `transfer_sub` for the user's profile information and+ // moves the user to the new team.+ // Ref: https://developer.apple.com/documentation/sign_in_with_apple/bringing_new_apps_and_users_into_your_team#3559300+ async exchangeIdentifiers(accessToken) {+ try {+ const options = {+ transfer_sub: this.transferSub,+ client_id: config.appleAuthConfig.clientId,+ client_secret: config.appleAuthConfig.clientSecret,+ };+ const res = await axios.post(USER_MIGRATION_ENDPOINT,+ new URLSearchParams(options).toString(),+ {+ headers: {+ Authorization: `Bearer ${accessToken}`,+ },+ },+ );++ // Data here contains `sub`, `email` and `is_private_email`+ this.appleUserInfo = res.data;+ return res.data;+ } catch (err) {+ this.setFailure(err);+ if (err.response && err.response.status === 429) {+ console.error(`Rate limit exceeded, try again later: ${this.transferSub}`);+ } else {+ console.error(`Something went wrong with transfer: ${this.transferSub} ${err}`);+ }+ }+ }++ setSuccess(accountRecord) {+ this.success = true;+ this.accountRecord = accountRecord;+ }++ setFailure(err) {+ this.success = false;+ this.err = err;+ }++ async createLinkedAccount(accountRecord, sub) {+ // If the user already has a linked account, delete it and create a new one.+ await this.db.deleteLinkedAccount(accountRecord.uid, APPLE_PROVIDER);+ await this.db.createLinkedAccount(accountRecord.uid, sub, APPLE_PROVIDER);+ }++ async createUpdateFxAUser() {+ const sub = this.appleUserInfo.sub; // The recipient team-scoped identifier for the user.+ const appleEmail = this.appleUserInfo.email; // The private email address specific to the recipient team. + + // TODO, maybe we should mark this failure+ // const isPrivateEmail = this.appleUserInfo.is_private_email; // Boolean if email is private+ // if (isPrivateEmail) {+ // this.setFailure({ message: 'Apple email is private' });+ // }++ // 1. Check if user exists in FxA via the uid value from Pocket. We should expect+ // the uid to be valid, but if it isn't error out.+ try {+ if (this.uid) {+ const accountRecord = await this.db.account(this.uid);+ await this.createLinkedAccount(accountRecord, sub);+ this.setSuccess(accountRecord);+ return;+ }+ } catch (err) {+ const msg = `Uid not found: ${this.uid}`;+ console.error(msg);+ this.setFailure(err);+ return;+ }++ // 2. Check all emails to see if there exists a match in FxA, link Apple account+ // to the FxA account.+ let accountRecord;+ // Insert email into the front of alternateEmails array, this will be the+ // most likely to exist in FxA.+ this.alternateEmails.unshift(appleEmail);+ this.alternateEmails.unshift(this.email);+ if (this.alternateEmails) {+ for (const email of this.alternateEmails) {+ try {+ accountRecord = await this.db.accountRecord(email);+ break;+ } catch (err) {+ // Ignore+ }+ }+ }+ // There was a match! Link the Apple account to the FxA account.+ if (accountRecord) {+ await this.createLinkedAccount(accountRecord, sub);+ this.setSuccess(accountRecord);+ return;+ }++ // 3. No matches mean this is a completely new FxA user, create the user and+ // link the Apple account to the FxA account.+ try {+ const emailCode = await random.hex(16);+ const authSalt = await random.hex(32);+ const [kA, wrapWrapKb] = await random.hex(32, 32);+ accountRecord = await this.db.createAccount({+ uid: uuid.v4({}, Buffer.alloc(16)).toString('hex'),+ createdAt: Date.now(),+ email: appleEmail,+ emailCode,+ emailVerified: true,+ kA,+ wrapWrapKb,+ authSalt,+ verifierVersion: config.verifierVersion,+ verifyHash: Buffer.alloc(32).toString('hex'),+ verifierSetAt: 0,+ });+ await this.createLinkedAccount(accountRecord, sub);+ this.setSuccess(accountRecord);+ } catch (err) {+ this.setFailure(err);+ }+ }++ async transferUser(accessToken) {+ await this.exchangeIdentifiers(accessToken);+ await this.createUpdateFxAUser(this.appleUserInfo);+ }+}++export class ApplePocketFxAMigration {+ constructor(filename, config, db) {+ this.users = [];+ this.db = db;+ this.filename = filename;+ this.config = config;+ }++ printUsers() {+ console.table(this.users);+ }+ + saveResults() {+ const output = this.users.map((user) => {+ const appleEmail = user.appleUserInfo.email;+ const fxaEmail = user.accountRecord.email;+ const uid = user.accountRecord.uid; // Newly created uid+ const transferSub = user.transferSub;+ const success = user.success;+ const err = (user.err && user.err.message) || '';+ return `${transferSub},${uid},${fxaEmail},${appleEmail},${success},${err}`;+ }).join('\n');+ fs.writeFileSync(path.resolve(`${Date.now()}_output.csv`), output);
Q: Should this accept an output filename? packages/fxa-auth-server/scripts/apple-transfer-users.js#L20 specifies an output filename in the commander config, but I don't think it's used currently.
comment created time in 9 hours
Pull request review commentmozilla/fxa
feat(apple): Add apple user migrations script
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++'use strict';++// This script is used to import users from Apple into FxA. It consumes a CSV+// containing the Apple `transfer_sub` id and exchanges it for their profile information.+// It then creates/updates the user and links the Apple account to the FxA account.+//+// Usage: node scripts/apple-transfer-users.js -i <input file> -o <output file>++const fs = require('fs');+const path = require('path');+const program = require('commander');++const axios = require('axios');+const random = require('../../lib/crypto/random');+const uuid = require('uuid');++const config = require('../../config').config.getProperties();++const GRANT_TYPE = 'client_credentials';+const SCOPE = 'user.migration';+const USER_MIGRATION_ENDPOINT = 'https://appleid.apple.com/auth/usermigrationinfo';++const APPLE_PROVIDER = 2;++export class AppleUser {+ constructor(email, transferSub, uid, alternateEmails, db) {+ this.email = email;+ this.transferSub = transferSub;+ this.uid = uid;+ this.alternateEmails = alternateEmails || [];+ this.db = db;+ }++ // Exchanges the Apple `transfer_sub` for the user's profile information and+ // moves the user to the new team.+ // Ref: https://developer.apple.com/documentation/sign_in_with_apple/bringing_new_apps_and_users_into_your_team#3559300+ async exchangeIdentifiers(accessToken) {+ try {+ const options = {+ transfer_sub: this.transferSub,+ client_id: config.appleAuthConfig.clientId,+ client_secret: config.appleAuthConfig.clientSecret,+ };+ const res = await axios.post(USER_MIGRATION_ENDPOINT,+ new URLSearchParams(options).toString(),+ {+ headers: {+ Authorization: `Bearer ${accessToken}`,+ },+ },+ );++ // Data here contains `sub`, `email` and `is_private_email`+ this.appleUserInfo = res.data;+ return res.data;+ } catch (err) {+ this.setFailure(err);+ if (err.response && err.response.status === 429) {+ console.error(`Rate limit exceeded, try again later: ${this.transferSub}`);+ } else {+ console.error(`Something went wrong with transfer: ${this.transferSub} ${err}`);+ }+ }+ }++ setSuccess(accountRecord) {+ this.success = true;+ this.accountRecord = accountRecord;+ }++ setFailure(err) {+ this.success = false;+ this.err = err;+ }++ async createLinkedAccount(accountRecord, sub) {+ // If the user already has a linked account, delete it and create a new one.+ await this.db.deleteLinkedAccount(accountRecord.uid, APPLE_PROVIDER);+ await this.db.createLinkedAccount(accountRecord.uid, sub, APPLE_PROVIDER);+ }++ async createUpdateFxAUser() {+ const sub = this.appleUserInfo.sub; // The recipient team-scoped identifier for the user.+ const appleEmail = this.appleUserInfo.email; // The private email address specific to the recipient team. + + // TODO, maybe we should mark this failure+ // const isPrivateEmail = this.appleUserInfo.is_private_email; // Boolean if email is private+ // if (isPrivateEmail) {+ // this.setFailure({ message: 'Apple email is private' });+ // }++ // 1. Check if user exists in FxA via the uid value from Pocket. We should expect+ // the uid to be valid, but if it isn't error out.+ try {+ if (this.uid) {+ const accountRecord = await this.db.account(this.uid);+ await this.createLinkedAccount(accountRecord, sub);+ this.setSuccess(accountRecord);+ return;+ }+ } catch (err) {+ const msg = `Uid not found: ${this.uid}`;+ console.error(msg);+ this.setFailure(err);+ return;+ }++ // 2. Check all emails to see if there exists a match in FxA, link Apple account+ // to the FxA account.+ let accountRecord;+ // Insert email into the front of alternateEmails array, this will be the+ // most likely to exist in FxA.+ this.alternateEmails.unshift(appleEmail);+ this.alternateEmails.unshift(this.email);+ if (this.alternateEmails) {+ for (const email of this.alternateEmails) {+ try {+ accountRecord = await this.db.accountRecord(email);+ break;+ } catch (err) {+ // Ignore+ }+ }+ }+ // There was a match! Link the Apple account to the FxA account.+ if (accountRecord) {+ await this.createLinkedAccount(accountRecord, sub);+ this.setSuccess(accountRecord);+ return;+ }++ // 3. No matches mean this is a completely new FxA user, create the user and+ // link the Apple account to the FxA account.+ try {+ const emailCode = await random.hex(16);+ const authSalt = await random.hex(32);+ const [kA, wrapWrapKb] = await random.hex(32, 32);+ accountRecord = await this.db.createAccount({+ uid: uuid.v4({}, Buffer.alloc(16)).toString('hex'),+ createdAt: Date.now(),+ email: appleEmail,+ emailCode,+ emailVerified: true,+ kA,+ wrapWrapKb,+ authSalt,+ verifierVersion: config.verifierVersion,+ verifyHash: Buffer.alloc(32).toString('hex'),+ verifierSetAt: 0,+ });+ await this.createLinkedAccount(accountRecord, sub);+ this.setSuccess(accountRecord);+ } catch (err) {+ this.setFailure(err);+ }+ }++ async transferUser(accessToken) {+ await this.exchangeIdentifiers(accessToken);+ await this.createUpdateFxAUser(this.appleUserInfo);+ }+}++export class ApplePocketFxAMigration {+ constructor(filename, config, db) {+ this.users = [];+ this.db = db;+ this.filename = filename;+ this.config = config;+ }++ printUsers() {+ console.table(this.users);+ }+ + saveResults() {+ const output = this.users.map((user) => {+ const appleEmail = user.appleUserInfo.email;+ const fxaEmail = user.accountRecord.email;+ const uid = user.accountRecord.uid; // Newly created uid+ const transferSub = user.transferSub;+ const success = user.success;+ const err = (user.err && user.err.message) || '';+ return `${transferSub},${uid},${fxaEmail},${appleEmail},${success},${err}`;+ }).join('\n');+ fs.writeFileSync(path.resolve(`${Date.now()}_output.csv`), output);+ }++ parseCSV() {+ try {+ const input = fs+ .readFileSync(path.resolve(this.filename))+ .toString('utf8');++ if (!input.length) {+ return [];+ }++ // Parse the input file CSV style+ return input.split(/\n/).map((s, index) => {+ if (index === 0) return;++ const delimiter = program.delimiter || ',';
This was the only other instance of program
i found in this file:
https://github.com/mozilla/fxa/blob/ea9bb536bc17407e2965d130e2c88f112166f2bb/packages/fxa-auth-server/scripts/apple-transfer-users/apple-transfer-users.js#L15
Should this be using the unused(?) delimiter
from packages/fxa-auth-server/scripts/apple-transfer-users.js#L7 above?
comment created time in 9 hours
Pull request review commentmozilla/fxa
feat(apple): Add apple user migrations script
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++'use strict';++// This script is used to import users from Apple into FxA. It consumes a CSV+// containing the Apple `transfer_sub` id and exchanges it for their profile information.+// It then creates/updates the user and links the Apple account to the FxA account.+// Example input file: /tests/fixtures/users-apple.csv+//+// Usage: node scripts/apple-transfer-users.js -i <input file> -o <output file>++const {ApplePocketFxAMigration} = require('./apple-transfer-users/apple-transfer-users');++const program = require('commander');++program+ .option('-d, --delimiter [delimiter]', 'Delimiter for input file', ',')+ .option('-o, --output <filename>', 'Output filename to save results to')
Q: Does the program.delimiter
and program.output
get used?
comment created time in 10 hours
Pull request review commentmozilla/blurts-server
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++import React, { ReactElement, use, useState } from "react";+import styles from "./ExposureCard.module.scss";+import { StatusPill } from "./StatusPill";+import { StaticImageData } from "next/image";+import {+ ChevronDown,+ EmailIcon,+ LocationPin,+ MultipleUsers,+ OpenInNew,+ PhoneIcon,+} from "./Icons";+import Image from "next/image";+import { Button } from "./Button";+import Email from "next-auth/providers/email";++export type Props = {+ exposureImg: StaticImageData;+ exposureName: string;+ exposureType: string;+ exposureDetailsLink: string;+ dateFound: string;+ statusPillType: string;+ statusPillContent: string;+};++type DetailsFoundProps = {+ whichExposed: string; // family | email | phone | address+ num: number;+ icon: ReactElement;+};++export const ExposureCard = (props: Props) => {+ const {+ exposureImg,+ exposureName,+ exposureType,+ exposureDetailsLink,+ dateFound,+ statusPillContent,+ statusPillType,+ } = props;++ const [detailsOpen, setDetailsOpen] = useState(false);++ const DetailsFoundItem = (props: DetailsFoundProps) => {+ let headline, description;+ if (props.whichExposed === "family") {+ headline = "Family members";++ description = `We found ${props.num} family member`;+ if (props.num > 1) {+ description = `We found ${props.num} family members`;+ }+ }++ if (props.whichExposed === "email") {+ headline = "Email";++ description = `We found ${props.num} email address`;+ if (props.num > 1) {+ description = `We found ${props.num} email addresses`;+ }+ }++ if (props.whichExposed === "phone") {+ headline = "Phone number";++ description = `We found ${props.num} phone number`;+ if (props.num > 1) {+ description = `We found ${props.num} phone numbers`;+ }+ }++ if (props.whichExposed === "address") {+ headline = "Address";++ description = `We found ${props.num} address`;+ if (props.num > 1) {+ description = `We found ${props.num} addresses`;+ }+ }++ return (+ <>+ <dt>+ <span className={styles.exposureTypeIcon}>{props.icon}</span>+ {headline}+ </dt>+ <dl>{description}</dl>
IIUC, i think <dl>
is supposed to be a parent of <dt>
and <dd>
tags
comment created time in 10 hours
Pull request review commentmozilla/blurts-server
+/** @type { import('@storybook/nextjs').StorybookConfig } */+const config = {+ stories: [+ // "../components/client/stories/**/*.mdx",+ "../components/client/stories/**/*.stories.@(js|jsx|ts|tsx)",
Do we need all these globs, or can we reduce this to just .ts and/or .tsx?
comment created time in 10 hours
Pull request review commentmozilla/blurts-server
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++import React, { ReactElement, use, useState } from "react";+import styles from "./ExposureCard.module.scss";+import { StatusPill } from "./StatusPill";+import { StaticImageData } from "next/image";+import {+ ChevronDown,+ EmailIcon,+ LocationPin,+ MultipleUsers,+ OpenInNew,+ PhoneIcon,+} from "./Icons";+import Image from "next/image";+import { Button } from "./Button";+import Email from "next-auth/providers/email";++export type Props = {+ exposureImg: StaticImageData;+ exposureName: string;+ exposureType: string;+ exposureDetailsLink: string;+ dateFound: string;+ statusPillType: string;+ statusPillContent: string;+};++type DetailsFoundProps = {+ whichExposed: string; // family | email | phone | address+ num: number;+ icon: ReactElement;+};++export const ExposureCard = (props: Props) => {+ const {+ exposureImg,+ exposureName,+ exposureType,+ exposureDetailsLink,+ dateFound,+ statusPillContent,+ statusPillType,+ } = props;++ const [detailsOpen, setDetailsOpen] = useState(false);++ const DetailsFoundItem = (props: DetailsFoundProps) => {+ let headline, description;+ if (props.whichExposed === "family") {+ headline = "Family members";++ description = `We found ${props.num} family member`;+ if (props.num > 1) {+ description = `We found ${props.num} family members`;+ }+ }++ if (props.whichExposed === "email") {+ headline = "Email";++ description = `We found ${props.num} email address`;+ if (props.num > 1) {+ description = `We found ${props.num} email addresses`;+ }+ }++ if (props.whichExposed === "phone") {+ headline = "Phone number";++ description = `We found ${props.num} phone number`;+ if (props.num > 1) {+ description = `We found ${props.num} phone numbers`;+ }+ }++ if (props.whichExposed === "address") {+ headline = "Address";++ description = `We found ${props.num} address`;+ if (props.num > 1) {+ description = `We found ${props.num} addresses`;+ }+ }++ return (+ <>+ <dt>+ <span className={styles.exposureTypeIcon}>{props.icon}</span>+ {headline}+ </dt>+ <dl>{description}</dl>+ </>+ );+ };+ const elementCard = (+ <div>
nit: Do we need this wrapper div
, or can we just use the child <div className={styles.exposureCard}>
as the top level container?
comment created time in 10 hours
Pull request review commentmozilla/blurts-server
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++import React, { ReactElement, use, useState } from "react";+import styles from "./ExposureCard.module.scss";+import { StatusPill } from "./StatusPill";+import { StaticImageData } from "next/image";+import {+ ChevronDown,+ EmailIcon,+ LocationPin,+ MultipleUsers,+ OpenInNew,+ PhoneIcon,+} from "./Icons";+import Image from "next/image";+import { Button } from "./Button";+import Email from "next-auth/providers/email";++export type Props = {+ exposureImg: StaticImageData;+ exposureName: string;+ exposureType: string;+ exposureDetailsLink: string;+ dateFound: string;+ statusPillType: string;+ statusPillContent: string;+};++type DetailsFoundProps = {+ whichExposed: string; // family | email | phone | address+ num: number;+ icon: ReactElement;+};++export const ExposureCard = (props: Props) => {+ const {+ exposureImg,+ exposureName,+ exposureType,+ exposureDetailsLink,+ dateFound,+ statusPillContent,+ statusPillType,+ } = props;++ const [detailsOpen, setDetailsOpen] = useState(false);++ const DetailsFoundItem = (props: DetailsFoundProps) => {+ let headline, description;+ if (props.whichExposed === "family") {+ headline = "Family members";
Not sure if we want to add TODO
statements to refactor all this for l10n-ing.
comment created time in 10 hours
Pull request review commentmozilla/blurts-server
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++import React, { ReactElement, use, useState } from "react";+import styles from "./ExposureCard.module.scss";+import { StatusPill } from "./StatusPill";+import { StaticImageData } from "next/image";+import {+ ChevronDown,+ EmailIcon,+ LocationPin,+ MultipleUsers,+ OpenInNew,+ PhoneIcon,+} from "./Icons";+import Image from "next/image";+import { Button } from "./Button";+import Email from "next-auth/providers/email";++export type Props = {+ exposureImg: StaticImageData;+ exposureName: string;+ exposureType: string;+ exposureDetailsLink: string;+ dateFound: string;+ statusPillType: string;+ statusPillContent: string;+};++type DetailsFoundProps = {+ whichExposed: string; // family | email | phone | address+ num: number;+ icon: ReactElement;+};++export const ExposureCard = (props: Props) => {+ const {+ exposureImg,+ exposureName,+ exposureType,+ exposureDetailsLink,+ dateFound,+ statusPillContent,+ statusPillType,+ } = props;++ const [detailsOpen, setDetailsOpen] = useState(false);++ const DetailsFoundItem = (props: DetailsFoundProps) => {+ let headline, description;+ if (props.whichExposed === "family") {+ headline = "Family members";++ description = `We found ${props.num} family member`;+ if (props.num > 1) {+ description = `We found ${props.num} family members`;+ }+ }++ if (props.whichExposed === "email") {+ headline = "Email";++ description = `We found ${props.num} email address`;+ if (props.num > 1) {+ description = `We found ${props.num} email addresses`;+ }+ }++ if (props.whichExposed === "phone") {+ headline = "Phone number";++ description = `We found ${props.num} phone number`;+ if (props.num > 1) {+ description = `We found ${props.num} phone numbers`;+ }+ }++ if (props.whichExposed === "address") {+ headline = "Address";++ description = `We found ${props.num} address`;+ if (props.num > 1) {+ description = `We found ${props.num} addresses`;+ }+ }++ return (+ <>+ <dt>+ <span className={styles.exposureTypeIcon}>{props.icon}</span>+ {headline}+ </dt>+ <dl>{description}</dl>+ </>+ );+ };+ const elementCard = (+ <div>+ <div className={styles.exposureCard}>+ <div className={styles.exposureHeader}>
nit: should this be <div>
or <header>
?
comment created time in 10 hours
Pull request review commentmozilla/blurts-server
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++import React, { ReactElement, use, useState } from "react";+import styles from "./ExposureCard.module.scss";+import { StatusPill } from "./StatusPill";+import { StaticImageData } from "next/image";+import {+ ChevronDown,+ EmailIcon,+ LocationPin,+ MultipleUsers,+ OpenInNew,+ PhoneIcon,+} from "./Icons";+import Image from "next/image";
Merge w/ L8 above?
comment created time in 10 hours
Pull request review commentmozilla/blurts-server
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++import React, { ReactElement, use, useState } from "react";+import styles from "./ExposureCard.module.scss";+import { StatusPill } from "./StatusPill";+import { StaticImageData } from "next/image";+import {+ ChevronDown,+ EmailIcon,+ LocationPin,+ MultipleUsers,+ OpenInNew,+ PhoneIcon,+} from "./Icons";+import Image from "next/image";+import { Button } from "./Button";+import Email from "next-auth/providers/email";++export type Props = {+ exposureImg: StaticImageData;+ exposureName: string;+ exposureType: string;
re: "*Type" suffix should these be generic string;
or enums?
(Same w/ L27 below for the statusPillType
declaration)
comment created time in 10 hours
Pull request review commentmozilla/blurts-server
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++import React, { ReactElement, use, useState } from "react";+import styles from "./ExposureCard.module.scss";+import { StatusPill } from "./StatusPill";+import { StaticImageData } from "next/image";+import {+ ChevronDown,+ EmailIcon,+ LocationPin,+ MultipleUsers,+ OpenInNew,+ PhoneIcon,+} from "./Icons";+import Image from "next/image";+import { Button } from "./Button";+import Email from "next-auth/providers/email";++export type Props = {
Same as above (or below, or somewhere), should this be renamed to "ExposureCardProps" (not sure if anybody is importing these props or not)
comment created time in 10 hours
Pull request review commentmozilla/blurts-server
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++import type { StorybookConfig } from "@storybook/nextjs";++const config: StorybookConfig = {+ stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
Tiny nit: do we need all these globs, or can we standardize on using .ts and/or .tsx files?
git ls-files src/**/*.stories.*
src/app/(nextjs_migration)/components/client/stories/Button.stories.ts
src/app/(nextjs_migration)/components/client/stories/ExposureCard.stories.ts
src/app/(nextjs_migration)/components/client/stories/StatusPill.stories.ts
comment created time in 11 hours
Pull request review commentmozilla/blurts-server
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++/* stylelint-disable max-line-length */++/* Font optimization resources:+https://web.dev/font-best-practices/+https://web.dev/css-size-adjust/+https://barrd.dev/article/create-a-variable-font-subset-for-smaller-file-size/+https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap+*/++/* Arial font-size matching for minimal CLS */++@font-face {+ font-family: Inter-fallback;
Not relevant to much probably, but I was reading through the Next.js docs the other day and came across this: https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
import { Inter } from 'next/font/google';
// If loading a variable font, you don't need to specify the font weight
const inter = Inter({ subsets: ['latin'] });
Not sure if that's worth filing a separate ticket/spike for to investigate in the future if we could potentially do font optimizing.
I also didn't see a Metropolis font on Google Fonts, so wasnt sure how to do potential font optimizing for locally hosted fonts.
comment created time in 13 hours
Pull request review commentmozilla/blurts-server
+/* This Source Code Form is subject to the terms of the Mozilla Public+ * License, v. 2.0. If a copy of the MPL was not distributed with this+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */++import React, { ReactElement, use, useState } from "react";+import styles from "./ExposureCard.module.scss";+import { StatusPill } from "./StatusPill";+import { StaticImageData } from "next/image";+import { ChevronDown } from "./Icons";+import Image from "next/image";+import { Button } from "./Button";++export type Props = {+ exposureImg: StaticImageData;+ exposureName: string;+ exposureType: string;+ exposureDetailsLink: string;+ dateFound: string;+ statusPillType: string;+ statusPillContent: string;+};++type DetailsFoundProps = {+ whichExposed: string; // family | email | phone | address+ num: number;+};++export const ExposureCard = (props: Props) => {+ const {+ exposureImg,+ exposureName,+ exposureType,+ exposureDetailsLink,+ dateFound,+ statusPillContent,+ statusPillType,+ } = props;++ const [detailsOpen, setDetailsOpen] = useState(false);++ const DetailsFoundItem = (props: DetailsFoundProps) => {+ let headline, description;+ if (props.whichExposed === "family") {+ headline = "Family members";++ description = `We found ${props.num} family member`;+ if (props.num > 1) {+ description = `We found ${props.num} family members`;+ }+ }++ if (props.whichExposed === "email") {+ headline = "Email";++ description = `We found ${props.num} email address`;+ if (props.num > 1) {+ description = `We found ${props.num} email addresses`;+ }+ }++ if (props.whichExposed === "phone") {+ headline = "Phone number";++ description = `We found ${props.num} phone number`;+ if (props.num > 1) {+ description = `We found ${props.num} phone numbers`;+ }+ }++ if (props.whichExposed === "address") {+ headline = "Phone number";++ description = `We found ${props.num} address`;+ if (props.num > 1) {+ description = `We found ${props.num} addresses`;+ }+ }++ return (+ <>+ <dt>{headline}</dt>+ <dl>{description}</dl>
I dont think this is semantically correct. IIUC, per https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl, it should be:
<dl>
<dt>Firefox</dt>
<dd>
A free, open source, cross-platform, graphical web browser developed by the
Mozilla Corporation and hundreds of volunteers.
</dd>
<!-- Other terms and descriptions -->
<dt>...</dt>
<dd>...</dd>
</dl>
So not sure if this should be:
<dl>
<dt>{headline}</dt>
<dd>{description}</dd>
</dl>
Although that just always creates a description list with one list item. Or if it just returns this (like you had, but with <dd>
instead of <dl>
) and then the parent container wraps it in a <dl>
somewhere:
<>
<dt>{headline}</dt>
<dd>{description}</dd>
</>
comment created time in 16 hours
Pull request review commentmozilla/blurts-server
Store new users in the database
import NextAuth, { AuthOptions } from "next-auth"; import mozlog from "../../../../utils/log.js"; import AppConstants from "../../../../appConstants.js";+import {+ getSubscriberByEmail,+ updateFxAData,+} from "../../../../db/tables/subscribers.js";+import { addSubscriber } from "../../../../db/tables/emailAddresses.js";+import { getBreaches } from "../../../functions/server/getBreaches";+import { getBreachIcons } from "../../../functions/server/getBreaches";
We've been looking at using prettier-plugin-organize-imports in FxA-land, but haven't tried setting it up yet across the monorepo. But might be interesting if it catches unused and duplicated imports (and we adopt Prettier after we finish Next.js migration).
comment created time in 2 days
Pull request review commentmozilla/blurts-server
Store new users in the database
export const authOptions: AuthOptions = { callbacks: { async jwt({ token, account, profile, trigger }) { if (profile) {- token.locale = profile.locale;- token.twoFactorAuthentication = profile.twoFactorAuthentication;- token.metricsEnabled = profile.metricsEnabled;- token.avatar = profile.avatar;- token.avatarDefault = profile.avatarDefault;+ token.fxa = {+ locale: profile.locale,+ twoFactorAuthentication: profile.twoFactorAuthentication,+ metricsEnabled: profile.metricsEnabled,+ avatar: profile.avatar,+ avatarDefault: profile.avatarDefault,+ };+ }+ if (account) {+ // We're signing in with FxA; store user in database if not present yet.+ log.debug("fxa-confirmed-fxaUser", account);++ // Note: we could create an [Adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)+ // to store the user in the database, but by doing it in the callback,+ // we can also store FxA account data. We also don't have to worry+ // about model mismatches (i.e. Next-Auth expecting one User to have+ // multiple Accounts at multiple providers).+ const email = profile?.email;+ const existingUser = await getSubscriberByEmail(email);
OK... So if we have an if (profile) {...}
block above on L95-103, and we assume that could be falsey. Is it possible that profile?.email
could be falsy/undefined here, in which case we might have something like const email = undefined;
here, and if so, what would await getSubscriberByEmail(undefined)
do? or do we need additional checks and exit early? Or if we do have a truthy account
are we reasonably confident that profile?.email
isn't going to be undefined
?
comment created time in 2 days
Pull request review commentmozilla/blurts-server
Store new users in the database
export const authOptions: AuthOptions = { }, clientId: AppConstants.OAUTH_CLIENT_ID, clientSecret: AppConstants.OAUTH_CLIENT_SECRET,+ // Parse data returned by FxA's /userinfo/ profile: async (profile: FxaProfile) => {+ log.debug("fxa-confirmed-profile-data", profile);
q: Was this for debugging? Got me a bit scared that we might be logging some PII to some log somewhere.
comment created time in 2 days
Pull request review commentmozilla/blurts-server
Store new users in the database
import NextAuth, { AuthOptions } from "next-auth"; import mozlog from "../../../../utils/log.js"; import AppConstants from "../../../../appConstants.js";+import {+ getSubscriberByEmail,+ updateFxAData,+} from "../../../../db/tables/subscribers.js";+import { addSubscriber } from "../../../../db/tables/emailAddresses.js";+import { getBreaches } from "../../../functions/server/getBreaches";+import { getBreachIcons } from "../../../functions/server/getBreaches";
Q: can these two import
s be merged?
comment created time in 2 days