# Система проверки бана пользователей ## Обзор Реализована многоуровневая система проверки бана пользователей, которая предотвращает доступ заблокированных пользователей к функциональности приложения. ## 1. База данных (`bannedUntil` поле) В Strapi добавлено поле `bannedUntil` типа `datetime` в модель `Customer`: - `null` = пользователь не забанен - `дата в будущем` = временный бан до указанной даты - `дата в далеком будущем` = постоянный бан ## 2. Утилита проверки (`packages/utils/src/customer.ts`) ```typescript export function isCustomerBanned(customer: { bannedUntil?: string | null }): boolean { return Boolean(customer.bannedUntil && new Date() < new Date(customer.bannedUntil)); } ``` ## 3. Next Auth проверка (`apps/web/config/auth.ts`) В `authorize` callback добавлена проверка бана: ```typescript async authorize(credentials) { const { telegramId } = credentials ?? {}; if (!telegramId) { throw new Error('Invalid Telegram ID'); } try { const { query } = await getClientWithToken(); const result = await query({ query: GetCustomerDocument, variables: { telegramId: Number(telegramId) }, }); const customer = result.data.customers.at(0); if (!customer || isCustomerBanned(customer)) { throw new Error('User is banned or not found'); } return { id: telegramId }; } catch (error) { throw new Error('Authentication failed'); } } ``` ## 4. Универсальная проверка в BaseService (`packages/graphql/api/base.ts`) Добавлен метод `checkIsBanned()` в `BaseService`: ```typescript /** * Универсальная проверка статуса бана пользователя * Должна вызываться в начале каждого метода сервиса */ protected async checkIsBanned() { const { query } = await getClientWithToken(); const result = await query({ query: GQL.GetCustomerDocument, variables: this._user, }); const customer = result.data.customers.at(0); if (!customer) { throw new Error(BASE_ERRORS.NOT_FOUND_CUSTOMER); } if (isCustomerBanned(customer)) { throw new Error(ERRORS.NO_PERMISSION); } return { customer }; } ``` **Использование в сервисах:** ```typescript async someMethod() { await this.checkIsBanned(); // Проверка бана в начале метода // ... остальная логика } ``` **Обновленные сервисы:** - ✅ `CustomersService` - все методы - ✅ `ServicesService` - все методы - ✅ `OrdersService` - все методы - ✅ `SlotsService` - все методы - ✅ `RegistrationService` - добавлена собственная проверка **Преимущества:** - Автоматическая проверка во всех сервисах, наследующих от BaseService - Единая точка проверки бана - Работает как в веб-приложении, так и в боте - Простота добавления в новые методы - Защита всех API методов от забаненных пользователей ## 5. Защита от изменения статуса бана (`packages/graphql/api/customers.ts` и `registration.ts`) Пользователи не могут изменять поле `bannedUntil` самостоятельно: ```typescript // В CustomersService async updateCustomer(variables: Omit, 'documentId'>) { await this.checkBanStatus(); const { customer } = await this._getUser(); // Проверяем, что пользователь не пытается изменить поле bannedUntil if (variables.data.bannedUntil !== undefined) { throw new Error(ERRORS.NO_PERMISSION); } // ... остальная логика обновления } // В RegistrationService async updateCustomer(variables: VariablesOf) { // Проверяем бан для существующего пользователя if (variables.documentId) { const { query } = await getClientWithToken(); const result = await query({ query: GQL.GetCustomerDocument, variables: { documentId: variables.documentId }, }); const customer = result.data.customers.at(0); if (customer && isCustomerBanned(customer)) { throw new Error(ERRORS.NO_PERMISSION); } } if (variables.data.bannedUntil) { throw new Error(ERRORS.NO_PERMISSION); } // ... остальная логика обновления } ``` **Преимущества:** - Пользователи не могут снять с себя бан - Только администраторы могут изменять статус блокировки - Дополнительный уровень безопасности - Защита работает во всех сервисах, которые обновляют данные пользователей - Единая ошибка `NO_PERMISSION` для всех случаев отсутствия доступа ## 6. Client-side Check (`components/auth/ban-check.tsx`) React компонент для проверки бана на клиенте: ```typescript export function BanCheck({ children }: Readonly) { const { data: session } = useSession(); const router = useRouter(); const isBanned = useIsBanned(); useEffect(() => { if (session?.user?.telegramId && isBanned) { router.push('/banned'); } }, [session?.user?.telegramId, isBanned, router]); if (session?.user?.telegramId && isBanned) { return null; } return <>{children}; } ``` **Использование в layout:** ```typescript export default function Layout({ children }: Readonly) { return (
{children}
); } ``` ## 7. Hook для проверки бана (`hooks/api/customers.ts`) ```typescript export const useIsBanned = () => { const { data: { customer } = {} } = useCustomerQuery(); if (!customer) return false; return isCustomerBanned(customer); }; ``` ## 8. Страница для забаненных пользователей (`apps/web/app/(auth)/banned/page.tsx`) Создана специальная страница с информацией о бане и возможностью выхода из аккаунта. ## 9. Централизованные ошибки (`packages/graphql/constants/errors.ts`) ```typescript export const ERRORS = { NO_PERMISSION: 'Нет доступа', } as const; ``` ## Архитектура системы ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Next Auth │ │ BaseService │ │ Client-side │ │ (авторизация) │ │ (API методы) │ │ (UI) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ isCustomerBanned│ │ checkBanStatus()│ │ BanCheck │ │ (утилита) │ │ (метод) │ │ (компонент) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ └───────────────────────┼───────────────────────┘ ▼ ┌─────────────────────────┐ │ bannedUntil (DB) │ │ (Strapi/PostgreSQL) │ └─────────────────────────┘ ``` ## Преимущества системы ✅ **Многоуровневая защита** - проверка на всех уровнях приложения ✅ **Универсальность** - работает в веб-приложении и боте ✅ **Простота использования** - один вызов `checkBanStatus()` в начале метода ✅ **Безопасность** - пользователи не могут обойти бан ✅ **UX** - понятные сообщения и страница для забаненных ✅ **DRY принцип** - нет дублирования кода ✅ **Легкость расширения** - просто добавить новые проверки