Пользователь в web-приложении на Python и Starlette

programming

Опубликован:
2025-01-03T07:14:31.383447Z
Отредактирован:
2025-01-03T07:22:20.318622Z
Статус:
публичный
16
0
0

Пользователь в web-приложении на Python и Starlette, процесс разработки которого мы сосредоточенно исследуем в этом цикле статей, является главным действующим лицом и объектом, вокруг которого строится модель хранения всех остальных данных. Пользовательские данные в SQL базе данных я предполагаю хранить в нескольких связанных соответствующими отношениями таблицах. Пришло время построить таблицы для хранения данных учётной записи пользователя и аккаунта пользователя, а так же создать удобные и эффективные инструменты для заполнения этих таблиц средствами написанной на Python программы. Приступаем к очередному этапу разработки...

SQL

Поскольку данные, которыми оперирует web-приложение в процессе своей работы, я планирую хранить в SQL базе данных — PostgreSQL, при разработке программы мне придётся последовательно и в разное время воспроизвести целую серию SQL-скриптов. Эти файлы нужно где-то упорядоченно хранить в рамках структуры каталогов приложения. Именно для этой цели создаю в корневом каталоге приложения, рядом с базовым каталогом ещё один вложенный каталог. Даю ему имя sql.

$ mkdir sql

Создаю с помощью текстового редактора Vim новый файл с именем auth.sql — скрипт SQL таблиц для системы аутентификации пользователей.

$ vim sql/auth.sql

Создаю в этом файле таблицу для хранения данных пользователей приложения, в этой таблице я планирую хранить следующие данные:

  • id — уникальный числовой идентификатор пользователя, с помощью которого впоследствии я свяжу эту таблицу с другими таблицами SQL базы данных;

  • username — уникальное имя пользователя, состоящее из текстовой строки длиной до 16 символов включительно;

  • ugroup — имя пользовательской группы, участником которой пользователь является, хранит текстовую строку до 16 символов включительно;

  • weight — вес пользователя, хранит целое число от 0 до 255;

  • registered — текстовая строка в формате временной метки, хранит дату и время момента регистрации пользователя в сервисе;

  • last_visit — текстовая строка в формате временной метки, хранит дату и время последнего визита пользователя в сервис;

  • password_hash — текстовая строка длиной до 128 символов, хранит пароль пользователя в зашифрованном виде;

  • description — текстовая строка длиной до 500 символов, хранит описание блога пользователя, может иметь значение NULL;

  • last_published — текстовая строка в формате временной метки, хранит дату и время момента публикации крайней на данный момент статьи в блоге пользователя, может иметь значение NULL.

Пишу в этот файл следующий код:

CREATE TABLE users (
    id             serial PRIMARY KEY,
    username       varchar(16) UNIQUE NOT NULL,
    ugroup         varchar(16),
    weight         smallint,
    registered     timestamp,
    last_visit     timestamp,
    password_hash  varchar(128),
    description    varchar(500) DEFAULT NULL,
    last_published timestamp DEFAULT NULL
);

Каждому посетителю сайта я планирую дать возможность самостоятельной регистрации в сервисе. Процесс регистрации будет происходить в два шага. На первом шаге пользователь вводит в специальной форме свой адрес электронной почты, отправляет эту форму в сервис. Сервис, получив от пользователя эти данные, создаёт аккаунт пользователя в своей базе данных и высылает на полученный от пользователя адрес регистрационную ссылку. На втором шаге пользователь следует по регистрационной ссылке и создаёт свою учётную запись, привязанную к созданному ранее, на первом шаге аккаунту. Здесь есть один тонкий момент. Учётной записью я буду считать запись в таблице users, которую только что создал. А аккаунт пользователя будет привязан в адресу электронной почты и храниться в другой SQL таблице, которая будет связана с таблицей users через числовой идентификатор пользователя — users.id. При этом аккаунт может существовать без пользователя, а вот пользователь без аккаунта не может быть создан.

