Laravel lighthouse current user is null via next apollo - reactjs

I have a fresh copy of laravel with sanctum and lighthouse. When I do the login route via axios, everything works as expected. After logging in via axios, I added a lazyquery to attempt to query some guarded fields but I get unauthenticated. I am not sure why and it has been three days I've been dealing with this. I'd really appreciate your help.
This works
useEffect(() => {
axios.defaults.withCredentials = true;
// get the token from the server
axios.get(`http://api.newods.test/sanctum/csrf-cookie`).then(function (resolve){
// try login with the user
axios.post('http://api.newods.test/api/login', {
email: 'test#test.com',
password: 'test'
}).then(function (resolve) {
console.log(`logged in ${resolve.data}`);
axios
.get("http://api.newods.test/api/gated", { withCredentials: true })
.then(function (resolve) {
console.log(`gated ${resolve.data}`);
axios
.get("http://api.newods.test/api/logout", {
withCredentials: true,
})
.then(function (resolve) {
console.log(`logged out ${resolve.data}`);
axios
.get("http://api.newods.test/api/gated", {
withCredentials: true,
})
.then(function (resolve) {
console.log(
`trying to get to gated after logging out ${resolve.data}`
);
});
});
});
});
});
}, []);
But when I cut it short and change to this, I get unauthenticated
const HELLO = gql\`
query hello {
hello
}
`;
function Home() {
const [hello, { loading, data }] = useLazyQuery(HELLO);
useEffect(() => {
axios.defaults.withCredentials = true;
// get the token from the server
axios.get(`http://api.newods.test/sanctum/csrf-cookie`).then(function (resolve){
// try login with the user
axios.post('http://api.newods.test/api/login', {
email: 'test#test.com',
password: 'test'
}).then(function (resolve) {
console.log('logged in');
});
});
}, []);
return (
<div className="container">
<div>Index</div>
<button onClick={() => hello()}>
Click to hello world
</button>
<p>{data && data.hello || ''}</p>
</div>
);
}
export default withApollo(Home);
And that returns unauthenticated when I add the #guard directive and I see the token from the axios login request is in the headers... I am not sure what I am missing here I'd greatly appreciate your help.
schema.graphql
type Query {
users: [User!]! #paginate(defaultCount: 10)
user(id: ID #eq): User #find
hello: String! #guard
me: User #auth
}
.env
SESSION_DRIVER=cookie
SESSION_LIFETIME=120
SESSION_DOMAIN=.newods.test
SANCTUM_STATEFUL_DOMAINS=newods.test:3000
config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie', 'graphql'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];
config/lighthouse
'route' => [
/*
* The URI the endpoint responds to, e.g. mydomain.com/graphql.
*/
'uri' => '/graphql',
/*
* Lighthouse creates a named route for convenient URL generation and redirects.
*/
'name' => 'graphql',
/*
* Beware that middleware defined here runs before the GraphQL execution phase,
* make sure to return spec-compliant responses in case an error is thrown.
*/
'middleware' => [
\Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class,
// Logs in a user if they are authenticated. In contrast to Laravel's 'auth'
// middleware, this delegates auth and permission checks to the field level.
\Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication::class,
],
/*
* The `prefix` and `domain` configuration options are optional.
*/
//'prefix' => '',
//'domain' => '',
],
In my next app with apollo
create.js
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import Cookies from 'js-cookie';
import { serverUrl } from '../config';
export default function createApolloClient(initialState, ctx) {
// The `ctx` (NextPageContext) will only be present on the server.
// use it to extract auth headers (ctx.req) or similar.
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = Cookies.get("XSRF-TOKEN");
// console.log(`token is ${token}`);
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
"Access-Control-Allow-Credentials": true,
...(token ? { authorization: `X-XSRF-TOKEN=${token}` } : {}),
},
};
});
const httpLink = createHttpLink({
uri: serverUrl,
credentials: 'same-origin',
});
return new ApolloClient({
ssrMode: Boolean(ctx),
link: authLink.concat(httpLink),
connectToDevTools: true,
cache: new InMemoryCache().restore(initialState),
});
}
withApollo.js
import React from "react";
import Head from "next/head";
import { ApolloProvider } from "#apollo/react-hooks";
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
import fetch from "isomorphic-unfetch";
import createApolloClient from './create';
let apolloClient = null;
/**
* Creates and provides the apolloContext
* to a next.js PageTree. Use it by wrapping
* your PageComponent via HOC pattern.
* #param {Function|Class} PageComponent
* #param {Object} [config]
* #param {Boolean} [config.ssr=true]
*/
export function withApollo(PageComponent, { ssr = true } = {}) {
const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
const client = apolloClient || initApolloClient(apolloState);
return (
<ApolloProvider client={client}>
<PageComponent {...pageProps} />
</ApolloProvider>
);
};
// Set the correct displayName in development
if (process.env.NODE_ENV !== "production") {
const displayName =
PageComponent.displayName || PageComponent.name || "Component";
if (displayName === "App") {
console.warn("This withApollo HOC only works with PageComponents.");
}
WithApollo.displayName = `withApollo(${displayName})`;
}
if (ssr || PageComponent.getInitialProps) {
WithApollo.getInitialProps = async (ctx) => {
const { AppTree } = ctx;
// Initialize ApolloClient, add it to the ctx object so
// we can use it in `PageComponent.getInitialProp`.
const apolloClient = (ctx.apolloClient = initApolloClient(
{},
ctx.req.headers.cookie
));
// Run wrapped getInitialProps methods
let pageProps = {};
if (PageComponent.getInitialProps) {
pageProps = await PageComponent.getInitialProps(ctx);
}
// Only on the server:
if (typeof window === "undefined") {
// When redirecting, the response is finished.
// No point in continuing to render
if (ctx.res && ctx.res.finished) {
return pageProps;
}
// Only if ssr is enabled
if (ssr) {
try {
// Run all GraphQL queries
const { getDataFromTree } = await import("#apollo/react-ssr");
await getDataFromTree(
<AppTree
pageProps={{
...pageProps,
apolloClient,
}}
/>
);
} catch (error) {
// Prevent Apollo Client GraphQL errors from crashing SSR.
// Handle them in components via the data.error prop:
// https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
console.error("Error while running `getDataFromTree`", error);
}
// getDataFromTree does not call componentWillUnmount
// head side effect therefore need to be cleared manually
Head.rewind();
}
}
// Extract query data from the Apollo store
// #ts-ignore
const apolloState = apolloClient.cache.extract();
return {
...pageProps,
apolloState,
};
};
}
return WithApollo;
}
/**
* Always creates a new apollo client on the server
* Creates or reuses apollo client in the browser.
* #param {Object} initialState
*/
function initApolloClient(initialState = {}, cookie = "") {
// Make sure to create a new client for every server-side request so that data
// isn"t shared between connections (which would be bad)
if (typeof window === "undefined") {
return createApolloClient(initialState, cookie);
}
// Reuse client on the client-side
if (!apolloClient) {
// #ts-ignore
apolloClient = createApolloClient(initialState);
}
return apolloClient;
}

