Fermer

juin 14, 2024

Premiers pas avec Appwrite (partie 2)

Premiers pas avec Appwrite (partie 2)


Cette série explorera Appwrite, un backend de serveur auto-hébergé, ainsi qu’une plate-forme BaaS. Nous plongerons dans une application de facturation React, présentant le flux d’authentification, le stockage de base de données et les fonctions sans serveur. Dans la deuxième partie, nous créerons le flux d’authentification d’inscription/de connexion.

Bienvenue dans la deuxième partie de la série sur « Premiers pas avec Appwrite ». Dans Partie 1nous avons géré la configuration, qui comprenait :

  • Création d’une nouvelle application React avec TailwindCSS à l’aide de Vite
  • Création d’un nouveau projet Appwrite
  • Mise en place d’une base de données, collecte et stockage des factures

Dans cette partie, nous allons créer un formulaire d’authentification et l’intégrer à Appwrite pour permettre aux utilisateurs de créer un nouveau compte et de se connecter.

Méthodes d’authentification

Dans la partie précédente, nous avons configuré les routes, y compris les routes d’authentification :auth/register et auth/login. Nous avons également créé des composants vides pour eux. Avant d’ajouter le formulaire d’authentification, préparons la logique métier qui gérera le processus d’authentification. Nous allons commencer par créer un nouveau fichier API appelé auth.api.js.

src/api/auth.api.js

import { ID } from "appwrite";
import { account } from "./appwrite.api";
 
export const createAccount = (email, password) => {
  return account.create(ID.unique(), email, password);
};
 
export const login = (email, password) => {
  return account.createEmailSession(email, password);
};
 
export const getCurrentAuthSession = () => {
  return account.get();
};

Nous avons trois méthodes…createAccount, login et getCurrentAuthSession. Ils utilisent le SDK Appwrite account instance configurée dans le appwrite.api.js déposer.

Je pense que les noms sont assez explicatifs, mais si vous vous demandez pourquoi nous avons besoin getCurrentAuthSession, voici la raison. Lorsqu’un utilisateur visite l’application, nous devons vérifier s’il est déjà connecté. S’il a une session de connexion active, nous ne voulons pas le forcer à se reconnecter. Ce n’est pas un bon UX. Cependant, s’ils ne sont pas connectés, nous ne voulons pas leur permettre d’accéder à n’importe quelle page de facture. Au lieu de cela, nous souhaitons les rediriger vers la page de connexion. Le getCurrentAuthSession sera utilisé pour vérifier si un utilisateur est connecté.

Les informations d’authentification et les détails de l’utilisateur doivent généralement être accessibles à plusieurs endroits différents dans une application. Par conséquent, il doit être stocké de manière à permettre à n’importe quel composant d’y accéder. Les applications plus volumineuses utilisent généralement une solution de gestion d’état telle que Zustand ou Redux Toolkit pour gérer l’état global et partageable. Cependant, nous ne construisons pas une grande application, plaçons donc la logique d’authentification à la racine de l’application React et fournissons-la aux descendants via l’API Context.

Fournisseur de contexte utilisateur

Commençons par créer un fichier de contexte qui créera et exportera deux contextes et fonctions pour les consommer.

src/context/user.context.js

import { createContext, useContext } from "react";
 
export const UserContext = createContext({});
export const UserActionsContext = createContext({});
 
export const useUserContext = () => useContext(UserContext);
export const useUserActionsContext = () => useContext(UserActionsContext);

Le UserContext fournira des informations sur l’utilisateur, tandis que UserActionsContext fournira des méthodes, telles que login et createAccount. La raison de l’utilisation de contextes séparés est la performance. Fondamentalement, chaque fois que la valeur du contexte change, tous les consommateurs de contexte sont restitués. Par conséquent, nous fournissons les données utilisateur et les méthodes séparément.

Notez que fournir des valeurs dans un contexte n’entraînera souvent pas de problèmes de performances, surtout si les valeurs fournies ne changent pas fréquemment. Cependant, c’est une bonne pratique de faire les choses de manière performante pour éviter les lenteurs causées par l’accumulation de nombreux petits problèmes. Si vous souhaitez en savoir plus à ce sujet, j’ai écrit un article sur les goulots d’étranglement des performances qui le couvre en détail.

Ensuite, ajoutons le UserContextProvider composant. Il utilisera les contextes que nous venons de créer, initialisera la session d’authentification de l’utilisateur et redirigera un utilisateur s’il tente d’accéder à une route non-auth sans être connecté.

