Objetivos Configuración inicial Instalar dependencias Empecemos Primero, añade el tsconfig.json al directorio principal: Ahora, vamos a crear src/server.ts para las implementaciones de los servidores. Luego agrega dos funciones: una para el servidor local y la segunda para lambda. Bien, no tenemos resolvers ni definiciones de tipos así que necesitamos crear algunos. Supongamos que, al principio, queremos [...]
Objetivos
Configure tanto los servidores locales como los lambda.
Conecta ambos a MongoDB.
Implantar la autenticación básica.
Despliegue de Apollo sin servidor GraphQL API con Netlify.
Utilizar Typescript .
Configuración inicial
npm init -y
Instalar dependencias
npm install --save typescript graphql aws-lambda @types/aws-lambda
Empecemos
En primer lugar, añada el tsconfig.json
al directorio 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"]
}
Ahora, vamos a crear src/server.ts
para la implementación de servidores. A continuación, agregue dos funciones: una para el servidor local y la segunda para lambda.
// src/server.ts
import { ApolloServer as ApolloServerLambda } from "apollo-server-lambda";
import { ApolloServer } de "apollo-server";
const createLambdaServer = () =>
new ApolloServerLambda({
typeDefs,
resolvers,
introspección: true,
playground: true,
},
});
const createLocalServer = () =>
new ApolloServer({
typeDefs,
resolvers,
introspección: true,
playground: true,
},
});
export { createLambdaServer, createLocalServer };
Bien, no tenemos resolvers ni definiciones de tipos, así que necesitamos crear algunos. Supongamos que, al principio, queremos crear usuarios y recibir información sobre ellos.
// src/schemas.ts
const { gql } = require("apollo-server-lambda");
const userSchema = gql`
type Usuario {
id: ID
email: ¡String!
nombre: ¡Cadena!
}
type Consulta {
usuario(id: ID!): ¡Usuario!
}
tipo Mutation {
createUser(nombre: ¡Cadena!, email: ¡Cadena!, contraseña: ¡Cadena!): ¡Usuario!
}
`;
Si no estás familiarizado con esto, Apollo ha preparado un tutorial muy bonito
Ahora vamos a crear una resolución de usuario con una consulta y una mutación.
// src/resolvers.ts
const userResolver = {
Consulta: {
user: async (parent, args, context, info) => {
{...}
},
},
Mutación: {
createUser: async (parent, args, context, info) => {
{...}
},
},
};
Pero no tenemos datos... Vamos a arreglarlo 😉
No olvide importar la definición de tipo y el resolver al servidor.
// src/server.ts
import { ApolloServer as ApolloServerLambda } from "apollo-server-lambda";
import { ApolloServer } de "apollo-server";
import { typeDefs } from "./schemas";
import { resolvers } from "./resolvers";
{...}
Conectar con MongoDB a través de mongoose
Ahora es un buen momento para crear una conexión con nuestra base de datos. En este caso particular, será MongoDB. Es gratis y fácil de mantener. Pero antes de eso, vamos a instalar dos dependencias más:
npm install --save mongoose dotenv
El primer paso es crear un Modelo de Usuario.
// src/model.ts
import mongoose, { Documento, Error, Esquema } from "mongoose";
export type Usuario = Documento & {
_id: cadena
email: string
nombre: cadena
contraseña: cadena,
};
borrar mongoose.connection.models["Usuario"];
const UserSchema: Schema = new Schema({
email: {
tipo: String,
requerido: true
único: true,
},
nombre: {
tipo: Cadena,
requerido: true
minLongitud: 3,
maxLength: 32,
},
contraseña: {
tipo: Cadena,
requerido: true,
},
});
export const userModel = mongoose.model ("Usuario", UserSchema);
Contraseñas más seguras
La seguridad ante todo Ahora vamos a asegurar nuestras contraseñas mediante hashing.
npm install --save bcrypt @tipos/bcrypt
Ahora, implementa la seguridad de la contraseña dentro del callback pre-middleware. Las funciones pre-middleware se ejecutan una tras otra, cuando cada middleware llama a continuación. Para hacer las contraseñas seguras, estamos usando una técnica que genera una sal y un hash en llamadas a funciones separadas.
// src/model.ts
import bcrypt from "bcrypt";
{...}
const SALT_WORK_FACTOR: número = 10;
UserSchema.pre("guardar", 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(usuario.contraseña, salt, function (err, hash) {
if (err) return next(err);
usuario.contraseña = hash;
next();
});
});
});
{...}
A continuación, añada el método comparePasswords a UserSchema:
// src/model.ts
{...}
UserSchema.methods.comparePasswords = function (
contraseñaCandidata: string,
cb: (err: Error | null, same: boolean | null) => void
) {
const usuario = this as Usuario;
bcrypt.compare(candidContraseña, usuario.contraseña, (err, isMatch) => {
if (err) {
return cb(err, null);
}
cb(null, isMatch);
});
};
{...}
Y, por supuesto, modificar el tipo de usuario.
tipo comparePasswordFunction = (
contraseñaCandidata: cadena,
cb: (err: Error, isMatch: boolean) => void
) => void;
exportar tipo User = Document & {
_id: cadena
email: cadena
nombre: cadena
contraseña: cadena,
comparePasswords: comparePasswordFunction,
};
Ahora podemos organizar una conexión entre servidores y bases de datos. MONGODB_URI
es una variable de entorno que contiene una cadena de conexión necesaria para crear la conexión. Puedes obtenerla desde el panel de tu cluster después de iniciar sesión en tu cuenta de MongoDB atlas. Ponla dentro de .env
// .env
MONGODB_URI = ...;
Recuerde siempre añadir ese archivo a .gitignore
. Genial. Ahora vamos a añadir una función que permite conectar con db.
// src/server.ts
import mongoose, { Connection } from "mongoose";
{...}
let cachedDb: Connection;
const connectToDatabase = async () => {
if (cachedDb) return;
await mongoose.connect(process.env.MONGODB_URI || "", {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
useCreateIndex: true,
});
cachedDb = mongoose.connection;
};
{...}
El contexto es un objeto que comparten todos los resolvers. Para proporcionarlo, sólo tenemos que añadir una función de inicialización del contexto al constructor de ApolloServer. Vamos a hacerlo.
// src/server.ts
import { userModel } from "./modelos/usuario.modelo";
{...}
const createLambdaServer = async () =>
new ApolloServerLambda({
typeDefs,
resolvers,
introspección: true,
playground: true,
context: async () => {
await connectToDatabase();
return {
models: {
userModel,
},
};
},
});
const createLocalServer = () =>
new ApolloServer({
typeDefs,
resolvers,
introspección: true,
playground: true,
context: async () => {
await connectToDatabase();
return {
models: {
userModel,
},
};
}
});
Como puede ver, también estamos pasando userModel
a través de contexto
. Ahora podemos actualizar el resolver:
// resolvers.ts
const userResolver = {
Consulta: {
user: async (_, { email, name, password }, { models: { userModel } }) => {
const user = await userModel.findById({ _id: id }).exec();
return usuario;
},
},
Mutación: {
createUser: async (_, { id }, { models: { userModel } }) => {
const usuario = await userModel.create({ email, nombre, contraseña });
return usuario;
},
},
};
¡Qué bonito! Ahora cree una instancia de servidor básica:
// src/index.ts
import { crearServidorLocal } from "./servidor";
require("dotenv").config();
const puerto = proceso.entorno.PUERTO || 4000;
const servidor = createLocalServer();
server.listen(port).then(({ url }) => {
console.log(`Servidor ir ejecutándose en ${url}`);
});
Iniciar el servidor local con Nodemon
Por último, antes de la ejecución, añada nodemon.json
{
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "ts-node --transpile-only ./src/index.ts"
}
Añadir script a paquete.json
"scripts": {
"start": "nodemon"
},
¡Y corre!
npm iniciar
Deberías obtener esa información dentro del terminal:
Servidor ir funcionando en http://localhost:4000/
En caso afirmativo, abra el http://localhost:4000/
dentro de su navegador.
Debería aparecer la zona de juegos GraphQL. ¡Vamos a crear un nuevo usuario!
Echa un vistazo a cómo se ve en la base de datos.
¡Genial! Todo funciona bien.
Intentemos obtener información del usuario.
Y añadir el campo de contraseña...
Muy bien. Recibimos error No se puede consultar el campo "contraseña" en el tipo "Usuario"."
. Como puede comprobar, no hemos añadido este campo dentro de la definición del tipo de usuario. Está ahí a propósito. No debemos hacer posible la consulta de contraseñas u otros datos sensibles.
Otra cosa... Podemos obtener datos del usuario sin ninguna autenticación... no es una buena solución. Tenemos que arreglarlo.
Pero antes...
Configurar Codegen
Utilicemos el GraphQL código para obtener un tipo base compatible, basado en nuestro esquema.
npm install --save @graphql-codegen/cli @graphql-codegen/introspection
@graphql-codegen/typescript @graphql-codegen/typescript-resolvers
Cree codegen.yml
sobrescribir: true
esquema: "http://localhost:4000"
genera:
./src/generated/graphql.ts:
plugins:
- "typescript"
- "typescript-resolvers"
./graphql.schema.json:
plugins:
- "introspection"
Añadir codegen
script para paquete.json
"scripts": {
"start": "nodemon",
"codegen": "graphql-codegen --config ./codegen.yml",
},
A continuación, cuando el servidor local se está ejecutando, ejecute el script:
npm run codegen
En caso de éxito, recibirá un mensaje:
√ Analizar la configuración
√ Generar salidas
Si recibes esa información, deberían aparecer dos archivos:
graphql.esquema.json
en el directorio principal
graphql.ts
en la ruta recién creada src/generado
Nos interesa más el segundo. Si lo abre, observará una bonita estructura de tipos.
Ahora podemos mejorar nuestros resolutores:
// 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 usuario;
},
},
Mutación: {
createUser: async (
_,
{ email, name, password },
{ modelos: { userModel } }
): Promise => {
const user = await userModel.create({
email,
nombre
contraseña,
});
return usuario;
},
},
};
Autenticación
A continuación, vamos a configurar una autenticación simple basada en token.
npm install --save jsonwebtoken @tipos/jsonwebtoken
Cree checkAuth
para verificar si el token es válido. Añadiremos el resultado al contexto para poder acceder a él dentro de los resolvers.
// 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,
},
};
},
});
}
Además, necesitamos una forma de crear dicho token. La mejor manera es implementar una consulta de inicio de sesión dentro de nuestro resolver.
// resolvers.ts
import { Resolvers, Token, User } from "./generated/graphql";
const userResolver: Resolvers = {
Consulta: {
user: async (_, { id }, { models: { userModel }, auth }): Promise => {
if (!auth) throw new AuthenticationError("No estás autenticado");
const user = await userModel.findById({ _id: id }).exec();
return usuario;
},
login: async (
_,
{ email, password },
{ models: { userModel } }
): Promise => {
const user = await userModel.findOne({ email }).exec();
if (!user) throw new AuthenticationError("Credenciales no válidas");
const matchPasswords = bcrypt.compareSync(password, user.password);
if (!matchPasswords) lanza un nuevo AuthenticationError("Credenciales no válidas");
const token = jwt.sign({ id: user.id }, "riddlemethis", {
expiresIn: 60,
});
return { token };
},
},
Mutación: {
createUser: async (
_,
{ email, name, password },
{ modelos: { userModel } }
): Promise => {
const user = await userModel.create({
email,
nombre
contraseña,
});
return usuario;
},
},
};
También es necesario actualizar las definiciones de tipo de usuario por tipo de token y consulta de inicio de sesión.
tipo Token {
token: String
}
type Consulta {
usuario(id: ID!): ¡Usuario!
login(email: String!, password: String!): ¡Token!
}
Intentemos ahora obtener el usuario sin token
OK, ¡funciona bien! Intenta iniciar sesión
Intentemos de nuevo obtener el usuario, pero esta vez con el token añadido a las cabeceras
¡Genial! ¿Y si nos equivocamos de credenciales?
¡Genial!
Preparar el despliegue
Último paso: ¡desplegar la api sin servidor con Netlify!
Crear carpeta lambda
en el directorio principal y poner dos archivos dentro:
Primero contiene AWS handler. Crea una instancia de servidor ApolloServerLambda y y luego exponer un manejador usando createHandler de esa instancia.
// lambda/graphql.ts
import { APIGatewayProxyEvent, Context } from "aws-lambda";
import { createLambdaServer } from "???";
export const handler = async (
evento: APIGatewayProxyEvent,
contexto: Contexto
) => {
const servidor = await crearServidorLambda(evento, contexto);
return new Promise((res, rej) => {
const cb = (err: Error, args: any) => (err ? rej(err) : res(args));
server.createHandler()(event, context, cb);
});
};
Puede leer más al respecto.
La segunda, es el tsconfig. La parte importante es el outDir
campo.
// lambda/tsconfig.json
{
"compilerOptions": {
"sourceMap": true
"noImplicitAny": true,
"module": "commonjs",
"target": "es6",
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "nodo ",
"skipLibCheck": true,
"esModuleInterop": true,
"outDir": "./dist"
},
"include": ["./*.ts", "./**/*.ts", "./**/*.js"]
}
Pero hay un problema. No podemos utilizar /src
dir porque Netlify no puede llegar fuera de la carpeta lambda. Así que debemos agruparlo.
npm install --save ncp
Es un paquete que permite copiar directorios.
Añadir paquete
script para paquete.json
"scripts": {
"bundle": "ncp ./src ./lambda/bundle",
"codegen": "graphql-codegen --config ./codegen.yml",
"start": "nodemon",
},
Ahora bien, si ejecuta npm run bundle
se puede ver que en los nuevos lambda/paquete
dir tenemos todos los archivos de src/
.
Actualizar la ruta de importación dentro de lambda/graphql.ts
// lambda/graphql.ts
import { APIGatewayProxyEvent, Context } from "aws-lambda";
import { createLambdaServer } de "./bundle/server";
{...}
Ahora puede añadir lambda/bundle dir a .gitignore
Despliegue con Netlify
Debemos decirle a Netlify cual es el comando de compilación y donde viven nuestras funciones. Para ello vamos a crear netlify.toml
file:
// netlify.toml
[build]
command = "npm run build:lambda"
funciones = "lambda/dist"
Como se puede ver, es el mismo directorio que se define como un outDir
campo en lambda/tsconfig.json
Así es como debería ser la estructura de tu aplicación (bueno... parte de ella ;))
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
Añadir paquete:lambda
script para paquete.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",
},
Despliegue
Intentemos desplegar nuestra aplicación a través de Netlify.
Accede a tu cuenta Netlify,
Conecta con tu Github,
Haga clic en el botón "Nuevo sitio desde Git",
Seleccione un repositorio adecuado,
Establezca el comando de compilación npm run build:lambda,
Añadir variable de entorno (MONGODB_URI),
Despliega...
Y TAAADAAAA...
Esto se debe a que el punto final no es por defecto el http://page/
pero http://page/.netlify/functions/graphql
.
¿Cómo solucionarlo? Es muy sencillo. Basta con crear _redirects
con:
¡/ /.netlify/functions/graphql 200!
Despliegue de nuevo y compruebe.
Espero que te haya gustado. Siéntase libre de mejorar y cambiar.
Más información:
¿Cómo no matar un proyecto con malas prácticas de codificación?
Seguridad de las aplicaciones web. Vulnerabilidad Target="_blank
Seguridad de aplicaciones web - Vulnerabilidad XSS