Дино (Deno): Создать API для отдыха с помощью JWT

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В преддверии старта курса "Node.JS Developer" приглашаем всех желающих посмотреть открытый урок на тему "Докеризация Node.js приложений".

А сейчас делимся традиционным переводом полезного материала. Приятного чтения.


Со времен первой версии Deno стал модным словом для разработчиков Javascript/TypeScript/Node. Давайте погрузимся в эту технологию, создав защищенный с помощью JWT REST API.

Желательно уже иметь некоторые основы в Node и его экосистеме (Express, Nodemon, Sequelize и т.д.), чтобы следовать этому руководству.

Что такое Дино (Deno)?

Deno - это простая, современная и безопасная среда выполнения JavaScript и TypeScript, которая использует V8 и встроена в 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 приложений".


ЗАБРАТЬ СКИДКУ

Источник: https://habr.com/ru/company/otus/blog/532582/


Интересные статьи

Интересные статьи

Знакомьтесь, эталонная нота ля первой октавы (440 Гц): Звучит больно, не правда ли? Что еще говорить о том, что одна и та же нота звучит по-разному на разных музыкальных инструме...
В одной из предыдущих статей цикла про гипервизор Proxmox VE мы уже рассказывали, как выполнять бэкап штатными средствами. Сегодня покажем, как для этих же целей использовать отличн...
Доброго времени суток, друзья! Представляю вашему вниманию перевод статьи «Graceful asynchronous programming with Promises» с MDN. «Обещания» (промисы, promises) — сравнительно новая особен...
Привет, Хабр! Сегодня мы построим систему, которая будет при помощи Spark Streaming обрабатывать потоки сообщений Apache Kafka и записывать результат обработки в облачную базу данных AWS RDS. ...
Одной из целей хостинг-провайдера является максимально возможная утилизация имеющегося оборудования для предоставления качественного сервиса конечным пользователям. Ресурсы конечных серверов ...