Objectifs Configuration initiale Installation des dépendances Commençons Tout d'abord, ajoutez le fichier tsconfig.json dans le répertoire principal : Maintenant, créons src/server.ts pour les implémentations des serveurs. Puis ajoutons deux fonctions : l'une pour le serveur local et l'autre pour le lambda. OK, nous n'avons pas de résolveurs ou de définitions de types, nous devons donc en créer. Supposons que, dans un premier temps, nous voulions [...]
Objectifs
Configurer les serveurs locaux et les serveurs lambda.
Connectez les deux à MongoDB.
Mettre en œuvre l'authentification de base.
Déployer Apollo sans serveur GraphQL API avec Netlify.
Utiliser Typescript .
Configuration initiale
npm init -y
Installer les dépendances
npm install --save typescript graphql aws-lambda @types/aws-lambda
Commençons
Tout d'abord, ajoutez le tsconfig.json
dans le répertoire principal :
{
"compilerOptions" : {
"target" : "es5",
"module" : "commonjs",
"allowJs" : true,
"strict" : true,
"esModuleInterop" : true,
"skipLibCheck" : true,
"forceConsistentCasingInFileNames" : true
},
"include" : ["src/*.ts", "src/**/*.ts", "src/**/*.js"],
"exclude" : ["node_modules"]
}
Maintenant, créons src/server.ts
pour les implémentations de serveurs. Ajoutez ensuite deux fonctions : l'une pour le serveur local et l'autre pour le serveur lambda.
// src/server.ts
import { ApolloServer as ApolloServerLambda } from "apollo-server-lambda" ;
import { ApolloServer } from "apollo-server" ;
const createLambdaServer = () =>
nouveau ApolloServerLambda({
typeDefs,
résolveurs,
introspection : true,
terrain de jeu : true,
},
}) ;
const createLocalServer = () =>
nouveau ApolloServer({
typeDefs,
résolveurs,
introspection : true,
terrain de jeu : true,
},
}) ;
export { createLambdaServer, createLocalServer } ;
OK, nous n'avons pas de résolveurs ou de définitions de types, nous devons donc en créer. Supposons que, dans un premier temps, nous voulions créer des utilisateurs et recevoir des informations à leur sujet.
// src/schemas.ts
const { gql } = require("apollo-server-lambda") ;
const userSchema = gql`
type User {
id : ID !
email : Chaîne !
name : Chaîne !
}
type Query {
user(id : ID !): Utilisateur !
}
type Mutation {
createUser(name : String !, email : String !, password : String !): Utilisateur !
}
` ;
Si vous n'êtes pas familier avec cela, Apollo a préparé un très beau tutoriel
Créons maintenant un résolveur d'utilisateurs avec une requête et une mutation.
// src/resolvers.ts
const userResolver = {
Query : {
user : async (parent, args, context, info) => {
{...}
},
},
Mutation : {
createUser : async (parent, args, context, info) => {
{...}
},
},
} ;
Mais nous n'avons pas de données... Corrigeons cela 😉
N'oubliez pas d'importer la définition de type et le résolveur sur le serveur.
// src/server.ts
import { ApolloServer as ApolloServerLambda } from "apollo-server-lambda" ;
import { ApolloServer } from "apollo-server" ;
import { typeDefs } from "./schemas" ;
import { resolvers } from "./resolvers" ;
{...}
Se connecter à MongoDB via mongoose
Il est maintenant temps de créer une connexion avec notre base de données. Dans ce cas particulier, il s'agit de MongoDB. C'est une base de données gratuite et facile à maintenir. Mais avant cela, installons deux autres dépendances :
npm install --save mongoose dotenv
La première étape consiste à créer un modèle d'utilisateur.
// src/model.ts
import mongoose, { Document, Error, Schema } from "mongoose" ;
export type User = Document & {
_id : string,
email : string,
name : chaîne de caractères,
password : chaîne de caractères,
} ;
supprimer mongoose.connection.models["User"] ;
const UserSchema : Schema = new Schema({
email : {
type : String,
requis : true,
unique : true,
},
name : {
type : Chaîne,
required : true,
minLength : 3,
maxLength : 32,
},
password : {
type : String,
required : true,
},
}) ;
export const userModel = mongoose.model ("User", UserSchema) ;
Rendre les mots de passe plus sûrs
La sécurité avant tout ! Sécurisons maintenant nos mots de passe en les hachant.
npm install --save bcrypt @types/bcrypt
Maintenant, implémentez la sécurité du mot de passe dans le callback du pre-middleware. Les fonctions pre-middleware sont exécutées l'une après l'autre, lors de l'appel suivant de chaque middleware. Pour sécuriser les mots de passe, nous utilisons une technique qui génère un sel et un hachage lors d'appels de fonction distincts.
// src/model.ts
import bcrypt from "bcrypt" ;
{...}
const SALT_WORK_FACTOR : nombre = 10 ;
UserSchema.pre("save", function (next) {
const user = this as User ;
if (!this.isModified("password")) return next() ;
bcrypt.genSalt(SALT_WORK_FACTOR, function (err, salt) {
if (err) return next(err) ;
bcrypt.hash(user.password, salt, function (err, hash) {
if (err) return next(err) ;
user.password = hash ;
next() ;
}) ;
}) ;
}) ;
{...}
Ajoutez ensuite la méthode comparePasswords à UserSchema :
// src/model.ts
{...}
UserSchema.methods.comparePasswords = function (
candidatePassword : string,
cb : (err : Error | null, same : boolean | null) => void
) {
const user = this as User ;
bcrypt.compare(candidatePassword, user.password, (err, isMatch) => {
if (err) {
return cb(err, null) ;
}
cb(null, isMatch) ;
}) ;
} ;
{...}
Et bien sûr, modifier le type d'utilisateur.
type comparePasswordFunction = (
candidatePassword : string,
cb : (err : Error, isMatch : boolean) => void
) => void ;
export type User = Document & {
id : chaîne de caractères,
email : string,
name : chaîne de caractères,
password : chaîne de caractères,
comparePasswords : comparePasswordFunction,
} ;
Nous pouvons maintenant établir une connexion entre les serveurs et les bases de données. MONGODB_URI
est une variable d'environnement qui contient une chaîne de connexion nécessaire pour créer la connexion. Vous pouvez l'obtenir à partir de votre cluster panel après vous être connecté à votre compte MongoDB atlas. Placez-la à l'intérieur de .env
// .env
MONGODB_URI = ... ;
N'oubliez jamais d'ajouter ce fichier à .gitignore
. Super ! Ajoutons maintenant une fonction qui permet de se connecter à la base de données.
// src/server.ts
import mongoose, { Connection } from "mongoose" ;
{...}
let cachedDb : Connexion ;
const connectToDatabase = async () => {
if (cachedDb) return ;
await mongoose.connect(process.env.MONGODB_URI || "", {
useNewUrlParser : true,
useUnifiedTopology : true,
useFindAndModify : false,
useCreateIndex : true,
}) ;
cachedDb = mongoose.connection ;
} ;
{...}
Le contexte est un objet partagé par tous les résolveurs. Pour le fournir, il suffit d'ajouter une fonction d'initialisation du contexte au constructeur d'ApolloServer. C'est ce que nous allons faire.
// src/server.ts
import { userModel } from "./models/user.model" ;
{...}
const createLambdaServer = async () =>
nouveau ApolloServerLambda({
typeDefs,
resolvers,
introspection : true,
terrain de jeu : true,
context : async () => {
attend connectToDatabase() ;
return {
modèles : {
userModel,
},
} ;
},
}) ;
const createLocalServer = () =>
nouveau ApolloServer({
typeDefs,
résolveurs,
introspection : true,
terrain de jeu : true,
context : async () => {
attend connectToDatabase() ;
return {
modèles : {
userModel,
},
} ;
}
}) ;
Comme vous pouvez le constater, nous transmettons également userModel
à travers contexte
. Nous pouvons maintenant mettre à jour le résolveur :
// resolvers.ts
const userResolver = {
Query : {
user : async (_, { email, name, password }, { models : { userModel } }) => {
const user = await userModel.findById({ _id : id }).exec() ;
return user ;
},
},
Mutation : {
createUser : async (_, { id }, { models : { userModel } }) => {
const user = await userModel.create({ email, name, password }) ;
return user ;
},
},
} ;
Ça a l'air bien ! Créez maintenant une instance de serveur de base :
// src/index.ts
import { createLocalServer } from "./server" ;
require("dotenv").config() ;
const port = process.env.PORT || 4000 ;
const server = createLocalServer() ;
server.listen(port).then(({ url })) => {
console.log(`Server ir running at ${url}`) ;
}) ;
Démarrer le serveur local avec Nodemon
Dernière chose avant l'exécution, ajouter nodemon.json
{
"watch" : ["src"],
"ext" : ".ts,.js",
"ignore" : [],
"exec" : "ts-node --transpile-only ./src/index.ts"
}
Ajouter un script à package.json
"scripts" : {
"start" : "nodemon"
},
Et courez !
npm start
Vous devriez obtenir ces informations à l'intérieur du terminal :
Le serveur ir fonctionne à l'adresse http://localhost:4000/
Si oui, ouvrez le http://localhost:4000/
dans votre navigateur.
Le terrain de jeu GraphQL devrait apparaître. Créons un nouvel utilisateur !
Regardez ce que cela donne dans la base de données.
Cool ! Tout fonctionne bien !
Essayons d'obtenir des informations sur l'utilisateur.
Et ajouter le champ du mot de passe...
Bien ! Nous recevons l'erreur Impossible d'interroger le champ "password" sur le type "User"".
. Comme vous pouvez le constater, nous n'avons pas ajouté ce champ dans la définition du type d'utilisateur. Il est là à dessein. Nous ne devrions pas permettre l'interrogation d'un mot de passe ou d'autres données sensibles.
Autre chose... Nous pouvons obtenir les données de l'utilisateur sans aucune authentification... ce n'est pas une bonne solution. Nous devons y remédier.
Mais avant...
Configurer Codegen
Utilisons l'outil GraphQL code pour obtenir un type de base compatible, basé sur notre schéma.
npm install --save @graphql-codegen/cli @graphql-codegen/introspection
@graphql-codegen/typescript @graphql-codegen/typescript-resolvers
Créer codegen.yml
overwrite : true
schema : "http://localhost:4000"
génère :
./src/generated/graphql.ts :
plugins :
- "typescript"
- "typescript-resolvers"
./graphql.schema.json :
plugins :
- "introspection"
Ajouter codegen
pour package.json
"scripts" : {
"start" : "nodemon",
"codegen" : "graphql-codegen --config ./codegen.yml",
},
Ensuite, lorsque le serveur local est en cours d'exécution, lancez le script :
npm run codegen
En cas de succès, vous recevrez un message :
√ Analyse de la configuration
√ Générer des sorties
Si vous recevez cette information, deux fichiers devraient apparaître :
graphql.schema.json
dans le répertoire principal
graphql.ts
dans le chemin nouvellement créé src/généré
C'est le second qui nous intéresse le plus. Si vous l'ouvrez, vous remarquerez une belle structure de types.
Nous pouvons maintenant améliorer nos résolveurs :
// src/resolvers.ts
import { Resolvers, Token, User } from "./generated/graphql" ;
const userResolver : Resolvers = {
Query : {
user : async (_, { id }, { models : { userModel }, auth }) : Promise => {
const user = await userModel.findById({ _id : id }).exec() ;
return user ;
},
},
Mutation : {
createUser : async (
_,
{ email, nom, mot de passe },
{ models : { userModel } }
) : Promise => {
const user = await userModel.create({
email,
nom,
mot de passe,
}) ;
return user ;
},
},
} ;
Authentification
Ensuite, nous allons mettre en place une authentification simple basée sur un jeton.
npm install --save jsonwebtoken @types/jsonwebtoken
Créer checkAuth
pour vérifier si le jeton est valide. Nous ajouterons le résultat au contexte afin de pouvoir y accéder dans les résolveurs.
// src/server.ts
import { IncomingHttpHeaders } from "http";
import {
APIGatewayProxyEvent,
APIGatewayProxyEventHeaders,
Context,
} from "aws-lambda";
import jwt from "jsonwebtoken";
{...}
const checkAuth = async ({ token } : APIGatewayProxyEventHeaders | IncomingHttpHeaders ) => {
if (typeof token === "string") {
try {
return await jwt.verify(token, "riddlemethis");
} catch (e) {
throw new AuthenticationError(`Your session expired. Sign in again.`);
}
}
};
{...}
const createLambdaServer = async (
{ headers }: APIGatewayProxyEvent,
context: Context
) => {
return new ApolloServerLambda({
typeDefs,
resolvers,
context: async () => {
await connectToDatabase();
const auth = await checkAuth(headers);
return {
auth,
models: {
userModel,
},
};
},
});
};
function createLocalServer() {
return new ApolloServer({
typeDefs,
resolvers,
introspection: true,
playground: true,
context: async ({ req: { headers } = {} }) => {
const auth = await checkAuth(headers);
await connectToDatabase();
return {
auth,
models: {
userModel,
},
};
},
});
}
Nous avons également besoin d'un moyen de créer un tel jeton. Le meilleur moyen est d'implémenter une requête de login dans notre résolveur.
// resolvers.ts
import { Resolvers, Token, User } from "./generated/graphql" ;
const userResolver : Resolvers = {
Query : {
user : async (_, { id }, { models : { userModel }, auth }) : Promise => {
if (!auth) throw new AuthenticationError("Vous n'êtes pas authentifié") ;
const user = await userModel.findById({ _id : id }).exec() ;
return user ;
},
login : async (
_,
{ email, password },
{ models : { userModel } }
) : Promise => {
const user = await userModel.findOne({ email }).exec() ;
if (!user) throw new AuthenticationError("Invalid credentials") ;
const matchPasswords = bcrypt.compareSync(password, user.password) ;
if (!matchPasswords) throw new AuthenticationError("Invalid credentials") ;
const token = jwt.sign({ id : user.id }, "riddlemethis", {
expiresIn : 60,
}) ;
return { token } ;
},
},
Mutation : {
createUser : async (
_,
{ email, nom, mot de passe },
{ models : { userModel } }
) : Promise => {
const user = await userModel.create({
email,
nom,
mot de passe,
}) ;
return user ;
},
},
} ;
Vous devez également mettre à jour les définitions des types d'utilisateurs en fonction du type de jeton et de la requête de connexion.
type Token {
token : Chaîne !
}
type Query {
user(id : ID !): Utilisateur !
login(email : String !, password : String !): Token !
}
Essayons maintenant d'obtenir l'utilisateur sans jeton
OK, ça marche ! Essayer de se connecter
Essayons à nouveau d'obtenir l'utilisateur, mais cette fois-ci avec un jeton ajouté aux en-têtes
Cool ! Et si nous nous trompons d'identifiants ?
C'est bien !
Préparer le déploiement
Dernière étape : déployer l'api sans serveur avec Netlify !
Créer un dossier lambda
dans le répertoire principal et y placer deux fichiers :
First contains AWS handler. It create a ApolloServerLambda server instance and puis exposer un gestionnaire en utilisant createHandler de cette instance.
// lambda/graphql.ts
import { APIGatewayProxyEvent, Context } from "aws-lambda" ;
import { createLambdaServer } from " ???";
export const handler = async (
event : APIGatewayProxyEvent,
context : Contexte
) => {
const server = await createLambdaServer(event, context) ;
return new Promise((res, rej) => {
const cb = (err : Error, args : any) => (err ? rej(err) : res(args)) ;
server.createHandler()(event, context, cb) ;
}) ;
} ;
Vous pouvez en savoir plus à ce sujet.
Le deuxième est le tsconfig. La partie importante est le outDir
champ.
// lambda/tsconfig.json
{
"compilerOptions" : {
"sourceMap" : true,
"noImplicitAny" : true,
"module" : "commonjs",
"target" : "es6",
"experimentalDecorators" : true,
"allowSyntheticDefaultImports" : true,
"moduleResolution" : "nœud ",
"skipLibCheck" : true,
"esModuleInterop" : true,
"outDir" : "./dist"
},
"include" : ["./*.ts", "./**/*.ts", "./**/*.js"]
}
Mais il y a un problème. Nous ne pouvons pas utiliser /src
car Netlify ne peut pas aller au-delà du dossier lambda. Nous devons donc l'empaqueter.
npm install --save ncp
Il s'agit d'un logiciel qui permet de copier un répertoire.
Ajouter liasse
pour package.json
"scripts" : {
"bundle" : "ncp ./src ./lambda/bundle",
"codegen" : "graphql-codegen --config ./codegen.yml",
"start" : "nodemon",
},
Maintenant, si vous exécutez npm run bundle
Vous pouvez constater que dans les lambda/bundle
nous avons tous les fichiers de src/
.
Mettre à jour le chemin d'importation à l'intérieur de lambda/graphql.ts
// lambda/graphql.ts
import { APIGatewayProxyEvent, Context } from "aws-lambda" ;
import { createLambdaServer } from "./bundle/server" ;
{...}
Vous pouvez maintenant ajouter le répertoire lambda/bundle à .gitignore
Déployer avec Netlify
Nous devons indiquer à Netlify quelle est la commande de construction et où se trouvent nos fonctions. Pour cela, créons netlify.toml
file:
// netlify.toml
[build]
command = "npm run build:lambda"
functions = "lambda/dist"
Comme vous pouvez le constater, il s'agit du même répertoire que celui défini en tant que outDir
champ en lambda/tsconfig.json
Voici à quoi devrait ressembler la structure de votre application (enfin... une partie ;))
app
└───lambda
│ └────bundle
│ │ graphql.ts
│ tsconfig.json.ts
└───src
└────generated
│ │ │ graphql.ts
│ │ index.ts
│ │ model.ts
│ │ resolvers.ts
│ │ schemas.ts
│ server.ts
│
│ codegen.yml
│ graphql.schema.json
│ netlify.toml
│ nodemon.json
│ tsconfig.json
Ajouter bundle:lambda
pour package.json
"scripts" : {
"build:lambda" : "npm run bundle && tsc -p lambda/tsconfig.json",
"bundle" : "ncp ./src ./lambda/bundle",
"codegen" : "graphql-codegen --config ./codegen.yml",
"start" : "nodemon",
},
Déployer
Essayons de déployer notre application via Netlify.
Connectez-vous à votre compte Netlify,
Connectez-vous à votre Github,
Cliquez sur le bouton "Nouveau site à partir de Git",
Sélectionnez un répertoire approprié,
Définir la commande de construction npm run build:lambda,
Ajouter une variable d'environnement (MONGODB_URI),
Déployer...
Et TAAADAAAA...
En effet, le point de terminaison n'est pas, par défaut, le point de terminaison http://page/
mais http://page/.netlify/functions/graphql
.
Comment y remédier ? C'est très simple. Il suffit de créer redirections
avec :
/ /.netlify/functions/graphql 200 !
Déployez à nouveau et vérifiez.
J'espère qu'il vous plaira ! N'hésitez pas à l'améliorer et à le modifier.
En savoir plus :
Comment ne pas tuer un projet avec de mauvaises pratiques de codage ?
Sécurité des applications web. Vulnérabilité Target="_blank
Sécurité des applications web - vulnérabilité XSS