diff --git a/backend/keinasystem/urls.py b/backend/keinasystem/urls.py index 0d79bac..5f28999 100644 --- a/backend/keinasystem/urls.py +++ b/backend/keinasystem/urls.py @@ -19,6 +19,28 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView from apps.fields.views import OfficialKyosaiFieldViewSet, OfficialChusankanFieldViewSet +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import IsAuthenticated + + +class ChangePasswordView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + user = request.user + current_password = request.data.get('current_password', '') + new_password = request.data.get('new_password', '') + if not current_password or not new_password: + return Response({'error': '現在のパスワードと新しいパスワードを入力してください'}, status=status.HTTP_400_BAD_REQUEST) + if not user.check_password(current_password): + return Response({'error': '現在のパスワードが正しくありません'}, status=status.HTTP_400_BAD_REQUEST) + if len(new_password) < 8: + return Response({'error': 'パスワードは8文字以上にしてください'}, status=status.HTTP_400_BAD_REQUEST) + user.set_password(new_password) + user.save() + return Response({'status': 'ok'}) master_router = DefaultRouter() master_router.register(r'kyosai-fields', OfficialKyosaiFieldViewSet, basename='kyosai-field') @@ -32,5 +54,6 @@ urlpatterns = [ path('api/reports/', include('apps.reports.urls')), path('api/auth/jwt/create/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('api/auth/jwt/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('api/auth/change-password/', ChangePasswordView.as_view(), name='change-password'), path('api/mail/', include('apps.mail.urls')), ] diff --git a/frontend/src/app/settings/password/page.tsx b/frontend/src/app/settings/password/page.tsx new file mode 100644 index 0000000..231fe56 --- /dev/null +++ b/frontend/src/app/settings/password/page.tsx @@ -0,0 +1,141 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { api } from '@/lib/api'; +import Navbar from '@/components/Navbar'; +import { KeyRound, CheckCircle } from 'lucide-react'; + +export default function ChangePasswordPage() { + const router = useRouter(); + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + const [submitting, setSubmitting] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (newPassword !== confirmPassword) { + setError('新しいパスワードと確認用パスワードが一致しません'); + return; + } + if (newPassword.length < 8) { + setError('パスワードは8文字以上にしてください'); + return; + } + + setSubmitting(true); + try { + await api.post('/auth/change-password/', { + current_password: currentPassword, + new_password: newPassword, + }); + setSuccess(true); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + } catch (err: unknown) { + const msg = + (err as { response?: { data?: { error?: string } } })?.response?.data?.error || + 'パスワードの変更に失敗しました'; + setError(msg); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ +
+
+
+ +

パスワード変更

+
+ + {success ? ( +
+ +

パスワードを変更しました

+

次回ログインから新しいパスワードをお使いください

+ +
+ ) : ( +
+
+ + setCurrentPassword(e.target.value)} + required + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500" + /> +
+
+ + setNewPassword(e.target.value)} + required + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500" + /> +

8文字以上

+
+
+ + setConfirmPassword(e.target.value)} + required + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500" + /> +
+ + {error && ( +
+ {error} +
+ )} + +
+ + +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 03a8dc2..9bcc139 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,7 +1,7 @@ 'use client'; import { useRouter, usePathname } from 'next/navigation'; -import { LogOut, Wheat, MapPin, FileText, Upload, LayoutDashboard, Mail, History, Shield } from 'lucide-react'; +import { LogOut, Wheat, MapPin, FileText, Upload, LayoutDashboard, Mail, History, Shield, KeyRound } from 'lucide-react'; import { logout } from '@/lib/api'; export default function Navbar() { @@ -102,7 +102,14 @@ export default function Navbar() { -
+
+