atellmer / dark

The lightweight and powerful UI rendering engine without dependencies and written in TypeScriptđź’« (Browser, Node.js, Android, iOS, Windows, Linux, macOS)
MIT License
43 stars 2 forks source link

[web-router] protected routes, how to implement them? #79

Open einar-hjortdal opened 4 months ago

einar-hjortdal commented 4 months ago

Web router expects factory functions in the component field, which means I cannot do

// ./routes.jsx
import AuthProvider from './components/AuthProvider'
const Dashboard = lazy(() => import('./pages/Dashboard'))
export const routes = [
  {
    path: '/',
    component: Root,
    children: [
      {
        path: '',
        component: <AuthProvider><Dashboard /></AuthProvider>
      },
      {
        path: 'login',
        component: lazy(() => import('./pages/Login'))
      },
      {
        path: 'not-found',
        component: lazy(() => import('./pages/NotFound'))
      },
      {
        path: '**',
        redirectTo: 'not-found'
      }
    ]
  }
]

I also cannot do

// ./pages/Dashboard.jsx
export default AuthProvider(Dashboard)

How does one implement "protected routes" with web-router?

atellmer commented 4 months ago

It might be worth using something like this

const protect = (factory: ComponentFactory) => {
  return component((props = {}) => {
    const { isLoggedIn } = useAuth();

    useLayoutEffect(() => {
      if (!isLoggedIn) {
        // redirect to login page
      }
    }, []);

    return isLoggedIn ? factory(props) : null;
  });
};

export const routes = [
  {
    path: '/',
    component: Root,
    children: [
      {
        path: '',
        component: protect(lazy(() => import('./pages/Dashboard'))),
      },
      {
        path: 'login',
        component: lazy(() => import('./pages/Login')),
      },
      {
        path: 'not-found',
        component: lazy(() => import('./pages/NotFound')),
      },
      {
        path: '**',
        redirectTo: 'not-found',
      },
    ],
  },
];
einar-hjortdal commented 4 months ago

Thank you for the lead. I think I got something working

const AuthContext = createContext(null)

export const useAuth = () => useContext(AuthContext)

// Wrap app
const AuthProvider = component(({ slot }) => {
  const api = useApi()
  const { isFetching, data, error } = useQuery('retrieveAdmin', api.retrieveAdmin)

  if (isFetching && !data) {
    return <div>Loading...</div>
  }

  return <AuthContext value={{ isFetching, data, error }}>{slot}</AuthContext>
})

export default AuthProvider

// Wrap pages that require auth
export const withAuth = (factory) => {
  return component((props) => {
    const { error } = useAuth()

    if (error) {
      const history = useHistory()
      history.push('/login')
      return null
    }

    return factory(props)
  })
}

// Login.jsx
const Login = component(() => {
  const { data } = useAuth()

  // redirect to dashboard if logged in
  if (data) {
    const history = useHistory()
    history.push('/')
    return null
  }

  return (
    <>
      login form
    </>
  )
})
einar-hjortdal commented 4 months ago

I noticed that the code above doesn't actually work as expected with SSR. I think history.push doesn't work right on the server, is that correct? How is such a redirection supposed to be done on the server?

One possibility would be to only redirect on the browser, moving history.push in useEffect, but I would like the server to render the correct page.

atellmer commented 4 months ago

I think history.push doesn't work right on the server, is that correct?

Such actions are usually done through effects. In reality, history.push plans a new render, but it cannot be executed on the server.

One possibility would be to only redirect on the browser, moving history.push in useEffect

Sounds like a reasonable solution.

but I would like the server to render the correct page.

This would require the router to implement some method that would synchronously check the rights to the route when building the route tree. There is no such method in this implementation. In addition, I can’t think of a use case where it would be necessary to redirect from a protected page to a login page only through the server. What's the benefit? This is a pretty narrow scenario for SSR.

einar-hjortdal commented 4 months ago

This would require the router to implement some method that would synchronously check the rights to the route when building the route tree. There is no such method in this implementation. In addition, I can’t think of a use case where it would be necessary to redirect from a protected page to a login page only through the server. What's the benefit? This is a pretty narrow scenario for SSR.

Not only through server, but on first requests to the server in a isomorphic fashion. It's mostly an optimization to immediately deliver the login page html, for a faster perceived response time. Not so important, so not going to pursue this now.