Auth0 client is null in Vue SPA on page refresh - javascript

I have a Vue SPA based on one of Auth0's quickstart apps (https://github.com/auth0-samples/auth0-vue-samples). Everything works fine out of the box, but as soon as I try using the Auth0 client in my component code I run into problems. I followed the "Calling an API" tutorial (https://auth0.com/docs/quickstart/spa/vuejs/02-calling-an-api), which unhelpfully only shows how to call an API using a button. What I want to do is trigger an authenticated call to my API on initial page load so that I can ensure certain data exists in my own API (or create it if it does not). This seems like it should be pretty straightforward. I just throw this code in my created hook of my Vue component:
await this.$auth.getTokenSilently().then((authToken) => {
// reach out to my API using authToken
});
This actually works fine if the app hot reloads from my npm dev server, it reaches out to my API, which authorizes the request using the token, and sends back the correct data. The problem is when I manually reload the page, which causes this:
Uncaught (in promise) TypeError: Cannot read property 'getTokenSilently' of null
at Vue.getTokenSilently (authWrapper.js?de49:65)
at _callee$ (App.vue?234e:49)
Inside the authWrapper.js file (where the Auth0 client lives), the function call is here:
getTokenSilently(o) {
return this.auth0Client.getTokenSilently(o);
}
When I debug the call, "auth0Client" doesn't exist, which is why it's failing. What I can't understand is the correct way to ensure it does exist before I attempt to make the call. There's nothing in the samples that indicates the right way to do this. I tried putting my component code in different components and different Vue lifecycle hooks (created, beforeMount, mounted, etc), all with the same result. The client becomes available after 800 ms or so, but not when this code executes.
This is clearly a timing problem, but it's not clear to me how to tell my component code to sit and wait until this.auth0Client is non-null without doing something horrible and hacky like a setInterval.

I figured out a workaround for now, which I'll add as an answer in case anyone else has this issue, although it's not really the answer I want. Per the authGuard, you can use the exported "instance" from the authWrapper and watch its "loading" flag before executing your code that depends on the auth0Client being ready, like this:
import { getInstance } from "./auth/authWrapper";
// ... Vue component:
created() {
this.init(this.doSomethingWithAuth0Client);
},
methods: {
init(fn) {
// have to do this nonsense to make sure auth0Client is ready
var instance = getInstance();
instance.$watch("loading", loading => {
if (loading === false) {
fn(instance);
}
});
},
async doSomethingWithAuth0Client(instance) {
await instance.getTokenSilently().then((authToken) => {
// do authorized API calls with auth0 authToken here
});
}
}
It's hardly ideal, but it does work.

Related

Why actions in SveltKit give "Error: Cannot prerender pages with actions"?

I have a SvelteKit application, just following the example from the docs https://learn.svelte.dev/tutorial/named-form-actions, the problem is that everything works until I try to write an action:
at: +page.server.js
export const actions = {
default: async () => {
console.log('test')
}
};
vite immediately fails with:
"Cannot prerender pages with actions"
Error: Cannot prerender pages with actions
at render_page (file:///mydir/node_modules/#sveltejs/kit/src/runtime/server/page/index.js:87:11)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async resolve (file:///mydir/node_modules/#sveltejs/kit/src/runtime/server/index.js:356:17)
at async respond (file:///mydir/node_modules/#sveltejs/kit/src/runtime/server/index.js:229:20)
at async file:///mydir/node_modules/#sveltejs/kit/src/exports/vite/dev/index.js:444:22
Probably I'm missing some configuration or forgot some basics, any idea?
The docs simply state:
Pages with actions cannot be prerendered, because a server must be able to handle the action POST requests.
The assumption is probably that a form action should have an effect on the page when submitted, which would not be possible when a static HTML page is served every time.
You could try to separate any logic to an API endpoint that is not associated with your prerendered page. It depends on what you are trying to do here, maybe the page should simply not be prerendered at all.
This means that +page.ts should set:
export const prerender = false;

Service Worker determine number of clients

We've got a SaaS app that's PWA capable and we're using Workbox for the grunt work. Up to now we've been following the tried and trusted recipe of displaying an update is available banner to the users, prompting them to update their web app.
Viewing usage data (via Sentry.io) we've noticed that most users simply seem to ignore the update banner and continue with the version they're on.
So we're looking at trying something different. This is to perform the update automatically when they change the route in the web app (when we know there's an update available).
Testing this shows that it does work. However there's a side-effect, and that is if they've got web app open in multiple tabs, then all the tabs get updated. This could be problematic for users' if they've got an un-saved form open in one of the tabs in the background - they'll potentially loose their work.
This happens during this piece of code:
// app shell page, created lifecycle hook
document.addEventListener('swUpdated', this.SetPwaRegistration, { once: true })
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (this.refreshing) {
return
}
this.refreshing = true
window.location.reload()
})
// app shell page, method in methods collection
SetPwaRegistration (event) {
// call mutation to pass the registration object to Vuex
this.PWA_REGISTRATION_SET({ pwaRegistration: event.detail })
}
// main.js
router.afterEach((to, from) => {
// retrieve the registration object from Vuex
const pwaRegistration = app.$store.getters.pwaRegistration
if (pwaRegistration) {
pwaRegistration.waiting.postMessage('skipWaiting')
}
})
the above code is from our Vue.js app code-base, the this.refreshing is set to false by default in the data property collection.
What I'd like to know if whether it is possible to determine if the Service Worker has only one client under it's control (i.e. the web app is only open in 1 browser tab), and if this is the case, the auto-update can happen without potential issues. If there's more than one client, then we'll display the update banner as usual.
As a brief update to this, I've come across code examples similar to this:
self.clients.matchAll().then(clients => {
const clientCount = clients.length
// store count of client in Vuex
})
Which looks like an option (i.e. count how many clients there are, store this in Vuex store), I'm just not sure where it should be used.
If you want to orchestrate the reload from entirely within the service worker, you can effectively do that by using the WindowClient interface to programmatically navigate to whatever the current URL is, which is roughly equivalent to a reload.
The two things to keep in in mind that navigate() will only work on WindowClients (not Workers) and that you can only call it if the service worker controls the WindowClient.
Putting that together, here's something to try:
// This can be run, e.g., in a `message` handler in your SW:
self.clients.matchAll({
// These options are actually the defaults, but just
// to be explicit:
includeUncontrolled: false,
type: 'window',
}).then((clients) => {
if (clients.length === 1) {
clients[0].navigate(clients[0].url);
}
})

