Dino: Buat Rest API dengan JWT

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, :





  • 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 ".













All Articles