Giter Club home page Giter Club logo

Comments (14)

maxdarque avatar maxdarque commented on May 28, 2024 6

I've written a forget password resolver which sends a link (which incorporates a password reset code) to the user's email which they then need to click on to reset their password

GraphQL Type

type PasswordResetCode @model {
  id: ID! @isUnique
  createdAt: DateTime!
  user: User @relation(name: "PasswordResetCodeOnUser")
}

Resolver

type SendResetPasswordEmailPayload {
  result: Boolean!
}

extend type Mutation {
  sendResetPasswordEmail(email: String!): SendResetPasswordEmailPayload
}

sendResetPasswordEmail.ts

// const sendResetPasswordEmail = gql`
// mutation sendResetPasswordEmail($email: String!) {
//   sendResetPasswordEmail(email: $email) {
//     result
//   }
// }
// `

import { fromEvent, FunctionEvent } from 'graphcool-lib'
import { GraphQLClient } from 'graphql-request'

// 1. Import npm modules
import * as fetch from 'isomorphic-fetch';
import * as Base64 from 'Base64'
import * as FormData from 'form-data'

interface EventData {
  email: string
}

interface User {
  id: string
  name: string
  email: string
  emailVerified: string
}

interface PasswordResetCode {
  id: string
}

// 2. Mailgun data
const MAILGUN_API_KEY = process.env['MAILGUN_API_KEY'];
const MAILGUN_SUBDOMAIN = process.env['MAILGUN_SUBDOMAIN'];
const PASSWORD_RESET_URL = process.env['PASSWORD_RESET_URL'];

const apiKey = `api:key-${MAILGUN_API_KEY}`;
const mailgunUrl = `https://api.mailgun.net/v3/${MAILGUN_SUBDOMAIN}/messages`;

export default async (event: FunctionEvent<EventData>) => {
  try {
    // create simple api client
    const { email } = event.data
    const api = fromEvent(event).api('simple/v1');

    // get user by email
    const user: User = await getUserByEmail(api, email)
    .then(r => r.User)

    // no user with this email
    if (!user) {
      return { error: 'Error on password reset' }
    }

    // check if email has been verified
    if (!user.emailVerified) {
      return { error: 'Email not verified!' }
    }

    const passwordResetCode: string = await createPasswordResetCode(api,user.id)

    // no data with this response
    if (!passwordResetCode) {
      return { error: 'error on createPasswordResetCode' }
    }

    const passwordResetUrl =`${PASSWORD_RESET_URL}/?passwordResetCode=${passwordResetCode}`;
    
    // // 3. Prepare body of POST request
    const form = new FormData()
    form.append('from', `<[email protected]>`)
    form.append('to', `${user.name} <${user.email}>`)
    form.append('subject', 'Password reset link')
    form.append('text', `Dear ${user.name} \n\n A request to reset your password has been submitted. If this was not you please contact us immediately on [email protected] \n\n Please click on the following link to verify your email: ${passwordResetUrl} \n\n Or enter the following code: ${passwordResetCode} \n\n Thank you! \n\nTeam`)

    // // 4. Send request to Mailgun API
    const resultOfMailGunPost = await fetch(mailgunUrl, {
      headers: { 'Authorization': `Basic ${Base64.btoa(apiKey)}`},
      method: 'POST',
      body: form
    }).then( res => res )

    if (!resultOfMailGunPost) {
      return { error: 'Failed to send email with mailgun' }
    }

    return { data: { result: true } }

    // return resultOfMailGunPost;
  } catch (e) {
    console.log(e)
    return { error: 'An unexpected error occured during creation of passwordResetCode and sending the URL.' }
  } 
}

async function getUserByEmail(api: GraphQLClient, email: string): Promise<{ User }> {
  const query = `
    query getUserByEmail($email: String!) {
      User(email: $email) {
        id
        name
        email
        emailVerified
      }
    }
  `
  const variables = { email }
  return api.request<{ User }>(query, variables)
}

async function createPasswordResetCode(api: GraphQLClient, userId: string): Promise<string> {
  const mutation = `
    mutation createPasswordResetCode($userId: ID) {
      createPasswordResetCode(userId: $userId) {
        id
      }
    }
  `
  const variables = { userId }
  return api.request<{ createPasswordResetCode: PasswordResetCode }>(mutation, variables)
  .then(r => r.createPasswordResetCode.id)
}

ResetPassword resolver

type resetPasswordPayload {
  result: Boolean!
}

extend type Mutation {
  resetPassword(id: ID!): resetPasswordPayload
}

resetPassword.ts

// const resetPassword = gql`
// mutation resetPassword($passwordResetCode: ID!) {
//   resetPassword(id: $passwordResetCode) {
//     result
//   }
// }
// `

import { fromEvent, FunctionEvent } from 'graphcool-lib'
import { GraphQLClient } from 'graphql-request'

