Перейти до змісту

Notes Chat App Auth — Специфіка проєкту

Ця глава описує деталі реалізації автентифікації специфічно для notes_chat_app — production-like стеку з Docker, PostgreSQL, Redis і WebSocket чатом. Матеріал не дублює те що описане у попередніх главах — тут тільки унікальні деталі.


Крок 0 — Docker запуск

# 1. Скопіювати конфігурацію
cp .env.example .env
# Відредагувати .env: SECRET_KEY, POSTGRES_*, за потреби NGROK_*

# 2. Підняти весь стек (PostgreSQL + Redis + Web + Nginx + ngrok + Selenium)
docker compose up --build

# 3. Переглянути логи (password reset email буде тут)
docker compose logs -f web

# 4. Відкрити у браузері
open http://localhost   # через nginx
# АБО
open http://localhost:8001   # напряму до Uvicorn

# 5. Демо-дані (3 юзери, групи, нотатки)
# Якщо SEED_DEMO_DATA=1 у .env → автоматично при старті
# Вручну:
docker compose exec web python manage.py seed_demo_data
# Логіни: demo_alice/demo1234, demo_bob/demo1234, demo_carol/demo1234

Що дивитись у браузері

URL Що показує
http://localhost/accounts/login/ Login форма
http://localhost/register/ Реєстрація нового акаунту
http://localhost/accounts/password_reset/ Форма відновлення пароля
http://localhost/accounts/password_change/ Зміна пароля (після входу)
http://localhost/notes/ Список нотаток (тільки залогінені)
http://localhost/groups/ Мої групи
http://localhost/groups/new/ Створити групу
http://localhost/admin/ Django Admin

Settings для Docker (settings.py)

На відміну від crispy_notes_project де SECRET_KEY може бути хардкодований для dev, у notes_chat_app всі чутливі дані виносяться у .env:

# notes_project/settings.py

import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

# Зчитуємо з environment (Docker передає через .env / docker-compose.yml):
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-insecure-key-change-in-production')
DEBUG = os.environ.get('DEBUG', 'True') == 'True'

ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')

# DATABASE_URL — PostgreSQL у Docker:
# postgresql://notes_user:notes_pass@db:5432/notes_db
# 'db' — ім'я сервісу з docker-compose.yml (DNS резолюція в Docker network)
import dj_database_url
DATABASES = {
    'default': dj_database_url.config(
        default=os.environ.get('DATABASE_URL', 'sqlite:///db.sqlite3')
    )
}

# REDIS_URL для Channel Layer (WebSocket):
REDIS_URL = os.environ.get('REDIS_URL', '')

Крок 2 — Реєстрація (Registration View)

django.contrib.auth.urls не включає реєстрацію — треба написати самому:

# notes_app/views.py
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import login
from django.shortcuts import redirect, render
from django.contrib import messages


def register(request):
    """
    Реєстрація нового юзера.
    Після успішної реєстрації — auto-login і redirect до note_list.
    """
    if request.user.is_authenticated:
        # Вже залогінений → не показуємо форму реєстрації
        return redirect('notes_app:note_list')

    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            # auto-login: не змушуємо юзера логінитись одразу після реєстрації
            login(request, user)
            messages.success(request, f'Вітаємо, {user.username}! Акаунт створено.')
            return redirect('notes_app:note_list')
    else:
        form = UserCreationForm()

    return render(request, 'registration/register.html', {'form': form})

templates/registration/register.html

