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

概要
Express の Session ストアとして、MySQL などのデータベースを TypeORM (v0.2) 経由で利用する場合の備忘録です。
TypeORM 設定
Modules
express-session
と connect-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)