介绍
在大多数情况下,应用程序的每个用户都希望更新其详细信息,例如个人资料图片,名称,密码,如果他们注意到违规或忘记时,以及我们的应用程序允许他们提供的其他数据。在本文中,我们将通过允许应用程序的用户更新其个人资料名称(首先和最后),缩略图,github链接,出生日期和密码来做到这一点。我们将学习如何利用较少记录的(或更恰当,无证件)的MultiPartParser从PATCH
请求手动处理FormData
。
假设和建议
假定您熟悉Django。我还建议您仔细研究我们如何创建previous series的前端,因为我们只会在那里改变很少的事情,并且不会深入研究我们如何将所有内容拼凑在一起。我们将在这里构建的API反映了我们在该系列中构建的内容。
源代码
该系列的源代码通过:
在GitHub上托管Sirneij / django-auth-backend
带有Sveltekit Frontend的基于Django会话的身份验证系统
django-auth-backend
基于Sveltekit Frontend和GitHub Actions的基于DJANGO会话的身份验证系统。
此应用程序使用最小依赖项(纯django-无REST API框架)来构建安全,性能和可靠(具有100%自动化测试覆盖范围,使用Python最佳统一代码标准执行静态分析) - 基于基于Sveltekit的前端应用程序消耗的基于身份验证的REST API。
用户的配置文件图像直接上传到AWS S3(在测试中,我们放弃了S3并使用Django的InMemoryStorage进行更快的测试)。
还合并了一个自定义密码重置过程,芹菜任务进行了电子邮件发送。
在本地运行
-
要运行应用程序,请克隆:
git克隆https://github.com/sirneij/django-auth-backend.git
您可以,如果需要,请抓住它的frontend counterpart。
-
将目录更改为文件夹,并使用Python 3.9、3.10或3.11(根据三个版本进行测试)创建虚拟环境。然后激活它:
执行
步骤1:用户配置文件更新
我们将从更新应用程序用户数据的某些非关键部分开始。在users/views/
文件夹中创建profile_update.py
,并用以下方式填充它:
# src/users/views/profile_update.py
import json
from io import BytesIO
from typing import Any
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, JsonResponse
from django.http.multipartparser import MultiPartParser
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from users.models import UserProfile
@method_decorator(csrf_exempt, name='dispatch')
class UserUpdateView(View, LoginRequiredMixin):
def patch(self, request: HttpRequest, **kwargs: dict[str, Any]) -> JsonResponse:
"""Handle user updates."""
if not request.user.is_authenticated:
return JsonResponse(
{'error': 'You are not logged in. Kindly ensure you are logged in and try again'}, status=401
)
data, files = MultiPartParser(
request.META, BytesIO(request.body), request.upload_handlers, request.encoding
).parse()
first_name = data.get('first_name')
last_name = data.get('last_name')
thumbnail = files.get('thumbnail')
phone_number = data.get('phone_number')
birth_date = data.get('birth_date')
github_link = data.get('github_link')
user_details = UserProfile.objects.filter(user=request.user).select_related('user').get()
if first_name:
user_details.user.first_name = first_name
if last_name:
user_details.user.last_name = last_name
if thumbnail:
user_details.user.thumbnail = thumbnail
user_details.user.save(update_fields=['first_name', 'last_name', 'thumbnail'])
if phone_number:
user_details.phone_number = phone_number
if birth_date:
user_details.birth_date = birth_date
if github_link:
user_details.github_link = github_link
user_details.save(update_fields=['phone_number', 'birth_date', 'github_link'])
res_data = {
'id': str(user_details.user.pk),
'email': user_details.user.email,
'first_name': user_details.user.first_name,
'last_name': user_details.user.last_name,
'is_staff': user_details.user.is_staff,
'is_active': user_details.user.is_active,
'date_joined': str(user_details.user.date_joined),
'thumbnail': user_details.user.thumbnail.url if user_details.user.thumbnail else None,
'profile': {
'id': str(user_details.id),
'user_id': str(user_details.user.pk),
'phone_number': user_details.phone_number,
'github_link': user_details.github_link,
'birth_date': str(user_details.birth_date) if user_details.birth_date else None,
},
}
response_data = json.loads(json.dumps(res_data))
return JsonResponse(response_data, status=200)
像往常一样,我们首先要确保我们的API消费者在访问此页面之前不需要提供CSRF令牌。我应该告诉您,我们实际上可以使用其form actions
的力量在Sveltekit中提供csrftoken
。但是,我们只想遵守previous series所采用的设计原则。接下来,我们在通用View
类中定义了PATCH
方法。 PATCH
是首选的,因为我们希望严格遵循HTTP verbs or methods的使用。 PATCH
用于PARTIAL资源修改。身份验证的用户只能访问此端点,我们在方法的顶部执行了这一点。现在,通过使用PATCH
,我们无法分别使用非常方便的request.POST.get('key')
和request.FILES.get('key')
Django提供的POST
数据和文件。如果您在这里使用其中的任何一个,它们将是空的!我们必须手动处理传入的PATCH
FormData
,为此,我们使用了令人敬畏但无证件的MultiPartParser,该koude16在koude16中可用。该类期望在初始化的Queltization的META
,body
(以原始文件般的性质),upload_handlers
(通常默认为request.upload_handlers
)和一个encoding
(具有settings.DEFAULT_CHARSET
的默认值)。 META
的内容类型必须从multipart/
开始,否则会出现错误。在提供的FormData
成功的parse
上,它返回数据和文件元组。然后,我们检索数据和文件,并检查是否提供了。相应地进行了更新。
您可以将此视图添加到我们的URL列表中。
步骤2:用户密码更改
接下来是更新用户的密码,以防其丢失,或者发生了一些不好的事情。我们将要求用户进行三步过程 - 请求使用已注册和经过验证的电子邮件地址进行更改,单击发送到电子邮件地址的链接,然后输入新密码。
到目前为止,我们非常熟悉允许用户请求某些内容,并向他们发送带有适当链接的电子邮件。这没有什么不同。在整个过程中,为了简洁的缘故,我们将在views
软件包中创建一个名为password
的子弹。在子软件包中,我们将创建request_change.py
,confirm_change_request.py
和change_password.py
。 src/users/views/password/request_change.py
的内容是:
# src/users/views/password/request_change.py
import json
from datetime import timedelta
from typing import Any
from asgiref.sync import sync_to_async
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.sites.shortcuts import get_current_site
from django.http import HttpRequest, JsonResponse
from django.urls.base import reverse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from users.tasks import send_email_message
from users.token import account_activation_token
from users.utils import validate_email
@method_decorator(csrf_exempt, name='dispatch')
class RequestPasswordChangeView(View):
async def post(self, request: HttpRequest, **kwargs: dict[str, Any]) -> JsonResponse:
"""Request user password change."""
data = json.loads(request.body.decode("utf-8"))
email = data.get('email')
if email is None:
return JsonResponse({'error': 'Email field is empty'}, status=400)
is_valid, error_text = validate_email(email)
if not is_valid:
return JsonResponse({'error': error_text}, status=400)
try:
user = await get_user_model().objects.filter(email=email, is_active=True).aget()
except get_user_model().DoesNotExist:
return JsonResponse(
{
'error': 'An active user with this e-mail address does not exist. '
'If you registered with this email, ensure you have activated your account. '
'You can check by logging in. If you have not activated it, '
f'visit {settings.FRONTEND_URL}/auth/regenerate-token to '
'regenerate the token that will allow you activate your account.'
},
status=404,
)
token = await sync_to_async(account_activation_token.make_token)(user)
uid = urlsafe_base64_encode(force_bytes(user.pk))
confirmation_link = (
f"{request.scheme}://{get_current_site(request)}"
f"{reverse('users:confirm_password_change_request', kwargs={'uidb64': uid, 'token': token})}",
)
subject = 'Password reset instructions'
ctx = {
'title': "(Django) RustAuth - Password Reset Instructions",
'domain': settings.FRONTEND_URL,
'confirmation_link': confirmation_link,
'expiration_time': (timezone.localtime() + timedelta(seconds=settings.PASSWORD_RESET_TIMEOUT)).minute,
'exact_time': (timezone.localtime() + timedelta(seconds=settings.PASSWORD_RESET_TIMEOUT)).strftime(
'%A %B %d, %Y at %r'
),
}
send_email_message.delay(
subject=subject,
template_name='password_reset_email.html',
user_id=user.id,
ctx=ctx,
)
return JsonResponse(
{
'message': 'Password reset instructions have been sent to your email address. '
'Kindly take action before its expiration'
},
status=200,
)
基本上,这几乎就像令牌再生的逻辑外,除了这里,我们正在使用提供的电子邮件地址检查活跃的用户。我们还更改了电子邮件主题和HTML模板名称。模板具有此内容:
<!-- src/templates/password_reset_email.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ title }}</title>
</head>
<body>
<table
style="
max-width: 555px;
width: 100%;
font-family: 'Open Sans', Segoe, 'Segoe UI', 'DejaVu Sans',
'Trebuchet MS', Verdana, sans-serif;
background: #fff;
font-size: 13px;
color: #323232;
"
cellspacing="0"
cellpadding="0"
border="0"
bgcolor="#ffffff"
align="center"
>
<tbody>
<tr>
<td align="left">
<h1 style="text-align: center">
<span style="font-size: 15px">
<strong>{{ title }}</strong>
</span>
</h1>
<p>
Your request to reset your password was submitted. If you did not
make this request, simply ignore this email. If you did make this
request just click the button below:
</p>
<table
style="
max-width: 555px;
width: 100%;
font-family: 'Open Sans', arial, sans-serif;
font-size: 13px;
color: #323232;
"
cellspacing="0"
cellpadding="0"
border="0"
bgcolor="#ffffff"
align="center"
>
<tbody>
<tr>
<td height="10"> </td>
</tr>
<tr>
<td style="text-align: center">
<a
href="{{ confirmation_link }}"
style="
color: #fff;
background-color: hsla(199, 69%, 84%, 1);
width: 320px;
font-size: 16px;
border-radius: 3px;
line-height: 44px;
height: 44px;
font-family: 'Open Sans', Arial, helvetica, sans-serif;
text-align: center;
text-decoration: none;
display: inline-block;
"
target="_blank"
data-saferedirecturl="https://www.google.com/url?q={{ confirmation_link }}"
>
<span style="color: #000000">
<strong>Change password</strong>
</span>
</a>
</td>
</tr>
</tbody>
</table>
<table
style="
max-width: 555px;
width: 100%;
font-family: 'Open Sans', arial, sans-serif;
font-size: 13px;
color: #323232;
"
cellspacing="0"
cellpadding="0"
border="0"
bgcolor="#ffffff"
align="center"
>
<tbody>
<tr>
<td height="10"> </td>
</tr>
<tr>
<td align="left">
<p align="center"> </p>
If the above button doesn't work, try copying and pasting
the link below into your browser. If you continue to
experience problems, please contact us.
<br />
{{ confirmation_link }}
<br />
</td>
</tr>
<tr>
<td>
<p align="center"> </p>
<br />
<p style="padding-bottom: 15px; margin: 0">
Kindly note that this link will expire in
<strong>{{expiration_time}} minutes</strong>. The exact
expiration date and time is:
<strong>{{ exact_time }}</strong>.
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
接下来是src/users/views/password/confirm_change.py
:
# src/users/views/password/confirm_change.py
from asgiref.sync import sync_to_async
from django.conf import settings
from django.contrib.auth import get_user_model
from django.http import HttpRequest, HttpResponseRedirect
from django.utils.encoding import force_bytes, force_str
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.views import View
from users.token import account_activation_token
class ConfirmPasswordChangeRequestView(View):
async def get(self, request: HttpRequest, uidb64: str, token: str) -> HttpResponseRedirect:
"""Confirm password change requests."""
try:
uid = force_str(urlsafe_base64_decode(uidb64))
user = await get_user_model().objects.aget(pk=uid, is_active=True)
except (TypeError, ValueError, OverflowError, get_user_model().DoesNotExist):
user = None
if user is not None and account_activation_token.check_token(user, token):
# Generate a new token
token = await sync_to_async(account_activation_token.make_token)(user)
uid = urlsafe_base64_encode(force_bytes(user.pk))
combined = f'{uid}:{token}'
return HttpResponseRedirect(f'{settings.FRONTEND_URL}/auth/password/change-password?token={combined}')
return HttpResponseRedirect(
f'{settings.FRONTEND_URL}/auth/regenerate-token?reason=It appears that '
'your confirmation token has expired or previously used. Kindly generate a new token',
)
也很熟悉。豁免是我们还在这里检查活跃的用户。然后,如果成功检索了用户,我们创建了一个新的令牌并编码了用户的ID。然后,我们使用:
组合了两者。这将有助于我们检测到以后在密码更改视图中要求密码更改的人员。然后,我们将用户重定向到前端末端页面,以将密码用“组合”代币作为查询参数更改。前端将此令牌发送回后端,并与用户的新密码一起发送。让我们立即编写更改密码的逻辑:
# src/users/views/password/change_password.py
import json
from typing import Any
from django.contrib.auth import get_user_model
from django.http import HttpRequest, JsonResponse
from django.utils.decorators import method_decorator
from django.utils.encoding import force_str
from django.utils.http import urlsafe_base64_decode
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from users.token import account_activation_token
@method_decorator(csrf_exempt, name='dispatch')
class ChangePasswordView(View):
async def post(self, request: HttpRequest, **kwargs: dict[str, Any]) -> JsonResponse:
"""Change user password."""
data = json.loads(request.body.decode("utf-8"))
password = data.get('password')
combined = data.get('token')
try:
uidb64, token = combined.split(':')
uid = force_str(urlsafe_base64_decode(uidb64))
user = await get_user_model().objects.aget(pk=uid, is_active=True)
except (TypeError, ValueError, OverflowError, get_user_model().DoesNotExist):
user = None
if user is not None and account_activation_token.check_token(user, token):
user.set_password(password)
await user.asave(update_fields=['password'])
return JsonResponse(
{
'message': 'Your password has been changed successfully. Kindly login with the new password',
}
)
return JsonResponse(
{
'error': 'It appears that your password request token has expired or previously used',
}
)
我们只是检索了我们先前发送的新密码和“组合”令牌。然后,我们试图破坏“组合”令牌以获取编码的用户ID和令牌本身。从那里,从数据库中获取了所涉及的用户,检查了令牌的正确性,并在一切正常的情况下保存了用户的密码。那很简单。
我们现在可以将这些视图添加到我们的URL中以测试它们。
# src/users/urls.py
...
from users.views.password import change_password, confirm_change_request, request_change
...
urlpatterns = [
...
# Password change
path(
'password-change/request-password-change/',
request_change.RequestPasswordChangeView.as_view(),
name='request_password_change',
),
path(
'password-change/confirm/change-password/<uidb64>/<token>/',
confirm_change_request.ConfirmPasswordChangeRequestView.as_view(),
name='confirm_password_change_request',
),
path(
'password-change/change-user-password/',
change_password.ChangePasswordView.as_view(),
name='change_password',
),
...
]
就是本文。我希望你喜欢它。接下来是具有GITHUB动作的自动化测试和静态分析方面。我们还将尝试将我们的应用程序自由部署在Vercel上。然后见。
其他
喜欢这篇文章?考虑contacting me for a job, something worthwhile or buying a coffee ☕。您也可以在LinkedIn和Twitter上与我联系/关注我。如果您帮助分享本文以获得更广泛的报道,那还不错。我会很感激...