Fermer

octobre 19, 2018

Construire une application Web simple avec Express, React et GraphQL –


Cet article a été publié à l'origine sur sur le blog des développeurs d'Okta . Merci de votre soutien aux partenaires qui rendent SitePoint possible.

GraphQL et React sont devenus très populaires ces dernières années et il est prudent de dire qu’ils vont ensemble comme un avocat et un pain grillé. Un serveur GraphQL peut être écrit dans Node et vous permet de créer facilement une API flexible utilisant des classes et des fonctions JavaScript. Lorsqu'un développeur frontal interroge le serveur, seules les informations demandées sont traitées. Cela signifie que vous pouvez rendre le backend aussi robuste que vous le souhaitez tout en gardant l'éclairage du front en ne demandant que les informations nécessaires pour la page affichée.

GraphQL est un standard relativement nouveau pour la définition des types et l'interrogation des données. quelques implémentations différentes, à la fois côté serveur et côté client. Aujourd'hui, je vais vous montrer comment utiliser Express pour créer un serveur GraphQL, ainsi que pour créer une application d'une page dans React utilisant le client d'Apollo pour interroger le serveur.

Création de l'application React

Pour commencer à utiliser une application React, utilisez Create React App . Si l'application Node, Yarn et Create React n'est pas déjà installée, vous pouvez exécuter les commandes suivantes:

 curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install .sh | frapper
npm installer --global yarn create-react-app

Ensuite, créez et démarrez une nouvelle application:

 create-react-app graphql-express-react
cd graphql-express-react
début de fil

Lorsque vous exécutez create-react-app vous obtenez un nouveau dossier avec tout ce dont vous avez besoin pour commencer et toutes les dépendances dont vous avez besoin seront installées localement à l'aide du fil . ]. Lorsque vous tapez dans le dossier, démarrez dans le dossier, vous lancez le serveur de développement frontend qui se met automatiquement à jour lorsque vous modifiez des fichiers.

 L'outil bootstrap de l'application create-react-app

Créer le serveur GraphQL

Avant de continuer à écrire l'interface, vous aurez besoin d'un serveur pour vous connecter. Exécutez les commandes suivantes pour installer les dépendances dont vous aurez besoin pour être opérationnel:

 yarn add express@4.16.3 cors@2.8.4 graphql@14.0.2 express-graphql@0.6.12 graphql-tag@2.9 .2

Créez un nouveau répertoire dans le dossier src de votre projet, nommé serveur :

 mkdir src / server

Créez un nouveau fichier nommé index.js avec le code suivant:

 const express = require ('express');
const cors = require ('cors');
const graphqlHTTP = require ('express-graphql');
const gql = require ('graphql-tag');
const {buildASTSchema} = require ('graphql');

const POSTS = [
  { author: "John Doe", body: "Hello world" },
  { author: "Jane Doe", body: "Hi, planet!" },
];

const schéma = buildASTSchema (gql`
  tapez Query {
    messages: [Post]
    post (id: ID!): Post
  }

  tapez Post {
    J'ai fait
    auteur: String
    corps: String
  }
`);

const mapPost = (post, id) => post && ({id, ... post});

const racine = {
  posts: () => POSTS.map (mapPost),
  post: ({id}) => mapPost (POSTS [id]id),
};

const app = express ();
app.use (cors ());
app.use ('/ graphql', graphqlHTTP ({
  schéma,
  rootValue: root,
  graphiql: true,
}));

const port = process.env.PORT || 4000
app.listen (port);
console.log (`Exécution d'un serveur d'API GraphQL sur localhost: $ {port} / graphql`);

Permettez-moi de vous expliquer les différentes parties de ce code.

En haut du fichier, vous utilisez la balise qui nécessite pour importer vos dépendances. Le nœud natif ne prend pas encore en charge la balise import mais vous pouvez utiliser require à la place. Une future version de Node supportera probablement l'importation . Create React App utilise babel pour transpiler le code avant de l'exécuter, ce qui vous permet d'utiliser la syntaxe import dans le code React. Ainsi, vous verrez que lorsque nous arriverons au front-end code.

Pour l’instant, il s’agit simplement de données fictives, ce que contient le const POSTS . Chaque élément contient un auteur et un corps .

La balise gql permet à votre éditeur de code favori de réaliser que vous écrivez du code GraphQL afin qu'il peut le styliser de manière appropriée. Il analyse également la chaîne et la convertit en arbre de syntaxe abstraite GraphQL AST . Vous devez ensuite construire un schéma en utilisant buildASTSchema .

Le schéma GraphQL est peut-être la partie la plus intéressante de ce code. C'est ce qui définit les différents types et vous permet de dire ce que le client peut interroger. Cela générera aussi automatiquement une documentation très utile pour que vous puissiez vous concentrer sur le codage.

 type Query {
  messages: [Post]
  post (id: ID!): Post
}

