zapishis-client/apps/web/BAN_CHECK_README.md
vchikalkin 7c1f79db2a feat(ban-system): implement multi-level user ban checks across services
- Added a comprehensive ban checking system to prevent access for banned users at multiple levels, including database, API, and client-side.
- Introduced `bannedUntil` field in the customer model to manage temporary and permanent bans effectively.
- Enhanced `BaseService` and various service classes to include ban checks, ensuring that banned users cannot perform actions or access data.
- Updated error handling to provide consistent feedback for banned users across the application.
- Improved user experience with a dedicated ban check component and a user-friendly ban notification page.
2025-08-26 12:32:22 +03:00

9.5 KiB
Raw Permalink Blame History

Система проверки бана пользователей

Обзор

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

1. База данных (bannedUntil поле)

В Strapi добавлено поле bannedUntil типа datetime в модель Customer:

  • null = пользователь не забанен
  • дата в будущем = временный бан до указанной даты
  • дата в далеком будущем = постоянный бан

2. Утилита проверки (packages/utils/src/customer.ts)

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 добавлена проверка бана:

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:

/**
 * Универсальная проверка статуса бана пользователя
 * Должна вызываться в начале каждого метода сервиса
 */
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 };
}

Использование в сервисах:

async someMethod() {
  await this.checkIsBanned(); // Проверка бана в начале метода
  // ... остальная логика
}

Обновленные сервисы:

  • CustomersService - все методы
  • ServicesService - все методы
  • OrdersService - все методы
  • SlotsService - все методы
  • RegistrationService - добавлена собственная проверка

Преимущества:

  • Автоматическая проверка во всех сервисах, наследующих от BaseService
  • Единая точка проверки бана
  • Работает как в веб-приложении, так и в боте
  • Простота добавления в новые методы
  • Защита всех API методов от забаненных пользователей

5. Защита от изменения статуса бана (packages/graphql/api/customers.ts и registration.ts)

Пользователи не могут изменять поле bannedUntil самостоятельно:

// В CustomersService
async updateCustomer(variables: Omit<VariablesOf<typeof GQL.UpdateCustomerDocument>, '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<typeof GQL.UpdateCustomerDocument>) {
  // Проверяем бан для существующего пользователя
  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 компонент для проверки бана на клиенте:

export function BanCheck({ children }: Readonly<PropsWithChildren>) {
  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:

export default function Layout({ children }: Readonly<PropsWithChildren>) {
  return (
    <Provider>
      <BanCheck>
        <UpdateProfile />
        <main className="grow">{children}</main>
        <BottomNav />
      </BanCheck>
    </Provider>
  );
}

7. Hook для проверки бана (hooks/api/customers.ts)

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)

export const ERRORS = {
  NO_PERMISSION: 'Нет доступа',
} as const;

Архитектура системы

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Next Auth     │    │   BaseService   │    │  Client-side    │
│   (авторизация) │    │  (API методы)   │    │   (UI)          │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         ▼                       ▼                       ▼
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│  isCustomerBanned│    │ checkBanStatus()│    │   BanCheck      │
│   (утилита)     │    │   (метод)       │    │  (компонент)    │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         └───────────────────────┼───────────────────────┘
                                 ▼
                    ┌─────────────────────────┐
                    │   bannedUntil (DB)      │
                    │   (Strapi/PostgreSQL)   │
                    └─────────────────────────┘

Преимущества системы

Многоуровневая защита - проверка на всех уровнях приложения
Универсальность - работает в веб-приложении и боте
Простота использования - один вызов checkBanStatus() в начале метода
Безопасность - пользователи не могут обойти бан
UX - понятные сообщения и страница для забаненных
DRY принцип - нет дублирования кода
Легкость расширения - просто добавить новые проверки