Preventing unauthed ajax requests from redux actions - authentication

I have a component defined like this. fetchBrands is a redux action.
class Brands extends Component {
componentWillMount() {
this.props.fetchBrands();
}
render() {
return (
// jsx omitted for brevity
);
}
}
function mapStateToProps(state) {
return { brands: state.brands.brands }
}
export default connect(mapStateToProps, { fetchBrands: fetchBrands })(Brands);
This component is wrapped in a Higher Order Component that looks like this:
export default function(ComposedComponent) {
class Authentication extends Component {
// kind if like dependency injection
static contextTypes = {
router: React.PropTypes.object
}
componentWillMount() {
if (!this.props.authenticated) {
this.context.router.push('/');
}
}
componentWillUpdate(nextProps) {
if (!nextProps.authenticated) {
this.context.router.push('/');
}
}
render() {
return <ComposedComponent {...this.props} />
}
}
function mapStateToProps(state) {
return { authenticated: state.auth.authenticated };
}
return connect(mapStateToProps)(Authentication);
}
Then, in my router config, I am doing the following:
<Route path="brands" component={requireAuth(Brands)} />
If the auth token doesn't exist in local storage, I redirect to a public page. However, the fetchBrands action is still being called which is firing off an ajax request. The server is rejecting it because of the lack of an auth token, but I don't want the call to even be made.
export function fetchBrands() {
return function(dispatch) {
// ajax request here
}
}
I could wrap the ajax request with an if/else check, but that isn't very DRY considering all the action functions I'd need to do this in. How can I implement something DRY to prevent the calls if auth fails at the browser level?

You should write a middleware for that. http://redux.js.org/docs/advanced/Middleware.html
Since you are using axios I would really recommend using something like https://github.com/svrcekmichal/redux-axios-middleware
Then you can use a middleware like
const tokenMiddleware = store => next => action => {
if (action.payload && action.payload.request && action.payload.request.secure) {
const { auth: { isAuthenticated } } = store.getState()
const secure = action.payload.request.secure
if (!isAuthenticated && secure) {
history.push({ pathname: 'login', query: { next: history.getCurrentLocation().pathname } })
return Promise.reject(next({ type: 'LOGIN_ERROR', ...omit(action, 'payload') }))
}
}
return next(action)
}
action.payload.request.secure is a custom prop I use to indicate the request needs authentication.
I this case I also redirect using history from react-router but you can handle this to dispatch another action (store.dispatch({ whatever })) and react as you need

Related

Unable to fetch query parameter in NextJS

I have an API route /api/form
// ./pages/api/form.js
import Router from 'next/router';
...
export default async function handler(req, res) {
...
res.redirect(307, '/summary?username=username');
Router.push({
pathname: '/summary',
query: {
username: username
}
});
}
// ./pages/summary.js
import { useRouter } from 'next/router';
export default function Summary() {
const router = useRouter();
console.log(router.query); // undefined
}
I am not able to fetch the query param. Also, if change the order of Router.push and res.redirect, I still stay on the /api/form route
I also tried using useRouter().push as per the documentation. Still, I stay in the /api/form route.
How to get the query param?
next/router allows you to do client-side transitions: https://vercel.fyi/next-router-client-side
For your use case I suggest putting the router.push event inside your form submit event, something like this:
<form
onSubmit={() => {
fetch(`/api/form`, {
...
}).then((res) => {
if (res.status === 200) {
router.push({
pathname: '/summary',
query: {
username: username
}
})
}
)
}
}
>
...
</form>

Returning Apollo useQuery result from inside a function in Vue 3 composition api

I'm having some issues finding a clean way of returning results from inside a method to my template using Apollo v4 and Vue 3 composition API.
Here's my component:
export default {
components: {
AssetCreationForm,
MainLayout,
HeaderLinks,
LoadingButton,
DialogModal
},
setup() {
const showNewAssetModal = ref(false);
const onSubmitAsset = (asset) => {
// how do I access result outside the handler function
const { result } = useQuery(gql`
query getAssets {
assets {
id
name
symbol
slug
logo
}
}
`)
};
}
return {
showNewAssetModal,
onSubmitAsset,
}
},
}
The onSubmitAsset is called when user clicks on a button on the page.
How do I return useQuery result from the setup function to be able to access it in the template? (I don't want to copy the value)
You can move the useQuery() outside of the submit method, as shown in the docs. And if you'd like to defer the query fetching until the submit method is called, you can disable the auto-start by passing enabled:false as an option (3rd argument of useQuery):
export default {
setup() {
const fetchEnabled = ref(false)
const { result } = useQuery(gql`...`, null, { enabled: fetchEnabled })
const onSubmitAsset = (asset) => {
fetchEnabled.value = true
}
return { result, onSubmitAsset }
}
}
demo

NextJS consistently access request object for every page

I'm using express + passport + nextjs to set up an app that will perform authentication using OpenID Connect. The user data is stored on the request object using express-session which gives me req.user on every request as usual.
Now I want to pass the user information to the front-end so that I can use it for something, but there does not seem to be any consistent way to do this for all requests. I can use getServerSideProps for individual pages, but not for every page through either _document or _app. How can I set this up?
Here is my current _document.tsx
import Document, {
Head,
Main,
NextScript,
DocumentContext,
} from "next/document"
export default class Doc extends Document {
public static async getInitialProps(ctx: DocumentContext) {
const req: any = ctx.req
console.log("req/user", `${!!req}/${!!(req && req.user)}`)
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
user: req?.user || "no user",
}
}
public render() {
return (
<html>
<Head />
<body>
<Main />
<NextScript />
</body>
</html>
)
}
}
It appears to return a request object only during the very first request, not any subsequent refreshes of the page.
I've created a small repo that reproduces the issue here: https://github.com/rudfoss/next-server-custom-req
It seems ridiculous that there is no way to do this for all pages in an easy manner.
Edit: For reference this is my server.js. It is the only other relevant file in the repo
const express = require("express")
const next = require("next")
const dev = process.env.NODE_ENV !== "production"
const start = async () => {
console.log("booting...")
const server = express()
const app = next({ dev, dir: __dirname })
const handle = app.getRequestHandler()
await app.prepare()
server.use((req, res, next) => {
req.user = {
authenticated: false,
name: "John Doe",
}
next()
})
server.get("*", handle)
server.listen(3000, (err) => {
if (err) {
console.error(err)
process.exit(1)
}
console.log("ready")
})
}
start().catch((error) => {
console.error(error)
process.exit(1)
})
It is recommended to do this via function components, as seen in the Next.js custom App docs:
// /pages/_app.tsx
import App, { AppProps, AppContext } from 'next/app'
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
MyApp.getInitialProps = async (appContext: AppContext) => {
// calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(appContext)
const req = appContext.ctx.req
return {
pageProps: {
...appProps.pageProps,
user: req?.user,
},
}
}
As in your answer, this will run on every request though so automatic static optimization will not be active.
Try a demo of changing pageProps in MyApp.getInitialProps (without usage of req.user) on the following CodeSandbox:
https://codesandbox.io/s/competent-thompson-l9r1u?file=/pages/_app.js
Turns out I can override getInitialProps on _app to make this work:
class MyApp extends App {
public static async getInitialProps({
ctx
}: AppContext): Promise<AppInitialProps> {
const req: any = ctx.req
return {
pageProps: {
user: req?.user
}
}
}
public render() {
//...
}
}
This will run on every request though so static optimization will not work, but in my case I need the information so I'm willing to accept the trade-off.
Edit: This answer also works, but it uses the "old" class-based component syntax which is no longer recommended. See answer from Karl for a more modern version using functional-component syntax.
I also had the similar problem where I had to fetch loggedIn user details from my Auth api. I solved it by wrapping my whole app inside a context provider, then using a set function for the initialState, which will remember if it was called before and fetch user details only once. Then in my each page, wherever I require these user details, I used the context to see if details are available and call the set function if details are not available. This way I think I achieved:
Only one request to fetch user details
Because it happens from the client side, TTFB is better
I can still take advantage of getStaticProps and getServerSideProps where it is required.

VueJS router.push not updating data inside page

I am using NuxtJS and I have a NavBar that goes to /website?web=1 and /website?web=2.. When I go from /website?web=1 to /website?web=2 vice versa.. My async fetch is not running at all.
website.vue
async fetch({ store, error, route, params }) {
let parameters;
const pageLimit = store.state.websites.pageLimit;
const offset = params.id ? (params.id - 1) * pageLimit : 0;
const web = route.query.web;
try {
if (web === "1") {
parameters = `?&is_global=true&offset=${offset}&limit=${pageLimit}`;
} else if (web === "2") {
parameters = `?&is_global=false&offset=${offset}&limit=${pageLimit}`;
} else {
parameters = `?co_id=${
route.query.co_id ? route.query.co_id : ""
}&ca_id=${
route.query.ca_id ? route.query.ca_id : ""
}&limit=${pageLimit}&offset=${offset}`;
}
await Promise.all([
store.dispatch("websites/fetchWebsites", parameters)
]);
} catch (e) {
console.log("Error: " + e);
}
},
NavBar.vue
methods: {
handleClick(tab, event) {
switch (tab.label) {
case "1":
this.$router.push({ name: "index" });
break;
case "2":
this.$router.push("/country");
break;
case "3":
this.$router.push("/website?web=1");
break;
case "4":
this.$router.push("/website?web=2");
break;
}
}
}
async fetch lifecycle is not invoked of query / param update
Sometimes you just want to fetch data and pre-render it on the server
without using a store. asyncData is called every time before loading
the page component. It will be called server-side once (on the first
request to the Nuxt app) and client-side when navigating to further
routes doc.
Also, a component does not remount on query / param update, so
lifecycles like created / mounted / beforeCreate etc are also not
invoked again. This helps in the application's performance as it avoids unnecessary rendering of the entire page where a few data changes would work.
Make a common method
methods: {
fetchData ({ store, error, route, params }) {
// your fetch logic here
let parameters;
const pageLimit = store.state.websites.pageLimit;
// ...
}
}
Call the method in async data
async fetch({ store, error, route, params }) {
this.fetchData({ store, error, route, params })
}
Call the method again on query change
watch: {
"$route.query.web": {
handler () {
this.fetchData({ store: this.$store, route: this.$route... });
}
},
Alternative to watch
beforeRouteUpdate (to, from, next) {
if (from.name === to.name) { // Call fn only when: route hasn't changed instead just query / params for the route has changed
this.fetchData({ store: this.$store, route: this.$route... })
}
},
When using Nuxt's fetch(), you need an explicit watcher to listen for route changes.
For a Nuxt component which has async fetch(), if you want it to update when the route changes, then setup a standard watcher.
See docs: https://nuxtjs.org/docs/2.x/features/data-fetching#listening-to-query-string-changes
export default {
watch: {
'$route.query': '$fetch' // This runs $fetch, defined below
},
async fetch() {
// Runs on server load (SSR), or when called (see above)
}
}
For other context's (or before Nuxt 2.12):
You could explore using watchQuery.
See docs: https://nuxtjs.org/docs/2.x/components-glossary/pages-watchquery/
export default {
watchQuery(newQuery, oldQuery) {
// Only execute component methods if the old query string contained `bar`
// and the new query string contains `foo`
return newQuery.foo && oldQuery.bar
}
}
https://nuxtjs.org/docs/2.x/components-glossary/pages-watchquery/

How to subscribe mutation changes of a store with multiple modules?

Using vue.js to build a login page. In my project, I have splited my store into two modules (User、Info).
In the User module, the actions.js aim to handle some asynchronous requests (such as login、register), and commit correspond mutation.
export const userActions = {
login({commit}, loginUser) {
commit(LOGIN)
axios.post(`${ API_BASE_USER }/login`, loginUser)
.then(res => {
const { token } = res.data
if (res.status == 200) { commit(LOGIN_SUCCESS, token) }
else { commit(LOGIN_FAILURE, res.data) }
})
}
.......
}
I knew that: In Vuex, we can subscribe store mutations.
I want to subuscribe every mutation changes in Login.vue
so I can load a notification to tell user login successully or not.
Login.vue
created () {
this.$store.subscribe(mutation => {
switch (mutation.type) {
case LOGIN_SUCCESS:
console.log('view success')
// load success nitification
break
case LOGIN_FAILURE:
console.log('view failure')
// load success nitification
break
case LOGIN_WARNING:
console.log('view warning')
break
}
})
}
But this seems doesn't work.
Is it impossible to subscribe specific module's mutations in a store which have mutiple modules ?
Since you use the namespace for the store modules, you need to consider it when subscribing. For example:
created () {
this.$store.subscribe(mutation => {
switch (mutation.type) {
case MODULE_NAME + '/' + LOGIN_SUCCESS:
console.log('view success')
// load success nitification
break
...
}
})
}