Comments (14)
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.
@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.
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.
@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.
any update on this?
from graphcool-templates.
@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.
i will be very happy if you can share the email verification?
from graphcool-templates.
deployement gives me this
Cannot find name 'FormData'
although form-data is added as dependency?
from graphcool-templates.
@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.
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.
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.
@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.
Can you explain event parameter ? I got event === undefined when I run function.
from graphcool-templates.
@vistriter this is old graphcool-framework code. You can find FunctionEvent type here.
from graphcool-templates.
Related Issues (20)
- Facebook Delegated Account Recovery
- Google auth mutation is not working HOT 3
- Auth-templates for Linkedin and Twitter HOT 2
- Templates should be written in TypeScript HOT 4
- Create language branches so people can easily contribute in their own programming language
- Email-password authentication returning an unhandled error HOT 2
- Auth0 authentication broken on React Native HOT 2
- Email / Password template : Passwords revealed in function error logs HOT 2
- Utility functions in a template - Please vote HOT 3
- jwt.decode fails on new id_tokens
- Question: Can this be used via an SPA and without Auth0 Lock? HOT 4
- Authentication template for Gitlab users
- Slack Bot command handling support
- Auth template for Twitch
- Should I PR an updated SMS authentication with Twilio? HOT 1
- Can't distinguish expired tokens from empty responses
- Add template and GraphCool.yml no longer working
- Token produced no longer works with GraphCool
- Passwords are stored in clear locally (no hash) HOT 2
- OAuth 2.0 support for linkedin auth-template
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from graphcool-templates.