Перейти к основному содержимому

Контроль доступа по NFT

Описание

Система контроля доступа на основе владения продуктовыми NFT. Реализована через NestJS Guard (RoleByNftGuard) и декоратор (@RoleByNft), которые проверяют наличие у пользователя NFT определенного типа перед предоставлением доступа к защищенным endpoint'ам.

Модуль находится в shared-пакете @magnetmlm/common-backend и может использоваться во всех backend сервисах проекта.

Архитектура

Компоненты

КомпонентТипРасположениеОписание
RoleByNftGuardNestJS Guardpackages/reusable-magnet-backendGuard для проверки доступа по NFT
@RoleByNftDecoratorpackages/reusable-magnet-backendДекоратор для маркировки защищенных endpoint'ов
checkProductAccessByNftService Methodapps/backend/src/nft/nft.service.tsПроверка владения NFT через контракт

Логика работы

1. Общий процесс проверки

Request → JwtAuthGuard → RoleByNftGuard → checkProductAccessByNft → Allow/Deny

Этапы:

  1. Пользователь отправляет запрос с JWT токеном
  2. JwtAuthGuard проверяет токен и извлекает данные пользователя
  3. RoleByNftGuard проверяет метаданные endpoint'а (декоратор @RoleByNft)
  4. Если требуется NFT — вызывается checkProductAccessByNft()
  5. Проверяется владение NFT через контракт и БД
  6. Доступ разрешен или выбрасывается ForbiddenException

2. Приоритеты доступа

Guard работает с учетом иерархии ролей:

Автоматический доступ (bypass):

  • UserRole.Admin
  • UserRole.UltimateAdmin
  • ✅ Endpoint без декоратора @RoleByNft
  • ✅ Отсутствие NftProductAccessService (fallback для совместимости)

Требуется проверка NFT:

  • Обычные пользователи
  • Endpoint с декоратором @RoleByNft(NftType.SMARTLINK) и т.д.

3. Проверка владения NFT

apps/backend/src/nft/nft.service.ts
/**
* Проверяет доступ пользователя к продукту через владение NFT
* @param uid - ID пользователя из diamond контракта
* @param nftType - Тип NFT (SMARTLINK | TG_SOFT)
* @returns true если у пользователя есть хотя бы один NFT данного типа
*/
public async checkProductAccessByNft(
uid: number,
nftType: NftType,
): Promise<boolean> {
// 1. Найти пользователя по uid
const user = await this.userRepository.findOne({ where: { uid } });

// 2. Получить token IDs из контракта по адресу пользователя
const tokenIds = await this.getUserNftTokenIds(nftType, user.addr);

// 3. Проверить наличие в БД
const count = await this.nftSchema.countDocuments({
nftId: { $in: tokenIds.map(Number) },
});

return count > 0;
}

Процесс проверки:

  1. Поиск пользователя в MySQL по uid
  2. Получение списка tokenIds NFT с блокчейна (через getUserNftTokenIds)
  3. Проверка существования хотя бы одного NFT в MongoDB
  4. Возврат true если NFT найден, иначе false

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

Защита контроллера

apps/backend/src/slink/slink.controller.ts
import { UseGuards } from '@nestjs/common';
import {
JwtAuthGuard,
RoleByNftGuard,
RoleByNft,
} from '@magnetmlm/common-backend';
import { NftType } from '@magnetmlm/common';

@Controller('slink')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RoleByNftGuard)
@RoleByNft(NftType.SMARTLINK)
export class SlinkController {
// Все endpoint'ы требуют наличия SMARTLINK NFT

@Get()
getAllSmartlinks() {
// Доступ только для владельцев SMARTLINK NFT
}
}

Защита отдельного endpoint'а

@Controller('product')
@UseGuards(JwtAuthGuard)
export class ProductController {
@Get('smartlink-feature')
@UseGuards(RoleByNftGuard)
@RoleByNft(NftType.SMARTLINK)
getSmartlinkFeature() {
// Требуется SMARTLINK NFT
}

@Get('tg-soft-feature')
@UseGuards(RoleByNftGuard)
@RoleByNft(NftType.TG_SOFT)
getTgSoftFeature() {
// Требуется TG_SOFT NFT
}

@Get('public-feature')
getPublicFeature() {
// Доступно всем авторизованным пользователям
}
}