Authentication with Firebase and context API in react give me a warning. Is this the right approach?

I am trying to set up an authentication on my web application in React using firebase and Context API.
I am using Context API since as long as I understood I cannot save my jwt token in local storage in order to not be vulnerable to XSS attack and at the moment I do not want to use Redux.
in my App.js I have:
const {setUserInfo} = useContext(userInfoContext);
useEffect(() => {
auth.onAuthStateChanged(user => {
if (user) {
setUserInfo({jwtToken: user.za});
} else {
setUserInfo({jwtToken: null});
}
console.log(user);
});
}, [setUserInfo]);
The methos "auth.onAuthStateChanged" is triggered every time I logged in or I logged out using firebase.auth.
The compiler tell me that to eliminate the warning I should have "[setUserInfo]" instead of "[]". However, doing as he say, the method setUserInfo is executed twice. There is a better way to achieve the result without a warning?
Your problem is that you don't clean up your effect when it is recomputed. As soon as you add setUserInfo to the dependency array, the effect is executed whenever its value changes. This means that you could potentially register many auth.onAuthStateChanged if the value of setUserInfo changes.
auth.onAuthStateChanged returns an unsubscribe function. You can simply return this function inside your effect, which will make react execute the unsubscribe function whenever the hook is executed again and prevent you from having multiple active listeners. I suggest you read more about this topic here.

Having trouble with persisting React state using localStorage with a SSR (Next.js) app

The app I'm making has customizable settings. I'd like to load default settings, then when a user makes any changes, the custom settings will be stored in localStorage. Now the next time a user comes back, we'll load the setting from their localStorage.
I'm using React context like so...
const SettingsContextProvider = (props: any) => {
const [settings, setSettings] = useState(getSettings());
useEffect(() => {
localStorage.setItem('settings', JSON.stringify(settings))
}, [settings]);
return (...some jsx...);
}
Then getSettings()...
getSettings() {
// get from local storage
if (process.browser) {
const localSettings = localStorage.getItem('settings');
if (localSettings) return JSON.parse(localSettings );
}
// fall back to default settings
return "Something else";
}
The issue I'm having is that the server side load (on the node side), we don't have local storage (which is why I check for process.browser), so it falls back to default settings. THEN when it gets to the browser, it seems to call getSettings() again, in which case we DO have local storage and it loads from there.
That works, but then I get an error:
Text content did not match. Server: "Something else" Client: "Something custom"
So I understand that the server isn't matching the client and it's upset about it. I get WHY it's happening, but I have NO IDEA how to fix it. Maybe I need to implement reducers or use cookies?
Has anyone come across this?
I'm more than happy to share my full repo code if it'll help.
I'm fairly comfortable with react but have pretty much NEVER used react hooks in my own code.
However, I feel like I wouldn't put the updating of the localStorage into a useEffect function like that (and I could be totally wrong about that).
So, first, I would check that that useEffect function is being called when you're expecting it to be.
And then, I would write a function
const updateSettings = (newSettings) => {
localStorage.setItem('settings', JSON.stringify(newSettings))
setSettings(newSettings);
}
And use that updateSettings function to pass down to components.
BUT, I could be totally off there, like I said I don't use react hooks and only have a theoretical understanding.

JHIPSTER - How can I show a different HOME page in JHipster for each ROLE?

I'm making a JHipster project and I need to show a different home page for each role that I log in with, I'm using Angular 1.x.
For example I have the ROLE_ADMINand the ROLE_USERand I need to show a different dashboard for each on.
I have read that I can put something like this in the home.controller.js
this.eventManager.subscribe('authenticationSuccess', (message) => {
this.principal.identity().then((account) => {
if (account.authorities.indexOf("ROLE_ADMIN") >=0)
{
this.router.navigate(['#/pages/prueba/prueba.html']);
}
else
{
this.account = account;
}
});
});
But I can't make it work, it shows this error: Error: this is undefined
Anyone have a clue about this?
You can have a look at auth.service.js. There is a method called authorize, which in turn calls authThen. These methods are invoked after the user is authenticated and normally redirects the user to the last state (normally the protected state that failed, since the user was not authenticated and therefore was redirected to the login). You may change the code here to redirect the user according to its authorities.
The same methods (authorize and authThen) are also called everytime before a state changes, because it is a "resolve" for each state (have a look at the app.state.js).
Another option would be to add an "onEnter" function to your state definition that redirects to the appropiate view.

Categories

Resources