Как я смог запустить суперапп и что это на самом деле

от автора

Всем привет! Пишу ознакомительную статью о моем творении и как работают супераппы, если возникнут вопросы, обращайтесь в коменты.

Первый шаг в бездну

Начну с вопроса: «что такое этот ваш суперапп?» и уже дальше опишу принцип работы и что в итоге получилось воссоздать за 8 месяцев непрерывного кодинга. Суперапп — это объединение двух и более функционалов нескольких сервисов в одно единое приложение или веб‑приложение. Проще говоря, его задача избавить обычного юзера интернета от лишних вкладок на компе и сделать все более централизованным. Хотя тут скорее зависит от того, на какой суперапп мы смотрим.

Архитектура подобных проектов всегда упирается в четыре важные вещи: бэкенд, фронтенд, сервер и защита всего этого. От сюда уже идут другие, не менее важные нюансы, которые следует упомянуть. А что вообще совмещать? Мой ответ, на такой вопрос прост и банален: Маркет и Мессенджер — что я и сделал. Звучит нереально, однако это реально, но крайне трудно, если у вас нет опыта и идеи как все это можно организовать. Хотя пожалуй самый важный аспект это безопасность и защита от IDOR атак. Но и они в этом сценарии отлетают, так как их крайне легко отражать проверками на уровне сервера.

Вот банальный пример, который гарантирует защиту, но при этом является одним из простейших защитных методов. Предположим, у нас есть WebSocketManager с обработкой подключений в consumers.py. Там реализован универсальный базовый метод connect. Для подключения пользователя к беспрерывному соединению с чатом как раз на этом уровне следует внедрять проверку безопасности. Делается это следующим образом:

@database_sync_to_async    def is_user_in_chat(self, user, chat_id):        # Проверяем, существует ли чат и входит ли в него данный юзер        chat = Chat.objects.filter(pk=chat_id, participants=user).exists()        if chat:             return chat        else:             chat = Chat.objects.filter(pk=chat_id, subscribers=user).exists()            return chat    # 1. Подключение    async def connect(self):        try:            # Получаем chat_id с проверкой            self.chat_id = str(self.scope['url_route']['kwargs']['chat_id'])            self.room_group_name = f"chat_{self.chat_id}"            self.user = self.scope["user"]            # (Безопасность соединения!)             # Проверка если юзер не аутентифицирован никак в чате:             user_has_access = await self.is_user_in_chat(self.user, self.chat_id)            if not user_has_access:                logger.warning(f"User {self.user.id} tried to access chat {self.chat_id} without permission!")                await self.close(code=4403) # Закрываем соединение (Forbidden)                return                        # остальные проверки            #  ...

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

Интерфейс чатов на пк

Интерфейс чатов на пк

Проект в вебе, поэтому стек у меня банальный для такого масштаба: Django на бэкенде (Python) и JavaScript на фронтенде. Как я уже упоминал, на разработку ушло 8 месяцев, и всё это время задачи были четко распределены по этапам.

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

Второй шаг в бездну — погружение

Следующим этапом (с третьего по пятый месяц) стало создание своего API и синхронизация базы данных. Пожалуй, это самое захватывающее и безусловно самое важное. Задача заключалась в построении единой системы защиты. Данные, передаваемые между пользователями, ясное дело, должны как-то шифроваться и быть недоступны посторонним.

Интерфейс чатов на телефоне

Интерфейс чатов на телефоне

Тут обычно есть два пути: либо использовать сквозное шифрование (E2EE), либо защищать данные в движении с помощью TLS‑протоколов, а на самом сервере шифровать их при хранении в базе данных с использованием секретных ключей. Выбор почти у всех всегда один — второй.

Это объясняется тем, что так проще управлять данными и реализовывать привычный бэкенд-функционал. Ведь в случае с E2EE данные становятся абсолютно «слепыми» для сервера. Мне кажется, что сквозной шифр эффективно подошел бы только для классического, изолированного чата без каких-либо надстроек и сложной бизнес-логики.

В качестве примера второго варианта шифрования (на стороне сервера) можно взять специализированный класс кастомного поля для моделей Django. Он перехватывает текстовые данные при записи в базу и автоматически расшифровывает их при чтении, гарантируя защиту данных «из коробки»:

