feat: implement user authentication features (registration, activation, and password reset)
All checks were successful
Build and Push Docker Image / build (push) Successful in 46s
All checks were successful
Build and Push Docker Image / build (push) Successful in 46s
- Added email templates for activation and password reset (`activation_email.html`, `password_reset_email.html`, etc.) - Created forms for registration (`RegisterForm`) and login (`LoginForm`) with custom validation - Developed new pages for account activation (`activation_success.html`, `activation_invalid.html`), password reset workflow, and profile - Configured email backend and environment file handling in settings - Updated header template to conditionally display profile or login links based on authentication status - Enhanced CSS styles for authentication-related pages and forms
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -44,3 +44,5 @@ build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
|
||||
.env*
|
||||
@@ -10,11 +10,33 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def load_env_file(path: Path):
|
||||
if not path.exists():
|
||||
return
|
||||
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
|
||||
if key and key not in os.environ:
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
ENV_FILE = Path(os.getenv("DJANGO_ENV_FILE", BASE_DIR / ".env.test"))
|
||||
load_env_file(ENV_FILE)
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
|
||||
@@ -109,7 +131,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'ru-ru'
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = 'Europe/Warsaw'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@@ -134,7 +156,11 @@ STORAGES = {
|
||||
'BACKEND': 'django.core.files.storage.FileSystemStorage',
|
||||
},
|
||||
'staticfiles': {
|
||||
'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage',
|
||||
'BACKEND': (
|
||||
'django.contrib.staticfiles.storage.StaticFilesStorage'
|
||||
if DEBUG else
|
||||
'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -142,3 +168,18 @@ STORAGES = {
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
SITE_NAME = 'SantehRay'
|
||||
|
||||
LOGIN_URL = '/users/login/'
|
||||
LOGIN_REDIRECT_URL = '/users/profile/'
|
||||
LOGOUT_REDIRECT_URL = '/users/login/'
|
||||
|
||||
EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend')
|
||||
EMAIL_HOST = os.getenv('EMAIL_HOST', 'localhost')
|
||||
EMAIL_PORT = int(os.getenv('EMAIL_PORT', '25'))
|
||||
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '')
|
||||
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '')
|
||||
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', 'false').lower() in {'1', 'true', 'yes', 'on'}
|
||||
EMAIL_USE_SSL = os.getenv('EMAIL_USE_SSL', 'false').lower() in {'1', 'true', 'yes', 'on'}
|
||||
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'no-reply@santehray.local')
|
||||
|
||||
@@ -1,36 +1,277 @@
|
||||
.login-form {
|
||||
padding: 25px;
|
||||
border: 1px solid rgba(94, 80, 125, 0.78);
|
||||
|
||||
& > h2 {
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 44px;
|
||||
|
||||
color: #36213e;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
.auth-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
.auth-section {
|
||||
flex: 1;
|
||||
padding: 60px 0 100px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(184, 243, 255, 0.45) 0, rgba(184, 243, 255, 0) 26%),
|
||||
linear-gradient(180deg, rgba(99, 118, 141, 0.06) 0%, rgba(99, 118, 141, 0.12) 100%);
|
||||
}
|
||||
|
||||
.auth-layout {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
gap: 56px;
|
||||
}
|
||||
|
||||
.auth-layout--reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.auth-intro {
|
||||
flex: 1 1 480px;
|
||||
min-height: 540px;
|
||||
padding: 56px 48px;
|
||||
background: #63768d;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.auth-intro--login {
|
||||
background-image:
|
||||
linear-gradient(rgba(54, 33, 62, 0.42), rgba(54, 33, 62, 0.42)),
|
||||
url("../img/kitchen-banner.jpg");
|
||||
}
|
||||
|
||||
.auth-intro--register {
|
||||
background-image:
|
||||
linear-gradient(rgba(54, 33, 62, 0.48), rgba(54, 33, 62, 0.48)),
|
||||
url("../img/about-image.jpg");
|
||||
}
|
||||
|
||||
.auth-intro--reset {
|
||||
background-image:
|
||||
linear-gradient(rgba(54, 33, 62, 0.44), rgba(54, 33, 62, 0.44)),
|
||||
url("../img/stats-image.jpg");
|
||||
}
|
||||
|
||||
.auth-kicker {
|
||||
margin-bottom: 18px;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
color: #b8f3ff;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
max-width: 430px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 50px;
|
||||
line-height: 1.08;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.auth-description {
|
||||
max-width: 450px;
|
||||
font-size: 18px;
|
||||
line-height: 1.5;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.auth-side-link {
|
||||
align-self: flex-start;
|
||||
margin-top: 38px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #ffffff;
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
color: #ffffff;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.auth-side-link:hover {
|
||||
background: #ffffff;
|
||||
color: #63768d;
|
||||
}
|
||||
|
||||
.auth-panel {
|
||||
flex: 0 1 520px;
|
||||
padding: 48px 40px;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(85, 73, 113, 0.22);
|
||||
box-shadow: 0 20px 40px rgba(54, 33, 62, 0.08);
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.auth-copy-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.auth-panel-title {
|
||||
font-size: 38px;
|
||||
line-height: 1.1;
|
||||
color: #36213e;
|
||||
}
|
||||
|
||||
.auth-panel-text {
|
||||
font-size: 18px;
|
||||
line-height: 1.5;
|
||||
color: #36213e;
|
||||
}
|
||||
|
||||
.auth-alert {
|
||||
margin-bottom: 10px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #9f4b4b;
|
||||
background: #f4dfdf;
|
||||
color: #6f2323;
|
||||
}
|
||||
|
||||
.auth-field {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.auth-field:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.auth-field label {
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
color: #554971;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
.auth-field input {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
padding: 12px 14px;
|
||||
background: #d9d9d9;
|
||||
|
||||
border: 1px solid #554971;
|
||||
font-family: 'Noah', sans-serif;
|
||||
font-size: 18px;
|
||||
color: #36213e;
|
||||
}
|
||||
|
||||
padding: 5px;
|
||||
}
|
||||
.auth-field input::placeholder {
|
||||
color: rgba(54, 33, 62, 0.58);
|
||||
}
|
||||
|
||||
.auth-field input:focus {
|
||||
outline: none;
|
||||
border-color: #36213e;
|
||||
background: #eeeeee;
|
||||
}
|
||||
|
||||
.auth-field-error {
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
color: #8c2f2f;
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.auth-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.auth-submit {
|
||||
padding: 18px 34px;
|
||||
border: 0;
|
||||
background: #63768d;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.auth-submit:hover {
|
||||
background: #554971;
|
||||
}
|
||||
|
||||
.auth-submit-link {
|
||||
display: inline-block;
|
||||
padding: 18px 34px;
|
||||
background: #63768d;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.auth-secondary-link {
|
||||
display: inline-block;
|
||||
padding: 10px 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
color: #63768d;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.auth-switch {
|
||||
margin-top: 24px;
|
||||
font-size: 18px;
|
||||
line-height: 1.4;
|
||||
color: #36213e;
|
||||
}
|
||||
|
||||
.auth-switch a {
|
||||
color: #63768d;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.auth-section {
|
||||
padding: 32px 0 72px;
|
||||
}
|
||||
|
||||
.auth-layout,
|
||||
.auth-layout--reverse {
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.auth-intro,
|
||||
.auth-panel {
|
||||
min-height: auto;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.auth-description {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.auth-side-link,
|
||||
.auth-submit,
|
||||
.auth-submit-link {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.auth-secondary-link,
|
||||
.auth-panel-text,
|
||||
.auth-field label,
|
||||
.auth-field input,
|
||||
.auth-switch {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.auth-panel-title {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<path d="M0 2.5A.5.5 0 0 1 .5 2H2a.5.5 0 0 1 .485.379L2.89 4H14.5a.5.5 0 0 1 .485.621l-1.5 6A.5.5 0 0 1 13 11H4a.5.5 0 0 1-.485-.379L1.61 3H.5a.5.5 0 0 1-.5-.5M3.14 5l1.25 5h8.22l1.25-5zM5 13a1 1 0 1 0 0 2 1 1 0 0 0 0-2m-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0m9-1a1 1 0 1 0 0 2 1 1 0 0 0 0-2m-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/users/login/">
|
||||
<a href="{% if request.user.is_authenticated %}{% url 'users:profile' %}{% else %}{% url 'users:login' %}{% endif %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-person" viewBox="0 0 16 16">
|
||||
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z"></path>
|
||||
@@ -71,4 +71,4 @@
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
36
templates/users/activation_invalid.html
Normal file
36
templates/users/activation_invalid.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends '_layouts/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Ссылка недействительна{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/login.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-page">
|
||||
{% include "_fragments/header.html" %}
|
||||
<main class="auth-section">
|
||||
<div class="container auth-layout">
|
||||
<section class="auth-intro auth-intro--reset">
|
||||
<p class="auth-kicker">Ошибка подтверждения</p>
|
||||
<h1 class="auth-title">Ссылка недействительна</h1>
|
||||
<p class="auth-description">
|
||||
Ссылка подтверждения истекла или уже была использована. Попробуйте зарегистрироваться снова.
|
||||
</p>
|
||||
</section>
|
||||
<section class="auth-panel">
|
||||
<div class="auth-copy-block">
|
||||
<h2 class="auth-panel-title">Что можно сделать</h2>
|
||||
<p class="auth-panel-text">Если аккаунт уже активирован, просто войдите. Если нет, создайте новый аккаунт и подтвердите почту повторно.</p>
|
||||
<div class="auth-buttons">
|
||||
<a class="auth-submit-link" href="{% url 'users:login' %}">Войти</a>
|
||||
<a class="auth-secondary-link" href="{% url 'users:register' %}">Регистрация</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
{% include "_fragments/footer.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
36
templates/users/activation_success.html
Normal file
36
templates/users/activation_success.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends '_layouts/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Почта подтверждена{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/login.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-page">
|
||||
{% include "_fragments/header.html" %}
|
||||
<main class="auth-section">
|
||||
<div class="container auth-layout">
|
||||
<section class="auth-intro auth-intro--login">
|
||||
<p class="auth-kicker">Аккаунт активирован</p>
|
||||
<h1 class="auth-title">Почта подтверждена</h1>
|
||||
<p class="auth-description">
|
||||
Аккаунт успешно активирован. Теперь вы можете пользоваться личным кабинетом и оформлять заказы.
|
||||
</p>
|
||||
</section>
|
||||
<section class="auth-panel">
|
||||
<div class="auth-copy-block">
|
||||
<h2 class="auth-panel-title">Готово</h2>
|
||||
<p class="auth-panel-text">Вы уже авторизованы в системе. Можете перейти в личный кабинет или вернуться к покупкам.</p>
|
||||
<div class="auth-buttons">
|
||||
<a class="auth-submit-link" href="{% url 'users:profile' %}">Личный кабинет</a>
|
||||
<a class="auth-secondary-link" href="/catalog/">В каталог</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
{% include "_fragments/footer.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
20
templates/users/emails/activation_email.html
Normal file
20
templates/users/emails/activation_email.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<html lang="ru">
|
||||
<body style="margin:0;padding:24px;font-family:Arial,sans-serif;background:#f5f7fa;color:#36213e;">
|
||||
<div style="max-width:640px;margin:0 auto;background:#ffffff;border:1px solid #d6dde6;padding:32px;">
|
||||
<h1 style="margin:0 0 16px;font-size:28px;color:#36213e;">Подтверждение регистрации</h1>
|
||||
<p style="margin:0 0 16px;font-size:16px;line-height:1.5;">
|
||||
Здравствуйте{% if user.get_full_name %}, {{ user.get_full_name }}{% endif %}!
|
||||
</p>
|
||||
<p style="margin:0 0 24px;font-size:16px;line-height:1.5;">
|
||||
Спасибо за регистрацию в {{ site_name }}. Подтвердите почту, чтобы активировать аккаунт.
|
||||
</p>
|
||||
<p style="margin:0 0 24px;">
|
||||
<a href="{{ activation_url }}" style="display:inline-block;padding:14px 22px;background:#63768d;color:#ffffff;text-decoration:none;">Подтвердить почту</a>
|
||||
</p>
|
||||
<p style="margin:0;font-size:14px;line-height:1.5;color:#554971;">
|
||||
Если кнопка не работает, скопируйте ссылку в браузер:<br>
|
||||
<a href="{{ activation_url }}">{{ activation_url }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
8
templates/users/emails/activation_email.txt
Normal file
8
templates/users/emails/activation_email.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Здравствуйте{% if user.get_full_name %}, {{ user.get_full_name }}{% endif %}!
|
||||
|
||||
Спасибо за регистрацию в {{ site_name }}.
|
||||
|
||||
Чтобы активировать аккаунт, перейдите по ссылке:
|
||||
{{ activation_url }}
|
||||
|
||||
Если вы не создавали аккаунт, просто проигнорируйте это письмо.
|
||||
1
templates/users/emails/activation_subject.txt
Normal file
1
templates/users/emails/activation_subject.txt
Normal file
@@ -0,0 +1 @@
|
||||
Подтверждение регистрации в {{ site_name }}
|
||||
19
templates/users/emails/password_reset_email.html
Normal file
19
templates/users/emails/password_reset_email.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<html lang="ru">
|
||||
<body style="margin:0;padding:24px;font-family:Arial,sans-serif;background:#f5f7fa;color:#36213e;">
|
||||
<div style="max-width:640px;margin:0 auto;background:#ffffff;border:1px solid #d6dde6;padding:32px;">
|
||||
<h1 style="margin:0 0 16px;font-size:28px;color:#36213e;">Восстановление пароля</h1>
|
||||
<p style="margin:0 0 24px;font-size:16px;line-height:1.5;">
|
||||
Вы запросили смену пароля для аккаунта в {{ site_name|default:"SantehRay" }}.
|
||||
</p>
|
||||
<p style="margin:0 0 24px;">
|
||||
<a href="{{ protocol }}://{{ domain }}{% url 'users:password_reset_confirm' uidb64=uid token=token %}" style="display:inline-block;padding:14px 22px;background:#63768d;color:#ffffff;text-decoration:none;">Сменить пароль</a>
|
||||
</p>
|
||||
<p style="margin:0;font-size:14px;line-height:1.5;color:#554971;">
|
||||
Если кнопка не работает, используйте ссылку:<br>
|
||||
<a href="{{ protocol }}://{{ domain }}{% url 'users:password_reset_confirm' uidb64=uid token=token %}">
|
||||
{{ protocol }}://{{ domain }}{% url 'users:password_reset_confirm' uidb64=uid token=token %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
8
templates/users/emails/password_reset_email.txt
Normal file
8
templates/users/emails/password_reset_email.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Здравствуйте!
|
||||
|
||||
Вы запросили восстановление пароля в {{ site_name|default:"SantehRay" }}.
|
||||
|
||||
Перейдите по ссылке, чтобы задать новый пароль:
|
||||
{{ protocol }}://{{ domain }}{% url 'users:password_reset_confirm' uidb64=uid token=token %}
|
||||
|
||||
Если вы не отправляли этот запрос, просто проигнорируйте это письмо.
|
||||
1
templates/users/emails/password_reset_subject.txt
Normal file
1
templates/users/emails/password_reset_subject.txt
Normal file
@@ -0,0 +1 @@
|
||||
Восстановление пароля в {{ site_name|default:"SantehRay" }}
|
||||
@@ -8,32 +8,76 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="full-page">
|
||||
<div class="auth-page">
|
||||
{% include "_fragments/header.html" %}
|
||||
|
||||
<div class="center-wrapper">
|
||||
<form class="login-form" action="">
|
||||
<h2>ВХОД</h2>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="login">Логин</label>
|
||||
<input class="form-input" type="text" id="login">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Пароль</label>
|
||||
<input class="form-input" type="password" id="password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<input class="form-input" type="checkbox" id="persist">
|
||||
<label class="form-label" for="persist">Оставаться в системе</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit">Войти</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<main class="auth-section">
|
||||
<div class="container auth-layout">
|
||||
<section class="auth-intro auth-intro--login">
|
||||
<p class="auth-kicker">Личный кабинет</p>
|
||||
<h1 id="login-title" class="auth-title">Вход в аккаунт</h1>
|
||||
<p class="auth-description">
|
||||
Войдите, чтобы управлять заказами, сохранять избранные товары и быстрее оформлять покупки.
|
||||
</p>
|
||||
<a class="auth-side-link" href="/catalog/">Перейти в каталог</a>
|
||||
</section>
|
||||
|
||||
<section class="auth-panel" aria-labelledby="login-title">
|
||||
<form class="auth-form" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="auth-alert auth-alert--error">
|
||||
{% for error in form.non_field_errors %}
|
||||
<p>{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="{{ form.email.id_for_label }}">Электронная почта</label>
|
||||
<input
|
||||
id="{{ form.email.id_for_label }}"
|
||||
name="{{ form.email.html_name }}"
|
||||
type="email"
|
||||
value="{{ form.email.value|default:'' }}"
|
||||
placeholder="example@mail.ru"
|
||||
autocomplete="email"
|
||||
>
|
||||
{% for error in form.email.errors %}
|
||||
<p class="auth-field-error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="{{ form.password.id_for_label }}">Пароль</label>
|
||||
<input
|
||||
id="{{ form.password.id_for_label }}"
|
||||
name="{{ form.password.html_name }}"
|
||||
type="password"
|
||||
placeholder="Введите пароль"
|
||||
autocomplete="current-password"
|
||||
>
|
||||
{% for error in form.password.errors %}
|
||||
<p class="auth-field-error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
<a class="auth-secondary-link" href="{% url 'users:password_reset' %}">Сбросить пароль</a>
|
||||
</div>
|
||||
|
||||
<div class="auth-actions">
|
||||
<div class="auth-buttons">
|
||||
<button class="auth-submit" type="submit">Войти</button>
|
||||
|
||||
</div>
|
||||
<p class="auth-switch">
|
||||
Нет аккаунта?
|
||||
<a href="{% url 'users:register' %}">Зарегистрироваться</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{% include "_fragments/footer.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
65
templates/users/password_reset.html
Normal file
65
templates/users/password_reset.html
Normal file
@@ -0,0 +1,65 @@
|
||||
{% extends '_layouts/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Сброс пароля{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/login.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-page">
|
||||
{% include "_fragments/header.html" %}
|
||||
|
||||
<main class="auth-section">
|
||||
<div class="container auth-layout">
|
||||
<section class="auth-intro auth-intro--reset">
|
||||
<p class="auth-kicker">Восстановление доступа</p>
|
||||
<h1 id="password-reset-title" class="auth-title">Сброс пароля</h1>
|
||||
<p class="auth-description">
|
||||
Укажите электронную почту, и мы отправим инструкции для восстановления доступа к вашему аккаунту.
|
||||
</p>
|
||||
<a class="auth-side-link" href="/users/login/">Вернуться ко входу</a>
|
||||
</section>
|
||||
|
||||
<section class="auth-panel" aria-labelledby="password-reset-title">
|
||||
<form class="auth-form" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="auth-alert auth-alert--error">
|
||||
{% for error in form.non_field_errors %}
|
||||
<p>{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="{{ form.email.id_for_label }}">Электронная почта</label>
|
||||
<input
|
||||
id="{{ form.email.id_for_label }}"
|
||||
name="{{ form.email.html_name }}"
|
||||
type="email"
|
||||
value="{{ form.email.value|default:'' }}"
|
||||
placeholder="example@mail.ru"
|
||||
autocomplete="email"
|
||||
>
|
||||
{% for error in form.email.errors %}
|
||||
<p class="auth-field-error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="auth-actions">
|
||||
<button class="auth-submit" type="submit">Отправить письмо</button>
|
||||
<p class="auth-switch">
|
||||
Вспомнили пароль?
|
||||
<a href="{% url 'users:login' %}">Вернуться ко входу</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{% include "_fragments/footer.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
33
templates/users/password_reset_complete.html
Normal file
33
templates/users/password_reset_complete.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends '_layouts/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Пароль обновлён{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/login.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-page">
|
||||
{% include "_fragments/header.html" %}
|
||||
<main class="auth-section">
|
||||
<div class="container auth-layout">
|
||||
<section class="auth-intro auth-intro--login">
|
||||
<p class="auth-kicker">Готово</p>
|
||||
<h1 class="auth-title">Пароль обновлён</h1>
|
||||
<p class="auth-description">
|
||||
Новый пароль сохранён. Теперь можно войти в аккаунт с обновлёнными данными.
|
||||
</p>
|
||||
</section>
|
||||
<section class="auth-panel">
|
||||
<div class="auth-copy-block">
|
||||
<h2 class="auth-panel-title">Вход в аккаунт</h2>
|
||||
<p class="auth-panel-text">Используйте новый пароль на странице входа.</p>
|
||||
<a class="auth-submit-link" href="{% url 'users:login' %}">Перейти ко входу</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
{% include "_fragments/footer.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
78
templates/users/password_reset_confirm.html
Normal file
78
templates/users/password_reset_confirm.html
Normal file
@@ -0,0 +1,78 @@
|
||||
{% extends '_layouts/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Новый пароль{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/login.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-page">
|
||||
{% include "_fragments/header.html" %}
|
||||
<main class="auth-section">
|
||||
<div class="container auth-layout">
|
||||
<section class="auth-intro auth-intro--reset">
|
||||
<p class="auth-kicker">Обновление пароля</p>
|
||||
<h1 class="auth-title">Придумайте новый пароль</h1>
|
||||
<p class="auth-description">
|
||||
Используйте надёжный пароль, который вы ещё не применяли для этого аккаунта.
|
||||
</p>
|
||||
</section>
|
||||
<section class="auth-panel">
|
||||
{% if validlink %}
|
||||
<form class="auth-form" method="post">
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="auth-alert auth-alert--error">
|
||||
{% for error in form.non_field_errors %}
|
||||
<p>{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="{{ form.new_password1.id_for_label }}">Новый пароль</label>
|
||||
<input
|
||||
id="{{ form.new_password1.id_for_label }}"
|
||||
name="{{ form.new_password1.html_name }}"
|
||||
type="password"
|
||||
placeholder="Введите новый пароль"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
{% for error in form.new_password1.errors %}
|
||||
<p class="auth-field-error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="{{ form.new_password2.id_for_label }}">Подтверждение пароля</label>
|
||||
<input
|
||||
id="{{ form.new_password2.id_for_label }}"
|
||||
name="{{ form.new_password2.html_name }}"
|
||||
type="password"
|
||||
placeholder="Повторите пароль"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
{% for error in form.new_password2.errors %}
|
||||
<p class="auth-field-error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="auth-actions">
|
||||
<button class="auth-submit" type="submit">Сохранить пароль</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="auth-copy-block">
|
||||
<h2 class="auth-panel-title">Ссылка недействительна</h2>
|
||||
<p class="auth-panel-text">Возможно, ссылка уже использована или срок её действия истёк. Запросите сброс пароля повторно.</p>
|
||||
<a class="auth-secondary-link" href="{% url 'users:password_reset' %}">Запросить новое письмо</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
{% include "_fragments/footer.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
33
templates/users/password_reset_done.html
Normal file
33
templates/users/password_reset_done.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends '_layouts/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Письмо отправлено{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/login.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-page">
|
||||
{% include "_fragments/header.html" %}
|
||||
<main class="auth-section">
|
||||
<div class="container auth-layout">
|
||||
<section class="auth-intro auth-intro--reset">
|
||||
<p class="auth-kicker">Проверьте почту</p>
|
||||
<h1 class="auth-title">Письмо отправлено</h1>
|
||||
<p class="auth-description">
|
||||
Если аккаунт с таким адресом существует, мы отправили письмо со ссылкой для смены пароля.
|
||||
</p>
|
||||
</section>
|
||||
<section class="auth-panel">
|
||||
<div class="auth-copy-block">
|
||||
<h2 class="auth-panel-title">Что дальше</h2>
|
||||
<p class="auth-panel-text">Откройте письмо и перейдите по ссылке. Если письма нет, проверьте папку «Спам».</p>
|
||||
<a class="auth-secondary-link" href="{% url 'users:login' %}">Вернуться ко входу</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
{% include "_fragments/footer.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends '_layouts/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Профиль{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/login.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-page">
|
||||
{% include "_fragments/header.html" %}
|
||||
<main class="auth-section">
|
||||
<div class="container auth-layout">
|
||||
<section class="auth-intro auth-intro--login">
|
||||
<p class="auth-kicker">Личный кабинет</p>
|
||||
<h1 class="auth-title">Здравствуйте, {{ request.user.first_name|default:request.user.username }}</h1>
|
||||
<p class="auth-description">
|
||||
Ваш аккаунт активен и подтверждён. Здесь можно продолжить работу с заказами и персональными данными.
|
||||
</p>
|
||||
<a class="auth-side-link" href="/catalog/">Перейти в каталог</a>
|
||||
</section>
|
||||
<section class="auth-panel">
|
||||
<div class="auth-copy-block">
|
||||
<h2 class="auth-panel-title">Данные аккаунта</h2>
|
||||
<p class="auth-panel-text"><strong>Email:</strong> {{ request.user.email }}</p>
|
||||
<p class="auth-panel-text"><strong>Имя:</strong> {{ request.user.get_full_name|default:"Не указано" }}</p>
|
||||
<form method="post" action="{% url 'users:logout' %}">
|
||||
{% csrf_token %}
|
||||
<div class="auth-actions">
|
||||
<button class="auth-submit" type="submit">Выйти из аккаунта</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
{% include "_fragments/footer.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
{% extends '_layouts/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Регистрация{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/login.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-page">
|
||||
{% include "_fragments/header.html" %}
|
||||
|
||||
<main class="auth-section">
|
||||
<div class="container auth-layout auth-layout--reverse">
|
||||
<section class="auth-intro auth-intro--register">
|
||||
<p class="auth-kicker">Новый аккаунт</p>
|
||||
<h1 id="register-title" class="auth-title">Регистрация</h1>
|
||||
<p class="auth-description">
|
||||
Создайте аккаунт, чтобы получать персональные предложения, отслеживать покупки и сохранять нужные позиции.
|
||||
</p>
|
||||
<a class="auth-side-link" href="/services/">Посмотреть услуги</a>
|
||||
</section>
|
||||
|
||||
<section class="auth-panel" aria-labelledby="register-title">
|
||||
<form class="auth-form" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="auth-alert auth-alert--error">
|
||||
{% for error in form.non_field_errors %}
|
||||
<p>{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="{{ form.full_name.id_for_label }}">Имя и фамилия</label>
|
||||
<input
|
||||
id="{{ form.full_name.id_for_label }}"
|
||||
name="{{ form.full_name.html_name }}"
|
||||
type="text"
|
||||
value="{{ form.full_name.value|default:'' }}"
|
||||
placeholder="Введите ваше имя"
|
||||
autocomplete="name"
|
||||
>
|
||||
{% for error in form.full_name.errors %}
|
||||
<p class="auth-field-error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="{{ form.email.id_for_label }}">Электронная почта</label>
|
||||
<input
|
||||
id="{{ form.email.id_for_label }}"
|
||||
name="{{ form.email.html_name }}"
|
||||
type="email"
|
||||
value="{{ form.email.value|default:'' }}"
|
||||
placeholder="example@mail.ru"
|
||||
autocomplete="email"
|
||||
>
|
||||
{% for error in form.email.errors %}
|
||||
<p class="auth-field-error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="{{ form.password1.id_for_label }}">Пароль</label>
|
||||
<input
|
||||
id="{{ form.password1.id_for_label }}"
|
||||
name="{{ form.password1.html_name }}"
|
||||
type="password"
|
||||
placeholder="Придумайте пароль"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
{% for error in form.password1.errors %}
|
||||
<p class="auth-field-error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="{{ form.password2.id_for_label }}">Подтверждение пароля</label>
|
||||
<input
|
||||
id="{{ form.password2.id_for_label }}"
|
||||
name="{{ form.password2.html_name }}"
|
||||
type="password"
|
||||
placeholder="Повторите пароль"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
{% for error in form.password2.errors %}
|
||||
<p class="auth-field-error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="auth-actions">
|
||||
<button class="auth-submit" type="submit">Создать аккаунт</button>
|
||||
<p class="auth-switch">
|
||||
Уже есть аккаунт?
|
||||
<a href="{% url 'users:login' %}">Войти</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{% include "_fragments/footer.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
34
templates/users/register_done.html
Normal file
34
templates/users/register_done.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends '_layouts/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Подтвердите почту{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/login.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-page">
|
||||
{% include "_fragments/header.html" %}
|
||||
<main class="auth-section">
|
||||
<div class="container auth-layout">
|
||||
<section class="auth-intro auth-intro--register">
|
||||
<p class="auth-kicker">Почти готово</p>
|
||||
<h1 class="auth-title">Подтвердите почту</h1>
|
||||
<p class="auth-description">
|
||||
Мы отправили письмо на адрес {{ email }}. Перейдите по ссылке из письма, чтобы активировать аккаунт.
|
||||
</p>
|
||||
</section>
|
||||
<section class="auth-panel">
|
||||
<div class="auth-copy-block">
|
||||
<h2 class="auth-panel-title">Что дальше</h2>
|
||||
<p class="auth-panel-text">Проверьте входящие, а также папки «Спам» и «Промоакции», если письмо не видно сразу.</p>
|
||||
<p class="auth-switch">После подтверждения вы автоматически сможете войти в личный кабинет.</p>
|
||||
<a class="auth-secondary-link" href="{% url 'users:login' %}">Вернуться на страницу входа</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
{% include "_fragments/footer.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
88
users/forms.py
Normal file
88
users/forms.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from django import forms
|
||||
from django.contrib.auth import authenticate, get_user_model
|
||||
from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, UserCreationForm
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
email = forms.EmailField(label="Электронная почта")
|
||||
password = forms.CharField(label="Пароль", strip=False, widget=forms.PasswordInput)
|
||||
|
||||
error_messages = {
|
||||
"invalid_login": "Неверная электронная почта или пароль.",
|
||||
"inactive": "Аккаунт ещё не подтверждён. Проверьте письмо на почте.",
|
||||
}
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
self.request = request
|
||||
self.user_cache = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_email(self):
|
||||
return self.cleaned_data["email"].strip().lower()
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
email = cleaned_data.get("email")
|
||||
password = cleaned_data.get("password")
|
||||
|
||||
if not email or not password:
|
||||
return cleaned_data
|
||||
|
||||
self.user_cache = authenticate(self.request, username=email, password=password)
|
||||
if self.user_cache is not None:
|
||||
return cleaned_data
|
||||
|
||||
user = User.objects.filter(username=email).first()
|
||||
if user and not user.is_active and user.check_password(password):
|
||||
raise ValidationError(self.error_messages["inactive"])
|
||||
|
||||
raise ValidationError(self.error_messages["invalid_login"])
|
||||
|
||||
def get_user(self):
|
||||
return self.user_cache
|
||||
|
||||
|
||||
class RegisterForm(UserCreationForm):
|
||||
full_name = forms.CharField(label="Имя и фамилия", max_length=150)
|
||||
email = forms.EmailField(label="Электронная почта")
|
||||
|
||||
class Meta(UserCreationForm.Meta):
|
||||
model = User
|
||||
fields = ("full_name", "email", "password1", "password2")
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data["email"].strip().lower()
|
||||
if User.objects.filter(email__iexact=email).exists():
|
||||
raise ValidationError("Пользователь с такой электронной почтой уже существует.")
|
||||
return email
|
||||
|
||||
def save(self, commit=True):
|
||||
user = super().save(commit=False)
|
||||
email = self.cleaned_data["email"]
|
||||
full_name = self.cleaned_data["full_name"].strip()
|
||||
parts = full_name.split(maxsplit=1)
|
||||
|
||||
user.username = email
|
||||
user.email = email
|
||||
user.first_name = parts[0] if parts else ""
|
||||
user.last_name = parts[1] if len(parts) > 1 else ""
|
||||
user.is_active = False
|
||||
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class SantehRayPasswordResetForm(PasswordResetForm):
|
||||
email = forms.EmailField(label="Электронная почта")
|
||||
|
||||
def clean_email(self):
|
||||
return self.cleaned_data["email"].strip().lower()
|
||||
|
||||
|
||||
class SantehRaySetPasswordForm(SetPasswordForm):
|
||||
pass
|
||||
33
users/services.py
Normal file
33
users/services.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
|
||||
|
||||
def send_activation_email(request, user):
|
||||
uid = urlsafe_base64_encode(force_bytes(user.pk))
|
||||
token = default_token_generator.make_token(user)
|
||||
activation_path = reverse("users:activate", kwargs={"uidb64": uid, "token": token})
|
||||
activation_url = request.build_absolute_uri(activation_path)
|
||||
|
||||
context = {
|
||||
"user": user,
|
||||
"activation_url": activation_url,
|
||||
"site_name": getattr(settings, "SITE_NAME", "SantehRay"),
|
||||
}
|
||||
|
||||
subject = render_to_string("users/emails/activation_subject.txt", context).strip()
|
||||
text_body = render_to_string("users/emails/activation_email.txt", context)
|
||||
html_body = render_to_string("users/emails/activation_email.html", context)
|
||||
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_body,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[user.email],
|
||||
)
|
||||
email.attach_alternative(html_body, "text/html")
|
||||
email.send()
|
||||
@@ -1,3 +1,81 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.core import mail
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend")
|
||||
class UserAuthFlowTests(TestCase):
|
||||
def test_registration_creates_inactive_user_and_sends_activation_email(self):
|
||||
response = self.client.post(
|
||||
reverse("users:register"),
|
||||
data={
|
||||
"full_name": "Иван Петров",
|
||||
"email": "ivan@example.com",
|
||||
"password1": "StrongPassword123",
|
||||
"password2": "StrongPassword123",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
user = User.objects.get(email="ivan@example.com")
|
||||
self.assertFalse(user.is_active)
|
||||
self.assertEqual(user.username, "ivan@example.com")
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertIn("/users/activate/", mail.outbox[0].body)
|
||||
|
||||
def test_activation_link_activates_user(self):
|
||||
user = User.objects.create_user(
|
||||
username="anna@example.com",
|
||||
email="anna@example.com",
|
||||
password="StrongPassword123",
|
||||
is_active=False,
|
||||
)
|
||||
uid = urlsafe_base64_encode(force_bytes(user.pk))
|
||||
token = default_token_generator.make_token(user)
|
||||
|
||||
response = self.client.get(reverse("users:activate", kwargs={"uidb64": uid, "token": token}))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
user.refresh_from_db()
|
||||
self.assertTrue(user.is_active)
|
||||
self.assertEqual(int(self.client.session["_auth_user_id"]), user.pk)
|
||||
|
||||
def test_login_with_email_works_for_active_user(self):
|
||||
user = User.objects.create_user(
|
||||
username="oleg@example.com",
|
||||
email="oleg@example.com",
|
||||
password="StrongPassword123",
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("users:login"),
|
||||
data={"email": "oleg@example.com", "password": "StrongPassword123"},
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse("users:profile"))
|
||||
self.assertEqual(int(self.client.session["_auth_user_id"]), user.pk)
|
||||
|
||||
def test_password_reset_sends_email_for_existing_user(self):
|
||||
User.objects.create_user(
|
||||
username="maria@example.com",
|
||||
email="maria@example.com",
|
||||
password="StrongPassword123",
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("users:password_reset"),
|
||||
data={"email": "maria@example.com"},
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse("users:password_reset_done"))
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertIn("/users/reset/", mail.outbox[0].body)
|
||||
|
||||
@@ -2,8 +2,16 @@ from django.urls import path
|
||||
|
||||
from users import views
|
||||
|
||||
app_name = "users"
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', views.login, name='login'),
|
||||
path('register/', views.register, name='register'),
|
||||
path('logout/', views.logout, name='logout'),
|
||||
path('activate/<uidb64>/<token>/', views.activate, name='activate'),
|
||||
path('password-reset/', views.SantehRayPasswordResetView.as_view(), name='password_reset'),
|
||||
path('password-reset/done/', views.SantehRayPasswordResetDoneView.as_view(), name='password_reset_done'),
|
||||
path('reset/<uidb64>/<token>/', views.SantehRayPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
|
||||
path('reset/done/', views.SantehRayPasswordResetCompleteView.as_view(), name='password_reset_complete'),
|
||||
path('profile/', views.profile, name='profile')
|
||||
]
|
||||
|
||||
@@ -1,14 +1,98 @@
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth import get_user_model, login as auth_login, logout as auth_logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.contrib.auth.views import (
|
||||
PasswordResetCompleteView,
|
||||
PasswordResetConfirmView,
|
||||
PasswordResetDoneView,
|
||||
PasswordResetView,
|
||||
)
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.http import urlsafe_base64_decode
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from users.forms import LoginForm, RegisterForm, SantehRayPasswordResetForm, SantehRaySetPasswordForm
|
||||
from users.services import send_activation_email
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
# Create your views here.
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def login(request):
|
||||
return render(request, 'users/login.html', context={})
|
||||
if request.user.is_authenticated:
|
||||
return redirect("users:profile")
|
||||
|
||||
form = LoginForm(request=request, data=request.POST or None)
|
||||
if request.method == "POST" and form.is_valid():
|
||||
auth_login(request, form.get_user())
|
||||
return redirect("users:profile")
|
||||
|
||||
return render(request, "users/login.html", context={"form": form})
|
||||
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def register(request):
|
||||
return render(request, 'users/register.html', context={})
|
||||
if request.user.is_authenticated:
|
||||
return redirect("users:profile")
|
||||
|
||||
form = RegisterForm(request.POST or None)
|
||||
if request.method == "POST" and form.is_valid():
|
||||
user = form.save()
|
||||
send_activation_email(request, user)
|
||||
return render(request, "users/register_done.html", context={"email": user.email})
|
||||
|
||||
return render(request, "users/register.html", context={"form": form})
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
def logout(request):
|
||||
auth_logout(request)
|
||||
return redirect("users:login")
|
||||
|
||||
|
||||
def activate(request, uidb64, token):
|
||||
try:
|
||||
uid = force_str(urlsafe_base64_decode(uidb64))
|
||||
user = User.objects.get(pk=uid)
|
||||
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
|
||||
user = None
|
||||
|
||||
if user and default_token_generator.check_token(user, token):
|
||||
user.is_active = True
|
||||
user.save(update_fields=["is_active"])
|
||||
auth_login(request, user)
|
||||
return render(request, "users/activation_success.html", context={"user": user})
|
||||
|
||||
return render(request, "users/activation_invalid.html", status=400)
|
||||
|
||||
|
||||
@login_required
|
||||
def profile(request):
|
||||
return render(request, 'users/profile.html', context={})
|
||||
return render(request, "users/profile.html", context={})
|
||||
|
||||
|
||||
class SantehRayPasswordResetView(PasswordResetView):
|
||||
form_class = SantehRayPasswordResetForm
|
||||
template_name = "users/password_reset.html"
|
||||
email_template_name = "users/emails/password_reset_email.txt"
|
||||
html_email_template_name = "users/emails/password_reset_email.html"
|
||||
subject_template_name = "users/emails/password_reset_subject.txt"
|
||||
success_url = reverse_lazy("users:password_reset_done")
|
||||
|
||||
|
||||
class SantehRayPasswordResetDoneView(PasswordResetDoneView):
|
||||
template_name = "users/password_reset_done.html"
|
||||
|
||||
|
||||
class SantehRayPasswordResetConfirmView(PasswordResetConfirmView):
|
||||
template_name = "users/password_reset_confirm.html"
|
||||
success_url = reverse_lazy("users:password_reset_complete")
|
||||
form_class = SantehRaySetPasswordForm
|
||||
|
||||
|
||||
class SantehRayPasswordResetCompleteView(PasswordResetCompleteView):
|
||||
template_name = "users/password_reset_complete.html"
|
||||
|
||||
Reference in New Issue
Block a user