Untuk mengantisipasi dimulainya kursus "Pengembang Node.JS", kami mengundang semua orang untuk menonton pelajaran terbuka dengan topik "Dockerisasi aplikasi Node.js" .
Dan sekarang kami membagikan terjemahan tradisional dari materi yang bermanfaat. Selamat membaca.
Sejak versi pertama, Deno telah menjadi kata kunci untuk Javascript / TypeScript / Node. Mari selami teknologi ini dengan membuat REST API yang diamankan oleh JWT.
Disarankan untuk memiliki beberapa dasar di Node dan ekosistemnya (Express, Nodemon, Sequelize, dll.) Untuk mengikuti tutorial ini.
Apa itu Dino (Deno)?
Deno adalah runtime JavaScript dan TypeScript yang sederhana, modern, dan aman yang menggunakan V8 dan dibangun ke dalam Rust.
Sudah ada banyak artikel yang merinci topik ini, jadi saya tidak akan membahasnya. Saya dapat merekomendasikan yang ini .
pengantar
Sejak rilis resmi V1 , Deno telah menjadi kata kunci dalam beberapa minggu (untuk bersenang-senang, berikut adalah kurva popularitas untuk pencarian "deno" di 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 ".