Следующим шагом в разработке бэкенда сервиса сокращения ссылок становится важный аспект — административная панель.
В обсуждениях FastAPI против Django довольно часто звучит аргумент вроде «зато у нас админка из коробки», и тут действительно есть разница подходов. Django — монолитный фреймворк со встроенными решениями, включая готовую админку. FastAPI же создавался как микрофреймворк, где по умолчанию есть только инструменты для построения API, а всё остальное добавляется при необходимости через сторонние библиотеки.
Тем не менее, экосистема вокруг FastAPI уже достаточно развилась, и вариантов админ-панелей более чем достаточно. Основные на сегодняшний день:
- Starlette Admin — современная админка, построенная поверх Starlette (того же ASGI-фреймворка, на котором работает FastAPI). Гибкая, быстрая, поддерживает кастомные модели и авторизацию.
- CRUDAdmin — административная панель с акцентом на автоматическую генерацию CRUD-интерфейсов. Требует отдельную базу для хранения администраторов и сильнее навязывает свою структуру.
- SQLAdmin — лёгкое решение, завязанное на SQLAlchemy. Простое в установке, но менее гибкое по сравнению с Starlette Admin и рассчитано скорее на быстрый старт, чем на глубокую кастомизацию.
Можно ещё встретить упоминания FastAPI Admin и FastAdmin, но первый давно не обновлялся и фактически заброшен, а второй развивается медленно и пока не рекомендуется для продакшн-использования.
Важно: эта статья — часть серии «Веб-сервис на FastAPI». Она опирается на код и архитектурные решения, описанные в предыдущих материалах. Если вы попали сюда напрямую, некоторые места могут показаться неполными или слишком короткими — это нормально, просто в рамках цикла мы практически не повторяем уже разобранное.
Почему Starlette Admin
Для этого проекта я решил выбрать Starlette Admin — библиотеку, которая предлагает хорошее сочетание производительности, гибкости и простоты интеграции.
SQLAdmin я уже использовал ранее в проекте Napkin Random Bot, причём в связке с Keycloak для авторизации. Работает неплохо, но хотелось попробовать что-то новое.
CRUDAdmin, с другой стороны, предлагает интересные возможности, но требует отдельной базы данных (например, SQLite) для хранения данных о администраторах. Такой подход не всегда удобен, особенно если хочется, чтобы админка была частью существующей системы авторизации и работала на тех же моделях пользователей, что и API.
Starlette Admin в этом плане выглядит оптимальным решением: библиотека активно развивается, имеет подробную документацию и открытый репозиторий на GitHub, а главное — позволяет интегрировать админку в текущую архитектуру без создания «второй сущности пользователей».
Нюансы готовых решений
Готовые админ-панели не всегда покрывают все возможные требования проекта.
Одним достаточно просто получить доступ к данным в базе и, при необходимости, изменять записи. Другие хотят видеть дашборды, графики, быстрые действия по сущностям или, например, каскадные фильтры и массовые операции. И вот здесь возникает нюанс: не все решения позволяют легко адаптироваться под такие задачи. Иногда добавление даже небольшой кастомизации превращается в боль, особенно если библиотека навязывает собственный способ организации данных или ограничивает в расширении функционала.
Важно помнить, что админ-панель — это ещё одна зависимость внутри проекта. Она имеет собственную архитектуру, ограничения и уровень допускаемой кастомизации. Если библиотека не рассчитана на гибкие изменения, со временем она может начать мешать развитию проекта, а не помогать.
С другой стороны, создание полноценной админки с нуля — будь то HTML и JavaScript или с использованием фреймворков — требует времени, внимательного проектирования интерфейсов и, по сути, превращается в отдельный мини-фронтенд. В большинстве проектов, особенно на ранних этапах, это неоправданная роскошь.
Поэтому готовые решения — отличный выбор, если цель админки понятна: минимальный, но удобный контроль над данными без необходимости тратить ресурсы на разработку собственного интерфейса.
План работ
В рамках этой части статьи мы реализуем следующее:
- Создадим отдельный пакет для админки, чтобы не смешивать логику панели управления с основной частью приложения.
- Добавим представления (view) для моделей
User
иLink
, чтобы их можно было просматривать и редактировать через панель. - Немного доработаем модуль
auth
, чтобы админка могла использовать текущую систему авторизации, а не создавать свою. - Подключим авторизацию в админке на основе существующей логики, чтобы вход в админ-панель происходил через ту же систему, что и для API.
После этого у нас появится полноценная административная панель, интегрированная в проект без «вторых» сущностей пользователей и без отдельных баз данных только ради админки.
Открываем IDE — поехали!
Подготовка
Прежде чем подключать админку, внесём небольшие изменения в текущую структуру проекта. К счастью, правок немного, и они логичные — мы просто приведём инфраструктуру к состоянию, когда админка сможет использовать уже существующие ресурсы проекта.
Установка Starlette Admin
Для начала установим саму библиотеку:
poetry add starlette-admin
Если проект у вас не на Poetry — можно использовать
uv add
илиpip install starlette-admin
. Принцип тот же.
Получение движка БД
Админка работает напрямую с базой данных и для инициализации ей нужен движок БД (AsyncEngine
).
В нашем проекте в модуле db_dependency.py
уже есть класс DBDependency
, который создаёт движок и фабрику сессий, но наружу он сейчас отдаёт только сессии.
Добавим в класс новое property
поле db_engine
, которое будет возвращать существующий движок, а не создавать новый.
@property
def db_engine(self) -> AsyncEngine:
return self._engine
Теперь из любого места в проекте мы можем получить тот самый движок, который уже используется в приложении.
Чтобы удобнее было вызывать его снаружи, добавим отдельную функцию:
def get_db_engine() -> AsyncEngine:
return DBDependency().db_engine
Так мы не создаём отдельный движок ради админки, а используем общий, что важно для согласованной работы транзакций и соединений с БД.
Полный код с учётом изменений:
from sqlalchemy.ext.asyncio import (
AsyncEngine,
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from lkeep.core.settings import settings
class DBDependency:
def __init__(self) -> None:
self._engine = create_async_engine(url=settings.db_settings.db_url, echo=settings.db_settings.db_echo)
self._session_factory = async_sessionmaker(bind=self._engine, expire_on_commit=False, autocommit=False)
@property
def db_session(self) -> async_sessionmaker[AsyncSession]:
return self._session_factory
@property
def db_engine(self) -> AsyncEngine:
return self._engine
def get_db_engine() -> AsyncEngine:
return DBDependency().db_engine
Схема данных и CRUD-получения пользователя с ролью
Сейчас в классе UserManager
есть метод get_user_by_email
, который возвращает только id
, email
и hashed_password
. Для API этого достаточно — при авторизации мы просто проверяем пароль и выдаём токен, не делая различий между обычным пользователем и администратором.
Однако для админки важно понимать, имеет ли пользователь права суперпользователя, иначе мы не сможем корректно разграничить доступ. Добавлять поле is_superuser
в основной метод не хочется — он используется в логике API, и тащить туда админскую часть не очень чисто архитектурно.
Поэтому сделаем отдельный путь получения пользователя специально для админки.
Создаём отдельную схему
Открываем auth/schemas.py
и добавляем схему GetUserForAdmin
, унаследованную от GetUserWithIDAndEmail
. Она повторяет готовую структуру, но расширяет её полем is_superuser
.
class GetUserForAdmin(GetUserWithIDAndEmail):
is_superuser: bool
Добавляем метод в UserManager
Теперь в UserManager
создадим отдельный метод — get_user_by_email_for_admin
. Он похож на стандартный get_user_by_email
, но дополнительно выбирает поле is_superuser
и возвращает расширенную схему.
async def get_user_by_email_for_admin(self, email: str) -> GetUserForAdmin | None:
async with self.db.db_session() as session:
query = select(self.model.id, self.model.email, self.model.hashed_password, self.model.is_superuser).where(
self.model.email email
)
result = await session.execute(query)
user = result.mappings().first()
if user:
return GetUserForAdmin(**user)
return None
Такой подход даёт нам чистое разделение обязанностей: API остаётся независимым от админской логики, а админка получает ровно то, что нужно.
Инициализация админки
Переходим от теории к практике — подключаем административную панель к приложению.
Создаём базу для админки
Внутри пакета apps
создаём новый пакет admin
. Это позволит изолировать всё, что касается административной панели, от остальной логики приложения.
Внутри apps/admin
создаём файл admin_base.py
. Именно в нём будет происходить инициализация админки.
Функция инициализации
В admin_base.py
объявляем функцию setup_admin(app: FastAPI)
. Она будет принимать уже созданный экземпляр FastAPI и подключать к нему административную панель. Функция ничего не возвращает — она просто «подмешивает» админку в приложение.
Внутри создаём экземпляр Admin
и указываем ему параметры:
engine
— движок БД. Мы передаём уже существующий движок черезget_db_engine()
, а не создаём новый — благодаря этому админка работает в одном соединении с основной частью приложения.title
— название панели, которое будет отображаться в интерфейсе.base_url
— опционально. По умолчанию админка доступна по/admin
, но при желании можно поменять, например, на/dashboard
или/control
.
Затем вызываем .mount_to(app)
— это ключевой шаг, который регистрирует все необходимые маршруты и подключает панели, формы и статические ресурсы, необходимые для работы админки.
from fastapi import FastAPI
from starlette_admin.contrib.sqla import Admin
from lkeep.core.core_dependency.db_dependency import get_db_engine
def setup_admin(app: FastAPI) -> None:
admin = Admin(engine=get_db_engine(), title="Lkeep Admin")
admin.mount_to(app=app)
Подключаем в main.py
Теперь осталось лишь один раз вызвать setup_admin()
при запуске приложения.
Открываем main.py
и после подключения middleware (app.add_middleware(...)
) добавляем:
from lkeep.apps.admin.admin_base import setup_admin
setup_admin(app)
После этого запускаем приложение:
poetry run app
Переходим по адресу: http://localhost:8000/admin
Если всё подключено корректно — вы увидите интерфейс Starlette Admin. Пока он пустой, потому что мы ещё не объявили модели — этим займёмся дальше.

Представления моделей
Чтобы модель появилась в админке, недостаточно просто добавить её в проект — нужно явно описать, как эта модель должна отображаться. Именно для этого используются представления (views).
Представление — это класс, который определяет:
- какие поля показывать в списке объектов;
- какие поля доступны при создании и редактировании;
- порядок сортировки;
- фильтры и поиск;
- дополнительные действия (например, "активировать", "деактивировать");
- и даже то, как именно объект создаётся или сохраняется (это важно, если требуется хэширование пароля или привязка владельца).
Два способа описания представления
Есть два подхода:
Использовать стандартный ModelView
ModelView(<SQLAlchemy модель>, pydantic_model=<Pydantic-схема>)
Подходит, если модель простая, не требует кастомной логики, и достаточно базового CRUD-интерфейса.
Создать собственный класс-представление
class UserView(ModelView):
...
UserView(<SQLAlchemy модель>)
Такой подход используется, когда нужно тонко управлять поведением: менять валидацию, автоматически подставлять значения, хэшировать пароль перед сохранением и т. д.
Какой подход используем мы
В проекте Lkeep
у нас как минимум два особых кейса:
- Пользователю при создании нужно хэшировать пароль, а не сохранять его в открытом виде.
- Для модели ссылки (
Link
) нужно автоматически подставлять владельца, чтобы ссылка была привязана к конкретному пользователю.
Поэтому стандартного ModelView
будет недостаточно — нам нужно написать кастомные представления для моделей User
и Link
.
Представление модели User
Для начала создадим отдельный пакет для представлений.
Внутри пакета admin
создаём новый пакет views
.
В нём создаём файл user_view.py
, где и будет находиться представление для пользователей.
Определяем класс представления
Создаём класс UserView
, унаследованный от ModelView
.
Наследование даёт нам доступ ко всем стандартным возможностям Starlette Admin — отображению таблиц, форм, фильтрам, сортировке и действиям CRUD (создание, редактирование, удаление).
Теперь пропишем основные настройки класса.
Основные поля конфигурации представления
Внутри класса задаются статические атрибуты, которые управляют поведением интерфейса:
fields
— список доступных полей. Здесь указываются те атрибуты модели, которые будут отображаться, а также кастомные поля, созданные специально для админки (как в нашем случае с полемpassword
, которого нет в модели).exclude_fields_from_edit
иexclude_fields_from_create
— эти списки позволяют скрывать поля, которые не должны быть изменяемыми. Например,created_at
иupdated_at
заполняются автоматически, и нет смысла показывать их при создании или редактировании объекта.sortable_fields
— разрешённые поля для сортировки. Это ограничивает административный интерфейс и предотвращает попытку отсортировать по полю, по которому сортировка не имеет смысла или технически невозможна.fields_default_sort
— определяет начальный порядок отображения объектов. Если мы не зададим сортировку, Starlette Admin покажет записи в порядке их первичного ключа, что не всегда удобно. Мы сразу укажем, что хотим видеть свежие записи первыми.searchable_fields
— поля, по которым будет работать встроенный поиск. Администратор сможет ввести текст, и админка построит SQL-запрос с фильтрацией.page_size
— сколько записей показывать на одной странице.page_size_options
— список вариантов, которые можно выбрать в интерфейсе. Важный нюанс — значение-1
значит «показать все записи». Это удобно, если вам нужно сразу просмотреть все данные таблицы.
Код класса с полями:
from starlette_admin import PasswordField
from starlette_admin.contrib.sqla import ModelView
class UserView(ModelView):
fields = [
"id",
"email",
PasswordField(
name="password",
label="Password",
required=True,
exclude_from_list=True,
exclude_from_detail=True,
exclude_from_edit=True,
),
"is_active",
"is_verified",
"is_superuser",
"created_at",
"updated_at",
]
exclude_fields_from_edit = ["created_at", "updated_at"]
exclude_fields_from_create = ["created_at", "updated_at"]
sortable_fields = ["email", "created_at"]
fields_default_sort = [("created_at", True)]
searchable_fields = ["email"]
page_size = 20
page_size_options = [5, 10, 25, 50, -1]
Особый случай — поле password
Так как в нашей модели нет поля password
, а есть только hashed_password
, мы добавляем виртуальное поле с использованием PasswordField
.
Starlette Admin подставит для него специальный интерфейс с полем ввода пароля и автоматически скроет его содержимое (как в обычных формах авторизации). Это поле не попадёт напрямую в БД, но мы сможем перехватить его в методе before_create
и преобразовать в хэш.
Важно понимать: если добавить hashed_password
напрямую, админка будет показывать хэш в интерфейсе, а это плохо с точки зрения безопасности.
В аргументы класса передаём следующие значения:
name
- название поля:password
.label
- отображаемое название поля:Password
.required
- делаем его обязательным к вводу.exclude_from_list
,exclude_from_detail
иexclude_from_edit
- исключаем его из отображения в общем списке, в подробностях и в редактировании.
Определение направления стандартной сортировки.
Список fields_default_sort
определяет, какие поля будут автоматически применяться для сортировки отображаемых объектов. Список принимает как просто строки, например, created_at
для сортировки по дате создания, так и кортежи, где первое значение строка, а вторая булево значение, определяющее порядок: ("created_at", True)
.
Есть два варианта сортировки:
ASC
- сортировка от старого к новому. Применяется по умолчанию или с указаниемFalse
.DESC
- сортировка от нового к старому. Применяется при указанииTrue
.
Переопределение создания объекта.
По умолчанию Starlette Admin при создании объекта просто берёт данные из формы и напрямую сохраняет их в модель. Для большинства простых сущностей это нормально, но в случае с пользователями такое поведение недопустимо — мы обязаны валидировать данные и хэшировать пароль вручную.
Для этого в ModelView
есть специальный хук — before_create
. Он вызывается перед сохранением объекта в базу и позволяет вмешаться в процесс.
Метод выглядит так:
async def before_create(self, request: Request, data: dict[str, Any], user: User):
...
request
— полный HTTP-запрос (можно извлечь куки, сессию, IP-адрес, при желании даже залогировать действия).data
— словарь с полями, которые были введены в форме.user
— ещё не сохранённый экземпляр моделиUser
, в который будут записаны значения.
Здесь важно понимать:
data
хранит то, что ввёл администратор, аuser
— это объект SQLAlchemy, который будет сохранён после выполнения всех хуков.
Шаг 1. Валидация email
Хотя в FastAPI мы уже валидируем email через Pydantic-схемы, админка обходит API-слой, поэтому валидацию нужно дублировать здесь вручную, чтобы не допустить сохранения некорректных данных.
Мы используем библиотеку email-validator
, которая бросает исключение EmailSyntaxError
, если строка не соответствует формату email. В случае ошибки вызываем FormValidationError
— это не просто исключение, а сигнал для админки подсветить поле с ошибкой в интерфейсе, что делает UX гораздо приятнее.
async def before_create(self, request: Request, data: dict[str, Any], user: User) -> None:
try:
validate_email(data["email"])
except EmailSyntaxError:
raise FormValidationError(errors={"email": "Invalid email"})
Шаг 2. Хэширование пароля
Данные формы приходят в виде словаря, и там есть ключ password
, который не существует в модели — его нет в БД, он чисто форменный.
Мы:
- Инициализируем
CryptContext
. - Извлекаем пароль через
data.pop("password")
. Используемpop
специально — это удаляет полеpassword
из словаря, чтобы Starlette Admin не пыталась сохранить его напрямую. - Записываем захэшированное значение в
user.hashed_password
.
Таким образом, в БД попадает только хэш, а чистый пароль нигде не сохраняется.
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
user.hashed_password = pwd_context.hash(data.pop("password"))
Полный код класса:
from typing import Any
from email_validator import EmailSyntaxError, validate_email
from passlib.context import CryptContext
from starlette.requests import Request
from starlette_admin import PasswordField
from starlette_admin.contrib.sqla import ModelView
from starlette_admin.exceptions import FormValidationError
from lkeep.database.models import User
class UserView(ModelView):
fields = [
"id",
"email",
PasswordField(
name="password",
label="Password",
required=True,
exclude_from_list=True,
exclude_from_detail=True,
exclude_from_edit=True,
),
"is_active",
"is_verified",
"is_superuser",
"created_at",
"updated_at",
]
exclude_fields_from_edit = ["created_at", "updated_at"]
exclude_fields_from_create = ["created_at", "updated_at"]
sortable_fields = ["email", "created_at"]
fields_default_sort = [("created_at", True)]
searchable_fields = ["email"]
page_size = 20
page_size_options = [5, 10, 25, 50, -1]
async def before_create(self, request: Request, data: dict[str, Any], user: User) -> None:
try:
validate_email(data["email"])
except EmailSyntaxError:
raise FormValidationError(errors={"email": "Invalid email"})
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
user.hashed_password = pwd_context.hash(data.pop("password"))
Представление модели Link
Теперь добавим в админку вторую главную сущность — ссылки.
В пакете apps/admin/views
создаём файл link_view.py
. Внутри объявляем класс LinkViewView
, унаследованный от ModelView
.
Особенность: внешний ключ owner_id
Модель Link
содержит поле owner_id
— это внешний ключ на пользователя. Starlette Admin не отображает такие поля автоматически, если нет явного отношения (relationship
) в модели SQLAlchemy.
Даже если добавить owner_id
в список fields
, админка попытается отрисовать обычное поле ввода, хотя это техническое поле и должно быть только для чтения.
Чтобы корректно отобразить его как текст, а не как редактор, мы используем StringField
. Это виртуальное поле, которое просто выводит значение из модели без попытки его редактировать.
Если бы в модели
Link
была настроена связьrelationship(User)
, можно было бы использоватьHasOne
, и тогда админка нарисовала бы выпадающий список с пользователями. Мы сознательно этого не делаем — в нашем сценарии ссылка всегда принадлежит текущему администратору.
fields = [
"id",
"full_link",
"short_link",
StringField("owner_id", label="owner_id", read_only=True),
]
Специальная логика при создании ссылки
По умолчанию Starlette Admin создаёт объект без связи с текущим пользователем, что нежелательно — владелец должен определяться автоматически, а не руками.
Для этого снова используется хук before_create
. Он предоставляет доступ к:
request
— где в полеrequest.state.user
(мы добавим это в авторизации позже) будет храниться текущий авторизованный администратор;data
— данные формы;link
— новый объектLink
, который ещё не сохранён.
В методе мы:
- Получаем текущего администратора:
admin_user = request.state.user
- Присваиваем ссылке владельца через
link.owner_id = admin_user["id"]
Это гарантирует, что каждая созданная ссылка будет привязана к тому администратору, кто её создал — без ручного выбора и риска ошибок.
Важно понимать
На данном этапеrequest.state.user
ещё не существует — мы подключим его позже, когда будем реализовывать авторизацию в админке. Сейчас просто закладываем логику, а механизм авторизации добавим дальше.
Код класса:
class LinkViewView(ModelView):
fields = [
"id",
"full_link",
"short_link",
StringField("owner_id", label="owner_id", read_only=True),
]
async def before_create(self, request: Request, data: dict[str, Any], link: Link):
admin_user = request.state.user
link.owner_id = admin_user["id"]
Подключение представлений
Теперь, когда у нас готовы классы UserView
и LinkViewView
, нужно подключить их к административной панели, чтобы Starlette Admin начала их учитывать при генерации интерфейса.
Для этого вернёмся в apps/admin/admin_base.py
и найдём функцию setup_admin
.
Между созданием экземпляра Admin
и вызовом mount_to()
необходимо зарегистрировать наши классы представлений через метод .add_view()
.
Важно: каждое представление привязывается к конкретной модели SQLAlchemy, поэтому мы передаём не только класс представления, но и соответствующую модель.
def setup_admin(app: FastAPI) -> None:
admin = Admin(engine=get_db_engine(), title="Lkeep Admin")
admin.add_view(UserView(User))
admin.add_view(LinkViewView(Link))
admin.mount_to(app=app)
Теперь можно снова запустить приложение и открыть http://localhost:8000/admin
.
Если всё подключено правильно — в левой панели появятся наши модели с названиями Users
и Links
, готовые к работе.

Делаем пользователя администратором
У нас нет механизма создания заранее заготовленного администратора. В одной из будущих статей определённо рассмотрим этот момент. Сейчас, чтобы у нас был способ войти в админ панель после добавления авторизации, нужно сделать одного пользователя администратором.
Для этого можно выполнить одно из двух действий:
- Подключиться к БД напрямую, например, в PyCharm или DBeaver и заменить флаг
is_admin
у пользователя созданного при регистрации сFalse
наTrue
. - Открыть модель
Users
в админке, выбрать пользователя и нажать "редактировать". На открывшейся странице переключить галочкуis_supreuser
и сохранить.
Любое из этих действий даст права администратора пользователю и на этапе авторизации не будет никаких проблем.
Админка по умолчанию не знает ничего о нашей системе авторизации. Starlette Admin предоставляет интерфейс AuthProvider
, через который можно подключить любой механизм проверки пользователя — от куки до OAuth2. Мы как раз реализуем свой класс, который адаптирует существующую логику авторизации к требованиям админ-панели.
Создадим в пакете apps/admin
новый файл — admin_auth.py
.
В нём объявим класс AdminAuthProvider
, унаследованный от AuthProvider
из Starlette Admin. Этот класс будет отвечать за:
- поиск пользователя по email,
- проверку пароля,
- проверку, является ли он суперпользователем,
- выдачу доступа или отказ.
Конструктор класса
Мы уже реализовали в auth
всё необходимое: есть UserManager
для получения пользователя и AuthHandler
для проверки пароля и генерации токенов. Используем существующие компоненты.
Передадим их в конструктор AdminAuthProvider
.
def __init__(self, handler: AuthHandler, manager: UserManager):
super().__init__()
self.handler = handler
self.manager = manager
Таким образом админка остаётся интегрирована в текущую систему авторизации, а не живёт отдельной жизнью. Это важно — мы не создаём второй тип пользователей, не делаем отдельную таблицу администраторов, а используем поле
is_superuser
в основной моделиUser
.
Метод is_authenticated
Этот метод вызывается Starlette Admin каждый раз, когда пользователь обращается к административной панели. Его задача — понять, авторизован ли пользователь, и дать или не дать доступ к интерфейсу.
По сути, это аналог нашего get_current_user
, только адаптированный под формат, который ожидает админка.
Создаём асинхронный метод is_authenticated
, принимающий request: Request
и возвращающий bool
.
В самом начале объявляем try-except
блок.
В блоке try
получаем токен из cookies через функцию get_token_from_cookies(request)
.
Если кука присутствует, функция вернёт токен.
Далее этот токен передаём в метод decode_access_token
из AuthHandler
, чтобы получить расшифрованные данные.
В блоке except
отлавливаем HTTPException
, которая может возникнуть, если токен отсутствует или невалиден.
Админка не умеет обрабатывать исключения этого типа, поэтому просто возвращаем False
— это корректный способ сообщить, что пользователь не авторизован.
Из расшифрованного токена (decoded_token
) извлекаем user_id
и session_id
.
Эти данные нужны, чтобы убедиться, что токен действительно хранится в Redis (а значит, сессия активна).
После этого ещё одна проверка. В блоке if not
вызываем у менеджера метод get_access_token
для проверки, есть ли токен в Redis. Если его там нет, то сработает инверсия и внутри блока if
возвращаем False
.
Если все проверки прошли успешно, то дальше обращаясь к request.state.user
присваиваем ему словарь с ключом id
и session_id
с соответствующими значениями. Ранее в представлении модели ссылок мы получали авторизованного администратора именно из этого состояния.
В конце возвращаем True
, сигнализируя, что пользователь успешно прошёл аутентификацию.
Код метода:
async def is_authenticated(self, request: Request) -> bool:
try:
token = await get_token_from_cookies(request=request)
decoded_token = await self.handler.decode_access_token(token=token)
except HTTPException:
return False
user_id = str(decoded_token.get("user_id"))
session_id = str(decoded_token.get("session_id"))
if not await self.manager.get_access_token(user_id=user_id, session_id=session_id):
return False
request.state.user = {"id": user_id}
return True
Почему мы не возвращаем объект пользователя целиком?
Админке достаточно знать, что пользователь существует и авторизован. Детальная проверка (is_superuser
и др.) будет происходить в отдельном методеlogin
, который мы добавим дальше. Однако, если вам нужно больше данных о пользователе, например, для представлений с доступом изrequest.state.user
, можно тут реализовать получение объекта пользователя.
Метод login
Метод login
отвечает за обработку входа пользователя в админку. Несмотря на то, что у нас уже реализован логин в пакете auth
, мы не можем использовать его напрямую, потому что Starlette Admin ожидает свой формат обработки авторизации и самостоятельно управляет объектом Response
. Поэтому мы адаптируем логику авторизации под механизмы админки, используя уже существующие инструменты AuthHandler
и UserManager
.
Создаём асинхронный метод login
, принимающий следующие параметры:
email
— строка, переданная из формы логина;password
— пароль пользователя;remember_me
— флаг "запомнить", передаваемый админкой (мы пока его игнорируем, но параметр обязателен по сигнатуре);request
— объект запроса;response
— объект ответа, в который мы вручную установим cookie с токеном.
Метод должен вернуть объект Response
, который админка обработает дальше.
Разбор логики по шагам
- Валидируем входные данные
Оборачиваем проверку вtry-except
, чтобы различать ошибку валидации и неверные учетные данные.
Создаём экземпляр схемыAuthUser
, чтобы гарантировать корректность структуры данных.
ЕслиPydantic
подниметValidationError
(например, email невалидный), мы перехватываем ошибку и бросаемLoginFailed
— это исключение из Starlette Admin, оно корректно отобразит ошибку на форме входа, не ломая интерфейс. - Получаем пользователя
Через методget_user_by_email_for_admin
извлекаем пользователя из БД.
Напоминаю: этот метод использует расширенную схему, включающуюis_superuser
. - Проверяем права и пароль В одном
if
объединяем три условия черезor
:- пользователя нет (
exist_user is None
); - он не является администратором (
not exist_user.is_superuser
); - пароль неверен (
not await self.handler.verify_password(...)
).
Если хотя бы одно условие истинно — выбрасываемLoginFailed
.
- пользователя нет (
- Генерируем токен и сохраняем его
Создаёмtoken
иsession_id
, затем вызываемstore_access_token
, чтобы привязать токен к пользователю и сохранить его в Redis — это нужно для дальнейшей проверки сессии. - Устанавливаем cookie
Именно здесь важен момент: админка сама не ставит cookie, нам нужно сделать это вручную черезresponse.set_cookie(...)
.
Мы передаём тот же ключ, что и в API —"Authorization"
, чтобы система авторизации была единой. - Возвращаем
response
После установки cookie просто возвращаем объектresponse
— Starlette Admin продолжит обработку автоматически.
Код метода:
async def login(
self, email: EmailStr, password: str, remember_me: bool, request: Request, response: Response
) -> Response:
try:
auth_data = AuthUser(email=email, password=password)
except ValidationError:
raise LoginFailed(msg="Invalid email or password")
exist_user = await self.manager.get_user_by_email_for_admin(email=auth_data.email)
if (
exist_user is None
or not exist_user.is_superuser
or not await self.handler.verify_password(
hashed_password=exist_user.hashed_password, raw_password=auth_data.password
)
):
raise LoginFailed(msg="Invalid credentials")
token, session_id = await self.handler.create_access_token(user_id=exist_user.id)
await self.manager.store_access_token(token=token, user_id=exist_user.id, session_id=session_id)
response.set_cookie(key="Authorization", value=token, httponly=True, max_age=settings.access_token_expire)
return response
Метод logout
Метод logout
отвечает за корректный выход администратора из системы. Он должен сделать две вещи:
- Удалить cookie с токеном (
Authorization
), чтобы браузер больше его не отправлял; - Отозвать токен в Redis, чтобы даже если кто-то попытается использовать старый токен вручную, он не прошёл проверку.
Создаём асинхронный метод logout
, принимающий request
и response
, и возвращающий Response
.
- Сначала удаляем cookie через
response.delete_cookie("Authorization")
; - Затем получаем пользователя из
request.state.user
; - И вызываем
revoke_access_token
, чтобы удалить активную сессию из Redis.
Важно: удаление cookie — это фронтовая часть выхода, а отзыв токена — бэкендовая. Убирать только cookie — недостаточно, иначе токен останется валидным до истечения TTL.
Код метода:
async def logout(self, request: Request, response: Response) -> Response:
response.delete_cookie("Authorization")
user = request.state.user
await self.manager.revoke_access_token(user_id=user["id"], session_id=user["session_id"])
return response
Метод get_admin_user
Этот метод вызывается Starlette Admin после успешной авторизации, чтобы получить объект текущего администратора.
Даже если в интерфейсе админки мы не используем эти данные напрямую, метод обязателен, иначе библиотека не сможет завершить цикл проверки доступа.
Создаём синхронный метод get_admin_user
, принимает объект request
, а возвращать будем нашу схему GetUserByID
.
Важный момент: метод обязательно должен быть синхронным. Если сделать его
async
, Starlette Admin просто проигнорирует результат — потому, что метод не будет вызван корректно.
Мы возвращаем Pydantic-схему GetUserByID
, в которую передаём данные из request.state.user
.
Код метода:
def get_admin_user(self, request: Request) -> GetUserByID:
return GetUserByID(**request.state.user)
Функция get_admin_auth_provider и регистрация обработчика
Наш класс AdminAuthProvider
принимает внешние зависимости — AuthHandler
и UserManager
.
Так как Starlette Admin не использует систему зависимостей FastAPI (Depends
), мы не можем просто объявить их в аргументах конструктора и рассчитывать на автоматическое внедрение.
Поэтому создаём функцию-фабрику, которая вручную инициализирует необходимые зависимости и возвращает готовый экземпляр AdminAuthProvider
.
Для этого ниже, под классом создадим функцию get_admin_auth_provider
, которая возвращает AdminAuthProvider
.
Внутри функции в переменной manager
создаём экземпляр класса UserManager
, передав в него экземпляры классов DBDependency
и RedisDependency
.
Затем в возврате создаём экземпляр нашего класса AdminAuthProvider
передав в него manager
и экземпляр класса AuthHandler
.
Код функции:
def get_admin_auth_provider() -> AdminAuthProvider:
manager = UserManager(db=DBDependency(), redis=RedisDependency())
return AdminAuthProvider(handler=AuthHandler(), manager=manager)
Теперь вернёмся в функцию setup_admin
, где создаётся экземпляр Admin
.
Туда необходимо передать новый аргумент auth_provider
, присвоив ему вызов get_admin_auth_provider()
.
Это будет выглядеть следующим образом:
def setup_admin(app: FastAPI) -> None:
admin = Admin(engine=get_db_engine(), title="Lkeep Admin", auth_provider=get_admin_auth_provider())
admin.add_view(UserView(User))
admin.add_view(LinkViewView(Link))
admin.mount_to(app=app)
Готово! Теперь админка будет запрашивать авторизацию и пускать только администраторов!

Заключение
Казалось бы, что может быть проще — поставить готовую библиотеку для админки и пользоваться. Но на практике быстро становится ясно: универсальные решения редко идеально вписываются в уже существующую архитектуру проекта. Они приносят с собой собственные правила, к которым приходится адаптироваться.
В нашем случае backend ещё небольшой, всего две модели и базовая логика авторизации, но даже этого оказалось достаточно, чтобы столкнуться с необходимостью доработки кода и продумывания архитектуры — особенно в части обработки токенов, передачи зависимостей и проверки прав доступа.
Тем не менее, результат того стоит — теперь у нас удобная админ-панель, которая не нарушает общую концепцию проекта и работает через нашу систему пользователей, а не через встроенную упрощённую авторизацию.
Комментарии
Оставить комментарийВойдите, чтобы оставить комментарий.
Комментариев пока нет.