tapez Post {
  J'ai fait
  auteur: String
  corps: String
}

Vous avez défini ici un type Post qui contient un auteur id et auteur et un corps . Vous devez dire quels sont les types pour chaque élément. Ici, author et body utilisent tous deux le type primitif String et id est un ID . [19659003] Le type Query est un type spécial qui vous permet d'interroger les données. Vous dites ici que posts vous donnera un tableau de Post s, mais si vous voulez un seul Post vous pouvez le consulter en appelant ] post et en passant l'ID.

 const mapPost = (post, id) => post && ({id, ... post});

const racine = {
  posts: () => POSTS.map (mapPost),
  post: ({id}) => mapPost (POSTS [id]id),
};

Vous devez fournir un ensemble de résolveurs pour indiquer à GraphQL comment traiter les requêtes. Quand quelqu'un interroge posts il exécute cette fonction, fournissant un tableau de tous les POSTS en utilisant leur index comme ID.

Lorsque vous interrogez post . ]il attend un id et renvoie le message à l’index donné.

 const app = express ();
app.use (cors ());
app.use ('/ graphql', graphqlHTTP ({
  schéma,
  rootValue: root,
  graphiql: true,
}));

const port = process.env.PORT || 4000
app.listen (port);
console.log (`Exécution d'un serveur d'API GraphQL sur localhost: $ {port} / graphql`);

Vous pouvez maintenant créer le serveur. La fonction graphqlHTTP crée un serveur Express exécutant GraphQL, qui attend les résolveurs sous la forme rootValue ainsi que le schéma. L'indicateur graphiql est facultatif et vous permet de gérer un serveur, ce qui vous permet de visualiser plus facilement les données et de voir la documentation générée automatiquement. Lorsque vous exécutez app.listen vous démarrez le serveur GraphQL.

Pour vous assurer que nous pouvons facilement exécuter le serveur et le client simultanément, ajoutez les dépendances de développement suivantes:

 ajout de fil -D nodemon@1.18.4 npm-run-all@4.1.3

Ensuite, éditez votre fichier package.json afin que la section scripts ressemble à ceci:

 {
  "start": "npm-run-all - veille parallèle: serveur start: web",
  "start: web": "react-scripts start",
  "start: serveur": "noeud src / serveur",
  "watch: server": "nodemon --watch src / server src / server",
  "build": "react-scripts build",
  "test": "react-scripts test --env = jsdom",
  "éjecter": "réagir-scripts éjecter"
},

Fermez votre serveur Web existant, puis tapez simplement yarn start pour exécuter le serveur et le client simultanément. Chaque fois que vous apportez des modifications au serveur, seul le serveur redémarre. Chaque fois que vous apportez des modifications au code frontal, la page devrait s'actualiser automatiquement avec les dernières modifications.

Pointez votre navigateur sur http: // localhost: 4000 / graphql pour obtenir le serveur GraphiQL. Vous pouvez toujours revenir ici et vous rafraîchir après avoir modifié du code sur le serveur pour voir le dernier schéma et tester vos requêtes.

 GraphiQL

Connect React to GraphQL

Vous devez ensuite connecter le frontend à GraphQL. J'utiliserai Bootstrap pour un style décent avec un minimum d'effort. Apollo est un excellent client React pouvant se connecter à n’importe quel serveur GraphQL. Pour installer les dépendances dont vous avez besoin pour l'interface, exécutez ce qui suit:

 yarn add bootstrap@4.1.3 reactstrap@6.4.0 apollo-boost@0.1.16 react-apollo@2.1.11

Vous devez configurer le client Apollo pour savoir où se connecter au serveur. Créez un nouveau fichier src / apollo.js avec le code suivant:

 importez ApolloClient à partir de 'apollo-boost';

exporter par défaut nouvel ApolloClient ({
  uri: "http: // localhost: 4000 / graphql",
});

Pour que le composant Réaction d’Apollo Query puisse se connecter à l’aide du client, l’application entière doit être encapsulée dans un composant ApolloProvider . Vous voudrez également inclure le style pour Bootstrap et vous pourrez vous débarrasser du fichier index.css fourni avec Create React App. Apportez les modifications suivantes à votre fichier src / index.js :

 @@ -1,8 +1,17 @@
 import Réagir de 'réagir';
 importer ReactDOM de 'react-dom';
-import './index.css';
+ import {ApolloProvider} de 'react-apollo';
+
+ import 'bootstrap / dist / css / bootstrap.min.css';
 importer l'application depuis './App';
 importer registerServiceWorker à partir de './registerServiceWorker';
+ client d'importation de './apollo';

-ReactDOM.render (document.getElementById ('root'));
+ ReactDOM.render (
+ 
+ 
+ 
+ document.getElementById ('root')
+);
 serviceWorker.unregister ();
