From 1aa051e8ffd67178807b51487b60c1ab8836e058 Mon Sep 17 00:00:00 2001 From: Ulle9 Date: Thu, 10 Aug 2023 13:53:19 +0300 Subject: [PATCH] Update password function --- rsconcept/backend/apps/users/serializers.py | 11 +++ rsconcept/backend/apps/users/urls.py | 1 + rsconcept/backend/apps/users/views.py | 30 ++++++++ rsconcept/frontend/src/App.tsx | 6 +- .../src/context/UserProfileContext.tsx | 30 ++++++-- .../pages/UserProfilePage/ChangePassword.tsx | 68 +++++++++++++++++++ .../src/pages/UserProfilePage/UserProfile.tsx | 24 +++++-- rsconcept/frontend/src/utils/backendAPI.ts | 10 ++- rsconcept/frontend/src/utils/models.ts | 5 ++ 9 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 rsconcept/frontend/src/pages/UserProfilePage/ChangePassword.tsx diff --git a/rsconcept/backend/apps/users/serializers.py b/rsconcept/backend/apps/users/serializers.py index cd81331b..6b76243a 100644 --- a/rsconcept/backend/apps/users/serializers.py +++ b/rsconcept/backend/apps/users/serializers.py @@ -70,6 +70,17 @@ class UserSerializer(serializers.ModelSerializer): 'last_name', ] +class ChangePasswordSerializer(serializers.Serializer): + """ + Serializer for password change endpoint. + """ + old_password = serializers.CharField(required=True) + new_password = serializers.CharField(required=True) + + # def validate(self, attrs): + # if attrs['new_password'] != "123": + # raise serializers.ValidationError({"password": r"Пароль не '123'"}) + # return attrs class SignupSerializer(serializers.ModelSerializer): ''' User profile create ''' diff --git a/rsconcept/backend/apps/users/urls.py b/rsconcept/backend/apps/users/urls.py index c80049d0..403f3932 100644 --- a/rsconcept/backend/apps/users/urls.py +++ b/rsconcept/backend/apps/users/urls.py @@ -10,4 +10,5 @@ urlpatterns = [ path('api/signup', views.SignupAPIView.as_view()), path('api/login', views.LoginAPIView.as_view()), path('api/logout', views.LogoutAPIView.as_view()), + path('api/change-password', views.UpdatePassword.as_view()), ] diff --git a/rsconcept/backend/apps/users/views.py b/rsconcept/backend/apps/users/views.py index 137dde1a..7c09a51d 100644 --- a/rsconcept/backend/apps/users/views.py +++ b/rsconcept/backend/apps/users/views.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from . import serializers from . import models +from django.contrib.auth.models import User class LoginAPIView(views.APIView): ''' @@ -74,3 +75,32 @@ class UserProfileAPIView(generics.RetrieveUpdateAPIView): def get_object(self): return self.request.user + +class UpdatePassword(views.APIView): + """ + An endpoint for changing password. + """ + # {"username": "admin", "password": "1234"} + # {"old_password": "1234", "new_password": "1234"} + permission_classes = (permissions.IsAuthenticated, ) + + def get_object(self, queryset=None): + return self.request.user + + def patch(self, request, *args, **kwargs): + self.object = self.get_object() + + serializer = serializers.ChangePasswordSerializer(data=request.data) + + if serializer.is_valid(): + # Check old password + old_password = serializer.data.get("old_password") + if not self.object.check_password(old_password): + return Response({"old_password": ["Wrong password."]}, + status=status.HTTP_400_BAD_REQUEST) + # set_password also hashes the password that the user will get + self.object.set_password(serializer.data.get("new_password")) + self.object.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/rsconcept/frontend/src/App.tsx b/rsconcept/frontend/src/App.tsx index ab78ed49..83bbd004 100644 --- a/rsconcept/frontend/src/App.tsx +++ b/rsconcept/frontend/src/App.tsx @@ -3,6 +3,7 @@ import { Route, Routes } from 'react-router-dom'; import Footer from './components/Footer'; import Navigation from './components/Navigation/Navigation'; import ToasterThemed from './components/ToasterThemed'; +import { useConceptTheme } from './context/ThemeContext'; import CreateRSFormPage from './pages/CreateRSFormPage'; import HomePage from './pages/HomePage'; import LibraryPage from './pages/LibraryPage'; @@ -15,6 +16,9 @@ import RSFormPage from './pages/RSFormPage'; import UserProfilePage from './pages/UserProfilePage'; function App () { + const {noNavigation} = useConceptTheme() + const nav_height: string = noNavigation ? "7.5rem" : "7.4rem"; + const main_clsN: string = `min-h-[calc(100vh-${nav_height})] px-2 h-fit`; return (
@@ -24,7 +28,7 @@ function App () { draggable={false} pauseOnFocusLoss={false} /> -
+
} /> diff --git a/rsconcept/frontend/src/context/UserProfileContext.tsx b/rsconcept/frontend/src/context/UserProfileContext.tsx index d5e20254..6179b18f 100644 --- a/rsconcept/frontend/src/context/UserProfileContext.tsx +++ b/rsconcept/frontend/src/context/UserProfileContext.tsx @@ -1,19 +1,22 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { ErrorInfo } from '../components/BackendError'; -import { DataCallback, getProfile, patchProfile } from '../utils/backendAPI'; -import { IUserProfile, IUserUpdateData } from '../utils/models'; +import { DataCallback, getProfile, patchPassword,patchProfile } from '../utils/backendAPI'; +import { IUserProfile, IUserUpdateData, IUserUpdatePassword } from '../utils/models'; +import { useAuth } from './AuthContext'; -interface IUserProfileContextContext { +interface IUserProfileContext { user: IUserProfile | undefined loading: boolean processing: boolean error: ErrorInfo setError: (error: ErrorInfo) => void updateUser: (data: IUserUpdateData, callback?: DataCallback) => void + updatePassword: (data: IUserUpdatePassword, callback?: () => void) => void } -const ProfileContext = createContext(null); +const ProfileContext = createContext(null); + export const useUserProfile = () => { const context = useContext(ProfileContext); if (!context) { @@ -33,6 +36,7 @@ export const UserProfileState = ({ children }: UserProfileStateProps) => { const [loading, setLoading] = useState(false); const [processing, setProcessing] = useState(false); const [error, setError] = useState(undefined); + const auth = useAuth() const reload = useCallback( () => { @@ -63,13 +67,29 @@ export const UserProfileState = ({ children }: UserProfileStateProps) => { }, [setUser] ); + const updatePassword = useCallback( + (data: IUserUpdatePassword, callback?: () => void) => { + setError(undefined); + patchPassword({ + data: data, + showError: true, + setLoading: setProcessing, + onError: error => { setError(error); }, + onSuccess: () => { + setUser(undefined); + auth.logout(); + if (callback) callback(); + }}); + }, [setUser, auth] + ); + useEffect(() => { reload(); }, [reload]); return ( {children} diff --git a/rsconcept/frontend/src/pages/UserProfilePage/ChangePassword.tsx b/rsconcept/frontend/src/pages/UserProfilePage/ChangePassword.tsx new file mode 100644 index 00000000..1c5c7e24 --- /dev/null +++ b/rsconcept/frontend/src/pages/UserProfilePage/ChangePassword.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { toast } from 'react-toastify'; + +import TextInput from '../../components/Common/TextInput'; +import { useUserProfile } from '../../context/UserProfileContext'; +import { IUserUpdatePassword } from '../../utils/models'; + + +export function ChangePassword() { + const { updatePassword, processing } = useUserProfile(); + + const [old_password, setOldPassword] = useState(''); + const [new_password, setNewPassword] = useState(''); + const [new_password_repeat, setNewPasswordRepeat] = useState(''); + const [password_equal, setPasswordEqual] = useState(true); + const navigate = useNavigate(); + + const input_class: string = `flex-grow max-w-xl px-3 py-2 border center min-w-full ${password_equal ? "" : "text-red-500"}` + + function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + if (new_password !== new_password_repeat) { + setPasswordEqual(false); + toast.error('Пароли не совпадают'); + } + else { + const data: IUserUpdatePassword = { + old_password: old_password, + new_password: new_password, + }; + updatePassword(data, () => {toast.success('Изменения сохранены'); navigate('/login')}); + } + } + + return ( +
+
+ (setOldPassword(event.target.value))} + /> + (setNewPassword(event.target.value), setPasswordEqual(true))} + /> + (setNewPasswordRepeat(event.target.value), setPasswordEqual(true))} + /> +
+ +
+ + +
+ )} \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/UserProfilePage/UserProfile.tsx b/rsconcept/frontend/src/pages/UserProfilePage/UserProfile.tsx index 539da2b4..a4deed6b 100644 --- a/rsconcept/frontend/src/pages/UserProfilePage/UserProfile.tsx +++ b/rsconcept/frontend/src/pages/UserProfilePage/UserProfile.tsx @@ -4,15 +4,20 @@ import { toast } from 'react-toastify'; import TextInput from '../../components/Common/TextInput'; import { useUserProfile } from '../../context/UserProfileContext'; import { IUserUpdateData } from '../../utils/models'; +import { ChangePassword } from './ChangePassword'; export function UserProfile() { - const { updateUser, user, processing } = useUserProfile(); + const { updateUser, user, processing, error } = useUserProfile(); const [username, setUsername] = useState(''); const [email, setEmail] = useState(''); const [first_name, setFirstName] = useState(''); const [last_name, setLastName] = useState(''); + + + // const [showChangePassword, setShowChangePassword] = useState(false); + useLayoutEffect(() => { if (user) { @@ -35,8 +40,10 @@ export function UserProfile() { } return ( -
-
+

Учетные данные пользователя

+
+
+ setLastName(event.target.value)}/> setEmail(event.target.value)}/> -
+
+ +
+ +
- )} \ No newline at end of file diff --git a/rsconcept/frontend/src/utils/backendAPI.ts b/rsconcept/frontend/src/utils/backendAPI.ts index 8d364431..ea828784 100644 --- a/rsconcept/frontend/src/utils/backendAPI.ts +++ b/rsconcept/frontend/src/utils/backendAPI.ts @@ -9,7 +9,7 @@ import { ICurrentUser, IExpressionParse, IRSExpression, IRSFormCreateData, IRSFormData, IRSFormMeta, IRSFormUpdateData, IRSFormUploadData, IUserInfo, - IUserLoginData, IUserProfile, IUserSignupData, IUserUpdateData + IUserLoginData, IUserProfile, IUserSignupData, IUserUpdateData, IUserUpdatePassword } from './models' export function initBackend() { @@ -101,6 +101,14 @@ export function patchProfile(request: FrontExchange) { + AxiosPatch({ + title: 'Update Password', + endpoint: '/users/api/change-password', + request: request + }); +} + export function getActiveUsers(request: FrontPull) { AxiosGet({ title: 'Active users list', diff --git a/rsconcept/frontend/src/utils/models.ts b/rsconcept/frontend/src/utils/models.ts index 1ec221ae..3ee977a1 100644 --- a/rsconcept/frontend/src/utils/models.ts +++ b/rsconcept/frontend/src/utils/models.ts @@ -25,6 +25,11 @@ export interface IUserUpdateData extends Omit {} export interface IUserProfile extends Omit {} export interface IUserInfo extends Omit {} +export interface IUserUpdatePassword { + old_password: string + new_password: string +} + // ======== RS Parsing ============ export enum Syntax { UNDEF = 'undefined',