I'm getting this error in a VueJS application: Uncaught (in promise) Error: Navigation cancelled from "/" to "/dashboard" with a new navigation. Its been documents in SO before
here and I've seen another article explaining it's due to the OAuth library I am using, making 2 calls thus causing a redirect.
I can't figure out how to suppress the error in my situation, the other answers use router.push(), i'm using a function in beforeEnter.
I've tried using: Try/Accept and catch (but not sure where to put them):
try {
next("/dashboard");
} catch (err) {
throw new Error(`Problem handling something: ${err}.`);
}
How do I suppress the error?
My Router:
export default new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "Landing",
component: Landing,
meta: {
title: "Landing",
},
beforeEnter: loginRedirectDashboard,
},
})
Router Guard (beforeEnter)
function loginRedirectDashboard(to, from, next) {
const authService = getInstance();
authService.$watch("loading", (loading) => {
if (loading === false) {
if (to.path === "/") {
next("/dashboard");
} else {
next();
}
}
});
}
Update:
instance() code
import Vue from "vue";
import createAuth0Client from "#auth0/auth0-spa-js";
/** Define a default action to perform after authentication */
const DEFAULT_REDIRECT_CALLBACK = () =>
window.history.replaceState({}, document.title, window.location.pathname);
let instance;
/** Returns the current instance of the SDK */
export const getInstance = () => instance;
/** Creates an instance of the Auth0 SDK. If one has already been created, it returns that instance */
export const useAuth0 = ({
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
redirectUri = window.location.origin,
...options
}) => {
if (instance) return instance;
// The 'instance' is simply a Vue object
instance = new Vue({
data() {
return {
loading: true,
isAuthenticated: false,
user: {},
auth0Client: null,
popupOpen: false,
error: null,
permission: null,
};
},
methods: {
/** Authenticates the user using a popup window */
async loginWithPopup(options, config) {
this.popupOpen = true;
try {
await this.auth0Client.loginWithPopup(options, config);
this.user = await this.getUserWithMeta(); //await this.auth0Client.getUser();
this.isAuthenticated = await this.auth0Client.isAuthenticated();
this.error = null;
this.permission = await this.permissionLevels();
} catch (e) {
this.error = e;
// eslint-disable-next-line
console.error(e);
} finally {
this.popupOpen = false;
}
this.user = await this.auth0Client.getUser();
this.isAuthenticated = true;
},
/** Handles the callback when logging in using a redirect */
async handleRedirectCallback() {
this.loading = true;
try {
await this.auth0Client.handleRedirectCallback();
this.user = await this.getUserWithMeta(); //await this.auth0Client.getUser();
this.isAuthenticated = true;
this.error = null;
this.permission = await this.permissionLevels();
} catch (e) {
this.error = e;
} finally {
this.loading = false;
}
},
/** Authenticates the user using the redirect method */
loginWithRedirect(o) {
return this.auth0Client.loginWithRedirect(o);
},
/** Returns all the claims present in the ID token */
getIdTokenClaims(o) {
return this.auth0Client.getIdTokenClaims(o);
},
/** Returns the access token. If the token is invalid or missing, a new one is retrieved */
getTokenSilently(o) {
return this.auth0Client.getTokenSilently(o);
},
/** Gets the access token using a popup window */
async getUserWithMeta() {
var userObject = await this.auth0Client.getUser();
if (userObject != null) {
var namespace = '********';
var extractUserMeta = userObject[namespace];
delete userObject[namespace];
userObject.userInfo = extractUserMeta;
return userObject;
}
},
async permissionLevels(o) {
if (this.isAuthenticated) {
const jwtToken = await this.auth0Client.getTokenSilently(o);
var base64Url = jwtToken.split(".")[1];
var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
var jsonPayload = decodeURIComponent(
atob(base64)
.split("")
.map(function(c) {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
})
.join("")
);
return JSON.parse(jsonPayload).permissions;
} else {
return false;
}
},
getTokenWithPopup(o) {
return this.auth0Client.getTokenWithPopup(o);
},
/** Logs the user out and removes their session on the authorization server */
logout(o) {
delete localStorage.authToken;
return this.auth0Client.logout(o);
},
},
/** Use this lifecycle method to instantiate the SDK client */
async created() {
// Create a new instance of the SDK client using members of the given options object
this.auth0Client = await createAuth0Client({
...options,
client_id: options.clientId,
redirect_uri: redirectUri,
});
try {
// If the user is returning to the app after authentication..
if (
window.location.search.includes("code=") &&
window.location.search.includes("state=")
) {
// handle the redirect and retrieve tokens
const { appState } = await this.auth0Client.handleRedirectCallback();
this.error = null;
// Notify subscribers that the redirect callback has happened, passing the appState
// (useful for retrieving any pre-authentication state)
onRedirectCallback(appState);
}
} catch (e) {
this.error = e;
} finally {
// Initialize our internal authentication state
this.isAuthenticated = await this.auth0Client.isAuthenticated();
this.user = await this.getUserWithMeta(); //await this.auth0Client.getUser();
this.loading = false;
this.permission = await this.permissionLevels();
}
},
});
return instance;
};
// Create a simple Vue plugin to expose the wrapper object throughout the application
export const Auth0Plugin = {
install(Vue, options) {
Vue.prototype.$auth = useAuth0(options);
},
};
Related
I've been generally following along with this code here: https://github.com/aaronksaunders/ionic-v6-firebase-tabs-auth
The issue I'm having is my auth state is not persisting when I refresh the page when using ionic serve and loading the app in the web browser.
code for pinia store:
import { defineStore } from "pinia";
import { User } from "firebase/auth";
import {
Profile,
getProfile,
setProfile,
} from "#/firebase/helpers/firestore/profileManager";
import { onSnapshot, Unsubscribe, doc } from "#firebase/firestore";
import { db } from "#/firebase/connectEmulators";
import { getAuth } from "#firebase/auth";
import {
onAuthStateChanged,
signInWithEmailAndPassword,
signOut,
createUserWithEmailAndPassword,
updateProfile as updateAuthProfile,
} from "firebase/auth";
import errorHandler from "#/helpers/errorHandler";
/**#see {#link Profile} */
export enum UserType {
DNE,
uploader,
checker,
host,
}
interface State {
user: User | null;
profile: Profile | null;
error: null | any;
unsub: Unsubscribe | null;
}
export const useUserStore = defineStore("user", {
state: (): State => {
return {
user: null,
profile: null,
error: null,
unsub: null,
};
},
getters: {
isLoggedIn: (state) => state.user !== null,
//DEV: do we need this to be a getter?
userError: (state) => {
if(state.error){
switch (state.error.code) {
case "auth/user-not-found":
return "Email or Password incorrect!";
case "auth/wrong-password":
return "Email or Password incorrect!";
default:
return state.error;
}
}
return null;
},
/**
* #see Profile
*/
getType: (state): UserType => {
if (state.user === null) return UserType.DNE;
if (!state.profile) return UserType.DNE;
if (state.profile.locations.length > 0) return UserType.host;
if (state.profile.queues.length > 0) return UserType.checker;
return UserType.uploader;
},
},
actions: {
initializeAuthListener() {
return new Promise((resolve) => {
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
console.log("AuthListener Initialized");
if (user) {
console.log("AuthListener: User exists!");
this.user = user;
getProfile(user.uid).then((profile) => {
if (profile) {
this.profile = profile;
this.initializeProfileListener();
} else {
this.profile = null;
if (this.unsub) this.unsub();
}
});
} else {
console.log("AuthListener: User does not exist!");
this.user = null;
}
resolve(true);
});
});
},
/**
*
* #param email email for login
* #param password password for login
*/
async signInEmailPassword(email: string, password: string) {
try {
const auth = getAuth();
const userCredential = await signInWithEmailAndPassword(
auth,
email,
password
);
this.user = userCredential.user ? userCredential.user : null;
this.error = null;
return true;
} catch (error: any) {
console.log(typeof error.code);
console.log(error.code);
this.user = null;
this.error = error;
return false;
}
},
async logoutUser() {
try {
const auth = getAuth();
await signOut(auth);
this.user = null;
this.profile = null;
this.error = null;
if (this.unsub) this.unsub();
return true;
} catch (error: any) {
this.error = error;
return false;
}
},
async createEmailPasswordAccount(
email: string,
password: string,
userName: string,
refSource: string
) {
try {
const auth = getAuth();
const userCredential = await createUserWithEmailAndPassword(
auth,
email,
password
);
//Add username to fireauth profile
//DEV: test for xss vulnerabilities
await updateAuthProfile(userCredential.user, { displayName: userName });
//create user profile data in firestore
let profile: Profile | undefined = new Profile(
userCredential.user.uid,
refSource
);
await setProfile(profile);
profile = await getProfile(userCredential.user.uid);
//set local store
this.user = userCredential.user ? userCredential.user : null;
this.profile = profile ? profile : null;
this.error = null;
//TODO: send email verification
return true;
} catch (error: any) {
this.user = null;
this.error = error;
return false;
}
},
initializeProfileListener() {
try {
if (!this.profile) errorHandler(Error("Profile not set in state!"));
else {
const uid = this.profile.uid;
const unsub: Unsubscribe = onSnapshot(
doc(db, "profiles", uid),
(snapshot) => {
const fbData = snapshot.data();
if (!fbData)
errorHandler(Error("Profile Listener snapshot.data() Null!"));
else {
const profile = new Profile(
snapshot.id,
fbData.data.referralSource
);
profile.data = fbData.data;
profile.settings = fbData.settings;
profile.locations = fbData.locations;
profile.queues = fbData.queues;
profile.checkers = fbData.checkers;
profile.uploadHistory = fbData.uploadHistory;
profile.hostLevel = fbData.hostLevel;
this.profile = profile;
}
},
(error) => {
errorHandler(error);
}
);
this.unsub = unsub;
}
} catch (error) {
errorHandler(error as Error);
}
},
},
});
main.ts where I initialize auth listener:
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { IonicVue } from "#ionic/vue";
/* Core CSS required for Ionic components to work properly */
import "#ionic/vue/css/core.css";
/* Basic CSS for apps built with Ionic */
import "#ionic/vue/css/normalize.css";
import "#ionic/vue/css/structure.css";
import "#ionic/vue/css/typography.css";
/* Optional CSS utils that can be commented out */
import "#ionic/vue/css/padding.css";
import "#ionic/vue/css/float-elements.css";
import "#ionic/vue/css/text-alignment.css";
import "#ionic/vue/css/text-transformation.css";
import "#ionic/vue/css/flex-utils.css";
import "#ionic/vue/css/display.css";
/* Theme variables */
import "./theme/variables.css";
/* PWA elements for using Capacitor plugins */
import { defineCustomElements } from "#ionic/pwa-elements/loader";
/* Pinia used for state management */
import { createPinia } from "pinia";
import { useUserStore } from "./store/userStore";
const pinia = createPinia();
const app = createApp(App)
.use(IonicVue, {
// TODO: remove for production
mode: process.env.VUE_APP_IONIC_MODE,
})
.use(pinia);
defineCustomElements(window);
//get the store
const store = useUserStore();
store.initializeAuthListener().then(() => {
app.use(router);
});
router.isReady().then(() => {
app.mount("#app");
});
I've tried refactoring main.ts to mount the app inside the callback for initialize auth listener and I've tried making my code exactly like the code in the main.ts of the above link. Neither solved the issue.
I also looked at the question here: https://stackoverflow.com/a/67774186/9230780
Most of the points in the answer shouldn't be related because I'm currently using the firebase emulators to test the app.
Even still, I've verified my api key is correct.
I can see that cookies are created in the browser when I launch the app, so I don't think its an issue with them being wiped.
Ideally I'd like to avoid implementing #capacitor/storage here because it shouldn't be necessary.
I do plan to implement this library to handle authentication for ios and android: https://github.com/baumblatt/capacitor-firebase-auth
but that shouldn't be pertinent to the web version of the app.
Edit:
Realized I was missing a piece of code pertinent to the question. Not sure how I didn't copy it over. Code added is the initialize Profile listener function.
I ended up doing a refactor of the pinia store and that solved the issue. I believe the issue may have been caused by how the auth listener called initializeProfileListener. I didn't have code in the auth listener to check if the profile listener was already initialized, so everytime the authstate changed or it would initialize a new profile listener without unsubbing the old one. I'm not absolutely certain that is what was causing the issue though.
Below is the new code that functions properly.
pinia store:
import { defineStore } from "pinia";
import { User } from "firebase/auth";
import {
Profile,
getProfile,
profileListener,
} from "#/firebase/helpers/firestore/profileManager";
import {
fbCreateAccount,
fbSignIn,
fbAuthStateListener,
fbSignOut,
} from "#/firebase/helpers/firestore/authHelper";
import {Unsubscribe} from "#firebase/firestore";
import errorHandler from "#/helpers/errorHandler";
/**#see {#link Profile} */
export enum UserType {
DNE,
uploader,
checker,
host,
}
interface State {
user: User | null;
profile: Profile | null;
error: null | any;
unsub: Unsubscribe | null;
}
export const useUserStore = defineStore("user", {
state: (): State => {
return {
user: null,
profile: null,
error: null,
unsub: null,
};
},
getters: {
isLoggedIn: (state) => state.user !== null,
//DEV: do we need this to be a getter?
userError: (state) => {
if (state.error) {
switch (state.error.code) {
case "auth/user-not-found":
return "Email or Password incorrect!";
case "auth/wrong-password":
return "Email or Password incorrect!";
default:
return state.error;
}
}
return null;
},
/**
* #see Profile
*/
getType: (state): UserType => {
if (state.user === null) return UserType.DNE;
if (!state.profile) return UserType.DNE;
if (state.profile.locations.length > 0) return UserType.host;
if (state.profile.queues.length > 0) return UserType.checker;
return UserType.uploader;
},
},
actions: {
initializeAuthListener() {
return new Promise((resolve) => {
fbAuthStateListener(async (user: any) => {
if (user) {
this.user = user;
const profile = await getProfile(user.uid);
if (profile) {
this.profile = profile;
//TODO: initialize profile listener
if(this.unsub === null) {
this.initializeProfileListener();
}
}
}
resolve(true);
});
});
},
/**
*
* #param email email for login
* #param password password for login
*/
async signInEmailPassword(email: string, password: string) {
try {
const userCredential = await fbSignIn(email, password);
this.user = userCredential.user ? userCredential.user : null;
this.error = null;
return true;
} catch (error: any) {
console.log(typeof error.code);
console.log(error.code);
this.user = null;
this.error = error;
return false;
}
},
async logoutUser() {
try {
await fbSignOut();
this.user = null;
this.profile = null;
this.error = null;
if (this.unsub) this.unsub();
return true;
} catch (error: any) {
this.error = error;
return false;
}
},
async createEmailPasswordAccount(
email: string,
password: string,
userName: string,
refSource: string
) {
try {
const { user, profile } = await fbCreateAccount(
email,
password,
userName,
refSource
);
//set local store
this.user = user ? user : null;
this.profile = profile ? profile : null;
this.error = null;
//TODO: send email verification
return true;
} catch (error: any) {
this.user = null;
this.error = error;
return false;
}
},
initializeProfileListener() {
try {
if (this.user) {
const unsub = profileListener(
this.user?.uid,
async (profile: any) => {
if (profile) {
this.profile = profile;
}
}
);
this.unsub = unsub;
}
} catch (error) {
errorHandler(error as Error);
}
},
},
});
authHelper.ts
import { auth } from "#/firebase/firebase";
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
updateProfile as updateAuthProfile,
} from "#firebase/auth";
import { Profile, setProfile, getProfile } from "./profileManager";
/**
* #param email
* #param password
* #param userName
* #param refSource #see profileManager
* #returns
*/
export const fbCreateAccount = async (
email: string,
password: string,
userName: string,
refSource: string
) => {
//DEBUG: creating a user works but throws an error.
const userCredential = await createUserWithEmailAndPassword(
auth,
email,
password
);
if (userCredential) {
//add username to fireauth profile
await updateAuthProfile(userCredential.user, { displayName: userName });
//create user profile data in firestore
let profile: Profile | undefined = new Profile(
userCredential.user.uid,
refSource
);
await setProfile(profile);
profile = await getProfile(userCredential.user.uid);
//TODO: errorHandling for setProfile and getProfile
return {
user: userCredential.user,
profile: profile,
};
} else {
return {
user: null,
profile: null,
};
}
};
/**
*
* #param email
* #param password
* #returns UserCredential {#link https://firebase.google.com/docs/reference/js/auth.usercredential.md?authuser=0#usercredential_interface}
*/
export const fbSignIn = async (email: string, password: string) => {
const userCredential = signInWithEmailAndPassword(auth, email, password);
//TODO: add call to add to profile signins array
return userCredential;
};
export const fbSignOut = async () => {
await signOut(auth);
return true;
};
/**
* #see {#link https://firebase.google.com/docs/reference/js/auth.md?authuser=0&hl=en#onauthstatechanged}
* #param callback contains either user or null
*/
export const fbAuthStateListener = (callback: any) => {
onAuthStateChanged(auth, (user) => {
if (user) {
//user is signed in
callback(user);
} else {
//user is signed out
callback(null);
}
});
};
When I open any link in my app in new tab I get on auth page. Other words current user session reset. I use JWT auth, my front-end developed on Vue.js. I've saved token in my cookies like this:
After hit any link in new tab (middle click or ctrl+click) I still have token in my cookie but I get on page auth:
How can I fix it? I need save current session during click link in new tab.
Why does it happen if my cookies have user token?
My auth mutations looks like:
export const mTypes = {
UPDATE_TOKEN: 'UPDATE_TOKEN',
CLEAR_TOKEN: 'CLEAR_TOKEN',
UPDATE_USER_INFO: 'UPDATE_USER_INFO',
UPDATE_USER_STATUS: 'UPDATE_USER_STATUS',
};
export const mutations = {
[mTypes.CLEAR_TOKEN](state) {
state.username = null;
toStorage(savingValues.token, 'remove');
},
[mTypes.UPDATE_TOKEN](state, token) {
state.token = token.exp;
toStorage(savingValues.token, token.exp);
},
[mTypes.UPDATE_USER_INFO](state, userInfo) {
state.email = userInfo.email;
state.id = userInfo.user_id;
toStorage(savingValues.id, userInfo.user_id);
state.username = userInfo.username;
toStorage(savingValues.username, userInfo.username);
toStorage(savingValues.token, userInfo.exp);
},
[mTypes.UPDATE_USER_STATUS](state, user) {
state.isTeacher = user.type !== 's';
toStorage(savingValues.isTeacher, state.isTeacher);
state.isStaff = user.type === 'e';
toStorage(savingValues.isStaff, state.isStaff);
state.firstName = user.first_name;
toStorage(savingValues.firstName, user.first_name);
state.lastName = user.last_name;
toStorage(savingValues.lastName, user.last_name);
},
};
My actions auth looks like:
export const aTypes = {
login: 'auth/login',
logout: 'auth/logout',
refresh: 'auth/refresh',
updateUserStatus: 'auth/updateUserStatus',
inspectToken: 'auth/inspectToken',
};
export const actions = {
async login(cntx, {username, pswd, vm}) {
/**
* Logs in user: creates new token and saves personal user information
* #param {string} username
* #param {string} pswd
* #param {Object} vm Vue instance
*/
try {
dispatch(cntx, appActions.startLoading);
let data = {};
await api.login(username, pswd)
.then(response => {
data = response.data;
vm.$connect();
});
cntx.commit(mutations.UPDATE_USER_INFO, jwt_decode(data.token));
await dispatch(cntx, aTypes.updateUserStatus, vm);
if (fromStorage('isStaff') === true) {
await router.push('/Stat');
} else {
await router.push('/Dashboard');
}
} catch (e) {
callErrorNotify(vm, vm.$t('loginError'), e, 5);
} finally {
dispatch(cntx, appActions.stopLoading);
}
},
async updateUserStatus(cntx, {vm}) {
/**
* Gets and saves personal user information
* #param {Object} vm Vue instance
*/
try {
dispatch(cntx, appActions.startLoading);
const {data} = await api.getFullUserInfo(cntx.getters.id);
cntx.commit(mutations.UPDATE_USER_STATUS, data);
if (localStorage.getItem(`tour_${data.id}_${data.username}`) === null) {
initialTourSettings(data);
}
if (data.type === 't') {
await dispatch(cntx, wsActions.createTeacherChannel);
}
} catch (e) {
callErrorNotify(vm, vm.$t('loadError'), e, 5);
} finally {
dispatch(cntx, appActions.stopLoading);
}
},
async logout(cntx, {vm}) {
/**
* Logs out user: delete its token from store and session storage.
* #param {Object} vm Vue instance
*/
try {
dispatch(cntx, appActions.startLoading);
cntx.commit(mutations.CLEAR_TOKEN);
if (cntx.state.isTeacher) {
await dispatch(cntx, wsActions.closeTeacherChannel);
}
sessionStorage.clear();
await router.push('/Auth').catch(err => {
if (err.name !== 'NavigationDuplicated') {
console.log('Navigation duplicated: /Auth');
}});
} catch (e) {
callErrorNotify(vm, vm.$t('logoutError'), e, 5);
} finally {
dispatch(cntx, appActions.stopLoading);
}
},
async refresh(cntx, {token, vm}) {
/**
* Refreshes user token.
* #param {string} token
* #param {Object} vm Vue instance
*/
try {
dispatch(cntx, appActions.startLoading);
const {data} = await api.refresh(token);
cntx.commit(mutations.UPDATE_TOKEN, jwt_decode(data.token));
console.log('token was updated');
return true;
} catch (e) {
callErrorNotify(vm, vm.$t('tokenUpdateError'), e, 5);
return false;
} finally {
dispatch(cntx, appActions.stopLoading);
}
},
async inspectToken(cntx, {vm}) {
/**
* Checks whether it needed to update token every 30 minutes.
* #param {Object} vm Vue instance
*/
try {
let token = fromStorage(savingValues.token);
if (!!token) {
// 60s*50 = 3000s == 50 minutes
const delta = new Date(token * 1000) - new Date();
if (delta < 0) {
await dispatch(cntx, aTypes.logout, {vm});
return;
}
if (delta / 1000 <= 3000) {
await dispatch(cntx, aTypes.refresh, {token, vm});
}
} else {
await dispatch(cntx, aTypes.logout, {vm});
}
} catch (e) {
console.log('error during inspectToken. ' + e);
await dispatch(cntx, aTypes.logout, {vm});
}
},
};
And finally my component looks like this:
#Component
export default class App extends Vue {
valid = false;
username = '';
pswd = '';
get pswdRules() {
return [
(v) => !!v || this.$t('passwordRequired'),
]
}
get usernameRules() {
return [
(v) => !!v || this.$t('loginRequired'),
]
}
get isDarkTheme() {
return this.$store.getters[appGetters.isDarkTheme];
}
get primary() {
return colors.primary;
}
get neonBlue() {
return colors.neonBlue;
}
get language() {
return i18n.locale;
}
#Watch('language')
onLanguageChanged(val) {
this.$refs.form.resetValidation();
}
login() {
if (this.$refs.form.validate()) {
this.$store.dispatch(
authActions.login,
{
username: this.username,
pswd: this.pswd,
vm: this
}
)
}
}
mounted() {
this.$disconnect();
}
}
I have a test to test my cloudflare worker that looks like this:
const workerScript = fs.readFileSync(
path.resolve(__dirname, '../pkg-prd/worker.js'),
'utf8'
);
describe('worker unit test', function () {
// this.timeout(60000);
let worker;
beforeEach(() => {
worker = new Cloudworker(workerScript, {
bindings: {
HTMLRewriter
},
});
});
it('tests requests and responses', async () => {
const request = new Cloudworker.Request('https://www.example.com/pathname')
const response = await worker.dispatch(request);
console.log(response);
// const body = await response.json();
expect(response.status).to.eql(200);
// expect(body).to.eql({message: 'Hello mocha!'});
});
});
In my worker I do something like this:
const response = await fetch(BASE_URL, request);
const modifiedResponse = new Response(response.body, response);
// Remove the webflow badge
class ElementHandler {
element(element) {
element.append('<style type="text/css">body .w-webflow-badge {display: none!important}</style>', {html: true})
}
}
console.log(3);
return new HTMLRewriter()
.on('head', new ElementHandler()).transform(modifiedResponse);
Now when i run my test I get this error message:
● worker unit test › tests requests and responses
TypeError: Cannot read property 'transform' of undefined
at evalmachine.<anonymous>:1:1364
at FetchEvent.respondWith (node_modules/#dollarshaveclub/cloudworker/lib/cloudworker.js:39:17)
What seems to be wrong?
HTMLRewriter i created looks like this:
function HTMLRewriter() {
const elementHandler = {};
const on = (selector, handler) => {
if (handler && handler.element) {
if (!elementHandler[selector]) {
elementHandler[selector] = [];
}
elementHandler[selector].push(handler.element.bind(handler));
}
};
const transform = async response => {
const tempResponse = response.clone();
const doc = HTMLParser.parse(await tempResponse.text());
Object.keys(elementHandler).forEach(selector => {
const el = doc.querySelector(selector);
if (el) {
elementHandler[selector].map(callback => {
callback(new _Element(el));
});
}
});
return new Response(doc.toString(), response);
};
return {
on,
transform
};
}
Since HTMLRewriter() is called with new, the function needs to be a constructor. In JavaScript, a constructor function should set properties on this and should not return a value. But, your function is written to return a value.
So, try changing this:
return {
on,
transform
};
To this:
this.on = on;
this.transform = transform;
So we're creating a React-Native app using Apollo and GraphQL. I'm using JWT based authentication(when user logs in both an activeToken and refreshToken is created), and want to implement a flow where the token gets refreshed automatically when the server notices it's been expired.
The Apollo Docs for Apollo-Link-Error provides a good starting point to catch the error from the ApolloClient:
onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
switch (err.extensions.code) {
case 'UNAUTHENTICATED':
// error code is set to UNAUTHENTICATED
// when AuthenticationError thrown in resolver
// modify the operation context with a new token
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: getNewToken(),
},
});
// retry the request, returning the new observable
return forward(operation);
}
}
}
})
However, I am really struggling to figure out how to implement getNewToken().
My GraphQL endpoint has the resolver to create new tokens, but I can't call it from Apollo-Link-Error right?
So how do you refresh the token if the Token is created in the GraphQL endpoint that your Apollo Client will connect to?
The example given in the the Apollo Error Link documentation is a good starting point but assumes that the getNewToken() operation is synchronous.
In your case, you have to hit your GraphQL endpoint to retrieve a new access token. This is an asynchronous operation and you have to use the fromPromise utility function from the apollo-link package to transform your Promise to an Observable.
import React from "react";
import { AppRegistry } from 'react-native';
import { onError } from "apollo-link-error";
import { fromPromise, ApolloLink } from "apollo-link";
import { ApolloClient } from "apollo-client";
let apolloClient;
const getNewToken = () => {
return apolloClient.query({ query: GET_TOKEN_QUERY }).then((response) => {
// extract your accessToken from your response data and return it
const { accessToken } = response.data;
return accessToken;
});
};
const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
switch (err.extensions.code) {
case "UNAUTHENTICATED":
return fromPromise(
getNewToken().catch((error) => {
// Handle token refresh errors e.g clear stored tokens, redirect to login
return;
})
)
.filter((value) => Boolean(value))
.flatMap((accessToken) => {
const oldHeaders = operation.getContext().headers;
// modify the operation context with a new token
operation.setContext({
headers: {
...oldHeaders,
authorization: `Bearer ${accessToken}`,
},
});
// retry the request, returning the new observable
return forward(operation);
});
}
}
}
}
);
apolloClient = new ApolloClient({
link: ApolloLink.from([errorLink, authLink, httpLink]),
});
const App = () => (
<ApolloProvider client={apolloClient}>
<MyRootComponent />
</ApolloProvider>
);
AppRegistry.registerComponent('MyApplication', () => App);
You can stop at the above implementation which worked correctly until two or more requests failed concurrently. So, to handle concurrent requests failure on token expiration, have a look at this post.
Update - Jan 2022
you can see basic React JWT Authentication Setup from: https://github.com/bilguun-zorigt/React-GraphQL-JWT-Authentication-Example
I've also added the safety points to consider when setting up authentication on both the frontend and backend on the Readme section of the repository. (XSS attack, csrf attack etc...)
Original answer - Dec 2021
My solution:
Works with concurrent requests (by using single promise for all requests)
Doesn't wait for error to happen
Used second client for refresh mutation
import { setContext } from '#apollo/client/link/context';
async function getRefreshedAccessTokenPromise() {
try {
const { data } = await apolloClientAuth.mutate({ mutation: REFRESH })
// maybe dispatch result to redux or something
return data.refreshToken.token
} catch (error) {
// logout, show alert or something
return error
}
}
let pendingAccessTokenPromise = null
export function getAccessTokenPromise() {
const authTokenState = reduxStoreMain.getState().authToken
const currentNumericDate = Math.round(Date.now() / 1000)
if (authTokenState && authTokenState.token && authTokenState.payload &&
currentNumericDate + 1 * 60 <= authTokenState.payload.exp) {
//if (currentNumericDate + 3 * 60 >= authTokenState.payload.exp) getRefreshedAccessTokenPromise()
return new Promise(resolve => resolve(authTokenState.token))
}
if (!pendingAccessTokenPromise) pendingAccessTokenPromise = getRefreshedAccessTokenPromise().finally(() => pendingAccessTokenPromise = null)
return pendingAccessTokenPromise
}
export const linkTokenHeader = setContext(async (_, { headers }) => {
const accessToken = await getAccessTokenPromise()
return {
headers: {
...headers,
Authorization: accessToken ? `JWT ${accessToken}` : '',
}
}
})
export const apolloClientMain = new ApolloClient({
link: ApolloLink.from([
linkError,
linkTokenHeader,
linkMain
]),
cache: inMemoryCache
});
If you are using JWT, you should be able to detect when your JWT token is about to expire or if it is already expired.
Therefore, you do not need to make a request that will always fail with 401 unauthorized.
You can simplify the implementation this way:
const REFRESH_TOKEN_LEGROOM = 5 * 60
export function getTokenState(token?: string | null) {
if (!token) {
return { valid: false, needRefresh: true }
}
const decoded = decode(token)
if (!decoded) {
return { valid: false, needRefresh: true }
} else if (decoded.exp && (timestamp() + REFRESH_TOKEN_LEGROOM) > decoded.exp) {
return { valid: true, needRefresh: true }
} else {
return { valid: true, needRefresh: false }
}
}
export let apolloClient : ApolloClient<NormalizedCacheObject>
const refreshAuthToken = async () => {
return apolloClient.mutate({
mutation: gql```
query refreshAuthToken {
refreshAuthToken {
value
}```,
}).then((res) => {
const newAccessToken = res.data?.refreshAuthToken?.value
localStorage.setString('accessToken', newAccessToken);
return newAccessToken
})
}
const apolloHttpLink = createHttpLink({
uri: Config.graphqlUrl
})
const apolloAuthLink = setContext(async (request, { headers }) => {
// set token as refreshToken for refreshing token request
if (request.operationName === 'refreshAuthToken') {
let refreshToken = localStorage.getString("refreshToken")
if (refreshToken) {
return {
headers: {
...headers,
authorization: `Bearer ${refreshToken}`,
}
}
} else {
return { headers }
}
}
let token = localStorage.getString("accessToken")
const tokenState = getTokenState(token)
if (token && tokenState.needRefresh) {
const refreshPromise = refreshAuthToken()
if (tokenState.valid === false) {
token = await refreshPromise
}
}
if (token) {
return {
headers: {
...headers,
authorization: `Bearer ${token}`,
}
}
} else {
return { headers }
}
})
apolloClient = new ApolloClient({
link: apolloAuthLink.concat(apolloHttpLink),
cache: new InMemoryCache()
})
The advantage of this implementation:
If the access token is about to expire (REFRESH_TOKEN_LEGROOM), it will request a refresh token without stopping the current query. Which should be invisible to your user
If the access token is already expired, it will refresh the token and wait for the response to update it. Much faster than waiting for the error back
The disadvantage:
If you make many requests at once, it may request several times a refresh. You can easily protect against it by waiting a global promise for example. But you will have to implement a proper race condition check if you want to guaranty only one refresh.
after checking this topic and some others very good on internet, my code worked with the following solution
ApolloClient,
NormalizedCacheObject,
gql,
createHttpLink,
InMemoryCache,
} from '#apollo/client';
import { setContext } from '#apollo/client/link/context';
import jwt_decode, { JwtPayload } from 'jwt-decode';
import {
getStorageData,
setStorageData,
STORAGE_CONTANTS,
} from '../utils/local';
export function isRefreshNeeded(token?: string | null) {
if (!token) {
return { valid: false, needRefresh: true };
}
const decoded = jwt_decode<JwtPayload>(token);
if (!decoded) {
return { valid: false, needRefresh: true };
}
if (decoded.exp && Date.now() >= decoded.exp * 1000) {
return { valid: false, needRefresh: true };
}
return { valid: true, needRefresh: false };
}
export let client: ApolloClient<NormalizedCacheObject>;
const refreshAuthToken = async () => {
const refreshToken = getStorageData(STORAGE_CONTANTS.REFRESHTOKEN);
const newToken = await client
.mutate({
mutation: gql`
mutation RefreshToken($refreshAccessTokenRefreshToken: String!) {
refreshAccessToken(refreshToken: $refreshAccessTokenRefreshToken) {
accessToken
status
}
}
`,
variables: { refreshAccessTokenRefreshToken: refreshToken },
})
.then(res => {
const newAccessToken = res.data?.refreshAccessToken?.accessToken;
setStorageData(STORAGE_CONTANTS.AUTHTOKEN, newAccessToken, true);
return newAccessToken;
});
return newToken;
};
const apolloHttpLink = createHttpLink({
uri: process.env.REACT_APP_API_URL,
});
const apolloAuthLink = setContext(async (request, { headers }) => {
if (request.operationName !== 'RefreshToken') {
let token = getStorageData(STORAGE_CONTANTS.AUTHTOKEN);
const shouldRefresh = isRefreshNeeded(token);
if (token && shouldRefresh.needRefresh) {
const refreshPromise = await refreshAuthToken();
if (shouldRefresh.valid === false) {
token = await refreshPromise;
}
}
if (token) {
return {
headers: {
...headers,
authorization: `${token}`,
},
};
}
return { headers };
}
return { headers };
});
client = new ApolloClient({
link: apolloAuthLink.concat(apolloHttpLink),
cache: new InMemoryCache(),
});
A much simpler solution is using RetryLink. retryIf supports async operations so one could do something like this:
class GraphQLClient {
constructor() {
const httpLink = new HttpLink({ uri: '<graphql-endpoint>', fetch: fetch })
const authLink = setContext((_, { headers }) => this._getAuthHeaders(headers))
const retryLink = new RetryLink({
delay: { initial: 300, max: Infinity, jitter: false },
attempts: {
max: 3,
retryIf: (error, operation) => this._handleRetry(error, operation)
}})
this.client = new ApolloClient({
link: ApolloLink.from([ authLink, retryLink, httpLink ]),
cache: new InMemoryCache()
})
}
async _handleRetry(error, operation) {
let requiresRetry = false
if (error.statusCode === 401) {
requiresRetry = true
if (!this.refreshingToken) {
this.refreshingToken = true
await this.requestNewAccessToken()
operation.setContext(({ headers = {} }) => this._getAuthHeaders(headers))
this.refreshingToken = false
}
}
return requiresRetry
}
async requestNewAccessToken() {
// get new access token
}
_getAuthHeaders(headers) {
// return headers
}
}
I have a Nuxt app with many request to the same API, but also i need to make request to different providers apart of my main API and i don't know how to manage the default headers.
This is my working setup create a plugin to add the headers to all the request like this:
plugins/axios.js
export default function({ $axios, store, redirect }) {
$axios.onRequest(config => {
config.headers.common.Authorization = 'token 123';
config.headers.common["Custom-header"] = 'blablabla';
}
}
nuxt.config.js
module.exports = {
plugins: ["#/plugins/axios"],
axios: {
baseURL: process.env.API_URL,
}
}
store.js
async changeKeyVersionOnline({ commit }) {
const response = await this.$axios.get(
`users/1`
);
return response;
},
This works great for the main API but the problem is i need also to make request to other endpoints of third party service provider and of course the headers should be different.
How can i do that, i read about the proxy option of the nuxt-axios package but what i understand is this only changes the request base URL, i cant find how to set different headers to a specific request.
My final solution was based on create some actions in a central store so the axios requests are made trough this actions.
central.js (Where the axios related actions live)
import qs from "qs";
export const state = () => ({
accessToken: "",
clientId: 0
});
export const getters = {
getHeadersWithAuth: state => {
const config = {
headers: {
Authorization: "Bearer " + state.accessToken
}
};
return config;
},
getHeadersWithAuthClient: state => {
const config = {
headers: {
Authorization: "Bearer " + state.accessToken,
Client: state.clientId
}
};
return config;
}
};
export const mutations = {};
export const actions = {
async getWithAuth({ getters }, { path, params }) {
const config = getters.getHeadersWithAuth;
config.params = params;
config.paramsSerializer = function(params) {
return qs.stringify(params, { encode: false });
};
const result = await this.$axios.get(path, config);
return result;
},
async getWithAuthClient({ getters }, { path, params }) {
const config = getters.getHeadersWithAuthClient;
config.params = params;
config.paramsSerializer = function(params) {
return qs.stringify(params, { encode: false });
};
const result = await this.$axios.get(path, config);
return result;
},
async putWithAuthClient({ getters }, { path, body, params }) {
const config = getters.getHeadersWithAuthClient;
config.params = params;
config.paramsSerializer = function(params) {
return qs.stringify(params, { encode: false });
};
const result = await this.$axios.put(path, body, config);
return result;
}
};
test.js Other store which use the custom axios requests
async updateProductDetailsAction({ commit, dispatch, state }, productData) {
const request = {
path: `endpoints/` + productData.id + `/details`,
body: {
length: 123,
name: 'The product name'
},
params: {}
};
const result = await dispatch("auth/putWithAuthClient", request, {
root: true
});
await commit("setProductDetails", productData.id);
return result;
}