Release 4-8-6

This commit is contained in:
2024-08-02 14:21:50 +00:00
parent 9d0d3ea102
commit d851e7e4ad
9 changed files with 448 additions and 21 deletions

View File

@ -71,7 +71,7 @@ ROOT_URLCONF = 'config.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], 'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@ -176,8 +176,8 @@ REST_FRAMEWORK = {
} }
#FRONTEND_URL = 'https://rogaining.intranet.sumasen.net' # フロントエンドのURLに適宜変更してください FRONTEND_URL = 'https://rogaining.intranet.sumasen.net' # フロントエンドのURLに適宜変更してください
FRONTEND_URL = 'https://rogaining.sumasen.net' # フロントエンドのURLに適宜変更してください #FRONTEND_URL = 'https://rogaining.sumasen.net' # フロントエンドのURLに適宜変更してください
# この設定により、メールは実際には送信されず、代わりにコンソールに出力されます。 # この設定により、メールは実際には送信されず、代わりにコンソールに出力されます。
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'

View File

@ -155,17 +155,22 @@ class UserRegistrationSerializer(serializers.ModelSerializer):
# return user # return user
class TempUserRegistrationSerializer(serializers.ModelSerializer): class TempUserRegistrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta: class Meta:
model = TempUser model = TempUser
fields = ('email', 'firstname', 'lastname', 'date_of_birth', 'female') fields = ('email', 'password', 'firstname', 'lastname', 'date_of_birth', 'female')
def create(self, validated_data): def create(self, validated_data):
validated_data['verification_code'] = str(uuid.uuid4()) # パスワードのハッシュ化はviewで行うので、ここではそのまま保存
raw_password = validated_data.get('password')
hashed_password = make_password(raw_password)
validated_data['password'] = hashed_password
return TempUser.objects.create(**validated_data) 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 UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -793,4 +798,16 @@ class EntryCreationSerializer(serializers.Serializer):
return entry 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

View File

@ -0,0 +1,23 @@
件名: 岐阜ロゲのパスワードリセットのお知らせ
{{name}} 様
こちらは岐阜ネットワークのAI担当です。
このメールはパスワードのリセットのご依頼によるリセット確認メールです。
身に覚えのない方は、お手数ですが削除をお願いします。
以下のリンクからパスワードのリセットが行えます。
{{activation_link}}
それでは、今後とも岐阜ロゲをよろしくお願いいたします。
※ 本メールは送信専用のメールアドレスで送信しております。 本メールに返信いただいてもご回答いたしかねますので、あらかじめご了承くださいご質問等はinfo@gifuai.netまでお願いいたします。もしこのメールに心当たりがない場合は破棄願います。
NPO岐阜aiネットワーク 担当AI

View File

