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