I have react-admin set up with GraphQL and works fine for the most part... however there seems to be an issue with logging in initially and having the Dashboard make a query; the issue being that it will not resolve upon initial loading. It goes from logging in -> Dashboard and I'm met with a component that loads forever with no data or errors.
My Admin is set up like this:
const routes = [
<Route exact path="/support-chat-list" component={SupportChatPage} noLayout />,
<Route exact path="/group-chat" component={GroupChatPage} noLayout />,
<Route exact path="/" component={Dashboard} />,
];
const App = () => (
<Admin dashboard={Dashboard} layout={MyLayout} customRoutes={routes} dataProvider={dataProvider} authProvider={authProvider} i18nProvider={i18nProvider}>
{/* my resources... */}
</Admin>
);
and my Dashboard looks like this:
const requestOptions = (startingAfter, startingBefore) => {
return {
type : 'getList',
resource: 'appointments',
payload : {
pagination: {
perPage: 100,
page : 0,
},
sort : {
field: 'start_at',
order: 'asc'
},
filter : {
startingAfter: startingAfter.toString(),
startingBefore: startingBefore.toString(),
}
}
};
};
const handleSelectEvent = (event, e, redirectTo) => {
redirectTo('show', ROUTES.APPOINTMENTS, event.resource.id);
};
const initialState = {
middleDate: DateTime.local().startOf('day'),
startDate: DateTime.local().minus({days: 3}).startOf('day'),
endDate: DateTime.local().plus({days: 3}).endOf('day'),
};
export function Dashboard() {
useAuthenticated();
const redirectTo = useRedirect();
const [dateRanges, setDateRanges] = useState(initialState);
const {data, loading, error} = useQueryWithStore(requestOptions(dateRanges.startDate, dateRanges.endDate));
if (error) return <Error />;
return (
<Card>
<Title title="Admin" />
<CardContent>
{loading && <Loading/>}
{!loading &&
<Calendar
events={translateAppointmentData(data)}
//.......
I've tried playing around with useQuery and useQueryWithStore but nothing seems to work. It will only resolve when I refresh the page, and only then it will load everything.
I have a suspicion that it has to do with my authProvider, which is a class that handles authentication. It's a bit bloated so I'll try my best to show the things related to login:
async login({username, password}) {
const response = await authClient.passwordGrant(username, password, ACCESS_TOKEN_SCOPES);
await this.processTokenResponse(response);
return Promise.resolve();
}
authClient.passwordGrant looks like this:
passwordGrant(username, password, scopes) {
const request = new Request(`${this.url}/oauth/access_token`, {
method: 'POST',
body: JSON.stringify({
username: username,
password: password,
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'password',
scope: scopes.join(' '),
}),
headers: new Headers({ 'Content-Type': 'application/json' }),
});
return fetch(request)
.then(response => {
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
return response.json();
});
}
while processTokenResponse looks like this:
processTokenResponse(response) {
const now = DateTime.local();
localStorage.setItem(TOKEN_STORAGE_KEY, response.access_token);
localStorage.setItem(TOKEN_EXPIRY_STORAGE_KEY, now.plus({seconds: response.expires_in - EXPIRY_BUFFER}).toString());
localStorage.setItem(SCOPES_STORAGE_KEY, JSON.stringify({scopes: response.scopes}));
localStorage.setItem(REFRESH_TOKEN_STORAGE_KEY, response.refresh_token);
localStorage.setItem(REFRESH_TOKEN_EXPIRY_STORAGE_KEY, now.plus({seconds: response.refresh_token_expires_in - EXPIRY_BUFFER}).toString());
return this.fetchActiveUserData();
}
and fetchActiveUserData looks like this:
async fetchActiveUserData() {
let dataProvider = new ActiveUserDataProvider();
return await dataProvider.getOne({}).then((response) => {
return this.processLoadUserResponse(response.data);
})
}
in which my dataProvider looks like this:
import apolloClient from '../apolloClient';
import { BaseResourceDataProvider } from './baseResourceDataProvider';
import {FetchMeQuery} from "../graphql/users";
const FetchMeQuery = gql`
query FetchMe {
me {
id
firstName
lastName
name
username
timezone
}
}
`;
class ActiveUserDataProvider extends BaseResourceDataProvider {
getOne({ id }: { id?: string }) { // maybe something wrong here?
return apolloClient.query({
query: FetchMeQuery,
fetchPolicy: 'cache-first'
}).then((result) => {
return {
data: result.data.me,
};
});
}
}
I apologize for the long post but I'm completely lost on why my Dashboard isn't querying data once initially logged in, and needing a refresh for it to work. I'm hoping someone can point where something's wrong. Thank you.
After much digging, it turned out to be how I set up my ApolloClient. Previously I was using Apollo Link to set up my auth middleware, and discovered that it wasn't calling the middleware at all.
I discovered this question and replaced ApolloLink with setContext and it worked perfectly. This is the code now:
const authMiddleware = setContext(async (_, { headers }) => {
const token = await authProvider.checkAuth();
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
}
}
});
const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: from([
authMiddleware,
httpLink
]),
});
Related
I'm working on a project based on React-Native.
When a user signs up, I want to send an email verification code using AWS-SES.
Back-End entered AWS-SES information for this purpose
I want to use Axios to retrieve that information to Front-End
and send someone an email authentication number when I click [Button].
To do this, I'm asking you a question here
because I want to know how to get AWS-SES information from Back-End to Front-End and run it.
I need your help.
Thank you for reading my post.
Back-End
const { Router } = require("express");
const router = Router();
const aws = require("aws-sdk");
router.get("/mail", get_email);
exports.get_email = async (req, res) => {
try {
const testA = () => {
let ses = new aws.SES({
region: "ap-northeast-2",
});
let params = {
Destination: {
ToAddresses: ["test#gmail.com"],
},
Message: {
Body: {
Text: {
Data: "TEST MESSAGE",
},
},
Subject: { Data: "TEST" },
},
Source: "test#gmail.com",
};
return ses.sendEmail(params).promise();
};
return res.status(200).json({ success: true, testA });
} catch (e) {
console.log(e);
}
};
Front-End
import React, { useState, useEffect } from "react";
import axios from "axios";
const [test, setTest] = useState([]);
useEffect(() => {
getTest();
}, []);
const getTest = async () => {
await axios.get("/mail").then((res) => {
if (res.data) {
setTest(res.data.testA);
}
});
};
return (
<div>
<div onClick={()=>{
getTest();
console.log("Test AWS-SES : ", test);
}}>
Test
</div>
</div>
);
Result
Test AWS-SES : undefined
I cannot correctly set my jwt token from my cookie to my Headers for an authenticaed gql request using apollo client.
I believe the problem is on my withApollo.js file, the one that wraps the App component on _app.js. The format of this file is based off of the wes bos advanced react nextjs graphql course. What happens is that nextauth saves the JWT as a cookie, and I can then grab the JWT from that cookie using a custom regex function. Then I try to set this token value to the authorization bearer header. The problem is that on the first load of a page with a gql query needing a jwt token, I get the error "Cannot read property 'cookie' of undefined". But, if I hit browser refresh, then suddenly it works and the token was successfully set to the header.
Some research led me to adding a setcontext link and so that's where I try to perform this operation. I tried to async await setting the token value but that doesn't seem to have helped. It just seems like the headers don't want to get set until on the refresh.
lib/withData.js
import { ApolloClient, ApolloLink, InMemoryCache } from '#apollo/client';
import { onError } from '#apollo/link-error';
import { getDataFromTree } from '#apollo/react-ssr';
import { createUploadLink } from 'apollo-upload-client';
import withApollo from 'next-with-apollo';
import { setContext } from 'apollo-link-context';
import { endpoint, prodEndpoint } from '../config';
import paginationField from './paginationField';
const getCookieValue = (name, cookie) =>
cookie.match(`(^|;)\\s*${name}\\s*=\\s*([^;]+)`)?.pop() || '';
let token;
function createClient(props) {
const { initialState, headers, ctx } = props;
console.log({ headers });
// console.log({ ctx });
return new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
if (networkError)
console.log(
`[Network error]: ${networkError}. Backend is unreachable. Is it running?`
);
}),
setContext(async (request, previousContext) => {
token = await getCookieValue('token', headers.cookie);
return {
headers: {
authorization: token ? `Bearer ${token}` : '',
},
};
}),
createUploadLink({
uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
fetchOptions: {
credentials: 'include',
},
headers,
}),
]),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
// TODO: We will add this together!
// allProducts: paginationField(),
},
},
},
}).restore(initialState || {}),
});
}
export default withApollo(createClient, { getDataFromTree });
page/_app.js
import { ApolloProvider } from '#apollo/client';
import NProgress from 'nprogress';
import Router from 'next/router';
import { Provider, getSession } from 'next-auth/client';
import { CookiesProvider } from 'react-cookie';
import nookies, { parseCookies } from 'nookies';
import Page from '../components/Page';
import '../components/styles/nprogress.css';
import withData from '../lib/withData';
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
function MyApp({ Component, pageProps, apollo, user }) {
return (
<Provider session={pageProps.session}>
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} {...user} />
</Page>
</ApolloProvider>
</Provider>
);
}
MyApp.getInitialProps = async function ({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
pageProps.query = ctx.query;
const user = {};
const { req } = ctx;
const session = await getSession({ req });
if (session) {
user.email = session.user.email;
user.id = session.user.id;
user.isUser = !!session;
// Set
nookies.set(ctx, 'token', session.accessToken, {
maxAge: 30 * 24 * 60 * 60,
path: '/',
});
}
return {
pageProps,
user: user || null,
};
};
export default withData(MyApp);
api/auth/[...nextAuth.js]
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import axios from 'axios';
const providers = [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
Providers.Credentials({
name: 'Credentials',
credentials: {
username: { label: 'Username', type: 'text', placeholder: 'jsmith' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
const user = await axios
.post('http://localhost:1337/auth/local', {
identifier: credentials.username,
password: credentials.password,
})
.then((res) => {
res.data.user.token = res.data.jwt;
return res.data.user;
}) // define user as res.data.user (will be referenced in callbacks)
.catch((error) => {
console.log('An error occurred:', error);
});
if (user) {
return user;
}
return null;
},
}),
];
const callbacks = {
// Getting the JWT token from API response
async jwt(token, user, account, profile, isNewUser) {
// WRITE TO TOKEN (from above sources)
if (user) {
const provider = account.provider || user.provider || null;
let response;
let data;
switch (provider) {
case 'google':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/google/callback?access_token=${account?.accessToken}`
);
data = await response.json();
if (data) {
token.accessToken = data.jwt;
token.id = data.user._id;
} else {
console.log('ERROR No data');
}
break;
case 'local':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/local/callback?access_token=${account?.accessToken}`
);
data = await response.json();
token.accessToken = user.token;
token.id = user.id;
break;
default:
console.log(`ERROR: Provider value is ${provider}`);
break;
}
}
return token;
},
async session(session, token) {
// WRITE TO SESSION (from token)
// console.log(token);
session.accessToken = token.accessToken;
session.user.id = token.id;
return session;
},
redirect: async (url, baseUrl) => baseUrl,
};
const sessionPreferences = {
session: {
jwt: true,
},
};
const options = {
providers,
callbacks,
sessionPreferences,
};
export default (req, res) => NextAuth(req, res, options);
I have a following function in Api.js
const _getCategories = async () => {
var categories = [];
let token = await getJwt();
var config = {
method: 'get',
url: 'myurl',
headers: {
'Authorization': 'Bearer' + ' ' + token
},
data : ''
};
axios(config)
.then(function (response) {
if(response.status == 200)
{
let res = response.data;
// Create a variable having name in the list.
categories = res.data.apps.map(function(item) {
return {
title: item.name,
color: AppStyles.colorSet.analyticsColor,
lightColor: AppStyles.colorSet.analyticsLightColor,
icon: AppStyles.iconSet.analytics
};
});
// console.log('Returning Categories');
console.log(categories);
return categories;
//console.log(data1)
// Handle and fetch the list of apps
}
else
{
// API didn't go through, generate the error functions
}
})
.catch(function (error) {
console.log(error);
});
};
and I am loading it in homscreen.js
class DashboardScreen extends React.Component {
constructor(props) {
super(props);
const { navigation } = props;
navigation.setOptions({
title: 'Dashboard',
headerLeft: () => (
<TouchableOpacity
onPress={() => {
navigation.openDrawer();
}}
>
<Icon
style={AppStyles.styleSet.menuButton}
name="ios-menu"
size={AppStyles.iconSizeSet.normal}
color={AppStyles.colorSet.mainThemeForegroundColor}
/>
</TouchableOpacity>
),
});
this.state = {
categories: [],
};
}
componentDidMount() {
if (!this.state.data) {
Api.getCategories().then(data => console.log("The data is "+data))
.catch(err => { /*...handle the error...*/});
}
}
onPressCategory = item => {
// Handle onpress for the items
};
render() {
//console.log(this.state.categories);
categoryButtonsRow1 = this.state.categories.map((item, index) => {
if (index < 3) {
return (
<CategoryButton
onPress={() => this.onPressCategory(item)}
color={item.color}
lightColor={item.lightColor}
icon={item.icon}
title={item.title}
/>
);
}
});
return (
<ScrollView style={styles.container}>
<View style={styles.row}>{categoryButtonsRow1}</View>
</ScrollView>
);
}
}
But I am getting category as undefined while printing in render().
I even tried to create an async function in the homescreen.js and call the api with await and set the state after the same but still it is coming as undefined.
Any guesses to what I am doing wrong here. Can anyone help with the same. My best guess is that I am not handling the api request properly.
EDIT
I tried Use Async/Await with Axios in React.js but it is still printing undefined to the same.
The reason for getting undefined is the _getCategories is that its not returning anything and you are chaining using .then to get data so the caller has no way to get this data as a callback is not passed.
You can change the to await like this
const _getCategories = async () => {
var categories = [];
let token = await getJwt();
var config = {
method: 'get',
url: 'myurl',
headers: {
Authorization: 'Bearer' + ' ' + token,
},
data: '',
};
const response = await axios(config);
if (response.status == 200) {
let res = response.data;
// Create a variable having name in the list.
categories = res.data.apps.map(function (item) {
return {
title: item.name,
color: AppStyles.colorSet.analyticsColor,
lightColor: AppStyles.colorSet.analyticsLightColor,
icon: AppStyles.iconSet.analytics,
};
});
// console.log('Returning Categories');
console.log(categories);
return categories;
//console.log(data1)
// Handle and fetch the list of apps
} else {
// API didn't go through, generate the error functions
return null;
}
};
And you can set the state in componentDidMount (should be async)
this.setState({categories:await api._getCategories()});
I'm using the stack apiplatform and react admin.
My JWT authentification works fine on apiplatform.
I try to use it on my react admin backoffice.
I followed those documentations:
https://api-platform.com/docs/admin/authentication-support/
https://marmelab.com/react-admin/doc/2.9/Authentication.html
The authentification/authorization works but on login success, I am redirected to the backoffice and I have a server communication error because the token send to the api is null.
I see it in the localstorage and if I refresh the page, everything works fine.
It's like the redirection on success happen before the token was store.
Here is my code:
App.js
import React from "react";
import { HydraAdmin, ResourceGuesser } from "#api-platform/admin";
import authProvider from "./components/authProvider";
import parseHydraDocumentation from "#api-platform/api-doc-parser/lib/hydra/parseHydraDocumentation";
import {
dataProvider as baseDataProvider,
fetchHydra as baseFetchHydra
} from "#api-platform/admin";
import { Redirect } from "react-router-dom";
const entrypoint = "http://localhost:8089/api";
const fetchHeaders = {
Authorization: `Bearer ${window.localStorage.getItem("token")}`
};
const fetchHydra = (url, options = {}) =>
baseFetchHydra(url, {
...options,
headers: new Headers(fetchHeaders)
});
const apiDocumentationParser = entrypoint =>
parseHydraDocumentation(entrypoint, {
headers: new Headers(fetchHeaders)
}).then(
({ api }) => ({ api }),
result => {
switch (result.status) {
case 401:
return Promise.resolve({
api: result.api,
customRoutes: [
{
props: {
path: "/",
render: () => <Redirect to={`/login`} />
}
}
]
});
default:
return Promise.reject(result);
}
}
);
const dataProvider = baseDataProvider(
entrypoint,
fetchHydra,
apiDocumentationParser
);
export default () => (
<HydraAdmin
apiDocumentationParser={apiDocumentationParser}
dataProvider={dataProvider}
authProvider={authProvider}
entrypoint={entrypoint}
>
<ResourceGuesser name="resource" />
</HydraAdmin>
);
authProvider.js
import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_CHECK, AUTH_ERROR } from "react-admin";
export default (type, params) => {
if (type === AUTH_LOGIN) {
const { email, password } = params;
const request = new Request("http://localhost:8089/api/login_check", {
method: "POST",
body: JSON.stringify({ email, password }),
headers: new Headers({ "Content-Type": "application/json" })
});
return fetch(request)
.then(response => {
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
return response.json();
})
.then(({ token }) => {
localStorage.setItem("token", token);
});
}
if (type === AUTH_LOGOUT) {
localStorage.removeItem("token");
return Promise.resolve();
}
if (type === AUTH_ERROR) {
console.log("AUTH_ERROR");
//localStorage.removeItem("token");
return Promise.resolve();
}
if (type === AUTH_CHECK) {
return localStorage.getItem("token")
? Promise.resolve()
: Promise.reject({ redirectTo: "/login" });
}
return Promise.resolve();
};
I don't know if it's the right solution because like you, I didn't find anything about this problem.
But for me, if I just call window.location.reload(); after localStorage.setItem('token', token); it solves the problem for me, because after login, it reloads admin and at that moment, it can recognize token. Maybe it's not the cleanest solution ever but it works well.
By the way, I think, this is not a problem related with the HydraAdmin component, I tried the classic React Admin component and the problem is still there, so it's related to React Admin.
I'm looking to implement a real world authentication for a React Router app. Every tutorial I've seen uses fakeAuth to simulate authentication, but doesn't actually implement real world authentication. I'm trying to actually implement authentication. Is this possible?
Right now I'm sending a jwt to the back end to check whether it is valid before returning the Component I want to render -- Redirect to Login if jwt authentication fails, or render Dashboard if it's a valid jwt. The problem is the ProtectedRoute is returning the redirect to /login before the back end is returning whether the jwt is valid or not.
How can I get real world authentication in my React-Router app? Is this even possible?
const PrivateRoute = ({ component: Component, ...rest }) => {
const [auth, setAuth] = useState(false);
useEffect(() => {}, [auth])
useEffect(() => {
// send jwt to API to see if it's valid
let token = localStorage.getItem("token");
if (token) {
fetch("/protected", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ token })
})
.then((res) => {
return res.json()
})
.then((json) => {
if (json.success) {
setAuth(true);
}
})
.catch((err) => {
setAuth(false);
localStorage.removeItem("token");
});
}
}, [])
return (<Route {...rest}
render={(props) => {
return auth ? <Component {...props} /> : <Redirect to="/login" />
}} />)
}
}
I would say you need a state between authenticated/not valid jwt. I would use another state field, isTokenValidated (or isLoading):
const PrivateRoute = ({ component: Component, ...rest }) => {
const [auth, setAuth] = useState(false);
const [isTokenValidated, setIsTokenValidated] = useState(false);
useEffect(() => {
// send jwt to API to see if it's valid
let token = localStorage.getItem("token");
if (token) {
fetch("/protected", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ token })
})
.then((res) => {
return res.json()
})
.then((json) => {
if (json.success) {
setAuth(true);
}
})
.catch((err) => {
setAuth(false);
localStorage.removeItem("token");
})
.then(() => setIsTokenValidated(true));
} else {
setIsTokenValidated(true); // in case there is no token
}
}, [])
if (!isTokenValidated) return <div />; // or some kind of loading animation
return (<Route {...rest}
render={(props) => {
return auth ? <Component {...props} /> : <Redirect to="/login" />
}} />)
}
}
As an alternative way, your backend server.js file can check if jwt is valid or not and send a status code accordingly. Then your frontend react component can check this status code and render (or not render) page accordingly.
For component:
import {useState, useEffect} from "react";
const Private = () => {
const [statusAuth, setStatusAuth] = useState(false);
useEffect(() => {
fetch("/privateroute")
.then((res) => {
if (res.status === 200) {
setStatusAuth(true)
} else {
setStatusAuth(false)
}
});
}, []);
return(
{statusAuth && <div>rendered page</div>}
{!statusAuth && <div>You need to login. <Link to="/login"><span>Click for login page</span></Link></div>}
)
}
For server.js file:
app.get("/privateroute", function(req, res){
const token = req.cookies.jwt;
if (token) {
jwt.verify(token, "signature-of-your-jwt-token", () => {
res.status(200).end();
})
} else {
res.status(404)
res.send("Tokens didnt match");
}
});