From d851e7e4adb918b248df9da683b55d2cabcd2974 Mon Sep 17 00:00:00 2001 From: Akira Miyata Date: Fri, 2 Aug 2024 14:21:50 +0000 Subject: [PATCH] Release 4-8-6 --- config/settings.py | 6 +- rog/serializers.py | 27 +++- rog/templates/email/reset_password_email.txt | 23 +++ rog/templates/password-reset-component.tsx | 116 ++++++++++++++ rog/templates/password_reset.html | 156 +++++++++++++++++++ rog/templates/password_reset_invalid.html | 41 +++++ rog/urls.py | 4 +- rog/utils.py | 9 +- rog/views.py | 87 +++++++++-- 9 files changed, 448 insertions(+), 21 deletions(-) create mode 100644 rog/templates/email/reset_password_email.txt create mode 100644 rog/templates/password-reset-component.tsx create mode 100644 rog/templates/password_reset.html create mode 100644 rog/templates/password_reset_invalid.html diff --git a/config/settings.py b/config/settings.py index 3f4df15..dda88a2 100644 --- a/config/settings.py +++ b/config/settings.py @@ -71,7 +71,7 @@ ROOT_URLCONF = 'config.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR / 'templates'], + 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -176,8 +176,8 @@ REST_FRAMEWORK = { } -#FRONTEND_URL = 'https://rogaining.intranet.sumasen.net' # フロントエンドのURLに適宜変更してください -FRONTEND_URL = 'https://rogaining.sumasen.net' # フロントエンドのURLに適宜変更してください +FRONTEND_URL = 'https://rogaining.intranet.sumasen.net' # フロントエンドのURLに適宜変更してください +#FRONTEND_URL = 'https://rogaining.sumasen.net' # フロントエンドのURLに適宜変更してください # この設定により、メールは実際には送信されず、代わりにコンソールに出力されます。 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' diff --git a/rog/serializers.py b/rog/serializers.py index fddaaf9..4028602 100644 --- a/rog/serializers.py +++ b/rog/serializers.py @@ -155,17 +155,22 @@ class UserRegistrationSerializer(serializers.ModelSerializer): # return user class TempUserRegistrationSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True) + class Meta: model = TempUser - fields = ('email', 'firstname', 'lastname', 'date_of_birth', 'female') + fields = ('email', 'password', 'firstname', 'lastname', 'date_of_birth', 'female') def create(self, validated_data): - validated_data['verification_code'] = str(uuid.uuid4()) - raw_password = validated_data.get('password') - hashed_password = make_password(raw_password) - validated_data['password'] = hashed_password + # パスワードのハッシュ化はviewで行うので、ここではそのまま保存 return TempUser.objects.create(**validated_data) + #validated_data['verification_code'] = str(uuid.uuid4()) + #raw_password = validated_data.get('password') + #hashed_password = make_password(raw_password) + #validated_data['password'] = hashed_password + #return TempUser.objects.create(**validated_data) + class UserSerializer(serializers.ModelSerializer): class Meta: @@ -793,4 +798,16 @@ class EntryCreationSerializer(serializers.Serializer): return entry +class PasswordResetRequestSerializer(serializers.Serializer): + email = serializers.EmailField() + +class PasswordResetConfirmSerializer(serializers.Serializer): + new_password = serializers.CharField(write_only=True) + confirm_password = serializers.CharField(write_only=True) + + def validate(self, data): + if data['new_password'] != data['confirm_password']: + raise serializers.ValidationError("Passwords do not match") + validate_password(data['new_password']) + return data diff --git a/rog/templates/email/reset_password_email.txt b/rog/templates/email/reset_password_email.txt new file mode 100644 index 0000000..af28d22 --- /dev/null +++ b/rog/templates/email/reset_password_email.txt @@ -0,0 +1,23 @@ +件名: 岐阜ロゲのパスワードリセットのお知らせ + +{{name}} 様 + +こちらは岐阜aiネットワークのAI担当です。 + +このメールはパスワードのリセットのご依頼によるリセット確認メールです。 +身に覚えのない方は、お手数ですが削除をお願いします。 + +以下のリンクからパスワードのリセットが行えます。 + +{{activation_link}} + + +それでは、今後とも岐阜ロゲをよろしくお願いいたします。 + + +※ 本メールは送信専用のメールアドレスで送信しております。 本メールに返信いただいてもご回答いたしかねますので、あらかじめご了承ください(ご質問等はinfo@gifuai.netまでお願いいたします)。もしこのメールに心当たりがない場合は破棄願います。 + +NPO岐阜aiネットワーク 担当AI + + + diff --git a/rog/templates/password-reset-component.tsx b/rog/templates/password-reset-component.tsx new file mode 100644 index 0000000..b793800 --- /dev/null +++ b/rog/templates/password-reset-component.tsx @@ -0,0 +1,116 @@ +import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { Eye, EyeOff } from 'lucide-react'; + +const PasswordReset = () => { + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [message, setMessage] = useState(''); + const { uid, token } = useParams(); + + const handleSubmit = async (e) => { + e.preventDefault(); + if (password !== confirmPassword) { + setMessage('パスワードが一致しません。'); + return; + } + try { + const response = await fetch(`/api/reset-password/${uid}/${token}/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ new_password: password }), + }); + const data = await response.json(); + if (response.ok) { + setMessage('パスワードが正常にリセットされました。'); + } else { + setMessage(data.message || 'パスワードのリセットに失敗しました。'); + } + } catch (error) { + setMessage('エラーが発生しました。もう一度お試しください。'); + } + }; + + return ( +
+
+