import * as moment from 'moment';
import * as bcrypt from 'bcryptjs'
import * as fetch from 'isomorphic-fetch';
import * as Base64 from 'Base64'
import * as FormData from 'form-data'
import * as uuidv4 from 'uuid/v4'

interface EventData {
  id: string
}

interface UpdatedUser {
  id: string
  name: string
  email: string
}

interface User {
  id: string
  name: string
  email: string
}

interface PasswordResetCode {
  id: string
  createdAt: Date
  user: User
}

const SALT_ROUNDS = 10

// 2. Mailgun data
const MAILGUN_API_KEY = process.env['MAILGUN_API_KEY'];
const MAILGUN_SUBDOMAIN = process.env['MAILGUN_SUBDOMAIN'];
const LOGIN_URL = process.env['LOGIN_URL'];

const apiKey = `api:key-${MAILGUN_API_KEY}`;
const mailgunUrl = `https://api.mailgun.net/v3/${MAILGUN_SUBDOMAIN}/messages`;

export default async (event: FunctionEvent<EventData>) => {
  console.log(event)

  try {
    const passwordResetCodeId = event.data.id;
    const api = fromEvent(event).api('simple/v1')

    // use the ID to get the passwordResetItem node
    const passwordResetItem: PasswordResetCode = await getPasswordResetCode(api, passwordResetCodeId)
    .then(r => r.PasswordResetCode)

    // check if it exists
    if (!passwordResetItem || !passwordResetItem.id || !passwordResetItem.user) {
      return { error: `Password reset not successful 1 ${JSON.stringify(passwordResetItem)}` }
    }

    // check the time stamp - 2 hours to reset password
    const now = moment();
    const createdAt = moment(passwordResetItem.createdAt);
    if ( moment(now).isBefore(createdAt.subtract(2,'hours')) ) {
      return { error: 'Password reset not successful 3' }
    }

    // create password hash
    const newPassword = uuidv4();
    const salt = bcrypt.genSaltSync(SALT_ROUNDS)
    const newPasswordHash = await bcrypt.hash(newPassword, salt)

    // everything checks out then change password
    const userWithNewPassword: UpdatedUser = await setUserPassword(api, passwordResetItem.user.id, newPasswordHash)

    // check if user exists
    if (!userWithNewPassword || !userWithNewPassword.id) {
      return { error: 'Password reset not successful 4' }
    }

    const { name, email } = userWithNewPassword
    console.log(email)
    // Prepare body of POST request
    const form = new FormData()
    form.append('from', `<[email protected]>`)
    form.append('to', `${name} <${email}>`)
    form.append('subject', 'XX.com - New password')
    form.append('text', `Dear ${name} \n\n You've reset your password. If this was not you please contact us immediately on [email protected] \n\n Your new password is: ${newPassword}\n\n Thank you! \n\n Team`)

    // // 4. Send request to Mailgun API
    const resultOfMailGunPost = await fetch(mailgunUrl, {
      headers: { 'Authorization': `Basic ${Base64.btoa(apiKey)}`},
      method: 'POST',
      body: form
    }).then( res => res )

    // console.log(resultOfMailGunPost)
    // console.log(resultOfMailGunPost.status)

    if (!resultOfMailGunPost || resultOfMailGunPost.status!==200) {
      return { error: 'Failed to send email with mailgun' }
    }
    
    return { data: { result: true } }
  } catch (e) {
    console.log(e)
    return { error: 'An unexpected error occured during password reset.' }
  }
}

async function getPasswordResetCode(api: GraphQLClient, id: string): Promise<{PasswordResetCode}> {
  const query = `
    query getPasswordResetCode($id: ID!) {
      PasswordResetCode(id: $id) {
        id
        createdAt
        user {
          id
          name
          email
        }
      }
    }
  `
  const variables = { id }
  return api.request<{PasswordResetCode}>(query, variables)
}

async function setUserPassword(api: GraphQLClient, id: string, newPassword: string): Promise<UpdatedUser> {
  const mutation = `
    mutation updateUser($id: ID!, $newPassword: String!) {
      updateUser(id: $id, password: $newPassword) {
        id
        name
        email
      }
    }
  `
  const variables = { id, newPassword }
  return api.request<{updateUser: UpdatedUser}>(mutation, variables)
  .then(r => r.updateUser)
}

from graphcool-templates.

maxdarque avatar maxdarque commented on May 28, 2024 4

@michaelspeed I've changed the wording so I call it Account Activation but it's basically email verification. When a user signs up, I call the sendAccountActivationEmail resolver which sends them an email with a link to activate their account. When they click on the link, it has an code in the url which is processed to activate the account and changes a flag on the user.

I won't include the permission filters but you'll need to limit it to ADMIN users in the db.
types.graphql

