Cet article a été publié à l'origine sur le blog de développeurs Okta . Merci de soutenir les partenaires qui rendent SitePoint possible.
Je pense que vous allez aimer l'histoire que je vais vous raconter. Je vais vous montrer comment construire une API GraphQL avec un framework Vesper, TypeORM et MySQL. Ce sont des frameworks Node, et j'utiliserai TypeScript pour le langage. Pour le client, j'utiliserai React, reactstrap et Apollo Client pour communiquer avec l'API. Une fois que cet environnement fonctionne et que vous ajoutez une authentification d'utilisateur sécurisée, je pense que vous allez adorer cette expérience!
Pourquoi se concentrer sur une authentification sécurisée? Eh bien, mis à part le fait que je travaille pour Okta, je pense que nous pouvons tous convenir que pratiquement toutes les applications dépendent d'un système de gestion des identités sécurisé. Pour la plupart des développeurs qui construisent des applications React, il faut décider entre lancer votre propre authentification / autorisation ou brancher un service comme Okta. Avant de me lancer dans la construction d'une application React, je voudrais vous parler un peu d'Okta et de la raison pour laquelle je pense que c'est une excellente solution pour tous les développeurs JavaScript.
Qu'est-ce qu'Okta?
la gestion des identités est beaucoup plus facile, plus sûre et plus évolutive que ce à quoi vous êtes habitué. Okta est un service cloud qui permet aux développeurs de créer, modifier et stocker en toute sécurité des comptes utilisateur et des données de compte utilisateur, et de les connecter à une ou plusieurs applications. Notre API vous permet de:
Êtes-vous vendu? Inscrivez-vous pour un compte de développeur en permanence et, une fois terminé, revenez pour en savoir plus sur la création d'applications sécurisées dans React!
Pourquoi une application de suivi de la santé?
fin septembre jusqu'à la mi-octobre 2014, j'ai fait une cure de 21 jours de désintoxication au sucre au cours de laquelle j'ai cessé de manger du sucre, j'ai commencé à faire de l'exercice régulièrement et j'ai cessé de boire de l'alcool. J'ai eu une pression artérielle élevée pendant plus de dix ans et je prenais des médicaments pour la tension artérielle à ce moment-là. Au cours de la première semaine de désintoxication, je n'ai plus eu de médicaments contre l'hypertension. Comme une nouvelle ordonnance nécessitait une visite chez le médecin, j'ai décidé d'attendre la désintoxication pour l'obtenir. Après trois semaines, non seulement j'ai perdu 15 livres, mais ma tension artérielle était à des niveaux normaux!
Avant de commencer la désintoxication, j'ai trouvé un système de 21 points pour voir comment j'étais en bonne santé chaque semaine. Ses règles étaient simples: vous pouvez gagner jusqu'à trois points par jour pour les raisons suivantes:
- Si vous mangez sainement, vous avez raison.
- Si vous ne consommez pas d'alcool, vous avez raison.
J'ai été surpris de constater que j'ai eu huit points la première semaine où j'ai utilisé ce produit. système. Pendant la désintoxication, j'ai eu 16 points la première semaine, 20 le deuxième et 21 le troisième. Avant la désintoxication, je pensais que manger sainement signifiait manger n'importe quoi, sauf la restauration rapide. Après la désintoxication, j'ai réalisé que manger sain pour moi signifiait ne pas manger de sucre. Je suis aussi un grand amateur de bière artisanale, alors j'ai modifié la règle de l'alcool pour permettre deux boissons alcoolisées plus saines (comme un lévrier ou un vin rouge) par jour.
Mon objectif est de gagner 15 points par semaine. Je trouve que si je reçois plus, je vais probablement perdre du poids et avoir une bonne tension artérielle. Si j'ai moins de 15 ans, je risque de tomber malade. J'ai suivi ma santé comme celle-ci depuis septembre 2014. J'ai perdu du poids et ma tension artérielle est revenue à la normale et a maintenu des niveaux normaux. Je n'ai pas eu une bonne tension artérielle depuis le début de la vingtaine, ce qui a changé ma vie.
J'ai construit 21 points Health pour suivre ma santé. J'ai pensé qu'il serait amusant de recréer une petite partie de cette application, en suivant uniquement les points quotidiens.
Construire une API avec TypeORM, GraphQL et Vesper
TypeORM est un ORM astucieux Framework qui peut s'exécuter sur la plupart des plates-formes JavaScript, y compris Node, un navigateur, Cordova, React Native et Electron. Il est fortement influencé par Hibernate, Doctrine et Entity Framework. Installez TypeORM globalement pour commencer à créer votre API.
npm i -g typeorm@0.2.7
Créez un répertoire contenant le client React et l'API GraphQL.
Suivi de santé mkdir
cd traqueur de santé
Créez un nouveau projet avec MySQL en utilisant la commande suivante:
typeorm init --name graphql-api --database mysql
Modifiez graphql-api / ormconfig.json
pour personnaliser le nom d'utilisateur, le mot de passe et la base de données.
{
...
"nom d'utilisateur": "santé",
"mot de passe": "pointstest",
"base de données": "points de vie",
...
}
ASTUCE: Pour voir les requêtes en cours d'exécution sur MySQL, modifiez la valeur "logging" de ce fichier pour qu'elle soit "all". De nombreuses autres options de journalisation sont également disponibles
Installer MySQL
Installez MySQL si vous ne l'avez pas déjà installé. Sur Ubuntu, vous pouvez utiliser sudo apt-get install mysql-server
. Sur macOS, vous pouvez utiliser Homebrew et brew install mysql
. Pour Windows, vous pouvez utiliser le MySQL Installer .
Une fois MySQL installé et configuré avec un mot de passe root, connectez-vous et créez une base de données healthpoints
.
mysql -u root -p
créer des points de vie de base de données;
utiliser des points de vie;
accorder tous les privilèges sur *. * à 'health' @ 'localhost' identifié par 'points';
Accédez au projet graphql-api
dans une fenêtre de terminal, installez les dépendances du projet, puis démarrez-le pour vous assurer que vous pouvez vous connecter à MySQL.
cd graphql-api
npm i
npm start
La sortie suivante devrait apparaître:
Insertion d'un nouvel utilisateur dans la base de données ...
Enregistré un nouvel utilisateur avec l'ID: 1
Chargement des utilisateurs depuis la base de données ...
Utilisateurs chargés: [ User { id: 1, firstName: 'Timber', lastName: 'Saw', age: 25 } ]
Ici, vous pouvez configurer et exécuter express / koa / tout autre framework.
Installation de Vesper pour intégrer TypeORM et GraphQL
Vesper est une structure de noeud qui intègre TypeORM et GraphQL. Pour l'installer, utilisez le bon vieux npm.
npm i vesper@0.1.9
Il est maintenant temps de créer des modèles GraphQL (qui définissent l'apparence de vos données) et certains contrôleurs (qui expliquent comment interagir avec vos données).
Créer graphql-api / src / schema / model /Points.graphql
:
type Points {
id: Int
date: date
exercice: Int
régime: Int
alcool: Int
notes: String
utilisateur: utilisateur
}
Créer graphql-api / src / schema / model / User.graphql
:
tapez User {
id: String
firstName: String
lastName: String
points: [Points]
}
Ensuite, créez un graphql-api / src / schema / controller / PointsController.graphql
avec des requêtes et des mutations:
type Query {
points: [Points]
pointsGet (id: Int): Points
utilisateurs: [User]
}
type Mutation {
pointsEnregistrer (id: Int, date: Date, exercice: Int, régime: Int, alcool: Int, notes: String): Points
pointsDelete (id: Int): Booléen
}
Maintenant que vos données ont des métadonnées GraphQL, créez des entités qui seront gérées par TypeORM. Modifiez src / entity / User.ts
pour avoir le code suivant permettant d'associer des points à un utilisateur.
import {Colonne, Entité, OneToMany, PrimaryColumn} de 'typeorm';
import {Points} de './Points';
@Entité()
classe d'exportation User {
@PrimaryColumn ()
id: string;
@Colonne()
firstName: string;
@Colonne()
lastName: string;
@OneToMany (() => Points, points => points.user)
points: Points [];
}
Dans le même répertoire src / entity
créez une classe Points.ts
avec le code suivant.
import {Entity, PrimaryGeneratedColumn, Column, ManyToOne} from 'typeorm ';
import {User} de './User';
@Entité()
classe d'exportation Points {
@PrimaryGeneratedColumn ()
id: numéro;
@ Column ({type: 'timestamp', par défaut: () => 'CURRENT_TIMESTAMP'})
date: date;
@Colonne()
exercice: nombre;
@Colonne()
régime alimentaire: nombre;
@Colonne()
alcool: numéro;
@Colonne()
notes: chaîne;
@ManyToOne (() => User, user => user.points, {cascade: ["insert"]})
user: User | null;
}
Notez la cascade : option ["insert"]
sur l'annotation @ManyToOne
ci-dessus. Cette option insère automatiquement un utilisateur s'il est présent sur l'entité. Créez src / controller / PointsController.ts
pour gérer la conversion des données à partir de vos requêtes et mutations GraphQL.
import {Contrôleur, mutation, requête} depuis "vesper";
import {EntityManager} à partir de 'typeorm';
import {Points} de '../entity/Points';
@Manette()
classe d'exportation PointsController {
constructeur (private entityManager: EntityManager) {
}
// sert "points: [Points]" demandes
@Question()
points() {
retourne this.entityManager.find (Points);
}
// sert "pointsGet (id: Int): points" requêtes
@Question()
pointsGet ({id}) {
retourne this.entityManager.findOne (Points, id);
}
// sert "pointsEnregistrer (id: Int, date: Date, exercice: Int, régime: Int, alcool: Int, notes: Chaîne): Points" demandes
@Mutation()
pointsSave (args) {
const points = this.entityManager.create (Points, args);
retourne this.entityManager.save (Points, points);
}
// sert les requêtes "pointsDelete (id: Int): Boolean"
@Mutation()
async pointsDelete ({id}) {
wait this.entityManager.remove (Points, {id: id});
retourne vrai;
}
}
Modifiez src / index.ts
pour utiliser Vesper bootstrap ()
pour tout configurer.
import {bootstrap} from 'vesper';
importer {PointsController} depuis './controller/PointsController';
import {Points} de './entity/Points';
import {User} depuis './entity/User';
bootstrap ({
port: 4000,
contrôleurs: [
PointsController
],
entités: [
Points,
User
],
schémas: [
__dirname + '/schema/**/*.graphql'
],
cors: vrai
}). alors (() => {
console.log ('Votre application fonctionne sur http: // localhost: 4000.' +
«Vous pouvez utiliser le terrain de jeu en mode développement sur http: // localhost: 4000 / playground»);
}). catch (error => {
console.error (error.stack? error.stack: erreur);
})
Ce code indique à Vesper d'enregistrer les contrôleurs, les entités, les schémas GraphQL, pour s'exécuter sur le port 4000 et d'activer le partage de ressources d'origine croisée.
Lancez votre API à l'aide de accédez à http: // localhost: 4000 / playground. Dans le volet gauche, entrez la mutation suivante et appuyez sur le bouton de lecture. Vous pourriez essayer de taper le code ci-dessous pour que vous puissiez voir le code que GraphQL vous fournit.
mutation {
pointsEnregistrer (exercice: 1, régime alimentaire: 1, alcool: 1, notes: "Hello World") {
id
rendez-vous amoureux
exercice
régime
de l'alcool
Remarques
}
}
Votre résultat devrait ressembler au mien.
Vous pouvez cliquer sur l'onglet "SCHEMA" à droite pour voir les requêtes et les mutations disponibles. Pretty slick, hein?!
Vous pouvez utiliser la requête points
suivante pour vérifier que les données se trouvent dans votre base de données.
query {
points {date de l'exercice, exercice de notes de régime}
}
Fix Dates
Vous remarquerez peut-être que la date renvoyée par pointsSave
et points
est dans un format difficile à comprendre par un client JavaScript. Vous pouvez résoudre ce problème en installant graphql-iso-date .
npm i graphql-iso-date@3.5.0
Ensuite, ajoutez une importation dans src / index.ts
et configurez des résolveurs personnalisés pour les différents types de date. Cet exemple n'utilise que Date
mais il est utile de connaître les autres options.
import {GraphQLDate, GraphQLDateTime, GraphQLTime} à partir de 'graphql-iso-date';
bootstrap ({
...
// https://github.com/vesper-framework/vesper/issues/4
customResolvers: {
Date: GraphQLDate,
Heure: GraphQLTime,
DateTime: GraphQLDateTime
},
...
})
L'exécution de la requête points
renverra un résultat plus convivial pour le client.
{
"Les données": {
"points": [
{
"id": 1,
"date": "2018-06-04",
"exercise": 1,
"diet": 1,
"notes": "Hello World"
}
]
}
}
Vous avez écrit une API avec GraphQL et TypeScript en 20 minutes environ. À quel point cela est cool?! Il y a encore du travail à faire cependant. Dans les sections suivantes, vous allez créer un client React pour cette API et ajouter une authentification avec OIDC. L'ajout de l'authentification vous donnera la possibilité d'obtenir les informations de l'utilisateur et d'associer un utilisateur à ses points.
Premiers pas avec React
L'un des moyens les plus rapides de démarrer React est d'utiliser Create React App . Installez la dernière version à l'aide de la commande ci-dessous.
npm i -g create-react-app@1.1.4
Accédez au répertoire dans lequel vous avez créé votre API GraphQL et créez un client React.
cd health-tracker
create-react-app react-client
Installez les dépendances dont vous aurez besoin pour intégrer Apollo Client avec React, ainsi que Bootstrap et reactstrap .
npm i apollo-boost@0.1.7 react-apollo@2.1.4 graphql-tag@2.9.2 graphql@0.13.2
Configurez le client Apollo pour votre API
Ouvrez react-client / src / App.js
et importez ApolloClient
à partir de apollo-boost
et ajoutez le Endpoint à votre API GraphQL.
importez ApolloClient de 'apollo-boost';
const client = new ApolloClient ({
uri: "http: // localhost: 4000 / graphql"
})
C'est ça! Avec seulement trois lignes de code, votre application est prête à récupérer des données. Vous pouvez le prouver en important la fonction gql
de graphql-tag
. Cela analysera votre chaîne de requête et la transformera en un document de requête.
importez gql depuis 'graphql-tag';
classe App étend le composant {
componentDidMount () {
client.query ({
requête: gql`
{
points {
id date exercice diète alcool notes
}
}
`
})
.then (resultat => console.log (resultat));
}
...
}
Assurez-vous d'ouvrir les outils de développement de votre navigateur pour pouvoir voir les données après avoir effectué cette modification. Vous pouvez modifier le console.log ()
pour utiliser this.setState ({points: results.data.points})
mais vous devez initialiser l’état par défaut dans le constructeur. Mais il existe un moyen plus simple: vous pouvez utiliser les composants ApolloProvider
et Requête
de
react-apollo
!
Voici une version modifiée de client / src / App.js
qui utilise ces composants.
import React, {Component} from 'reaction';
importer le logo de './logo.svg';
import './App.css';
importer ApolloClient de 'apollo-boost';
importer gql depuis 'graphql-tag';
import {ApolloProvider, Query} de 'react-apollo';
const client = new ApolloClient ({
uri: "http: // localhost: 4000 / graphql"
})
classe App étend le composant {
render () {
revenir (
Bienvenue dans React
Pour commencer, éditez src / App.js
et sauvegardez pour recharger.
{({chargement, erreur, données}) => {
si (chargement) retour Chargement en cours ...
;
if (erreur) retourne Erreur: {erreur}
;
return data.points.map (p => {
retour
Date: {p.date}
Points: {p.exercise + p.diet + p.alcohol}
Notes: {p.notes}
})
}}
);
}
}
exportation App par défaut;
Vous avez construit une API GraphQL et une interface utilisateur React qui lui parlent - un excellent travail! Cependant, il reste encore beaucoup à faire. Dans les sections suivantes, je vais vous montrer comment ajouter une authentification à React, vérifier les JWT avec Vesper et ajouter des fonctionnalités CRUD à l'interface utilisateur. La fonctionnalité CRUD existe déjà dans l'API grâce aux mutations écrites précédemment.
Ajouter une authentification pour React avec OpenID Connect
Vous devez configurer React pour utiliser Okta pour l'authentification. Pour cela, vous devez créer une application OIDC.
Connectez-vous à votre compte Okta Developer (ou inscrivez-vous si vous ne possédez pas de compte) et accédez à Applications. > Ajouter une demande . Cliquez sur Single-Page App cliquez sur Suivant et attribuez à l'application un nom dont vous vous souviendrez. Modifiez toutes les instances de localhost: 8080
à localhost: 3000
et cliquez sur Terminé . Vos paramètres doivent être similaires à ceux de la capture d'écran ci-dessous.
Le kit SDK React d'Okta vous permet d'intégrer OIDC dans une application React. Pour installer, exécutez les commandes suivantes:
npm i @ okta / okta-react @ 1.0.2 react-router-dom@4.2.2
Le Reak SDK d'Okta dépend de react-router d'où la raison de l'installation de react-router-dom
. Configurer le routage dans client / src / App.tsx
est une pratique courante. Remplacez donc son code par le code JavaScript ci-dessous qui configure l'authentification avec Okta.
import React, {Component} from 'Reaction';
import {BrowserRouter as Router, Route} de "react-router-dom";
import {{ImplicitCallback, SecureRoute, Security} depuis '@ okta / okta-react';
importer Home de './Home';
importation Connexion depuis './Login';
importer des points à partir de «./Points»;
fonction onAuthRequired ({history}) {
history.push ('/ login');
}
classe App étend le composant {
render () {
revenir (
} />
);
}
}
exportation App par défaut;
Assurez-vous de remplacer {yourOktaDomain}
et {clientId}
dans le code ci-dessus. Vous pouvez trouver les deux valeurs dans la console de développeur Okta.
Le code dans App.js
fait référence à deux composants qui n'existent pas encore: Accueil
Connexion
]et Points
. Créez src / Home.js
avec le code suivant. Ce composant restitue la route par défaut, fournit un bouton de connexion et des liens vers vos points et déconnexion après vous être connecté.
import React, {Component} from 'reaction';
import {withAuth} depuis '@ okta / okta-react';
import {Button, Container} de 'reactstrap';
importer AppNavbar depuis './AppNavbar';
import {Link} de 'react-router-dom';
exportation par défaut avecAuth (la classe Home étend Component {
constructeur (accessoires) {
super (accessoires);
this.state = {authentifié: null, userinfo: null, isOpen: false};
this.checkAuthentication = this.checkAuthentication.bind (this);
this.checkAuthentication ();
this.login = this.login.bind (this);
this.logout = this.logout.bind (this);
}
async checkAuthentication () {
const authentifié = wait this.props.auth.isAuthenticated ();
if (authentifié! == this.state.authenticated) {
if (authentifié &&! this.state.userinfo) {
const userinfo = wait this.props.auth.getUser ();
this.setState ({authentifié, userinfo});
} autre {
this.setState ({authentifié});
}
}
}
async componentDidMount () {
this.checkAuthentication ();
}
async componentDidUpdate () {
this.checkAuthentication ();
}
async login () {
this.props.auth.login ('/');
}
déconnexion asynchrone () {
this.props.auth.logout ('/');
this.setState ({authentifié: null, userinfo: null});
}
render () {
if (this.state.authenticated === null) renvoie null;
bouton const = this.state.authenticated?
:
;
const message = this.state.userinfo?
Bonjour, {this.state.userinfo.given_name}!
:
Connectez-vous pour gérer vos points.
;
revenir (
{message}
{bouton}
);
}
})
Ce composant utilise et de reactstrap. Installez reactstrap, donc tout se compile. Cela dépend de Bootstrap, donc incluez-le aussi.
npm i reactstrap@6.1.0 bootstrap@4.1.1
Ajoutez le fichier CSS Bootstrap en tant qu'import dans src / index.js
.
import 'bootstrap / dist / css / bootstrap.min.css';
Vous remarquerez peut-être un dans la méthode Home
component render ()
. Créez src / AppNavbar.js
afin de pouvoir utiliser un en-tête commun entre les composants.
import React, {Component} from 'Reaction';
importer {Réduire, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink} à partir de 'reactstrap';
import {Link} de 'react-router-dom';
exportation classe par défaut AppNavbar étend Component {
constructeur (accessoires) {
super (accessoires);
this.state = {isOpen: false};
this.toggle = this.toggle.bind (this);
}
basculer () {
this.setState ({
isOpen:! this.state.isOpen
})
}
render () {
return
Accueil
;
}
}
Dans cet exemple, je vais incorporer le widget de connexion à Okta . Une autre option consiste à rediriger vers Okta et à utiliser une page de connexion hébergée. Installez le widget de connexion en utilisant npm.
npm i @ okta / okta-signin-widget @ 2.9.0
Créez src / Login.js
et ajoutez-y le code suivant:
import React, {Component} from 'Reaction';
import {Redirect} de 'react-router-dom';
importer OktaSignInWidget de './OktaSignInWidget';
import {withAuth} depuis '@ okta / okta-react';
exportation par défaut avecAuth (la classe Login étend le composant {
constructeur (accessoires) {
super (accessoires);
this.onSuccess = this.onSuccess.bind (this);
this.onError = this.onError.bind (this);
this.state = {
authentifié: null
};
this.checkAuthentication ();
}
async checkAuthentication () {
const authentifié = wait this.props.auth.isAuthenticated ();
if (authentifié! == this.state.authenticated) {
this.setState ({authentifié});
}
}
componentDidUpdate () {
this.checkAuthentication ();
}
onSuccess (res) {
retourne this.props.auth.redirect ({
sessionToken: res.session.token
})
}
onError (err) {
console.log ('erreur de connexion', err);
}
render () {
if (this.state.authenticated === null) retourne null;
renvoyer this.state.authenticated?
:
;
}
})
Le composant Login
fait référence à OktaSignInWidget
. Créez src / OktaSignInWidget.js
:
import React, {Component} from 'reaction';
importer ReactDOM à partir de 'réact-dom';
importer OktaSignIn depuis '@ okta / okta-signin-widget';
importer '@ okta / okta-signin-widget / dist / css / okta-sign-in.min.css';
importer '@ okta / okta-signin-widget / dist / css / okta-theme.css';
import './App.css';
classe d'export par défaut OktaSignInWidget extend Component {
componentDidMount () {
const el = ReactDOM.findDOMNode (this);
this.widget = new OktaSignIn ({
baseUrl: this.props.baseUrl
})
this.widget.renderEl ({el}, this.props.onSuccess, this.props.onError);
}
componentWillUnmount () {
this.widget.remove ();
}
render () {
retour ;
}
};
Créez src / Points.js
pour afficher la liste des points de votre API.
import React, {Component} from 'reaction';
importer {ApolloClient} de 'apollo-client';
import {createHttpLink} depuis 'apollo-link-http';
import {setContext} de 'apollo-link-context';
import {InMemoryCache} depuis 'apollo-cache-inmemory';
importer gql depuis 'graphql-tag';
import {withAuth} depuis '@ okta / okta-react';
importer AppNavbar depuis './AppNavbar';
import {Alert, Button, Container, Table} de 'reactstrap';
importer PointsModal à partir de './PointsModal';
export const httpLink = createHttpLink ({
uri: 'http: // localhost: 4000 / graphql'
})
exportation par défaut avecAuth (la classe Points étend le composant {
client;
constructeur (accessoires) {
super (accessoires);
this.state = {points: []erreur: null};
this.refresh = this.refresh.bind (this);
this.remove = this.remove.bind (this);
}
rafraîchir (item) {
let existing = this.state.points.filter (p => p.id === item.id);
laisser des points = [...this.state.points];
if (existing.length === 0) {
points.push (item);
this.setState ({points});
} autre {
this.state.points.forEach ((p, idx) => {
if (p.id === item.id) {
points [idx] = item;
this.setState ({points});
}
})
}
}
supprimer (élément, index) {
const deletePoints = gql`mutation pointsDelete ($ id: Int) {pointsDelete (id: $ id)} `;
this.client.mutate ({
mutation: deletePoints,
variables: {id: item.id}
}). alors (résultat => {
if (result.data.pointsDelete) {
let updatedPoints = [...this.state.points] .filter (i => i.id! == item.id);
this.setState ({points: updatedPoints});
}
})
}
componentDidMount () {
const authLink = setContext (async (_, {en-têtes}) => {
const token = wait this.props.auth.getAccessToken ();
const user = wait this.props.auth.getUser ();
// renvoie les en-têtes au contexte pour que httpLink puisse les lire
revenir {
en-têtes: {
... les en-têtes,
autorisation: jeton? `Porteur $ {jeton}`: '',
'x-forwarded-user': utilisateur? JSON.stringify (utilisateur): ''
}
}
})
this.client = new ApolloClient ({
link: authLink.concat (httpLink),
cache: new InMemoryCache (),
connectToDevTools: true
})
this.client.query ({
requête: gql`
{
points {
id,
utilisateur {
ID,
nom de famille
}
rendez-vous amoureux,
de l'alcool,
exercice,
régime,
Remarques
}
} `
}). alors (résultat => {
this.setState ({points: result.data.points});
}). catch (error => {
this.setState ({error: Échec de communication avec l'API. });
})
}
rendre () {
const {points, erreur} = this.state;
const pointsList = points.map (p => {
const total = p.exercise + p.diet + p.alcool;
return
<td className = {total {total}
{p.notes}
});
revenir (
{erreur}
Vos points
Date
Points
Notes
Actions
{pointsList}
);
}
})
Ce code commence par les méthodes refresh ()
et remove ()
auxquelles je vais accéder dans un instant. La partie importante se produit dans componentDidMount ()
où le jeton d'accès est ajouté dans un en-tête Authorization
et les informations de l'utilisateur sont stockées dans un x-forwarded-user
entête. Un ApolloClient
est créé avec ces informations, un cache est ajouté et l'indicateur connectToDevTools
est activé. Cela peut être utile pour le débogage avec Apollo Client Developer Tools .
componentDidMount () {
const authLink = setContext (async (_, {en-têtes}) => {
const token = wait this.props.auth.getAccessToken ();
// renvoie les en-têtes au contexte pour que httpLink puisse les lire
revenir {
en-têtes: {
... les en-têtes,
autorisation: jeton? `Porteur $ {jeton}`: '',
'x-forwarded-user': utilisateur? JSON.stringify (utilisateur): ''
}
}
})
this.client = new ApolloClient ({
link: authLink.concat (httpLink),
cache: new InMemoryCache (),
connectToDevTools: true
})
// this.client.query (...);
}
L'authentification avec Apollo Client nécessite quelques nouvelles dépendances. Installez-les maintenant.
npm apollo-link-context@1.0.8 apollo-link-http@1.5.4
Dans le JSX de la page, il existe un bouton de suppression qui appelle la méthode remove ()
dans Points
. Il y a aussi un composant . Ceci est référencé pour chaque élément, ainsi que dans le bas. Vous remarquerez ces deux références à la méthode refresh ()
qui met à jour la liste.
Ce composant affiche un lien pour modifier un composant ou un bouton Ajouter lorsque aucun élément
n'est défini.
Créez src / PointsModal.js
et ajoutez le code suivant à
import React, {Component} from 'reaction';
importer {Button, Form, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader} de 'reactstrap';
import {withAuth} depuis '@ okta / okta-react';
import {httpLink} à partir de "./Points";
importer {ApolloClient} de 'apollo-client';
import {setContext} de 'apollo-link-context';
import {InMemoryCache} depuis 'apollo-cache-inmemory';
importer gql depuis 'graphql-tag';
import {Link} de 'react-router-dom';
export par défaut avecAuth (la classe PointsModal étend le composant {
client;
emptyItem = {
date: (new Date ()). toISOString (). split ('T') [0],
exercice: 1,
régime alimentaire: 1,
alcool: 1,
Remarques: ''
};
constructeur (accessoires) {
super (accessoires);
this.state = {
modal: faux,
item: this.emptyItem
};
this.toggle = this.toggle.bind (this);
this.handleChange = this.handleChange.bind (this);
this.handleSubmit = this.handleSubmit.bind (this);
}
componentDidMount () {
si (this.props.item) {
this.setState ({item: this.props.item})
}
const authLink = setContext (async (_, {en-têtes}) => {
const token = wait this.props.auth.getAccessToken ();
const user = wait this.props.auth.getUser ();
// renvoie les en-têtes au contexte pour que httpLink puisse les lire
revenir {
en-têtes: {
... les en-têtes,
autorisation: jeton? `Porteur $ {jeton}`: '',
'x-forwarded-user': JSON.stringify (utilisateur)
}
}
})
this.client = new ApolloClient ({
link: authLink.concat (httpLink),
cache: nouveau InMemoryCache ()
})
}
basculer () {
si (this.state.modal &&! this.state.item.id) {
this.setState ({item: this.emptyItem});
}
this.setState ({modal:! this.state.modal});
}
render () {
const {item} = this.state;
const opener = item.id? {this.props.item.date} :
;
revenir (
{ouvreur}
{(item.id? 'Edit': 'Add')} Points
{''}
)
};
handleChange (event) {
const target = event.target;
const value = target.type === 'checkbox'? (target.checked? 1: 0): target.value;
const nom = target.name;
let item = {... this.state.item};
article [name] = valeur;
this.setState ({item});
}
handleSubmit (event) {
event.preventDefault ();
const {item} = this.state;
const updatePoints = gql`
points de mutationEnregistrer ($ id: Int, $ date: Date, $ exercice: Int, $ diet: Int, $ alcohol: Int, $ notes: String) {
pointsEnregistrer (id: $ id, date: $ date, exercice: $ exercice, régime: $ régime, alcool: $ alcool, notes: $ notes) {
date id
}
} `
this.client.mutate ({
mutation: updatePoints,
variables: {
id: item.id,
date: item.date,
exercice: item.exercise,
régime: item.diet,
alcool: article.alcool,
notes: item.notes
}
}). alors (résultat => {
let newItem = {... item};
newItem.id = result.data.pointsSave.id;
this.props.callback (newItem);
this.toggle ();
})
}
})
Assurez-vous que votre backend GraphQL est démarré, puis démarrez le frontend React avec npm start
. Le texte écrase la barre de navigation supérieure, ajoutez donc un remplissage en ajoutant une règle dans src / index.css
.
.container-fluid {
dessus rembourré: 10px;
}
Vous devriez voir le composant Page d'accueil
et un bouton pour vous connecter.
Cliquez sur Connexion pour entrer vos informations d'identification Okta.
Et vous serez connecté!
Cliquez Gérer les points pour voir la liste des points.
C'est cool de tout voir, n'est-ce pas?! : D
Votre interface React est sécurisée, mais votre API est toujours ouverte. Corrige ce problème.
Récupération des informations utilisateur des JWT
Accédez au projet graphql-api
dans une fenêtre de terminal et installez le vérificateur JWT d'Okta.
npm i @ okta / jwt-verifier @ 0.0.12
Créez graphql-api / src / CurrentUser.ts
pour contenir les informations de l'utilisateur actuel.
classe d'exportation CurrentUser {
constructeur (identifiant public: string, public firstName: string, public lastName: string) {}
}
Importez OktaJwtVerifier
et CurrentUser
dans graphql-api / src / index.ts
et configurez le vérificateur JWT pour utiliser les paramètres de votre application OIDC.
import * comme OktaJwtVerifier de '@ okta / jwt-verifier';
import {CurrentUser} depuis './CurrentUser';
const oktaJwtVerifier = new OktaJwtVerifier ({
clientId: '{clientId}',
issuer: 'https://{yourOktaDomain}/oauth2/default'
})
In the bootstrap configuration, define setupContainer
to require an authorization
header and set the current user from the x-forwarded-user
header.
bootstrap({
...
cors: true,
setupContainer: async (container, action) => {
const request = action.request;
// require every request to have an authorization header
if (!request.headers.authorization) {
throw Error('Authorization header is required!');
}
let parts = request.headers.authorization.trim().split(' ');
let accessToken = parts.pop();
await oktaJwtVerifier.verifyAccessToken(accessToken)
.then(async jwt => {
const user = JSON.parse(request.headers['x-forwarded-user'].toString());
const currentUser = new CurrentUser(jwt.claims.uid, user.given_name, user.family_name);
container.set(CurrentUser, currentUser);
})
.catch(error => {
throw Error('JWT Validation failed!');
})
}
...
})
Modify graphql-api/src/controller/PointsController.ts
to inject the CurrentUser
as a dependency. While you’re in there, adjust the points()
method to filter by user ID and modify pointsSave()
to set the user when saving.
import { Controller, Mutation, Query } from 'vesper';
import { EntityManager } from 'typeorm';
import { Points } from '../entity/Points';
import { User } from '../entity/User';
import { CurrentUser } from '../CurrentUser';
@Controller()
export class PointsController {
constructor(private entityManager: EntityManager, private currentUser: CurrentUser) {
}
// serves "points: [Points]" requests
@Query()
points() {
return this.entityManager.getRepository(Points).createQueryBuilder("points")
.innerJoin("points.user", "user", "user.id = :id", { id: this.currentUser.id })
.getMany();
}
// serves "pointsGet(id: Int): Points" requests
@Query()
pointsGet({id}) {
return this.entityManager.findOne(Points, id);
}
// serves "pointsSave(id: Int, date: Date, exercise: Int, diet: Int, alcohol: Int, notes: String): Points" requests
@Mutation()
pointsSave(args) {
// add current user to points saved
if (this.currentUser) {
const user = new User();
user.id = this.currentUser.id;
user.firstName = this.currentUser.firstName;
user.lastName = this.currentUser.lastName;
args.user = user;
}
const points = this.entityManager.create(Points, args);
return this.entityManager.save(Points, points);
}
// serves "pointsDelete(id: Int): Boolean" requests
@Mutation()
async pointsDelete({id}) {
await this.entityManager.remove(Points, {id: id});
return true;
}
}
Restart the API, and you should be off to the races!
Source Code
You can find the source code for this article here.
Learn More About React, Node, and User Authentication
This article showed you how to build a secure React app with GraphQL, TypeORM, and Node/Vesper. I hope you enjoyed the experience!
At Okta, we care about making authentication with React and Node easy to implement. We have several blog posts on the topic, and documentation too! I encourage you to check out the following links:
I hope you have an excellent experience building apps with React and GraphQL. If you have any questions, please hit me up on Twitter or my whole kick-ass team on @oktadev. Our DMs are wide open! 🙂
Source link