1
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?

Expressでログイン実装を試した

Posted at

機能設計

新規サインアップ

DBにユーザテーブルに対して新規ユーザーを登録する
JWTでトークンを生成したセッションテーブルに登録する
トークンをクライアントへ返す

ログイン

IDとパスワードでログイン認証

ログアウト

トークンをセッションテーブルから削除(無効化)

フォルダ構成

src/
├── controllers/(ルートに対応する処理(コントローラー)を書く場所)
├── middleware/(リクエストとレスポンスの間に挟む処理)
├── lib/prisma(データ構造やORMの定義(自動生成))
├── routes/(エンドポイントとコントローラーの対応づけ)
├── services/(ビジネスロジックを書く場所)
├── utils/(汎用的な関数・ユーティリティ置き場)
├── types/(TypeScript型定義)
└── index.ts(エントリーポイント)
prisma/
  └─ schema.prisma(Prismaのモデルを編集する場所)
.env(環境変数)

ソースコード

index.ts

APIの入り口
いまはauthルーターしかないが、各機能種別でrouterをわけたいと考えています。

import express, { Request, Response } from "express";
import dotenv from "dotenv";
import authRoutes from "./routes/auth";
import { authenticateToken } from "./middleware/auth";

// 環境変数読み込む
dotenv.config();

// Express アプリケーションのインスタンスを作成
const app = express();
// application/json のリクエストボディを自動でパースしてくれるミドルウェア
app.use(express.json());

// 全てのリクエストでトークン検査を行う
app.use(authenticateToken);

app.use("/auth", authRoutes);

app.get("/", (req: Request, res: Response) => {
  res.send("Hello");
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

ルーター

APIの定義

import { Router } from "express";
import * as auth from "../controllers/auth";

const router = Router();

router.post("/login", auth.login);
router.post("/logout", auth.logout);
router.post("/signup", auth.signup);

export default router;

コントローラー

各API処理をコントロールする
必要なサービスを呼び出し、処理を依頼する
responseを作りクライアントへ返す

import { Request, Response } from "express";
import * as auth from "../services/auth";

export const login = async (req: Request, res: Response) => {
  const { username, password } = req.body;

  const token = await auth.authenticateUser(username, password);
  if (token) {
    res.json({ token });
  } else {
    res.status(401).json({ message: "Invalid credentials" });
  }
};

export const signup = async (req: Request, res: Response) => {
  const { username, password } = req.body;
  if (!username || !password) {
    res.status(400).json({ message: "メールとパスワードは必須です。" });
  }

  const existingUSer = await auth.findUserByUsername(username);
  if (existingUSer) {
    res.status(409).json({ message: "すでに登録されています。" });
  }

  const token = await auth.createUser(username, password);

  res.status(201).json({ token });
};

export const logout = async (req: Request, res: Response) => {
  // "Bearer token" 形式でトークンを取得
  const token = req.header("Authorization")?.split(" ")[1] || "";

  await auth.logout(token);

  res.status(200).json({ message: "ログアウトしました!" });
};

export const protectedRoute = async (req: Request, res: Response) => {
  res.json({ message: "This is protected data" });
};


サービス

DBへアクセスし、CRUD処理をする
処理結果をコントローラーへ返す

import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";

import prisma from "../lib/prisma";

export const authenticateUser = async (
  username: string,
  password: string
): Promise<string | null> => {
  const user = await prisma.user.findUnique({ where: { username } });
  if (!user) return null;

  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) return null;

  // Token作成
  const token = jwt.sign(
    { id: user.id, username: user.username },
    process.env.JWT_SECRET as string,
    {
      expiresIn: "1h",
    }
  );

  const expiresAt = new Date(Date.now() + 60 * 60 * 1000);

  // TokenをSessionテーブルに登録
  await prisma.session.create({
    data: {
      token,
      userId: user.id,
      expiresAt,
    },
  });

  return token;
};

export const createUser = async (username: string, password: string) => {
  const hashedPassword = await bcrypt.hash(password, 10);

  // ユーザをUserテーブルに新規登録
  const user = await prisma.user.create({
    data: {
      username,
      password: hashedPassword,
    },
  });

  // Token作成
  const token = jwt.sign(
    { id: user.id, username: user.username },
    process.env.JWT_SECRET as string,
    {
      expiresIn: "1h",
    }
  );

  const expiresAt = new Date(Date.now() + 60 * 60 * 1000);

  // TokenをSessionテーブルに登録
  await prisma.session.create({
    data: {
      token,
      userId: user.id,
      expiresAt,
    },
  });

  return token;
};

export const logout = async (token: string) => {
  if (token) {
    return await prisma.session.deleteMany({
      where: { token },
    });
  }
};

export const findUserByUsername = async (username: string) => {
  return await prisma.user.findUnique({
    where: { username },
  });
};

ミドルウエア

APIコントローラーに入る前の全体共通処理
トークンのチェックを行う
(一部工事中)

import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import prisma from "../lib/prisma";

export const authenticateToken = async (
  req: Request,
  res: Response,
  next: NextFunction
): Promise<void> => {
  // "Bearer token" 形式でトークンを取得
  const token = req.header("Authorization")?.split(" ")[1];

  const isLogin = req.path === "/auth/login";
  const isSignup = req.path === "/auth/sighup";

  // トークンがない場合
  if (!token) {
    // ログインかサインアップしたい人の場合はログイン処理をさせる
    if (isLogin || isSignup) {
      next();
      return;
    } else {
      // その他画面へアクセスしたい人の場合は止める
      res
        .status(401)
        .json({ message: "Tokenがないため、ログインが必要です。" });
      return;
    }
  }

  // トークンがある場合、検証を行う
  try {
    // トークンのDecode
    const decodedToken = jwt.verify(token, process.env.JWT_SECRET as string);

    // セッションの存在チェック
    const session = await prisma.session.findUnique({
      where: { token },
    });

    // セッションの有効期限チェック
    if (!session || session.expiresAt < new Date()) {
      res.status(401).json({ message: "セッション切れです。" });
    }

    // トークンが有効が、ログインかサインアップが来た → ホームへリダイレクト
    if (isLogin || isSignup) {
      res.status(302).redirect("/");
      return;
    }

    // ユーザー情報をリクエストに追加
    (req as any).user = { user: decodedToken };

    next();
    return;
  } catch (err) {
    res.status(403).json({ message: "無効なトークンです。" });
    return;
  }
};

schema.prisma

DBモデルの定義

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

generator client {
  provider = "prisma-client-js"
  output   = "../src/lib/prisma-client"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int       @id @default(autoincrement())
  username  String    @unique
  password  String
  createdAt DateTime  @default(now())
  Session   Session[]
}

model Session {
  id        Int      @id @default(autoincrement())
  token     String   @unique
  userId    Int
  user      User     @relation(fields: [userId], references: [id])
  createdAt DateTime @default(now())
  expiresAt DateTime
}

1
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
1
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?