utamt engineer blog

アプリケーション開発について学んだことの備忘録です。

TypeORM を利用して Session を管理する

概要

Express の Session ストアとして、MySQL などのデータベースを TypeORM (v0.2) 経由で利用する場合の備忘録です。

TypeORM 設定

Modules

express-sessionconnect-typeorm をインストールします。

$ npm i @types/express-session express-session
$ npm i connect-typeorm

ormconfig

ormconfig.js に DB の接続情報を記述します。

module.exports = [
  {
    name: 'default',
    type: 'mysql',
    host: 'db',
    port: 3306,
    username: 'root',
    password: 'password',
    database: 'mydb',
    charset: 'utf8mb4',
    synchronize: false,
    logging: false,
    entities: [__dirname + '/src/entity/**/*.ts'],
    migrations: [__dirname + '/src/migration/**/*{.ts,.js}'],
    subscribers: [],
    cli: {
      entitiesDir: 'src/entity',
      migrationsDir: 'src/migration',
      subscribersDir: '',
    },
  }
];

Entity

Session の Entity を作成します。

import { ISession } from "connect-typeorm";
import { Column, Entity, Index, PrimaryColumn } from "typeorm";

@Entity()
export class Session implements ISession {
  @Index()
  @Column("bigint")
  public expiredAt = Date.now();

  @PrimaryColumn("varchar", { length: 255 })
  public id = "";

  @Column("text")
  public json = "";
}

DB Migration

package.json に、DB migration 用のスクリプトを記述しておきます。

  "scripts": {
    "migration:generate": "ts-node $(npm bin)/typeorm migration:generate",
    "migration:run": "ts-node $(npm bin)/typeorm migration:run"
  },

DB migration ファイルを作成のため以下を実行します。

Session の部分は任意で ok です。

$ npm run migration:generate -n Session

このコマンドにより、以下のようなファイルが作成されます。

import {MigrationInterface, QueryRunner} from "typeorm";

export class Session1650179415742 implements MigrationInterface {
    name = 'Session1650179415742'

    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`CREATE TABLE \`session\` (\`expiredAt\` bigint NOT NULL, \`id\` varchar(255) NOT NULL, \`json\` text NOT NULL, INDEX \`IDX_28c5d1d16da7908c97c9bc2f74\` (\`expiredAt\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`DROP INDEX \`IDX_28c5d1d16da7908c97c9bc2f74\` ON \`session\``);
        await queryRunner.query(`DROP TABLE \`session\``);
    }

}

DB migration を実行します。

$ npm run migration:run

これにより DB に Session テーブルが作成されます。

> show columns from session;
+------------+--------------+------+-----+---------+-------+
| Field      | Type         | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| expiredAt  | bigint(20)   | NO   | MUL | NULL    |       |
| id         | varchar(255) | NO   | PRI | NULL    |       |
| json       | text         | NO   |     | NULL    |       |
+------------+--------------+------+-----+---------+-------+
3 rows in set (0.010 sec)

Express アプリ作成

Express から DB に接続し、Session の Read/Write を実行してみます。 ここでは、以下の簡易なログインアプリを作成しています。

  • ページアクセス時に Session を確認し、有効な Session が無い場合は '/login' にリダイレクトする
  • '/login' にてログインが成功した場合、Session を発行する
  • ログアウトした場合、Session を削除する
import express, { Request, Response, NextFunction } from "express";
import session from "express-session";
import bodyParser from "body-parser";
import { createConnection } from "typeorm";
import { TypeormStore } from "connect-typeorm";
import { Session } from "./entity/Session";

// Add field `userId` in session
declare module "express-session" {
  export interface SessionData {
    userId: string;
  }
}

const app = express();
const port = 3000;

const AppServer = async (): Promise<void> => {
  // DB connection
  const connection = await createConnection();
  const sessionRepository = connection.getRepository(Session);

  // Session settings
  app.use(
    session({
      secret: "session-sample",
      resave: false,
      saveUninitialized: false,
      cookie: {
        path: "/",
        httpOnly: true,
        secure: false,
        maxAge: 86400000,
      },
      store: new TypeormStore({
        cleanupLimit: 2,
        limitSubquery: false,
        ttl: 3600, // 60 minutes
      }).connect(sessionRepository),
    })
  );

  app.use(bodyParser.urlencoded({ extended: true }));

  app.get("/login", (req: Request, res: Response) => {
    res.type("text/html").send(
      `
      <form method="POST" action="/login">
        <div>UserID<input type="text" name="userId"></div>
        <div>Password<input type="password" name="password"></div>
        <div><input type="submit" value="Login"></div>
      </form>
      `
    );
  });

  app.post("/login", async (req: Request, res: Response) => {
    const { userId, password } = req.body;
    if (userId === "admin" && password === "password") {
      req.session.regenerate((err) => {
        req.session.userId = userId;
        res.redirect('/');
      });
    } else {
      res.redirect("/login");
    }
  });

  app.get("/logout", (req: Request, res: Response) => {
    req.session.destroy((err) => {
      res.redirect("/");
    });
  });

  // If no valid session, redirect to login page
  app.use((req: Request, res: Response, next: NextFunction) => {
    if (req.session.userId) {
      next();
    } else {
      res.redirect("/login");
    }
  });

  app.get("/", async (req: Request, res: Response) => {
    res.type("text/html").send(
      `
      <div>Hello ${req.session.userId}</div>
      <div><a href="/logout">Logout</a></div>
      `
    );
  });

  app.listen(port, () => {
    console.log(`App listening on port ${port}`);
  });
};

AppServer();

動作確認

Express アプリを起動し動作を確認します。

初回アクセス時

Session が無いため、ログインページにリダイレクトされます。

ログイン後

Session に保持した userId ("admin") が画面に表示されます。

DB を見ると、確かに Session 情報が保持されています。

> select * from session;
+---------------+----------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
| expiredAt     | id                               | json                                                                                                                                   |
+---------------+----------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
| 1650648411894 | iNJEcXhrQF8y4v-OXC72qVoqZH740vUK | {"cookie":{"originalMaxAge":86400000,"expires":"2022-04-23T16:26:51.881Z","secure":false,"httpOnly":true,"path":"/"},"userId":"admin"} |
+---------------+----------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
Empty set (0.001 sec)

ログアウト後

DB から Session 情報が削除されたため、ログインページにリダイレクトされます。

> select * from session;
Empty set (0.001 sec)