I am making a React Application with a GraphQL backend. My code is as follows:
App.js
import { UserContext } from "./UserContext"
function App() {
const userQuery = useQuery(USER)
...
return {
<UserContext.Provider value={userQuery.data}>
LoginForm.js
import { UserContext } from "./UserContext"
function LoginForm() {
...
useEffect(() => {
if (result.data) {
const token = result.data.login.value
localStorage.setItem("user-token", token)
navigate("/")
}
}, [result.data])
index.js
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem("mouldy-peppers-user-token")
return {
headers: {
...headers,
authorization: token ? `bearer ${token}` : null,
},
}
})
But when the LoginForm navigates back to "/", the app component re-renders and user is null. Until I refresh the page, then it gets the logged-in user. The authorisation data sent with every graphQL query uses the local storage token. I would like to know how to get the query to use the new auth data without having to refresh the page.
Related
I'm trying to implement Server Side Authentication with Supabase and Sveltekit. I followed the Quickstart Guide and was able to do authentication client-side. (preventDefault on the submit event and do client-side POST request).
But when trying to do the same thing Server-Side , the auth cookie with the token is not created. Here's the logic:
// src/routes/login/+page.server.ts
import type { PostgrestResponse } from '#supabase/supabase-js';
import { supabaseClient } from '$lib/supabaseClient';
import type { Database } from '$lib/types/database.types';
import type { PageLoad } from './$types';
import type { PageServerLoad, Actions } from './$types';
import { redirect } from '#sveltejs/kit';
export const actions: Actions = {
'login-with-password': async ({ request }) => {
const formData = await request.formData();
const email = formData.get('email');
const password = formData.get('password');
console.log(email, password);
const { data, error } = await supabaseClient.auth.signInWithPassword({ email, password });
console.log(data);
if (error) {
return {
status: 500,
body: {
error: error.message
}
};
}
throw redirect(302, '/');
return { success: true };
}
};
data seems to hold the correct response, with token and everything, but that's not persisted as a cookie.
https://stackblitz.com/~/github.com/gkatsanos/client
I have a simple React app with a "/login" page in it. After the user logs in, I set the token with localstorage.setItem() and then navigate the user to the main page "/". In there I have different components, and those componenst use different API calls with the token.
All my API calls are in a "services.js" file.
Problem: After log in, the api calls are getting fired, and localstorage.getItem(token) returning null (i guess it is asnyc and takes some time to set the data), 401 Unauthorized
How can I make my app to wait for the localstorage item? What's the best way to acccess my bearer token in the services.js file, where all my api calls lay?
Login component:
const [token, setToken] = useContext(UserContext);
const navigate = useNavigate();
const asyncLocalStorage = {
getItem: async function () {
await null;
return localStorage.getItem('esteticaToken');
}
};
const submitLogin = async () => {
await logIn({username:username, password:pw})
.then((response)=>{
if (response.status === 401) {
setErrorMessage("Hibás felhasználónév, vagy jelszó!")
return
}
setLoading(false)
setToken(response.token);
//I try to wait for the token, and only after navigate to the main page (not working)
if (asyncLocalStorage.getItem() !== null) {
navigate("/");
}
})
.catch(()=>setLoading(false))
};
services.js:
//Main page API calls
export async function getProjects(){
let config = {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + localStorage.getItem('esteticaToken') //this is null
}
}
const { data } = await axios.get(API_URL+'/projects',config)
return data;
}
export async function getHits(){
let config = {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + localStorage.getItem('esteticaToken')
}
}
const { data } = await axios.get(API_URL+'/hits',config)
return data;
}
My main component, where I call for the api's
export default function MainPage(){
//React Query firing the api call from services.js after log in
const { isFetching: loading, data:dataProjects } = useQuery('projects', getAllProjects)
return(
...rest of the app
)
I am using Remix, along with Remix-Auth and using the Twitch API/OAuth, which requires that I check in with their /validate endpoint every hour docs. I had someone recommend that I use a resource route and POST to that if the validation endpoint returned a status of 401, however, I need as I stated before the request needs to be sent every hour I figured maybe I could use something like React-Query to POST to the resource route every hour.
Just pointing out that I use createCookieSessionStorage with Remix Auth to create the session
Problem
I haven't been able to achieve the actual session being destroyed and a user being re-routed to the login page, I have left what actual code I have currently any help or suggestions to actually achieve the session being destroyed and be re-routed to the login page if the validation fails would be greatly appreciated.
// React Query client side, checks if the users token is still valid
const { error, data } = useQuery("TV-Revalidate", () =>
fetch("https://id.twitch.tv/oauth2/validate", {
headers: {
Authorization: `Bearer ${user?.token}`,
},
}).then((res) => res.json())
);
The above React Query returns this
// My attempt at the resource route
// ~routes/auth/destroy.server.ts
import { ActionFunction, redirect } from "#remix-run/node";
import { destroySession, getSession } from "~/services/session.server";
export const action: ActionFunction = async ({request}) => {
const session = await getSession(request.headers.get("cookie"))
return redirect("/login", {
headers: {
"Set-Cookie": await destroySession(session)
}
})
}
// Second attempt at resource route
// ~routes/auth/destroy.server.ts
import { ActionFunction, redirect } from "#remix-run/node";
import { destroySession, getSession } from "~/services/session.server";
export const action: ActionFunction = async ({request}) => {
const session = await getSession(request.headers.get("cookie"))
return destroySession(session)
}
I attempted using an if statement to POST to the resource route or else render the page, however, this definitely won't work as React errors out because functions aren't valid as a child and page is blank.
//index.tsx
export default function Index() {
const { user, bits, vali } = useLoaderData();
console.log("loader", vali);
const { error, data } = useQuery("TV-Revalidate", () =>
fetch("https://id.twitch.tv/oauth2/validate", {
headers: {
Authorization: `Bearer ${user?.token}`,
},
}).then((res) => res.json())
);
if (data?.status === 401)
return async () => {
await fetch("~/services/destroy.server", { method: "POST" });
};
else
return ( ... );}
You could use Remix' useFetcher hook.
https://remix.run/docs/en/v1/api/remix#usefetcher
// Resource route
// routes/api/validate
export const loader: LoaderFunction = async ({ request }) => {
const session = await getSession(request);
try {
const { data } = await fetch("https://id.twitch.tv/oauth2/validate", {
headers: {
Authorization: `Bearer ${session.get("token")}`
}
});
return json({
data
}, {
headers: {
"Set-Cookie": await commitSession(session),
}
});
} catch(error) {
return redirect("/login", {
headers: {
"Set-Cookie": await destroySession(session)
}
});
}
}
And then in your route component something like this:
const fetcher = useFetcher();
useEffect(() => {
if (fetcher.type === 'init') {
fetcher.load('/api/validate');
}
}, [fetcher]);
useEffect(() => {
if(fetcher.data?.someValue {
const timeout = setTimeout(() => fetcher.load('/api/validate'), 1 * 60 * 60 * 1000);
return () => clearTimeout(timeout);
}
},[fetcher.data]);
I'm using react-msal to my application. I need to acquire the access token and attach it to the axios globally, but unfortunately, they only provide hooks to get the access token (as far as I know).
So far, here's my api.js file.
import axios from "axios";
import { useMsal } from "#azure/msal-react";
const axiosInstance = axios.create({
baseURL: "https://localhost:4211/api",
});
const { instance, accounts } = useMsal();
instance
.acquireTokenSilent({
...loginApiRequest,
account: accounts[0],
})
.then((response) => {
axiosInstance.defaults.headers.common[
"Authorization"
] = `Bearer ${response.accessToken}`;
})
.catch((error) => {
console("Error acquiring access token");
});
export default axiosInstance;
And here's I call my API in my component.
api.get('/foods').then(response => {
alert(response.data)
}).catch(error => {
console.log(error.response)
})
But I'm getting an issue that says: Error: Invalid hook call. Hooks can only be called inside of the body of a function component. which is obvious but I need alternatives to get the access token and assign it to my axios globally as part of the header so I don't need to rewrite header each time I need to call an endpoints. Any help?
This is a React application, right?
You can't call hooks from outside of your React components, or other hooks.
https://reactjs.org/docs/hooks-rules.html
You could do something like this:
const App = () => {
const { instance, accounts } = useMsal();
useEffect(() => {
instance.acquireTokenSilent()
.then(() => {})
.catch(() => {})
},[]);
};
You can use PublicClientApplication instance passed into the MsalProvider.
To get the accounts call instance.getAllAccounts().
You can't access the inProgress value outside of a component or context, but since you're just using acquireTokenSilent you probably will not need it.
below is my working sample.
import axios from 'axios';
import * as App from '../index'
import * as utils from './utils'
const instance = axios.create({
baseURL: utils.getEndpoint(),
timeout: 15000
});
instance.interceptors.request.use(function (config) {
const instance = App.msalInstance;
const accounts = instance.getAllAccounts();
const accessTokenRequest = {
scopes: ["user.read"],
account: accounts[0],
};
return instance
.acquireTokenSilent(accessTokenRequest)
.then((accessTokenResponse) => {
// Acquire token silent success
let accessToken = accessTokenResponse.accessToken;
// Call your API with token
config.headers.Authorization = `Bearer ${accessToken}`;
return Promise.resolve(config)
})
}, function (error) {
return Promise.reject(error);
});
instance.interceptors.response.use((response) => {
if(response.status === 401) {
// Clear local storage, redirect back to login
window.location.href = "/logout"
}
return response;
}, (error) => {
return Promise.reject(error);
});
export default instance
and index.js below
import React from "react";
import ReactDOM from "react-dom";
import { PublicClientApplication, EventType } from "#azure/msal-browser";
import { msalConfig } from "./authConfig";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
export const msalInstance = new PublicClientApplication(msalConfig());
// Default to using the first account if no account is active on page load
if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) {
// Account selection logic is app dependent. Adjust as needed for different use cases.
msalInstance.setActiveAccount(msalInstance.getAllAccounts()[0]);
}
// Optional - This will update account state if a user signs in from another tab or window
msalInstance.enableAccountStorageEvents();
msalInstance.addEventCallback((event) => {
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload.account) {
const account = event.payload.account;
msalInstance.setActiveAccount(account);
}
});
ReactDOM.render(<App pca={msalInstance} />,
document.getElementById("app"),
);
serviceWorker.unregister();
I'm doing project with React , firebase auth social signin(google, github provider) and backend(spring boot)
I'm wondering how can i use useSWR for global state for google userData
Here's my Code This is Login page simply i coded
In this page, I fetch userData(email, nickname ,, etc) with header's idToken(received from firebase auth) and backend validates idToken and send me a response about userData
This is not problem I guess.. But
// import GithubLogin from '#src/components/GithubLogin';
import GoogleLogin from '#src/components/GoogleLogin';
import { auth, signOut } from '#src/service/firebase';
import { fetcherWithToken } from '#src/utils/fetcher';
import React, { useEffect, useState } from 'react';
import useSWR from 'swr';
const Login = () => {
const [token, setToken] = useState<string | undefined>('');
const { data: userData, error } = useSWR(['/api/user/me', token], fetcherWithToken);
useEffect(() => {
auth.onAuthStateChanged(async (firebaseUser) => {
const token = await firebaseUser?.getIdToken();
sessionStorage.setItem('user', token!);
setToken(token);
});
}, []);
return (
<div>
<button onClick={signOut}>Logout</button>
<h2>Login Page</h2>
<GoogleLogin />
</div>
);
};
export default Login;
Here's Code about fetcher using in useSWR parameter
export const fetcherWithToken = async (url: string, token: string) => {
await axios
.get(url, {
headers: {
Authorization: `Bearer ${token}`,
Content-Type: 'application/json',
},
withCredentials: true,
})
.then((res) => res.data)
.catch((err) => {
if (err) {
throw new Error('There is error on your site');
}
});
};
problem
I want to use userData from useSWR("/api/user/me", fetcherWithToken) in other page! (ex : Profile Page, header's Logout button visibility)
But for doing this, I have to pass idToken (Bearer ${token}) every single time i use useSWR for userData. const { data: userData, error } = useSWR(['/api/user/me', token], fetcherWithToken);
Like this.
What is the best way to use useSWR with header's token to use data in other pages too?
seriously, I'm considering using recoil, context api too.
but I don't want to.
You can make SWR calls reusable by wrapping them with a custom hook. See the SWR docs page below.
Make It Reusable
When building a web app, you might need to reuse the data in many
places of the UI. It is incredibly easy to create reusable data hooks
on top of SWR:
function useUser (id) {
const { data, error } = useSWR(`/api/user/${id}`, fetcher)
return {
user: data,
isLoading: !error && !data,
isError: error
}
}
And use it in your components:
function Avatar ({ id }) {
const { user, isLoading, isError } = useUser(id)
if (isLoading) return <Spinner />
if (isError) return <Error />
return <img src={user.avatar} />
}