🦏

ReactとDjangoで認証機能作ってみた(サインアップ機能編)

2024/09/20に公開

元美容師のDjangoポートフォリオリニューアル日記Part.7:認証機能(サインアップ機能編)

こんにちは、やぎです!前回の記事では、サロン情報に画像の登録ができるような実装をしました。
今回は認証機能の第一回目としてまずはサインアップ機能の追加をしてみます!
本当はログインやアクセス権限なども一気にやりたいのですが記事のボリュームが大きくなりすぎてしまう為、分けることにしました。
気長にお待ちください。。。

それでは今回も頑張ります!実装開始!!

既存のApp.jsのコードを別のコンポーネントに移植

※この部分は前回までの記事での実装内容の変更についてですので、本題とは少しずれます。サインアップ機能の実装からご覧になりたい方はサインアップ機能の実装までスキップしてください。

現在の実装ではApp.jsにサロン登録機能をを表示させるコードを記載していますが、今回の実装からサインアップ画面やログイン画面を表示するため、App.jsにはルーティングの機能のみを記載します。
新たにHome画面専用のHome.jsを作成し、既存のApp.jsの機能をHome.jsに移植します。
そして新しいApp.jsでは'react-router-dom'を使用してルーティングの設定を記述していきます。
今回はサインアップ、ログイン機能がメインテーマのため、ルーティングに関しては簡潔に記載します。
今後別の記事でルーティングについて記載予定です。

react-router-domをインストール

ターミナル等から以下のコマンドでreact-router-domをインストールします。

npm install react-router-dom

Home.jsを作成

components/Home.jsを作成し、既存のApp.jsのコードをそのまま移植します。
変更点は関数名をApp()からHome()に、exportもAppからHomeに変更します。

コードは以下の通りです。

//{インポート関係}

function Home() { //変更 関数名をApp()からHome()に変更
  //{App.jsのコードをそのまま使用}
}

export default Home; //変更 AppからHomeに変更

App.jsを更新

App.jsの既存のコードを削除して、新たにreact-router-domを使用して、ルーティングの設定をします。
まずは先ほど作成したHome.jsを表示させるため以下のコードを記載します。

import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Home from './components/Home';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        {/* 他のルートはここに追加します */}
      </Routes>
    </Router>
  );
}

export default App;

この後の実装でサインアップ画面やログイン画面を作成し、App.jsでルーティングできるように追加します。

ルーティングが正しく機能しているか確認

では、サーバーを起動して、サロン一覧が正しく表示されるか確認します。

エラーが表示されましたw
SalonListやSalonFormが見つからない的な感じですかねw

原因はHome.jsにApp.jsのコードをそのままコピペしたので、importする際にのパスが間違えていました。
Home.jsはcomponentsフォルダに移動したので以下のように修正します

// 誤ったコード
import SalonList from './components/SalonList';
import SalonForm from './components/SalonForm';
import Modal from './components/Modal';

// 正しいコード
import SalonList from './SalonList';
import SalonForm from './SalonForm';
import Modal from './Modal';

もう一度画面を確認します。

無事に表示されました。
これでルーティングは上手くいっていることがわかりました。
ファイルを移動した際はパスの記載ミスが発生しやすいので気をつけます。
では次から本題です!

サインアップ機能の実装

それでは本題のサインアップやログイン機能の実装にとりかかります。
まずはサインアップ機能から実装していきます。

このアプリのサインアップ機能で重要なのはサロンオーナーさんと美容師さんでサインアップの入り口を分ける点です。
イメージとしては「サロンオーナーとして登録」「美容師として登録」のボタンをそれぞれ用意し、押したボタンによってユーザーのタイプを区別するようにします。

バックエンド(Django)の実装

Userモデルを作成

まずはサインアップ時にユーザーのデータを登録する為のモデルを作成します。
ここではDjangoのmodelsに備わっているAbstractUserを使用します。
AbstractUserにはユーザー情報を登録する際に必要なさまざまなフィールドが最初から備わっています。
事前に備わっているのは以下のフィールドです。
username
first_name
last_name
email
password
is_staff
is_active
date_joined