@ -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 (
<div className="min-h-screen bg-gray-100 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form className="space-y-6" onSubmit={handleSubmit}>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
</label>
<div className="mt-1 relative">
<input
id="password"
name="password"
type={showPassword ? "text" : "password"}
autoComplete="new-password"
required
value={password}
onChange={(e) => 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"
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-gray-400" />
) : (
<Eye className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
</div>
<div>
<label htmlFor="confirm-password" className="block text-sm font-medium text-gray-700">
</label>
<div className="mt-1">
<input
id="confirm-password"
name="confirm-password"
type="password"
autoComplete="new-password"
required
value={confirmPassword}
onChange={(e) => 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"
/>
</div>
</div>
<div>
<button
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
</button>
</div>
</form>
{message && (
<div className="mt-6 text-center text-sm text-gray-500">
{message}
</div>
)}
</div>
</div>
</div>
);
};
export default PasswordReset;

View File

@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>パスワードのリセット</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f0f2f5;
margin: 0;
padding: 20px;
line-height: 1.6;
}
.container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px;
max-width: 400px;
margin: 0 auto;
}
h1 {
color: #1a1a1a;
text-align: center;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
color: #4a4a4a;
}
input[type="password"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 10px;
background-color: #0056b3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #003d82;
}
.message {
margin-top: 15px;
padding: 10px;
border-radius: 4px;
text-align: center;
}
.message.error {
background-color: #ffe6e6;
color: #d8000c;
}
.message.success {
background-color: #e6ffe6;
color: #006400;
}
</style>
</head>
<body>
<div class="container">
<h1>岐阜ナビ:パスワードのリセット</h1>
<form id="reset-form">
<div class="form-group">
<label for="password">新しいパスワード</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label for="confirm-password">パスワードの確認</label>
<input type="password" id="confirm-password" name="confirm-password" required>
</div>
<button type="submit">パスワードをリセット</button>
</form>
<div id="message" class="message"></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const currentUrl = window.location.href;
function extractParams(url) {
const regex = /\/reset-password\/([^\/]+)\/([^\/]+)\/?/;
const match = url.match(regex);
if (match) {
return { uidb64: match[1], token: match[2] };
}
const urlParams = new URLSearchParams(new URL(url).search);
return {
uidb64: urlParams.get('uidb64') || '',
token: urlParams.get('token') || ''
};
}
const { uidb64, token } = extractParams(currentUrl);
document.getElementById('reset-form').addEventListener('submit', async function(e) {
e.preventDefault();
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirm-password').value;
const messageElement = document.getElementById('message');
if (password !== confirmPassword) {
messageElement.textContent = 'パスワードが一致しません。';
messageElement.className = 'message error';
return;
}
if (!uidb64 || !token) {
messageElement.textContent = '無効なリセットリンクです。';
messageElement.className = 'message error';
return;
}
try {
const response = await fetch(`/api/reset-password/${uidb64}/${token}/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
new_password: password,
confirm_password: confirmPassword
}),
});
const data = await response.json();
if (response.ok) {
messageElement.textContent = 'パスワードが正常にリセットされました。';
messageElement.className = 'message success';
} else {
messageElement.textContent = `パスワードのリセットに失敗しました。エラー: ${data.error || response.statusText}`;
messageElement.className = 'message error';
}
} catch (error) {
console.error('Error:', error);
messageElement.textContent = 'エラーが発生しました。もう一度お試しください。';
messageElement.className = 'message error';
}
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>無効なパスワードリセットリンク</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f3f4f6;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
text-align: center;
}
h1 {
color: #1f2937;
margin-bottom: 1rem;
}
p {
color: #4b5563;
}
</style>
</head>
<body>
<div class="container">
<h1>無効なリンク</h1>
<p>このパスワードリセットリンクは無効です。新しいリセットリンクを要求してください。</p>
</div>
</body>
</html>

View File

@ -1,7 +1,7 @@
from sys import prefix from sys import prefix
from rest_framework import urlpatterns from rest_framework import urlpatterns
from rest_framework.routers import DefaultRouter 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 django.urls import path, include
from knox import views as knox_views from knox import views as knox_views
@ -90,5 +90,7 @@ urlpatterns += [
path('userdetail/<int:user_id>/',update_user_detail, name='update_user_detail'), path('userdetail/<int:user_id>/',update_user_detail, name='update_user_detail'),
path('activate-member/<int:user_id>/<int:team_id>/', ActivateMemberView.as_view(), name='activate-member'), path('activate-member/<int:user_id>/<int:team_id>/', ActivateMemberView.as_view(), name='activate-member'),
path('activate-new-member/<uuid:verification_code>/<int:team_id>/', ActivateNewMemberView.as_view(), name='activate-new-member'), path('activate-new-member/<uuid:verification_code>/<int:team_id>/', ActivateNewMemberView.as_view(), name='activate-new-member'),
path('password-reset/', PasswordResetRequestView.as_view(), name='password_reset_request'),
path('reset-password/<uidb64>/<token>/', PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
] ]

View File

@ -43,7 +43,14 @@ def send_verification_email(user, activation_link):
share_send_email(subject,body,user.email) 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)
# 既にユーザーになっている人にチームへの参加要請メールを出す。 # 既にユーザーになっている人にチームへの参加要請メールを出す。

View File

@ -2,12 +2,17 @@ from .models import JpnSubPerf # このインポート文をファイルの先
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
User = get_user_model() User = get_user_model()
import traceback 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 import requests
from rest_framework import serializers from rest_framework import serializers
from django.db import IntegrityError from django.db import IntegrityError
from django.urls import reverse 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 from django.conf import settings
import uuid import uuid
from rest_framework.exceptions import ValidationError as DRFValidationError from rest_framework.exceptions import ValidationError as DRFValidationError
@ -26,7 +31,7 @@ from curses.ascii import NUL
from django.core.serializers import serialize 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 .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 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 knox.models import AuthToken
from rest_framework import viewsets, generics, status from rest_framework import viewsets, generics, status
@ -483,7 +488,6 @@ class LoginView(APIView):
password = request.data.get('password') password = request.data.get('password')
# デバッグコード # デバッグコード
from django.contrib.auth.hashers import make_password, check_password
user = CustomUser.objects.filter(email=email).first() user = CustomUser.objects.filter(email=email).first()
if user: if user:
stored_hash = user.password stored_hash = user.password
@ -1699,12 +1703,17 @@ class TempUserRegistrationView(APIView):
# 新規仮登録 # 新規仮登録
serializer = TempUserRegistrationSerializer(data=request.data) serializer = TempUserRegistrationSerializer(data=request.data)
if serializer.is_valid(): 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() verification_code = uuid.uuid4()
temp_user.verification_code = verification_code temp_user.verification_code = verification_code
#password = serializer.validated_data.pop('password')
#temp_user.set_password(password)
temp_user.save() temp_user.save()
verification_url = request.build_absolute_uri( verification_url = request.build_absolute_uri(
reverse('verify-email', kwargs={'verification_code': verification_code}) reverse('verify-email', kwargs={'verification_code': verification_code})
) )
@ -1736,6 +1745,8 @@ class VerifyEmailView(APIView):
if temp_user.is_valid(): if temp_user.is_valid():
user_data = { user_data = {
'email': temp_user.email, 'email': temp_user.email,
'is_rogaining':True, # ここでis_rogainingをTrueに設定
'password':temp_user.password,
'is_rogaining': temp_user.is_rogaining, 'is_rogaining': temp_user.is_rogaining,
'zekken_number': temp_user.zekken_number, 'zekken_number': temp_user.zekken_number,
'event_code': temp_user.event_code, 'event_code': temp_user.event_code,
@ -1752,11 +1763,13 @@ class VerifyEmailView(APIView):
try: try:
# CustomUserを作成 # CustomUserを作成
user = CustomUser.objects.create_user( user = CustomUser.objects.create(**user_data)
email=user_data['email'],
password=temp_user.password, #user = CustomUser.objects.create_user(
**{k: v for k, v in user_data.items() if k != 'email'} # email=user_data['email'],
) # password=temp_user.password, # ハッシュ化されたパスワードを直接使用
# **{k: v for k, v in user_data.items() if k != 'email'}
#)
except ValidationError as e: except ValidationError as e:
# パスワードのバリデーションエラーなどの処理 # パスワードのバリデーションエラーなどの処理
return render(request, 'verification_error.html', {'message': str(e), 'title': 'エラー'}) return render(request, 'verification_error.html', {'message': str(e), 'title': 'エラー'})
@ -1795,3 +1808,55 @@ class TeamMembersWithUserView(generics.ListAPIView):
team_id = self.kwargs['team_id'] team_id = self.kwargs['team_id']
return Member.objects.filter(team_id=team_id).select_related('user', 'team') 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)