password reset in graphcool-framework

// setup a subscription to listen for your password reset trigger
// SendPasswordResetEmail.graphql
subscription {
  User(
    filter: {
      mutation_in: [UPDATED]
      updatedFields_contains: "resetToken"
      node: { resetToken_not: "null" }
    }
  ) {
    updatedFields
    node {
      id
      email
      resetToken
      firstname
      lastname
      company {
        id
        host
      }
    }
  }
}
// send the email when a user record has a resetToken added to it 
// SendPasswordResetEmail.js
const fetch = require('isomorphic-fetch');
const Base64 = require('Base64');
const FormData = require('form-data');
const apiKey = 'api:key-YOUR_MAIL_GUN_API_KEY';
const url = 'https://api.mailgun.net/v3/your.mailgun.url/messages';
module.exports = async event => {
  try {
    const {
      email, resetToken, firstname, company: { host },
    } = event.data.User.node;
    if (!resetToken) {
      return;
    }
    const resetUrl = `${host}/reset/${resetToken}`; // this would be specific to your app
    const message = `
    <p>Hi ${firstname}, </p>
    <p>We've received a password reset request for the account with this email. <a href="${resetUrl}">Click here</a> to reset your password.</p>
    
  `;
    const form = new FormData();
    form.append('from', 'Your Email Name <no-reply@yourwebsite.com>');
    form.append('to', email);
    form.append('subject', 'Password Reset Confirmation');
    form.append('html', message);
    // 5. Send request to Mailgun API
    // eslint-disable-next-line compat/compat
    await fetch(url, {
      headers: {
        Authorization: `Basic ${Base64.btoa(apiKey)}`,
      },
      method: 'POST',
      body: form,
    });
    return { data: { status: 'success' } };
  } catch (error) {
    return {
      error: e.message,
    };
  }
};
// TriggerPasswordReset.graphql - call this mutation from publically accessible form with `email` input 
type triggerPasswordResetPayload {
  id: ID!
}
extend type Mutation {
  triggerPasswordReset(email: String!): triggerPasswordResetPayload
}
// TriggerPasswordReset.js
process.env.TZ = 'UTC';
const crypto = require('crypto');
const { fromEvent } = require('graphcool-lib');
module.exports = event => {
  const { email } = event.data;
  const graphcool = fromEvent(event);
  const api = graphcool.api('simple/v1');
  const generateResetToken = () => crypto.randomBytes(20).toString('hex');
  const generateExpiryDate = () => {
    const now = new Date();
    return new Date(now.getTime() + 3600000).toISOString();
  };
  const getGraphcoolUser = email => api
    .request(`
    query {
      User(email: "${email}"){
        id
      }
    }`)
    .then(userQueryResult => {
      if (userQueryResult.error) {
        return Promise.reject(userQueryResult.error);
      }
      return userQueryResult.User;
    });
  const toggleReset = graphcoolUserId => api.request(`
      mutation {
        updateUser(
          id: "${graphcoolUserId}",
          resetToken: "${generateResetToken()}",
          resetExpires: "${generateExpiryDate()}"
        ) {
          id
        }
      }
    `);
  return getGraphcoolUser(email)
    .then(graphcoolUser => {
      if (graphcoolUser === null) {
        return Promise.reject(Error('An unexpected error occurred.')); // returning same generic error so user doesnt see if its a real user or not
      }
      return toggleReset(graphcoolUser.id);
    })
    .then(response => {
      const { id } = response.updateUser;
      return { data: { id } };
    })
    .catch(error => {
      console.log(error);
      // don't expose error message to client!
      return { error: 'An unexpected error occured.' };
    });
};
// last but not least, reset the password with new + token
// PasswordReset.graphql
type ResetPasswordPayload {
  id: ID!
}
extend type Mutation {
  resetPassword(resetToken: String!, password: String!): ResetPasswordPayload
}
// PasswordReset.js
const { fromEvent } = require('graphcool-lib');
const bcrypt = require('bcryptjs');
module.exports = event => {
  const { resetToken } = event.data;
  const newPassword = event.data.password;
  const graphcool = fromEvent(event);
  const api = graphcool.api('simple/v1');
  const saltRounds = 10;
  const getUserWithToken = resetToken =>
    api
      .request(`
      query {
        User(resetToken: "${resetToken}") {
          id
          resetExpires
        }
      }`)
      .then(userQueryResult => {
        if (userQueryResult.error) {
          return Promise.reject(userQueryResult.error);
        } else if (
          !userQueryResult.User ||
          !userQueryResult.User.id ||
          !userQueryResult.User.resetExpires
        ) {
          return Promise.reject(new Error('Not a valid token'));
        }
        return userQueryResult.User;
      });
  const updatePassword = (id, newPasswordHash) =>
    api
      .request(`
      mutation {
        updateUser(
          id: "${id}",
          password: "${newPasswordHash}",
          resetToken: null,
          resetExpires: null
        ) {
          id
        }
      }`)
      .then(userMutationResult => userMutationResult.updateUser.id);
  return getUserWithToken(resetToken)
    .then(graphcoolUser => {
      console.log(graphcoolUser);
      const userId = graphcoolUser.id;
      const { resetExpires } = graphcoolUser;
      if (new Date() > new Date(resetExpires)) {
        return Promise.reject(Error('Token expired.'));
      }
      return bcrypt
        .hash(newPassword, saltRounds)
        .then(hash => updatePassword(userId, hash))
        .then(id => ({ data: { id } }))
        .catch(error => ({ error: error.toString() }));
    })
    .catch(error => {
      console.log(error);
      // don't expose error message to client!
      return { error: 'An unexpected error occured.' };
    });
};