Второй таблице я даю имя accounts, в ней планирую хранить следующие данные:

  • id — уникальный числовой идентификатор аккаунта пользователя;

  • address — адрес электронной почты пользователя, хранит текстовую строку до 128 символов включительно;

  • swap — специальное поле, предназначенное для смены адреса электронной почты, хранит текстовую строку до 128 символов включительно;

  • requested — текстовая строка в формате временной метки, хранит дату и время последнего запроса на регистрацию этого аккаунта;

  • user_id — ссылка на уникальный числовой идентификатор пользователя из таблицы users, с помощью которого аккаунт будет связан с пользователем, один пользователь — один аккаунт.

Дописываю в открытый в текстовом редакторе файл следующий код:

CREATE TABLE accounts (
    id        serial PRIMARY KEY,
    address   varchar(128) UNIQUE,
    swap      varchar(128),
    requested timestamp,
    user_id   integer REFERENCES users(id) UNIQUE
);

В этой таблице три столбца имеют уникальные данные: id, address, user_id. Это ограничение не позволит на одном аккаунте создать две пользовательские записи.

Сохраняю изменения в файл, покидаю текстовый редактор. Теперь мне предстоит на основе этого простого SQL скрипта сформировать структуру базы данных.

База данных

База данных web-приложения уже была создана в одном из предыдущих выпусков этого цикла, и на данный момент в ней уже хранятся данные, базу необходимо воссоздать, то есть удалить прежнюю версию и создать новую. Прежнюю версию удаляю следующей командой:

$ dropdb websitedev

На эту команду терминал не отзовётся никаким выхлопом, просто удалит текущую базу данных с именем websitedev. На её месте создаю новую базу данных с таким же именем — websitedev.

$ createdb websitedev

Воспроизвожу в только что созданную базу данных все таблицы из только что созданного SQL скрипта auth.sql.

$ psql -d websitedev -f sql/auth.sql

После этого в базу можно войти с помощью стандартной для PostgreSQL консоли psql.

$ psql -d websitedev

Проверить состав базы данных в консоли можно при помощи команды \dt, состав таблиц базы данных можно посмотреть с помощью команды \d имя_таблицы. Кроме этого можно попробовать ввести элементарные SQL запросы, дабы убедиться, что в существующих таблицах пока нет записей. Вот как выглядит окно моего терминала после всех предпринятых действий.

InTHx1hxPn.png

Покинуть консоль psql можно с помощью команды \q. Таблицы для хранения учётных записей пользователей в базе данных готовы, и теперь мне нужно позаботиться об удобном заполнении этих таблиц посредством написанной на Python программы.

Виртуальное окружение

В ближайшей перспективе мне предстоит разработать программные средства для создания учётных записей пользователей в только что созданной базе данных. При этом, разработанная программа будет получать от пользователя его данные, в том числе пароль и адрес электронной почты. Контролировать, что именно вводит пользователь, программист не может, но предусмотреть проверку введённых данных по определённым критериям ему вполне по силам. Для проверки электронного адреса логично будет использовать готовую библиотеку — validate_email. Пароль пользователя я собираюсь хранить в виде зашифрованного хеша, и для его создания мне потребуются инструменты специализированной библиотеки passlib. Обе зависимости необходимо установить в виртуальное окружение приложения.

$ pip install passlib validate_email

ezt4zVAcVt.png

Как видно на снимке экрана, оба пакета успешно установлены. Изменения виртуального окружения необходимо отразить в хранилище Git, чтобы иметь возможность при развёртывании приложения на сервер это виртуальное окружение восстановить. Исполняю ещё одну команду.

$ pip freeze > requirements.txt 

Теперь можно смело приступать к программированию инструментов для создания учётных записей пользователей.

Создание объектов БД

Итак, мне необходимы инструменты для создания записей в только что созданные таблицы базы данных users и accounts. С помощью текстового редактора Vim в каталоге auth создаю ещё один модуль с именем pg.py следующей командой.

$ vim webapp/auth/pg.py

В этом модуле мне потребуется инструмент passlib, с помощью которого можно вычислить зашифрованный хеш пользовательского пароля. Подключаю.

from passlib.hash import pbkdf2_sha256

Кроме этого, мне потребуется имя и вес пользовательской группы, участником которой будет создаваемый пользователь. Эти данные хранятся в классе groups, его я разработал на предыдущем этапе проектирования. Подключаю.

