Контроль доступа по NFT
Описание
Система контроля доступа на основе владения продуктовыми NFT. Реализована через NestJS Guard (RoleByNftGuard) и декоратор (@RoleByNft), которые проверяют наличие у пользователя NFT определенного типа перед предоставлением доступа к защищенным endpoint'ам.
Модуль находится в shared-пакете @magnetmlm/common-backend и может использоваться во всех backend сервисах проекта.
Архитектура
Компоненты
| Компонент | Тип | Расположение | Описание |
|---|---|---|---|
RoleByNftGuard | NestJS Guard | packages/reusable-magnet-backend | Guard для проверки доступа по NFT |
@RoleByNft | Decorator | packages/reusable-magnet-backend | Декоратор для маркировки защищенных endpoint'ов |
checkProductAccessByNft | Service Method | apps/backend/src/nft/nft.service.ts | Проверка владения NFT через контракт |
Логика работы
1. Общий процесс проверки
Request → JwtAuthGuard → RoleByNftGuard → checkProductAccessByNft → Allow/Deny
Этапы:
- Пользователь отправляет запрос с JWT токеном
JwtAuthGuardпроверяет токен и извлекает данные пользователяRoleByNftGuardпроверяет метаданные endpoint'а (декоратор@RoleByNft)- Если требуется NFT — вызывается
checkProductAccessByNft() - Проверяется владение NFT через контракт и БД
- Доступ разрешен или выбрасывается
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;
}
Процесс проверки:
- Поиск пользователя в MySQL по
uid - Получение списка
tokenIdsNFT с блокчейна (черезgetUserNftTokenIds) - Проверка существования хотя бы одного NFT в MongoDB
- Возврат
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');
}
}
}