0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeExpressTutorial - 9

Posted at

やりたいこと

  • GET api/auth/me にリクエストをするとユーザー情報が返却されるようにする
  • 認可を制御する処理をミドルウェアとして設置する
  • Express.js の Request の型定義を上書きしてアプリ全体で使えるようにする
全体像
  1. プロジェクトを作成する
  2. Mysql + prisma でDBを構築する
  3. 環境変数を設定する
  4. APIを作成する
  5. ユーザー登録とログインを実装する
  6. オリジナルのエラーハンドリングを構築する
  7. zod を使ったバリデーションを構築する
  8. エラーハンドリングのリファクタリング
  9. 認可の処理を実装する
  10. 10
  11. 11
  12. 12

作業対象のファイル

  • src/types/express/index.d.ts
  • tsconfig.json
  • src/middlewares/auth.ts
  • src/routes/auth.ts

Express.js の Request型 に userプロパティ を追加してアプリ全体で使えるようにする

src/types/express/index.d.ts
// ↓こちらの記述方法も在るが、使用しない
// import { User } from '@prisma/client'

// declare global {
//   namespace Express {
//     interface Request {
//       user?: User;
//     }
//   }
// }

// ↓のコードを適用
import { User } from '@prisma/client'
import express  from 'express'

declare module 'express' {
  export interface Request {
    user?: User  // userプロパティをオプショナルとして追加
  }
}

解説

そもそも d.ts とは?

  • d.ts 拡張子を持つファイルは「宣言ファイル」と呼ばれ、既存の JavaScript コードや外部ライブラリの型情報を提供するファイル形式のこと
  • 今回のように、既存のモジュールやライブラリの 型定義を拡張する のに使用される
  • 型を拡張する方法として、declare global を使ってグローバルスコープに定義して拡張する or declare module を使ってモジュールを直接上書きして拡張する 方法がある
  • 上記の2通りの方法ともに、 Express の Requstオブジェクト に user プロパティが存在すること を定義してTypeScript に認識させている

declare global とは

  • グローバルスコープに新しい型定義を追加する方法
  • declare global { } とすることで、グローバルスコープに新しい型定義を追加することができる
    • 他のライブラリが同じグローバル名前空間を使用している場合に衝突する可能性があるため declare module を使用することが推奨される
    • (コメントあうとしている記述例では)グローバルスコープで Express の名前空間を拡張して Request インターフェースに user プロパティを追加している

declare module とは

  • モジュールを直接拡張する方法
  • declare module 'express' { } -> とすることで、特定のモジュール(この場合は 'express')の型定義を拡張することができる
  • 今回のケースでは、express モジュールの型定義を拡張して Request オブジェクトのインターフェースに user プロパティを追加している
  • 特定のモジュールの型定義を拡張する場合に使用され、モジュールの既存の型定義とマージされる
  • 特定のモジュールにのみ影響を与えるため、他のライブラリとの衝突リスクが低くなる

tsconfig.json に上書きした型を読み込ませるための記述をする

これ設定しないとエラるので気をつけてね(ここ分からなくてクソ嵌った)

tsconfig.json
{
  "compilerOptions": {
    ・・・
    "typeRoots": [
      "./src/types", // <-追加
      "./node_modules/@types" // <-追加
    ],
    ・・・
  }
}

解説

  • TypeScriptは typeRoots に指定されたディレクトリを順番に検索される
  • ./node_modules/@types を含めると、そこにある型定義が優先されることがあるため、記述の順番には注意
  • 下記のパターンだと ./node_modules/@types の定義が優先して読み込まれる
// NGパターン
    "typeRoots": [
      "./node_modules/@types",
      "./src/types"
    ],

認可処理の実装

src/middlewares/auth.ts
import { NextFunction, Request, Response } from 'express'
import jwt, { JwtPayload } from 'jsonwebtoken'

import { prisma } from '..'
import { ErrorCode } from '../exceptions/root'
import { UnauthorizedException } from '../exceptions/unauthorized'
import { JWT_SECRET } from '../utils/secrets'

export const authMiddleware = async(req: Request, res: Response, next: NextFunction) => {
  // リクエストヘッダーから認証トークン(JWT)を取得
  const token = req.headers.authorization
  
  // トークンが存在しない場合、未認証エラーを返す
  if (!token) {
    return next(new UnauthorizedException('アクセスしようとしたページは表示できませんでした', ErrorCode.UNAUTHORIZED))
  }
  
  try {
    // トークンが存在する場合、そのトークンを検証し、ペイロードを取得
    const payload = jwt.verify(token, JWT_SECRET) as JwtPayload & { userId: number }
    const user = await prisma.user.findFirst({
      where: {
        id: payload.userId,
      },
    })

    if (!user) {
      return next(new UnauthorizedException('アクセスしようとしたページは表示できませんでした', ErrorCode.UNAUTHORIZED))
    }

    // 取得した user の情報を Requetオブジェクト に含める
    req.user = user
    next()
  } catch (error) {
    return next(new UnauthorizedException('アクセスしようとしたページは表示できませんでした', ErrorCode.UNAUTHORIZED))
  }
}

解説

そもそも Express.js の Request Response とは

  • Request : クライアント(通常はブラウザ)からサーバーに送られてくる情報を格納するオブジェクトのこと
    • URL パラメータ (req.params)
    • クエリ文字列 (req.query)
    • HTTP ヘッダー (req.headers)
    • リクエストボディ (req.body) - POST リクエストのデータなど
    • クッキー (req.cookies)
    • セッション情報 (req.session) - セッションミドルウェアを使用している場合
    • リクエストメソッド (req.method) - GET, POST, PUT, DELETE など
  • Response : サーバーからクライアントに送られてくる情報を格納するオブジェクトのこと

処理でやっていること

  • ユーザーが存在する場合、そのユーザー情報をリクエストオブジェクトに追加し、次のミドルウェアに処理を渡す処理をしている
  • 通常のリクエストオブジェクトには user プロパティが存在しないため、 src/types/express/index.d.ts で型を拡張して定義して user プロパティを認識できるようにしている
  • クライアントから送られてきた情報を変更するのではなく、サーバーサイドでの処理の過程で得られた情報(userの情報)を リクエストオブジェクト 保存している
  • ここで保存した情報(userの情報)は、同じリクエストを処理する後続のミドルウェアやルートハンドラーで利用可能となる

ミドルウェアを適用させる

src/routes/auth.ts
import { Router } from 'express'

import { login, me, signup } from '../controllers/auth'
import { errorHnadler } from '../utils/error-handler'
import { authMiddleware } from '../middlewares/auth' // <-追加

const authRouter: Router = Router()

authRouter.post('/signup', errorHnadler(signup))
authRouter.post('/login', errorHnadler(login))
authRouter.get('/me', [authMiddleware], errorHnadler(me)) // <-追記

export default authRouter
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?