Update password function

This commit is contained in:
Ulle9 2023-08-10 13:53:19 +03:00
parent 47564c9d91
commit 1aa051e8ff
9 changed files with 171 additions and 14 deletions

View File

@ -70,6 +70,17 @@ class UserSerializer(serializers.ModelSerializer):
'last_name', '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): class SignupSerializer(serializers.ModelSerializer):
''' User profile create ''' ''' User profile create '''

View File

@ -10,4 +10,5 @@ urlpatterns = [
path('api/signup', views.SignupAPIView.as_view()), path('api/signup', views.SignupAPIView.as_view()),
path('api/login', views.LoginAPIView.as_view()), path('api/login', views.LoginAPIView.as_view()),
path('api/logout', views.LogoutAPIView.as_view()), path('api/logout', views.LogoutAPIView.as_view()),
path('api/change-password', views.UpdatePassword.as_view()),
] ]

View File

@ -6,6 +6,7 @@ from rest_framework.response import Response
from . import serializers from . import serializers
from . import models from . import models
from django.contrib.auth.models import User
class LoginAPIView(views.APIView): class LoginAPIView(views.APIView):
''' '''
@ -74,3 +75,32 @@ class UserProfileAPIView(generics.RetrieveUpdateAPIView):
def get_object(self): def get_object(self):
return self.request.user 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)

View File