その為今回使用するフィールドの内、パスワードやメールアドレスのフィールドは追加する必要がなく、事前に備わっていないフィールドのみ追加すればOKです!
今回はサロンオーナーと美容師を区別するためのuser_type、電話番号のphone_number、紹介文のprofile_infoを追加します。
またuser_typeを区別するための選択肢(ownerとstylist)を記載します。

api/models.py に以下のコードを追加します

from django.contrib.auth.models import AbstractUser #追記
from django.db import models

class User(AbstractUser): #追記
    USER_TYPE_CHOICES = (
        ('owner','サロンオーナー'),
        ('stylist','美容師'),
    )
    user_type = models.CharField(max_length=10, choices=USER_TYPE_CHOICES)
    phone_number = models.CharField(max_length=15, blank=True)
    profile_info = models.TextField(blank=True)

    def __str__(self):
        return self.username

# {既存のsalonモデル}

シリアライザーの追記

つづいてシリアライザーの設定をします!
serializers.pyを更新します

ここで重要な点は以下の2点です。
・passordをwrite_only: Trueとすることでpasswordフィールドのデータを書き込み専用にします。何言ってんだと思いますよねw
簡潔に説明するとAPIのレスポンスにpasswordを含めないようにしてくれるので情報漏洩などのリスクを減らしてくれます!あとはパスワードの更新時に、他の全てのフィールドを再送する必要がなくなります。

・create_userメソッドを使用することでシリアライズの際にpasswordを勝手に暗号化(ハッシュ化)してくれます!
以前salonのシリアライザーを作成した際はあえてdef createは書いていませんが、これはModelSerializerにデフォルトでcreateメソッドが含まれており、特に暗号化の必要もないシンプルなデータのためです。

それではserializers.pyに追記するコードは以下の通りです。
salonのシリアライザーと差がわかるようにどちらも記載しておきます。

from rest_framework import serializers
from .models import Salon,User

class UserSerializer(serializers.ModelSerializer): #追記 Userのシリアライザー
    class Meta:
        model = User
        fields = ('id', 'username', 'email', 'password', 'user_type', 'phone_number', 'profile_info')
        extra_kwargs = {'password':{'write_only': True}} #パスワードフィールドを書き込み専用にする
    
    def create(self, validated_data): #create時にcreate_userメソッドを使用
        user = User.objects.create_user(**validated_data)
        return user

class SalonSerializer(serializers.ModelSerializer): #salonのシリアライザー
    class Meta:
        model = Salon
        fields = ['id', 'name', 'address', 'description','phone_number','email','image','created_at','updated_at']

    def get_image(self, obj):
        if obj.image:
            return self.context['request'].build_absolute_uri(obj.image.url)
        return None

viewの追記

つづいてviews.pyを更新してサインアップ時のviewを追加します。
ここで重要なポイントは以下の点です。

・以前作成したSalonListCreateViewやSalonUpdateDeleteViewはrest_frameworkのgenericsを使用しましたが、今回のUserSignupViewはrest_frameworkのAPIViewを使用しています。
それぞれ特徴があるので簡単に説明します。
generics:標準的なCRUD操作に対応しておりシンプルに使えるが、パスワードのハッシュ化等の複雑な処理には向かない

APIView:カスタマイズに優れており柔軟にに扱える。その分自身で書くコードの量が増える。

今回はUserの処理の中でパスワードのハッシュ化が含まれていること、今後ユーザー登録した際に機能を追加しやすいようにするためAPIviewを使用しています。またサインアップの際はCRUD処理のうち登録しかしないため使用するのはpostメソッドのみです。

#{その他のimport}
from rest_framework import status 
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import UserSerializer

class UserSignup(APIView): APIViewを継承
    def post(self, request):
        serializer = UserSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response({"message": "ユーザー登録が完了しました"}, status=status.HTTP_201_CREATED) #post成功時のレスポンス
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) #エラー時のレスポンス

#{SalonListCreateView,SalonUpdateDeleteView}

urls.pyの更新

api/urls.pyにサインアップ時のパスを追加します。
ここは特に特記はありません。

