Reactとバックエンドで作る！実践Web開発入門 | 第7章: ユーザー認証の実装

## はじめに
第6章でデータベースを追加しました。今回は、ユーザー認証（登録・ログイン）を追加し、商品追加をログイン済みユーザーに限定します。JWT（JSON Web Token）を使って安全に認証を行います。

## 目標
- ユーザー登録とログイン機能を実装する  
- JWTで認証を管理する  
- APIを保護する  

## バックエンドの準備
`backend`で必要なパッケージをインストール：
```bash
npm install bcryptjs jsonwebtoken
```

## ユーザー認証の実装
`backend/index.js`を以下のように更新：
```javascript
const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const app = express();
const port = 5000;

app.use(express.json());
app.use(cors());

mongoose.connect('mongodb://localhost:27017/productdb', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
}).then(() => console.log('MongoDB connected'));

// ユーザースキーマ
const userSchema = new mongoose.Schema({
  username: String,
  password: String,
});
const User = mongoose.model('User', userSchema);

// 商品スキーマ
const productSchema = new mongoose.Schema({
  name: String,
  price: Number,
});
const Product = mongoose.model('Product', productSchema);

// JWT認証ミドルウェア
const authenticateToken = (req, res, next) => {
  const token = req.headers['authorization']?.split(' ')[1];
  if (!token) return res.status(401).json({ message: 'トークンが必要です' });

  jwt.verify(token, 'secret_key', (err, user) => {
    if (err) return res.status(403).json({ message: '無効なトークン' });
    req.user = user;
    next();
  });
};

// ユーザー登録
app.post('/register', async (req, res) => {
  const { username, password } = req.body;
  const hashedPassword = await bcrypt.hash(password, 10);
  const user = new User({ username, password: hashedPassword });
  await user.save();
  res.status(201).json({ message: 'ユーザー登録完了' });
});

// ログイン
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const user = await User.findOne({ username });
  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({ message: '認証失敗' });
  }
  const token = jwt.sign({ id: user._id }, 'secret_key', { expiresIn: '1h' });
  res.json({ token });
});

// APIエンドポイント
app.get('/products', async (req, res) => {
  const products = await Product.find();
  res.json(products);
});

app.post('/products', authenticateToken, async (req, res) => {
  const { name, price } = req.body;
  const product = new Product({ name, price });
  await product.save();
  res.status(201).json(product);
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
```
- `bcrypt`でパスワードをハッシュ化。
- `jsonwebtoken`でトークンを生成。
- `authenticateToken`で保護されたルートを制限。

## React側の更新
`frontend/src`に`AuthContext.tsx`を作成：
```tsx
import React, { createContext, useState, useContext } from 'react';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [token, setToken] = useState(localStorage.getItem('token') || '');

  const login = (newToken) => {
    setToken(newToken);
    localStorage.setItem('token', newToken);
  };

  const logout = () => {
    setToken('');
    localStorage.removeItem('token');
  };

  return (
    <AuthContext.Provider value={{ token, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuthContext = () => useContext(AuthContext);
```

`App.tsx`を更新：
```tsx
import React, { useEffect } from 'react';
import './App.css';
import { ProductProvider, useProductContext } from './ProductContext';
import { AuthProvider, useAuthContext } from './AuthContext';

function App() {
  return (
    <AuthProvider>
      <ProductProvider>
        <div className="App">
          <Header />
          <ProductList />
          <Footer />
        </div>
      </ProductProvider>
    </AuthProvider>
  );
}

const Header = () => {
  const { token, logout } = useAuthContext();
  return (
    <header>
      <h1>商品リスト</h1>
      {token && <button onClick={logout}>ログアウト</button>}
    </header>
  );
};

const ProductList = () => {
  const { products, setProducts } = useProductContext();
  const { token, login } = useAuthContext();
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [newProduct, setNewProduct] = React.useState({ name: '', price: '' });
  const [credentials, setCredentials] = React.useState({ username: '', password: '' });

  useEffect(() => {
    const fetchProducts = async () => {
      try {
        const response = await fetch('http://localhost:5000/products');
        if (!response.ok) throw new Error('データの取得に失敗しました');
        const data = await response.json();
        setProducts(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchProducts();
  }, [setProducts]);

  const handleAddProduct = async (e) => {
    e.preventDefault();
    if (!token) return alert('ログインしてください');
    try {
      const response = await fetch('http://localhost:5000/products', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${token}`,
        },
        body: JSON.stringify({ name: newProduct.name, price: Number(newProduct.price) }),
      });
      if (!response.ok) throw new Error('商品の追加に失敗しました');
      const addedProduct = await response.json();
      setProducts((prev) => [...prev, addedProduct]);
      setNewProduct({ name: '', price: '' });
    } catch (err) {
      setError(err.message);
    }
  };

  const handleLogin = async (e) => {
    e.preventDefault();
    try {
      const response = await fetch('http://localhost:5000/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials),
      });
      if (!response.ok) throw new Error('ログインに失敗しました');
      const { token } = await response.json();
      login(token);
    } catch (err) {
      setError(err.message);
    }
  };

  if (loading) return <main>読み込み中...</main>;
  if (error) return <main>エラー: {error}</main>;

  return (
    <main>
      {!token ? (
        <form onSubmit={handleLogin}>
          <h2>ログイン</h2>
          <input
            type="text"
            placeholder="ユーザー名"
            value={credentials.username}
            onChange={(e) => setCredentials({ ...credentials, username: e.target.value })}
          />
          <input
            type="password"
            placeholder="パスワード"
            value={credentials.password}
            onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
          />
          <button type="submit">ログイン</button>
        </form>
      ) : (
        <>
          <h2>商品一覧</h2>
          <form onSubmit={handleAddProduct}>
            <input
              type="text"
              placeholder="商品名"
              value={newProduct.name}
              onChange={(e) => setNewProduct({ ...newProduct, name: e.target.value })}
            />
            <input
              type="number"
              placeholder="価格"
              value={newProduct.price}
              onChange={(e) => setNewProduct({ ...newProduct, price: e.target.value })}
            />
            <button type="submit">追加</button>
          </form>
          <ul>
            {products.map((product) => (
              <ProductItem key={product._id} name={product.name} price={product.price} />
            ))}
          </ul>
        </>
      )}
    </main>
  );
};

const ProductItem = ({ name, price }) => (
  <li>
    {name} - {price}円
  </li>
);

const Footer = () => (
  <footer>
    <p>© 2025 React Web開発入門</p>
  </footer>
);

export default App;
```

## 動作確認
1. バックエンドを起動（`node index.js`）。
2. フロントエンドを起動（`npm start`）。
3. `/register`でユーザーを作成（PostmanなどでPOSTリクエスト）。
4. ログイン後、商品追加が可能に。

## 次回予告
第8章では、アプリを本番環境にデプロイする方法を学びます。お楽しみに！

---

この記事が役に立ったら、ぜひ「いいね」や「ストック」をお願いします！質問や感想はコメント欄で気軽にどうぞ。次回も一緒に学びましょう！