Перейти к содержанию
Версия: 3.3.51 Обновлено: 2025-11-13

Authentication System Architecture

Архитектурные принципы

Два типа токенов

  1. АДМИНСКИЕ ТОКЕНЫ (AdminClaims) - используются для /api/admin/* endpoints
  2. Полные права доступа к Crypto2B мониторингу
  3. Permissions и roles
  4. Email-based идентификация

  5. ПОЛЬЗОВАТЕЛЬСКИЕ ТОКЕНЫ (JWTClaims) - получаются через email-first регистрацию

  6. User-specific данные
  7. Email MANDATORY как primary identifier
  8. Используются для /api/user/* endpoints

Критическое правило

НИКОГДА НЕ СМЕШИВАЙ АДМИНСКИЕ И ПОЛЬЗОВАТЕЛЬСКИЕ ТОКЕНЫ!

Каждый тип токена имеет свой middleware и свои endpoints.

📧 Email как PRIMARY IDENTIFIER

КРИТИЧЕСКИЙ ПРИНЦИП: Email адрес является ЕДИНСТВЕННЫМ PRIMARY IDENTIFIER пользователя в системе Saga.

Архитектурные принципы Email системы

  • ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ: Email = единственный идентификатор (wallet addresses не используются)
  • АБСОЛЮТНАЯ ОБЯЗАТЕЛЬНОСТЬ: Все регистрации требуют email - if email == "" { return error }
  • УНИКАЛЬНОСТЬ: Database constraint email VARCHAR(255) UNIQUE NOT NULL
  • ВАЛИДАЦИЯ: Email validation на всех уровнях (frontend, backend, database)
  • SUPABASE SYNC: Email синхронизируется с Supabase Auth

Database Schema (Updated for Integration-Only)

-- ОБНОВЛЁННАЯ СХЕМА: EMAIL MANDATORY
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,  -- ЕДИНСТВЕННЫЙ identifier
    full_name VARCHAR(255),
    status VARCHAR(20) NOT NULL DEFAULT 'active',
    -- Убираны все wallet_address поля
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Таблица wallets больше НЕ НУЖНА
-- DROP TABLE IF EXISTS wallets CASCADE;

CREATE TABLE sessions (
    session_id VARCHAR(255) PRIMARY KEY,
    user_id UUID REFERENCES users(id),
    -- Email включается в JWT claims для идентификации
    expires_at TIMESTAMPTZ NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

Критические запреты:

  • ❌ Создание пользователей без email (КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО)
  • ❌ Использование wallet_address (таблица wallets удалена)
  • ❌ Synthetic email или email workarounds
  • ❌ Пропуск email в frontend API calls
  • ❌ Optional email в регистрации

JWT System

Архитектура JWT

Библиотека: github.com/golang-jwt/jwt/v4 - официальная, активно поддерживаемая

Clean Architecture слои:

backend/
├── cmd/jwt-helper/main.go              # CLI утилита
├── auth/service/jwt_mvp.go             # Основной сервис
├── shared/http/jwt_validator.go        # Валидация
└── auth/service/token_manager.go       # Claims управление

Генерация токенов

# Админские токены
make jwt-admin EMAIL=admin@saga-test.com
make jwt-admin-token EMAIL=admin@saga-test.com  # Только токен

# Пользовательские токены (с проверкой БД)
make jwt-user EMAIL=user@saga-test.com

# Валидация токена
make -f makefiles/development.mk jwt-validate TOKEN=<jwt_token>

Конфигурация JWT (через UnifiedConfig)

cfg := config.LoadUnifiedConfig()

secret := cfg.GetJWTSecret()
expirationHours := cfg.GetJWTExpirationHours()
issuer := cfg.GetJWTIssuer()
timeout := cfg.GetJWTHelperTimeout()

JWT Claims структуры

// AdminClaims для админских токенов
type AdminClaims struct {
    Email string `json:"email"`
    Permissions []string `json:"permissions"`
    Roles []string `json:"roles"`
    jwt.RegisteredClaims
}

// JWTClaims для пользовательских токенов
type JWTClaims struct {
    UserID string `json:"user_id"` // UUID вместо int64
    Email string `json:"email"`    // MANDATORY primary identifier
    FullName string `json:"full_name,omitempty"`
    // wallet_address УДАЛЕНО
    jwt.RegisteredClaims
}

Middleware Integration

Admin Middleware

// backend/shared/http/admin_auth_middleware.go
func AdminAuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Извлечение JWT из Authorization header
        tokenString := extractToken(r)

        // Валидация и парсинг AdminClaims
        claims, err := jwtValidator.ValidateAdminToken(tokenString)
        if err != nil {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }

        // Добавление claims в context
        ctx := context.WithValue(r.Context(), "admin_claims", claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

User Middleware

// backend/shared/http/auth_middleware.go
func UserAuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Извлечение и валидация JWT
        tokenString := extractToken(r)

        // Парсинг JWTClaims
        claims, err := jwtValidator.ValidateUserToken(tokenString)
        if err != nil {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }

        // Добавление claims в context
        ctx := context.WithValue(r.Context(), "jwt_claims", claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Email-First Authentication

Процесс регистрации пользователя

1. Frontend: Суpabase Auth (Google OAuth или email/password)
2. Frontend: Получение email из Supabase профиля (MANDATORY)
3. Backend: CreateUserFromEmail(email, fullName)
4. Backend: Создание user только с email identifier
5. Backend: Генерация JWT токена с email claims
6. Frontend: Сохранение токена в localStorage

Backend Implementation

func (s *UserService) CreateUserFromEmail(
    ctx context.Context,
    email string,
    fullName string,
) (*User, error) {
    // Валидация email (КРИТИЧЕСКИ ВАЖНО!)
    if email == "" {
        return nil, errors.New("email is MANDATORY - cannot create user without email")
    }

    // Проверка существующего пользователя
    existingUser, err := s.repo.GetUserByEmail(ctx, email)
    if err == nil && existingUser != nil {
        return existingUser, nil // Пользователь уже существует
    }

    // Создание пользователя (только email + metadata)
    user := &User{
        ID:       uuid.New().String(),
        Email:    email,
        FullName: fullName,
        Status:   "active",
    }

    if err := s.repo.CreateUser(ctx, user); err != nil {
        return nil, fmt.Errorf("failed to create user: %w", err)
    }

    return user, nil
}

Frontend Integration (Supabase Auth)

// Supabase Auth (Google OAuth + email mandatory)
import { supabase } from './supabase-client';

const signInWithGoogle = async () => {
  try {
    const { data, error } = await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: {
        redirectTo: `${window.location.origin}/api/auth/supabase/callback`
      }
    });

    if (error) throw error;
  } catch (error) {
    console.error('Google Auth error:', error);
  }
};

// Обработка успешной аутентификации
const handleAuthCallback = async () => {
  const { data: { user } } = await supabase.auth.getUser();

  if (!user?.email) {
    throw new Error('Email is MANDATORY - authentication failed');
  }

  // Создание/получение пользователя через Backend API
  const response = await fetch('/api/auth/supabase/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${user.access_token}`
    },
    body: JSON.stringify({
      email: user.email,
      fullName: user.user_metadata?.full_name || '',
    })
  });

  const { token } = await response.json();

  // Сохранение JWT токена
  localStorage.setItem('auth_token', token);
};

Sessions Management

Таблица sessions - единый источник истины для управления сессиями и токенами:

CREATE TABLE sessions (
    session_id VARCHAR(255) PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    token_hash VARCHAR(255),
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

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

  • Связывает session_id с user через email в JWT claims
  • Хранит hash токена для валидации
  • Управляет временем жизни сессий
  • НЕТ отдельной таблицы token_blacklist - всё в sessions!

Информация об администраторах

КРИТИЧЕСКОЕ ПРАВИЛО: Информация об администраторах и стратегии инвестирования хранятся ТОЛЬКО в конфигурации.

  • ✅ Админы определены в config/auth.yaml
  • ✅ Стратегии в config/limits.yaml
  • НИКАКИХ таблиц в базе данных для админов
  • НИКАКИХ таблиц admin_tasks, admin_sessions

Обоснование: Минимальная архитектура БД согласно принципам CLAUDE.md.

E2E Testing с JWT

Архитектура E2E JWT

Полная интеграция с Go backend - никаких моков:

// frontend/e2e/utils/admin-jwt-helper.ts - ОСНОВНОЙ хелпер
import { generateAdminJWT } from './admin-jwt-helper';

test('admin can approve withdrawal', async ({ page }) => {
  // Динамическая генерация токена через Go backend
  const adminToken = await generateAdminJWT('admin@saga-test.com');

  // Использование в тестах
  await page.setExtraHTTPHeaders({
    'Authorization': `Bearer ${adminToken}`
  });

  // Или через localStorage
  await page.evaluate((token) => {
    localStorage.setItem('admin_auth_token', token);
  }, adminToken);
});

Ключевые принципы:

  • ДИНАМИЧЕСКАЯ ГЕНЕРАЦИЯ: Всегда используй generateAdminJWT() через Go backend
  • НЕТ ХАРДКОДА: Никаких фиксированных токенов в коде
  • ЦЕНТРАЛИЗОВАННАЯ КОНФИГУРАЦИЯ: Все настройки через test-config.ts
  • СТАНДАРТ ИМЕНОВАНИЯ: admin_auth_token (underscore format) в localStorage

Связанная документация

Best Practices

  1. Email АБСОЛЮТНО обязателен при любой регистрации (КРИТИЧНО)
  2. Никогда не смешивай админские и пользовательские токены
  3. Используй UnifiedConfig для всех JWT настроек
  4. Валидируй email на всех уровнях (frontend, backend, database)
  5. Храни токены в localStorage (frontend) или context (backend)
  6. Тестируй с реальными токенами через jwt-helper утилиту
  7. Логируй auth failures для security audit
  8. НЕ ИСПОЛЬЗУЙ wallet addresses как идентификаторы
  9. Sync with Supabase для всех email operations
  10. Reject synthetic emails или email workarounds