Je suis complètement confus par le middleware Django disponible:
Je veux simplement faire fonctionner la fonctionnalité de réinitialisation de mot de passe (et plus tard de changement de mot de passe), en utilisant Django
avec rest_auth
Sur le backend et Vue sur le frontend.
Jusqu'à présent, j'ai fait un CustomPasswordResetView
:
# project/accounts/views.py
from rest_auth.views import PasswordResetView
class CustomPasswordResetView(PasswordResetView):
pass
et un CustomPasswordResetSerializer
:
# project/accounts/serializers.py
from rest_auth.serializers import PasswordResetSerializer
class CustomPasswordResetSerializer(PasswordResetSerializer):
email = serializers.EmailField()
password_reset_form_class = ResetPasswordForm
def validate_email(self, value):
# Create PasswordResetForm with the serializer
self.reset_form = self.password_reset_form_class(data=self.initial_data)
if not self.reset_form.is_valid():
raise serializers.ValidationError(self.reset_form.errors)
###### FILTER YOUR USER MODEL ######
if not get_user_model().objects.filter(email=value).exists():
raise serializers.ValidationError(_('Invalid e-mail address'))
return value
def save(self):
request = self.context.get('request')
# Set some values to trigger the send_email method.
opts = {
'use_https': request.is_secure(),
'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'),
'request': request,
}
opts.update(self.get_email_options())
self.reset_form.save(**opts)
Dans le settings.py
J'ai ces champs, qui me semblent pertinents pour mon problème:
# project/vuedj/settings.py
REST_AUTH_SERIALIZERS = {
"USER_DETAILS_SERIALIZER": "accounts.serializers.CustomUserDetailsSerializer",
"LOGIN_SERIALIZER": "accounts.serializers.CustomUserLoginSerializer",
"PASSWORD_RESET_SERIALIZER": "accounts.serializers.CustomPasswordResetSerializer"
}
(Le settings.py
Complet est joint en bas)
Mes URL détectent déjà ma demande d'API afin d'envoyer l'e-mail de réinitialisation du mot de passe:
# project/vuedj/urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('api.urls')),
path('accounts/', include('allauth.urls')),
path('', api_views.index, name='home')
]
# project/api/urls.py
urlpatterns = [
path('auth/', include('accounts.urls')),
# other paths...
]
# project/accounts/urls.py
urlpatterns = [
path('', acc_views.UserListView.as_view(), name='user-list'),
path('login/', acc_views.UserLoginView.as_view(), name='login'),
path('logout/', acc_views.UserLogoutView.as_view(), name='logout'),
path('register/', acc_views.CustomRegisterView.as_view(), name='register'),
path('reset-password/', acc_views.CustomPasswordResetView.as_view(), name='reset-password'),
path('reset-password-confirm/', acc_views.CustomPasswordResetConfirmView.as_view(), name='reset-password-confirm'),
path('<int:pk>/', acc_views.UserDetailView.as_view(), name='user-detail')
]
La vue CustomPasswordReset va éventuellement générer un email Nice avec un lien Nice pw-reset. Le lien est valide, en cliquant dessus, je peux parfaitement réinitialiser le mot de passe à travers les modèles allauth.
Ce code est utilisé par rest-auth (indirectement) pour générer le jeton de réinitialisation:
# project/.venv/Lib/site-packages/allauth/account/forms.py
def save(self, request, **kwargs):
current_site = get_current_site(request)
email = self.cleaned_data["email"]
token_generator = kwargs.get("token_generator",
default_token_generator)
for user in self.users:
temp_key = token_generator.make_token(user)
# save it to the password reset model
# password_reset = PasswordReset(user=user, temp_key=temp_key)
# password_reset.save()
# send the password reset email
path = reverse("account_reset_password_from_key",
kwargs=dict(uidb36=user_pk_to_url_str(user),
key=temp_key))
url = build_absolute_uri(
request, path)
context = {"current_site": current_site,
"user": user,
"password_reset_url": url,
"request": request}
if app_settings.AUTHENTICATION_METHOD \
!= AuthenticationMethod.EMAIL:
context['username'] = user_username(user)
get_adapter(request).send_mail(
'account/email/password_reset_key',
email,
context)
return self.cleaned_data["email"]
Ce PasswordResetTokenGenerator
est utilisé dans le code ci-dessus:
# project/.venv/Lib/site-packages/Django/contrib/auth/tokens.py
class PasswordResetTokenGenerator:
"""
Strategy object used to generate and check tokens for the password
reset mechanism.
"""
key_salt = "Django.contrib.auth.tokens.PasswordResetTokenGenerator"
secret = settings.SECRET_KEY
def make_token(self, user):
"""
Return a token that can be used once to do a password reset
for the given user.
"""
return self._make_token_with_timestamp(user, self._num_days(self._today()))
def check_token(self, user, token):
"""
Check that a password reset token is correct for a given user.
"""
if not (user and token):
return False
# Parse the token
try:
ts_b36, hash = token.split("-")
except ValueError:
return False
try:
ts = base36_to_int(ts_b36)
except ValueError:
return False
# Check that the timestamp/uid has not been tampered with
if not constant_time_compare(self._make_token_with_timestamp(user, ts), token):
return False
# Check the timestamp is within limit. Timestamps are rounded to
# midnight (server time) providing a resolution of only 1 day. If a
# link is generated 5 minutes before midnight and used 6 minutes later,
# that counts as 1 day. Therefore, PASSWORD_RESET_TIMEOUT_DAYS = 1 means
# "at least 1 day, could be up to 2."
if (self._num_days(self._today()) - ts) > settings.PASSWORD_RESET_TIMEOUT_DAYS:
return False
return True
Les classes ci-dessus seront appelées par le rest_auth
PasswordResetView
:
# project/.venv/Lib/site-packages/rest_auth/views.py
class PasswordResetView(GenericAPIView):
"""
Calls Django Auth PasswordResetForm save method.
Accepts the following POST parameters: email
Returns the success/fail message.
"""
serializer_class = PasswordResetSerializer
permission_classes = (AllowAny,)
def post(self, request, *args, **kwargs):
# Create a serializer with request.data
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save() # <----- Code from above (TokenGenerator) will be called inside this .save() method
# Return the success message with OK HTTP status
return Response(
{"detail": _("Password reset e-mail has been sent.")},
status=status.HTTP_200_OK
)
Comme vous pouvez le voir, le Tokengenerator retournera un uidb36
Avec le jeton. Il suppose également un uidb36
Lorsque l'utilisateur confirmera la réinitialisation du mot de passe. Un jeton généré (par exemple le lien complet dans le courrier généré) ressemblerait à ceci:
http://localhost:8000/accounts/password/reset/key/16-52h-42b222e6dc30690b2e91/
Où 16
est l'ID utilisateur dans la base 36 (uidb36
), Je ne sais pas encore ce que 52h
Signifie, mais je suppose que la troisième partie du jeton est le jeton lui-même (42b222e6dc30690b2e91
)
Je suis coincé ici. Les API-Endpoints des Rest-Auth-Framework disent:
/ rest-auth/mot de passe/reset/confirm// (POST)
uid
token
new_password1
new_password2
Et quand j'envoie un objet par exemple:
{
uid: '16', // TODO maybe I have to convert it to base10...
token: '42b222e6dc30690b2e91',
new_password1: 'test123A$',
new_password2: 'test123A$'
}
via mon api à http://localhost:8000/api/v1/auth/reset-password/
avec l'objet ci-dessus dans le corps d'une demande de publication axios
-, ma CustomPasswordResetConfirmView
est déclenchée comme prévu, qui n'est aussi qu'une sous-classe de PasswordResetConfirmView
from rest_auth
, donc ce code est exécuté:
# project/.venv/Lib/site-packages/rest_auth/views.py
class PasswordResetConfirmView(GenericAPIView):
"""
Password reset e-mail link is confirmed, therefore
this resets the user's password.
Accepts the following POST parameters: token, uid,
new_password1, new_password2
Returns the success/fail message.
"""
serializer_class = PasswordResetConfirmSerializer
permission_classes = (AllowAny,)
@sensitive_post_parameters_m
def dispatch(self, *args, **kwargs):
return super(PasswordResetConfirmView, self).dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(
{"detail": _("Password has been reset with the new password.")}
)
La ligne serializer.is_valid(raise_exception=True)
appellera run_validation
De Serializer(BaseSerializer)
de rest_framework
. Cela utilisera en outre le PasswordResetConfirmSerializer
de rest_auth
:
# project/.venv/Lib/site-packages/rest_auth/serializers.py
class PasswordResetConfirmSerializer(serializers.Serializer):
"""
Serializer for requesting a password reset e-mail.
"""
new_password1 = serializers.CharField(max_length=128)
new_password2 = serializers.CharField(max_length=128)
uid = serializers.CharField()
token = serializers.CharField()
set_password_form_class = SetPasswordForm
def custom_validation(self, attrs):
pass
def validate(self, attrs):
self._errors = {}
# Decode the uidb64 to uid to get User object
try:
uid = force_text(uid_decoder(attrs['uid']))
self.user = UserModel._default_manager.get(pk=uid)
except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist):
raise ValidationError({'uid': ['Invalid value']})
self.custom_validation(attrs)
# Construct SetPasswordForm instance
self.set_password_form = self.set_password_form_class(
user=self.user, data=attrs
)
if not self.set_password_form.is_valid():
raise serializers.ValidationError(self.set_password_form.errors)
if not default_token_generator.check_token(self.user, attrs['token']):
raise ValidationError({'token': ['Invalid value']})
return attrs
Et comme vous pouvez enfin le voir, cette classe attend un uidb64 au lieu d'un uidb36 pour l'ID utilisateur, et je ne veux même pas savoir si le format de jeton correspond de toute façon à ce qui est attendu ici.
Je ne peux vraiment pas trouver de bonne documentation sur la façon de configurer correctement rest_auth
Pour le processus de réinitialisation complète du mot de passe: j'ai obtenu le courrier électronique, mais il me semble que rest_auth
Générerait un jeton/réinitialisation incorrect -lien pour ce qu'il attend réellement de l'utilisateur.
Je crois que le processus de confirmation de réinitialisation du mot de passe se termine par le bon code backend, tandis que la génération d'e-mails/de jetons est foirée.
Tout ce que je veux, c'est récupérer un uid et un jeton que je peux renvoyer à Django rest-auth afin de permettre aux utilisateurs de réinitialiser leurs mots de passe. Actuellement, il semble que ces uids et jetons sont créés par une bibliothèque et consommés par une autre bibliothèque qui attend et crée des formats de jetons et uids?
Merci d'avance!
settings.py
Voici mon settings.py
Complet:
# project/vuedj/settings.py
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_PATH = os.path.realpath(os.path.dirname(__file__))
SECRET_KEY = persisted_settings.SECRET_KEY
DEBUG = True
ALLOWED_HOSTS = ['127.0.0.1', 'localhost']
CORS_Origin_ALLOW_ALL = True
CORS_URLS_REGEX = r'^/api/.*$'
CORS_ALLOW_CREDENTIALS = True
# Application definition
INSTALLED_APPS = [
'Django.contrib.admin',
'Django.contrib.auth',
'Django.contrib.contenttypes',
'Django.contrib.sessions',
'Django.contrib.messages',
'Django.contrib.staticfiles',
'Django.contrib.sites',
'rest_framework',
'rest_framework.authtoken',
'corsheaders',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.github',
'rest_auth',
'rest_auth.registration',
'sceneries',
'accounts',
'api',
'app',
]
EMAIL_BACKEND = 'Django.core.mail.backends.filebased.EmailBackend'
EMAIL_FILE_PATH = 'app-messages'
SITE_ID = 1
AUTH_USER_MODEL = 'accounts.User'
ACCOUNT_USER_MODEL_USERNAME_FIELD = 'username'
ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = 'none'
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_USERNAME_REQUIRED = True
ACCOUNT_USER_EMAIL_FIELD = 'email'
ACCOUNT_LOGOUT_ON_GET = True
ACCOUNT_FORMS = {"login": "accounts.forms.UserLoginForm"}
LOGIN_REDIRECT_URL = 'home'
LOGIN_URL = 'api/v1/accounts/login/'
CSRF_COOKIE_NAME = "csrftoken"
REST_AUTH_SERIALIZERS = {
"USER_DETAILS_SERIALIZER": "accounts.serializers.CustomUserDetailsSerializer",
"LOGIN_SERIALIZER": "accounts.serializers.CustomUserLoginSerializer",
"PASSWORD_RESET_SERIALIZER": "accounts.serializers.CustomPasswordResetSerializer"
}
REST_AUTH_REGISTER_SERIALIZERS = {
"REGISTER_SERIALIZER": "accounts.serializers.CustomRegisterSerializer",
}
# Following is added to enable registration with email instead of username
AUTHENTICATION_BACKENDS = (
# Needed to login by username in Django admin, regardless of `allauth`
"Django.contrib.auth.backends.ModelBackend",
# `allauth` specific authentication methods, such as login by e-mail
"allauth.account.auth_backends.AuthenticationBackend",
)
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'Django.middleware.security.SecurityMiddleware',
'Django.contrib.sessions.middleware.SessionMiddleware',
'Django.middleware.common.CommonMiddleware',
'Django.middleware.csrf.CsrfViewMiddleware',
'Django.contrib.auth.middleware.AuthenticationMiddleware',
'Django.contrib.messages.middleware.MessageMiddleware',
'Django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'vuedj.urls'
TEMPLATES = [
{
'BACKEND': 'Django.template.backends.Django.DjangoTemplates',
'DIRS': [
'templates/',
'templates/emails/'
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'Django.template.context_processors.debug',
'Django.template.context_processors.request',
'Django.contrib.auth.context_processors.auth',
'Django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'vuedj.wsgi.application'
try:
DATABASES = persisted_settings.DATABASES
except AttributeError:
DATABASES = {
'default': {
'ENGINE': 'Django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'Django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'Django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'Django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'Django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
STATIC_ROOT = os.path.join(BASE_DIR, '../staticfiles/static')
MEDIA_ROOT = os.path.join(BASE_DIR, '../staticfiles/mediafiles')
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
TEST_RUNNER = 'Django_nose.NoseTestSuiteRunner'
NOSE_ARGS = [
'--with-coverage',
'--cover-package=app', # For multiple apps use '--cover-package=foo, bar'
]
Nous avons la même configuration et je peux vous dire que cela fonctionne mais je ne peux pas vous aider avec la base 36 sauf que même la documentation Django dit que c'est la base 64!
Cependant, vous avez écrit que cette partie théorique n'est pas si importante pour vous et trouvons le point qui vous manque. La configuration est un peu déroutante car vous n'avez pas besoin de tout. Je ne comprends pas exactement où tu es coincé. Par conséquent, je veux vous dire comment je l'ai fait:
J'ai défini l'URL de réinitialisation du mot de passe juste pour Django/allauth pour le trouver lors de la création du lien dans l'e-mail:
from Django.views.generic import TemplateView
PASSWORD_RESET = (
r'^auth/password-reset-confirmation/'
r'(?P<uidb64>[0-9A-Za-z_\-]+)/'
r'(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})$'
)
urlpatterns += [
re_path(
PASSWORD_RESET,
TemplateView.as_view(),
name='password_reset_confirm',
),
]
Vous n'êtes pas obligé de le faire (parce que vous include('allauth.urls')
, vous avez en fait vous n'avez pas besoin de ces URL ) mais je tiens à préciser que cette URL ne pointe pas vers le backend ! Cela dit, laissez votre frontend servir cette URL avec un formulaire pour entrer un nouveau mot de passe, puis utilisez axios ou quelque chose pour POST
uid
, token
, new_password1
et new_password2
à votre point de terminaison.
Dans votre cas, le point final est
path(
'reset-password-confirm/',
acc_views.CustomPasswordResetConfirmView.as_view(),
name='reset-password-confirm'
),
Est-ce que cela vous aide? Sinon, faites-le moi savoir.