This commit is contained in:
Louis Mazin 2026-09-25 10:29:24 +02:00
parent 71978c7877
commit 667a0035c6
4 changed files with 1987 additions and 19 deletions

View File

@ -5,4 +5,6 @@ urlpatterns = [
path('', views.EventListView.as_view(), name='event-list'), path('', views.EventListView.as_view(), name='event-list'),
path('<int:id>/', views.EventDetailView.as_view(), name='event-detail'), path('<int:id>/', views.EventDetailView.as_view(), name='event-detail'),
path('create/', views.EventCreateView.as_view(), name='event-create'), path('create/', views.EventCreateView.as_view(), name='event-create'),
path('change-password/', views.ChangePasswordView.as_view(), name='change-password',
),
] ]

View File

@ -7,6 +7,7 @@ from django.utils import timezone
from datetime import timedelta from datetime import timedelta
from .models import Invitation, User from .models import Invitation, User
from .serializers import UserSerializer, RegisterSerializer from .serializers import UserSerializer, RegisterSerializer
from django.contrib.auth import password_validation
class RegisterView(generics.CreateAPIView): class RegisterView(generics.CreateAPIView):
queryset = User.objects.all() queryset = User.objects.all()
@ -73,7 +74,42 @@ class UserPromoteView(APIView):
except User.DoesNotExist: except User.DoesNotExist:
return Response({'error': 'Utilisateur non trouvé'}, status=404) return Response({'error': 'Utilisateur non trouvé'}, status=404)
class ChangePasswordView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
current_password = request.data.get("current_password", "")
new_password = request.data.get("new_password", "")
confirm_password = request.data.get("confirm_password", "")
if not request.user.check_password(current_password):
return Response(
{"current_password": ["Mot de passe actuel incorrect."]},
status=status.HTTP_400_BAD_REQUEST,
)
if new_password != confirm_password:
return Response(
{"confirm_password": ["Les mots de passe ne correspondent pas."]},
status=status.HTTP_400_BAD_REQUEST,
)
try:
password_validation.validate_password(
new_password,
user=request.user,
)
except DjangoValidationError as error:
return Response(
{"new_password": error.messages},
status=status.HTTP_400_BAD_REQUEST,
)
request.user.set_password(new_password)
request.user.save(update_fields=["password"])
return Response({"detail": "Mot de passe modifié."})
class UserDeleteView(UserDetailView): class UserDeleteView(UserDetailView):
http_method_names = ['delete', 'options'] http_method_names = ['delete', 'options']

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,89 @@
import React from 'react'; import React, { useState } from 'react';
import LoadingCard from '@/components/LoadingCard'; import LoadingCard from '@/components/LoadingCard';
import MainCard from '@/components/MainCard'; import MainCard from '@/components/MainCard';
import { useAuth } from '../context/useAuth'; import { useAuth } from '../context/useAuth';
import { import { CardContent } from '@/components/ui/card';
CardContent,
} from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { User } from 'lucide-react'; import { User } from 'lucide-react';
import api from '../api/axios'; // Adapte ce chemin si ton instance Axios est ailleurs.
const MyspacePage = () => { const MyspacePage = () => {
const { user } = useAuth(); const { user } = useAuth();
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordError, setPasswordError] = useState('');
const [passwordSuccess, setPasswordSuccess] = useState('');
const [savingPassword, setSavingPassword] = useState(false);
const resetPasswordForm = () => {
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
setPasswordError('');
};
const handlePasswordDialogChange = (open) => {
if (savingPassword) return;
setPasswordDialogOpen(open);
if (!open) resetPasswordForm();
};
const handleChangePassword = async (event) => {
event.preventDefault();
setPasswordError('');
setPasswordSuccess('');
if (!currentPassword || !newPassword || !confirmPassword) {
setPasswordError('Remplis tous les champs.');
return;
}
if (newPassword !== confirmPassword) {
setPasswordError('Les nouveaux mots de passe ne correspondent pas.');
return;
}
setSavingPassword(true);
try {
await api.post('/users/change-password/', {
current_password: currentPassword,
new_password: newPassword,
confirm_password: confirmPassword,
});
resetPasswordForm();
setPasswordDialogOpen(false);
setPasswordSuccess('Mot de passe modifié.');
} catch (error) {
const data = error.response?.data;
const message =
data?.current_password?.[0] ||
data?.new_password?.[0] ||
data?.confirm_password?.[0] ||
data?.detail ||
'Impossible de modifier le mot de passe.';
setPasswordError(message);
} finally {
setSavingPassword(false);
}
};
if (!user) { if (!user) {
return ( return (
@ -24,25 +97,24 @@ const MyspacePage = () => {
return ( return (
<MainCard title={user.first_name} icon={User}> <MainCard title={user.first_name} icon={User}>
<CardContent className="overflow-y-auto"> <CardContent className="overflow-y-auto">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between gap-4">
<span className="font-medium">Prénom:</span> <span className="font-medium">Prénom :</span>
<span>{user.first_name}</span> <span>{user.first_name}</span>
</div> </div>
<Separator /> <Separator />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between gap-4">
<span className="font-medium">Nom:</span> <span className="font-medium">Nom :</span>
<span>{user.last_name}</span> <span>{user.last_name}</span>
</div> </div>
<Separator /> <Separator />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between gap-4">
<span className="font-medium">Admin:</span> <span className="font-medium">Admin :</span>
<Badge variant={user.is_admin ? 'default' : 'secondary'}> <Badge variant={user.is_admin ? 'default' : 'secondary'}>
{user.is_admin ? 'Oui' : 'Non'} {user.is_admin ? 'Oui' : 'Non'}
</Badge> </Badge>
@ -50,23 +122,122 @@ const MyspacePage = () => {
<Separator /> <Separator />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between gap-4">
<span className="font-medium">Nom d'utilisateur:</span> <span className="font-medium">Nom d'utilisateur :</span>
<span>{user.username}</span> <span>{user.username}</span>
</div> </div>
<Separator /> <Separator />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between gap-4">
<span className="font-medium">Date d'inscription:</span> <span className="font-medium">Date d'inscription :</span>
<span> <span>
{user.date_joined {user.date_joined
? new Date(user.date_joined).toLocaleDateString('fr-FR') ? new Date(user.date_joined).toLocaleDateString('fr-FR')
: 'Non spécifié'} : 'Non spécifié'}
</span> </span>
</div> </div>
<Separator />
<div className="flex items-center justify-between gap-4">
<span className="font-medium">Mot de passe :</span>
<Button
type="button"
variant="outline"
onClick={() => {
setPasswordSuccess('');
setPasswordDialogOpen(true);
}}
>
Modifier le mot de passe
</Button>
</div>
{passwordSuccess && (
<p role="status" className="text-sm text-green-600">
{passwordSuccess}
</p>
)}
</div> </div>
</CardContent> </CardContent>
<Dialog
open={passwordDialogOpen}
onOpenChange={handlePasswordDialogChange}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Modifier le mot de passe</DialogTitle>
<DialogDescription>
Saisis ton mot de passe actuel, puis le nouveau deux fois.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleChangePassword} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="current-password">Mot de passe actuel</Label>
<Input
id="current-password"
type="password"
autoComplete="current-password"
value={currentPassword}
onChange={(event) => setCurrentPassword(event.target.value)}
disabled={savingPassword}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="new-password">Nouveau mot de passe</Label>
<Input
id="new-password"
type="password"
autoComplete="new-password"
value={newPassword}
onChange={(event) => setNewPassword(event.target.value)}
disabled={savingPassword}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password">
Confirmer le nouveau mot de passe
</Label>
<Input
id="confirm-password"
type="password"
autoComplete="new-password"
value={confirmPassword}
onChange={(event) => setConfirmPassword(event.target.value)}
disabled={savingPassword}
required
/>
</div>
{passwordError && (
<p role="alert" className="text-sm text-destructive">
{passwordError}
</p>
)}
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => handlePasswordDialogChange(false)}
disabled={savingPassword}
>
Annuler
</Button>
<Button type="submit" disabled={savingPassword}>
{savingPassword ? 'Modification...' : 'Enregistrer'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</MainCard> </MainCard>
); );
}; };