from django.urls import path
from .views import SalonListCreateView,SalonUpdateDeleteView,UserSignupView

urlpatterns = [
    path('signup/', UserSignupView.as_view(), name='user-signup'), #追記
    #{その他のパス}
]

settings.pyにカスタムユーザーを使用する記載を追加

settings.pyに以下のコードを追記します.

AUTH_USER_MODEL = 'api.User'

この追記をしないと、せっかくカスタムユーザーモデルを作成してもデフォルトのユーザーモデルとして扱われてしまいます。
今回作成したuser_typeなどは認識されず、バリデーション等でエラーも発生してしまします。
この設定は途中で変更すると改修が大変なのでカスタムユーザーモデルを使用する際はかならずプロジェクト開始時に設定するようにしましょう。

マイグレーションを実行

モデルの更新をしたのでマイグレーションを実行しますが、ここで問題が発生しました。
今まで使用していたアプリのDBが新しく追加したカスタムユーザーモデルと競合を起こしてしまい以下のエラーが発生しました。

ValueError: The field admin.LogEntry.user was declared with a lazy reference to 'api.user', but app 'api' doesn't provide model 'user'.

この場合は、既存のDBとマイグレーションをリセットし、再度マイグレーションをし直すことで解決しました。以前までに作成したサロン情報が消えますが、そこまで影響はないので良かったです。
先ほども書きましたがUserモデルの設定などはプロジェクトで一番先に設定するべきですね。
※この操作はDBの内容をリセットするので注意してください。

以下のコマンドをターミナルで実行しました。

Copyrm db.sqlite3  # SQLiteを使用している場合
rm api/migrations/000*.py  # 既存のマイグレーションファイルを削除

これで既存のDBとマイグレーションファイルを削除できます。
続いてマイグレーションを実施します。

python manage.py makemigrations
python manage.py migrate

無事にマイグレーションが成功しました。
念の為サーバーを起動してアプリが動作するか確認しました。

DBを削除しているので、以前までに登録したサロンデータは消えており、「サロンは登録されていません」が正しく表示されています。

スーパーユーザーの登録を実施

この時点で最高権限をもつスーパーユーザーの作成も実施します。
スーパーユーザーはDjangoの管理画面(admin site)にアクセスし、すべてのデータを管理できます。
データベース内のすべてのモデルに対して、作成、読み取り、更新、削除の権限を持っています。
まずは管理画面でUserのデータが表示出来るようにしておきましょう。ついでにSalonデータも見れるようにします。この部分は管理画面についてなので説明を省略します(別記事で記載予定)

api/admin.pyに以下のコードを記載します。

from django.contrib import admin
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User, Salon

# カスタムUserモデル用の管理画面設定クラス
class CustomUserAdmin(UserAdmin):
    list_display = UserAdmin.list_display + ('user_type',)
    fieldsets = UserAdmin.fieldsets + (
        ('Custom fields', {'fields': ('user_type',)}),
    )
# カスタムUserモデルを管理画面に登録
admin.site.register(User, CustomUserAdmin)

# Salonモデル用の管理画面設定クラス
class SalonAdmin(admin.ModelAdmin):
    list_display = ('name', 'address', 'phone_number', 'email', 'created_at', 'updated_at')
    search_fields = ('name', 'address', 'phone_number', 'email')
    list_filter = ('created_at', 'updated_at')

# Salonモデルを管理画面に登録
admin.site.register(Salon, SalonAdmin)

ターミナルで以下のコマンドを入力するとユーザー名とパスワードの入力を求められるので任意の値を入力し登録します

python manage.py createsuperuser

以下のメッセージが表示されたら登録完了です。

Superuser created successfully.

試しにDjangoの管理画面にアクセスして登録したスーパーユーザーでログインしてみましょう。
サーバーを立ち上げて以下のURLにブラウザでアクセスします。
http://localhost:8000/admin/

Django管理画面のログイン画面が表示されました。
ここで先ほど登録したスーパーユーザーのusernameとpasswordを入力します。