+ if (module.hot) module.hot.accept ();

Le module de module.hot.accept () n’est pas vraiment nécessaire, mais permet uniquement aux composants qui changent dans l’application de s’actualiser à mesure que vous les mettez à jour, plutôt que d’actualiser la page entière. De temps en temps, vous devrez peut-être actualiser uniquement pour réinitialiser l'état de l'application, mais en règle générale, le délai d'exécution est raccourci.

Créez un nouveau fichier src / PostViewer.js qui le remplacera. récupère les données et les rend dans un tableau:

 import Réagissez à partir de 'réagir';
importer le gql de 'graphql-tag';
importer {Query} de 'react-apollo';
importer {Table} à partir de 'reactstrap';

export const GET_POSTS = gql`
  requête GetPosts {
    des postes {
      identifiant
      auteur
      corps
    }
  }
`;

export default () => (
  
    {({loading, data}) =>! loading && (
      
           {data.posts.map (post => (
            
          )}}
        
Auteur Body
{post.author} {post.body}
)}   
)

Le composant Query nécessite une requête GraphQL. Dans ce cas, vous obtenez simplement tous les messages avec leur ID et les auteurs et body . Le composant Query requiert également une fonction de rendu en tant qu'unique enfant. Il fournit un état loading mais dans notre cas, nous ne montrerons rien pendant le chargement, car il sera très rapide de récupérer les données localement. Une fois le chargement terminé, la variable data sera un objet contenant les données demandées.

Le code ci-dessus rend un tableau (. Le tableau est un composant qui inclut toutes les classes Bootstrap. avec tous les articles.

Vous devez maintenant modifier votre fichier src / App.js pour inclure le composant PostViewer que vous venez de créer. Cela devrait ressembler à ceci:

 import React, {Component} from 'react';

importer PostViewer à partir de './PostViewer';

La classe App étend le composant {
  render () {
    revenir (
      
);   } } exportation par défaut App;

Maintenant, si vous allez à http: // localhost: 3000 vous devriez voir ceci:

 tableau brut

Ajoutez la possibilité d’éditer des articles dans GraphQL

dans GraphQL , une requête est généralement en lecture seule. Si vous souhaitez modifier des données, utilisez plutôt la mutation

Créez un nouveau type de Mutation dans votre schéma const en . ] src / server / index.js pour envoyer un message. Vous pouvez créer un type d'entrée pour simplifier vos variables d'entrée. La nouvelle mutation devrait rendre le nouveau Post en cas de succès:

 de type Mutation {
  submitPost (entrée: PostInput!): Post
}

entrée PostInput {
  J'ai fait
  auteur: String!
  corps: String!
}

Vous devez également mettre à jour votre variable racine pour créer un nouveau résolveur pour submitPost . Ajoutez le résolveur suivant:

 submitPost: ({input: {id, author, body}}) => {
  const post = {author, body};
  let index = POSTS.length;

  if (id! = null && id> = 0 && id <POSTS.length) {
    if (POSTS [id] .authorId! == authorId) renvoie null;

    POSTS.splice (id, 1, post);
    index = id;
  } autre {
    POSTS.push (post);
  }

  retour mapPost (post, index);
},

Si vous fournissez un id il essaiera de trouver le poste correspondant à cet index et remplacera les données par les corps de auteur et fourni . Sinon, un nouveau message sera ajouté. Ensuite, il renvoie le message que vous avez fourni avec le nouveau id . Lorsque vous envoyez une demande de mutation à GraphQL, vous pouvez définir les éléments à récupérer:

 submit post

Pour le frontal, vous devez créer un nouveau composant pour l'édition de posts. Les formulaires dans React peuvent être facilités par une bibliothèque appelée Final Form . Installez-le avec le fil :

 fil ajouté final-form@4.10.0 react-final-form@3.6.5

Créez maintenant un nouveau fichier src / PostEditor.js et remplissez-le avec le texte suivant (je l’expliquerai plus en détail ci-dessous):

 import React from 'react';
importer le gql de 'graphql-tag';
importer {
  Bouton,
  Forme,
  FormGroup,
  Étiquette,
  Modal,
  ModalHeader,
  ModalBody,
  ModalFooter,
} de 'reactstrap';
importer {Form comme FinalForm, Field} à partir de 'react-final-form';

client d'importation depuis './apollo';
importer {GET_POSTS} de './PostViewer';

const SUBMIT_POST = gql`
  mutation SubmitPost ($ input: PostInput!) {
    submitPost (input: $ input) {
      identifiant
    }
  }
`;

const PostEditor = ({post, onClose}) => (
   {
      const input = {id, author, body};

      wait client.mutate ({
        variables: {entrée},
        mutation: SUBMIT_POST,
        refetchQueries: () => [{ query: GET_POSTS }],
      });

      onClose ();
    }}
    initialValues ​​= {post}
    render = {({handleSubmit, vierge, invalide}) => (
      
        
{post.id? 'Modifier le message': 'Nouveau message'}           
)}   /> ) défaut d'exportation PostEditor;

La ​​mutation submitPost est la nouvelle mutation à connecter au serveur. Il peut utiliser le type PostInput défini dans le serveur:

 const SUBMIT_POST = gql`
  mutation SubmitPost ($ input: PostInput!) {
    submitPost (input: $ input) {
      identifiant
    }
  }
`;

Final Form prend une fonction onSubmit qui transmet les données saisies par l'utilisateur. Une fois le message soumis, vous voudrez fermer le modal, donc PostEditor prend un onClose accessoire à appeler lorsque vous avez terminé.

Final Form prend également un message. initialValues ​​ objet pour définir les valeurs que la forme doit initialement avoir. Dans ce cas, le composant PostEditor prendra un accessoire post contenant les variables dont vous avez besoin, de sorte qu'il soit transmis comme valeur initiale.

L'autre élément requis est la fonction render qui rendra la forme. Final Form vous donne quelques accessoires de formulaire utiles pour vous permettre de savoir si le formulaire est valide ou non, ou s'il a été modifié à partir des initialValues ​​.

 const PostEditor = ({post, onClose}) = > (
  
)

défaut d'exportation PostEditor;

Dans la fonction onSubmit vous appelez la mutation requise pour soumettre le message. Apollo vous permet de récupérer des requêtes. Comme vous savez que votre liste de publications sera périmée une fois que vous aurez soumis les modifications, vous pouvez extraire à nouveau la requête GET_POSTS ici.

 onSubmit = {async ({id, author, body}) = > {
  const input = {id, author, body};

  wait client.mutate ({
    variables: {entrée},
    mutation: SUBMIT_POST,
    refetchQueries: () => [{ query: GET_POSTS }],
  });

  onClose ();
}}

La ​​fonction render affichera un modal Bootstrap. Ce composant PostEditor ne sera rendu que si vous souhaitez qu'il soit ouvert. Par conséquent, estOpen est défini sur true . Ici, vous utilisez également l'outil onClose pour fermer le modal lorsque l'utilisateur clique en dehors de celui-ci, clique sur Esc ou clique sur le bouton Annuler.

Le formulaire doit comporter le caractère La fonction handleSubmit lui a été transmise sous la forme de SubSmmit prop. Cela indique au formulaire de passer par le formulaire final au lieu d'envoyer une demande POST à la page.

Le formulaire final traite également tout le bricolage nécessaire pour avoir une entrée contrôlée . Au lieu de stocker les données dans l'état chaque fois que l'utilisateur tape quelque chose, vous pouvez simplement utiliser le composant Field .

 render = {({handleSubmit, Pristine, invalid}) => (
  
    
{post.id? 'Modifier le message': 'Nouveau message'}       
)}

Ensuite, vous devrez apporter quelques petites modifications à votre PostViewer . Cela ajoute un crochet à chaque ligne afin que vous puissiez déterminer si la ligne doit être modifiable ou non et, le cas échéant, modifie un peu les styles et vous permet de cliquer sur la ligne. Un clic sur la ligne appelle un autre rappel, que vous pouvez utiliser pour définir la publication à modifier.

 diff --git a / src / PostViewer.js b / src / PostViewer.js
index 5c53b5a..84177e0 100644
--- a / src / PostViewer.js
+++ b / src / PostViewer.js
@@ -13,7 +13,11 @@ export const GET_POSTS = gql`
   }
 `;

-export default () => (
+ const rowStyles = (post, canEdit) => canEdit (post)
+? {curseur: 'pointeur', fontWeight: 'gras'}
+: {};
+
+ const PostViewer = ({canEdit, onEdit}) => (
   
     {({loading, data}) =>! loading && (
       
@@ -25,7 +29,11 @@ export default () => (
         
            {data.posts.map (post => (
- 
+  canEdit (post) && onEdit (post)}
+>
               
 @@ -35,3 +43,10 @@ export default () => (
     )}
   
 )
+
+ PostViewer.defaultProps = {
+ canEdit: () => false,
+ onEdit: () => null,
+};
+
+ export PostViewer par défaut;

Maintenant, liez tout cela ensemble dans src / App.js . Vous pouvez créer un bouton «Nouvelle publication» pour créer une nouvelle publication et lui permettre de modifier également toute autre publication existante:

 import React, {Component} de 'react';
importer {Button, Container} de 'reactstrap';

importer PostViewer à partir de './PostViewer';
importer PostEditor à partir de './PostEditor';

La classe App étend le composant {
  state = {
    édition: null,
  };

  render () {
    const {édition} = this.state;

    revenir (
      
        
          true}
          onEdit = {(post) => this.setState ({édition: post})}
        />
        {édition && (
           this.setState ({édition: null})}
          />
        )}
      
    )
  }
}

exportation par défaut App;

Ajouter une authentification d'utilisateur à l'application Web React + GraphQL

Okta est un moyen simple d'ajouter une authentification à votre projet. Okta est un service en nuage qui permet aux développeurs de créer, éditer et stocker en toute sécurité des comptes d'utilisateurs et des données de comptes d'utilisateurs, et de les connecter à une ou plusieurs applications. Si vous n'en avez pas déjà un, inscrivez-vous pour un compte de développeur gratuit pour toujours . Connectez-vous à votre console de développeur, accédez à Applications puis cliquez sur Ajouter une application . Sélectionnez Application unique puis cliquez sur Suivant .

Puisque Create React App s'exécute sur le port 3000 par défaut, vous devez l'ajouter en tant qu'URI de base et URI de redirection de connexion. Vos paramètres devraient ressembler à ce qui suit:

 créer de nouveaux paramètres d'application

Cliquez sur Terminé pour enregistrer votre application, puis copiez votre ID client et collez-le. il en fait une variable dans un fichier nommé .env.local à la racine de votre projet. Cela vous permettra d'accéder au fichier dans votre code sans avoir besoin de stocker les informations d'identification dans le contrôle de source. Vous devrez également ajouter l'URL de votre organisation (sans le suffixe -admin ). Les variables d'environnement (autres que NODE_ENV ) doivent commencer par REACT_APP_ pour que Create React App puisse les lire, ainsi le fichier devrait ressembler à ceci:

.env.local

 REACT_APP_OKTA_CLIENT_ID = {votreClientId}
REACT_APP_OKTA_ORG_URL = https: // {yourOktaDomain}

Vous aurez également besoin d'un jeton d'API plus tard pour le serveur. Par conséquent, accédez à API -> Jetons puis cliquez sur . ] Créer un jeton . Vous pouvez avoir plusieurs jetons. Donnez simplement à celui-ci un nom qui vous rappelle à quoi il sert, comme «GraphQL Express». Vous recevrez un jeton que vous ne pouvez voir que pour le moment. Si vous perdez le jeton, vous devrez en créer un autre. Ajoutez également ceci à .env également.

 REACT_APP_OKTA_TOKEN = {yourOktaAPIToken}

Le moyen le plus simple d’ajouter l’authentification avec Okta à une application React consiste à utiliser le React SDK d’Okta. Vous devez également ajouter des itinéraires, ce qui peut être effectué à l'aide du fil React Router .

 add @ okta / okta-react @ 1.1.1 react-router-dom@4.3.1

Pour savoir si l'utilisateur est authentifié, Okta exige que l'application soit encapsulée dans un composant Security avec une configuration quelconque. Cela dépend également de React Router, vous allez donc vous retrouver avec un composant BrowserRouter enveloppant un composant Security enveloppant un composant ApolloProvider qui encapsule enfin votre App sur un itinéraire . Votre fichier src / index.js devrait ressembler à ceci:

 import Réagissez à partir de 'react';
importer ReactDOM de 'react-dom';
importer {BrowserRouter, Route} depuis 'react-router-dom';
importer {Security, ImplicitCallback} à partir de '@ okta / okta-react';
importer {ApolloProvider} de 'react-apollo';

importer 'bootstrap / dist / css / bootstrap.min.css';
importer l'application depuis './App';
importer registerServiceWorker à partir de './registerServiceWorker';
client d'importation depuis './apollo';

ReactDOM.render (
  
    
      
        
        
      
    
  
  document.getElementById ('root')
)
registerServiceWorker ();
if (module.hot) module.hot.accept ();

Le kit SDK d’Okta est livré avec un composant d’ordre supérieur withAuth qui peut être utilisé pour une grande variété de choses liées à l’autorisation, mais pour cet exemple, vous aurez seulement besoin de savoir si ou vous n'êtes pas authentifié, et quelques informations sur l'utilisateur. Pour rendre cela un peu plus facile, j'ai écrit une simple CdC pour remplacer celle fournie avec le SDK Okta. Créez un nouveau fichier src / withAuth.js contenant les éléments suivants:

 import Réagissez à partir de 'react';
import {withAuth} à partir de '@ okta / okta-react';

composant par défaut d'exportation => withAuth (la classe WithAuth étend React.Component {
  state = {
    ... this.props.auth,
    authentifié: null,
    utilisateur: null,
    chargement: vrai,
  };

  composantDidMount () {
    this.updateAuth ();
  }

  composantDidUpdate () {
    this.updateAuth ();
  }

  async updateAuth () {
    const authentifié = wait this.props.auth.isAuthenticated ();
    if (authentifié! == this.state.authenticated) {
      const user = wait this.props.auth.getUser ();
      this.setState ({authentifié, utilisateur, chargement: false});
    }
  }

  render () {
    const {auth, ... props} = this.props;
    retour ;
  }
});

En encapsulant un composant avec cette nouvelle fonction, votre application sera automatiquement restituée chaque fois qu'un utilisateur se connectera ou se déconnectera, et vous pourrez accéder aux informations qui le concernent.

Vous pouvez maintenant encapsuler le App avec ce avecAuth HoC. Pendant le bref moment où l'application se charge pour la première fois, Okta ne sait pas vraiment si un utilisateur est connecté ou non. Pour simplifier les choses, il suffit de ne rien restituer dans votre composant App pendant cette période de chargement. Vous pouvez toutefois choisir de rendre les articles et de désactiver l'édition jusqu'à ce que vous connaissiez plus d'informations sur l'utilisateur.

Tout en haut de votre fonction de rendu dans src / App.js ajoutez ce qui suit. :

 const {auth} = this.props;
if (auth.loading) renvoie null;

const {utilisateur, connexion, déconnexion} = auth;

Vous pouvez maintenant remplacer le bouton “Nouveau message” par le code suivant, qui affichera un bouton “Connexion” si vous n'êtes pas connecté. Si vous êtes connecté, vous verrez à la fois le “Nouveau Post ”que vous aviez auparavant, ainsi qu'un bouton“ Déconnexion ”. Cela fera en sorte que vous deviez être connecté pour créer un nouveau message.

 {utilisateur? (
  
): (    )}

Pour vous assurer que vous ne pouvez pas non plus modifier un message à moins que vous ne soyez connecté, modifiez le canEdit pour vérifier que vous avez bien un utilisateur.

 canEdit = {() => Boolean ( utilisateur)}

Vous devez également exporter avecAuth (App) au lieu de App . Votre fichier src / App.js devrait maintenant ressembler à ceci:

 import React, {Component} à partir de 'react';
importer {Button, Container} de 'reactstrap';

importer PostViewer à partir de './PostViewer';
importer PostEditor à partir de './PostEditor';
importer avecAuth de './withAuth';

La classe App étend le composant {
  state = {
    édition: null,
  };

  render () {
    const {aut}} = this.props;
    if (auth.loading) renvoie null;

    const {utilisateur, connexion, déconnexion} = auth;
    const {édition} = this.state;

    revenir (
      
        {utilisateur? (
          
): (                    )}          Boolean (utilisateur)}           onEdit = {(post) => this.setState ({édition: post})}         />         {édition && (            this.setState ({édition: null})}           />         )}       
    )   } } défaut d'exportation avecAuth (App);

Ajouter une authentification d'utilisateur au serveur

L'application Web nécessite désormais que vous soyez connecté pour créer une publication, mais un utilisateur averti peut toujours modifier les données en envoyant une demande directement à votre serveur. Pour empêcher cela, ajoutez une authentification au serveur. Vous devez ajouter le SDK d’Okta Node SDK et le vérificateur JWT en tant que dépendances. Vous devez également utiliser dotenv pour lire les variables de .env.local .

 warn add @ okta / jwt-verifier @ 0.0.12 @ okta / okta-sdk-nodejs@1.2.0 dotenv@6.0.0

Au sommet de votre fichier src / server / index.js vous devez indiquer à dotenv de lire dans les variables d'environnement:

 require ('dotenv ') .config ({path:' .env.local '});

Vous allez avoir besoin du frontal pour envoyer un jeton Web JSON (JWT) afin que les utilisateurs puissent s’identifier. Lorsque vous obtenez un JWT sur le serveur, vous devez le vérifier à l’aide du vérificateur JWT d’Okta. Pour obtenir plus d’informations sur un utilisateur, vous devez également utiliser le SDK d’Okta’s Node. Vous pouvez les configurer près du sommet de votre serveur, juste après toutes les autres instructions require .

 const {Client} = require ('@ okta / okta-sdk-nodejs');
const OktaJwtVerifier = require ('@ okta / jwt-verifier');

const oktaJwtVerifier = new OktaJwtVerifier ({
  clientId: process.env.REACT_APP_OKTA_CLIENT_ID,
  issuer: `$ {process.env.REACT_APP_OKTA_ORG_URL} / oauth2 / default`,
});

const client = new Client ({
  orgUrl: process.env.REACT_APP_OKTA_ORG_URL,
  jeton: process.env.REACT_APP_OKTA_TOKEN,
});

Maintenant que vous allez utiliser de vrais utilisateurs, il n’a pas beaucoup de sens de simplement envoyer une chaîne avec le nom d’utilisateur, d’autant plus que cela pourrait changer avec le temps. Ce serait mieux si une publication est associée à un utilisateur. Pour ce faire, créez une nouvelle variable AUTHORS pour vos utilisateurs et modifiez la variable POSTS pour avoir simplement un auteur au lieu d'un auteur chaîne:

 const AUTHORS = {
  1: {id: 1, name: "John Doe"},
  2: {id: 2, name: "Jane Doe"},
};

const POSTS = [
  { authorId: 1, body: "Hello world" },
  { authorId: 2, body: "Hi, planet!" },
];

Dans votre schéma, vous n'avez plus besoin de l'auteur : String saisi dans PostInput et de auteur sur Post devrait maintenant être de type Author au lieu de String . Vous devrez également créer ce nouveau type Author :

 type Author {
  J'ai fait
  nom: chaîne
}

Lorsque vous recherchez votre utilisateur, vous voulez maintenant extraire l’auteur de la variable AUTHORS :

 const mapPost = (post, id) => post && ({
  ...poster,
  identifiant,
  auteur: AUTEURS [post.authorId],
});

Vous devez maintenant créer une fonction getUserId permettant de vérifier le jeton d'accès et d'extraire des informations sur l'utilisateur. Le jeton sera envoyé comme en-tête Authorization et ressemblera à quelque chose comme Le porteur eyJraWQ ... 7h-zfqg . La fonction suivante ajoutera le nom de l'auteur à l'objet AUTHORS s'il n'existe pas déjà.

 const getUserId = async ({autorisation}) => {
  essayer {
    const accessToken = autorisation.trim (). split ('') [1];
    const {revendications: {uid}} = wait oktaJwtVerifier.verifyAccessToken (accessToken);

    si (! AUTEURS [uid]) {
      const {profile: {firstName, lastName}} = wait client.getUser (uid);

      AUTEURS [uid] = {
        id: uid,
        nom: [firstName, lastName] .filter (booléen) .join (''),
      };
    }

    retourner uid;
  } catch (error) {
    return null;
  }
};

