Authentication System Architecture¶
Архитектурные принципы¶
Два типа токенов¶
- АДМИНСКИЕ ТОКЕНЫ (
AdminClaims) - используются для/api/admin/*endpoints - Полные права доступа к Crypto2B мониторингу
- Permissions и roles
-
Email-based идентификация
-
ПОЛЬЗОВАТЕЛЬСКИЕ ТОКЕНЫ (
JWTClaims) - получаются через email-first регистрацию - User-specific данные
- Email MANDATORY как primary identifier
- Используются для
/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¶
- Email АБСОЛЮТНО обязателен при любой регистрации (КРИТИЧНО)
- Никогда не смешивай админские и пользовательские токены
- Используй UnifiedConfig для всех JWT настроек
- Валидируй email на всех уровнях (frontend, backend, database)
- Храни токены в localStorage (frontend) или context (backend)
- Тестируй с реальными токенами через jwt-helper утилиту
- Логируй auth failures для security audit
- НЕ ИСПОЛЬЗУЙ wallet addresses как идентификаторы
- Sync with Supabase для всех email operations
- Reject synthetic emails или email workarounds