無事にログインできました。スーパーユーザーについては今は使用しないので説明はここまでとします。
これでバックエンド側のサインアップ機能の実装は完了です。
念の為ユーザー登録ができるかpostmanでテストをしてみましょう。

APIテストの実施

作成したapiで正しくユーザーが登録できるかpostmanでテストしたいと思います。
postmanについてはこちらの記事をご覧ください。

POSTメソッドでアクセスポイントは以下の通りに設定します。

http://localhost:8000/api/signup/

レスポンスのBodyにはJsonで以下のように入力します

{
  "username": "testuser",
  "email": "test@mail.com",
  "password": "testpass",
  "user_type": "stylist",
  "phone_number": "123-1234-5678",
  "profile_info": "テストユーザーです"
}

Sendをクリックします。

成功時のメッセージ「ユーザー登録が完了しました」と201Createdが表示されているので無事登録ができました!

また先ほど登録したスーパーユーザーで管理画面に入りUserモデルに登録したユーザーが追加されているか確認します。

無事に表示されました。userTypeもstylistで登録したので「美容師」と正しく表示されています!
サロンオーナーでの登録はフロントの実装時にテストしてみます。

エラーがでないかドキドキでしたが一安心ですw
ではフロントエンドの実装に移ります。

フロントエンド(React)の実装

それではフロント側の実装をしていきます。
大まかな流れは以下の通りです。

1.「サロンオーナーとして登録」「美容師として登録」のボタンを表示するサインアップ登録画面の作成
2.1のボタン押下で遷移するサインアップフォーム画面の作成
3.App.jsのルーティングの設定を追加
今回は最後に画面確認をしますが、実装しながら画面を確認する場合はルーティングの設定は最初にやってしまってもいいかもしれません。
最低限のMUIの実装もしていますがこれはお好みですw

それでは実装していきます。

SignupSelect コンポーネントの作成

ここではサロンオーナーと美容師でサインアップフォームへの入り口を分けるためのボタンを表示する画面を作成します。
この実装でのポイントは以下の通りです。
・押下するボタンに応じてuserTypeを分けています。
・ReactのフックスuseNavigateを使用します。useNavigateは画面遷移先に値を渡すことができます。
今回はボタンクリック時に navigate('/signup/owner') を呼び出し、ユーザーを '/signup/owner' ルートに遷移させています。

src/components/SignupSelect.js ファイルを作成します。

import React from "react";
import { Button, Typography, Box } from "@mui/material";
import { useNavigate } from "react-router-dom"; #useNavigateをインポート

function SignupSelect(){
  const navigate = useNavigate(); #

  return (
    <Box sx={{display: 'flex', flexDirection: 'column', alignItems: 'center', mt: 4}}>
      <Typography variant="h4">
        アカウント作成
      </Typography>
      <Button
        variant="contained"
        sx={{ mt: 2 }} 
        onClick={() => navigate('/signup/owner')}
      >
        サロンオーナーとして登録する
      </Button>
      <Button
        variant="contained"
        sx={{ mt: 2 }} 
        onClick={() => navigate('/signup/stylist')}
      >
        美容師として登録する
      </Button>
    </Box>
  )
}

export default SignupSelect;

SignupForm コンポーネントの作成

ここでは「登録はこちら」ボタン押下で遷移する登録フォーム画面の実装をします。
ポイントは以下の通りです。
・useParams()でURLのパラメータを取得しuserTypeを区別しています。例えば、URL が /signup/owner の場合、userType は 'owner' になります。
・このURLの値はこの後実装するApp.jsのuserTypeによって決まります。
・上記の機能のおかげでuserTypeの異なるユーザーそれぞれのフォームをSignupForm一つで作成することができます。

import { useState } from "react";
import { TextField, Button, Typography, Box } from "@mui/material";
import { useNavigate, useParams } from "react-router-dom";
import axios from "axios";


