En prévision du début du cours "Node.JS Developer", nous invitons tout le monde à regarder la leçon ouverte sur le thème "Dockerization des applications Node.js" .
Et maintenant, nous partageons la traduction traditionnelle de matériel utile. Bonne lecture.
Depuis la première version, Deno est devenu un mot à la mode pour Javascript / TypeScript / Node. Plongeons dans cette technologie en créant une API REST sécurisée par JWT.
Il est conseillé d'avoir déjà quelques bases dans Node et son écosystème (Express, Nodemon, Sequelize, etc.) afin de suivre ce tutoriel.
Qu'est-ce que Dino (Deno)?
Deno est un runtime JavaScript et TypeScript simple, moderne et sécurisé qui utilise V8 et est intégré à Rust.
Il existe déjà de nombreux articles détaillant ce sujet, je ne m'y attarderai donc pas. Je peux recommander celui-ci .
introduction
Depuis la sortie officielle de la V1 , Deno est devenu un mot à la mode en quelques semaines (pour le plaisir, voici la courbe de popularité des recherches "deno" sur Google).
Typescript and Javascript"?
, JWT REST API .
Node.js Express.
REST API, :
ORM
CRUD-
JWT
REST API JWT, :
( ):
-, , " " .
|-- DenoRestJwt |-- controllers/ | |-- database/ | |-- models/ |-- helpers/ |-- middlewares/ |-- routers/ |-- app.ts
Node + Express , Nodemon , Nodemon .
Nodemon - , node.js, Node .
" ", Denon, Deno.
deno install --allow-read --allow-run --allow-write -f --unstable
https://deno.land/x/denon/denon.ts
Denon. ( ).
// into denon.json
{
"$schema": "https://deno.land/x/denon/schema.json",
"env": {},
"scripts": {
"start": {
"cmd": "deno run app.ts"
}
}
}
! Denon, denon start
:
➜ denon start
[denon] v2.0.2
[denon] watching path(s): *.*
[denon] watching extensions: ts,js,json
[denon] starting `deno run app.ts`
Compile file:///deno-crashtest/app.ts
[denon] clean exit - waiting for changes before restart
, … ! , app.ts
.
Oak.
Oak - http- Deno, . Koa, @koa/router
.
Oak :
// app.ts
import { Application, Router, Status } from "https://deno.land/x/oak/mod.ts";
// Initialise app
const app = new Application();
// Initialise router
const router = new Router();
// Create first default route
router.get("/", (ctx) => {
ctx.response.status = Status.OK;
ctx.response.body = { message: "It's work !" };
});
app.use(router.routes());
app.use(router.allowedMethods());
console.log("? Deno start !");
await app.listen("0.0.0.0:3001");
, denon start
.
error: Uncaught PermissionDenied: network access to "0.0.0.0:3001",
run again with the --allow-net flag
Deno Node: Deno network
. :
// into denon.json
"scripts": {
"start": {
// add --allow-net
"cmd": "deno run --allow-net app.ts"
}
}
( Postman) localhost:3001 :
{
"message": "It's work !"
}
DenoDB ORM ( , Sqlite3). , Sequelize ( ).
Database
Sqlite3.
|-- DenoRestJwt |-- controllers/ | |-- Database.ts | |-- database/ | | |-- db.sqlite | |-- models/ |-- app.ts
// Database.ts
import { Database } from "https://deno.land/x/denodb/mod.ts";
export class DatabaseController {
client: Database;
/**
* Initialise database client
*/
constructor() {
this.client = new Database("sqlite3", {
filepath: Deno.realPathSync("./controllers/database/db.sqlite"),
});
}
/**
* Initialise models
*/
async initModels() {
this.client.link([]);
await this.client.sync({});
}
}
ORM . , realPathSync
, . --allow-read
--allow-write
denon.json
:
"scripts": {
"start": {
"cmd": "deno run --allow-write --allow-read --allow-net app.ts"
}
}
, , ORM:
|-- DenoRestJwt |-- controllers/ | |-- models/ | |-- User.ts |-- app.ts
// User.ts
import { Model, DATA_TYPES } from "https://deno.land/x/denodb/mod.ts";
import nanoid from "https://deno.land/x/nanoid/mod.ts";
export interface IUser {
id?: string;
firstName: string;
lastName: string;
password: string;
}
export class User extends Model {
static table = "users";
static timestamps = true;
static fields = {
id: {
primaryKey: true,
type: DATA_TYPES.STRING,
},
firstName: {
type: DATA_TYPES.STRING,
},
lastName: {
type: DATA_TYPES.STRING,
},
password: {
type: DATA_TYPES.TEXT,
},
};
// Id will generate a nanoid by default
static defaults = {
id: nanoid(),
};
}
, . (ps: nanoid
UUID, ).
, , : . Bcrypt:
// inside User's class
import * as bcrypt from "https://deno.land/x/bcrypt/mod.ts";
// ...
static async hashPassword(password: string) {
const salt = await bcrypt.genSalt(8);
return bcrypt.hash(password, salt);
}
, ORM :
// Database.ts
import { User } from "./models/User.ts";
export class DatabaseController {
//...
initModels() {
// Add User here
this.client.link([User]);
return this.client.sync({});
}
}
! , , …
User controller
, CRUD:
|-- DenoRestJwt |-- controllers/ | |-- Database.ts | |-- UserController.ts
import * as bcrypt from "https://deno.land/x/bcrypt/mod.ts";
import { IUser, User } from "./models/index.ts";
export class UserController {
async create(values: IUser) {
// Call static user method
const password = await User.hashPassword(values.password);
const user: IUser = {
firstName: values.firstName,
lastName: values.lastName,
password,
};
await User.create(user as any);
return values;
}
async delete(id: string) {
await User.deleteById(id);
}
getAll() {
return User.all();
}
getOne(id: string) {
return User.where("id", id).first();
}
async update(id: string, values: IUser) {
await User.where("id", id).update(values as any);
return this.getOne(id);
}
async login(lastName: string, password: string) {
const user = await User.where("lastName", lastName).first();
if (!user || !(await bcrypt.compare(password, user.password))) {
return false;
}
// TODO generate JWT
}
}
, ORM. JWT.
.
|-- DenoRestJwt |-- routers |-- UserRoute.ts
import { Router, Status } from "https://deno.land/x/oak/mod.ts";
import { UserController } from "../controllers/UserController.ts";
import { BadRequest } from "../helpers/BadRequest.ts";
import { NotFound } from "../helpers/NotFound.ts";
// instantiate our controller
const controller = new UserController();
export function UserRoutes(router: Router) {
return router
.get("/users", async (ctx) => {
const users = await controller.getAll();
if (users) {
ctx.response.status = Status.OK;
ctx.response.body = users;
} else {
ctx.response.status = Status.NotFound;
ctx.response.body = [];
}
return;
})
.post("/login", async (ctx) => {
if (!ctx.request.hasBody) {
return BadRequest(ctx);
}
const { value } = await ctx.request.body();
// TODO generate JWT
ctx.response.status = Status.OK;
ctx.response.body = { jwt };
})
.get("/user/:id", async (ctx) => {
if (!ctx.params.id) {
return BadRequest(ctx);
}
const user = await controller.getOne(ctx.params.id);
if (user) {
ctx.response.status = Status.OK;
ctx.response.body = user;
return;
}
return NotFound(ctx);
})
.post("/user", async (ctx) => {
if (!ctx.request.hasBody) {
return BadRequest(ctx);
}
const { value } = await ctx.request.body();
const user = await controller.create(value);
if (user) {
ctx.response.status = Status.OK;
ctx.response.body = user;
return;
}
return NotFound(ctx);
})
.patch("/user/:id", async (ctx) => {
if (!ctx.request.hasBody || !ctx.params.id) {
return BadRequest(ctx);
}
const { value } = await ctx.request.body();
const user = await controller.update(ctx.params.id, value);
if (user) {
ctx.response.status = Status.OK;
ctx.response.body = user;
return;
}
return NotFound(ctx);
})
.delete("/user/:id", async (ctx) => {
if (!ctx.params.id) {
return BadRequest(ctx);
}
await controller.delete(ctx.params.id);
ctx.response.status = Status.OK;
ctx.response.body = { message: "Ok" };
});
}
, , .
HTTP . . GitHub! , , :
// app.ts
import { DatabaseController } from "./controllers/Database.ts";
import { UserRoutes } from "./routers/UserRoute.ts";
const userRoutes = UserRoutes(router);
app.use(userRoutes.routes());
app.use(userRoutes.allowedMethods());
await new DatabaseController().initModels();
JWT
! JWT .
1.
, :
, "Authorization".
/
Djwt.
|-- DenoRestJwt |-- middlewares/ | |-- jwt.ts
, , .
import { Context, Status } from "https://deno.land/x/oak/mod.ts";
import { validateJwt } from "https://deno.land/x/djwt/validate.ts";
/**
* Create a default configuration
*/
export const JwtConfig = {
header: "Authorization",
schema: "Bearer",
// use Env variable
secretKey: Deno.env.get("SECRET") || "",
expirationTime: 60000,
type: "JWT",
alg: "HS256",
};
export async function jwtAuth(
ctx: Context<Record<string, any>>,
next: () => Promise<void>
) {
// Get the token from the request
const token = ctx.request.headers
.get(JwtConfig.header)
?.replace(`${JwtConfig.schema} `, "");
// reject request if token was not provide
if (!token) {
ctx.response.status = Status.Unauthorized;
ctx.response.body = { message: "Unauthorized" };
return;
}
// check the validity of the token
if (
!(await validateJwt(token, JwtConfig.secretKey, { isThrowing: false }))
) {
ctx.response.status = Status.Unauthorized;
ctx.response.body = { message: "Wrong Token" };
return;
}
// JWT is correct, so continue and call the private route
next();
}
, , . Deno. Denon: Deno .
{
"$schema": "<https://deno.land/x/denon/schema.json>",
// Add env variable
"env": {
"SECRET": "ADRIEN_IS_THE_BEST_AUTHOR_ON_MEDIUM"
},
"scripts": {
"start": {
// add the permission with --allow-env
"cmd": "deno run --allow-env --allow-read --allow-net app.ts"
}
}
}
.
|-- DenoRestJwt |-- routers |-- UserRoute.ts |-- PrivateRoute.ts
:
import { Router, Status } from "https://deno.land/x/oak/mod.ts";
import { jwtAuth } from "../middlewares/jwt.ts";
export function PrivateRoutes(router: Router) {
// call our middleware before our private route
return router.get("/private", jwtAuth, async (ctx) => {
ctx.response.status = Status.OK;
ctx.response.body = { message: "Conntected !" };
});
}
:
import { Router, Status } from "https://deno.land/x/oak/mod.ts";
import { jwtAuth } from "../middlewares/jwt.ts";
export function PrivateRoutes(router: Router) {
// call our middleware before our private route
return router.get("/private", jwtAuth, async (ctx) => {
ctx.response.status = Status.OK;
ctx.response.body = { message: "Conntected !" };
});
}
API /private , :
{
"message": "Unauthorized"
}
2. JWT
. , // TODO generate JWT . User, .
// User.ts
import {
makeJwt,
setExpiration,
Jose,
Payload,
} from "https://deno.land/x/djwt/create.ts";
import { JwtConfig } from "../../middlewares/jwt.ts";
// ...
export class User extends Model {
// ...
static generateJwt(id: string) {
// Create the payload with the expiration date (token have an expiry date) and the id of current user (you can add that you want)
const payload: Payload = {
id,
exp: setExpiration(new Date().getTime() + JwtConfig.expirationTime),
};
const header: Jose = {
alg: JwtConfig.alg as Jose["alg"],
typ: JwtConfig.type,
};
// return the generated token
return makeJwt({ header, payload, key: JwtConfig.secretKey });
}
// ...
}
:
// UserController.ts
export class UserController {
// ...
async login(lastName: string, password: string) {
const user = await User.where("lastName", lastName).first();
if (!user || !(await bcrypt.compare(password, user.password))) {
return false;
}
// Call our new static method
return User.generateJwt(user.id);
}
}
, :
// UserRoute.ts
// ...
.post("/login", async (ctx) => {
if (!ctx.request.hasBody) {
return BadRequest(ctx);
}
const { value } = await ctx.request.body();
// generate jwt
const jwt = await controller.login(value.lastName, value.password);
if (!jwt) {
return BadRequest(ctx);
}
ctx.response.status = Status.OK;
// and return it
ctx.response.body = { jwt };
})
// ...
, , :
// localhost:3001/login
{
"jwt":
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IlEyY0ZZcUxKWk5Hc0toN0FWV0hzUiIsImV4cCI6MTU5MDg0NDU2MDM5MH0.drQ3ay5_DYuXEOnH2Z0RKbhq9nZElWCMvmypjI4BjIk"
}
( )
Authorization
:
// localhost:3001/private with token in headers
{
"message": "Connected !"
}
! API ?.
Deno
Depo, :
URL - : npm i
yarn add
. , Deno, , .
The remote module XXX has not been cached
TypeScript Javascript, . , .
: permissions. , , Deno, , permissions . , , . ( )
, (https://deno.land/x → 460 NPM → + 1 ).
, Deno . + , Node, Deno. , , javascript…
"Node.JS Developer".
" Node.js ".