+ パスワードのリセット +

+
+ +
+
+
+
+ +
+ setPassword(e.target.value)} + className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + /> + +
+
+ +
+ +
+ setConfirmPassword(e.target.value)} + className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + /> +
+
+ +
+ +
+
+ + {message && ( +
+ {message} +
+ )} +
+
+
+ ); +}; + +export default PasswordReset; diff --git a/rog/templates/password_reset.html b/rog/templates/password_reset.html new file mode 100644 index 0000000..3be87f2 --- /dev/null +++ b/rog/templates/password_reset.html @@ -0,0 +1,156 @@ + + + + + + パスワードのリセット + + + +
+

岐阜ナビ:パスワードのリセット

+
+
+ + +
+
+ + +
+ +
+
+
+ + + + diff --git a/rog/templates/password_reset_invalid.html b/rog/templates/password_reset_invalid.html new file mode 100644 index 0000000..2c27657 --- /dev/null +++ b/rog/templates/password_reset_invalid.html @@ -0,0 +1,41 @@ + + + + + + 無効なパスワードリセットリンク + + + +
+

無効なリンク

+

このパスワードリセットリンクは無効です。新しいリセットリンクを要求してください。

+
+ + diff --git a/rog/urls.py b/rog/urls.py index 05cdfc2..cccc7b8 100644 --- a/rog/urls.py +++ b/rog/urls.py @@ -1,7 +1,7 @@ from sys import prefix from rest_framework import urlpatterns from rest_framework.routers import DefaultRouter -from .views import LocationViewSet, Location_lineViewSet, Location_polygonViewSet, Jpn_Main_PerfViewSet, LocationsInPerf, ExtentForSubPerf, SubPerfInMainPerf, ExtentForMainPerf, LocationsInSubPerf, CatView, RegistrationAPI, LoginAPI, UserAPI, UserActionViewset, UserMakeActionViewset, UserDestinations, UpdateOrder, LocationInBound, DeleteDestination, CustomAreaLocations, GetAllGifuAreas, CustomAreaNames, userDetials, UserTracksViewSet, CatByCity, ChangePasswordView, GoalImageViewSet, CheckinImageViewSet, ExtentForLocations, DeleteAccount, PrivacyView, RegistrationView, TeamViewSet,MemberViewSet,EntryViewSet,RegisterView, VerifyEmailView, NewEventListView,NewEvent2ListView,NewCategoryListView,CategoryListView, MemberUserDetailView, TeamMembersWithUserView,MemberAddView,UserActivationView,RegistrationView,TempUserRegistrationView,ResendInvitationEmailView,update_user_info,update_user_detail,ActivateMemberView, ActivateNewMemberView +from .views import LocationViewSet, Location_lineViewSet, Location_polygonViewSet, Jpn_Main_PerfViewSet, LocationsInPerf, ExtentForSubPerf, SubPerfInMainPerf, ExtentForMainPerf, LocationsInSubPerf, CatView, RegistrationAPI, LoginAPI, UserAPI, UserActionViewset, UserMakeActionViewset, UserDestinations, UpdateOrder, LocationInBound, DeleteDestination, CustomAreaLocations, GetAllGifuAreas, CustomAreaNames, userDetials, UserTracksViewSet, CatByCity, ChangePasswordView, GoalImageViewSet, CheckinImageViewSet, ExtentForLocations, DeleteAccount, PrivacyView, RegistrationView, TeamViewSet,MemberViewSet,EntryViewSet,RegisterView, VerifyEmailView, NewEventListView,NewEvent2ListView,NewCategoryListView,CategoryListView, MemberUserDetailView, TeamMembersWithUserView,MemberAddView,UserActivationView,RegistrationView,TempUserRegistrationView,ResendInvitationEmailView,update_user_info,update_user_detail,ActivateMemberView, ActivateNewMemberView, PasswordResetRequestView, PasswordResetConfirmView from django.urls import path, include from knox import views as knox_views @@ -90,5 +90,7 @@ urlpatterns += [ path('userdetail//',update_user_detail, name='update_user_detail'), path('activate-member///', ActivateMemberView.as_view(), name='activate-member'), path('activate-new-member///', ActivateNewMemberView.as_view(), name='activate-new-member'), + path('password-reset/', PasswordResetRequestView.as_view(), name='password_reset_request'), + path('reset-password///', PasswordResetConfirmView.as_view(), name='password_reset_confirm'), ] diff --git a/rog/utils.py b/rog/utils.py index a6719da..413361d 100644 --- a/rog/utils.py +++ b/rog/utils.py @@ -43,7 +43,14 @@ def send_verification_email(user, activation_link): share_send_email(subject,body,user.email) - +def send_reset_password_email(email,activation_link): + context = { + 'name': email, + 'activation_link': activation_link, + } + logger.info(f"send_reset_password_email : {context}") + subject, body = load_email_template('reset_password_email.txt', context) + share_send_email(subject,body,email) # 既にユーザーになっている人にチームへの参加要請メールを出す。 diff --git a/rog/views.py b/rog/views.py index d5ebe53..ee31e0d 100644 --- a/rog/views.py +++ b/rog/views.py @@ -2,12 +2,17 @@ from .models import JpnSubPerf # このインポート文をファイルの先 from django.contrib.auth import get_user_model User = get_user_model() import traceback +from django.contrib.auth.hashers import make_password + +from django.contrib.auth.tokens import default_token_generator +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.utils.encoding import force_bytes, force_str import requests from rest_framework import serializers from django.db import IntegrityError from django.urls import reverse -from .utils import send_verification_email,send_invitation_email,send_team_join_email +from .utils import send_verification_email,send_invitation_email,send_team_join_email,send_reset_password_email from django.conf import settings import uuid from rest_framework.exceptions import ValidationError as DRFValidationError @@ -26,7 +31,7 @@ from curses.ascii import NUL from django.core.serializers import serialize from .models import GoalImages, Location, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, GifuAreas, RogUser, CustomUser, UserTracks, GoalImages, CheckinImages, NewEvent,NewEvent2, Team, Category, NewCategory,Entry, Member, TempUser,EntryMember from rest_framework import viewsets -from .serializers import LocationSerializer, Location_lineSerializer, Location_polygonSerializer, JPN_main_perfSerializer, LocationCatSerializer, UserSerializer, LoginUserSerializer, UseractionsSerializer, UserDestinationSerializer, GifuAreaSerializer, LocationEventNameSerializer, RogUserSerializer, UserTracksSerializer, ChangePasswordSerializer, GolaImageSerializer, CheckinImageSerializer, RegistrationSerializer, MemberWithUserSerializer,TempUserRegistrationSerializer +from .serializers import LocationSerializer, Location_lineSerializer, Location_polygonSerializer, JPN_main_perfSerializer, LocationCatSerializer, UserSerializer, LoginUserSerializer, UseractionsSerializer, UserDestinationSerializer, GifuAreaSerializer, LocationEventNameSerializer, RogUserSerializer, UserTracksSerializer, ChangePasswordSerializer, GolaImageSerializer, CheckinImageSerializer, RegistrationSerializer, MemberWithUserSerializer,TempUserRegistrationSerializer, PasswordResetRequestSerializer, PasswordResetConfirmSerializer from knox.models import AuthToken from rest_framework import viewsets, generics, status @@ -483,7 +488,6 @@ class LoginView(APIView): password = request.data.get('password') # デバッグコード - from django.contrib.auth.hashers import make_password, check_password user = CustomUser.objects.filter(email=email).first() if user: stored_hash = user.password @@ -1699,12 +1703,17 @@ class TempUserRegistrationView(APIView): # 新規仮登録 serializer = TempUserRegistrationSerializer(data=request.data) if serializer.is_valid(): - temp_user = serializer.save() + # シリアライザのvalidated_dataからパスワードを取得 + password = serializer.validated_data.get('password') + # パスワードをハッシュ化 + hashed_password = make_password(password) + # ハッシュ化されたパスワードでTempUserを作成 + temp_user = serializer.save(password=hashed_password) + verification_code = uuid.uuid4() temp_user.verification_code = verification_code - #password = serializer.validated_data.pop('password') - #temp_user.set_password(password) temp_user.save() + verification_url = request.build_absolute_uri( reverse('verify-email', kwargs={'verification_code': verification_code}) ) @@ -1736,6 +1745,8 @@ class VerifyEmailView(APIView): if temp_user.is_valid(): user_data = { 'email': temp_user.email, + 'is_rogaining':True, # ここでis_rogainingをTrueに設定 + 'password':temp_user.password, 'is_rogaining': temp_user.is_rogaining, 'zekken_number': temp_user.zekken_number, 'event_code': temp_user.event_code, @@ -1752,11 +1763,13 @@ class VerifyEmailView(APIView): try: # CustomUserを作成 - user = CustomUser.objects.create_user( - email=user_data['email'], - password=temp_user.password, - **{k: v for k, v in user_data.items() if k != 'email'} - ) + user = CustomUser.objects.create(**user_data) + + #user = CustomUser.objects.create_user( + # email=user_data['email'], + # password=temp_user.password, # ハッシュ化されたパスワードを直接使用 + # **{k: v for k, v in user_data.items() if k != 'email'} + #) except ValidationError as e: # パスワードのバリデーションエラーなどの処理 return render(request, 'verification_error.html', {'message': str(e), 'title': 'エラー'}) @@ -1795,3 +1808,55 @@ class TeamMembersWithUserView(generics.ListAPIView): team_id = self.kwargs['team_id'] return Member.objects.filter(team_id=team_id).select_related('user', 'team') + +class PasswordResetRequestView(APIView): + def post(self, request): + serializer = PasswordResetRequestSerializer(data=request.data) + if serializer.is_valid(): + email = serializer.validated_data['email'] + user = CustomUser.objects.filter(email=email).first() + if user: + token = default_token_generator.make_token(user) + uid = urlsafe_base64_encode(force_bytes(user.pk)) + reset_link = f"{settings.FRONTEND_URL}/api/reset-password/{uid}/{token}/" + send_reset_password_email(email,reset_link) + + return Response({"message": "Password reset email sent"}, status=status.HTTP_200_OK) + return Response({"message": "User not found"}, status=status.HTTP_404_NOT_FOUND) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class PasswordResetConfirmView(APIView): + def get(self, request, uidb64, token): + try: + uid = force_str(urlsafe_base64_decode(uidb64)) + user = CustomUser.objects.get(pk=uid) + except (TypeError, ValueError, OverflowError, CustomUser.DoesNotExist): + user = None + + if user is not None and default_token_generator.check_token(user, token): + return render(request, 'password_reset.html', {'uid': uidb64, 'token': token}) + else: + return render(request, 'password_reset_invalid.html') + + if user is not None and default_token_generator.check_token(user, token): + return Response({"message": "Token is valid"}, status=status.HTTP_200_OK) + return Response({"message": "Invalid reset link"}, status=status.HTTP_400_BAD_REQUEST) + + + def post(self, request, uidb64, token): + try: + uid = force_str(urlsafe_base64_decode(uidb64)) + user = CustomUser.objects.get(pk=uid) + except (TypeError, ValueError, OverflowError, CustomUser.DoesNotExist): + return Response({"error": "Invalid reset link"}, status=status.HTTP_400_BAD_REQUEST) + + if default_token_generator.check_token(user, token): + serializer = PasswordResetConfirmSerializer(data=request.data) + if serializer.is_valid(): + user.set_password(serializer.validated_data['new_password']) + user.save() + return Response({"message": "Password has been reset successfully"}, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response({"error": "Invalid reset link"}, status=status.HTTP_400_BAD_REQUEST) + +