- 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.
9.5 KiB
Система проверки бана пользователей
Обзор
Реализована многоуровневая система проверки бана пользователей, которая предотвращает доступ заблокированных пользователей к функциональности приложения.
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 принцип - нет дублирования кода
✅ Легкость расширения - просто добавить новые проверки