В этой статье мы рассмотрим настройку и запуск PgAdmin в контейнере Docker. PgAdmin — удобный инструмент для управления базами данных PostgreSQL, и в некоторых случаях может быть полезным иметь его у себя на сервере. Также мы интегрируем реверс-прокси Traefik для управления SSL-сертификатами, а так же для реализации базовой HTTP-аутентификации в целях ограничения доступа к веб-интерфейсу PgAdmin.
Конфигурация успешно используется на нашем сервере.
Предварительные требования
Перед началом убедитесь, что у вас установлены следующие компоненты:
services: — Раздел, где определяются различные сервисы. В нашем случае это один сервис — pgadmin.
pgadmin: — Имя сервиса. Так будет называться контейнер.
container_name: pgadmin — Устанавливает имя контейнера в Docker.
image: dpage/pgadmin4:8.11.0 — Используемый образ контейнера. В данном случае это PgAdmin версии 8.11.0.
restart: unless-stopped — Политика перезапуска контейнера. Контейнер будет перезапущен, если остановится из-за сбоев, но не будет перезапущен, если его остановить вручную.
networks: — Указание сетей, к которым подключен контейнер. Здесь используется сеть proxynet.
env_file: — Указание файла с переменными окружения. Этот файл должен содержать переменные, такие как имя сервера и домен сервера.
environment: — Переменные окружения для настройки PgAdmin.
PGADMIN_CONFIG_SERVER_MODE: 'False' — Отключает режим сервера (многопользовательский), позволяя запускать PgAdmin в режиме рабочего стола (однопользовательский). Мы устанавливаем для него значение False, поэтому не будут запрашиваться учетные данные для входа. Вместо этого используется базовая HTTP аутентификация.
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' — Позволяет обойти запрашивание мастер-пароля.
PGADMIN_DEFAULT_EMAIL: ${ADMIN_EMAIL} — Устанавливает адрес электронной почты администратора, который берется из переменных окружения.
labels: — Используются для конфигурации Traefik, который выступает обратным прокси-сервером.
traefik.enable=true — Включает Traefik для данного сервиса.
traefik.http.routers.pgadmin.rule=Host(pgadmin.${SERVER_DOMAIN}) — Устанавливает правила маршрутизации по домену.
traefik.http.routers.pgadmin.middlewares=pgadmin-auth — Указывает использовать middleware для аутентификации.
traefik.http.middlewares.pgadmin-auth.basicauth.usersfile=/httpauth/usersfile.htpasswd — Указывает файл с пользователями для базовой HTTP-аутентификации.
volumes: — Раздел для монтирования внешних папок в контейнер.
/data/secrets/${SERVER_DOMAIN}/httpauth:/httpauth — Монтирует директорию с конфигом для HTTP-аутентификации.
/data/appdata/pgadmin:/var/lib/pgadmin — Монтирует директорию для хранения данных PgAdmin.
networks: — Определение используемых сетей. Сеть proxynet задана как внешняя.
Подготовка окружения
У нас на сервере используется специально разработанная система скриптов для автоматизации установки. Эти скрипты по большей части опубликованы в нашем репозитории и описаны в отдельной статье. В нашем случае, вместе с этим docker-compose.yml идет Bash скрипт для инициализации всех необходимых параметров:
В своем случае, вы можете выполнить эти команды вручную.
Далее нужно инициализировать переменные окружения:
Bash
sudobashenv-gen.shPGADMIN_DEFAULT_PASSWORD
На нашем сервере используется специализированный скрипт env-gen.sh из репозитория utils, который добавляет записи в .env файл. Но вы так же можете вручную создать файл ${SERVER_DOMAIN}.env в директории /data/secrets/${SERVER_DOMAIN}/ и заполнить его необходимыми значениями:
PGADMIN_DEFAULT_PASSWORD=your_default_password
Замените your_default_password на свое значение.
Так же для работы потребуется указать глобальные переменные сервера PGADMIN_DEFAULT_EMAIL и SERVER_DOMAIN. Для этих целей у нас в репозитории так же есть специальный скрипт. Например, можно сделать так:
Так же необходимо настроить HTTP-аутентификацию. В нашей конфигурации файл, в котором хранятся акаунты для входа находится по пути /data/secrets/${SERVER_DOMAIN}/httpauth/usersfile.htpasswd. В него необходимо добавить строчку вида admin:passoword_hash, генерация которой описана в этой статье. Но например, для генерации вы так же можете использовать сторонний сервис вроде этого.
Запуск сервиса
Откройте терминал, перейдите в директорию с файлом docker-compose.yml и выполните команду:
docker-compose up -d
Эта команда загрузит образ PgAdmin, создаст и запустит контейнер в фоновом режиме. После успешного запуска вы сможете пользоваться PgAdmin через веб-браузер, набрав https://pgadmin.<your_domain>. Для доступа вам потребуется ввести логин и пароль, сконфигурированные ранее.
id -u возвращает идентификатор текущего пользователя. 0 соответствует пользователю root.
Если текущий пользователь не root, скрипт выводит сообщение об ошибке и завершает выполнение.
Настройка обработки ошибок
Bash
trap'echo -e "\033[31minit-docker.sh: Something went wrong\033[0m"; exit 1'ERRset-e
Здесь используется команда trap, чтобы установить обработчик ошибок, который будет активироваться при любой ошибке выполнения команд (ERR).
trap позволяет выполнить специальные команды при возникновении ошибок.
set -e завершает скрипт при возникновении любой ошибки, делая скрипт более устойчивым.
Отключение интерактивного режима
Bash
exportDEBIAN_FRONTEND=noninteractive
Переменная окружения DEBIAN_FRONTEND устанавливается в значение noninteractive, чтобы установка не требовала пользовательского ввода. Это полезно для автоматического скрипта, при запуске которого не будет возможности взаимодействия с пользователем.
Установка зависимостей и Docker
Этот код отвечает за установку необходимых пакетов и самого Docker:
Данная конфигурация представляет собой полный цикл разработки и развертывания веб-приложения Quasar Vue.js, начиная от написания кода и заканчивая его публикацией.
Приложение упаковано в Docker контейнер, настроенный на работу с реверс прокси Traefik. Имеется два режима запуска проекта — developer и production.
Конфигурация используется в нашем приложении Messenger.
Ниже приведем код docker-compose.yml (production):
В development режиме клиент запускается командой quasar dev и используется сервер Node.js. Этот режим позволяет динамически отслеживать изменения в коде, что бы сразу отображать их в браузере.
В volumes добавляется .:/app:rw, что позволяет в процессе разработки динамически обновлять файлы проекта в контейнере.
command: sh -c "npm install && quasar dev" — здесь, npm install необходимо указать для того, что бы дирекотория node_modules была доступна для отслеживания зависимостей в VS Code.
Dockerfile
Эта конфигурация Dockerfile описывает многоэтапную сборку образа для Quasar Vue.js приложения:
Dockerfile
# develop stageFROM node:22.8.0-alpine3.20 AS develop-stageWORKDIR /appCOPY package*.json ./RUN npm config set fund false --location=globalRUN npm install -g @quasar/cliCOPY . .# build stageFROM develop-stage AS build-stageRUN npm installARG VITE_API_URLENV VITE_API_URL=${VITE_API_URL}ARG VITE_APP_VERSIONENV VITE_APP_VERSION=${VITE_APP_VERSION}ARG VITE_CLIENT_URLENV VITE_CLIENT_URL=${VITE_CLIENT_URL}#RUN quasar buildRUN quasar build -m pwa# production stageFROM nginx:1.27.1-alpine3.20 AS production-stageWORKDIR /appCOPY --from=build-stage /app/dist/pwa /usr/share/nginx/html# COPY nginx.conf /etc/nginx/nginx.confEXPOSE 80
Он состоит из трёх основных этапов:
Разработка (develop stage):
Базовый образ: Используется образ node:22.8.0-alpine3.20, что обеспечивает легковесную среду на основе Alpine Linux для работы с Node.js.
Рабочий каталог: Устанавливается /app как основной рабочий каталог для всех последующих команд.
Зависимости: Копируются файлы package*.json для последующей установки необходимых зависимостей.
Настройки npm: Отключает спам в консоли о финансировании (fund) пакетов в NPM.
Установка Quasar CLI: Глобально устанавливается Quasar CLI для возможности управления приложением Quasar.
Копирование приложения: Все исходные файлы приложения копируются в контейнер.
Сборка (build stage):
Исходный образ: Используется ранее созданный develop-stage как основа.
Зависимости: Выполняется установка всех зависимостей приложения.
Переменные окружения: Аргументы и переменные окружения (VITE_API_URL, VITE_APP_VERSION, VITE_CLIENT_URL) передаются в окружение сборки для использования в конфигурациях.
Сборка приложения: Приложение собирается в режиме PWA при помощи Quasar CLI.
Продакшн (production stage):
Базовый образ: Используется образ nginx:1.27.1-alpine3.20, что обеспечивает легковесную и производительную среду для развертывания статических файлов.
Рабочий каталог: Устанавливается /app как основной рабочий каталог.
Копирование файлов сборки: Скомпилированные файлы PWA копируются в каталог Nginx /usr/share/nginx/html для обслуживания.
Конфигурация Nginx: В комментарии указана возможность изменения конфигурации Nginx, что позволяет настраивать его работу при необходимости.
Открытие порта: Контейнер будет прослушивать стандартный 80 порт (в дальнейшем он будет перенаправлен через реверс-прокси Traefik, который позволит управлять SSL сертификатами для обеспечения соединения HTTPS).
Если вам интересен наш проект, есть вопросы, замечания, или предложения — оставляйте комментарии или пишите на почту: checkerwars@mail.ru
Кроме того, автор проекта ищет работу. Мое резюме.
Данная конфигурация представляет собой полный цикл разработки и развертывания серверного Nest.js приложения, начиная от написания кода и заканчивая его публикацией.
Сервер упакован в Docker контейнер, настроенный на работу с реверс прокси Traefik. Имеется два режима запуска проекта — developer и production.
Конфигурация используется в нашем приложении Messenger.
Ниже приведем код docker-compose.yml (production):
Его содержимое похоже на конфигурацию production, но есть несколько важных отличий:
target: 'develop-stage'
В volumes добавляется еще .:/app:rw, что позволяет в процессе разработки динамически обновлять файлы проекта в контейнере
command: sh -c "npm install && npm run start:dev" — здесь, npm install необходимо указать для того, что бы дирекотория node_modules была доступна для отслеживания зависимостей в VS Code.
Dockerfile
Эта конфигурация Dockerfile описывает многоэтапную сборку образа для Node.js приложения:
Dockerfile
# develop stageFROM node:22.8.0-alpine3.20 AS develop-stageWORKDIR /appCOPY package*.json ./RUN npm config set fund false --location=globalCOPY . .# build stageFROM develop-stage AS build-stageRUN npm installRUN npm run build# production stageFROM node:22.8.0-alpine3.20 AS production-stageWORKDIR /appCOPY --from=build-stage /app/node_modules /app/node_modulesCOPY --from=build-stage /app/dist /app/distCOPY --from=build-stage /app/static /app/staticCOPY package.json ./CMD ["node", "dist/main.js"]EXPOSE 5000
Он состоит из трёх основных этапов:
Разработка (develop stage):
Базируется на Alpine-образе Node.js для его легковесности и эффективности.
Устанавливает рабочую директорию /app.
Копирует файлы package.json и package-lock.json (package*.json для большей гибкости).
Отключает спам в консоли о финансировании (fund) пакетов в NPM.
Копирует все остальные файлы приложения в контейнер.
Сборка (build stage):
Использует промежуточный образ develop-stage как основу.
Устанавливает зависимости с помощью npm install.
Запускает процесс сборки приложения с помощью npm run build, формируя конечную сборку в директории dist.
Продакшн (production stage):
Начинается с чистого Node.js Alpine-образа той же версии.
Устанавливает рабочую директорию /app.
Копирует директорию node_modules из build-stage для использования в продакшене.
Копирует директории dist и static из build-stage, содержащие скомпилированные и статические файлы приложения.
Копирует package.json.
Определяет, что при запуске контейнера должен исполняться файл dist/main.js через Node.js.
Открывает 5000 порт (в дальнейшем он будет перенаправлен через реверс-прокси Traefik, который позволит управлять SSL сертификатами для обеспечения соединения HTTPS).
Если вам интересен наш проект, есть вопросы, замечания, или предложения — оставляйте комментарии или пишите на почту: checkerwars@mail.ru
Кроме того, автор проекта ищет работу. Мое резюме.
Traefik — современный прокси-сервер и балансировщик нагрузки, с помощью которого можно легко управлять маршрутизацией трафика в приложениях, работающих внутри Docker контейнеров.
Конфигурация успешно используется в нашем приложении Messenger
Мы используем образ traefik:v3.1.2 и настраиваем его так, чтобы он автоматически запускался вместе с системой (restart: unless-stopped).
Переменные окружения загружаются из файла .env.
Командная строка:
Traefik конфигурируется через параметры командной строки. Важные параметры включают указание файлов для хранения конфигурации сертификатов и определение сетевой среды Docker, в которой будет работать Traefik.
Точки входа:
Точки входа определяют порты, на которые будет поступать входящий трафик (http, https, postgres и mariadb). Также обеспечивается редирект HTTP на HTTPS для повышения безопасности.
SSL и Let’s Encrypt:
Включена поддержка автоматической выдачи SSL-сертификатов Let’s Encrypt через certResolver. Это позволяет обеспечивать безопасное соединение с использованием TLS.
Панель управления (Dashboard):
Traefik предоставляет веб-интерфейс для мониторинга и управления (dashboard). Он защищён с помощью HTTP-базовой аутентификации, данные для которой хранятся в файле usersfile.htpasswd.
Монтирование томов:
Конфигурация involves монтирование различных ресурсов и секретов в контейнер Traefik. Это включает сокет Docker для интеграции с контейнерами, данные для сертификатов и аутентификации.
Сетевые настройки:
Используемая сеть называется proxynet и она должна быть внешней для обеспечения корректной работы Traefik с другими контейнерами.
Логирование:
Происходит настройка логирования через json-file с ограничением размера файла в 1 MB, что помогает управлять объемом логов и упрощает диагностику.
В этой статье мы рассмотрим, как запустить и настроить экземпляр базы данных PostgreSQL с помощью Docker. Также мы интегрируем Traefik для проксирования соединений.
Конфигурация успешно используется в нашем приложении Messenger
Предварительные требования
Перед началом убедитесь, что у вас установлены следующие компоненты:
services.postgres: Конфигурация для контейнера PostgreSQL.
container_name: Устанавливает имя контейнера в Docker.
image: Используется образ postgres:16.4-alpine3.20, который является более легким вариантом на базе Alpine Linux.
restart: Политика перезапуска unless-stopped гарантирует, что контейнер автоматически перезапустится в случае сбоев.
networks: Подключение к внешней сети proxynet, которая может использоваться Traefik.
env_file: Переменные окружения загружаются из файла, что позволяет хранить конфиденциальные данные вне кода.
environment: Переменные окружения, такие как POSTGRES_USER и POSTGRES_DB, определяют пользователя и базу данных по умолчанию.
volumes: Монтирует локальную директорию /data/appdata/pgdata в контейнер для постоянного хранения данных.
labels: Настройка Traefik для проксирования TCP соединений к PostgreSQL.
networks.proxynet: Используется для подключения к существующей сети Traefik.
Подготовка окружения
У нас на сервере используется специально разработанная система скриптов для автоматизации установки. Эти скрипты по большей части опубликованы в нашем репозитории и описаны в отдельной статье. В нашем случае, вместе с этим docker-compose.yml идет Bash скрипт для инициализации всех необходимых параметров:
На нашем сервере используется специализированный скрипт global-env.sh из репозитория utils, который добавляет переменные окружения на сервер глобально.
Если переменная SERVER_DOMAIN не была инициализирована ранее, ее так же можно добавить в глобальные переменные сервера:
Bash
bashglobal-env.shSERVER_DOMAINcheckerwars.com
Затем добавляется переменная в .env файл, для чего используется специализированный скрипт env-gen.sh из того же репозитория utils:
Bash
sudobashenv-gen.shPOSTGRES_PASSWORD
Но вы так же можете вручную создать файл ${SERVER_DOMAIN}.env в директории /data/secrets/${SERVER_DOMAIN}/ и заполнить его необходимыми значениями:
POSTGRES_PASSWORD=your_postgres_password
Замените your_postgres_password на свое значение.
В конце производится запуск Docker контейнера. Перейдите в директорию с файлом docker-compose.yml и выполните:
Bash
cd/data/postgressudodockercomposeup-d
Эта команда загрузит образ PostgreSQL, создаст и запустит контейнер в фоновом режиме.
Проверка конфигурации
После запуска контейнера вы можете проверить работу сервиса, подключившись к базе данных. Например, используйте команду ниже, чтобы войти в контейнер и подключиться к PostgreSQL:
В этом материале я опишу общую архитектуру и некоторые детали реализации клиентской части Messenger. Обзор будет не слишком подробным, т.к если пытаться описать каждый компонент приложения, статья напротив, получится очень объемной. В целом, принципы реализации от компонента к компоненту схожи. Но в случаях, когда есть на что обратить особое внимание, я постараюсь раскрыть детали подробнее.
Клиент платформы CheckerWars написан на фреймворке Vue.js с использованием Quasar Framework. Первоначально разработка велась на чистом Javascript, но в последствии было принято решение переписать все на Typescript — что в значительной степени уже сделано. Кроме того, я так же принял решение отказаться от Qusasr, часть страниц, компонентов уже используют чистый Vue.js с Typescript.
Так же есть подробный обзор сервера, который написан на Nest.js:
Но продолжим разбираться с клиентом на Vue.js.
При разработке клиентского приложения CheckerWars я обращаю серьезное внимание на оптимизацию зависимостей. В конечном итоге, я хочу минимизировать использование внешних зависимостей, что бы иметь максимальный контроль над кодом и функционалом, а так же не загромождать код тяжеловесными и избыточными во многих случаях библиотеками, вроде Moment.js.
Основные моменты:
Используется фреймворк Vue.js 3
Преимущественно используется Options API
Код пишется на Typescript
Интерфейс во многом сделан с использованием Quasar framework (но в процессе отказа от него)
Используется хранилище Pinia
Передача данных в реальном времени осуществляется при помощи библиотеки Socket.io
Версия PWA (средствами Quasar framework)
Версия под Android с использованием Capacitor (средствами Quasar framework)
Клиент, как и сервер, упакован в Docker контейнер. Имеется два режима запуска проекта — developer и production.
В статье по ссылке ниже, приведена конфигурация Docker для этого приложения:
Роутер
Данная структура маршрутизации в проекте Vue.js определяет различные пути и их соответствующие компоненты для рендера. Ниже приведем краткое описание каждого маршрута:
Главный файл маршрута
Это основная конфигурация роутера, в нее подключаются маршруты для вложенных страниц (файлы маршрутов размещены в папках модулей проекта):
Описание: Второстепенные страницы приложения, такие как «О проекте»
Мессенджер
Путь: '/im'
Компонент: ImLayout.vue
Вложенные маршруты: messenger
Описание: Страницы мессенджера
Пользователи
Путь: '/users'
Компонент: AppLayout.vue
Вложенные маршруты: users
Описание: Страницы для управления и просмотра информации о пользователях.
Аккаунт
Путь: '/account'
Компонент: AppLayout.vue
Вложенные маршруты: account
Описание: Управление настройками учетной записи.
Профиль
Путь: '/profile'
Компонент: AppLayout.vue
Вложенные маршруты: profile
Описание: Просмотр и редактирование профиля пользователя.
Друзья
Путь: '/friends'
Компонент: AppLayout.vue
Вложенные маршруты: friends
Описание: Взаимодействие с друзьями пользователя.
Аутентификация
Путь: '/auth'
Компонент: NLayout.vue
Вложенные маршруты: auth
Описание: Управление входом и регистрацией пользователей.
Кроме того, есть маршрут /:catchAll(.*)* который перехватывает неопознанные страницы, и переадресовавает запросы на страницу ErrorNotFound.vue.
Этот список описывает, как различные части приложения ассоциируются с определенными путями на уровне маршрутизации, обеспечивая организацию и доступ к различным функциональным областям приложения.
Маршруты для Messenger:
Главная страница подписки
Путь: ''
Компонент: SubscribePage
Описание: Страница друзей и подписок пользователя
Страница чата
Путь: ':idMixed'
Компонент: ChatPage
Описание: Страница с чатом
Страница создания комнаты
Путь: 'create/:type'
Компонент: RoomCreate
Описание: Страница с формой создания новой коснаты (группа, канал)
Редактирование аватара
Путь: 'avatar/:roomId'
Компонент: AvatarEdit
Описание: Страница с формой для редакирования аватара
Страница комнаты
Путь: 'room/:pageId'
Компонент: RoomPage
Описание: Страница с описанием комнаты (группа, канал)
В данном коде осуществляется базовая конфигурация Axios: разрешается использование cookie, а так же устанавливается базовый url.
Затем, устанавливается два перехватчика — на запрос, и ответ. Перехватчики (interceptors) — это функции, которые выполняются при каждом запросе axios. В данном случае, при каждом запросе на сервер, к заголовкам запроса обновляется токен авторизации. В свою очередь, при каждом полученном ответе от сервера, выполняется проверка на ошибку «401 Unauthorized». Если ошибка произошла, клиент делает запрос к серверу на обновление токена доступа, и повторяет предыдущий запрос уже с новым токеном.
В файле api.ts находятся основные методы приложения для работы с удаленным API:
Эти методы уже непосредственно вызываются из компонентов приложения.
В целом, формирование и обработка запроса в приложении происходит в несколько этапов. В будущем, я хочу лучше оптимизировать и улучшить архитектуру формирования запросов, т.к даже нахождение методов запросов к API в хранилище Pinia (и они в основном не используют его возможности) выглядит не вполне корректным, и сохранилось по историческим причинам — первые примеры кода, с которых я начинал изучение Vue.js были очень упрощенные, и работа с API там проходила полностью в хранилище и самих компонентах. В моей же реализации, код работы с API уже полностью вынесен из компонентов, сделана обработка ошибок (пока неидеально), формирование запросов. В дальнейших итерациях разработки я планирую оставить в хранилищах лишь тот код, которому непосредственно необходимы возможности хранилища. А код работы с API вынести в отдельные файлы и оптимизировать еще лучше.
Работа с Socket.io API
Работа с API через Socket.io вынесена в отделный файл socket.service.ts. Приведу его содержимое полностью:
Здесь реализованы всплывающие уведомления, оверлей с ошибкой и загрузкой, а так же целый ряд функций, связанных с работой мессенджера через Socket.io (отправка событий онлайн или офлайн в зависимости от видимости окна, первоначальная инициализация вроде joinUserToAllRooms(), устанавливается ряд обработчиков событий сокетов onConnect, onDisconnect, onException, onUserOnlineStatus, onSendMessage, onTyping).
Выпадающее меню в окне чата DropMenu:
Полностью реализовано самостоятельно. Код целиком:
Образка аватара реализована при помощи сторннего компонента vue-advanced-cropper. Этот компонент позволяет удобно определить координаты обрезки для изображения аватара, а затем уже на сервере, будет произведена обрезка. Ниже привожу код страницы загрузки аватара полностью:
На текущий момент — самая сложная часть приложения. Особенно хочу отметить, что здесь реализована «бесконечная» прокрутка сообщений в чате, и сделано это без использования сторонних компонентов (Для примера был найден готовый пример реализации такого алгоритма, который, однако, потребовал полного вникания в его принцип работы и значительной доработки). Я пробовал использовать сторонние компоненты, но ни одно из решений меня не устроило, а так же являлось «черным ящиком». В конечном итоге, я решил, что для столь важной части приложения необходимо иметь полное понимание и контроль над кодом, что привело к созданию кастомного решения.
Пока это первая черновая реализация чата, и далее планируется глубокий рефакторинг — большая часть кода будет выделена в отдельные компоненты и переписана. Кроме того, будут исправлены некотрые баги и недоделки. Тем не менее, я приведу все 1755 строк кода этой страницы целиком (включая код CSS), не смотря на имеющиеся недочеты:
В этом материале я опишу общую архитектуру и некоторые детали реализации серверной части Messenger. Полный исходный код сервера можно найти здесь. Однако, что бы показать основные моменты, по ходу повествования буду приводить некоторые фрагменты кода.
Сервер платформы CheckerWars работает на Node.js фреймворке Nest.js, использующем Typescript. Первоначальная версия создавалась на Express.js, но в нем не хватало встроенных возможностей — работа с Express больше напоминает работу с набором библиотек, чем с полноценным фреймворком. Тем более, у меня был некоторый опыт разработки на Yii Framework, и хотелось чего то похожего. Безусловно, в руках профессионала Express.js может быть хорошей основой для проекта, но мне, как новичку в js бэкэнд разработке, удобнее иметь изначально строгую архитектуру приложения. Бонусом, мне пришлось осваивать Typescript, и в результате я его очень полюбил — клиент на Vue.js так же будет полностью переписан с использованием Typescript. У нас есть подробный обзор клиентского приложения:
Продолжим разбираться с сервером на Nest.js.
Основные моменты:
В качестве ORM используется Sequelize
СУБД PostgreSQL
Передача данных в реальном времени осуществляется при помощи библиотеки Socket.io, для работы с которой в Nest.js есть своя обертка
Работа с электронной почтой осуществляется с помощью Nodemailer
В проекте настроена система документирования API Swagger
Сервер упакован в Docker контейнер, настроенный на работу с реверс прокси Traefik. Имеется два режима запуска проекта — developer и production.
В статье по ссылке ниже, приведена конфигурация Docker для этого приложения:
Предварительно перед запуском контейнера необходимо запустить скрипт первоначальной инициализации переменных и базы данных install.sh:
*.module.ts — используется для организации и управления зависимостями в приложении, объединяя связанные компоненты, сервисы и контроллеры в один модуль.
*.model.ts — описание структуры данных, например, схемы и интерфейсы, чтобы упорядочить и типизировать данные в приложении. В нашем случае, в первую очередь используются для описания структуры базы данных.
*.service.ts — используется для бизнес-логики приложения. Он обрабатывает данные и выполняет задачи, которые не связаны напрямую с веб-запросами или ответами.
*.controller.ts — нужен для обработки входящих запросов и отправки ответов. Он определяет, как приложение должно реагировать на определённые маршруты.
*.dto.ts (Data Transfer Object) — используется для структурирования данных при их передаче между клиентом и сервером, чтобы проще проверять входящие данные и преобразовывать их в нужный формат.
Далее сделаем краткий обзор всех модулей в проекте.
Модуль отвечает за управление пользователями и связанными с ними сущностями. Он предоставляет функциональность, связанную с обработкой пользователей, включая управление их профилями и ролями.
Контроллеры:
UsersController: Обрабатывает HTTP-запросы, связанные с пользователями, и направляет их в UsersService для выполнения бизнес-логики.
Поставщики:
UsersService: Предоставляет методы для управления пользователями, включая создание, обновление и получение данных пользователей.
MailService: Позволяет отправлять электронные письма пользователям, например, для подтверждения регистрации.
SequelizeModule.forFeature([User, Role, UserRoles, Post, Profile]): Позволяет работать с моделями User, Role, UserRoles, Post, Profile через ORM Sequelize.
RolesModule: Предоставляет функциональность для управления ролями пользователей в системе.
FilesModule: Обрабатывает операции с файлами, которые могут быть связаны с пользователями, например, загрузка аватаров.
forwardRef(() => AuthModule): Зависимость от AuthModule для управления аутентификацией, реализована через отложенную загрузку, чтобы избежать циклических зависимостей.
Экспортируемые компоненты:
UsersService: Экспортируется для использования в других модулях приложения, где требуется взаимодействие с пользователями.
UsersModule интегрируется с другими модулями системы, такими как AuthModule, для обеспечения полной функциональности управления пользователями, включая аутентификацию, авторизацию и работу с профилями.
Модель User
Модель User представляет собой сущность пользователя в приложении, сопоставленную с таблицей user в базе данных. Эта модель включает в себя атрибуты, необходимые для создания, аутентификации и управления учетной записью пользователя. Основные атрибуты и их описания:
id: Уникальный идентификатор пользователя.
userName: Уникальное имя пользователя.
email: Адрес электронной почты пользователя, необязательный.
password: Пароль пользователя, необязательный.
isActivated: Указывает, подтвержден ли электронный адрес пользователя.
activationLink: Токен для активации электронной почты.
banned: Статус, указывающий, заблокирован ли пользователь.
banReason: Причина блокировки пользователя.
avatar: Имя файла изображения аватара пользователя.
isDeleted: Указывает, удалена ли учетная запись пользователя.
userIp: IP-адрес пользователя при регистрации.
userAgent: Данные о браузере пользователя во время регистрации.
lastSeen: Метка времени последней активности пользователя.
msgPrivacy: Настройки конфиденциальности для сообщений.
userColor: Назначенный цвет для пользователя.
Установлены отношения через:
roles: Отношение «многие ко многим» с моделью Role.
posts: Отношение «один ко многим» с моделью Post.
tokens: Отношение «один ко многим» с моделью Token.
profile: Отношение «один к одному» с моделью Profile.
Модель обеспечивает целостность данных с помощью ограничений, таких как уникальные и ненулевые поля, предлагая комплексную структуру для управления данными, связанными с пользователями.
Контроллер UsersController
В контроллере User реализуются следующие методы API:
getAlladm: Этот метод позволяет администратору получить список всех пользователей. Доступ к методу ограничен только для пользователей с ролью «ADMIN». Метод поддерживает постраничное отображение результатов с параметрами page и pageSize.
getAll: Этот метод предоставляет возможность получить список всех пользователей. Доступ ограничен с помощью защитника AccessTokenGuard. Метод также поддерживает постраничное отображение с помощью параметров page и pageSize.
searchUsers: Метод предназначен для поиска пользователей по заданным критериям. Пользователь передает данные в формате SearchUsersDto, содержащие запрос, страницу и размер страницы. Доступ к методу защищен AccessTokenGuard.
getUserByIdAdmin: Этот метод позволяет администратору получить информацию об одном пользователе по его идентификатору (ID). Доступ к методу ограничен только для пользователей с ролью «ADMIN». Метод возвращает полные данные пользователя.
getUserByIdPublic: Этот метод позволяет получить публичные данные одного пользователя по его идентификатору (ID). Доступ к методу защищён токеном доступа. Возвращаются только публичные данные, которые доступны для общего просмотра.
getUserSelf: Этот метод позволяет пользователю получить собственные данные. Доступ к методу защищён токеном доступа, с проверкой, что запрашивающий пользователь является владельцем данных, либо имеет привилегии администратора или модератора.
getByEmail: Этот метод позволяет администратору получить одного пользователя по его Email. Доступ к методу ограничен только для пользователей с ролью «ADMIN».
getByUserNameAdmin: Этот метод позволяет администратору получить одного пользователя по его userName. Доступ ограничен для пользователей с ролью «ADMIN».
getByUserName: Этот метод позволяет получить информацию о пользователе по его userName. Доступ к методу ограничен с использованием Access Token.
editUserName: Этот метод позволяет пользователю или привилегированным пользователям (с ролями «ADMIN» или «MODERATOR») изменить имя пользователя. Запрос требует предоставления доступа с помощью AccessToken. После проверки прав доступа, метод вызывает сервис для редактирования имени пользователя.
editEmail: Метод предназначен для изменения адреса электронной почты пользователя. Только сам пользователь или пользователи с ролями «ADMIN» или «MODERATOR» могут выполнять эту операцию. Используется защита с помощью AccessToken для проверки прав доступа.
editPassword: Этот метод позволяет изменить пароль пользователя. Доступ к изменению пароля возможен для самого пользователя или для привилегированных пользователей с ролями «ADMIN» или «MODERATOR». Метод требует старый и новый пароли в теле запроса и защищен AccessToken’ом.
editAdmin: Этот метод позволяет администратору редактировать информацию для всех пользователей. Доступен только пользователям с ролью «ADMIN».
addRole: Метод позволяет администратору назначить роль пользователю. Доступ ограничен для пользователей с ролью «ADMIN».
ban: Используйте этот метод, чтобы забанить пользователя. Доступ предоставляется только администраторам.
confirmEmail: Этот метод отправляет ссылку для активации учетной записи пользователя на указанный Email. Доступен пользователю при наличии валидного токена, а также администратору и модератору.
activate: Этот метод активирует пользователя по предоставленной ссылке. При успешной активации выводится сообщение об успешной активации аккаунта. В случае ошибки возвращается статус ошибки с соответствующим сообщением.
setAvatar: Этот метод позволяет пользователю загрузить аватарку. Доступ к методу защищен и требует наличия действительного токена доступа. Загружаемый файл должен быть изображением с ограничением по размеру не более 10 МБ. Метод поддерживает проверку ролей, позволяя только самому пользователю или привилегированным ролям («ADMIN», «MODERATOR») изменить аватарку.
Сервис UsersService
В качестве примера кода приведем пару методов:
Метод searchUsers для поиска пользователей по логину, фамилии, или имени:
Модуль ProfileModule управляет функциональностью, связанной с профилями пользователей. Он включает контроллер ProfileController для обработки HTTP-запросов и сервис ProfileService для бизнес-логики. В модуле используются модели Profile и User с помощью SequelizeModule, что позволяет взаимодействовать с базой данных. Также, модуль экспортирует ProfileService, чтобы его можно было использовать в других модулях.
Модель Profile
Profile — это модель, представляющая профиль пользователя. Она содержит следующие поля:
userId: уникальный идентификатор пользователя, являющийся первичным ключом и внешним ключом, связывающим профиль с пользователем.
firstName: имя пользователя.
lastName: фамилия пользователя.
about: информация о пользователе.
birthday: дата рождения пользователя.
sex: пол пользователя (по умолчанию 0).
country: страна, в которой находится пользователь.
city: город, в котором находится пользователь.
Модель связывается с моделью User с помощью ассоциации BelongsTo.
Контроллер ProfileController
В контроллере Profile реализуются следующие методы API:
getById: Этот метод позволяет получить профиль пользователя по его идентификатору. Если идентификатор не указан, возвращается профиль текущего аутентифицированного пользователя. Доступ к методу ограничен аутентифицированными пользователями с использованием защитного механизма AccessTokenGuard.
editProfile: Этот метод позволяет редактировать профиль пользователя. Для выполнения операции необходимо наличие прав у текущего пользователя либо роль «ADMIN», либо «MODERATOR». Доступ к методу защищён с помощью AccessTokenGuard.
AuthModule — модуль аутентификации приложения, который объединяет функции работы с пользователями, JWT токенами и стратегиями доступа. Включает контроллер для управления аутентификацией и сервис для обработки логики аутентификации. Экспортирует AuthService и JwtModule для использования в других модулях.
Контроллер AuthController
В контроллере Auth реализуются следующие методы API:
registration: Этот метод осуществляет полную регистрацию нового пользователя. Принимает данные пользователя и создает учетную запись, запоминает IP-адрес и информацию о клиенте. После успешной регистрации возвращает токены доступа и обновления, сохраняет refresh токен в cookie, а также возвращает данные о пользователе и идентификатор сессии.
simpleRegistration: Этот метод выполняет регистрацию пользователя, используя только имя пользователя (userName). После успешной регистрации возвращает токен доступа (accessToken), идентификатор сессии (sessionId) и данные пользователя (userData). Также устанавливает cookie с токеном обновления (refreshToken) для дальнейшей аутентификации. Пользовательские данные, такие как IP-адрес и User Agent, фиксируются для регистрации.
login: Этот метод позволяет пользователю выполнить вход в систему. Он принимает учетные данные пользователя и возвращает accessToken и sessionId для аутентификации, а также сохраняет refreshToken в cookies. Метод также фиксирует IP-адрес и пользовательский агент пользователя.
logout: Этот метод выполняет выход пользователя из системы, удаляя сессию пользователя. Доступ к методу ограничен только для самого пользователя или пользователей с ролями «ADMIN» и «MODERATOR». После выполнения операции куки-файл ‘refreshToken’ очищается.
refreshTokens: Этот метод обновляет токены доступа для пользователя. Для выполнения требуется наличие refresh-токена, который отправляется в куки. Метод создает новые токены, обновляет куки-файлы и возвращает новый access-токен в ответе.
RolesModule отвечает за управление ролями в приложении. Он включает в себя провайдер RolesService для бизнес-логики, контроллер RolesController для обработки HTTP-запросов, и использует SequelizeModule.forFeature для работы с моделями Role, User и UserRoles. Также экспортирует RolesService для использования в других модулях.
Модель Roles
Roles— это модель, представляющая роли пользователя. Она содержит следующие поля:
id: Уникальный идентификатор роли.
value: Уникальное строковое значение, представляющее роль (например, ‘ADMIN’).
description: Описание роли.
Кроме того, с помощью декоратора @BelongsToMany устанавливается связь многие-ко-многим с моделью User через промежуточную таблицу UserRoles.
Модель UserRoles
UserRoles — это модель, представляющая связь между пользователями и их ролями в базе данных. Таблица ‘user.role’ состоит из уникальных идентификаторов для каждой записи, а также внешних ключей, ссылающихся на соответствующие записи в таблицах пользователей и ролей.
Контроллер RolesController
В контроллере Roles реализуются следующие методы API:
create: Этот метод позволяет создать новую роль. Метод принимает объект CreateRoleDto, содержащий данные для создания роли, и возвращает созданную роль.
getByValue: Этот метод позволяет получить роль на основе заданного значения. Метод принимает параметры запроса в виде объекта GetRoleDto и возвращает найденную роль.
Сервис RolesService
В данном случае кода совсем немного, и можно привести его целиком:
TokensModule управляет функциональностью по работе с токенами. Он включает в себя подключение модели Token через Sequelize, предоставляет логику обработки токенов через TokensService и контролирует взаимодействие с API через TokensController. Модуль также экспортирует TokensService для использования в других частях приложения.
Модель Token
Token — это модель, представляющая сессию пользователя в приложении. Она содержит следующие ключевые поля:
id: Уникальный идентификатор сессии.
userId: ID пользователя, связанный с данной сессией.
deviceId: Уникальный идентификатор устройства, с которого пользователь осуществил вход.
userIp: IP-адрес пользователя, использующийся для аутентификации.
userAgent: Информация о браузере и устройстве пользователя.
refreshToken: Уникальный токен для обновления сессии.
Модель связывается с пользователем через отношение BelongsTo, что позволяет получать информацию о пользователе для конкретной сессии.
Контроллер TokensController
Содержит один метод:
getTokens: позволяет пользователю получить список своих активных сессий. Доступ к методу ограничен и предоставляется лишь владельцу сессии, или пользователям, обладающим соответствующими правами доступа, такими как «ADMIN».
Сервис TokensService
TokensService — это сервис для управления токенами пользователей. Он предоставляет методы для обновления, сохранения и удаления токенов, а также для получения сессий пользователя:
updateToken: обновляет существующий токен по его идентификатору.
saveToken: сохраняет новый токен или обновляет существующий для указанной сессии пользователя.
removeToken: удаляет токен по идентификатору сессии.
findUserSession: находит сессию пользователя по ее идентификатору, включая имя пользователя.
getUserSessions: возвращает все сессии для заданного пользователя, исключая поле refreshToken и упорядочивая их по времени обновления.
Данный модуль предназначен для управления сущностями «друзья» в приложении. Он включает в себя FriendsService для обработки бизнес-логики и FriendsController для обработки HTTP-запросов. Модуль использует SequelizeModule для работы с моделью Friend, обеспечивая интеграцию с базой данных.
Модель Friend
Модель Friend нужна для обеспечения функциональности добавления пользователей в друзья, подписки на них. Таблица user.friend и содержит следующие основные поля:
id: Уникальный идентификатор.
from: Идентификатор пользователя, отправившего запрос на дружбу.
to: Идентификатор пользователя, получившего запрос на дружбу.
fromActive: Статус дружбы пользователя, инициировавшего запрос.
toActive: Статус дружбы пользователя, принимающего запрос.
fromActiveDate: Дата изменения статуса дружбы у пользователя, отправившего запрос.
toActiveDate: Дата изменения статуса дружбы у пользователя, получившего запрос.
Дополнительно, модель содержит ссылки на связанные модели пользователей, которые представлены через поля fromUser и toUser.
Контроллер FriendsController
Все методы защищены и требует использования токена доступа:
addFriend: добавляет пользователя в список друзей текущего пользователя.
deleteFriend: удаляет пользователя из списка друзей текущего пользователя.
isFriend: проверяет статус «дружбы» между текущим пользователем и указанным пользователем.
getFriends: возвращает список друзей пользователя с возможностью фильтрации по типу и постраничного отображения.
countFriends: возвращает количество друзей указанного пользователя.
searchFriends: осуществляет поиск друзей пользователя на основе заданного запроса.
Сервис FriendsService
Приведу полностью код friends.service.ts:
TypeScript
import { HttpException, HttpStatus, Injectable } from'@nestjs/common';import { InjectModel } from"@nestjs/sequelize";import { Friend } from"./friends.model";import { Sequelize, Op } from'sequelize';import { User } from"../users/user.model";import { Profile } from"../profile/profile.model";@Injectable()exportclassFriendsService {constructor(@InjectModel(Friend) privatefriendsRepository: typeofFriend) {}asyncaddFriend(from: number, to: number) {constfriend = awaitthis.friendsRepository.findOne({ where: {[Op.or]: [{ from, to }, { from:to, to:from }], }, });if (friend) {if(friend.from === from) { // Если я был подписан когда тоfriend.fromActive = true;friend.fromActiveDate = newDate().toISOString(); } elseif(friend.to === from) { // Если на меня подписан или когда то былfriend.toActive = true;friend.toActiveDate = newDate().toISOString(); }returnawaitfriend.save(); } else {//const friendCreate = awaitthis.friendsRepository.create({from, to, fromActive:true, fromActiveDate:newDate().toISOString()});//return friendCreate;return { status:true} } }asyncdeleteFriend(from: number, to: number) {constfriend = awaitthis.friendsRepository.findOne({ where: {[Op.or]: [{ from, to }, { from:to, to:from }], }, });if (friend) {if(friend.from === from) { // Если я был подписан когда тоfriend.fromActive = false;friend.fromActiveDate = newDate().toISOString(); } elseif(friend.to === from) { // Если на меня подписан или когда то былfriend.toActive = false;friend.toActiveDate = newDate().toISOString(); }awaitfriend.save();return { status:true} } }asyncisFriend(from: number, to: number) { // return: notFriends, friends, subscribed, subscriberconstfriend = awaitthis.friendsRepository.findOne({ where: {[Op.or]: [{ from, to }, { from:to, to:from }], }, });if (friend) {if(friend.from == from) { // Если я подписывалсяif(friend.fromActive === true && friend.toActive === true) { // Взаимная подпискаreturn"mutually"; // Друзья } elseif(friend.fromActive === true && friend.toActive === false) { // Я подписчик на негоreturn"subscribed"; // Подписан } elseif(friend.fromActive === false && friend.toActive === true) { // Он подписчик на меняreturn"subscriber"; // Подписчик } elseif(friend.fromActive === false && friend.toActive === false) { // Не друзя, но когда тоreturn"notFriends"; // Не друзья } } elseif(friend.to == from) { // Если на меня подписывалисьif(friend.toActive === true && friend.fromActive === true) { // Взаимная подпискаreturn"mutually"; // Друзья } elseif(friend.toActive === true && friend.fromActive === false) { // Я подписчик на негоreturn"subscribed"; // Подписан } elseif(friend.toActive === false && friend.fromActive === true) { // Он подписчик на меняreturn"subscriber"; // Подписчик } elseif(friend.toActive === false && friend.fromActive === false) { // Не друзя, но когда тоreturn"notFriends"; // Не друзья } } } else { // Не друзья, и никогда небылиreturn"notFriends"; // Не друзья } }asyncgetFriends(id: number, type: string, page: number = 1, pageSize: number = 10 ) {constoffset = (page - 1) * pageSize;constlimit = pageSize;letcondition = [];switch(type) {default: // mutuallycondition = [{ from:id, fromActive:true, toActive:true }, { to:id, fromActive:true, toActive:true }];break;case"subscribed":condition = [{ from:id, fromActive:true, toActive:false }, { to:id, fromActive:false, toActive:true }];break;case"subscriber":condition = [{ from:id, fromActive:false, toActive:true }, { to:id, fromActive:true, toActive:false }];break; }constfriends = awaitthis.friendsRepository.findAndCountAll({where: {[Op.or]:condition, },limit,offset,include: [ { model:User, as:'fromUser', attributes: ['id', 'userName', 'avatar'],include: [{model:Profile,as:'profile',attributes: ['firstName', 'lastName'] }] }, { model:User, as:'toUser', attributes: ['id', 'userName', 'avatar'],include: [{model:Profile,as:'profile',attributes: ['firstName', 'lastName'] }] }, ],attributes: ['id','from','to','fromActive','toActive' ] });consttotalPages = Math.ceil(friends.count / pageSize); constformatted = friends.rows.map(function(item) {letuser: { id: number, userName: string, avatar: string, profile: { firstName?: string, lastName?: string } };if(item.fromUser.id === id) {user = item.toUser; } elseif(item.toUser.id === id) {user = item.fromUser; }return {userId:user.id,userName:user.userName,avatar:user.avatar,firstName:user?.profile?.firstName,lastName:user?.profile?.lastName, }; })return {friends:formatted,page,pageSize,totalPages,totalFriends:friends.count } }asyncsearchFriends(query: string,id: number, type: string, page: number = 1, pageSize: number = 10 ) {constoffset = (page - 1) * pageSize;constlowercaseQuery = `%${query.toLowerCase()}%`;letconditionTypeFrom: object;letconditionTypeTo: object;switch(type) {case"mutually":conditionTypeFrom = { from:id, fromActive:true, toActive:true };conditionTypeTo = { to:id, fromActive:true, toActive:true };break;case"subscribed":conditionTypeFrom = { from:id, fromActive:true, toActive:false };conditionTypeTo = { to:id, fromActive:false, toActive:true };break;case"subscriber":conditionTypeFrom = { from:id, fromActive:false, toActive:true };conditionTypeTo = { to:id, fromActive:true, toActive:false };break;default: thrownewHttpException('friend type incorrect', HttpStatus.NOT_FOUND); }constuserSearchConditionFrom = {[Op.or]: [Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('fromUser.userName')), {[Op.like]:lowercaseQuery, }),Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('fromUser.profile.firstName')), {[Op.like]:lowercaseQuery, }),Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('fromUser.profile.lastName')), {[Op.like]:lowercaseQuery, }), ] };constuserSearchConditionTo = {[Op.or]: [Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('toUser.userName')), {[Op.like]:lowercaseQuery, }),Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('toUser.profile.firstName')), {[Op.like]:lowercaseQuery, }),Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('toUser.profile.lastName')), {[Op.like]:lowercaseQuery, }), ] };constwhereCondition = {[Op.or]: [ { [Op.and]: [ conditionTypeFrom, userSearchConditionTo ] }, { [Op.and]: [ conditionTypeTo, userSearchConditionFrom ] } ] }constfriends = awaitthis.friendsRepository.findAndCountAll({where:whereCondition,limit:pageSize,offset,include: [ { model:User, as:'fromUser', attributes: ['id', 'userName', 'avatar'],include: [{model:Profile,as:'profile',attributes: ['firstName', 'lastName'] }] }, { model:User, as:'toUser', attributes: ['id', 'userName', 'avatar'],include: [{model:Profile,as:'profile',attributes: ['firstName', 'lastName'] }] }, ],attributes: ['id','from','to','fromActive','toActive' ] });consttotalPages = Math.ceil(friends.count / pageSize); constformatted = friends.rows.map(function(item) {letuser: { id: number, userName: string, avatar: string, profile: { firstName?: string, lastName?: string } };if(item.fromUser.id === id) {user = item.toUser; } elseif(item.toUser.id === id) {user = item.fromUser; }return {id:item.id,userId:user.id,userName:user.userName,avatar:user.avatar,firstName:user?.profile?.firstName,lastName:user?.profile?.lastName, }; })return {friends:formatted,page,pageSize,totalPages,totalFriends:friends.count } }asynccountFriends(id: number) {constresults = awaitthis.friendsRepository.sequelize.query(` SELECT (SELECT COUNT(*) FROM "user.friend" WHERE ("from" = ? AND "fromActive" = true AND "toActive" = true) OR ("to" = ? AND "fromActive" = true AND "toActive" = true)) AS mutually, (SELECT COUNT(*) FROM "user.friend" WHERE ("from" = ? AND "fromActive" = true AND "toActive" = false) OR ("to" = ? AND "fromActive" = false AND "toActive" = true)) AS subscribed, (SELECT COUNT(*) FROM "user.friend" WHERE ("from" = ? AND "fromActive" = false AND "toActive" = true) OR ("to" = ? AND "fromActive" = true AND "toActive" = false)) AS subscriber `, { replacements: [id, id, id, id, id, id] });returnresults[0][0];}}
MessageModule — модуль, отвечающий за управление сообщениями. Он предоставляет и экспортирует MessageService, использует MessageController для управления запросами и включает зависимости от нескольких модулей: SubscribeModule, RoomModule, UsersModule и FriendsModule. Модуль также взаимодействует с моделью Message через Sequelize.
Модель Message
Message — модель, представляющая сообщения в системе мессенджера. Она описывается следующими полями:
id: Уникальный идентификатор сообщения.
owner: Идентификатор пользователя, который отправил сообщение. Является внешним ключом, ссылающимся на модель User.
room: Идентификатор комнаты, в которой размещено сообщение. Является внешним ключом, ссылающимся на модель Room.
content: Содержимое сообщения. По умолчанию это пустая строка.
type: Тип сообщения, например, текст, изображение или стикер.
replyTo: Идентификатор сообщения, на которое идет ответ. Является внешним ключом на саму модель Message.
deletedSelf: Флаг, указывающий на то, что пользователь удалил сообщение только для себя.
deletedAll: Флаг, указывающий на то, что сообщение удалено для всех пользователей.
Модель также определяет связи с другими моделями:
replyToMessage: Связь с сообщением, на которое был дан ответ.
ownerUser: Связь с владельцем сообщения, моделью User.
Контроллер MessageController
Доступ к методам защищен и требует валидного токена доступа:
createMessage: метод предназначен для создания сообщения. Указывается идентификатор получателя или комнаты, содержимое сообщения, тип и, при необходимости, ответ на другое сообщение.
getMessages: метод позволяет получить список сообщений. Указывается комната, из которой необходимо получить сообщения, а также параметры для навигации по страницам, такие как номер страницы и количество сообщений.
Модуль RoomModule управляет функциональностью комнат чата и взаимодействует с компонентами системы через сервис RoomService и контроллер RoomController. Он использует SequelizeModule для интеграции моделей Room, User и Message, обеспечивая хранение и обработку данных. Импортируются зависимые модули, такие как SubscribeModule (с отложенной загрузкой), FilesModule, UsersModule, и FriendsModule. Экспортируется RoomService.
Модель Room
Модель Room представляет собой сущность комнаты в системе обмена сообщениями, определяемую в таблице базы данных с именем messenger.room. Эта модель используется для управления основными атрибутами комнаты, включая информацию о создателе и владельце, параметры конфиденциальности и другие свойства. Модель содержит следующие поля:
id: Уникальный идентификатор комнаты.
ownerId: Идентификатор пользователя, который является владельцем комнаты. Может быть необязательным.
creatorId: Идентификатор пользователя, который создал комнату. Обязательное поле.
type: Тип комнаты, обозначаемый числовым значением. Обязательное поле.
name: Название комнаты.
about: Описание комнаты.
roomAvatar: Имя файла, представляющего аватар комнаты.
privacy: Уровень конфиденциальности комнаты.
approved: Флаг, указывающий, подтверждена ли комната.
banned: Флаг, указывающий, заблокирована ли комната.
banReason: Причина блокировки комнаты.
bannedAt: Дата, когда комната была заблокирована.
isDeleted: Флаг, указывающий, удалена ли комната.
disableEdit: Флаг для запрета редактирования комнаты.
readonly: Флаг для установки режима только для чтения.
allowSearch: Флаг, разрешающий поиск комнаты.
countSubscribers: Число подписчиков комнаты.
Связи:
ownerUser: Связь с пользователем, который является владельцем комнаты.
creatorUser: Связь с пользователем, который создал комнату.
roomMessages: Связь с сообщениями, принадлежащими данной комнате.
Контроллер RoomController
Доступ к методам ограничен и требуется наличия соответствующего токена доступа:
createRoom: Метод для создания новой комнаты. Метод принимает данные о типе комнаты, ее названии, приватности и описании.
editAvatar: Метод для изменения аватарки комнаты. Требует загрузки изображения с ограничением на размер (до 10 MB) и поддержкой определенных форматов (jpg, jpeg, png, gif, bmp, webp, avif). Включает возможность настраивать параметры обрезки изображения.
createDialogue: Метод для создания диалога между пользователями. Принимает идентификатор пользователя, с которым необходимо создать диалог.
getRoom: Метод для получения данных о конкретной комнате.
editRoom: Метод для редактирования параметров комнаты. Пользователь с соответствующими правами может изменить параметры комнаты, предоставив необходимые данные.
getRooms: Метод для получения списка всех комнат с возможностью фильтрации и постраничного отображения. Поддерживает параметры поиска и сортировки.
Сервис RoomService
В качестве примера кода приведем несколько методов:
Метод getRoomData для получения данных комнаты. Реализована сложная проверка на права доступа — пользователь получит только те данные, на которые у него есть права:
Модуль SubscribeModule представляет собой часть приложения, которая отвечает за управление подписками. Он включает контроллер SubscribeController для обработки входящих запросов и сервис SubscribeService для реализации бизнес-логики, связанной с подписками. Модуль использует SequelizeModule для работы с моделями данных Subscribe и Room, что обеспечивает взаимодействие с базой данных. Он также импортирует модуль RoomModule с помощью функции forwardRef, чтобы избежать циклической зависимости. SubscribeService экспортируется для использования в других модулях приложения.
Модель Subscribe
Subscribe — модель реализует систему подписок пользователей на определенные комнаты в мессенджере. Данная модель содержит информацию о пользователе, комнате, а также статусах подписки и уведомлений. У модели следующая структура:
id: Уникальный идентификатор подписки.
userId: Идентификатор пользователя, который подписан на комнату. Является внешним ключом, связывающим с моделью User.
roomId: Идентификатор комнаты, на которую подписан пользователь. Является внешним ключом, связывающим с моделью Room.
companionId: Опциональный идентификатор другого пользователя, связанного с данной подпиской. Связь с моделью User.
isSubscribed: Флаг, показывающий, активна ли подписка.
userRole: Роль пользователя в контексте подписки. Значения: 1 — пользователь, 2 — администратор.
notifyState: Состояние уведомлений.
isApproved: Флаг, показывающий, подтверждена ли подписка.
banned: Флаг, показывающий, забанен ли пользователь в данной комнате.
banReason: Причина блокировки пользователя в комнате.
bannedAt: Дата и время блокировки пользователя.
newMsgCount: Счетчик новых сообщений в комнате, которые пользователь еще не видел.
Связи:
subscribeUser: Пользователь, связанный с данной подпиской через userId.
companionUser: Опциональный пользователь, связанный через companionId.
subscribeRoom: Комната, связанная с подпиской через roomId.
Контроллер SubscribeController
Доступ к методам ограничен и требуется наличия соответствующего токена доступа:
subscribe: позволяет пользователю подписаться на определённую комнату. Метод учитывает привилегированные роли пользователя, такие как «ADMIN» или «MODERATOR», для обработки подписки.
getSubscribes: предоставляет пользователю возможность получить список всех его подписок. Метод поддерживает постраничный вывод результатов с возможностью поиска по ключевым словам.
changeNotifyState: позволяет пользователю изменить состояние уведомлений для конкретной комнаты. Пользователь может включить или выключить уведомления в зависимости от своих предпочтений.
Сервис SubscribeService
В качестве примера кода приведем несколько методов:
ChatGateway — это WebSocket-шлюз, который отвечает за обработку в реальном времени сообщений в чате, управление соединениями клиентов и их взаимодействие с комнатами в приложении.
Зависимости
ChatGateway построен на следующих сервисах:
MessageService для управления сообщениями.
SubscribeService для работы с подписками и комнатами.
RoomService для управления комнатами.
UsersService для управления пользователями и обновлением их статуса.
CORS
Разрешены запросы с указанных клиентов, взятых из переменных окружения WEB_CLIENT_URL и APP_CLIENT_URL.
Используется атрибут credentials: true для передачи учетных данных в кросс-доменных запросах.
Основные методы
Все методы защищены с помощью WsJwtGuard, что обеспечивает безопасность и проверку JWT-токена.
userOnlineEmit:
Метод для оповещения о статусе пользователя (онлайн/оффлайн) в определенной комнате. Обновляет время последнего появления пользователя с помощью usersService и отправляет оповещение в соответствующую комнату.
handleJoinUserToAllRooms:
Пользователь присоединяется ко всем своим подпискам и собственной комнате.
Вызывает userOnlineEmit для отправки уведомления о входе.
Позволяет пользователю присоединиться к определенной пользовательской комнате.
При успешном присоединении отправляет уведомление о статусе с помощью userOnlineEmit.
handleJoinToRoom:
Метод предназначен для присоединения пользователя к определенной комнате.
Проверяет уровень приватности комнаты через roomService и определяет доступ пользователя.
Если доступ разрешен, пользователь присоединяется к комнате, и вызывается метод userOnlineEmit для оповещения о его онлайн-статусе.
В случае отказа доступа возвращает сообщение об ошибке с информацией о запрете.
handleLeaveRoom:
Метод отвечает за выход пользователя из комнаты.
Оповещает о статусе пользователя (оффлайн) через метод userOnlineEmit.
Выполняет операцию выхода пользователя из комнаты и возвращает успешный результат. В случае ошибки возвращает сообщение об ошибке.
handleCallUser:
Используется для определения статуса пользователя
В случае успеха возвращает успешный ответ, или сообщение об ошибке.
handleUserOffline:
Метод для оповещения всех комнат о том, что пользователь ушел в оффлайн.
Использует userOnlineEmit, чтобы уведомить все комнаты, в которых состоял пользователь, о его уходе в оффлайн.
Возвращает успешный ответ, или сообщение об ошибке.
handleTyping:
Отправляет уведомление о статусе «печатает» в определенной комнате.
Метод транслирует статус «печатает» пользователю через событие broadcastTyping, добавляя информацию о пользователе, времени, и комнате.
Возвращает успешный ответ, или сообщение об ошибке.
handleCreateMessage:
Обрабатывает создание нового сообщения в чате.
Принимает данные сообщения через DTO MessageCreateDto и объект сокета AuthSocket.
Вызывает messageService для сохранения нового сообщения в базе данных.
Если сообщение успешно создано, отправляет его всем пользователям в соответствующей комнате с помощью client.broadcast.
Обновляет данные о диалоге, если он был создан, и присоединяет клиента к комнате.
Возвращает статус создания сообщения и данных о диалоге.
handleSubscribeRoom:
Обрабатывает подписку пользователя на комнату.
Принимает идентификатор комнаты и объект сокета AuthSocket.
Определяет, имеет ли пользователь привилегии администратора или модератора.
Использует subscribeService для добавления пользователя в подписку на указанную комнату.
Если подписка успешно создана и это групповая комната, создаёт служебное сообщение о новом подписчике и отправляет его остальным пользователям в комнате.
Возвращает статус успеха, или информацию об ошибке.
Socks5 — это универсальный протокол сетевого уровня, который используется для маршрутизации трафика между клиентами и серверами через прокси. Он предоставляет пользователям возможность безопасно и анонимно подключаться к интернет-ресурсам, поддерживая различные типы запросов, включая TCP и UDP. Протокол Socks5 стал популярен благодаря своей гибкости и относительно низкой задержке, что делает его хорошим выбором для обхода региональных ограничений, защиты конфиденциальности и оптимизации работы сетевых приложений.
В этой статье мы рассмотрим скрипт, который автоматизирует установку и конфигурирование прокси-сервера Dante на Ubuntu Server 24.04 LTS , с использованием аутентификации на основе имени пользователя и пароля. Dante является одним из наиболее известных и надежных серверных решений для реализации Socks5-прокси.
1. Проверяем, выполняется ли скрипт с правами суперпользователя, используя id -u. Если нет, завершаем с ошибкой:
4. Определение активного сетевого интерфейса: с помощью команды ip скрипт находит активный сетевой интерфейс, который используется для подключения к сети:
Bash
echo"Find active network interface..."INTERFACE=$(ip-o-4 route show to default |awk '{print $5}')if [ -z "$INTERFACE" ]; thenecho"Failed to find active network interface"exit1fiecho"Active network interface found: $INTERFACE"
5. Установка dante-server: с помощью команды apt-get устанавливается dante-server. Перед этим выполняется проверка на блокировку системы apt:
Bash
# Wait for unlocking:wait_for_lock() {whilefuser/var/lib/dpkg/lock-frontend >/dev/null 2>&1; doecho"Waiting for /var/lib/dpkg/lock-frontend to be unlocked..."sleep5done }echo"Install dante-server..."wait_for_lockapt-getupdatewait_for_lockapt-getinstall-ydante-server
6. Создание резервной копии конфигурационного файла: если конфигурационный файл dante уже существует, скрипт создаёт его резервную копию:
8. Создание учётной записи пользователя для аутентификации: создаётся новый пользователь и пароль для аутентификации при подключении к proxy. Имя пользователя — usrsocks, а пароль — тот, который вы передали в качестве аргумента:
Bash
echo"Create user $DANTE_USER for SOCKS5 auth..."useradd-s/bin/false$DANTE_USERecho"$DANTE_USER:$DANTE_PASSWORD" | chpasswd
9. Перезапуск службы danted: скрипт перезапускает сервис и добавляет его в автозапуск:
10. Для дополнительной оптимизации автоматизированного выполнения, обернем основное тело скрипта таким образом:
Bash
trap'echo -e "\033[31mSomething went wrong\033[0m"; exit 1'ERRset-eexportDEBIAN_FRONTEND=noninteractive# Script BODY# ...trap-ERRecho-e"\033[32mDante SOCKS5 proxy has been installed and configured with authentication\033[0m"
Таким образом, мы будем отслеживать любые ошибки во время выполнения команд, чем предотвратим дальнейшее частичное выполнение скрипта в результате возможных ошибок, а так же отключим любые нежелательные запросы, которые могут прервать выполнение установщика.
Использование скрипта
Скопируйте скрипт и сохраните его в файл (например, socks5.sh). Или клонируйте репозиторий.
Убедитесь, что у файла есть права на выполнение:
chmod +x socks5.sh
Запустите скрипт с использованием прав суперпользователя и передав ему пароль для аутентификации:
sudo install_dante.sh your_password
Как альтернативу назначению прав на выполнение, можно запустить скрипт через прямое указание интерпретатора bash, что может быть удобно при автоматизации:
sudo bash socks5.sh your_password
Проверка установки: убедитесь, что прокси сервер работает, проверив статус danted сервиса:
systemctl status danted
Сервис должен быть в состоянии active (running).
Запуск скрипта
Настройте ваше приложение для использования SOCKS5 прокси с адресом вашей машины и портом 1080. Введите учётные данные, указанные в скрипте (usrsocks и пароль your_password, переданный в качестве аргумента).
Полная версия скрипта приведена ниже:
Bash
#!/bin/bashif [ "$(id-u)" != "0" ]; thenecho-e"\033[31mThis script requires superuser rights.\033[0m"exit1fiif [ -z "$1" ]; thenecho"Please provide socks5 password as argument"exit1fiDANTE_CONF="/etc/danted.conf"DANTE_USER="usrsocks"DANTE_PASSWORD=$1trap'echo -e "\033[31mSomething went wrong\033[0m"; exit 1'ERRset-eexportDEBIAN_FRONTEND=noninteractive# Wait for unlocking:wait_for_lock() {whilefuser/var/lib/dpkg/lock-frontend >/dev/null 2>&1; doecho"Waiting for /var/lib/dpkg/lock-frontend to be unlocked..."sleep5done }echo"Find active network interface..."INTERFACE=$(ip-o-4 route show to default |awk '{print $5}')if [ -z "$INTERFACE" ]; thenecho"Failed to find active network interface"exit1fiecho"Active network interface found: $INTERFACE"echo"Install dante-server..."wait_for_lockapt-getupdatewait_for_lockapt-getinstall-ydante-serverecho"Backup existing configuration file $DANTE_CONF..."if [ -f "${DANTE_CONF}" ]; thencp"$DANTE_CONF""${DANTE_CONF}.bak"echo-e"\033[32mBackup existing configuration file\033[0m"fiNEW_CONFIG=$(cat<<-EOM logoutput: syslog stdout /data/logs/danted.log internal: $INTERFACE port = 1080 external: $INTERFACE socksmethod: username user.privileged: root user.unprivileged: nobody user.libwrap: nobody client pass { from: 0.0.0.0/0 to: 0.0.0.0/0 log: error connect disconnect } client block { from: 0.0.0.0/0 to: 0.0.0.0/0 log: connect error } socks pass { from: 0.0.0.0/0 to: 0.0.0.0/0 log: error connect disconnect } socks block { from: 0.0.0.0/0 to: 0.0.0.0/0 log: connect error }EOM)echo"Write configuration file $DANTE_CONF..."echo"$NEW_CONFIG" > "$DANTE_CONF"echo"Create user $DANTE_USER for SOCKS5 auth..."useradd-s/bin/false$DANTE_USERecho"$DANTE_USER:$DANTE_PASSWORD" | chpasswdecho"restart danted service..."systemctlrestartdantedsystemctlenabledantedtrap-ERRecho-e"\033[32mDante SOCKS5 proxy has been installed and configured with authentication\033[0m"