function SignupForm(){
  const [formData, setFormData] = useState({
    username:'',
    email:'',
    password:'',
    phone_number:'',
    profile_info:''
  });

  const navigate = useNavigate();
  const { userType } = useParams();

  const handleChange = (e) => {
    setFormData({...formData, [e.target.name]: e.target.value });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    try{
      const response = await axios.post('http://localhost:8000/api/signup/', {
        ...formData,
        user_type: userType
      });
      alert(response.data.message);
      navigate('/login');
    } catch(error){
      alert('登録に失敗しました');
      console.error(error);
    }
  };

  return(
    <Box component="form" onSubmit={handleSubmit} sx={{display: 'flex', flexDirection: 'column', alignItems: 'center', mt: 4}}>
      <Typography variant="h4">
        {userType === 'owner' ? 'サロンオーナー' : '美容師'}として登録
      </Typography>
      <TextField
        sx={{ mt: 2 }}
        name="username"
        label="ユーザー名"
        value={formData.username}
        onChange={handleChange}
        required
      />
      <TextField
        sx={{ mt: 2 }}
        name="email"
        label="メールアドレス"
        value={formData.email}
        onChange={handleChange}
        required
      />
      <TextField
        sx={{ mt: 2 }}
        name="password"
        label="パスワード"
        value={formData.password}
        onChange={handleChange}
        required
      />
      <TextField
        sx={{ mt: 2 }}
        name="phone_number"
        label="電話番号"
        value={formData.phone_number}
        onChange={handleChange}
        required
      />
      <TextField
        sx={{ mt: 2 }}
        name="profile_info"
        label="プロフィール情報"
        value={formData.profile_info}
        onChange={handleChange}
        required
      />
      <Button type="submit" variant="contained" sx={{ mt: 2 }}>
        登録
      </Button>
    </Box>
  );
}

export default SignupForm;

App.jsの更新

ここではルーティングの設定を追加します。
ポイントはSignSelectのボタンによってURLのusertypeを変更する部分です

import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Home from './components/Home';
import SignupSelect from './components/SignupSelect';
import SignupForm from './components/SignupForm';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/signup" element={<SignupSelect />} />
        <Route path="/signup/:userType" element={<SignupForm />} /> //userTypeでURLを変更
      </Routes>
    </Router>
  );
}

export default App;

これでフロントの実装も完了です!
画面の確認はこの後の動作確認で合わせて記載します!

実際の画面で動作の確認

それでは実装が完了したので、実際にサインアップを実施してユーザーの登録をしてみましょう。
サーバーを起動して
http://localhost:3000/signup
にアクセスします。

サインアップセレクト画面

サインアップセレクト画面は正しく表示されました!
まずサロンオーナーで登録してみます。
「サロンオーナーとして登録」をクリックします。

サロンオーナーサインアップ画面

フォーム画面も表示されました!
データを入力します。

「登録」をクリック

無事に成功メッセージが表示されました!!
念の為管理画面に登録されているか確認します。

ちゃんと表示されていました!

続いて美容師の登録を実施します。
サインアップセレクト画面で「美容師として登録」をクリック

美容師サインアップ画面

データを入力します。

「登録」をクリック

成功メッセージが表示されました!
管理画面を確認します。

無事に美容師として登録ができました!!
これでサロンオーナーと美容師でユーザーのタイプを分けたサインアップ機能の実装は一旦完了です!!
では最後にまとめを記載して終わります。

最後に

今回の実装でサインアップ登録フォームからユーザーの登録が出来るようになりました!
サロンオーナーと美容師でユーザーのタイプを分けて登録することで、今後の実装でサロンオーナーだけの機能や美容師側だけの機能の実装の際に役立ちます。
また、このアプリだけでなく、マッチングアプリや求人アプリ等を作成する際も、この知識は活かせそうです!

認証周りの実装はまだまだこれからです。まだユーザーの登録が出来ただけですからねw
サインアップしたユーザーでのログイン機能はもちろん、ログインしないとサロン一覧画面にアクセス出来ないようにするなど、権限周り実装も残っています。さらにユーザーライクなバリデーションの追加や説明の記載も必要ですね。

次回は、登録したユーザーでのログイン機能と記事のボリューム次第で権限周りの実装もする予定です。
お読みいただきありがとうございました!次回もお楽しみに!

やぎ

Discussion