src/context/UserContextProvider.jsx

import { useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { createAccount, getCurrentAuthSession, login } from "../api/auth.api";
import { UserActionsContext, UserContext } from "./user.context";
 
const UserContextProvider = props => {
  const [user, setUser] = useState(null);
  const [isInitialized, setIsInitialized] = useState(false);
  const navigate = useNavigate();
  const location = useLocation();
  const initUserSession = async () => {
    
    try {
      const currentSession = await getCurrentAuthSession();
      if (currentSession) {
        setUser(currentSession);
        if (location.pathname.includes("auth")) {
          navigate("https://www.telerik.com/");
        }
      } else {
        navigate("/auth/login");
      }
    } catch (error) {
      console.error(error);
      navigate("/auth/login");
    }
    setIsInitialized(true);
  };
 
  useEffect(() => {
    
    if (isInitialized) {
      if (!user && !location.pathname.includes("auth")) {
        navigate("/auth/login");
      }
    } else {
      initUserSession();
    }
  }, [location.pathname]);
 
  const value = useMemo(() => {
    return {
      user,
    };
  }, [user]);
 
  const actions = useMemo(() => {
    return {
      login,
      createAccount,
      setUser,
    };
  }, []);
 
  return (
    <UserContext.Provider value={value}>
      <UserActionsContext.Provider value={actions}>
        {isInitialized ? (
          props.children
        ) : (
          <div className="flex items-center justify-center min-h-screen font-semibold text-indigo-600">
            Loading...
          </div>
        )}
      </UserActionsContext.Provider>
    </UserContext.Provider>
  );
};
 
export default UserContextProvider;

Digérons ce qui se passe dans le UserContextProvider composant. Premièrement, nous avons useState, useNavigate et useLocation crochets.

const [user, setUser] = useState(null);
const [isInitialized, setIsInitialized] = useState(false);
const navigate = useNavigate();
const location = useLocation();

Dans le user état, nous stockons les informations sur l’utilisateur qui sont renvoyées par le getCurrentAuthSesssion fonction. Le isInitialized state est utilisé pour déterminer si l’état d’authentification de l’utilisateur a déjà été initialisé ou non. Le navigate La fonction sera utilisée pour rediriger l’utilisateur vers une page de connexion ou de factures, en fonction de son statut d’authentification.

Enfin, nous avons besoin d’accéder au location objet. Il sera utilisé par un garde d’authentification pour empêcher les utilisateurs non authentifiés d’accéder aux pages de facture. Notez que dans un projet plus vaste, il serait peut-être préférable d’ajouter des gardes d’authentification dans les définitions de routes, mais cette approche est suffisante pour ce didacticiel.

Une fois les hooks exécutés, nous avons le initUserSession. Cette fonction utilise le getCurrentAuthSession fonction à partir du appwrite.api.js déposer. Si un utilisateur est déjà connecté, nous mettons à jour l’état de l’utilisateur et accédons à l’itinéraire des factures si un utilisateur tente d’accéder à une page de connexion ou d’inscription.

const initUserSession = async () => {
  
  try {
    const currentSession = await getCurrentAuthSession();
    if (currentSession) {
      setUser(currentSession);
      if (location.pathname.includes("auth")) {
        navigate("https://www.telerik.com/");
      }
    } else {
      navigate("/auth/login");
    }
  } catch (error) {
    console.error(error);
    navigate("/auth/login");
  }
  setIsInitialized(true);
};

Si un utilisateur n’est pas authentifié, il est redirigé vers la page de connexion.

Ensuite, nous avons le useEffect crochet. Il s’exécutera lors du premier rendu du composant et à chaque fois que le chemin d’accès de l’URL change. Le initUserSession La fonction est appelée si l’état d’authentification d’un utilisateur n’a pas encore été initialisé. Sinon, s’il n’y a pas de données utilisateur et que l’utilisateur tente d’accéder à une page non-authentifiée, il sera redirigé vers la page de connexion.

useEffect(() => {
  
  if (isInitialized) {
    if (!user && !location.pathname.includes("auth")) {
      navigate("/auth/login");
    }
  } else {
    initUserSession();
  }
}, [location.pathname]);

Enfin et surtout, nous avons value et actions variables et JSX renvoyés par le composant.

const value = useMemo(() => {
  return {
    user,
  };
}, [user]);
 
const actions = useMemo(() => {
  return {
    login,
    createAccount,
    setUser,
  };
}, []);
 
return (
  <UserContext.Provider value={value}>
    <UserActionsContext.Provider value={actions}>
      {isInitialized ? (
        props.children
      ) : (
        <div className="flex items-center justify-center min-h-screen font-semibold text-indigo-600">
          Loading...
        </div>
      )}
    </UserActionsContext.Provider>
  </UserContext.Provider>
);

Le value et actions les variables sont mémorisées pour garantir que les références des objets transmises via les contextes restent les mêmes s’il n’y a eu aucun changement. Cela permettra d’éviter de restituer inutilement les consommateurs de contexte.

Le children ne sont rendus que lorsque l’état d’authentification de l’utilisateur a été initialisé. Sinon, un texte de chargement s’affiche.

Maintenant, mettons à jour le App composant et enveloppez son contenu avec le UserContextProvider.

src/App.jsx

import { Outlet } from "react-router-dom";
import "./App.css";
import { Suspense } from "react";
import { Toaster } from "react-hot-toast";
import UserContextProvider from "./context/UserContextProvider";
 
function App() {
  return (
    <UserContextProvider>
      <Suspense loading={<div />}>
        <Outlet />
      </Suspense>
      <Toaster />
    </UserContextProvider>
  );
}
 
export default App;

Outre l’ajout du UserContextProvidernous incluons également le Toaster composant de react-hot-toast. Il sera utilisé pour les notifications d’erreurs.

Permettez-moi simplement d’ajouter ici qu’il y a une raison pour laquelle les contextes sont créés dans un fichier séparé et ne sont pas placés à l’intérieur du UserContextProvider.jsx déposer. En mode développement, Plugin Vite React les usages Réagissez à l’actualisation rapide, qui nous permet de modifier les composants React dans une application en cours d’exécution sans perdre leur état. Cependant, cela nécessite que les fichiers contenant des composants React n’exportent qu’un composant et rien d’autre.

Formulaires de connexion et d’inscription

Maintenant que nous avons configuré l’essentiel de la logique d’authentification, passons au formulaire d’authentification. Tout d’abord, nous allons créer un personnalisé Input composant.

src/components/form/Input.jsx

const Input = props => {
  const { id, label, value, onChange, rootProps, ...inputProps } = props;
  return (
    <div className="flex flex-col w-full gap-1" {...rootProps}>
      {label ? (
        <label htmlFor={id} className="text-sm text-indigo-950/75">
          {label}
        </label>
      ) : null}
      <input
        id={id}
        className="px-4 py-2 rounded-md shadow"
        type="text"
        value={value}
        onChange={event => {
          onChange(event.target.value, event);
        }}
        {...inputProps}
      />
    </div>
  );
};
 
export default Input;

Dans le formulaire d’authentification, nous n’aurons que deux champs de saisie : e-mail et mot de passe. Cependant, dans la prochaine partie de cette série, nous créerons un formulaire de facture, qui en nécessitera bien plus. C’est pourquoi nous utiliserons un composant personnalisé pour afficher une entrée avec une étiquette.

Après le Input Le composant est prêt, il est temps de passer au Auth.jsx déposer.

src/views/auth/Auth.jsx

import { useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import Input from "../../components/form/Input";
import { useUserActionsContext } from "../../context/user.context";
 
const config = {
  login: {
    header: "Login",
    submitButtonText: "Log In",
    toggleAuthModeLink: {
      to: "/auth/register",
      text: "Create a new account",
    },
  },
  register: {
    header: "Create Account",
    submitButtonText: "Register",
    toggleAuthModeLink: {
      to: "/auth/login",
      text: "Already have an account?",
    },
  },
};
 
const Auth = () => {
  const { login, createAccount, setUser } = useUserActionsContext();
  const [form, setForm] = useState({
    email: "",
    password: "",
  });
  const navigate = useNavigate();
  const [error, setError] = useState(null);
  const location = useLocation();
  const isCreateAccountPage = location.pathname.includes("register");
  const { header, submitButtonText, toggleAuthModeLink } =
    config[isCreateAccountPage ? "register" : "login"];
 
  const onFormChange = key => value => {
    setForm(state => ({
      ...state,
      [key]: value,
    }));
  };
 
  const onFormSubmit = async event => {
    event.preventDefault();
    const { email, password } = form;
 
    if (!email) {
      setError("Please enter your email.");
      return;
    }
 
    if (!password) {
      setError("Please enter the password.");
      return;
    }
 
    try {
      if (isCreateAccountPage) {
        await createAccount(email, password);
      }
 
      const loginSession = await login(email, password);
      setUser(loginSession);
 
      navigate("https://www.telerik.com/");
    } catch (error) {
      console.error(error);
      setError(error.messsage);
    }
  };
 
  return (
    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-r from-pink-300 via-purple-300 to-indigo-400">
      <div className="flex items-center justify-center w-3/4 mx-8 bg-white md:w-1/2 md:min-h-screen md:ml-auto md:mx-0 max-md:rounded-2xl">
        <form className="w-full p-8 md:w-96 md:p-4" onSubmit={onFormSubmit}>
          <h1 className="mb-8 text-2xl font-semibold text-center">{header}</h1>
 
          <div className="flex flex-col items-start gap-3">
            <Input
              label="Email"
              id="email-field"
              className="px-4 py-2 rounded-md shadow"
              type="email"
              value={form.email}
              onChange={onFormChange("email")}
            />
            <Input
              label="Password"
              id="password-field"
              className="px-4 py-2 rounded-md shadow"
              type="password"
              value={form.password}
              onChange={onFormChange("password")}
            />
          </div>
 
          {error ? <p className="block mt-2 text-red-600">{error}</p> : null}
          <button
            className="block w-full h-12 mt-6 text-indigo-100 transition-colors duration-150 bg-indigo-600 rounded-md hover:bg-indigo-800"
            type="submit"
          >
            {submitButtonText}
          </button>
 
          <Link
            className="block mt-6 text-center text-indigo-900 transition-colors duration-150 hover:text-indigo-600"
            to={toggleAuthModeLink.to}
          >
            {toggleAuthModeLink.text}
          </Link>
        </form>
      </div>
    </div>
  );
};
 
export default Auth;

Nous déterminons si nous sommes sur la page de connexion ou d’inscription en vérifiant si l’URL comprend le mot register. Le résultat est stocké dans le isCreateAccountPage variable, qui à son tour est utilisée pour obtenir des informations du config objet pour la page actuelle. Sur la page de connexion, les utilisateurs verront le Login en-tête avec Log In texte du bouton et Create a new account lien, tandis que sur la page d’inscription, ils verront le Create Account en-tête avec Register texte du bouton et Already have an account? lien. Le GIF ci-dessous montre à quoi ressemblent les pages d’authentification.

Pages d'authentification

Lorsqu’un utilisateur soumet le formulaire d’authentification, le onFormSubmit le gestionnaire est appelé. Nous avons une validation très simple, qui vérifie si l’email et le mot de passe sont présents. Normalement, c’est une bonne idée de vérifier si l’e-mail fourni ressemble à un e-mail réel et si le mot de passe répond à des règles spécifiques, telles que la longueur minimale, les caractères, etc. Si un utilisateur est sur la page de création de compte, le createAccount la fonction est appelée. Il est important de noter que createAccount ne connecte pas automatiquement un utilisateur, nous devons donc toujours exécuter le login méthode.

const onFormSubmit = async event => {
  event.preventDefault();
  const { email, password } = form;
  
  if (!email) {
    setError("Please enter your email.");
    return;
  }
 
  if (!password) {
    setError("Please enter the password.");
    return;
  }
 
  try {
    if (isCreateAccountPage) {
      await createAccount(email, password);
    }
 
    const loginSession = await login(email, password);
    setUser(loginSession);
 
    navigate("https://www.telerik.com/");
  } catch (error) {
    console.error(error);
    setError(error)
  }
};

Maintenant, vous pouvez créer un nouveau compte. Après la création du compte, vous serez redirigé vers la page des factures, qui, pour le moment, ne contient pas grand-chose, mais nous nous en occuperons dans la partie suivante.

Conclusion

Nous avons expliqué comment implémenter l’authentification dans React à l’aide d’Appwrite. Nous avons créé un UserContextProvider et a profité de l’API Context pour fournir les données utilisateur ainsi que les méthodes utilisateur et d’authentification au reste de l’application. Nous nous sommes également assurés qu’un utilisateur ne puisse pas visiter les pages de facture sans être authentifié. Enfin, nous avons créé un composant d’authentification pour gérer les pages d’inscription et de connexion. Dans la partie suivante, nous créerons un formulaire pour créer et mettre à jour les factures.




Source link