class EncryptedTextField(models.TextField):    def __init__(self, *args, **kwargs):        super().__init__(*args, **kwargs)        self._cipher = None        @property    def cipher(self):        if self._cipher is None:            try:                from cryptography.fernet import Fernet                if settings.ENCRYPTION_KEY:                    key = settings.ENCRYPTION_KEY.encode()                    self._cipher = Fernet(key)                else:                    logger.warning("ENCRYPTION_KEY not set, encryption disabled")            except Exception as e:                logger.error(f"Failed to initialize cipher: {e}")        return self._cipher        def from_db_value(self, value, expression, connection):        try:            encrypted_bytes = base64.b64decode(value)            decrypted_bytes = self.cipher.decrypt(encrypted_bytes)            return decrypted_bytes.decode('utf-8')        except (base64.binascii.Error, ValueError):            return value        except Exception as e:            # Ошибка расшифровки            logger.error(f"Decryption error: {e}")            return f"[DECRYPT_ERROR: {value[:30]}...]"        def get_prep_value(self, value):        if value is None:            return None                # Если значение уже зашифровано или это ошибка        if isinstance(value, str) and (value.startswith('gAAAAA') or value.startswith('[DECRYPT_ERROR:')):            return value        try:            # Шифруем            encrypted_bytes = self.cipher.encrypt(value.encode('utf-8'))            return base64.b64encode(encrypted_bytes).decode('utf-8')        except Exception as e:            logger.error(f"Encryption error: {e}")            return value

После чего этот класс можно спокойно использовать для текстового поля у сообщений, заменив им базовый CharField. Что гарантирует безопасность и надежность для пользователей.

Взять тот же поиск или фильтрацию: если данные зашифрованы на клиенте, реализовать их силами сервера или силами админа в БД становится технически невозможно. Именно поэтому техпроекты со сложной архитектурой выбирают второй способ — шифрование на стороне сервера (At Rest), чтобы сохранить контроль над собственной архитектурой. Безусловно, E2EE обеспечивает максимальную приватность, но платить за неё приходится огромным усложнением разработки и потерей гибкости.

Интерфейс маркета

Интерфейс маркета

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

Намного интереснее устроены алгоритмы ранжирования, или, проще говоря, лента листингов. Для её формирования нужен полноценный перебор и скоринг данных. Система начисляет определенные баллы за каждый кейс: например, учитывается дата публикации, (триггер для моментального взлета листинга вверх ), количество просмотров, количество лайков, количество заказов на объявление. На основе финального веса товара Django формирует персональную выдачу для каждого пользователя в реальном времени.

Пример такой работы можно представить в виде выделенной функции, которая обрабатывает каждый запрос по листингам и возвращает отсортированный кверисет с использованием встроенных инструментов агрегации Django ORM:

def get_listings(search_query=None):    base_qs = Listing.objects.filter(        delete_status=False,        is_available=True,        successfull_flag=True,    ).select_related('category').only(        'name', 'description', 'type_listing', 'state_listing',        'address', 'price_listing', 'sum_listing', 'delivery_state',        'rating', 'model_type', 'created_at', 'category__name'    )        # Параметры алгоритма    new_listing_boost_hours = 1    base_rating_weight = 1.0    newness_weight = 2.0        def annotate_listings(queryset):        return queryset.annotate(            hours_since_creation=ExpressionWrapper(                ExtractHour(timezone.now() - F('created_at')),                output_field=FloatField()            ),            is_new=Case(                When(hours_since_creation__lt=new_listing_boost_hours,                      then=Value(newness_weight)),                default=Value(0),                output_field=FloatField()            ),            weighted_score=ExpressionWrapper(                F('rating') * Value(base_rating_weight) +                 F('is_new') * (Value(newness_weight) - F('hours_since_creation')/Value(new_listing_boost_hours)),                output_field=FloatField()            )        )    result = list(annotate_listings(base_qs).order_by('-weighted_score', '-created_at'))    return result

Третий шаг в бездну — всплытие

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

Супераппом уже можно воспользоваться и ознакомиться лично: vendergram.com. Это бета‑версия, поэтому я готов к любой конструктивной критике в свой адрес.

ссылка на оригинал статьи https://habr.com/ru/articles/1050372/