type AccountActivationCode @model {
  id: ID! @isUnique
  createdAt: DateTime!
  user: User  @relation(name: "AccountActivationCodeOnUser")
}

graphcool.yml

#send verification email - creates and sends uuid with url to user 
  sendAccountActivationEmail: 
    type: resolver
    schema: functions/accountActivation/sendAccountActivationEmail.graphql
    handler:
      code: 
        src: functions/accountActivation/sendAccountActivationEmail.ts
        environment:
          MAILGUN_API_KEY: XXXXXX
          MAILGUN_SUBDOMAIN: mail.your-domain.com
          ACCOUNT_ACTIVATION_URL: XXXXXX

  # handles verification of user's email using supplied verification code
  activateAccount:
    type: resolver
    schema: functions/accountActivation/activateAccount.graphql
    handler:
      code: functions/accountActivation/activateAccount.ts

sendAccountActivationEmail.graphql

type SendAccountActivationEmailPayload {
  result: Boolean!
}

extend type Mutation {
  sendAccountActivationEmail(id: ID!, name: String!, email: String!): SendAccountActivationEmailPayload
}

sendAccountActivationEmail.ts

import { fromEvent, FunctionEvent } from 'graphcool-lib'
import { GraphQLClient } from 'graphql-request'

import * as validator from 'validator'
import * as fetch from 'isomorphic-fetch';
import * as Base64 from 'Base64'
import * as FormData from 'form-data'

interface EventData {
  id: string
  name: string
  email: string
}

interface AccountActivationCode {
  id: string
}

// 2. Mailgun data
const MAILGUN_API_KEY = process.env['MAILGUN_API_KEY'];
const MAILGUN_SUBDOMAIN = process.env['MAILGUN_SUBDOMAIN'];
const ACCOUNT_ACTIVATION_URL = process.env['ACCOUNT_ACTIVATION_URL'];

const apiKey = `api:key-${MAILGUN_API_KEY}`;
const mailgunUrl = `https://api.mailgun.net/v3/${MAILGUN_SUBDOMAIN}/messages`;

export default async (event: FunctionEvent<EventData>) => {

  // check if user is authenticated
  if (!event.context.auth || !event.context.auth.nodeId) {
    return { data: null }
  }

  // check if root
  // if (event.context.auth.token!==event.context.graphcool.rootToken) {
  if (event.context.auth.typeName!=='PAT') {
    return { error: 'Insufficient permissions 1' }
  }

  try {

    const { id, name, email } = event.data
    const api = fromEvent(event).api('simple/v1');

    if (!validator.isEmail(email)) {
      return { error: 'Not a valid email' }
    }
    
    const accountActivationCode: string = await createUserAccountActivationCode(api, id)
    
    // no data with this response
    if (!accountActivationCode) {
      return { error: 'error on createUserVerification' }
    }

    const accountActivationUrl =`${ACCOUNT_ACTIVATION_URL}/?accountActivationCode=${accountActivationCode}`;
    
    // // 3. Prepare body of POST request
    const form = new FormData()
    form.append('from', `Team <[email protected]>`)
    form.append('to', `${name} <${email}>`)
    form.append('subject', 'Activate your account')
    form.append('text', `Click on the link below to activate your account:
    
    ${accountActivationUrl}
    
    Thank you,
    
    Team team
    
    If you never signed up to Team immediately email us at [email protected]
    
    Activation code: ${accountActivationCode}`)

    // // 4. Send request to Mailgun API
    const resultOfMailGunPost = await fetch(mailgunUrl, {
      headers: { 'Authorization': `Basic ${Base64.btoa(apiKey)}`},
      method: 'POST',
      body: form
    }).then( res => res )

    if (!resultOfMailGunPost || resultOfMailGunPost.status!==200) {
      return { error: 'Failed to send email with mailgun' }
    }

    return { data: { result: true } }
  } catch (e) {
    console.log(e)
    return { error: 'An unexpected error occured during creation of verificationCode and sending the URL.' }
  }
}

async function createUserAccountActivationCode(api: GraphQLClient, userId: string): Promise<string> {
  const mutation = `
    mutation ($userId: ID!) {
      createAccountActivationCode(userId: $userId) {
        id
      }
    }
  `;

  const variables = { userId }
  return api.request<{ createAccountActivationCode: AccountActivationCode }>(mutation, variables)
  .then(r => r.createAccountActivationCode.id)
}

activateAccount.graphql

type ActivateAccountPayload {
  result: Boolean!
}

extend type Mutation {
  activateAccount(id: ID!): ActivateAccountPayload
}

activateAccount.ts

import { fromEvent, FunctionEvent } from 'graphcool-lib'
import { GraphQLClient } from 'graphql-request'
import * as moment from 'moment'

interface EventData {
  id: string
}

