この記事の続きです
今回の記事について
前回記事(👆)では、ユーザーがパスワードを忘れたときにDjnago側がワンタイムパスワードを発行してダミーメールサーバSMTPDevへパスワード付きメールを送信する方法でした。
今回は、メールにて送られてきたパスワードを使ってログインできるかどうかの機能を作ってみます。
使うデータベースはPostgreSQL
サーバ側Djangoの処理
生のSELECT文を使ってデータを取得したいので、ライブラリを追加しましょう。
from django.db import connection # 追加
from django.contrib.auth.hashers import check_password# 追加
最初のライブラリは、DB接続するためのものです。
2番目は、パスワードチェックを行ってくれるものです。
つづいて、今回の処理を書いていきましょう。
今回の処理は下記です。
[処理の流れ]
1.ブラウザからE-mailとパスワードを取得する。
2.生のSQLでハッシュ化したパスワードを取得する。
3.パスワードとハッシュ化パスワードを照合する。
という流れになっています。
では、処理を書いていきましょう。
# パスワード変更画面からワンタイムパスワードを取得して再度ログインする処理
@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を書くときはちょっと工夫が必要です。
どんな工夫かというと下記のように書きます。
# === 大文字を含むカラム名を明示的にダブルクォーテーションで囲む ===
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は大文字を区別するために "カラム名" のように囲む必要があります。
例:
SELECT "generalUserPassword" FROM "generalusers_generalusers";
テーブル名にも大文字が含まれている場合
例えばテーブルが "GeneralUsers" のように作られているなら、
SQL全体でダブルクォートが必要になります👇
cursor.execute(
'SELECT "generalUserPassword" FROM "GeneralUsers" WHERE "generalUserEmail" = %s',
[general_user_email]
)
全体の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)
ルーティング設定
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))
]
フロント側のコード
'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>
)
}
