I’m refactoring a Google Books app from a Restful API to GraphQL, and I am stuck on a mutation not behaving the way I expect.
When a user fills out the form found on Signup.js the Mutation ADD_USER should create a user within Mongoose, this user should have a JWT token assigned to them, and user should be logged in upon successful execution of the Mutation.
Actions observed:
• Mutation is being fired off from the front end. When I open developer tools in the browser I can see the Username, Email and Password being passed as variables.
• I have tried console logging the token, and keep getting an undefined return
• When I try to run the mutation in the GraphQL sandbox I get a null value returned.
• When I console log the args in resolvers.js no value appears on the console, which tells me the request is not reaching the resolver.
SignupForm.js (React FE Page)
import React, { useState } from "react";
import { Form, Button, Alert } from "react-bootstrap";
import { useMutation } from "#apollo/client";
import { ADD_USER } from "../utils/mutations";
import Auth from "../utils/auth";
const SignupForm = () => {
// set initial form state
const [userFormData, setUserFormData] = useState({
username: "",
email: "",
password: "",
});
// set state for form validation
const [validated] = useState(false);
// set state for alert
const [showAlert, setShowAlert] = useState(false);
const [addUser] = useMutation(ADD_USER);
const handleInputChange = (event) => {
const { name, value } = event.target;
setUserFormData({ ...userFormData, [name]: value });
};
const handleFormSubmit = async (event) => {
event.preventDefault();
// check if form has everything (as per react-bootstrap docs)
const form = event.currentTarget;
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
try {
///Add user is not returning data. payload is being passed as an object
const response = await addUser({
variables: { ...userFormData },
});
if (!response.ok) {
throw new Error("OH NO!SOMETHING WENT WRONG!");
}
const { token, user } = await response.json();
console.log(user);
Auth.login(token);
} catch (err) {
console.error(err);
setShowAlert(true);
}
setUserFormData({
username: "",
email: "",
password: "",
});
};
Mutation.js
export const ADD_USER = gql`
mutation addUser($username: String!, $email: String!, $password: String!) {
addUser(username: $username, email: $email, password: $password) {
token
user {
username
email
}
}
}
`;
typeDefs.js
const { gql } = require("apollo-server-express");
const typeDefs = gql`
input SavedBooks {
authors: [String]
description: String
bookId: String
image: String
link: String
title: String
}
type Books {
authors: [String]
description: String
bookId: ID
image: String
link: String
title: String
}
type User {
_id: ID
username: String
email: String
password: String
savedBooks: [Books]
}
type Auth {
token: ID!
user: User
}
type Query {
me: User
}
type Mutation {
##creates a user profile through the Auth type, that way we can pass a token upon creation
addUser(username: String!, email: String!, password: String!): Auth
login(email: String!, password: String!): Auth
saveBook(bookData: SavedBooks): User
deleteBook(bookId: ID!): User
}
`;
module.exports = typeDefs;
resolvers.js
const { User, Book } = require("../models");
const { AuthenticationError } = require("apollo-server-express");
const { signToken } = require("../utils/auth");
const resolvers = {
Query: {
me: async (parent, args, context) => {
if (context.user) {
return User.findOne({ _id: context.user._id }).populate("books");
}
throw new AuthenticationError("You need to log in");
},
},
};
Mutation: {
//try refactoring as a .then
addUser: async (parent, args) => {
//create user profile
await console.log("resolver test");
console.log(args);
const user = await User.create({ username, email, password });
//assign token to user
const token = signToken(user);
return { token, user };
};
login: async (parent, { email, password }) => {
const user = User.findOne({ email });
if (!user) {
throw new AuthenticationError("Invalid Login Credentials");
}
const correctPw = await profile.isCorrectPassword(password);
if (!correctPw) {
throw new AuthenticationError("Invalid Login Credentials");
}
const token = signToken(user);
return { token, user };
};
saveBook: async (parent, { bookData }, context) => {
if (context.user) {
return User.findOneAndUpdate(
{ _id: context.user._id },
{ $addToSet: { savedBooks: bookData } },
{ new: true }
);
}
throw new AuthenticationError("You need to log in");
};
deleteBook: async (parent, { bookId }, context) => {
if (context.user) {
return User.findOneAndUpdate(
{ _id: contex.user._id },
//remove selected books from the savedBooks Array
{ $pull: { savedBooks: context.bookId } },
{ new: true }
);
}
throw new AuthenticationError("You need to log in");
};
}
module.exports = resolvers;
auth.js
const jwt = require("jsonwebtoken");
// set token secret and expiration date
const secret = "mysecretsshhhhh";
const expiration = "2h";
module.exports = {
// function for our authenticated routes
authMiddleware: function ({ req }) {
// allows token to be sent via req.query or headers
let token = req.query.token || req.headers.authorization || req.body.token;
// ["Bearer", "<tokenvalue>"]
if (req.headers.authorization) {
token = token.split(" ").pop().trim();
}
if (!token) {
return req;
}
// verify token and get user data out of it
try {
const { data } = jwt.verify(token, secret, { maxAge: expiration });
req.user = data;
} catch {
console.log("Invalid token");
return res.status(400).json({ message: "invalid token!" });
}
// send to next endpoint
return req;
},
signToken: function ({ username, email, _id }) {
const payload = { username, email, _id };
return jwt.sign({ data: payload }, secret, { expiresIn: expiration });
},
};
Basically, I have combed from front to back end looking for where I introduced this bug, and am stuck. Any suggestions or feedback is greatly appreciated.
I was able to figure out the issue. First, a syntax error on resolver.js was preventing my mutations from being read.
Next, I made the following adjustment to handleFormSubmit on SignupForm.js
try {
///Add user is not returning data. payload is being passed as an object
const {data} = await addUser({
variables: { ...userFormData },
});
console.log(data)
console.log(userFormData)
**Auth.login(data.addUser.token);**
} catch (err) {
console.error(err);
setShowAlert(true);
}
That way my FE was properly accounting for what my Auth Middleware was passing back after successful user creation. Thanks for your help xadm, being able to talk this out got me thinking about where else to attack the bug.
Related
I am trying to verify user emails with JWT. My current set up is that a JWT is sent to a user when they try to log in if they do not have a confirmed email.
When the email is sent it composes a URL with the token and then sends the request to the server to verify the email. It worked great in postman as I could easily add the email that I want to verify in the body. But I can't think of a way how to do it in the browser.
This is the code that should verify the email.
confirmEmail = async (req, res, next) => {
const { email } = req.body
const param = req.params.token
const user = await userModel.findOne({email})
if(!user)
{
throw new HttpException(401, 'User not found')
}
if(user.confirmed)
{
throw new HttpException(401, 'User already confirmed')
}
if(!user.confirmed)
{
const confirmJWT = jwt.verify(param, process.env.SECRET_JWT)
if(!confirmJWT)
{
throw new HttpException(200, 'Token invalid')
}
const result = await userModel.emailConfirmed(email)
}
res.send('Database updated.')
}
This is the code that generates the JWT and sends it in an email.
if(!user.confirmed)
{
const emailToken = jwt.sign(
{
email: user.email
},
process.env.SECRET_JWT,
{
expiresIn: '15m'
}
)
console.log(emailToken)
emailModel.verifyEmail(email, emailToken)
throw new HttpException(401, 'Email not confirmed')
}
I was wondering if there is any way I can use the just the token to find the email of the user or is that not possible with JWT?
export const verifyEmail = () => {
try
{
return API()
.post(`/api/confirm/:token`, {}, {
params: {
token: store.user.authToken
},
email: store.user.email
})
.then(({data: userData}) => {
console.log('worked')
})
}
catch(error)
{
console.log(error)
}
}
import { verifyEmail } from '../../services/authAPI'
import { useUserStore } from '../../stores/user'
const store = useUserStore()
export default {
data()
{
return {
email: store.user.email
}
},
methods: {
async handleSubmit()
{
try
{
const response = await verifyEmail(this.email)
}
catch(err)
{
console.log(err)
}
}
}
}
</script>
Basically you do not need to send the email in the body as already encoded the email into the JWT. Once you do const verifiedToken = jwt.sign(token, secret key) You can do verifiedToken.email to grab the email.
I follow modular or component based strucutre. I found a sample repo.
https://github.com/sujeet-agrahari/node-express-clean-architecture
So, there is a main component.module.js files which is responsible for connecting all other pieces like controller, route, and services.
For controller, services are being injected using higher order functions. Now, controller are super easy to test, I can stub or mock services easily.
auth.module.js
const router = require('express').Router();
const {
makeExpressCallback,
makeValidatorCallback,
} = require('../../middlewares');
// validator
const AuthValidator = require('./auth.validator');
// service
const { doRegister, doLogin, doCheckUserExist } = require('./auth.service');
const { BadRequestError } = require('../../utils/api-errors');
// controller
const controller = require('./auth.controller');
const register = controller.register({ BadRequestError, doCheckUserExist, doRegister });
const login = controller.login({ doCheckUserExist, doLogin });
const AuthController = { register, login };
// routes
const routes = require('./auth.routes')({
router,
AuthController,
AuthValidator,
makeValidatorCallback,
makeExpressCallback,
});
module.exports = {
AuthController,
AuthService: {
doCheckUserExist,
doLogin,
doRegister,
},
AuthRoutes: routes,
};
auth.controller.js
const login = (doCheckUserExist, doLogin) => async (httpRequest) => {
const { username, password } = httpRequest.body;
const userData = await doCheckUserExist({ username });
const loginData = {
username,
role: userData.role_id,
passedPassword: password,
actualPassword: userData.password,
};
const loginResult = await doLogin(loginData);
return {
statusCode: 200,
body: {
success: true,
message: 'Successfully logged in!',
data: loginResult,
},
};
};
const register = ({ BadRequestError, doCheckUserExist, doRegister }) => async (httpRequest) => {
const { username, password } = httpRequest.body;
try {
await doCheckUserExist({ username });
} catch (error) {
// user doesn't exist
const registerResult = await doRegister({ username, password });
return {
statusCode: 200,
body: {
success: true,
message: 'Registered successfully!',
data: registerResult,
},
};
}
throw new BadRequestError('User already exist!');
};
module.exports = { register, login };
Things are fine with the controller, now the problem is with the services. I can't find any pattern to make them thin and clean.
auth.services.js
const {
JWT_ACCESS_TOKEN_SECRET,
ACCESS_TOKEN_EXPIRES_IN,
SIGN_OPTION,
} = require('config');
const bcrypt = require('bcryptjs');
const { User } = require('../../db');
const { generateJWT } = require('./jwt.service');
const { NotFoundError, BadRequestError } = require('../../utils/api-errors');
const doRegister = async ({ username, password }) => {
const user = await User.create({
username,
password,
role_id: 1, // assign role id here
});
// generate access token
const payload = {
username,
role: user.role_id,
};
const token = await generateJWT({
secretKey: JWT_ACCESS_TOKEN_SECRET,
payload,
signOption: {
...SIGN_OPTION,
expiresIn: ACCESS_TOKEN_EXPIRES_IN,
},
});
return {
access_token: token,
...payload,
};
};
const doLogin = async ({
username, userRole, passedPassword, actualPassword,
}) => {
const isValidPass = bcrypt.compareSync(passedPassword, actualPassword);
if (!isValidPass) throw new BadRequestError('Username or Password is invalid!');
// generate access token
const payload = {
username,
role: userRole,
};
const token = await generateJWT({
secretKey: JWT_ACCESS_TOKEN_SECRET,
payload,
signOption: {
...SIGN_OPTION,
expiresIn: ACCESS_TOKEN_EXPIRES_IN,
},
});
return {
access_token: token,
...payload,
};
};
const doCheckUserExist = async ({ username }) => {
const user = await User.findOne({
where: {
username,
},
});
if (!user) throw new NotFoundError('User not found!');
return user;
};
module.exports = { doRegister, doLogin, doCheckUserExist };
A lot is happening in the services, model imports, constants imports, and other utils.
Now services become really hard to test.
Is there any way or pattern I can separate some logic from services and make them lighter?
I can implement reository pattern for db methods, but I am not aware how I can implement using sequelize?
Should I use also higher order function to inject all the utils and constants in the service like I did for controller?
I built the API with apollo server and everything works fine in graphiql. I make requests to the api from front-end react app with apollo client.
const [getUserPosts, { loading, error, data }] = useLazyQuery(GET_USER_POSTS);
useEffect(() => {
getUserProfile();
getUserPosts({ variables: { email: userEmail } });
}, [userEmail]);
SO getUserProfile fetches the user email from the express back end (I have an express serving react and a separate graphql api), then I query the posts of that user on the api. Below is the query itself
export const GET_USER_POSTS = gql`
query User($email: String) {
user(email: $email) {
email
posts {
content
}
}
}
`;
This is the typedefs and resolver on the api server
const typeDefs = gql`
type User {
email: String
posts: [Post]
}
type Post {
id: ID!
email: String
content: String
}
type Query {
users: [User]
posts: [Post]
user(email: String): [User]
post(id: String): [Post]
}
type Mutation {
addPost(email: String, content: String): [Post]
deletePost(id: String): [Post]
}
`;
const resolvers = {
Query: {
users: () => User.find(),
posts: () => Post.find(),
user: (parent, args) => User.find({ email: args.email }),
post: (parent, args) => Post.find({ _id: args.id }),
},
User: {
posts: async user => {
try {
const postsByUser = Post.find({ email: user.email });
return postsByUser;
} catch (error) {
console.log(err);
}
},
},
Mutation: {
addPost: async (parent, args) => {
const newPost = new Post({
email: args.email,
content: args.content,
});
try {
newPost.save();
} catch (err) {
console.log(err);
}
},
deletePost: async (parent, args) => {
try {
const deletedPost = await Post.deleteOne({ _id: args.id });
} catch (err) {
console.log(err);
}
},
},
};
then I try to console.log the data here
if (loading) {
console.log(loading);
}
if (error) {
console.log(error);
}
if (data) {
console.log(loading);
let test = data.user[0];
//I can see the data logged in the console as an object {email: "abc", posts: [array of posts]}
console.log(test);
}
BUT if I try to console.log(test.posts) react results with can not read property "posts" of undefined
UPDATE-1 !!
So when react results with the above error, I try to refresh the page again and it now can logs the "posts" array. But it sometimes take 2 or 3 refresh to make it work and sometimes when I refresh again it does not work anymore. Why is this happening ????
UPDATE-2 !!
So I try to troubleshoot with this line:
{data ? console.log(data.user[0].posts) : console.log("nothing")}
and interestingly it actually does log "nothing" a few times in the console before logging the data. But this is weird because I explicitly write that if only "data" is "true" then log it in the console. But somehow "data" is somtimes null itself. This data is provided by apollo client and it should be always true after loading is false, how is data still null after loading is false already ???
So I found the problem. Turns out it actually comes from within this block:
useEffect(() => {
getUserProfile();
getUserPosts({ variables: { email: userEmail } });
}, [userEmail]);
After observing in the network tab, it seems that my app try to send request to graphQL api before getUserProfile was done pulling user email, so it sent an empty request and hence received nothing. I was naive to think getUserProfile and getUserPosts will be executed synchronously. So I wrap getUserPosts with
if (userEmail) {
getUserPosts({ variables: { email: userEmail } });
}
So now only after I received the uerEmail then getUserPosts will be executed.
I am creating an e-commerce web site.
Now I finished creating the new account with email and password.
And I want to insert the user email, full name, and timestamp in the database.
As you can see in the picture below, I could see the USER data in the google chrome dev console.
But when I checked the firebase database in the browser, I cannot see the user id. And instead, I see undefined in the user id column.
Now I am on the step3 process.
Add user data into database
I cannot figure out why it's happening, so I hope you can help me out.
This is my store/index.js file.
import fireApp from '#/plugins/firebase'
export const state = () => ({
user: null,
error: null,
busy: false,
jobDone: false
})
export const mutations = {
setUser (state, payload) {
state.user = payload
},
setError (state, payload) {
state.error = payload
},
clearError (state, payload) {
state.error = null
},
setBusy (state, payload) {
state.busy = payload
},
setJobDone (state, payload) {
state.jobDone = payload
},
}
export const actions = {
signUpUser({commit}, payload) {
commit('setBusy', true)
commit('clearError')
//1.Signup new user.
//2.Update firebase user profile & set local user data.
//3.Add user data into database
//4.Attach user to consumer group
let newUser = null
fireApp.auth().createUserWithEmailAndPassword(payload.email, payload.password)
.then(user => {
newUser = user
var user = fireApp.auth().currentUser;
user.updateProfile({ displayName: payload.fullname })
const currentUser = {
id: user.uid,
email: payload.email,
name: payload.fullname,
role: 'consumer'
}
console.log('USER', currentUser)
commit('setUser', currentUser)
})
.then(() => {
const userData = {
email: payload.email,
fullname: payload.fullname,
createdAt: new Date().toISOString()
}
fireApp.database().ref(`users/${newUser.uid}`).set(userData)
})
.then(() => {
commit('setJobDone', true)
commit('setBusy', false)
})
.catch(error => {
commit('setBusy', false)
commit('setError', error)
})
}
}
export const getters = {
user (state) {
return state.user
},
error (state) {
return state.error
},
busy (state) {
return state.busy
},
jobDone (state) {
return state.jobDone
}
}
This is because the promise returned by createUserWithEmailAndPassword() method resolves with an UserCredential object and not with a User one.
You should use the user property of the UserCredential, as follows:
let newUser = null
fireApp.auth().createUserWithEmailAndPassword(payload.email, payload.password)
.then(userCredential => {
newUser = userCredential.user;
//...
Note also that you don't need to call fireApp.auth().currentUser to get the user.
When using the createUserWithEmailAndPassword() method, on successful creation of the user account, this user will also be signed in to your application, so just get the user with userCredential.user, as explained above.
In addition, note that the updateProfile() method is asynchronous and returns a Promise, which you need to include in your promises chain.
So the following should do the trick (untested):
signUpUser({commit}, payload) {
commit('setBusy', true)
commit('clearError')
//1.Signup new user.
//2.Update firebase user profile & set local user data.
//3.Add user data into database
//4.Attach user to consumer group
let user = null;
fireApp.auth().createUserWithEmailAndPassword(payload.email, payload.password)
.then(userCredential => {
user = userCredential.user;
return user.updateProfile({ displayName: payload.fullname });
})
.then(() => {
const currentUser = {
id: user.uid,
email: payload.email,
name: payload.fullname,
role: 'consumer'
}
console.log('USER', currentUser)
commit('setUser', currentUser)
const userData = {
email: payload.email,
fullname: payload.fullname,
createdAt: new Date().toISOString()
}
return fireApp.database().ref(`users/${user.uid}`).set(userData)
})
.then(() => {
commit('setJobDone', true)
commit('setBusy', false)
})
.catch(error => {
commit('setBusy', false)
commit('setError', error)
})
}
I'm trying to hash my password field before storing it in the database. For that reason I've created a virtual field called password and an actual field called hashedPassword. The trouble is that when I try to encrypt the password in beforeCreate hook, it the user.password is undefined. I have tried every thing. I've also defined custom getters and setters for the virtual field. I don't know what I'm doing wrong here. Any help or guidance would be appreciated. Thanks.
import bcrypt from 'bcrypt';
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
'User',
{
passwordhash: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: true
}
},
password: {
allowNull: false,
type: DataTypes.VIRTUAL,
set(password) {
const valid = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[#$^+=!*()#%&]).{8,30}$/.test(
password
);
if (!valid) {
throw new Error(`Password not valid ${password}`);
}
this.setDataValue('password', password);
},
get() {
return this.getDataValue('password');
}
}
}
},
{
hooks: {
beforeCreate: function hashPassword(user) {
console.log('user is:', user);
return bcrypt
.hash('Abcdefgh1#', 12)
.then(hashed => {
user.passwordhash = hashed;
})
.catch(error => error);
}
}
}
);
User.associate = function(models) {
// associations can be defined here
};
return User;
};
use hooks :
hooks: {
beforeCreate: async (user, options) => {
let salt = await bcrypt.genSalt(10)
let hash = await bcrypt.hash(user.password, salt)
user.password = hash;
}
}
and for updating password fields use this:
User.beforeBulkUpdate(async instance => {
if (instance.attributes.password) {
let salt = await bcrypt.genSalt(10)
let hash = await bcrypt.hash(instance.attributes.password, salt)
instance.attributes.password = hash;
}
})
this works fine for me