interface User {
  id: string
}

interface AccountActivationCode {
  id: string
  createdAt: Date
  user: User
}

interface ActivatedUser {
  id: string
  accountActivated: boolean
}

export default async (event: FunctionEvent<EventData>) => {
  console.log(event)

  try {
    const accountActivationCodeId = event.data.id;
    const api = fromEvent(event).api('simple/v1')

    // use the ID to get the AccountActivationCode node
    const accountActivationCode: AccountActivationCode = await getAccountActivationCode(api, accountActivationCodeId)
      .then(r => r.AccountActivationCode)

    // check if it exists
    if (!accountActivationCode || !accountActivationCode.id) {
      return { error: 'User not activate not activated 1' }
    }

    // check the time stamp - 12 hours to verify an email address
    const now = moment();
    const createdAt = moment(accountActivationCode.createdAt);
    if ( moment(now).isBefore(createdAt.subtract(12,'hours')) ) {
      return { error: 'User not activate not activated 2' }
    }

    // everything checks out then set accountActivated on user to true and return true
    const activatedUser: ActivatedUser = await activateUserAccount(api, accountActivationCode.user.id)

    // check if user exists and was updated
    if (!activatedUser || !activatedUser.id) {
      return { error: 'User not activate not activated 3' }
    }

    return { data: { result: true } }
  } catch (e) {
    console.log(e)
    return { error: 'An unexpected error occured during email verification.' }
  }
}

async function getAccountActivationCode(api: GraphQLClient, id: string): Promise<{ AccountActivationCode }> {
  const query = `
    query getAccountActivationCode($id: ID!) {
      AccountActivationCode(id: $id) {
        id
        createdAt
        user {
          id
        }
      }
    }
  `
  const variables = { id }
  return api.request<{AccountActivationCode}>(query, variables)
}

async function activateUserAccount(api: GraphQLClient, id: string): Promise<ActivatedUser> {
  const mutation = `
    mutation updateUser($id: ID!, $accountActivated: Boolean!) {
      updateUser(id: $id, accountActivated: $accountActivated) {
        id
      }
    }
  `
  const variables = { id, accountActivated: true }
  return api.request<{updateUser: ActivatedUser}>(mutation, variables)
  .then(r => r.updateUser)
}

from graphcool-templates.

maxdarque avatar maxdarque commented on May 28, 2024 3

Any feedback or improvements are welcome as it's the first time I've written such a function and it's my first day using Typescript :)

I've also written email verification functions if anyone is interested

from graphcool-templates.

emilbruckner avatar emilbruckner commented on May 28, 2024 3

@maxdarque I’m not sure whether anyone cares, but your code doesn’t check the Mailgun result. The sendResetPasswordEmail function returns true if any result is sent by Mailgun. The status of the Mailgun request should probably be checked.

from graphcool-templates.

michaelspeed avatar michaelspeed commented on May 28, 2024 2

any update on this?

from graphcool-templates.

maxdarque avatar maxdarque commented on May 28, 2024 1

@michaelspeed I post a version of these here:
https://github.com/maxdarque/templates/tree/master/auth/email-verification
https://github.com/maxdarque/templates/tree/master/auth/reset-password

from graphcool-templates.

michaelspeed avatar michaelspeed commented on May 28, 2024

i will be very happy if you can share the email verification?

from graphcool-templates.

michaelspeed avatar michaelspeed commented on May 28, 2024

deployement gives me this
Cannot find name 'FormData'
although form-data is added as dependency?

from graphcool-templates.

maxdarque avatar maxdarque commented on May 28, 2024

@michaelspeed not sure what the solution is. Double check it's in the parent graphcool package.json? Have you run yarn install or npm install in your local directory before deploying?

from graphcool-templates.

michaelspeed avatar michaelspeed commented on May 28, 2024

yup i have tried multiple times. but still cannot get it to work. the Base64 module also gives error. i am using the window.btoa() instead

from graphcool-templates.

ed428 avatar ed428 commented on May 28, 2024

I'm trying to try out the email verification, but am getting

{ "data": { "sendAccountActivationEmail": null } }

any idea what this could be?

sending an email through a basic mailgun mutation is working for me.

from graphcool-templates.

maxdarque avatar maxdarque commented on May 28, 2024

@edcvb00 you may need to spend some time debugging it. I don't have enough information to understand what the problem is.

@michaelspeed interesting... not sure why. you could also use new Buffer(apiKey).toString('base64')

from graphcool-templates.

colevels avatar colevels commented on May 28, 2024

screen shot 2561-06-05 at 23 12 35

Can you explain event parameter ? I got event === undefined when I run function.

from graphcool-templates.

maxdarque avatar maxdarque commented on May 28, 2024

@vistriter this is old graphcool-framework code. You can find FunctionEvent type here.

from graphcool-templates.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.