Next.js: Cookies are not properly stored from nested registration page - reactjs

Customers are redirected to our Next.js app via a third-party checkout page. We need to track the account type and promotion used to purchase the program. We have created a dynamic registration page /sign-up/[type]/[promotion].tsx that customers are redirected to depending on their purchase so that we can save those values on account creation.
The issue we are having is cookies are returning undefined in our index page getServerSideProps so we are unable to fetch the user's data we also redirect to the /login page if the cookie is missing. I can see the cookie in the console. We are also not having this issue with the standard login and registration pages. I have gone so far as installing the nookies library to try to resolve the issue, but it has offered nothing new.
I have also tried ditching the dynamic route by building out all of the different registration routes, which are also failing.
const RegisterForm: React.FC<RegisterFormProps> = () => {
...
const router = useRouter()
const { type, promotion } = router.query
<Formik
initialValues={{ email: '', password: '' }}
validationSchema={Yup.object({
email: Yup.string().email('Invalid email address').required('Required'),
password: Yup.string()
.min(6, 'Password must be longer than 6 characters.')
.required('Required'),
})}
onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true)
try {
const response = await apolloClient.mutate({
mutation: CREATE_USER,
variables: {
email: values.email,
password: values.password,
isRootUser: true,
type,
promotion,
},
})
if (response?.data?.createUser?.error) {
setLoading(false)
return setError(response.data.createUser?.error)
}
setSubmitting(false)
const data = response.data.createUser
setAuthToken(data.authToken)
nookies.set({}, 'authToken', data.authToken, {
maxAge: 60 * 60 * 24,
})
setAuthenticated(true)
router.push('/')
} catch (error) {
setSubmitting(false)
alert(error)
}
}}
>
...
}
index.tsx
export const getServerSideProps: GetServerSideProps = async (context: GetServerSidePropsContext) => {
const cookies = nookies.get(context)
const authToken = cookies?.authToken
// if there is no authToken, redirect to login
if (!authToken)
return {
redirect: {
permanent: false,
destination: '/login',
},
props: {},
}
const apolloClient = initializeApollo()
const response = await apolloClient.query({
query: GET_USER_PROGRESS,
variables: {
authToken,
},
fetchPolicy: 'no-cache',
})
const progress = response?.data?.getUserProgress
return {
props: {
data: progress,
},
}
}

Related

When sign in with google firebase always register user as new

Tech: Firebase, Next.js, Google Sign in, Firebase Stripe exstension
Bug reproduction:
When login with Google
Subscribe on stripe
Stripe saves subscription data for that user in firestore
Logout
Login in with Google and old data are overide with new one, and Subscription is lost
Does anyone had similar problem?
Maybe my implementation of Sign-in is bad, here is the Google Sign in code:
const handleGoogleLogin = () => {
signInWithPopup(auth, googleProvider)
.then(async result => {
if (!result.user) return;
const { displayName, email, uid, providerData, photoURL, phoneNumber } =
result.user;
const name = splitName(displayName as string);
const providerId =
(providerData.length && providerData[0]?.providerId) || '';
const data = {
firstName: name?.firstName || '',
lastName: name?.lastName || '',
email,
photoURL,
phoneNumber,
providerId,
};
await updateUser(uid, data);
})
.catch(error => {
console.log('Google login error: ', error);
});
};
Update user function:
export const updateUser = async (uid: string, data: UpdateUserParams) => {
try {
if (!uid) {
return;
}
await setDoc(doc(firestore, 'users', uid), {
account: {
...data,
initials: `${data.firstName[0]}${data.lastName[0]}`,
},
});
} catch (error) {
console.error('Error updating user: ', error);
}
};
setDoc is overwriting the contents of the document with each sign-in. You should instead use set with merge to prevent overwriting the fields you don't want to lose, or check first if the document exists before creating it.
See also:
https://firebase.google.com/docs/firestore/manage-data/add-data#set_a_document
Difference Between Firestore Set with {merge: true} and Update

React and Typescript, createContext where the default value is a Promise