from .attri import groups

Пользовательский аккаунт при самостоятельной регистрации пользователя будет создаваться последовательно. На первом шаге регистрации будет создана запись в таблице accounts с полученным от пользователя адресом электронной почты, на втором шаге будет создана запись в таблице users с полученным от пользователя именем и паролем, а затем запись в таблице accounts будет отредактирована и привязана к записи в таблице users с помощью столбца user_id. Таков план.

Создавать учётную запись пользователя в таблице users я планирую вот такой простой функцией.

async def create_user_record(conn, username, passwd, group, now):
    await conn.execute(
        '''INSERT INTO users
           (username, ugroup, weight, registered, last_visit, password_hash)
           VALUES ($1, $2, $3, $4, $5, $6)''',
        username, group, groups.weigh(group),
        now, now, pbkdf2_sha256.hash(passwd))
    return await conn.fetchval(
        'SELECT id FROM users WHERE username = $1', username)

Здесь следует обратить внимание на аргументы, которые я передаю функции create_user_record. Первый аргумент (conn) — это подключение к базе данных, созданное с помощью разработанной ранее функции get_conn. Далее следуют полученные от пользователя данные (username и passwd), имя группы (group) и текущие дата и время (now), выраженные с помощью библиотечных инструментов из модуля datetime стандартной библиотеки Python.

В теле функции create_user_record я создаю на основе полученных в вызове функции параметров учётную запись пользователя. После того, как учётная запись создана, я должен получить значение её идентификатор (id), так как идентификаторы создаются автоматически и значения им присваиваются последовательно из заданного ряда, в данном случае это возрастающая последовательность целых чисел. Это значение функция и должна вернуть, оно будет использовано при создании или редактировании аккаунта.

С помощью второй функции я буду редактировать пользовательский аккаунт, если он существует, или создавать его вновь, если нет. Даю этой функции имя update_account, вот её код.

async def update_account(conn, address, uid, now):
    account = await conn.fetchrow(
        'SELECT * FROM accounts WHERE address = $1', address)
    if account:
        await conn.execute(
            '''UPDATE accounts
                 SET user_id = $1, requested = $2 WHERE address = $3''',
            uid, now, address)
    else:
        await conn.execute(
            '''INSERT INTO accounts (address, requested, user_id)
                 VALUES ($1, $2, $3)''', address, now, uid)

В аргументах у этой функции подключение к базе данных (conn), полученный от пользователя адрес электронной почты (address), идентификатор пользователя (uid), которому принадлежит аккаунт и текущие дата и время.

В логике этой функции я проверяю, существует ли в таблице accounts запись с переданным в параметрах вызова адресом электронной почты. Если запись существует, обновляю её, добавляя соответствующие значения в поле user_id и requested. Если запись не существует, создаю её на основе переданных в параметрах вызова функции значений.

Как видно из предложенного кода, в логике обеих только что созданных функций я использую прямые текстовые SQL запросы, адресованные посредством инструментов asyncpg прямо в базу данных PostgreSQL. Открою небольшой секрет, все эти текстовые запросы SELECT, INSERT и UPDATE я не вынул из глубин своего бурного воображения в готовом виде, а сконструировал и отладил в консоли psql методом проб и ошибок до получения нужного результата. Уже в следующем выпуске этого цикла статей я покажу использование этих функций для создания учётной записи суперпользователя web-приложения. А все цели этой демонстрации достигнуты.

Подводим промежуточный итог

В базе данных web приложения на Python и Starlette, разработку которого мы последовательно исследуем в этом цикле статей появились таблицы для хранения пользовательских данных. Кроме этого, в приложении есть все инструменты для корректного добавления в эти таблицы учётных записей новых пользователей приложения. В следующем выпуске этого блога я покажу практическое использование разработанного только что кода в рамках консольной программы, с помощью которой планирую создать суперпользователя этого web-приложения. Продолжение следует...

Напоминаю, что продолжение этого цикла статей возможно, только если в блоге будет активность. Ваши посещения, подписки, лайки, комментарии и донаты имеют большое значение и качественно мотивируют автора, заряжают его соответствующей жизненной силой. Не оставайтесь в стороне, будет интересно...

Метки: