Cíle Počáteční nastavení Instalace závislostí Začněme Nejprve přidejte soubor tsconfig.json do hlavního adresáře: Nyní vytvořme src/server.ts pro implementaci serverů. Poté přidáme dvě funkce: jednu pro lokální server a druhou pro lambdu. OK, nemáme žádné resolvery ani definice typů, takže je musíme vytvořit. Předpokládejme, že nejprve chceme [...]
Cíle
Konfigurace místních serverů i serverů lambda.
Připojte obě k MongoDB.
Implementujte základní ověřování.
Nasazení bezserverového systému Apollo GraphQL API s Netlify.
Použití Typescriptu .
Počáteční nastavení
npm init -y
Instalace závislostí
npm install --save strojopis graphql aws-lambda @types/aws-lambda
Začněme
Nejprve přidejte tsconfig.json do hlavního adresáře:
{
"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"]
}
Nyní vytvořme src/server.ts pro implementace serverů. Pak přidejte dvě funkce: jednu pro místní server a druhou pro lambdu.
// src/server.ts
import { ApolloServer as ApolloServerLambda } from "apollo-server-lambda";
import { ApolloServer } z "apollo-server";
const createLambdaServer = () =>
new ApolloServerLambda({
typeDefs,
resolvers,
introspection: true,
playground: true,
},
});
const createLocalServer = () =>
new ApolloServer({
typeDefs,
resolvers,
introspection: true,
playground: true,
},
});
export { createLambdaServer, createLocalServer };
OK, nemáme žádné resolvery ani definice typů, takže je musíme vytvořit. Předpokládejme, že nejprve chceme vytvářet uživatele a přijímat o nich informace.
// src/schemas.ts
const { gql } = require("apollo-server-lambda");
const userSchema = gql`
typ User {
id: ID!
email: String!
name: String!
}
typ Query {
user(id: ID!): User!
}
typ Mutace {
createUser(name: String!, email: String!, password: String!): User!
}
`;
Pokud to neznáte, Apollo připravil velmi pěkný výukový program
Nyní vytvoříme uživatelský resolver s jedním dotazem a jednou mutací.
// src/resolvers.ts
const userResolver = {
Dotaz: {
user: async (parent, args, context, info) => {
{...}
},
},
Mutace: {
createUser: async (parent, args, context, info) => {
{...}
},
},
};
Ale nemáme žádná data... Pojďme to napravit 😉
Nezapomeňte importovat definici typu a resolver na server.
// src/server.ts
import { ApolloServer as ApolloServerLambda } from "apollo-server-lambda";
import { ApolloServer } z "apollo-server";
import { typeDefs } z "./schemas";
import { resolvers } z "./resolvers";
{...}
Připojení k MongoDB prostřednictvím mongoose
Nyní je vhodný čas vytvořit spojení s naší databází. V tomto konkrétním případě to bude MongoDB. Je zdarma a snadno se udržuje. Ještě předtím však nainstalujme další dvě závislosti:
npm install --save mongoose dotenv
Prvním krokem je vytvoření uživatelského modelu.
// src/model.ts
import mongoose, { Document, Error, Schema } z "mongoose";
export type User = Document & {
_id: string,
email: string,
name: string,
password: string,
};
delete mongoose.connection.models["User"];
const UserSchema: Schema = new Schema({
email: {
typ: : String,
required: true,
unique: true,
},
name: {
type: :: String,
required: true,
minLength: 3,
maxLength: 32,
},
heslo: {
typ: : String,
required: true,
},
});
export const userModel = mongoose.model ("User", UserSchema);
Zvýšení bezpečnosti hesel
Bezpečnost především! Nyní zabezpečíme naše hesla pomocí hashování.
npm install --save bcrypt @types/bcrypt
Nyní implementujte zabezpečení hesla v rámci zpětného volání pre-middleware. Funkce pre-middlewaru se provádějí jedna po druhé, když každý middleware volá další. Pro zabezpečení hesel používáme techniku, která generuje sůl a hash při samostatných voláních funkcí.
// src/model.ts
import bcrypt from "bcrypt";
{...}
const SALT_WORK_FACTOR: number = 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();
});
});
});
{...}
Pak přidejte metodu comparePasswords do 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);
});
};
{...}
A samozřejmě upravte typ uživatele.
typ comparePasswordFunction = (
candidatePassword: string,
cb: (err: Error, isMatch: boolean) => void
) => void;
export type User = Document & {
_id: string,
email: string,
name: string,
password: string,
comparePasswords: comparePasswordFunction,
};
Nyní můžeme zajistit spojení mezi servery a databázemi. MONGODB_URI je proměnná prostředí, která obsahuje připojovací řetězec potřebný k vytvoření připojení. Můžete jej získat z panelu clusteru po přihlášení k účtu MongoDB atlas. Vložte ji dovnitř .env
// .env
MONGODB_URI = ...;
Vždy nezapomeňte přidat tento soubor do .gitignore. Skvělé! Nyní přidáme funkci, která umožní spojení s db.
// src/server.ts
import mongoose, { Connection } z "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;
};
{...}
Kontext je objekt, který je sdílený všemi resolvery. Abychom jej mohli poskytnout, stačí přidat inicializační funkci kontextu do konstruktoru ApolloServeru. Udělejme to.
// src/server.ts
import { userModel } z "./models/user.model";
{...}
const createLambdaServer = async () =>
new ApolloServerLambda({
typeDefs,
resolvers,
introspection: true,
playground: true,
context: async () => {
await connectToDatabase();
return {
models: {
userModel,
},
};
},
});
const createLocalServer = () =>
new ApolloServer({
typeDefs,
resolvers,
introspection: true,
playground: true,
context: async () => {
await connectToDatabase();
return {
models: {
userModel,
},
};
}
});
Jak vidíte, předáváme také userModel prostřednictvím kontext. Nyní můžeme aktualizovat resolver:
// resolvers.ts
const userResolver = {
Query: {
user: async (_, { email, name, password }, { models: { userModel } }) => {
const user = await userModel.findById({ _id: id }).exec();
return user;
},
},
Mutace: {
createUser: async (_, { id }, { models: { userModel } }) => {
const user = await userModel.create({ email, name, password });
return user;
},
},
};
Vypadá to pěkně! Nyní vytvořte základní instanci serveru:
// src/index.ts
import { createLocalServer } z "./server";
require("dotenv").config();
const port = process.env.PORT || 4000;
const server = createLocalServer();
server.listen(port).then(({ url }) => {
console.log(`Server ir běží na adrese ${url}`);
});
Spuštění místního serveru pomocí Nodemonu
Poslední věc před spuštěním, přidejte nodemon.json
{
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "ts-node --transpile-only ./src/index.ts"
}
Přidání skriptu do package.json
"skripty": {
"start": "nodemon"
},
A utíkejte!
npm start
Tyto informace byste měli získat v terminálu:
Server ir běží na adrese http://localhost:4000/
Pokud ano, otevřete http://localhost:4000/ v prohlížeči.
Mělo by se zobrazit hřiště GraphQL. Vytvořme nového uživatele!
Podívejte se, jak to vypadá v databázi.
Super! Všechno funguje dobře!
Zkusme získat nějaké informace o uživateli.
A přidejte pole hesla...
Pěkné! Obdržíme chybu Nelze zadat dotaz do pole "heslo" na typu "Uživatel".". Jak si můžete zpětně ověřit, nepřidali jsme toto pole do definice typu uživatele. Je tam záměrně. Neměli bychom umožnit dotazování na heslo nebo jiné citlivé údaje.
Další věc... Můžeme získat uživatelská data bez jakéhokoli ověření... to není dobré řešení. Musíme to opravit.
Ale ještě předtím...
Konfigurace aplikace Codegen
Použijme GraphQL kód získat kompatibilní základní typ na základě našeho schématu.
npm install --save @graphql-codegen/cli @graphql-codegen/introspection
@graphql-codegen/typescript @graphql-codegen/typescript-resolvers
Vytvořit codegen.yml
přepsat: true
schéma: "http://localhost:4000"
generuje:
./src/generated/graphql.ts:
plugins:
- "typescript"
- "typescript-resolvers"
./graphql.schema.json:
plugins:
- "introspection"
Přidat codegen skript na package.json
"skripty": {
"start": "nodemon",
"codegen": "graphql-codegen --config ./codegen.yml",
},
Když je místní server spuštěn, spusťte skript:
npm run codegen
V případě úspěchu se zobrazí zpráva:
√ Konfigurace parsování
√ Generování výstupů
Pokud tyto informace obdržíte, měly by se objevit dva soubory:
graphql.schema.json v hlavním adresáři
graphql.ts v nově vytvořené cestě src/generated
Nás zajímá spíše ta druhá. Pokud ji otevřete, všimnete si pěkné struktury typů.
Nyní můžeme vylepšit naše resolvery:
// src/resolvers.ts
import { Resolvers, Token, User } z "./generated/graphql";
const userResolver: Resolvers = {
Query: {
user: async (_, { id }, { models: { userModel }, auth }): Promise => {
const user = await userModel.findById({ _id: id }).exec();
return user;
},
},
Mutace: {
createUser: async (
_,
{ email, name, password },
{ models: { userModel } }
): Promise => {
const user = await userModel.create({
email,
name,
password,
});
return user;
},
},
};
Ověřování
Dále nastavíme jednoduché ověřování pomocí tokenu.
npm install --save jsonwebtoken @types/jsonwebtoken
Vytvořit checkAuth ověřit, zda je token platný. Výsledek přidáme do kontextu, abychom k němu mohli přistupovat uvnitř resolverů.
// 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,
},
};
},
});
}
Také potřebujeme způsob, jak takový token vytvořit. Nejlepším způsobem je implementovat přihlašovací dotaz uvnitř našeho resolveru.
// resolvers.ts
import { Resolvers, Token, User } z "./generated/graphql";
const userResolver: Resolvers = {
Query: {
user: async (_, { id }, { models: { userModel }, auth }): Promise => {
if (!auth) throw new AuthenticationError("Nejste ověřeni");
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("Neplatné pověření");
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 };
},
},
Mutace: {
createUser: async (
_,
{ email, name, password },
{ models: { userModel } }
): Promise => {
const user = await userModel.create({
email,
name,
password,
});
return user;
},
},
};
Je také třeba aktualizovat definice typu uživatele podle typu tokenu a přihlašovacího dotazu.
typ Token {
token: String!
}
typ Query {
user(id: ID!): User!
login(email: String!, password: String!): Token!
}
Zkusme nyní získat uživatele bez tokenu
OK, funguje dobře! Zkuste se přihlásit
Zkusme znovu získat uživatele, ale tentokrát s tokenem přidaným do hlaviček
Super! A co když nastavíme špatné pověření?
Pěkné!
Příprava na nasazení
Poslední krok: nasazení serverless api pomocí Netlify!
Vytvořit složku lambda v hlavním adresáři a vložte do něj dva soubory:
První obsahuje AWS obsluha. Vytvoří instanci serveru ApolloServerLambda a pak vystavit obslužnou rutinu pomocí createHandler této instance.
// lambda/graphql.ts
import { APIGatewayProxyEvent, Context } z "aws-lambda";
import { createLambdaServer } z "???";
export const handler = async (
event: APIGatewayProxyEvent,
context: Context
) => {
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);
});
};
Můžete si o něm přečíst více.
Druhým je tsconfig. Důležitou součástí je outDir pole.
// lambda/tsconfig.json
{
"compilerOptions": {
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es6",
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "uzel ",
"skipLibCheck": true,
"esModuleInterop": true,
"outDir": /dist": "./dist"
},
"include": ]: ["./*.ts", "./**/*.ts", "./**/*.js"].
}
Je tu však problém. Nemůžeme použít /src dir, protože Netlify nemůže dosáhnout mimo složku lambda. Musíme ji tedy svázat do balíčku.
npm install --save ncp
Je to balíček, který umožňuje kopírovat adresáře.
Přidat svazek skript na package.json
"skripty": {
"bundle": "ncp ./src ./lambda/bundle",
"codegen": "graphql-codegen --config ./codegen.yml",
"start": "nodemon",
},
Pokud nyní spustíte npm run bundle, můžete vidět, že v nově vytvořeném lambda/bundle dir máme všechny soubory z src/.
Aktualizujte cestu importu uvnitř lambda/graphql.ts
// lambda/graphql.ts
import { APIGatewayProxyEvent, Context } z "aws-lambda";
import { createLambdaServer } z "./bundle/server";
{...}
Nyní můžete přidat lambda/bundle dir do adresáře .gitignore
Nasazení pomocí Netlify
Musíme společnosti Netlify sdělit, jaký je příkaz pro sestavení a kde se nacházejí naše funkce. Za tímto účelem vytvořme netlify.toml file:
// netlify.toml
[build]
command = "npm run build:lambda"
functions = "lambda/dist"
Jak vidíte, jedná se o stejný adresář, který je definován jako outDir pole v lambda/tsconfig.json
Takto by měla vypadat struktura vaší aplikace (tedy... její část ;))
aplikace
└───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
Přidat bundle:lambda skript na package.json
"skripty": {
"build:lambda": "npm run bundle && tsc -p lambda/tsconfig.json",
"bundle": "ncp ./src ./lambda/bundle",
"codegen": "graphql-codegen --config ./codegen.yml",
"start": "nodemon",
},
Nasazení
Zkusme nasadit naši aplikaci prostřednictvím služby Netlify.
Přihlaste se ke svému účtu Netlify,
Připojte se ke svému Githubu,
Klikněte na tlačítko "Nový web ze systému Git",
Vyberte správné úložiště,
Nastavte příkaz pro sestavení npm run build:lambda,
Přidání proměnné prostředí (MONGODB_URI),
Nasazení...
A TAAADAAAA...
Je to proto, že koncový bod není ve výchozím nastavení http://page/ ale http://page/.netlify/functions/graphql.
Jak to napravit? Je to velmi jednoduché. Stačí vytvořit _redirects s:
/ /.netlify/functions/graphql 200!
Znovu nasaďte a zkontrolujte.
Doufám, že se vám to líbilo! Nebojte se vylepšovat a měnit.
Přečtěte si více:
Jak nezničit projekt špatnými kódovacími postupy?
Zabezpečení webových aplikací. Target="_blank" zranitelnost
Zabezpečení webových aplikací - zranitelnost XSS