oedotme / render

Opinionated React Template
https://renderapp.vercel.app
MIT License
128 stars 19 forks source link

Guarding private dynamic routes #8

Closed hugo-instasense closed 1 year ago

hugo-instasense commented 1 year ago

Love this library, great work! One thing though that I am not sure how to do is add a slug path to the PRIVATE paths array.

Example say location.pathName = '/order/1043', currently the PRIVATE paths matches on exact ones. Would be cool to specify:

// Slug path
const PRIVATE = ['/logout', '/order/:orderId']

// Wildcard path
const PRIVATE = ['/logout', '/order/*']

Here is the modified guard.tsx, this might be a great example to add the the project?

import { Navigate, useLocation } from 'react-router-dom'

import { useAuth } from '@/context'

const PRIVATE = ['/logout', '/order/:orderId']
// or this
// const PRIVATE = ['/logout', '/order/*']
const PUBLIC = ['/login']

const findMatchingPath = (currentPath: string, pathPatterns: string[]) => {
  const currentPathParts = currentPath.split('/')

  for (const pattern of pathPatterns) {
    const patternParts = pattern.split('/')

    if (currentPathParts.length !== patternParts.length && !pattern.endsWith('*')) {
      continue
    }

    let isMatch = true
    for (let i = 0; i < patternParts.length; i++) {
      if (patternParts[i] === '*' || patternParts[i].startsWith(':')) {
        continue
      }
      if (currentPathParts[i] !== patternParts[i]) {
        isMatch = false
        break
      }
    }

    if (isMatch) {
      return pattern
    }
  }

  return null
}

export const Guard = ({ children }: { children: JSX.Element }) => {
  const auth = useAuth()
  const location = useLocation()

  const authedOnPublicPath = auth.token && findMatchingPath(location.pathname, PUBLIC)
  const unAuthedOnPrivatePath = !auth.token && findMatchingPath(location.pathname, PRIVATE)

  if (authedOnPublicPath) return <Navigate to="/" replace />
  if (unAuthedOnPrivatePath) return <Navigate to="/login" replace />

  return <>{children}</>
}
oedotme commented 1 year ago

Hey @hugo-instasense, I usually do something similar, here's a snippet using the matchPath util from react-router-dom:

import { matchPath, Navigate, useLocation } from 'react-router-dom'

import { useAuth } from '@/context'

const PRIVATE = ['/logout', '/dynamic/:timestamp']
const PUBLIC = ['/login']

const hasMatch = (paths: string[], path: string) => paths.some((x) => matchPath(x, path))

export const Guard = ({ children }: { children: JSX.Element }) => {
  const auth = useAuth()
  const { pathname } = useLocation()

  const authedOnPublicPath = auth.token && PUBLIC.includes(pathname)
  const unAuthedOnPrivatePath = !auth.token && (PRIVATE.includes(pathname) || hasMatch(PRIVATE, pathname))

  if (authedOnPublicPath) return <Navigate to="/" replace />
  if (unAuthedOnPrivatePath) return <Navigate to="/login" replace />

  return <>{children}</>
}

I'm thinking to include the original full path at each route object to directly compare the two paths without using the matchPath utility, will probably update it once that's supported by generouted.