Vous pouvez maintenant modifier la fonction submitPost pour obtenir l’identifiant de l’utilisateur lorsqu’il poste. Si l'utilisateur n'est pas connecté, vous pouvez simplement renvoyer null . Cela empêchera la publication de la publication. Vous pouvez également renvoyer null si l’utilisateur tente de modifier un message qu’il n’a pas créé.

 - submitPost: ({input: {id: {id, auteur, corps}}) => {
- const post = {author, body};
+ submitPost: async ({input: {id, body}}, {en-têtes}) => {
+ const authorId = wait getUserId (en-têtes);
+ if (! authorId) return null;
+
+ const post = {authorId, body};
     let index = POSTS.length;

     if (id! = null && id> = 0 && id <POSTS.length) {
+ if (POSTS [id] .authorId! == authorId) renvoie null;
+
       POSTS.splice (id, 1, post);
       index = id;
     } autre {

Votre fichier final src / server / index.js devrait maintenant ressembler à ceci:

 require ('dotenv'). Config ({chemin: '.env.local'});

const express = require ('express');
const cors = require ('cors');
const graphqlHTTP = require ('express-graphql');
const gql = require ('graphql-tag');
const { buildASTSchema } = require('graphql');
const { Client } = require('@okta/okta-sdk-nodejs');
const OktaJwtVerifier = require('@okta/jwt-verifier');

const oktaJwtVerifier = new OktaJwtVerifier({
  clientId: process.env.REACT_APP_OKTA_CLIENT_ID,
  issuer: `${process.env.REACT_APP_OKTA_ORG_URL}/oauth2/default`,
});

const client = new Client({
  orgUrl: process.env.REACT_APP_OKTA_ORG_URL,
  token: process.env.REACT_APP_OKTA_TOKEN,
});

const AUTHORS = {
  1: { id: 1, name: "John Doe" },
  2: { id: 2, name: "Jane Doe" },
};

const POSTS = [
  { authorId: 1, body: "Hello world" },
  { authorId: 2, body: "Hi, planet!" },
];

const schema = buildASTSchema(gql`
  type Query {
    posts: [Post]
    post(id: ID): Post
  }

  type Mutation {
    submitPost(input: PostInput!): Post
  }

  input PostInput {
    id: ID
    body: String
  }

  type Post {
    id: ID
    author: Author
    body: String
  }

  type Author {
    id: ID
    name: String
  }
`);

const mapPost = (post, id) => post && ({
  ...post,
  id,
  author: AUTHORS[post.authorId],
});

const getUserId = async ({ authorization }) => {
  try {
    const accessToken = authorization.trim().split(' ')[1];
    const { claims: { uid } } = await oktaJwtVerifier.verifyAccessToken(accessToken);

    if (!AUTHORS[uid]) {
      const { profile: { firstName, lastName } } = await client.getUser(uid);

      AUTHORS[uid] = {
        id: uid,
        name: [firstName, lastName].filter(Boolean).join(' '),
      };
    }

    return uid;
  } catch (error) {
    return null;
  }
};

const root = {
  posts: () => POSTS.map(mapPost),
  post: ({ id }) => mapPost(POSTS[id]id),
  submitPost: async ({ input: { id, body } }, { headers }) => {
    const authorId = await getUserId(headers);
    if (!authorId) return null;

    const post = { authorId, body };
    let index = POSTS.length;

    if (id != null && id >= 0 && id < POSTS.length) {
      if (POSTS[id].authorId !== authorId) return null;

      POSTS.splice(id, 1, post);
      index = id;
    } else {
      POSTS.push(post);
    }

    return mapPost(post, index);
  },
};

const app = express();
app.use(cors());
app.use('/graphql', graphqlHTTP({
  schema,
  rootValue: root,
  graphiql: true,
}));

const port = process.env.PORT || 4000
app.listen(port);
console.log(`Running a GraphQL API server at localhost:${port}/graphql`);

You’ll now need to make a few more frontend changes to make sure you’re requesting an author object instead of assuming it’s a string, and you’ll need to pass in your auth token as a header.

The PostViewer component will need a minor update

diff --git a/src/PostViewer.js b/src/PostViewer.js
index 84177e0..6bfddb9 100644
--- a/src/PostViewer.js
+++ b/src/PostViewer.js
@@ -7,7 +7,10 @@ export const GET_POSTS = gql`
   query GetPosts {
     posts {
       identifiant
-      author
+      author {
+        id
+        name
+      }
       corps
     }
   }
@@ -34,7 +37,7 @@ const PostViewer = ({ canEdit, onEdit }) => (
               style={rowStyles(post, canEdit)}
               onClick={() => canEdit(post) && onEdit(post)}
             >
-              
+ ))}

In PostEditor you’ll just need to get rid of the author altogether since that won’t be editable by the user, and will be determined by the auth token.

diff --git a/src/PostEditor.js b/src/PostEditor.js
index 182d1cc..6cb075c 100644
--- a/src/PostEditor.js
+++ b/src/PostEditor.js
@@ -25,8 +25,8 @@ const SUBMIT_POST = gql`

 const PostEditor = ({ post, onClose }) => (
    {
-      const input = { id, author, body };
+    onSubmit={async ({ id, body }) => {
+      const input = { id, body };

       await client.mutate({
         variables: { input },
@@ -44,15 +44,6 @@ const PostEditor = ({ post, onClose }) => (
             {post.id ? 'Edit Post' : 'New Post'}
           
           
-            
-              
-              
-            
             
               
               <Field

Your Apollo Client is where you’ll be sending the auth token. In order to access the auth token, you’ll need some sort of closure. On each request, Apollo lets you modify headers. Change src/apollo.js to the following:

import ApolloClient from 'apollo-boost';

let auth;

export const updateAuth = (newAuth) => {
  auth = newAuth;
};

export default new ApolloClient({
  uri: "http://localhost:4000/graphql",
  request: async (operation) => {
    const token = await auth.getAccessToken();
    operation.setContext({
      headers: {
        authorization: `Bearer ${token}`,
      },
    });
  },
});

Now you’ll need to call the updateAuth component whenever auth changes in src/withAuth.jsto make sure that’s always up to date.

diff --git a/src/withAuth.js b/src/withAuth.js
index cce1b24..6d29dcc 100644
--- a/src/withAuth.js
+++ b/src/withAuth.js
@@ -1,6 +1,8 @@
 import React from 'react';
 import { withAuth } from '@okta/okta-react';

+import { updateAuth } from './apollo';
+
 export default Component => withAuth(class WithAuth extends React.Component {
   state = {
     ...this.props.auth,
@@ -18,6 +20,8 @@ export default Component => withAuth(class WithAuth extends React.Component {
   }

   async updateAuth() {
+    updateAuth(this.props.auth);
+
     const authenticated = await this.props.auth.isAuthenticated();
     if (authenticated !== this.state.authenticated) {
       const user = await this.props.auth.getUser();

Now if you change canEdit in your src/App.js file once again, you can make it so users can only edit their own posts:

onChange={(post) => user && user.sub === post.author.id}

Learn more about GraphQL, React, Express and Web Security

You’ve now successfully built a GraphQL server, hooked it up to React, and locked it down with secure user authentication! As an exercise, see if you can switch the server from using simple, in-memory JavaScript objects to using a persistent data storage. For an example of using Sequelize in Node, check out Randall’s blog.

If you’d like to see the final sample code, you can find it on github.

If you’d like to learn more about GraphQL, Express, or React, check out some of these other posts on the Okta developer blog:

If you have any questions about this post, please add a comment below. For more awesome content, follow @oktadev on Twitter, like us on Facebookor subscribe to our YouTube channel.






Source link

{post.author} {post.body}
{post.author}{post.author.name} {post.body}