Antecipando o início do curso "Desenvolvedor Node.JS", convidamos a todos a assistir a aula aberta sobre o tópico "Dockerização de aplicativos Node.js" .
E agora estamos compartilhando a tradução tradicional de material útil. Gostar de ler.
Desde a primeira versão, Deno se tornou uma palavra da moda para Javascript / TypeScript / Node. Vamos mergulhar nessa tecnologia criando uma API REST protegida por JWT.
É aconselhável já ter noções básicas de Node e seu ecossistema (Express, Nodemon, Sequelize, etc.) para seguir este tutorial.
O que é Dino (Deno)?
Deno é um tempo de execução de JavaScript e TypeScript simples, moderno e seguro que usa V8 e é integrado ao Rust.
, , . .
V1, Deno " " ( , "deno" Google).
Typescript and Javascript"?
, JWT REST API .
Node.js Express.
REST API, :
ORM
CRUD-
JWT
REST API JWT, :
Deno ( : )
VSCode Deno,
( ):
Denon
Oak
Djwt
Denodb
Bcrypt
-, , " " .
|-- 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"
}
}
}
(ps: , )
.
|-- 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 ?.
Github: ( , ).
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 ".