I have a very similar architecture, but using Vue. From comparing your code against my working implementation, I think the majority of your problems are in create.js.
I don't know much about js-cookie, but this is how I get the XSRF-TOKEN, and decode it.
let token = RegExp('XSRF-TOKEN[^;]+').exec(document.cookie)
token = decodeURIComponent(token ? token.toString().replace(/^[^=]+./, '') : '')
Then, in your setContext, you need to set the header as follows.
return {
headers: {
...headers,
'X-XSRF-TOKEN': token,
}
}
Also, I had trouble with credentials: 'same-origin' even though I'm using a subdomain. Therefore I would suggest:
const httpLink = createHttpLink({
uri: serverUrl,
credentials: 'include',
})

Related

Google OAuth components must be used within GoogleOAuthProvider

I want to build my next js project in which i am using
https://www.npmjs.com/package/#react-oauth/google
but when I build it i get the following :
this is layout.js and in _app.js I have all the components wrapped in GoogleOAuthProvider
import { GoogleLogin } from '#react-oauth/google';
import {FcGoogle} from "react-icons/Fc"
import { useGoogleLogin } from '#react-oauth/google';
export default function Layout({ children }) {
const client_id = ""
const responseGoogle = (response) => {
console.log(response);
}
CUTTED (NOT RELEVANT)
const login = useGoogleLogin({
onSuccess: codeResponse => {
const { code } = codeResponse;
console.log(codeResponse)
axios.post("http://localhost:8080/api/create-tokens", { code }).then(response => {
const { res, tokens } = response.data;
const refresh_token = tokens["refresh_token"];
const db = getFirestore(app)
updateDoc(doc(db, 'links', handle), {
refresh_token : refresh_token
})
updateDoc(doc(db, 'users', useruid), {
refresh_token : refresh_token
}).then(
CUTTED (NOT RELEVANT)
)
}).catch(err => {
console.log(err.message);
})
},
onError: errorResponse => console.log(errorResponse),
flow: "auth-code",
scope: "https://www.googleapis.com/auth/calendar"
});
return (
<>
CUTTED (NOT RELEVANT)
</>
)
}
Everything works perfect in dev mode but it does not want to build
I've faced this issue too. So I use 'GoogleLogin' instead of 'useGoogleLogin', then you can custom POST method on 'onSuccess' property.
import { GoogleLogin, GoogleOAuthenProvider} from '#react-oauth/google';
return(
<GoogleOAuthProvider clientId="YOUR CLIENT ID">
<GoogleLogin
onSuccess={handleLogin}
/>
</GoogleOAuthProvider>
The async function will be like...
const handleLogin = async = (credentialResponse) => {
var obj = jwt_decode(credentialResponse.credential);
var data = JSON.stringify(obj);
console.log(data);
const data = {your data to send to server};
const config = {
method: 'POST',
url: 'your backend server or endpoint',
headers: {},
data: data
}
await axios(config)
}
Spending whole day, this solve me out. Just want to share.
You have to wrap your application within GoogleOAuthProvider component. Please keep in mind that you will need your client ID for this.
import { GoogleOAuthProvider } from '#react-oauth/google';
<GoogleOAuthProvider clientId="<your_client_id>">
<SomeComponent />
...
<GoogleLoginButton onClick={handleGoogleLogin}/>
</GoogleOAuthProvider>;

How to access the request context from the setContext function from graphql apollo-client in SSR from nextJS?

So I'm trying to do a custom header for the requests to my graphql backend from my nextJS frontend. The code of the graphql client is as follows:
import { ApolloClient, ApolloLink, InMemoryCache, createHttpLink } from '#apollo/client'
import { setContext } from '#apollo/client/link/context'
import getConfig from 'next/config'
import nookies, { parseCookies } from 'nookies'
const { publicRuntimeConfig } = getConfig()
const httpLink = createHttpLink({
uri: publicRuntimeConfig.uri,
credentials: 'same-origin'
})
const authLink = setContext((_, { headers }) => {
let token = ''
if (typeof window !== 'undefined') {
const { 'toDo-token': newToken } = parseCookies()
token = newToken
} else {
// error is right here. I cant find a way to access the ctx
const { 'toDo-token': newToken } = nookies.get(ctx)
token = newToken
}
return {
headers: {
...headers,
Authorization: token ?? undefined
}
}
})
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
ssrMode: typeof window === 'undefined'
})
export default client
On the client side it works just fine. However, since I cant access client cookies on the server side, I'm trying to use the get function from nookies to grab the auth cookies and pass to the headers with the setContext function. The problem I'm having is that I cant find a way to grab the app context from next so I can look at the request info and grab the ctx from there so I can see the cookies.
I do something similar in my _app.tsx in the following way:
MyApp.getInitialProps = async ({ ctx }: AppContext) => {
const { locale } = ctx
const { 'toDo-token': token } = nookies.get(ctx)
let user = null
try {
user = await useUser()
} catch {
user = null
}
return {
locale: locale === 'default' ? 'en' : locale as LocaleEnum,
token: token ?? null,
user
}
}
Does anyone know how I can access the request context in the graphql setContext function?

Graphql useSubscription not giving response everytime

I am using Graphql useSubscription hook to hit a websocket api but there seems to be an issue. I am only receiving data when i enter the component for the first time or when i go back to some other component and come back again, when trying to refresh majority of the times I do not get the data.
Below is my setup for the following.
/* eslint-disable flowtype/no-types-missing-file-annotation */
import Cookies from 'js-cookie'
import { split, HttpLink, InMemoryCache, ApolloClient } from '#apollo/client'
import { setContext } from '#apollo/client/link/context'
import { WebSocketLink } from '#apollo/client/link/ws'
import { getMainDefinition } from '#apollo/client/utilities'
import { onError } from 'apollo-link-error'
import { ApolloLink } from 'apollo-link'
const env = process.env.NODE_ENV
const domain = env === 'development' ? 'localhost' : '.xyz'
const url = env === 'development' ? 'https://staging-xxx.xxxx.xx' : process.env.REACT_APP_API_URL;
const wsURL = env === 'development' ? 'wss://staging-xxx.xxxxx.xx/subscriptions' : process.env.REACT_APP_WSS_URL;
const httpLink = new HttpLink({
uri: url,
credentials: 'include'
})
const authLink = setContext((_: any, { headers }: any) => {
const app_token = Cookies.get('xxxxx', { domain: domain })
let token = app_token || 'insta-checkout'
return {
headers: {
...headers,
MUDEY_AUTH_TOKEN: token
}
}
})
const wsLink = new WebSocketLink({
uri: wsURL,
options: {
reconnect: true,
connectionParams: async () => {
const app_token = await Cookies.get('xxxxx', { domain: domain })
return {
credentials: 'include',
MUDEY_AUTH_TOKEN: app_token,
Authorization: 'Basic xxxxxxxxxxxxxxxxxxx'
}
}
}
})
const link = split(
({ query }: any) => {
const definition = getMainDefinition(query)
return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'
},
wsLink,
authLink.concat(httpLink)
)
const client = new ApolloClient({
cache: new InMemoryCache(),
link: ApolloLink.from([link])
})
export default client
Now when i go my component for the first time, i am calling the subscriptions api by
import React from "react";
import { useMutation, useSubscription } from "#apollo/react-hooks";
export const NewComponent = () => {
const {
loading: loadingPackages,
data,
error,
} = useSubscription(SUBSCRIBE_CAR_PACKAGES, {
onSubscriptionData: useCallback((res: any) => {
const {
subscribeCarJourneyPackages: { message: stopWS, data: packagesResult },
} = res.subscriptionData.data;
if (packagesResult !== null) {
console.log("packarray", packagesResult);
setIsSubsLoading(true);
}
if (stopWS === "SUBSCRIPTION_COMPLETE") {
dispatch({ type: SET_ALL_PACKAGES, payload: packArray });
setIsSubsLoading(false);
} else {
// setIsSubsLoading(true)
}
}, []),
onError: useCallback((err: any) => {
apiErrorHandler(err);
}, []),
variables: { id: journeyID },
});
return null;
};
So the response i see is
But once i start refreshing the page , i only see
So what the issue in my frontend, for not getting the response 100% of the time ? should we need to close the connection everytime we receive response ?
Also i see the subscription api hitting even when i am in my homepage, where ideally it should hit in the results page where i want it, do this happens the moment we define connection and is it normal?
I know it's late but it maybe help someone else !
try giving fetchPolicy: 'cache-and-network' as option below your variables field.
like this :
variables: {... your variables },
fetchPolicy: 'cache-and-network'

React relay auth middleware

I am trying to build a react app using relay following instructions from the react-relay step by step guide. In the guide the auth token is stored in the env file, and I am trying to retrieve my token from memory which is created when the user logs in and is passed down to all components using the context API. I am not storing it in local storage and have a refresh token to automatically refresh the JWT.
From the tutorial, the relay environment class is not a React component because of which I cannot access the context object.
Is there a way to pass the token from my context to the relay environment class or any middleware implementation to accomplish this.
Any help is greatly appreciated.
import { useContext } from 'react';
import { Environment, Network, RecordSource, Store } from 'relay-runtime';
import axios from "axios";
import { AppConstants } from './app-constants';
import { AuthContext, AuthSteps } from "./context/auth-context";
import { useCookie } from './hooks/useCookie';
interface IGraphQLResponse {
data: any;
errors: any;
}
async function fetchRelay(params: { text: any; name: any; }, variables: any, _cacheConfig: any) {
const authContext = useContext(AuthContext); //Error - cannot access context
const { getCookie } = useCookie(); //Error - cannot access context
axios.interceptors.request.use(
(config) => {
const accessToken = authContext && authContext.state && authContext.state.token;
if(accessToken) config.headers.Authorization = `Bearer ${accessToken}`;
return config;
},
(error) => {
Promise.reject(error);
}
);
axios.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;
const refreshToken = getCookie(AppConstants.AUTH_COOKIE_NAME);
if(refreshToken && error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const response = await axios
.post(process.env.REACT_APP_REFRESH_TOKEN_API_URL!, { refreshToken: refreshToken });
if (response.status === 200 && response.data && response.data.accessToken) {
authContext && authContext.dispatch && authContext.dispatch({
payload: {
token: response.data.accessToken
},
type: AuthSteps.SIGN_IN
});
accessToken = response.data.accessToken;
return axios(originalRequest);
}
}
return Promise.reject(error);
}
);
const data: IGraphQLResponse = await axios.post(process.env.REACT_APP_GRAPHQL_URL!, {
query: params.text,
variables
});
if(Array.isArray(data.errors)) {
throw new Error(
`Error fetching GraphQL query '${
params.name
}' with variables '${JSON.stringify(variables)}': ${JSON.stringify(
data.errors,
)}`,
);
}
return data;
}
export default new Environment({
network: Network.create(fetchRelay),
store: new Store(new RecordSource(), {
gcReleaseBufferSize: 10,
}),
});

React Log out all Pages using Context and Axios interceptors

I am outside a React component but need to update my auth context 'AuthContext' to delete the existing user to log the user out entirely from all logged in pages
I'm able to dispatch a redux action using Redux. How can I achieve the same using Context?
Below are my code... the logOutAll function is not working because useContext can't be used outside of a React component or function
Appreciate advise from those who have successfully implemented this. Thanks
import { useContext } from "react"
import axios from "axios"
import { AuthContext } from "../context/authContext"
const api = axios.create({
baseURL: "http://localhost:5000/api",
headers: {
"Content-Type": "application/json",
},
})
/**
intercept any error responses from the api
and check if the token is no longer valid.
ie. Token has expired or user is no longer
authenticated.
logout the user if the token has expired
**/
// const logOutAll = () => {
// const { authStatus, setAuthStatus } = useContext(AuthContext)
// console.log(authStatus)
// setAuthStatus({
// ...authStatus,
// isAuthenticated: false,
// isLoading: false,
// user: null,
// })
// }
api.interceptors.response.use(
res => res,
err => {
if (err.response.status === 401) {
console.log("log out all")
logOutAll() //! CANNOT WORK BECAUSE UseContext cannot be called outside React function
or component
}
return Promise.reject(err)
}
)
export default api
I set up my axios interceptors in my app.js file instead and it seems to be working fine
const App = () => {
const { authStatus, setAuthStatus } = useContext(AuthContext)
useEffect(() => {
const checkLoggedIn = async () => {
let token = localStorage.getItem("#token")
if (token === null) {
localStorage.setItem("#token", "")
token = ""
}
const res = await api.get("/users/me", {
headers: {
authorization: token,
},
})
//TODO
// const token = localStorage.getItem("#token")
// console.log("TOKEN", token)
// if (token) {
// setAuthToken(token)
// }
// const res = await api.get("http://localhost:5000/api/users/me")
console.log(res)
setAuthStatus(authStatus => ({
...authStatus,
isAuthenticated: true,
isLoading: false,
user: res.data,
}))
}
checkLoggedIn()
}, [setAuthStatus])
api.interceptors.response.use(
res => res,
err => {
if (err.response.status === 401) {
setAuthStatus({
...authStatus,
isAuthenticated: false,
isLoading: false,
user: null,
})
}
return Promise.reject(err)
}
)

Resources