-
Notifications
You must be signed in to change notification settings - Fork 100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Forget password for email and user management #111
Comments
any update on this? |
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
Resolver
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
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)
} |
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 |
i will be very happy if you can share the email verification? |
@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 I won't include the permission filters but you'll need to limit it to ADMIN users in the db.
graphcool.yml
sendAccountActivationEmail.graphql
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
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)
} |
deployement gives me this |
@michaelspeed not sure what the solution is. Double check it's in the parent graphcool package.json? Have you run |
yup i have tried multiple times. but still cannot get it to work. the Base64 module also gives error. i am using the |
I'm trying to try out the email verification, but am getting
any idea what this could be? sending an email through a basic mailgun mutation is working for me. |
@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 |
@vistriter this is old graphcool-framework code. You can find FunctionEvent type here. |
@maxdarque I’m not sure whether anyone cares, but your code doesn’t check the Mailgun result. The |
Feature Request
Please upgrade the old email-user-managment for forget password
The text was updated successfully, but these errors were encountered: