Научете за съхраняването на потребителски сесии и частни маршрути със Supabase и React рутер v6!

Едно от най-добрите изобретения в света на уеб разработката според мен е моделът Backend-as-a-Service. Колкото и фантастично да звучи да не се налага да го пишете сами, има услуги като Firebase и Supabase, които наистина осигуряват удостоверяване, съхранение в облак и всички добри неща веднага след изваждането от кутията.

Наскоро си играх със Supabase, тъй като предлага точно същите функционалности като Firebase, но със среда, базирана на PostgreSQL. Доколкото се чувствам добре от това.

В тази поредица от статии искам да ви представя начин за справяне с удостоверяването на потребителя и съхранението на сесии със Supabase в приложенията на React.

Това може да звучи тривиално на пръв поглед, но има много движещи се части и трябваше да направя много проучвания, за да намеря нещо, което работи точно за мен. Надявам се да е така и за вас.

Нека се потопим! 👇

Крайната цел

Нашето уеб приложение за краен резултат трябва да има следните основни характеристики:

  • Трябва да има целева страница
  • Трябва да позволява влизане и регистрация поотделно (което означава, по различни маршрути)
  • Трябва да има лично табло за управление, достъпно само за влезли потребители

След това нашето приложение трябва да има и следната междинна функция:

  • Трябва да може да поддържа потребителите влезли дори след презареждане на уеб страницата и затваряне на раздела

Стекът, който ще използваме, ще бъде съставен от React с Tailwind CSS, защото това е до голяма степен по подразбиране за мен поради чистото качество и използваемост.

Ще използвам също и react-router-dom версия 6. Така че, ако искате да следвате, моят package.json изглежда така:

{
  "name": "basics_advanced_sup",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@heroicons/react": "^2.0.16",
    "@supabase/supabase-js": "^2.10.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.9.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.27",
    "@types/react-dom": "^18.0.10",
    "@vitejs/plugin-react": "^3.1.0",
    "autoprefixer": "^10.4.14",
    "postcss": "^8.4.21",
    "tailwindcss": "^3.2.7",
    "vite": "^4.1.0"
  }
}

Настройка на проекта

Нека създадем нов React проект с Vite и да инсталираме нашите зависимости:

$ yarn create vite

# or

$ npm create vite@latest

Следвайте инструкциите и стартирайте проекта. Сега следвайте ръководството на Tailwind от тук, за да работи и той.

Сега, що се отнася до CSS класовете, не се фокусирам върху тях, защото този урок е за изучаване на Supabase удостоверяване, така че просто ще ви пренасоча към моето репо в GitHub, за да ги вземете от тук:



Сега да преминем към хубавите неща.

Структурата на проекта

Окончателната структура на проекта, с която ще работим, е следната и ще бъде изключително полезно да я следвате, ако сте направили същата във вашия собствен проект:

Настройка на рутера и маршрутите

Както можете да видите, нямам отделен файл или директория за рутера и това е така, защото нашият прост случай на използване в този пример ще бъде по-добре да го дефинираме направо в App.jsx:

function App() {
  const router = createBrowserRouter([
    {
      path: "/",
      element: <Main />,
      errorElement: <Error />,
      children: [
        {
          index: true,
          element: <LandingPage />,
        },
        {
          path: "login",
          element: <Login />,
        },
        {
          path: "signup",
          element: <Signup />,
        },
        {
          path: "logout",
        },
        {
          path: "dashboard",
          element: <Dashboard />,
        },
      ],
    },
  ])
  return (
    <div>
        <RouterProvider router={router} />
    </div>
  )
}

export default App

Нека да видим какво се случва тук с един поглед:

  • Имаме елемента Main, който също е нашият елемент Layout. Това означава, че всеки негов дъщерен елемент, както е дефиниран по-горе, ще разшири по някакъв начин нашия Главен елемент на оформлението.
  • Ние дефинираме другите пътеки или маршрутикато деца на елемента Main. Това означава, че пътищата на другите елементи ще бъдат добавени към основния елемент. Например path: “login” означава, че страницата ни за вход ще се намира на „localhost:5173/login“.
  • Елементът LandingPage е, както пише на кутията, целевата страница на нашето малко приложение.
  • Елементът Табло за управление е този, който ще бъде частен и достъпен само за влезли потребители.

Сега, тъй като сме тук за удостоверяването, нека да се заемем с него.

Настройка на удостоверяване в таблото за управление на Supabase

Влезте във вашите имейл настройки и доставчици и активирайте удостоверяването „Имейл“.

Засега нека деактивираме опцията „потвърждаване на имейл“, тъй като просто се учим, но не забравяйте, че е добре да я включите в производството.

Сега, обратно към кода.

Във файла services/supabase.jsx поставете следното:

import { createClient } from "@supabase/supabase-js"

const supabaseUrl = import.meta.env.VITE_PROJECT_URL_SUPABASE
const supabaseKey = import.meta.env.VITE_PROJECT_ANON_KEY_SUPABASE
const supabase = createClient(supabaseUrl, supabaseKey)

export { supabase }

Горният метод за четене на променливи на средата е за четене при работа с Vite. Ако използвате прост React, вместо префикса VITE_ във вашите променливи на средата, използвайте REACT_.

Настройка на основното оформление

Новата версия на React Router ни дава начин да дефинираме оформления. Елементът Outlet е като елемент на контейнер, който можете да поставите и той ще гарантира, че всички деца на оформлението, които се показват, ще бъдат поставени точно там.

const Main = () => {
  return (
    <div>
      <Navbar />
      <div className="flex justify-center items-center h-screen">
        <Outlet />
      </div>
    </div>
  )
}

export default Main

Страхотно, сега нека дефинираме проста примерна целева страница:

const LandingPage = () => {

  return <div>I am the LandingPage</div>
}

export default LandingPage

След като дефинираме правилно контекстите за удостоверяване, ще трябва да проверим за влезли потребители, когато тези компоненти се монтират.

По подобен начин можете да дефинирате компонентите Регистрация и Вход. Използвах Tailwind CSS, за да ги стилизирам и ето как изглеждат:

Настройване на куките за контекст

От файловата структура вече сте наясно, че ще дефинираме auth.jsx файл, който да съдържа контекста на широкообхватно удостоверяване на нашето приложение.

Първо дефинирайте обект Контекст и го експортирайте:

const AuthContext = createContext()

export function useAuth() {
  return useContext(AuthContext)
}

Това е функцията, която ще разкрие текущия удостоверен потребител и действията по регистрация, влизане и излизане пред цялото ни приложение.

Нека сега дефинираме Доставчик за нашия контекст. Този доставчик ще обвие всички наши маршрути, като по този начин ще осигури необходимите състояния на удостоверяване във всеки един момент, навсякъде в приложението.

Нека да видим какво искаме да направим в нашия доставчик:

  • първо проверете състоянието на удостоверяване на потребителя, докато приложението се монтира
  • използвайте някои кукички за състояние, за да следите потребителските сесии
  • уверете се, че потребителските сесии остават независимо от опресняване или дори при затваряне на раздела

Това може да се направи с localStorage!

След горната функция дефинирайте AuthProvider така:

export function AuthProvider({ children }) {
  const [session, setSession] = useState()
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    let gotSession = localStorage.getItem("authSession")
    if (gotSession) {
      console.log("Retrieved: ", gotSession)
      setSession(JSON.parse(gotSession))
      setUser(JSON.parse(gotSession))
    }
    async function getSession() {
      setLoading(false)
      const { subscription } = supabase.auth.onAuthStateChange(
        async (event, session) => {
          if (session) {
            console.log("New session: ", session)
            setUser(session.user)
            localStorage.setItem("authSession", JSON.stringify(session))
            setSession(session)
          } else {
            localStorage.removeItem("authSession")
            setSession(null)
            setUser(null)
          }
          setLoading(false)
        }
      )
      return () => {
        subscription?.unsubscribe()
      }
    }
    getSession()
  }, [])

  const value = {
    signUp: (data) => supabase.auth.signUp(data),
    logIn: (data) => supabase.auth.signInWithPassword(data),
    signOut: () => supabase.auth.signOut(),
    user,
  }

  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  )
}

Нека го разопаковаме сега:

Ние следим две основни състояния на приложението: потребител и сесия.

Първо, в куката useEffect, която ще бъде извикана веднага щом компонентът бъде монтиран, искаме да проверим дали нашето localStorage вече има някои данни за сесията от по-рано. Ако е така, ние го вземаме и населяваме нашите щати.

Ако в localStorage няма намерени данни за предишна сесия, искаме просто да извикаме supabase за нова потребителска сесия със слушателя onAuthChange. Това ще ни предостави нови токени за достъп и опресняване.

И накрая, в стойността дефинираме три функции за регистрация, влизане и излизане на потребителя. Връщаме ги заедно с потребителя като value={} на AuthContext.Provider.

Страхотен! Сега, след като имаме дефинирани контексти, нека направим някои промени в нашите компоненти.

В App.jsx обвийте Доставчика около маршрутите:

return (
    <div>
      <AuthProvider>
        <RouterProvider router={router} />
      </AuthProvider>
    </div>
  )

В оформлението Main.jsx използвайте auth user от useAuth (😉), за да проверите дали потребителят е влязъл:

const Main = () => {
  const { user } = useAuth()
  const navigate = useNavigate()

  useEffect(() => { // this is new!
    if (!user) {
      navigate("/")
    }
  }, [user])

  return (
    <div>
      <Navbar />
      <div className="flex justify-center items-center h-screen">
        <Outlet />
      </div>
    </div>
  )
} 

След това използвайте същата логика в LandingPage:

const LandingPage = () => {
  const { user } = useAuth()
  const navigate = useNavigate()

  useEffect(() => {
    if (user) {
      navigate("/dashboard")
    }
  }, [user])

  return <div>I am the LandingPage</div>
}

Осъзнайте, че с куката useEffect можем лесно да проверим и пренасочим потребителя към желаната от нас страница, ако удостоверяването е открито или не е открито.

И накрая, дефинирайте функциите за влизане и регистрация!

useRef Hook при влизане и регистрация

Използвайте препратки във въведенията на вашия имейл и парола с useRef:

  const emailRef = useRef()
  const passwordRef = useRef()

  const { user } = useAuth()

  const { logIn } = useAuth()
  const navigate = useNavigate()

И във входовете:

<input
                name="email"
                type="email"
                ref={emailRef}
                className="w-full rounded border bg-gray-50 px-3 py-2 text-gray-800 outline-none ring-indigo-300 transition duration-100 focus:ring"
              />


<input
                name="password"
                ref={passwordRef}
                type="password"
                className="w-full rounded border bg-gray-50 px-3 py-2 text-gray-800 outline-none ring-indigo-300 transition duration-100 focus:ring"
              />

И накрая, дефинирайте функция за обработка на изпращания:

async function handleLogIn(e) {
    e.preventDefault()

    const email = emailRef.current.value
    const password = passwordRef.current.value

    const { error } = await logIn({ email, password })

    if (error) {
      alert("Error logging in: ", error)
    } else {
      navigate("/dashboard")
    }
  }

И направете същото за страницата Регистрация.

Сега нека го тестваме:

Сега, ако се опитате да отворите /login ръчно, автоматично ще ви пренасочи към таблото за управление. Същото за пътя „/“ (целева страница) и пътя „/signup“ (страница за регистрация). Те могат да бъдат достъпни само от неудостоверен потребител.

И това е! Вече успешно внедрихте удостоверяване със съхранение на сесии!

Няколко прощални думи…

Исках урок, който щеще следвам за прилагане на горния случай на употреба, и когато не намерих такъв, реших да напиша и да споделя своя.

Следващия път ще пиша за разширяване на това удостоверяване чрез събиране и съхраняване също на потребителски имена и потребителски аватари.

Абонирайте се, за да получавате моите седмични уроци по програмиране и много повече!

Кодиране на ниво нагоре

Благодарим ви, че сте част от нашата общност! Преди да тръгнеш:

🚀👉 Присъединете се към колектива за таланти Level Up и намерете невероятна работа