{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block body %}
<div class="min-vh-100 d-flex align-items-center justify-content-center bg-light">
  <div class="card shadow" style="width: 440px;">
    <div class="card-body p-4">

      <div class="text-center mb-4">
        <i class="bi bi-person-plus-fill display-4 text-success"></i>
        <h5 class="fw-bold mt-2 mb-0">Реєстрація</h5>
        <p class="text-muted small">Створи новий акаунт</p>
      </div>

      <form method="post">
        {% csrf_token %}
        {{ form|crispy }}
        {# UserCreationForm: username, password1, password2 #}
        {# crispy: автоматично Bootstrap 5 стилі #}
        <button type="submit" class="btn btn-success w-100 mt-2 py-2">
          <i class="bi bi-check-circle me-2"></i>Зареєструватись
        </button>
      </form>

      <hr class="my-3">
      <p class="text-center text-muted small mb-0">
        Вже є акаунт? <a href="{% url 'login' %}">Увійти</a>
      </p>

    </div>
  </div>
</div>
{% endblock %}

Крок 3 — URL конфігурація

Місце: notes_project/urls.py

from django.contrib import admin
from django.urls import path, include
from debug_toolbar.toolbar import debug_toolbar_urls

urlpatterns = [
    path("admin/", admin.site.urls),

    # Django built-in auth: login, logout, password change, password reset
    # Реєструє 8 URL одним рядком!
    path("accounts/", include("django.contrib.auth.urls")),

    # Наш застосунок: нотатки, групи, реєстрація, WebSocket chat
    path("", include("notes_app.urls", namespace="notes_app")),
] + debug_toolbar_urls()

Місце: notes_app/urls.py

from django.urls import path
from . import views

app_name = 'notes_app'

urlpatterns = [
    # ── Notes ──────────────────────────────────────────────────────────────
    path('notes/',              views.note_list,    name='note_list'),
    path('notes/new/',          views.note_create,  name='note_create'),
    path('notes/<int:pk>/',     views.note_detail,  name='note_detail'),
    path('notes/<int:pk>/edit/',   views.note_edit,   name='note_edit'),
    path('notes/<int:pk>/delete/', views.note_delete, name='note_delete'),

    # ── Notebooks ──────────────────────────────────────────────────────────
    path('notebooks/',               views.notebook_list,   name='notebook_list'),
    path('notebooks/new/',           views.notebook_create, name='notebook_create'),
    path('notebooks/<int:pk>/edit/', views.notebook_edit,   name='notebook_edit'),
    path('notebooks/<int:pk>/delete/', views.notebook_delete, name='notebook_delete'),

    # ── Auth (custom — registration not in auth.urls) ──────────────────────
    path('register/', views.register, name='register'),

    # ── Groups ─────────────────────────────────────────────────────────────
    path('groups/',                  views.group_list,    name='group_list'),
    path('groups/new/',              views.group_create,  name='group_create'),
    path('groups/<int:pk>/',         views.group_detail,  name='group_detail'),
    path('groups/<int:pk>/delete/',  views.group_delete,  name='group_delete'),

    # ── Chat (WebSocket, через groups) ────────────────────────────────────
    path('groups/<int:pk>/chat/',    views.group_chat,    name='group_chat'),
]

Структура файлів notes_chat_app після кроку

notes_chat_app/
│
├── notes_project/
│   ├── settings.py          ★ LOGIN_URL, EMAIL_BACKEND, CSRF_TRUSTED_ORIGINS,
│   │                           SESSION_COOKIE_HTTPONLY, X_FRAME_OPTIONS
│   └── urls.py              ★ include('django.contrib.auth.urls') + notes_app
│
├── notes_app/
│   ├── models.py            ★ Note.group FK(Group, SET_NULL)
│   │                           ShoppingList.group FK(Group, SET_NULL)
│   │                           ChatMessage.group FK(Group, CASCADE)
│   ├── selectors.py         ★ get_user_notes (Q filter)
│   │                           get_user_groups (annotate member_count)
│   │                           get_group_with_members (membership check)
│   ├── services.py          ★ create_group, add_user_to_group,
│   │                           remove_user_from_group, delete_group
│   ├── views.py             ★ register, group_list, group_create,
│   │                           group_detail (3-action POST), group_delete
│   │                           + IDOR захист у note_edit, note_delete, etc.
│   ├── consumers.py         ★ GroupChatConsumer (WebSocket, membership check)
│   ├── urls.py              ★ /groups/ + /register/ URL patterns
│   ├── context_processors.py ← sidebar: notebooks + tags (без змін)
│   └── templates/notes_app/
│       ├── note_list.html       ← Q-filter нотатки (особисті + групові)
│       ├── note_form.html       ← group поле у формі
│       ├── group_list.html      ← картки груп з member_count  ★
│       ├── group_form.html      ← форма назви нової групи     ★
│       ├── group_detail.html    ← учасники, action=add/remove/leave ★
│       └── group_confirm_delete.html ← SET_NULL пояснення    ★
│
└── templates/
    ├── base.html                ← Bootstrap CDN, messages block
    ├── layouts/
    │   └── dashboard.html       ← "Групи" у sidebar + dropdown:
    │                               "Змінити пароль", "Вийти" (POST logout)
    ├── registration/
    │   ├── login.html           ★ CSRF токен, "next" hidden input
    │   ├── register.html        ★ UserCreationForm + crispy
    │   ├── password_reset_form.html    ★ email форма
    │   ├── password_reset_done.html    ★ DEV: підказка про docker logs
    │   ├── password_reset_confirm.html ★ validlink check + crispy form
    │   ├── password_reset_complete.html ★ успіх + посилання login
    │   ├── password_reset_email.html   ★ текст листа (protocol/domain/token)
    │   ├── password_change_form.html   ★ PasswordChangeForm + crispy
    │   └── password_change_done.html   ★ успіх
    └── components/
        ├── empty_state.html
        ├── pagination.html
        └── confirm_modal.html

Перевірка у браузері: що тестувати

Дія URL Що перевіряти
Реєстрація /register/ Auto-login → redirect /notes/
Login /accounts/login/ Session cookie у DevTools → Application → Cookies
Login CSRF DevTools → Application → Cookies csrftoken присутній
Logout POST кнопка Cookie sessionid видалено
Password Reset /accounts/password_reset/ Email у docker compose logs web
Токен expired /accounts/reset/... (стара URL) validlink=False → "Посилання недійсне"
IDOR тест /notes/2/edit/ (чужа нотатка) 404 (не 403!)
Password Change /accounts/password_change/ old_password перевіряється
Група /groups/new/ → /groups/<pk>/ Creator є учасником
Групові нотатки Позначити нотатку групою Інший член бачить у /notes/
Group chat /groups/<pk>/chat/ WebSocket підключення
DELETE group /groups/<pk>/delete/ → POST Нотатки стають особистими (group=NULL)

Далі

Наступна глава: Checkpoint — чеклист самоперевірки, IDOR test вручну, типові помилки, практичне завдання.