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

【Next.js×Django】生のクエリ(SELECT文)をDjnagoで使ってみる方法

Posted at

この記事の続きです

今回の記事について

前回記事(👆)では、ユーザーがパスワードを忘れたときにDjnago側がワンタイムパスワードを発行してダミーメールサーバSMTPDevへパスワード付きメールを送信する方法でした。

今回は、メールにて送られてきたパスワードを使ってログインできるかどうかの機能を作ってみます。

使うデータベースはPostgreSQL

image.png

サーバ側Djangoの処理

生のSELECT文を使ってデータを取得したいので、ライブラリを追加しましょう。

views.py
from django.db import connection # 追加
from django.contrib.auth.hashers import check_password# 追加

最初のライブラリは、DB接続するためのものです。
2番目は、パスワードチェックを行ってくれるものです。

つづいて、今回の処理を書いていきましょう。
今回の処理は下記です。

[処理の流れ]
1.ブラウザからE-mailとパスワードを取得する。
2.生のSQLでハッシュ化したパスワードを取得する。
3.パスワードとハッシュ化パスワードを照合する。

という流れになっています。

では、処理を書いていきましょう。

views.py
# パスワード変更画面からワンタイムパスワードを取得して再度ログインする処理
@api_view(['POST'])
def check_login_general_user_after_regenerate_password(request):
    if request.method == 'POST':
        try:
            # Nameを取得する
            general_user_name = request.data.get('generalUserName')
            print('一般ユーザー名は:',general_user_name)

            # Emailを取得する
            general_user_email = request.data.get('generalUserEmail')
            # パスワードを取得する
            general_user_password = request.data.get('generalUserPassword')
            if not general_user_email or not general_user_password:
                return Response({'error':'Email or Password missing'},status=status.HTTP_400_BAD_REQUEST)
            
            # 生SQLでパスワードハッシュを取得
            with connection.cursor() as cursor:
                print('生のSQL')
                cursor.execute('SELECT "generalUserPassword" FROM "generalusers_generalusers" WHERE "generalUserEmail" = %s',[general_user_email])
                row = cursor.fetchone()
                print('取得した行の中身は:',row)

            if row is None:
                # ユーザーが存在しない場合
                return Response({'error':'User not found'},status=status.HTTP_404_NOT_FOUND)
            
            # 取得したハッシュ化パスワード
            hashed_password = row[0]
            print('ハッシュ化パスワードは:',hashed_password)
          
            # ブラウザから送られてきたパスワードとDBのハッシュ化パスワードを比較する
            if not check_password(general_user_password,hashed_password):
                # 不一致の場合
                return Response({'error':'Invalid password'},status=status.HTTP_401_UNAUTHORIZED)

            # パスワードが一致なら成功コードをリターンする
            return Response({'message':'Login Success'},status=status.HTTP_200_OK)
        except Exception as e:
            return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

ここで、気を付けないといけないのが、PostgreSQLのカラムがlowerCamelケースになっています。
そのため、生のSQLを書くときはちょっと工夫が必要です。

どんな工夫かというと下記のように書きます。

views.py
# === 大文字を含むカラム名を明示的にダブルクォーテーションで囲む ===
            with connection.cursor() as cursor:
                cursor.execute(
                    'SELECT "generalUserPassword" FROM "generalusers_generalusers" WHERE "generalUserEmail" = %s',
                    [general_user_email]
                )
                row = cursor.fetchone()

PostgreSQLでは 大文字を含むカラム名 を扱う場合、
SQL文中で必ずダブルクォーテーションで囲む必要 があります。
これを忘れると、PostgreSQLは自動的に小文字に変換して探しに行くため、
「列が存在しません」というエラーになります。

💡 注意点

ダブルクォーテーション " は必須

PostgreSQLは大文字を区別するために "カラム名" のように囲む必要があります。

例:
views.py
SELECT "generalUserPassword" FROM "generalusers_generalusers";

テーブル名にも大文字が含まれている場合

例えばテーブルが "GeneralUsers" のように作られているなら、
SQL全体でダブルクォートが必要になります👇

views.py
cursor.execute(
    'SELECT "generalUserPassword" FROM "GeneralUsers" WHERE "generalUserEmail" = %s',
    [general_user_email]
)

全体のviews.pyのソースコードはこちら👇

