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

# 機能設計
### 新規サインアップ
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
}

```