3 горизонтальные линии, бургер
3 горизонтальные линии, бургер
3 горизонтальные линии, бургер
3 горизонтальные линии, бургер

3 горизонтальные линии, бургер
Удалить все
ЗАГРУЗКА ...

Содержание



    Как интегрировать allauth в django, всё на react. И как при этом организовать регистрацию и автризацию

    Часы
    31.01.2025
    /
    Часы
    08.03.2026
    /
    Часы
    16 минут
    Глазик
    811
    Сердечки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    Соединённые точки
    0

    Введение

    Да, давно я не писал про этот сайт. И теперь считаю своим долгом закончить его во чтобы-то ни стало. Итак, нужно разработать и добавить систему аутентификации и регистрации пользователей на сайт. Как это сделать и как это будет выглядеть?
    Здесь на самом деле нет ничего сложного. Сам подумай, нам необходимо разработать следующие элементы:
    1. Форму входа
    2. Форму регистрации
    3. Форму изменения пароля
    4. Форму выхода
    5. Форму подтверждения почты + шаблон
    6. Форму сброса пароля + шаблон
    7. Модальное окно для профиля пользователя
    И, собственно говоря, всё ... Хотя AllAuth необязательно интегрировать на сайт, можно реализовать регистрацию и управление пользователями самостоятельно, это всё таки будет проще. Нам останется реализовать только фронтенд часть сайта.
    Я не очень сильно хочу усложнять и перегружать данную статью имплементацией JWT-токенов или реализации таких архитектур управления учётными профилями пользователей, как SAML2 или LDAP, или даже регистрацию через социальные сети. Это будет в следующих проектах и статьях, ибо она и так получилась слишком большой.
    То есть в этой статье я опишу процесс интеграции AllAuth библиотеки на django сайт с минимально необходимым функционалом.

    Пишем фронтенд для формы входа и регистрации

    Для начала подготовим контейнеры в которых будем рисовать наши модальные окна. Добавь следующие строчки кода в шаблон base.html, сразу перед тегом "main" и после конца блока с шапкой сайта:
    ... {% block header %} {% endblock %} <div data-type="profile"><a href="#">profile</a></div> ... <div id="auth-modal"></div> <main class="flex-auto pl-2 pr-2 pt-3 pb-3"> {% block main %} {% endblock %} </main>
    В моём случае тег с атрибутом data-type="profile" будет играть роль ещё одной кнопки в шапке сайта. Она будет открывать карточку пользователя. Карточка пользователя будет отрисовываться в виде модального окна, которое будет находиться в теге с id="auth-modal".
    Теперь подключим новый компонент Auth. Этот реакт-компонент и будет рисовать для нас модальные окна Входа, Регистрации, карточку пользователя и прочие. В файле компонента src/index.js, сделаем импорт:
    import Header from './components/Header'; import Footer from './components/Footer'; import AppSettings from './components/AppSettings'; import AppUtils from './components/AppUtils'; import AppActions from './components/AppActions'; import AppQueries from './components/AppQueries'; import Msg from './components/Msg'; import Hint from './components/Tutorial'; import Auth from './components/Auth';
    Сам файл компонента реализует компонент под названием Auth, который через пропсы определяет какое модальное окно показать и как обратиться к серверу, соответственно. Наверняка, есть более простой и элегантный способ сделать всё то, что я сейчас сделал, но мне просто нужны точки общения с сервером откуда я смогу получать шаблоны для входа и регистрации и менять сайт соответственно. Вот шаблонный файл src/components/Auth.js:
    import * as React from 'react'; import { createRoot } from 'react-dom/client'; import Modal from '@mui/material/Modal'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Button from '@mui/material/Button' import axios from "axios"; import FormControl from '@mui/material/FormControl'; import FormControlLabel from '@mui/material/FormControlLabel'; import Input from '@mui/material/Input'; import InputLabel from '@mui/material/InputLabel'; import IconButton from '@mui/material/IconButton'; import Link from '@mui/material/Link'; import Checkbox from '@mui/material/Checkbox'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; import { Wait, StopWait, Msg } from './Waiter'; function getCookie (name) { let cookieValue = null if (document.cookie && document.cookie !== '') { const cookies = document.cookie.split(';') for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim() // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)) break } } } return cookieValue } export function getCSRFToken () { return getCookie('csrftoken') } const BASE_URL = `/_allauth/browser/v1` export const URLs = Object.freeze({ // Meta CONFIG: BASE_URL + '/config', // Account management CHANGE_PASSWORD: BASE_URL + '/account/password/change', EMAIL: BASE_URL + '/account/email', PROVIDERS: BASE_URL + '/account/providers', // Account management: 2FA AUTHENTICATORS: BASE_URL + '/account/authenticators', RECOVERY_CODES: BASE_URL + '/account/authenticators/recovery-codes', TOTP_AUTHENTICATOR: BASE_URL + '/account/authenticators/totp', // Auth: Basics LOGIN: BASE_URL + '/auth/login', LOGOUT: BASE_URL + '/auth/session', REQUEST_LOGIN_CODE: BASE_URL + '/auth/code/request', CONFIRM_LOGIN_CODE: BASE_URL + '/auth/code/confirm', SESSION: BASE_URL + '/auth/session', REAUTHENTICATE: BASE_URL + '/auth/reauthenticate', REQUEST_PASSWORD_RESET: BASE_URL + '/auth/password/request', RESET_PASSWORD: BASE_URL + '/auth/password/reset', SIGNUP: BASE_URL + '/auth/signup', VERIFY_EMAIL: BASE_URL + '/auth/email/verify', // Auth: 2FA MFA_AUTHENTICATE: BASE_URL + '/auth/2fa/authenticate', MFA_REAUTHENTICATE: BASE_URL + '/auth/2fa/reauthenticate', // Auth: Social PROVIDER_SIGNUP: BASE_URL + '/auth/provider/signup', REDIRECT_TO_PROVIDER: BASE_URL + '/auth/provider/redirect', PROVIDER_TOKEN: BASE_URL + '/auth/provider/token', // Auth: Sessions SESSIONS: BASE_URL + '/auth/sessions', // Auth: WebAuthn REAUTHENTICATE_WEBAUTHN: BASE_URL + '/auth/webauthn/reauthenticate', AUTHENTICATE_WEBAUTHN: BASE_URL + '/auth/webauthn/authenticate', LOGIN_WEBAUTHN: BASE_URL + '/auth/webauthn/login', SIGNUP_WEBAUTHN: BASE_URL + '/auth/webauthn/signup', WEBAUTHN_AUTHENTICATOR: BASE_URL + '/account/authenticators/webauthn' }) var session = { username: null, email: { address: null, verified: null, }, is_authenticated: false } const updateEmail = async () =>{ const {data} = await axios.get(URLs.EMAIL, { headers: { "accept": "application/json", 'Content-Type': "application/json", } }).catch(error=>{ return error.response }) if (data.status === 200){ session.email.address = data.data[0].email if (data.data[0].verified){ session.email.verified = 'verified' }else{ session.email.verified = 'not verified' } }else if(data.status === 401){ session.email.address = null session.email.verified = null } } const updateSession = async () => { const {data} = await axios.get(URLs.SESSION, { headers: { "accept": "application/json", 'Content-Type': "application/json", } }).catch(error=>{ return error.response }) if (data.status === 200){ session.username = data.data.user.username session.is_authenticated = true }else if(data.status === 401){ session.username = null session.is_authenticated = false } updateEmail() } function onRequestConfirmEmail(req, email){ } function onRequestPasswordReset(req){ } function fetchPasswordReset(req){ } function onPasswordChange(req){ } function fetchPasswordChange(req){ } function fetchLogin(req){ } function onLogin(req){ } function fetchSignup(req){ } function onSignup(req){ } function onSignout(req){ } function fetchProfile(req){ } export default function Auth(props){ const openModal = (event) =>{ setModal(true); } const [isModal, setModal] = React.useState(false); const modalRef = React.useRef(null) const req = {'setModal': setModal, 'modal': modalRef} const onInitBtn = document.getElementById(props.btnId) onInitBtn.addEventListener(props.btnId, openModal) return ( <Modal ref={modalRef} open={isModal} onClose={()=>{setModal(false)}} > <Box id='auth-modal' className="absolute top-1/2 left-1/2 -translate-x-2/4 -translate-y-2/4 shadow-md p-4 bg-white"> <div className="flex flex-col gap-3 min-w-72 "> <div id="modal-title"> <Typography variant="h6" component="h3"> {props.title} </Typography> </div> <hr></hr> <div id="modal-content"> {props.fetchFunc(req)} </div> <hr></hr> <div id="modal-button"> {props.procedBtnFunc !== null && <Box className="flex flex-row gap-4 items-center justify-between"> <Button onClick={()=>{props.procedBtnFunc(req)}} variant='text'>proceed</Button> </Box> } </div> </div> </Box> </Modal> ) } const authentication_profile_container = document.getElementById('auth-modal'); if (authentication_profile_container){ const authentication_profile_root = createRoot(authentication_profile_container); authentication_profile_root.render(<Auth title='Profile' btnId='onProfile' procedBtnFunc={null} fetchFunc={fetchProfile}></Auth>); }
    Теперь не много о структуре и работе данного модуля. Есть главный экспортируемый модуль - Auth. Это собственно, модальное окно, которое будет содержать, и карточку пользователя, и форму входа, и формы регистрации, и прочие формы.
    Ещё есть два вида функций on* и fetch*. Первые общаются с сервером, вторые обновляют модальное окно. Не обязательно следовать данному принципу, но так легче поддерживать код.
    Дальше есть функции updateEmail и updateSession. Они занимаются ровно тем, чем они обзываются, обновляют информацию об адресе почты и обновляют информацию о текущей сессии. Так же добавил такие дополнительные функции как:
    1. getCookie(name) - на случай если захочу получить что-нибудь и куки браузера.
    2. getCSRFToken() - почти каждый запрос требует csrf-токен поэтому написал отдельную функцию
    На счёт, глобального объекта URLs. Это не весь список доступных путей к allauth api, но минимально необходимый для нас. Можно конечно и удалить его, и прописывать все URL вручную, но это уже без меня. Двигаемся дальше.
    Надо не забыть поменять компонент Header в src/components/header.js. При создании кнопок в шапке сайта я создаю кастомные события, к которым подключу модальное окно, вот так:
    export default function Header() { const header_buttons = document.getElementById('meta-header') const btns = [] for ( const btn of header_buttons.children){ const ref = btn.firstElementChild.getAttribute('href') if (btn.dataset.type == 'inner-link'){ btns.push(<Button color='primary'><a href={ref}>{btn.innerText}</a><LinkIcon className='mb-2' fontSize='small' /></Button>) } else if (btn.dataset.type == 'tutorial' ){ btns.push(<Button id='onTutorial' onClick={(e)=>{ const onTutorialEvent = new Event('onTutorial') e.currentTarget.dispatchEvent(onTutorialEvent) }} color='primary'><a href={ref}>{btn.innerText}</a><QuestionMarkIcon className='mb-2' fontSize='small'/></Button>) } else if (btn.dataset.type == 'profile' ){ btns.push( <Button id='onProfile' onClick={(e)=>{ const onProfileEvent = new Event('onProfile') e.currentTarget.dispatchEvent(onProfileEvent) }} color='primary'><a href="#">{btn.innerText}</a><AccountBoxIcon className='mb-2' fontSize='small'/></Button> ) } }
    Поменяй всё что у тебя было на выделенные строчки

    Подключаем Allauth к приложению

    Для начала хотел бы отметить официальную документацию, она полная и вполне самодостаточная, хотя, чтобы разобраться в том как использовать данную библиотеку конкретно на своём сайте потребуется некоторое время и изучение официальных примеров. Слава им, они у них есть.
    Как всегда, начнём с установки необходимых пакетов. Пакет django-allauth обязателен, но django-allauth[socialaccount] нет, если не нужно использовать соцсети в качестве провайдера для входа. Но я конечно же установлю их оба:
    pip install django-allauth
    Дальше необходимо добавить следующий allauth-бэкенд в settings.py:
    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 email 'allauth.account.auth_backends.AuthenticationBackend', ]
    Allauth разработан как встраиваемое django-приложение, вернее целая куча приложений, которые необходимо подключить:
    INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'corsheaders', 'rest_framework', 'Backend.apps.BackendConfig', 'Frontend.apps.FrontendConfig', 'Authentication.apps.AuthenticationConfig', # BY ALLAUTH 'allauth', 'allauth.account', ] MIDDLEWARE = ( "django.middleware.common.CommonMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", # BY ALLAUTH "allauth.account.middleware.AccountMiddleware", )
    Также будет необходимо подключить и настроить email-backend. С той лишь целью, чтобы протестировать подтверждение отправленной почты и возможность сбросить пароль. Я подключу тестовый бэкенд, ибо это просто быстрее и нагляднее. Все сообщения будут записываться в файлы. В дальнейшем мы конечно же поменяем его на нормальный email-бэкенд.
    # BY ALLAUTH testing only EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' EMAIL_FILE_PATH = BASE_DIR / 'emails'
    Добавь эти строчки где-нибудь в settings.py
    В файле settings.py мы закончили. Осталось только добавить пути и провести миграцию:
    from django.contrib import admin from django.urls import path, include from rest_framework import routers from Backend import views router = routers.DefaultRouter() router.register(r'results', views.BackendModelView, 'result') urlpatterns = [ path('admin/', admin.site.urls), path('api/', include(router.urls)), path('', include('Frontend.urls')), path('', include('Authentication.urls')), # BY ALLAUTH path('accounts/', include('allauth.urls')), ]
    В файле Website/urls.py
    python manage.py migrate
    Создаём миграцию
    После всех этих манипуляций, перейди по адресу http://localhost:8000/accounts/, ты должен увидеть что-то вроде этого:

    Переводим Allauth в "headless" режим

    По факту, всё готово, но не совсем. Мне не нужно перенаправляться на отдельную страницу (это не то, как мой django-react-сайт работает). В идеале мне нужен лишь api, а дизайн я уж и сам сделаю. И чтобы получить доступ к api allauth, потребуется ещё кое-что докрутить. Опять же, официальна документация вполне самодостаточна насчёт этого вопроса.
    INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'corsheaders', 'rest_framework', 'Backend.apps.BackendConfig', 'Frontend.apps.FrontendConfig', 'Authentication.apps.AuthenticationConfig', # BY ALLAUTH 'allauth', 'allauth.account', 'allauth.headless', 'allauth.mfa', 'allauth.usersessions', ]
    Добавь следующие приложения
    И ещё нужно добавить новые пути к api allauth
    from django.contrib import admin from django.urls import path, include from rest_framework import routers from Backend import views router = routers.DefaultRouter() router.register(r'results', views.BackendModelView, 'result') urlpatterns = [ path('admin/', admin.site.urls), path('api/', include(router.urls)), path('', include('Frontend.urls')), path('', include('Authentication.urls')), # BY ALLAUTH path('accounts/', include('allauth.urls')), path("_allauth/", include("allauth.headless.urls")), ]
    В файле Website/urls.py
    Это делать необязательно, но всё же. Раз уж весь фронтенд на реакте, то и лишние страницы мне тоже не нужны, а значит мой сайт будет использовать allauth в режиме "headless only". Для этого добавь данные строчки, где-нибудь в settings.py:
    HEADLESS_ONLY = True HEADLESS_FRONTEND_URLS = { "account_confirm_email": "http://localhost:8000/email-verify/{key}", "account_reset_password_from_key": "http://localhost:8000/password-reset/{key}", }
    Как ты мог заметить мы добавили ещё словарь HEADLESS_FRONTEND_URLS. Он будет необходим когда будем реализовывать подтверждение почты или сброса пароля, а пока не беспокойся из-за этого.

    Связываем всё вместе

    С этого момента мы можем поступить следующим образом; В тупую написать POST запросы к API allauth прямо из реакта (нам нужно буквально 4 таких запроса: на вход /auth/login, на регистрацию /auth/signup, на подтверждение почты /auth/email/verify и на восстановление пароля /auth/password/request). При этом, никакое django-приложение не потребуется.
    Кстати, весь headless API для выполнения запросов можно посмотреть тут, !!крайне рекомендую!!.
    Но можно поступить по другому; Написать отдельное приложение, собрать там необходимые ModelForm-ы отрендерить их на сервере и при необходимости возвращать клиенту. Это куда дольше, но позволит тебе и мне лучше понять внутреннюю работу allauth пакета и более того, такой подход более гибкий в плане дизайна и отказоустойчивости приложения.
    Мы пойдём по первому пути ибо Реакт ... >﹏<

    Пишем запросы на сервер (Фронтенд)

    Делаем карточку пользователя

    Начнём мы с карточки пользователя. Конечно она будет не слишком большой и сложной, скорее даже наоборот. Минимум функционала, только необходимые данные. Пока что без привязки к другим соц. сетям.
    function fetchProfile(req){ updateSession() return ( <div> { session.is_authenticated ? <Box> <Typography> You are logged in as <b>{session.username}</b> </Typography> <Box className="flex flex-row gap-4 items-center"> <Button onClick={()=>{fetchPasswordChange(req)}} variant='text'>Change password</Button> <Button onClick={()=>{fetchPasswordReset(req)}} variant='text'>Reset password</Button> </Box> <Typography> Your email are: <b>{session.email.address}</b> | {session.email.verified} </Typography> { session.email.verified === 'not verified' && <Box className="flex flex-row gap-4 items-center"> <Button onClick={()=>{onRequestConfirmEmail(req, session.email.address)}} variant='text'>verify</Button> </Box> } <Box className="flex flex-row gap-4 items-center"> <Button onClick={()=>{onSignout(req)}} variant='text'>Sign out</Button> </Box> </Box> : <Box> <Typography> You are not logged in </Typography> <Box className="flex flex-row gap-4 items-center"> <Button onClick={()=>{fetchLogin(req)}} variant='text'>Log in</Button> <Typography>or</Typography> <Button onClick={()=>{fetchSignup(req)}} variant='text'>Sign up</Button> </Box> </Box> } </div> ) }
    Сначала мы получаем статус сессии, то есть узнаём гость это или уже зарегистрированный пользователь. После, если это зарегистрированный пользователь, отображаем всю инфу которая может ему пригодиться (почта, тип почты, имя). И конечно же добавляем кнопки для взаимодействия.
    Как-то так, зарегистрированный пользователь
    Как-то так, не зарегистрированный пользователь
    К каждой кнопке была привязана функция обновления модального окна. И дальше мы будем разбирать, то как это окно меняется и как будет отправляться запрос на сервер.

    Форма и запрос на регистрацию

    Для регистрации пользователя я использую 4 поля (одно из которых опционально) (+1 одно поле скрыто, это поле мы заполняем csrf-токеном). Так же меняю заголовок и основную кнопку модального окна.
    function fetchSignup(req){ var container = req.modal.current.querySelector('#modal-content') createRoot(container).render( <form method='post' class="flex flex-col gap-5" style={{maxHeight: '500px', overflow: 'scroll'}}> <FormControl style={{maxWidth: 'fit-content'}}> <InputLabel htmlFor="username">Your username</InputLabel> <Input class="w-fit" id="username"/> </FormControl> <FormControl style={{maxWidth: 'fit-content'}}> <InputLabel htmlFor="email">Your email(optional)</InputLabel> <Input type="email" id="email"/> </FormControl> <FormControl style={{maxWidth: 'fit-content'}}> <InputLabel htmlFor="password">Your password</InputLabel> <Input type='password' id="password"/> </FormControl> <FormControl style={{maxWidth: 'fit-content'}}> <InputLabel htmlFor="password2">Your password(again)</InputLabel> <Input type='password' id="password2"/> </FormControl> <List style={{maxWidth: '300px'}}> <ListItem> <ListItemText style={{color: "#6c6c24"}} primary="* Your password can’t be too similar to your other personal information." /> </ListItem> <ListItem> <ListItemText style={{color: "#6c6c24"}} primary="* Your password must contain at least 8 characters." /> </ListItem> <ListItem> <ListItemText style={{color: "#6c6c24"}} primary="* Your password can’t be a commonly used password." /> </ListItem> <ListItem> <ListItemText style={{color: "#6c6c24"}} primary="* Your password can’t be entirely numeric." /> </ListItem> </List> </form> ) var container_btn = req.modal.current.querySelector('#modal-button') createRoot(container_btn).render( <Box className="flex flex-row gap-4 items-center justify-between"> <Button onClick={()=>{onSignup(req)}} variant='text'>proceed</Button> </Box> ) var container_title = req.modal.current.querySelector('#modal-title') createRoot(container_title).render( <Typography variant="h6" component="h3"> Sign up </Typography> ) }
    При нажатии кнопки PROCEED, отправляется форма. Ну это если кратко, а если развёрнуто то ... Сначала открывается спинер, после собираем все необходимые данные с формы, проверяем их. Делаем POST-запрос не сервер по пути /_allauth/browser/v1/auth/signup т. е. URLs.SIGNUP, предварительно оставив в заголовке запроса csrf-токен и требуемый формат ответа. Дальше, вне зависимости от ответа, мы закрываем форму и оставляем сообщение пользователю об успехе запроса.
    function onSignup(req){ // Do POST request to server // Try to sign up Wait() var form_data = new FormData() var password1 = document.querySelector('#password').value var password2 = document.querySelector('#password2').value form_data.append('username', document.querySelector('#username').value) form_data.append('email', document.querySelector('#email').value) form_data.append('password', password1) form_data.append('csrfmiddlewaretoken', getCSRFToken()) if (password2 !== password1) { Msg('Password does not match.', 'error') return } axios.post(URLs.SIGNUP, form_data, { headers: { "accept": "application/json", "X-CSRFToken": getCSRFToken(), 'Content-Type': "application/json", } }) .then(data => { StopWait(`Welcome ${document.querySelector('#username').value}.`, 'success') updateSession() req.setModal(false); }) .catch(err => { StopWait(`${err.response.data.errors[0].message}`, 'error') req.setModal(false); }); }

    Форма и запрос на вход

    Форма входа, проще чем регистрация. Тут требуется только имя пользователя и его пароль.
    Мы создаём три корневых элемента и меняем соответственно саму форму, кнопку и заголовок формы.
    function fetchLogin(req){ var container = req.modal.current.querySelector('#modal-content') createRoot(container).render( <form method='post' class="flex flex-col gap-5"> <FormControl> <InputLabel htmlFor="username">Your username</InputLabel> <Input id="username"/> </FormControl> <FormControl> <InputLabel htmlFor="password">Your password</InputLabel> <Input type='password' id="password"/> </FormControl> <Link href="#" onClick={()=>{fetchPasswordReset(req)}}><Typography>Forgot your password?</Typography></Link> <FormControl> <FormControlLabel control={<Checkbox id="remember-me"/>} label="Remember me?" /> </FormControl> </form> ) var container_btn = req.modal.current.querySelector('#modal-button') createRoot(container_btn).render( <Box className="flex flex-row gap-4 items-center justify-between"> <Button onClick={()=>{onLogin(req)}} variant='text'>proceed</Button> </Box> ) var container_title = req.modal.current.querySelector('#modal-title') createRoot(container_title).render( <Typography variant="h6" component="h3"> Log in </Typography> ) }
    Отправляем форму по адресу /_allauth/browser/v1/auth/login т.е. URLs.LOGIN.
    function onLogin(req){ Wait() var form_data = new FormData() form_data.append('username', document.querySelector('#username').value) form_data.append('password', document.querySelector('#password').value) form_data.append('remember', document.querySelector('#remember-me').checked) form_data.append('csrfmiddlewaretoken', getCSRFToken()) axios.post(URLs.LOGIN, form_data, { headers: { "accept": "application/json", "X-CSRFToken": getCSRFToken(), 'Content-Type': "application/json", } }) .then(data => { StopWait(`Welcome ${document.querySelector('#username').value}.`, 'success') updateSession() req.setModal(false); }) .catch(err => { StopWait(`${err.response.data.errors[0].message}`, 'error') req.setModal(false); }); }

    Форма и запрос на изменение пароля

    Для измения пароля нам потребуется отдельная форма. В функции fetchPasswordChange, мы меняем форму, заголовок и кнопку подтверждения. Так же, я запрашиваю ввести новый пароль дважды, ну на всякий случай...
    function fetchPasswordChange(req){ var container = req.modal.current.querySelector('#modal-content') createRoot(container).render( <form method='post' class="flex flex-col gap-5"> <FormControl style={{maxWidth: 'fit-content'}}> <InputLabel htmlFor="current_password">Current password</InputLabel> <Input type="password" id="current_password"/> </FormControl> <FormControl style={{maxWidth: 'fit-content'}}> <InputLabel htmlFor="new_password1">New password</InputLabel> <Input type="password" id="new_password1"/> </FormControl> <FormControl style={{maxWidth: 'fit-content'}}> <InputLabel htmlFor="new_password2">New password(again)</InputLabel> <Input type="password" id="new_password2"/> </FormControl> </form> ) var container_btn = req.modal.current.querySelector('#modal-button') createRoot(container_btn).render( <Box className="flex flex-row gap-4 items-center justify-between"> <Button onClick={()=>{onPasswordChange(req)}} variant='text'>change</Button> </Box> ) var container_title = req.modal.current.querySelector('#modal-title') createRoot(container_title).render( <Typography variant="h6" component="h3"> Change password </Typography> ) }
    Перед тем как отправить запрос на изменение пароля, нужно проверить совпадают ли новые пароли друг с другом. После чего делаем POST-запрос по адресу /_allauth/browser/v1/account/password/change или URLs.CHANGE_PASSWORD.
    function onPasswordChange(req){ var form_data = new FormData() var password1 = document.querySelector('#new_password1').value var password2 = document.querySelector('#new_password2').value form_data.append('current_password', document.querySelector('#current_password').value) form_data.append('new_password', password1) form_data.append('csrfmiddlewaretoken', getCSRFToken()) if (password2 !== password1) { Msg('Password does not match.', 'error') return } axios.post(URLs.CHANGE_PASSWORD, form_data, { headers: { "accept": "application/json", "X-CSRFToken": getCSRFToken(), 'Content-Type': "application/json", } }) .then(data => { StopWait("You had successfully change a password", 'success') updateSession() req.setModal(false); }) .catch(err => { StopWait(`${err.response.data.errors[0].message}`, 'error') req.setModal(false); }); }

    Форма и запрос на сброс пароля

    Обновляем модальное окно т.е. заголовок, форму и саму кнопку конечно же. Для отправки запроса на востановление пароля потребуется только адресс электронной почты.
    function fetchPasswordReset(req){ var container = req.modal.current.querySelector('#modal-content') createRoot(container).render( <form method='post' class="flex flex-col gap-5"> <Typography> You will recieve a confirmation message.</Typography> <FormControl style={{maxWidth: 'fit-content'}}> <InputLabel htmlFor="password_email">Your email</InputLabel> <Input type="email" id="password_email"/> </FormControl> </form> ) var container_btn = req.modal.current.querySelector('#modal-button') createRoot(container_btn).render( <Box className="flex flex-row gap-4 items-center justify-between"> <Button onClick={()=>{onRequestPasswordReset(req)}} variant='text'>send</Button> </Box> ) var container_title = req.modal.current.querySelector('#modal-title') createRoot(container_title).render( <Typography variant="h6" component="h3"> Password reset </Typography> ) }
    Итак как это вообще работает? Пользователь забыл пароль и хочет его востановить. Для этого он отравляет POST-запрос по адресу /_allauth/browser/v1/auth/password/request или URLs.REQUEST_PASSWORD_RESET. Этот запрос отправляется на сервер, откуда будет отправленно электронное письмо на указаный адрес.
    function onRequestPasswordReset(req){ Wait() var form_data = new FormData() var email = document.querySelector('#password_email').value form_data.append('email', email) axios.post(URLs.REQUEST_PASSWORD_RESET, form_data, { headers: { "accept": "application/json", "X-CSRFToken": getCSRFToken(), 'Content-Type': "application/json", } }).then((data)=>{ StopWait(`Successfully send an email to ${email}`, 'success') req.setModal(false); }).catch((err)=>{ StopWait(`${err.response.data.errors[0].message}`, 'error') req.setModal(false); }) }
    Функция запроса на востановление пароля
    Письмо отправилось. В письме будет ссылка на страницу с ключом в адресе. Нужно перейти по данному адресу чтобы попасть на такую вот страницу.
    Из коробки django-allauth не предоставляет страницу для обработки запростов по востановлению пароля или верификации почты (ЕСЛИ КОНЕЧНО ЭТО НЕ HEADLESS РЕЖИМ). В противном случае будет перенаправление на шаблонную страницу предоставляемую при обычном режиме.
    На этой странице нужно заполнить данную форму. То есть ввести новый пароль. Было бы наверное лучше если бы я ещё запрашивал подтверждение пароля, но сойдёт и так. В действительности форма должна быть отправленна с двумя полями:
    1. новый пароль
    2. сгенерированный ключ
    function onConfirmReset(key, container){ var form_data = new FormData() var password1 = document.querySelector('#password1').value var password2 = document.querySelector('#password2').value if (password2 !== password1) { Msg('Password does not match.', 'error') return } form_data.append('key', key) form_data.append('password', password1) axios.post(URLs.RESET_PASSWORD, form_data, { headers: { "accept": "application/json", "X-CSRFToken": getCSRFToken(), 'Content-Type': "application/json", } }).then((data) => { Msg('You have successfully changed your password.', 'success') createRoot(container.current).render( <Typography>You have successfully changed your password.</Typography> ) }).catch((err) => { if (err.response.status === 401){ Msg('You have successfully changed your password.', 'success') createRoot(container.current).render( <Box className="flex flex-col gap-6 items-center justify-between"> <Typography>You have successfully changed your password.</Typography> </Box> ) }else{ var errors = err.response.data.errors const lines = [] errors.forEach((error) =>{ lines.push(<Typography>{error.message}</Typography>) }) createRoot(container.current).render( <Box className="flex flex-col gap-6 items-center justify-between"> {lines} </Box> ) } }); }
    В данном случае я не делал разделение на on* и fetch* функции ибо это отдельный компонент и компонент довольно маленький. Помести данную функцию в ./components/PasswordReset.js
    Для того чтобы данная функция вообще имела смысл я создал отдельный компонент под названием PasswordReset. Создал файл-компонент PasswordReset.js в components и подключил его в index.js:
    import Header from './components/Header'; import Footer from './components/Footer'; import AppSettings from './components/AppSettings'; import AppUtils from './components/AppUtils'; import AppActions from './components/AppActions'; import AppQueries from './components/AppQueries'; import Message from './components/Msg'; import Hint from './components/Tutorial'; import Auth from './components/Auth'; import PasswordReset from './components/PasswordReset';
    Вот полный код компонента:
    import * as React from 'react'; import axios from "axios"; import { createRoot } from 'react-dom/client'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Button from '@mui/material/Button' import {URLs, getCSRFToken} from './Auth' import FormControl from '@mui/material/FormControl'; import Input from '@mui/material/Input'; import InputLabel from '@mui/material/InputLabel'; import { Wait, StopWait, Msg } from './Waiter'; function onConfirmReset(key, container){ var form_data = new FormData() var password1 = document.querySelector('#password1').value var password2 = document.querySelector('#password2').value if (password2 !== password1) { Msg('Password does not match.', 'error') return } form_data.append('key', key) form_data.append('password', password1) axios.post(URLs.RESET_PASSWORD, form_data, { headers: { "accept": "application/json", "X-CSRFToken": getCSRFToken(), 'Content-Type': "application/json", } }).then((data) => { Msg('You have successfully changed your password.', 'success') createRoot(container.current).render( <Typography>You have successfully changed your password.</Typography> ) }).catch((err) => { if (err.response.status === 401){ Msg('You have successfully changed your password.', 'success') createRoot(container.current).render( <Box className="flex flex-col gap-6 items-center justify-between"> <Typography>You have successfully changed your password.</Typography> </Box> ) }else{ var errors = err.response.data.errors const lines = [] errors.forEach((error) =>{ lines.push(<Typography>{error.message}</Typography>) }) createRoot(container.current).render( <Box className="flex flex-col gap-6 items-center justify-between"> {lines} </Box> ) } }); } export default function PasswordReset(){ const key = document.querySelector('#password-reset-block').dataset.key const containerRef = React.useRef(null) return( <Box ref={containerRef} className="w-full h-full content-center"> <Box className="flex flex-col gap-6 items-center justify-between"> <Typography>To finish password reset procedure fill the form bellow</Typography> <FormControl style={{maxWidth: 'fit-content'}}> <InputLabel htmlFor="password1">Your new password</InputLabel> <Input type='password' id="password1"/> </FormControl> <FormControl style={{maxWidth: 'fit-content'}}> <InputLabel htmlFor="password2">Your new password(again)</InputLabel> <Input type='password' id="password2"/> </FormControl> <Button onClick={()=>{onConfirmReset(key, containerRef)}} variant='text'>proceed</Button> </Box> </Box> ) } const password_reset_container = document.getElementById('password-reset-block') if (password_reset_container){ const password_reset_root = createRoot(password_reset_container); password_reset_root.render(<PasswordReset></PasswordReset>); }
    Это ещё не всё. Потребуется ещё создание html-шаблона на стороне сервера и добавление соответствующих путей в urls.py

    Форма и запрос на подтверждение почты

    Так же моя система аутентификации поддерживает проверку почты на действительность. Пока смысла в этом особо нет, но в будущем это будет вполне полезно если я, например, хочу знать действительный ли это адресс электронной почты или нет.
    Мы должны отправить PUT-запрос по адресу /_allauth/browser/v1/account/email или URLs.EMAIL.
    function onRequestConfirmEmail(req, email){ Wait() var form_data = new FormData() form_data.append('email', email) axios.put(URLs.EMAIL, form_data, { headers: { "accept": "application/json", "X-CSRFToken": getCSRFToken(), 'Content-Type': "application/json", } }).then( data =>{ StopWait(`Successfully send an email to ${email}`, 'success') }).catch( err =>{ if (err.response.status !== 403){ StopWait(`${err.response.data.errors[0].message}`, 'error') } }) }
    На указанную почту придёт письмо с сылкой на страницу с ключом. Перейдя на данную страницу пользователь увидит следующее:
    Где нужно только нажать на кнопку. Оптравкой формы подтверждения занимается вот эта функция:
    function onConfirmEmail(key, container){ var form_data = new FormData() form_data.append('key', key) axios.post(URLs.VERIFY_EMAIL, form_data, { headers: { "accept": "application/json", "X-CSRFToken": getCSRFToken(), 'Content-Type': "application/json", } }).then((data) => { createRoot(container.current).render( <Typography>You successfully verified your email.</Typography> ) }).catch((err) => { if (err.response.status === 401){ createRoot(container.current).render( <Box className="flex flex-col gap-6 items-center justify-between"> <Typography>You successfully verified your email.</Typography> </Box> ) } var errors = err.response.data.errors const lines = [] errors.forEach((error) =>{ lines.push(<Typography>{error.message}</Typography>) }) createRoot(container.current).render( <Box className="flex flex-col gap-6 items-center justify-between"> {lines} </Box> ) }); }
    В данном случае я не делал разделение на on* и fetch* функции ибо это отдельный компонент и компонент довольно маленький. Помести данную функцию в ./components/EmailVerify.js
    Она отправляет ключ на сервер для подтверждения почты и возвращает соответствующее сообщение о статусе верификации.
    Ровно как и в случае со сбросом пароля, я создал отдельный компонент и подключил его в index.js:
    import Header from './components/Header'; import Footer from './components/Footer'; import AppSettings from './components/AppSettings'; import AppUtils from './components/AppUtils'; import AppActions from './components/AppActions'; import AppQueries from './components/AppQueries'; import Message from './components/Msg'; import Hint from './components/Tutorial'; import Auth from './components/Auth'; import EmailVerify from './components/EmailVerify'; import PasswordReset from './components/PasswordReset';
    А вот и полный код компонента EmailVerify:
    import * as React from 'react'; import axios from "axios"; import { createRoot } from 'react-dom/client'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Button from '@mui/material/Button' import {URLs, getCSRFToken} from './Auth' import { Wait, StopWait, Msg } from './Waiter'; function onConfirmEmail(key, container){ var form_data = new FormData() form_data.append('key', key) axios.post(URLs.VERIFY_EMAIL, form_data, { headers: { "accept": "application/json", "X-CSRFToken": getCSRFToken(), 'Content-Type': "application/json", } }).then((data) => { createRoot(container.current).render( <Typography>You successfully verified your email.</Typography> ) }).catch((err) => { if (err.response.status === 401){ createRoot(container.current).render( <Box className="flex flex-col gap-6 items-center justify-between"> <Typography>You successfully verified your email.</Typography> </Box> ) } var errors = err.response.data.errors const lines = [] errors.forEach((error) =>{ lines.push(<Typography>{error.message}</Typography>) }) createRoot(container.current).render( <Box className="flex flex-col gap-6 items-center justify-between"> {lines} </Box> ) }); } export default function EmailVerify(){ const key = document.querySelector('#email-verify-block').dataset.key const containerRef = React.useRef(null) return( <Box ref={containerRef} className="w-full h-full content-center"> <Box className="flex flex-col gap-6 items-center justify-between"> <Typography>To finish email verification click the button below</Typography> <Button onClick={()=>{onConfirmEmail(key, containerRef)}} variant='text'>proceed</Button> </Box> </Box> ) } const email_verify_container = document.getElementById('email-verify-block') if (email_verify_container){ const email_verify_root = createRoot(email_verify_container); email_verify_root.render(<EmailVerify></EmailVerify>); }
    Это ещё не всё. Потребуется ещё создание html-шаблона на стороне сервера и добавление соответствующих путей в urls.py

    Форма и запрос на выход

    И наконец, выход из текущей сессии. Делается DELETE запрос по адресу /_allauth/browser/v1/auth/session или URLs.LOGOUT. Будет возвращена ошибка с кодом 401, поэтому выход делаем в catch блоке.
    function onSignout(req){ Wait() axios.delete(URLs.LOGOUT, { headers: { "accept": "application/json", "X-CSRFToken": getCSRFToken(), 'Content-Type': "application/json", } }).catch(error =>{ StopWait('You have beed log out.') updateSession() req.setModal(false) }) }

    Настраиваем маршруты и представления для подтверждения почты и сброса пароля

    Мы почти закончили. Осталось только настроить Бэкенд для подтверждения почты и изменения пароля. Создай новое django-приложение, если не знаешь как, смотри тут. Назови его Authentication ну и конечно подключи его к проекту сайта.
    Дальше создай два шаблона в templates/Authentication, email_verify.html и password_reset.html
    Содержание email_verify.html:
    {% extends "Frontend/base.html" %} {% block main %} <div id="email-verify-block" class="w-full h-full" data-key="{{key}}"> </div> {% endblock%}
    Содержание password_reset.html:
    {% extends "Frontend/base.html" %} {% block main %} <div id="password-reset-block" class="w-full h-full" data-key="{{key}}"> </div> {% endblock%}
    Как видишь, они почти идентичны. Впринципе, мог и один общий шаблон сделать, но подумал что возможно в будущем может потребоваться их как-то развивать по отдельности. Теперь добавим два маршрута в Authentication/urls.py:
    from django.urls import path from .views import email_verify, password_reset urlpatterns = [ path('email-verify/<str:key>', email_verify, name='email_verify'), path('password-reset/<str:key>', password_reset, name='password_reset'), ]
    Заметь пути совпадают с теми что я указывал в settings.py ранее. Осталось только создать соответствующие представления для данных шаблонов, email_verify и password_reset:
    from django.shortcuts import render def email_verify(request, key): context = { 'key': key, } return render(request, 'Authentication/email_verify.html', context) def password_reset(request, key): context = { 'key': key, } return render(request, 'Authentication/password_reset.html', context)
    Здесь всё довольно прямолинейно, получаем ключ из URL, сохраняем его в контекст при рендеринге и вставляем key в качестве значения атрибута data-key в контейнерах. Дальше этот атрибут будет использовать Реакт для отправки обратных запросов.

    Заключение

    Я закончил добавлять систему аутентификации и регистрации моих гостей на сайте. Конечно, можно ещё добавить вход и регистрацию через социальные сети или на основе отправляемых ключей подтверждения. Всё это будет позже и возможно уже в других проектах, но точно не в этой статье, она и так получилась слишком большой.
    Текущую и полную версию сайта-проекта ты можешь скачать здесь.
    В следующей серии мы займёмся переводами нашего сайта на другие языки, ну а пока, до скорых встреч. ( ̄︶ ̄)↗ 


    Не забудь поделиться, лайкнуть и оставить комментарий)

    Комментарии

    (0)

    captcha
    Отправить
    ЗАГРУЗКА ...
    Сейчас тут пусто. Буть первым (o゚v゚)ノ

    Другое

    Похожие статьи


    Серия статей о создании и продвижении SearchResultParser | Tim the webmaster

    Часы
    16.07.2024
    /
    Часы
    05.10.2025
    Глазик
    612
    Сердечки
    0
    Соединённые точки
    0
    Соединённые точки
    2
    Соединённые точки
    0
    Это статья-вступление и статья-навигатор по проекту/веб-инструменту SearchResultParser. Чему можно будет научиться и для кого эта серия статей

    Создание базового(пустого) React приложения

    Часы
    19.07.2024
    /
    Часы
    08.03.2026
    Глазик
    432
    Сердечки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    Туториал по тому, как создать реакт приложение/проекта с использованием npm. С подробным описанием комманд и процессов.

    Как (и можно ли) объединить React JS с Django проектом ч. 1

    Часы
    03.08.2024
    /
    Часы
    08.03.2026
    Глазик
    1557
    Сердечки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    В этой статье ты узнаешь как создать Django приложение и настроить его для работы с React JS чтобы получилось полноценное фулстак приложение. Так же в статье приведено видео-туториал и скачиваемые …

    Разработка фронтенда сайта на React с бэкендом на Django | SearchResultParser ч. 2

    Часы
    16.08.2024
    /
    Часы
    02.10.2025
    Глазик
    1765
    Сердечки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    Показываю и рассказываю о том как разработать фронтен для сайта на Реакте с бэкендом на django. Использую MaterialUI и TailwindCSS, с исходным кодом и комментариями.

    Использованные термины


    Релевантные вопросы