I am learning React and Typescript and following this tutorial (AWS Cognito + React JS Tutorial: Getting Sessions and Logging out (2020) [Cognito Episode #3] https://youtu.be/T7tXYHy0wKE). I am trying to call my new React Context with AccountContext.Provider but the default value is not working because there is a Promise in the function that is being used in the value field, but no Promise in the default value. Can I write something like this?
const AccountContext = createContext<UserAuthTypes>(username: '', password: '' => await new Promise);
I defined AccountContext to have the default value username: '', password: '', but I get this error in the return statement, on the value= line:
TS2739: Type '{ authenticate: (username: string, password: string) => Promise<unknown>; }' is missing the following properties from type 'UserAuthTypes': username, password
I see that authenticate has username and password as the inputs and a Promise as the output.
The code which is based off of this tutorial: AWS Cognito + React JS Tutorial: Getting Sessions and Logging out (2020) [Cognito Episode #3] https://youtu.be/T7tXYHy0wKE):
export interface UserAuthTypes {
username: string;
password: string;
authenticate?: any;
}
export const initialUserAuthState = {
username: '',
password: '',
};
........
const AccountContext = createContext<UserAuthTypes>(initialUserAuthState);
const Account = (props: { children: ReactNode }) => {
const authenticate = async (username: string, password: string) =>
await new Promise((resolve, reject) => {
const user = new CognitoUser({
Username: username,
Pool: UserPool,
});
const authDetails = new AuthenticationDetails({
Username: username,
Password: password,
});
user.authenticateUser(authDetails, {
onSuccess: (data) => {
resolve(data);
},
onFailure: (err) => {
reject(err);
},
newPasswordRequired: (data) => {
resolve(data);
},
});
});
return (
<AccountContext.Provider value={{ authenticate }}>
{props.children}
</AccountContext.Provider>
);
};
export { Account, AccountContext };

React state does not update on time, asynchronous issue

I've build a web application using React that has user registration.
The user data:
{
username: 'string',
password: 'string'
}
exists in a postgresql database and as state.
I'm trying to handle a case where a user is trying to register with a previously used username.
Here's a screenshot of my ui component and react dev tools :
Here's a summary of how I'm handling the case where a username (i'll use the username 'bob' as seen of the screenshot) has already been used:
// Login.js
The 'Register' <input> in the <Login/> component (which contains all the .jsx for the ui form seen on the screen) invokes the userRegister() function via an onClick event. It receives this function as a prop from my <App/> (App.js) component, then the following happens...
// App.js
1 - userRegister() is invoked
2- it invokes 2 functions that check whether the password and username are valid
3- if they are, it invokes a function to check if the username has already been used by checking if it exists as a record in the postgresql database
4- it does this by sending an axios request with the current username + password object as params to the '/users' endpoint, written out in my server.js file
axios({
method: 'get',
url: '/users',
params: { username: 'string', password: 'string'
})
// server.js
1- the request is received by an express route handler at the '/users' endpoint
2- inside the route handler, it invokes an imported getUser() function from postgres.js that communicates directly with the database
const { getData, getUser, addUser } = require('./db/postgres.js');
app.get('/users', (req, res) => {
getUser(req.query)
});
// postgres.js
1- the getUser() function sends the following query to the postgres database
const getUser = (user) => {
return client.query(`select * from users where username='${user.username}'`)
};
after sending that query
this object should reach the client
response.rows: [
{ id: null, username: 'bob', password: 'Pi2#' },
{ id: null, username: 'bob', password: '8*lA' },
{ id: null, username: 'bob', password: '8*lA' },
{ id: null, username: 'bob', password: '7&lL' },
{ id: null, username: 'bob', password: '*lL0' },
{ id: null, username: 'bob', password: 'mM0)' },
{ id: null, username: 'bob', password: '8*fF' },
{ id: null, username: 'bob', password: '7&yY' },
{ id: null, username: 'bob', password: '55$rR' },
{ id: null, username: 'bob', password: '7&yY' },
{ id: null, username: 'bob', password: '8*LAAa' },
{ id: null, username: 'bob', password: '8*lAa' },
{ id: null, username: 'bob', password: '8*lA' }
]
which proves that the username does, in fact already exist in the database
which should, in turn, update the alreadyRegistered state to true and invoke the alert 'user already exists', but, as demonstrated in the top screenshot, alreadyRegistered doesn't update in time
it DOES show the update in Rect devtools a few ms later
I'm fairly certain that the issue is due to state hooks and lifecycle methods being asynchronous, but with that being said how do I update the alreadyRegisted state to true before the code on line 63 executes?
Here are snippets of the code files working together. Let me know if more information/context is needed
Login.js
import React from 'react';
import { Link } from 'react-router-dom';
import { NavBar, Footer } from'../Imports.js';
const Login = ({ userAuth, getUserAuth, userLogin, authorized, userRegister }) => {
console.log('userAuth:', userAuth);
return (
<div id='login'>
<NavBar userAuth={userAuth} getUserAuth={getUserAuth} authorized={authorized} />
<section id='login-main'>
<form>
<input type='text' placeholder='username' onChange={() => {getUserAuth({...userAuth, username: event.target.value})}}/>
<input type='text' placeholder='password'onChange={() => {getUserAuth({...userAuth, password: event.target.value})}}/>
<input type='submit' value='Login' onClick={() => {event.preventDefault(); userLogin()}}/>
<input type='submit' value='Register' onClick={() => {event.preventDefault(); userRegister();}}/>
</form>
</section>
<Footer/>
</div>
)
};
export default Login;
App.js
import React, { useState, useEffect } from 'react';
import Router from './Router.js';
const axios = require('axios');
const App = () => {
// Login/user state
const [userAuth, getUserAuth] = useState({username: '', password: ''});
const [authorized, authorizeUser] = useState(false);
const [alreadyRegistered, userExists] = useState(false);
// new user register
const userRegister = () => { // <- 1)
if (validUsername() && validPassword()) {
checkIfExists();
console.log('alreadyRegistered:', alreadyRegistered);
if (alreadyRegistered) {
alert('user already exists');
} else {
console.log('ERROR: should update alreadyRegistered state to TRUE and invoke ALERT')
}
}
}
// check if username is already registered
const checkIfExists = () => { // <- 3)
axios({
method: 'get',
url: '/users',
params: userAuth
})
.then((res) => {
console.log(res.data.length)
if (res.data.length) {
console.log(res.data.length, 'INNER')
userExists(true);
}
})
.catch((err) => {
throw err;
})
}
};
server.js
// ----- external modules -----
require('dotenv').config();
const express = require('express');
const path = require('path');
const { getData, getUser, addUser } = require('./db/postgres.js');
// check if user exists
app.get('/users', (req, res) => {
console.log('req.body:', req.query)
getUser(req.query)
.then((response) => {
console.log('response.rows:', response.rows);
res.send(response.rows);
})
.catch((err) => {
if (err) {
throw err;
}
});
});
postgres.js
const { Client } = require('pg');
const getUser = (user) => {
return client.query(`select * from users where username='${user.username}'`)
};
Before answering the state question, it's important to know your application as written is vulnerable to SQL injection. Instead of a template string in your postgres.js file, you should use parameterized queries.
As for the value of alreadyRegistered, you're correct that the issue lies in the asynchronous nature of state hooks.
You can use the useEffect function to handle updates to alreadyRegistered like so:
useEffect(() => {
if (alreadyRegistered) {
alert('user already exists');
} else {
// create user...
}
}, [alreadyRegistered]);
You could also avoid tracking alreadyRegistered in state and just call checkIfExists as needed:
// first, remove alreadyRegistered state
const [userAuth, getUserAuth] = useState({username: '', password: ''});
const [authorized, authorizeUser] = useState(false);
// then redefine checkIfExists to return Promise<boolean>
const checkIfExists = async () => {
const res = await axios({
method: 'get',
url: '/users',
params: userAuth
})
return !!res.data.length; // convert fetch result to boolean
}
// then call checkIfExists in userRegister
const userRegister = async () => {
if (validUsername() && validPassword()) {
const alreadyRegistered = await checkIfExists();
if (alreadyRegistered) {
alert('user already exists');
} else {
// handle user creation
}
}
}

AWS Amplify and Next.JS with GraphQL Server Error No current user from getStaticPaths

I'm having trouble accessing data from Amplify's API Graphql, and it keeps returning
Server Error
Error: No current user
I've been following this tutorial: https://youtu.be/13nYLmjZ0Ys?t=2292
I know I'm signed into Amplify because if I go into different pages, I can grab user Auth and I can even display the SignOut button. But for whatever reason, I'm not sure why I'm getting this error
import { API } from "aws-amplify";
import { useRouter } from "next/router";
import { listActivations, getActivation } from "../../graphql/queries";
const Activation = ({ activation }) => {
const router = useRouter();
if (router.isFallback) {
return <div>Loading</div>;
}
return <div>{activation.title}</div>;
};
export default Activation;
export async function getStaticPaths() {
const SSR = withSSRContext();
console.log("static paths");
const activationData = await SSR.API.graphql({
query: listActivations,
});
console.log("activationData", activationData);
const paths = activationData.data.listActivations.items.map((activation) => ({
params: { id: activation.id },
}));
return {
paths,
fallback: true,
};
}
export async function getStaticProps({ params }) {
const SSR = withSSRContext(); // added SSR, but still getting error
console.log("static props");
const { id } = params;
const activationData = await SSR.API.graphql({
query: getActivation,
variables: { id },
});
return {
props: {
activation: activationData.data.getActivation,
},
};
}
The console log static paths appears, and then after that, I get errors.
Do you think it has anything to do with my GraphQL schema?
type User #model #auth(rules: [{ allow: owner, ownerField: "username" }]) {
id: ID!
username: String!
email: String!
userType: UserType
}
type Activation
#model
#key(
name: "activationsByStudentId"
fields: ["student"]
queryField: "activationsByStudentId"
)
#auth(
rules: [
{ allow: groups, groups: ["Admin"] }
{ allow: owner }
{
allow: owner
ownerField: "studentId"
operations: [create, update, delete]
}
{ allow: private, operations: [read] }
{ allow: public, operations: [read] }
]
) {
id: ID!
studentId: ID!
title: String!
student: Student #connection(fields: ["studentId"])
teachers: [TeachersActivations] #connection(name: "ActivationTeachers")
}
Edit: I've also added User model to see if this could be a cause too.
Since both getStaticProps and getStaticPaths are called during build time, and on the server when fallback is equal to true, you need to configure Amplify for SSR (Server-Side Rendering). Make sure to take a look at SSR Support for AWS Amplify JavaScript Libraries.
The solution: first, configure Amplify for SSR:
Amplify.configure({ ...awsExports, ssr: true });
Then you need to use withSSRContext, and add the the authMode parameter. As quoted from the link above:
For example, take an AppSync GraphQL API that is backed by an identity provider such as Amazon Cognito User pools, Okto, or Auth0. Some GraphQL types may require a user to be authenticated to perform certain requests. Using the API class, the user identity will now automatically be configured and passed into the API request headers:
const SSR = withSSRContext();
const activationData = await SSR.API.graphql({
query: listActivations,
authMode: "AMAZON_COGNITO_USER_POOLS"
});
Still, I couldn't figure out the issue why this can't work, so I decided to move my query into client-side
const [activation, setActivation] = useState(null);
const router = useRouter();
const { aid } = router.query;
useEffect(() => {
if (!aid) return;
async function activationDataFromClient() {
try {
const getActivationData = await API.graphql({
query: getActivation,
variables: {
id: aid,
},
});
setActivation(getActivationData.data.getActivation);
} catch (err) {
console.log("error fetching activation data: ", err);
}
}
activationDataFromClient();
}, [aid]);
I had the same problem. Changing the authMode to 'API_KEY' enabled it to work for me. See example below:
export async function getStaticPaths(context) {
const SSR = withSSRContext();
const { data } = await SSR.API.graphql({
query: listArticles,
authMode: 'API_KEY'
});
const paths = data.listArticles.items.map((article) => ({
params: { id: article.id },
}));
return {
paths,
fallback: true,
};
}
export async function getStaticProps({ params }) {
const SSR = withSSRContext();
const { data } = await SSR.API.graphql({
query: getArticle,
variables: {
id: params.id,
},
authMode: 'API_KEY'
});
return {
props: {
article: data.getArticle
}
}
}

Setting Apollo cache / state after mutation

I am using Apollo 2.0 to manage my graphQL API calls and to handle the global state of my react application.
I am trying to create a login screen where a user enters their username and password, this gets sent to my API to authenticate and upon success, I want to then set the global state of isLoggedIn to true.
So far, I am able to set the global state with one mutation which utilises the #client declaration so it is only concerned with local state. I have another mutation which makes the graphQL API call and validates username / password and then returns success / error responses.
I want to be able to set isLoggedIn once the API call mutation has completed or failed.
My client has the following default state and resolvers set like so:
const httpLink = new HttpLink({
uri: '/graphql',
credentials: 'same-origin'
});
const cache = new InMemoryCache();
const stateLink = withClientState({
cache,
resolvers: {
Mutation: {
updateLoggedInStatus: (_, { isLoggedIn }, { cache }) => {
const data = {
loggedInStatus: {
__typename: 'LoggedInStatus',
isLoggedIn
},
};
cache.writeData({ data });
return null;
},
},
},
defaults: {
loggedInStatus: {
__typename: 'LoggedInStatus',
isLoggedIn: false,
},
},
});
const link = ApolloLink.from([stateLink, httpLink])
const client = new ApolloClient({
link,
cache
});
export default client
Then in my Login component I have the following mutations and queries which I pass as a HOC with the help of compose:
const UPDATE_LOGGED_IN_STATUS = gql`
mutation updateLoggedInStatus($isLoggedIn: Boolean) {
updateLoggedInStatus(isLoggedIn: $isLoggedIn) #client
}`
const AUTHENTICATE = gql`
mutation authenticate($username: String!, $password: String!) {
auth(username: $username, password: $password) {
username
sales_channel
full_name
roles
}
}`
const GET_AUTH_STATUS = gql`
query {
loggedInStatus #client {
isLoggedIn
}
}`
export default compose(
graphql(GET_AUTH_STATUS, {
props: ({ data: { loading, error, loggedInStatus } }) => {
if (loading) {
return { loading };
}
if (error) {
return { error };
}
return {
loading: false,
loggedInStatus
};
},
}),
graphql(UPDATE_LOGGED_IN_STATUS, {
props: ({ mutate }) => ({
updateLoggedInStatus: isLoggedIn => mutate({ variables: { isLoggedIn } }),
}),
}),
graphql(AUTHENTICATE, {
props: ({ mutate }) => ({
authenticate: (username, password) => mutate({ variables: { username, password } }),
}),
})
)(withRouter(Login));
So as you can see I have this.props.authenticate(username, password) which is used when the login form is submitted.
Then I have the this.props.updateLoggedInStatus(Boolean) which I am able to update the client cache / state.
How do I combine these so that I can call authenticate() and if it's successful, set the loggedInStatus and if it fails, set a hasErrored or errorMessage flag of sorts?
Thanks in advance.
EDIT:
I have attempted to handle updating the state within the callback of my mutation.
// Form submission handler
onSubmit = async ({ username, password }) => {
this.setState({loading: true})
this.props.authenticate(username, password)
.then(res => {
this.setState({loading: false})
this.props.updateLoggedInStatus(true)
})
.catch(err => {
this.setState({loading: false, errorMessage: err.message})
console.log('err', err)
})
}
Is there a better way of doing it than this? It feels very convoluted having to wait for the call back. I would have thought I could map the response to my cache object via my resolver?
I think the way you're currently handling it (calling authenticate and then updateLoggedInStatus) is about as clean and simple as you're going to get with apollo-link-state. However, using apollo-link-state for this is probably overkill in the first place. It would probably be simpler to derive logged-in status from Apollo's cache instead. For example, you could have a HOC like this:
import client from '../wherever/client'
const withLoggedInUser = (Component) => {
const user = client.readFragment({
id: 'loggedInUser',
fragment: gql`
fragment loggedInUser on User { # or whatever your type is called
username
sales_channel
full_name
roles
# be careful about what fields you list here -- even if the User
# is in the cache, missing fields will result in an error being thrown
}
`
})
const isLoggedIn = !!user
return (props) => <Component {...props} user={user} isLoggedIn={isLoggedIn}/>
}
Notice that I use loggedInUser as the key. That means we also have to utilize dataIdFromObject when configuring the InMemoryCache:
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory'
const cache = new InMemoryCache({
dataIdFromObject: object => {
switch (object.__typename) {
case 'User': return 'loggedInUser'
// other types you don't want the default behavior for
default: return defaultDataIdFromObject(object);
}
}
})

Resources