diff --git a/functions/src/auth2Users.ts b/functions/src/auth2Users.ts index 05fd5e5..972fdbb 100755 --- a/functions/src/auth2Users.ts +++ b/functions/src/auth2Users.ts @@ -1,4 +1,6 @@ import * as Admin from 'firebase-admin' +import { Firestore } from 'firebase-admin/firestore' +import { Auth } from 'firebase-admin/auth' import { UserRecord } from 'firebase-functions/lib/providers/auth' import { info, error } from "firebase-functions/logger" import { User } from './User' @@ -8,88 +10,98 @@ import got from 'got' const moment = require('moment') const gravatar = require('gravatar') - export interface Auth2UsersOptions { syncGravatar: boolean } -const Auth2Users = (admin: Admin.app.App) => { - const firestore = admin.firestore() - const auth = admin.auth() +export default class Auth2Users { + private firestore: Firestore + private auth: Auth - const listAllUsers = async (options: Auth2UsersOptions, nextPageToken?: string) => { - // List batch of users, 1000 at a time. - const listUsersResult = await auth.listUsers(1000, nextPageToken) - // Since this function is called upon creation of a new account, we want to - // sync the newest acccounts first. - listUsersResult.users.sort((userRecord1: UserRecord, userRecord2: UserRecord) => { - const createdAt1 = moment(userRecord1.metadata.creationTime) - const createdAt2 = moment(userRecord2.metadata.creationTime) - return createdAt2.diff(createdAt1) - }) - await each(listUsersResult.users, async (userRecord: UserRecord) => { - try { - const { - uid, - email, - emailVerified, - displayName, - metadata: { creationTime, lastSignInTime }, - } = userRecord + constructor(admin: Admin.app.App) { + this.firestore = admin.firestore() + this.auth = admin.auth() + } + async Sync(userRecord: UserRecord, options: Auth2UsersOptions) { + try { + const { + uid, + email, + emailVerified, + displayName, + metadata: { creationTime, lastSignInTime }, + } = userRecord - const createdAt = moment(creationTime) - .utc() - .format() - const lastSignedInAt: string = moment(lastSignInTime) - .utc() - .format() - const userRef = firestore.doc(`users/${uid}`) - const data: User = { - uid, - createdAt, - email: email || '', - emailVerified, - displayName: displayName || '', - lastSignedInAt - } - if (options.syncGravatar) { - let hasGravatar = false - const gravatarUrl = gravatar.url(email, { - protocol: 'https', - default: '404' - }) - try { - await got.get(gravatarUrl) - info('found gravatar.', { gravatarUrl }) - hasGravatar = true - } catch (err) { - info('Error while fetching gravatar.', { gravatarUrl, error: err }) - } - data.gravatarUrl = hasGravatar ? gravatarUrl : null - } + const createdAt = moment(creationTime) + .utc() + .format() + const lastSignedInAt: string = moment(lastSignInTime) + .utc() + .format() + const userRef = this.firestore.doc(`users/${uid}`) + const data: User = { + uid, + createdAt, + email: email || '', + emailVerified, + displayName: displayName || '', + lastSignedInAt + } - await userRef.set(data, { merge: true }) - info(`Updated ${uid} ${createdAt} ${lastSignedInAt}`) - } catch (err) { - error('Error while syncing auth2user.', { 'uid': userRecord.uid, 'error': err }); + if (options.syncGravatar) { + let hasGravatar = false + const gravatarUrl = gravatar.url(email, { + protocol: 'https', + default: '404' + }) + try { + await got.get(gravatarUrl) + info('found gravatar.', { gravatarUrl }) + hasGravatar = true + } catch (err) { + info('Error while fetching gravatar.', { gravatarUrl, error: err }) + } + data.gravatarUrl = hasGravatar ? gravatarUrl : null } - }) - info( - `checking listUsersResult.pageToken: ${listUsersResult.pageToken} num of results previously found: ${listUsersResult.users.length}`) - if (listUsersResult.pageToken) { - // List next batch of users. - await listAllUsers(options, listUsersResult.pageToken) - } else { - info('Done. Exiting...') - return + + await userRef.set(data, { merge: true }) + info(`Updated ${uid} ${createdAt} ${lastSignedInAt}`) + } catch (err) { + error('Error while syncing auth2user.', { 'uid': userRecord.uid, 'error': err }); } } - // Start listing users from the beginning, 1000 at a time. + async SyncAll(options: Auth2UsersOptions) { - return listAllUsers + const listAllUsers = async (nextPageToken?: string) => { + // List batch of users, 1000 at a time. + const listUsersResult = await this.auth.listUsers(1000, nextPageToken) + // We want to sync the most active acccounts first. + listUsersResult.users.sort((userRecord1: UserRecord, userRecord2: UserRecord) => { + const lastSignInTime1 = moment(userRecord1.metadata.lastSignInTime) + const lastSignInTime2 = moment(userRecord2.metadata.lastSignInTime) + return lastSignInTime2.diff(lastSignInTime1) + }) + await each(listUsersResult.users, async (userRecord: UserRecord) => { + await this.Sync(userRecord, options); + }) + info( + `checking listUsersResult.pageToken: ${listUsersResult.pageToken} num of results previously found: ${listUsersResult.users.length}`) + if (listUsersResult.pageToken) { + // List next batch of users. + await listAllUsers(listUsersResult.pageToken) + } else { + info('Done. Exiting...') + return + } + } + + // Start listing users from the beginning, 1000 at a time. + + return listAllUsers() + } } -export default Auth2Users + diff --git a/functions/src/auth2UsersCMD.ts b/functions/src/auth2UsersCMD.ts index 9af5a60..7af3346 100755 --- a/functions/src/auth2UsersCMD.ts +++ b/functions/src/auth2UsersCMD.ts @@ -10,8 +10,8 @@ const admin: Admin.app.App = Admin.initializeApp({ databaseURL: 'https://belmont-runners-1548537264040.firebaseio.com' }) -const auth2Users = Auth2Users(admin) -auth2Users({ syncGravatar: true }) +const auth2Users = new Auth2Users(admin) +auth2Users.SyncAll({ syncGravatar: true }) .then((res) => { console.info('done', res) return diff --git a/functions/src/index.ts b/functions/src/index.ts index 96c35a1..60430f1 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,5 +1,5 @@ import AddContact from './addContact' -import Auth2Users, { Auth2UsersOptions } from './auth2Users' +import Auth2Users from './auth2Users' import Contacts2MailChimp from './contacts2MailChimp' import DeleteUser from './deleteUser' import GenerateICal from './generateICal' @@ -11,12 +11,14 @@ import UpdateEvents from './updateEvents' import Users2Contacts from './users2Contacts' import * as functions from 'firebase-functions' import { info, error } from "firebase-functions/logger" +import { UserRecord } from 'firebase-functions/lib/providers/auth' import * as Admin from 'firebase-admin' import { EMAIL } from './fields' import { props } from 'bluebird' +import { Firestore } from 'firebase-admin/firestore' const admin: Admin.app.App = Admin.initializeApp() -const firestore = admin.firestore() +const firestore: Firestore = admin.firestore() const apiKey = functions.config().mailchimp.apikey const { app_id, city_id } = functions.config().openweathermap @@ -26,7 +28,7 @@ const { } = functions.config().stripe const addContactImpl = AddContact(admin) -const auth2Users = Auth2Users(admin) +const auth2Users = new Auth2Users(admin) const contacts2MailChimp = Contacts2MailChimp(admin, apiKey) const deleteUserImpl = DeleteUser(admin, apiKey) const generateICal = GenerateICal() @@ -40,38 +42,29 @@ const stripeImpl = Stripe(admin, { const users2Contacts = Users2Contacts(admin) const updateEvents = UpdateEvents(admin, app_id, city_id) -const AUTH_2_USERS_TIMEOUT_IN_SECONDS = 180 - -const auth2UsersExec = (options: Auth2UsersOptions) => async () => { - try { - await auth2Users(options) - info('Calling process.exit(0)') - setTimeout(function () { - process.exit(0) - }, 5000) - } catch (err) { - error('While calling auth2UsersExec', { err }) - info('Calling process.exit(1)') - setTimeout(function () { - process.exit(1) - }, 5000) - } -} - -export const purgeUsersUnder13CronJob = functions.pubsub +const ITERATION_ON_ACCOUNTS_TIMEOUT_IN_SECONDS = 180 + +export const purgeUsersUnder13CronJob = functions + .runWith({ timeoutSeconds: ITERATION_ON_ACCOUNTS_TIMEOUT_IN_SECONDS }) + .pubsub .schedule('10 */6 * * *') .onRun(async () => await purgeUsersUnder13()) + export const auth2UsersCronJob = functions - .runWith({ timeoutSeconds: AUTH_2_USERS_TIMEOUT_IN_SECONDS }) + .runWith({ timeoutSeconds: ITERATION_ON_ACCOUNTS_TIMEOUT_IN_SECONDS }) .pubsub .schedule('20 */6 * * *') - .onRun(async () => await auth2UsersExec({ syncGravatar: true })) + .onRun(async () => await auth2Users.SyncAll({ syncGravatar: true })) + export const auth2UsersOnCreate = functions - .runWith({ timeoutSeconds: AUTH_2_USERS_TIMEOUT_IN_SECONDS }) + .runWith({ timeoutSeconds: ITERATION_ON_ACCOUNTS_TIMEOUT_IN_SECONDS }) .auth - .user().onCreate(auth2UsersExec({ syncGravatar: false })) + .user() + .onCreate(async (userRecord: UserRecord) => await auth2Users.Sync(userRecord, { syncGravatar: false })) -export const users2ContactsCronJob = functions.pubsub +export const users2ContactsCronJob = functions + .runWith({ timeoutSeconds: ITERATION_ON_ACCOUNTS_TIMEOUT_IN_SECONDS }) + .pubsub .schedule('30 */6 * * *') .onRun(async () => { try { @@ -83,8 +76,9 @@ export const users2ContactsCronJob = functions.pubsub }) export const contacts2MailChimpCronJob = functions - .runWith({ timeoutSeconds: 180 }) - .pubsub.schedule('40 */6 * * *') + .runWith({ timeoutSeconds: ITERATION_ON_ACCOUNTS_TIMEOUT_IN_SECONDS }) + .pubsub + .schedule('40 */6 * * *') .onRun(async () => { try { await contacts2MailChimp() @@ -101,18 +95,21 @@ export const contacts2MailChimpCronJob = functions } }) -export const updateEventsCronJob = functions.pubsub +export const updateEventsCronJob = functions + .pubsub .schedule('*/20 * * * *') .onRun(async () => await updateEvents()) export const waiver = functions - .https.onRequest(async (req: functions.https.Request, res: functions.Response) => { + .https + .onRequest(async (req: functions.https.Request, res: functions.Response) => { res.redirect('https://docs.google.com/forms/d/e/1FAIpQLSfYxlbWAzK1jAcdE_5-ijxORNVz2YU4BdSVt2Dk-DByncIEkw/viewform') }) export const ical = functions .runWith({ memory: '512MB' }) - .https.onRequest(async (req: functions.https.Request, res: functions.Response) => { + .https + .onRequest(async (req: functions.https.Request, res: functions.Response) => { try { const body = await generateICal() res.set({ @@ -136,19 +133,24 @@ export const ical = functions } }) -export const stripe = functions.runWith({ memory: '512MB' }).https.onCall(stripeImpl) +export const stripe = functions + .runWith({ memory: '512MB' }) + .https.onCall(stripeImpl) export const addContact = functions .runWith({ memory: '512MB' }) - .https.onCall(addContactImpl) + .https + .onCall(addContactImpl) export const getMembers = functions .runWith({ timeoutSeconds: 30, memory: '512MB' }) - .https.onCall(getMembersImpl) + .https + .onCall(getMembersImpl) export const deleteUser = functions .runWith({ timeoutSeconds: 30, memory: '512MB' }) - .https.onCall(async (data, context) => { + .https + .onCall(async (data, context) => { if (!context || !context.auth || !context.auth.uid) { throw new functions.https.HttpsError( 'unauthenticated', @@ -182,6 +184,8 @@ export const deleteUser = functions await deleteUserImpl({ uid: targetUID, email: targetEmail }) }) -export const sendMembershipRemindersCronJob = functions.pubsub +export const sendMembershipRemindersCronJob = functions + .runWith({ timeoutSeconds: ITERATION_ON_ACCOUNTS_TIMEOUT_IN_SECONDS }) + .pubsub .schedule('0 19 * * *') .onRun(async () => await sendMembershipReminders()) diff --git a/functions/src/users2Contacts.ts b/functions/src/users2Contacts.ts index 41c7145..df7faf1 100755 --- a/functions/src/users2Contacts.ts +++ b/functions/src/users2Contacts.ts @@ -5,7 +5,6 @@ import calc from './membershipUtils' import { ARRAY_KEY, UID } from './fields' import { props } from 'bluebird' -const normalizeEmail = require('normalize-email') const _ = require('underscore') const Users2Contacts = (admin: Admin.app.App) => { @@ -55,7 +54,9 @@ const Users2Contacts = (admin: Admin.app.App) => { */ contacts.forEach((contact: Contact) => { const foundUser: User | undefined = users.find(user => { - return normalizeEmail(contact.email) === normalizeEmail(user.email) + // Do not "normalize" the email addresses since some users may have + // created multiple different accounts with the same normalized email. + return contact.email.trim().toLowerCase() === user.email.trim().toLowerCase() }) if (foundUser) { contact.uid = foundUser.uid