views.py
#from django.shortcuts import render
from .models import GeneralUsers
from .serializers import GeneralUsersSerializer
from rest_framework import viewsets
from rest_framework.decorators import api_view #追加
from rest_framework.response import Response # 追加
from rest_framework import status #追加
from django.db import transaction # 追加
from django.contrib.auth.hashers import make_password #追加
from django.core.exceptions import ObjectDoesNotExist #Add
from django.contrib.auth.hashers import check_password #Add
from .serializers import GeneralUsersSerializer
import secrets #追加
import string #追加
from django.core.mail import send_mail # 追加
import logging
# Create your views here.
from django.db import connection # 追加
from django.contrib.auth.hashers import check_password# 追加

class GeneralUsersViewSet(viewsets.ModelViewSet):
    queryset = GeneralUsers.objects.all()
    serializer_class = GeneralUsersSerializer

@api_view(['POST'])
def create_general_user(request):
    if request.method == 'POST':
        with transaction.atomic():
            serializer = GeneralUsersSerializer(data=request.data)
            if serializer.is_valid():
                password = serializer.validated_data['password']
                hashed_password = make_password(password)

                serializer.validated_data['generalUserPassword'] = hashed_password
                if serializer.is_valid():
                    serializer.save()
                    return Response(serializer.data,status=status.HTTP_201_CREATED)
                else:
                    return Response(serializer.errors,status=status.HTTP_500_INTERNAL_SERVER_ERROR)
                

@api_view(['PUT'])
def update_general_user(request):
    try:
        general_user_id = request.data.get('generalUserId')
        image_url = request.data.get('generalUserImage')

        user = GeneralUsers.objects.get(generalUserId=general_user_id)
        user.generalUserImage = image_url
        user.save()

        serializer = GeneralUsersSerializer(user)
        return Response(serializer.data,status=status.HTTP_200_OK)
    except GeneralUsers.DoesNotExist:
        return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
    except Exception as e:
        return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

#ランダムなパスワードを生成する処理
def generate_random_password(length = 10):
    characters = string.ascii_letters + string.digits
    return ''.join(secrets.choice(characters) for _ in range(length))

#パスワード変更画面から登録済Emailあてにメール送信
@api_view(['POST'])
def send_general_user_email_to_change_password(request):
    try:
        general_user_email = request.data.get('generalUserEmail')
        print('ブラウザから受け取ったメールアドレス:',general_user_email)
        #Emailが空文字の場合処理を終了
        if not general_user_email:
            return Response({'error':'Your Password is required.'},status=status.HTTP_404_NOT_FOUND)
        
        #ブラウザから取得したEmailから一意のデータを取得する
        user = GeneralUsers.objects.get(generalUserEmail = general_user_email)
        print('Userは',user)
        #ランダムなパスワードを生成
        new_plain_password = generate_random_password()

        #ハッシュ化して保存
        user.generalUserPassword = make_password(new_plain_password)
        #コンソールデバッグ
        print(f"Generated password: {new_plain_password}")
        user.save()

        #メールにはハッシュ化される前のパスワードを送信する
        
        send_mail(
            subject='【重要】新しいパスワードのお知らせ',
            message = f'新しいパスワードは以下の通りです。\n\n{new_plain_password}\n\nログイン後、パスワードの変更をおすすめします。',
            from_email='noreply@example.com',
            recipient_list=[general_user_email],
            fail_silently=False,
        )
        

        return Response({'message': 'Password has been reset and sent to your email.'},status=status.HTTP_200_OK)
    except Exception as e:
        return Response({'error':str(e)},status=status.HTTP_500_INTERNAL_SERVER_ERROR)
    
# パスワード変更画面からワンタイムパスワードを取得して再度ログインする処理
@api_view(['POST'])
def check_login_general_user_after_regenerate_password(request):
    if request.method == 'POST':
        try:
            # Nameを取得する
            general_user_name = request.data.get('generalUserName')
            print('一般ユーザー名は:',general_user_name)

            # Emailを取得する
            general_user_email = request.data.get('generalUserEmail')
            # パスワードを取得する
            general_user_password = request.data.get('generalUserPassword')
            if not general_user_email or not general_user_password:
                return Response({'error':'Email or Password missing'},status=status.HTTP_400_BAD_REQUEST)
            
            # 生SQLでパスワードハッシュを取得
            with connection.cursor() as cursor:
                print('生のSQL')
                cursor.execute('SELECT "generalUserPassword" FROM "generalusers_generalusers" WHERE "generalUserEmail" = %s',[general_user_email])
                row = cursor.fetchone()
                print('取得した行の中身は:',row)

            if row is None:
                # ユーザーが存在しない場合
                return Response({'error':'User not found'},status=status.HTTP_404_NOT_FOUND)
            
            # 取得したハッシュ化パスワード
            hashed_password = row[0]
            print('ハッシュ化パスワードは:',hashed_password)
          
            # ブラウザから送られてきたパスワードとDBのハッシュ化パスワードを比較する
            if not check_password(general_user_password,hashed_password):
                # 不一致の場合
                return Response({'error':'Invalid password'},status=status.HTTP_401_UNAUTHORIZED)

            # パスワードが一致なら成功コードをリターンする
            return Response({'message':'Login Success'},status=status.HTTP_200_OK)
        except Exception as e:
            return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
    

ルーティング設定

urls.py
from django.urls import path,include
from .views import GeneralUsersViewSet #Add
from rest_framework.routers import DefaultRouter
#from .views import GeneralUsersViewSet
from .views import update_general_user #Add
from .views import send_general_user_email_to_change_password #Add
from .views import check_login_general_user_after_regenerate_password #Add

router = DefaultRouter()
router.register('registergeneralusers',GeneralUsersViewSet,basename='create_general_user')

urlpatterns =[
    path('updategeneralusers/', update_general_user),
    path('sendGeneralUserEmailToChangePassword/',send_general_user_email_to_change_password),
    path('checkLoginGeneralUserAfterRegeneratePassword/',check_login_general_user_after_regenerate_password),
    path('',include(router.urls))
]

フロント側のコード

page.tsx
'use client'
import { useState } from "react"
import { useRouter } from "next/navigation"
import { useForm } from "react-hook-form"
import axios from "axios"

export default function Login(){
    const router = useRouter();
    const defaultValues = {
        Name:'',
        Email:'',
        Password:''
    }

    const {register,handleSubmit,formState:{errors}} = useForm({
        defaultValues
    });

    const [name,setName] = useState('');
    const [email,setEmail] = useState('');
    const [password,setPassword] = useState('');

    const onSubmit = async()=>{
        try{
            const formData = new FormData();
            formData.append('generalUserName',name);
            formData.append('generalUserEmail',email);
            formData.append('generalUserPassword',password);
            const response = await axios.post('http://127.0.0.1:8000/generalusers/checkLoginGeneralUserAfterRegeneratePassword/',formData)
            if(response.status !== 200){
                alert('Login User Error');
                return;
            }
            alert('Success!');
        }catch(error){
            console.log('Error:',error.message);
            alert('Something Error has happend.');
        }                             
    }

    return (
        <div className="flex items-center justify-center h-screen">
            <form onSubmit={handleSubmit(onSubmit)} className="border rounded-md px-4 py-4">
                <div className="mb-4">
                    <label>Name</label>
                    <input
                        type="text"
                        className="w-full border border-gray-500 rounded shadow appearance-none focus:shadow-outline text-gray-700 leading-tight px-3 py-2"
                        defaultValue={defaultValues.Name}
                        {...register('Name',{
                            required:'Name must be required.'
                        })}
                        placeholder="UserName"
                        onChange={(e)=>setName(e.target.value)}
                    />
                    <div className="text-rose-500">{errors.Name?.message}</div>
                </div>
                <div className="mb-4">
                    <label>Email</label>
                    <input
                        type="email"
                        className="w-full border border-gray-500 rounded shadow appearance-none focus:shadow-outline text-gray-700 leading-tight px-3 py-2"
                        defaultValue={defaultValues.Email}
                        {...register('Email',{
                            required:'Email must be required.'
                        })}
                        placeholder="Sample@email.com"
                        onChange={(e)=>setEmail(e.target.value)}
                    />
                    <div className="text-rose-500">{errors.Email?.message}</div>
                </div>
                <div className="mb-4">
                    <label>Password</label>
                    <input
                        type="password"
                        className="w-full border border-gray-500 rounded shadow appearance-none focus:shadow-outline text-gray-700 leading-tight px-3 py-2"
                        defaultValue={defaultValues.Password}
                        {...register('Password',{
                            required:'Password must be require.'
                        })}
                        placeholder="●●●●●●●"
                        onChange={(e)=>setPassword(e.target.value)}
                    />
                    <div className="text-rose-500">{errors.Password?.message}</div>
                </div>
                <div>
                    <button
                        type="submit"
                        className="bg bg-blue-500 rounded text-white px-4 py-4 mr-4 hover:bg-blue-700 cursor-pointer font-bold"
                    >
                        Login
                    </button>
                    <button
                        type="button"
                        className="bg bg-green-500 rounded text-white px-4 py-4 hover:bg-green-700 cursor-pointer mr-4 font-bold"
                    >
                        Back to TopPage
                    </button>
                    <a href="/generalUsers/regeneratePassword" className="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-700">
                        Forget Password?
                    </a>
                </div>
            </form>
        </div>
    )
}


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