Множественные требования NFT

@Get('advanced-feature')
@UseGuards(RoleByNftGuard)
@RoleByNft(NftType.SMARTLINK, NftType.TG_SOFT)
getAdvancedFeature() {
// Требуется наличие ОБОИХ типов NFT
}

Интеграция в модуль

1. Подключение в модуле

apps/backend/src/slink/slink.module.ts
import { Module } from '@nestjs/common';
import { RoleByNftGuard } from '@magnetmlm/common-backend';
import { NftModule } from '../nft/nft.module';

@Module({
imports: [NftModule],
controllers: [SlinkController],
providers: [
SlinkService,
RoleByNftGuard,
{
provide: 'NftProductAccessService',
useExisting: NftService,
},
],
})
export class SlinkModule {}

Важно:

  • Импортировать NftModule для доступа к NftService
  • Зарегистрировать провайдер 'NftProductAccessService'NftService
  • Добавить RoleByNftGuard в providers

2. Реализация интерфейса

NftService должен реализовывать интерфейс INftProductAccessChecker:

packages/reusable-magnet-backend/src/role-by-nft.guard.ts
export interface INftProductAccessChecker {
checkProductAccessByNft(
uid: number,
productType: NftType,
): Promise<boolean>;
}

Обработка ошибок

Ошибки Guard'а

ForbiddenException:

{
"statusCode": 403,
"message": "Access denied. NFT product required: SMARTLINK"
}

Выбрасывается когда:

  • Пользователь не владеет требуемым NFT
  • У пользователя нет ни одного NFT указанного типа

Ошибки проверки доступа

NotFoundException (внутренняя):

  • User uid not found — uid не передан
  • User not found — пользователь не найден в БД
  • balance not found — нет NFT на балансе в контракте

Обработка:

apps/backend/src/nft/nft.service.ts
try {
const user = await this.userRepository.findOne({ where: { uid } });
// ... проверка
return count > 0;
} catch (error) {
console.error('Error checking product access by NFT:', error);
return false; // При ошибке доступ запрещен
}

Фильтрация ролей

Guard учитывает дату начала действия роли:

packages/reusable-magnet-backend/src/role-by-nft.guard.ts
const roles = user.roles
?.filter(
(role) =>
!role.startDate || Date.now() > new Date(role.startDate).valueOf(),
)
.map((role) => role.role);

if (
roles?.includes(UserRole.Admin) ||
roles?.includes(UserRole.UltimateAdmin)
) {
return true; // Bypass для администраторов
}

Логика:

  • Фильтрация ролей с startDate в будущем
  • Проверка наличия Admin или UltimateAdmin
  • Если есть — автоматический доступ без проверки NFT

Примеры использования

Пример 1: Защита всего контроллера

@Controller('smartlink')
@UseGuards(JwtAuthGuard, RoleByNftGuard)
@RoleByNft(NftType.SMARTLINK)
export class SmartlinkController {
@Get()
getAll() {
/* Требуется SMARTLINK NFT */
}

@Post()
create() {
/* Требуется SMARTLINK NFT */
}
}

Пример 2: Разные требования для endpoint'ов

@Controller('features')
@UseGuards(JwtAuthGuard)
export class FeaturesController {
@Get('basic')
getBasic() {
// Доступно всем авторизованным
}

@Get('smartlink')
@UseGuards(RoleByNftGuard)
@RoleByNft(NftType.SMARTLINK)
getSmartlink() {
// Только для владельцев SMARTLINK
}

@Get('premium')
@UseGuards(RoleByNftGuard)
@RoleByNft(NftType.SMARTLINK, NftType.TG_SOFT)
getPremium() {
// Требуется оба типа NFT
}
}

Пример 3: Обработка на клиенте

// Frontend
async function accessSmartlinkFeature() {
try {
const response = await api.get('/slink');
return response.data;
} catch (error) {
if (error.response?.status === 403) {
// Показать пользователю:
// "Для доступа требуется SMARTLINK NFT"
showNftRequiredModal('SMARTLINK');
}
}
}

Ссылки