@ -3,6 +3,7 @@ import { Route, Routes } from 'react-router-dom';
import Footer from './components/Footer'; import Footer from './components/Footer';
import Navigation from './components/Navigation/Navigation'; import Navigation from './components/Navigation/Navigation';
import ToasterThemed from './components/ToasterThemed'; import ToasterThemed from './components/ToasterThemed';
import { useConceptTheme } from './context/ThemeContext';
import CreateRSFormPage from './pages/CreateRSFormPage'; import CreateRSFormPage from './pages/CreateRSFormPage';
import HomePage from './pages/HomePage'; import HomePage from './pages/HomePage';
import LibraryPage from './pages/LibraryPage'; import LibraryPage from './pages/LibraryPage';
@ -15,6 +16,9 @@ import RSFormPage from './pages/RSFormPage';
import UserProfilePage from './pages/UserProfilePage'; import UserProfilePage from './pages/UserProfilePage';
function App () { 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 ( return (
<div className='antialiased clr-app'> <div className='antialiased clr-app'>
<Navigation /> <Navigation />
@ -24,7 +28,7 @@ function App () {
draggable={false} draggable={false}
pauseOnFocusLoss={false} pauseOnFocusLoss={false}
/> />
<main className='min-h-[calc(100vh-7.5rem)] px-2 h-fit'> <main className={main_clsN}>
<Routes> <Routes>
<Route path='/' element={ <HomePage/>} /> <Route path='/' element={ <HomePage/>} />

View File

@ -1,19 +1,22 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { ErrorInfo } from '../components/BackendError'; import { ErrorInfo } from '../components/BackendError';
import { DataCallback, getProfile, patchProfile } from '../utils/backendAPI'; import { DataCallback, getProfile, patchPassword,patchProfile } from '../utils/backendAPI';
import { IUserProfile, IUserUpdateData } from '../utils/models'; import { IUserProfile, IUserUpdateData, IUserUpdatePassword } from '../utils/models';
import { useAuth } from './AuthContext';
interface IUserProfileContextContext { interface IUserProfileContext {
user: IUserProfile | undefined user: IUserProfile | undefined
loading: boolean loading: boolean
processing: boolean processing: boolean
error: ErrorInfo error: ErrorInfo
setError: (error: ErrorInfo) => void setError: (error: ErrorInfo) => void
updateUser: (data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => void updateUser: (data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => void
updatePassword: (data: IUserUpdatePassword, callback?: () => void) => void
} }
const ProfileContext = createContext<IUserProfileContextContext | null>(null); const ProfileContext = createContext<IUserProfileContext | null>(null);
export const useUserProfile = () => { export const useUserProfile = () => {
const context = useContext(ProfileContext); const context = useContext(ProfileContext);
if (!context) { if (!context) {
@ -33,6 +36,7 @@ export const UserProfileState = ({ children }: UserProfileStateProps) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
const auth = useAuth()
const reload = useCallback( const reload = useCallback(
() => { () => {
@ -63,13 +67,29 @@ export const UserProfileState = ({ children }: UserProfileStateProps) => {
}, [setUser] }, [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(() => { useEffect(() => {
reload(); reload();
}, [reload]); }, [reload]);
return ( return (
<ProfileContext.Provider <ProfileContext.Provider
value={{user, updateUser, error, loading, setError, processing}} value={{user, updateUser, updatePassword, error, loading, setError, processing}}
> >
{children} {children}
</ProfileContext.Provider> </ProfileContext.Provider>

View File

@ -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<HTMLFormElement>) {
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 (
<div className='flex-grow max-w-sm px-4'>
<form onSubmit={handleSubmit} className='flex-grow min-h-full px-6 py-2 border min-w-fit bg-slate-200'>
<TextInput id='old_password'
type='password'
label='Введите старый пароль:'
value={old_password}
onChange={event => (setOldPassword(event.target.value))}
/>
<TextInput id='new_password'
className={input_class}
label="Введите новый пароль:"
value={new_password}
onChange={event => (setNewPassword(event.target.value), setPasswordEqual(true))}
/>
<TextInput id='new_password'
className={input_class}
label="Повторите новый пароль:"
value={new_password_repeat}
onChange={event => (setNewPasswordRepeat(event.target.value), setPasswordEqual(true))}
/>
<div className='relative flex justify-center my-4 border'>
<button
type='submit'
className='absolute bottom-0 px-2 py-1 bg-blue-500 border'
disabled={processing}>
<span>Сменить пароль</span>
</button>
</div>
</form>
</div>
)}

View File

@ -4,16 +4,21 @@ import { toast } from 'react-toastify';
import TextInput from '../../components/Common/TextInput'; import TextInput from '../../components/Common/TextInput';
import { useUserProfile } from '../../context/UserProfileContext'; import { useUserProfile } from '../../context/UserProfileContext';
import { IUserUpdateData } from '../../utils/models'; import { IUserUpdateData } from '../../utils/models';
import { ChangePassword } from './ChangePassword';
export function UserProfile() { export function UserProfile() {
const { updateUser, user, processing } = useUserProfile(); const { updateUser, user, processing, error } = useUserProfile();
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [first_name, setFirstName] = useState(''); const [first_name, setFirstName] = useState('');
const [last_name, setLastName] = useState(''); const [last_name, setLastName] = useState('');
// const [showChangePassword, setShowChangePassword] = useState(false);
useLayoutEffect(() => { useLayoutEffect(() => {
if (user) { if (user) {
setUsername(user.username); setUsername(user.username);
@ -35,8 +40,10 @@ export function UserProfile() {
} }
return ( return (
<form onSubmit={handleSubmit} className='flex-grow max-w-xl px-4 py-2 border min-w-fit'> <div><h1 className='flex justify-center py-2'> Учетные данные пользователя </h1>
<div className='flex flex-col items-center justify-center px-2 py-2 border'> <div className='flex flex-row justify-center py-2'>
<div className='flex-grow max-w-sm px-4 '>
<form onSubmit={handleSubmit} className='flex-grow px-6 py-2 border center min-w-fit'>
<TextInput id='username' <TextInput id='username'
label='Логин:' label='Логин:'
value={username} value={username}
@ -49,15 +56,18 @@ export function UserProfile() {
/> />
<TextInput id='last_name' label="Фамилия:" value={last_name} onChange={event => setLastName(event.target.value)}/> <TextInput id='last_name' label="Фамилия:" value={last_name} onChange={event => setLastName(event.target.value)}/>
<TextInput id='email' label="Электронная почта:" value={email} onChange={event => setEmail(event.target.value)}/> <TextInput id='email' label="Электронная почта:" value={email} onChange={event => setEmail(event.target.value)}/>
<div className='flex items-center justify-between my-4'> <div className='flex items-center justify-center my-4'>
<button <button
type='submit' type='submit'
className='px-2 py-1 bg-green-500 border' className='px-2 py-1 bg-green-500 border center'
disabled={processing}> disabled={processing}>
<span>Сохранить</span> <span>Сохранить мои данные</span>
</button> </button>
</div> </div>
</div>
</form> </form>
</div>
<ChangePassword />
</div>
</div>
)} )}

View File

@ -9,7 +9,7 @@ import {
ICurrentUser, IExpressionParse, IRSExpression, ICurrentUser, IExpressionParse, IRSExpression,
IRSFormCreateData, IRSFormData, IRSFormCreateData, IRSFormData,
IRSFormMeta, IRSFormUpdateData, IRSFormUploadData, IUserInfo, IRSFormMeta, IRSFormUpdateData, IRSFormUploadData, IUserInfo,
IUserLoginData, IUserProfile, IUserSignupData, IUserUpdateData IUserLoginData, IUserProfile, IUserSignupData, IUserUpdateData, IUserUpdatePassword
} from './models' } from './models'
export function initBackend() { export function initBackend() {
@ -101,6 +101,14 @@ export function patchProfile(request: FrontExchange<IUserUpdateData, IUserProfil
}); });
} }
export function patchPassword(request: FrontPush<IUserUpdatePassword>) {
AxiosPatch({
title: 'Update Password',
endpoint: '/users/api/change-password',
request: request
});
}
export function getActiveUsers(request: FrontPull<IUserInfo[]>) { export function getActiveUsers(request: FrontPull<IUserInfo[]>) {
AxiosGet({ AxiosGet({
title: 'Active users list', title: 'Active users list',

View File

@ -25,6 +25,11 @@ export interface IUserUpdateData extends Omit<IUser, 'is_staff' | 'id'> {}
export interface IUserProfile extends Omit<IUser, 'is_staff'> {} export interface IUserProfile extends Omit<IUser, 'is_staff'> {}
export interface IUserInfo extends Omit<IUserProfile, 'email'> {} export interface IUserInfo extends Omit<IUserProfile, 'email'> {}
export interface IUserUpdatePassword {
old_password: string
new_password: string
}
// ======== RS Parsing ============ // ======== RS Parsing ============
export enum Syntax { export enum Syntax {
UNDEF = 'undefined', UNDEF = 'undefined',