Files
xtremobile/IMPLEMENTATION_GUIDE.md
T
GitHub Copilot c5fcafcce1 Phase 2: Performance Profiling Setup - Add profiling utilities
- Add StartupProfiler: Measure cold/warm start times
- Add VideoProfiler: Profile video load, playback metrics
- Add MemoryProfiler: Detect memory leaks, monitor heap growth
- Add HiveEncryptionBenchmark: Measure encryption overhead
- Add PHASE_2_PROFILING_PLAN.md: Complete testing procedures

Ready for profiling execution once Flutter SDK is initialized
2026-03-25 21:56:08 +01:00

17 KiB

🔧 GUIDE D'IMPLÉMENTATION TECHNIQUE

1️⃣ LOGGING PROFESSIONNEL (Jour 1-2)

1.1 Ajouter dépendances

# pubspec.yaml (bin/)
dependencies:
  logger: ^2.2.0
  path: ^1.9.0

1.2 Créer LoggingService (Backend)

Fichier: bin/services/logging_service.dart

import 'dart:io';
import 'package:logger/logger.dart';

class LoggingService {
  static final LoggingService _instance = LoggingService._internal();
  factory LoggingService() => _instance;

  late final logger = Logger(
    printer: PrettyPrinter(
      methodCount: 2,
      errorMethodCount: 8,
      lineLength: 120,
      colors: true,
      printEmojis: true,
      dateTimeFormat: DateTimeFormat.dateAndTime,
    ),
  );

  LoggingService._internal() {
    _initializeFileHandler();
  }

  void _initializeFileHandler() {
    final logsDir = Directory('/app/logs');
    if (!logsDir.existsSync()) {
      logsDir.createSync(recursive: true);
    }
  }

  void logRequest(String method, String path, int statusCode, int durationMs) {
    final emoji = statusCode < 400 ? '✅' : '❌';
    logger.i('$emoji $method $path → $statusCode (${durationMs}ms)');
  }

  void logError(String message, Object error, StackTrace stackTrace) {
    logger.e(message, error: error, stackTrace: stackTrace);
  }

  void logWarning(String message) => logger.w(message);
  void logInfo(String message) => logger.i(message);
  void logDebug(String message) => logger.d(message);
}

1.3 Middleware Logging (Backend)

Fichier: bin/middleware/logging_middleware.dart

import 'package:shelf/shelf.dart';
import '../services/logging_service.dart';
import 'dart:async';

Middleware loggingMiddleware() {
  final logger = LoggingService();

  return (Handler handler) {
    return (Request request) async {
      final stopwatch = Stopwatch()..start();

      try {
        final response = await handler(request);

        logger.logRequest(
          request.method,
          request.url.path,
          response.statusCode,
          stopwatch.elapsedMilliseconds,
        );

        return response;
      } catch (e, st) {
        stopwatch.stop();
        logger.logError(
          'Error handling ${request.method} ${request.url.path}',
          e,
          st,
        );
        rethrow;
      }
    };
  };
}

2️⃣ SYSTÈME D'ERREURS UNIFIÉ (Jour 2)

2.1 Exceptions Personnalisées

Fichier: bin/utils/app_exceptions.dart

abstract class AppException implements Exception {
  final String message;
  final String code;
  final StackTrace? stackTrace;
  final Map<String, dynamic>? context;
  final int httpCode;

  AppException({
    required this.message,
    required this.code,
    this.stackTrace,
    this.context,
    this.httpCode = 500,
  });

  Map<String, dynamic> toJson() => {
    'code': code,
    'message': message,
    'context': context,
  };

  @override
  String toString() => '$code: $message';
}

class AuthException extends AppException {
  AuthException({
    required String message,
    String code = 'AUTH_ERROR',
    Map<String, dynamic>? context,
  }) : super(
    message: message,
    code: code,
    context: context,
    httpCode: 401,
  );
}

class ValidationException extends AppException {
  ValidationException({
    required String message,
    String code = 'VALIDATION_ERROR',
    Map<String, dynamic>? context,
  }) : super(
    message: message,
    code: code,
    context: context,
    httpCode: 400,
  );
}

class DatabaseException extends AppException {
  DatabaseException({
    required String message,
    String code = 'DATABASE_ERROR',
    Map<String, dynamic>? context,
  }) : super(
    message: message,
    code: code,
    context: context,
    httpCode: 500,
  );
}

class StreamingException extends AppException {
  StreamingException({
    required String message,
    String code = 'STREAMING_ERROR',
    Map<String, dynamic>? context,
  }) : super(
    message: message,
    code: code,
    context: context,
    httpCode: 500,
  );
}

2.2 Error Handler Middleware

Fichier: bin/middleware/error_handler.dart

import 'package:shelf/shelf.dart';
import 'dart:convert';
import '../utils/app_exceptions.dart';
import '../services/logging_service.dart';

Middleware errorHandlerMiddleware() {
  final logger = LoggingService();

  return (Handler handler) {
    return (Request request) async {
      try {
        return await handler(request);
      } on AppException catch (e, st) {
        logger.logError(
          'AppException in ${request.method} ${request.url}',
          e,
          st,
        );

        return Response(
          e.httpCode,
          body: jsonEncode({
            'success': false,
            'error': e.toJson(),
          }),
          headers: {'Content-Type': 'application/json'},
        );
      } catch (e, st) {
        logger.logError(
          'Unexpected error in ${request.method} ${request.url}',
          e,
          st,
        );

        return Response(
          500,
          body: jsonEncode({
            'success': false,
            'error': {
              'code': 'INTERNAL_SERVER_ERROR',
              'message': 'Une erreur interne s\'est produite',
            },
          }),
          headers: {'Content-Type': 'application/json'},
        );
      }
    };
  };
}

3️⃣ AUTHENTIFICATION SÉCURISÉE (Jour 2-4)

3.1 Ajouter Dépendances

# bin/pubspec.yaml
dependencies:
  bcrypt: ^1.1.0
  dart_jsonwebtoken: ^2.11.0
  totp: ^3.0.0

3.2 Password Hasher avec Bcrypt

Fichier: bin/utils/password_hasher.dart (REMPLACER)

import 'package:bcrypt/bcrypt.dart';

class PasswordHasher {
  static String hash(String password) {
    // Generate salt and hash in one step
    return BCrypt.hashpw(password, BCrypt.gensalt(logRounds: 12));
  }

  static bool verify(String password, String hash) {
    try {
      return BCrypt.checkpw(password, hash);
    } catch (e) {
      return false;
    }
  }

  /// Verify and potentially upgrade hash if using older algorithm
  static Map<String, dynamic> verifyAndUpgrade(
    String password,
    String hash,
  ) {
    final isValid = verify(password, hash);
    final needsUpgrade = false; // No upgrade needed with bcrypt

    return {
      'valid': isValid,
      'needs_upgrade': needsUpgrade,
      'new_hash': isValid ? hash : null,
    };
  }
}

3.3 JWT Token Service

Fichier: bin/services/jwt_service.dart (NEW)

import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import '../models/user.dart';

class JwtService {
  static final JwtService _instance = JwtService._internal();
  factory JwtService() => _instance;

  late final String _secret;
  final Duration accessTokenDuration = const Duration(minutes: 15);
  final Duration refreshTokenDuration = const Duration(days: 7);

  JwtService._internal() {
    _secret = const String.fromEnvironment(
      'JWT_SECRET',
      defaultValue: 'your-secret-key-change-in-production',
    );
  }

  /// Generate access token (15 min)
  String generateAccessToken(User user) {
    final payload = {
      'id': user.id,
      'username': user.username,
      'is_admin': user.isAdmin,
      'type': 'access',
      'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
      'exp': DateTime.now()
          .add(accessTokenDuration)
          .millisecondsSinceEpoch ~/
          1000,
    };

    return JWT(payload).sign(SecretKey(_secret));
  }

  /// Generate refresh token (7 days)
  String generateRefreshToken(User user) {
    final payload = {
      'id': user.id,
      'type': 'refresh',
      'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
      'exp': DateTime.now()
          .add(refreshTokenDuration)
          .millisecondsSinceEpoch ~/
          1000,
    };

    return JWT(payload).sign(SecretKey(_secret));
  }

  /// Verify and decode token
  dynamic verifyToken(String token) {
    try {
      return JWT.verify(token, SecretKey(_secret));
    } on JWTExpiredException catch (e) {
      throw Exception('Token expired: ${e.message}');
    } on JWTException catch (e) {
      throw Exception('Invalid token: ${e.message}');
    }
  }

  /// Check if token is expired
  bool isTokenExpired(String token) {
    try {
      final decoded = JWT.decode(token);
      final exp = decoded.payload['exp'] as int?;
      if (exp == null) return true;

      final expirationTime = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
      return expirationTime.isBefore(DateTime.now());
    } catch (e) {
      return true;
    }
  }
}

3.4 Mettre à jour Auth Handler

Modifié: bin/api/auth_handler.dart

// REMPLACER la méthode _login complètement par:

Future<Response> _login(Request request) async {
  try {
    final payload = jsonDecode(await request.readAsString())
        as Map<String, dynamic>;
    final username = payload['username'] as String?;
    final password = payload['password'] as String?;

    if (username == null || password == null) {
      throw ValidationException(
        message: 'Username and password required',
      );
    }

    // Verify credentials avec bcrypt
    final user = db.verifyCredentials(username, password);
    if (user == null) {
      throw AuthException(
        message: 'Invalid credentials',
        code: 'INVALID_CREDENTIALS',
      );
    }

    // Generate tokens
    final jwtService = JwtService();
    final accessToken = jwtService.generateAccessToken(user);
    final refreshToken = jwtService.generateRefreshToken(user);

    // Create session
    final session = db.createSession(user.id, accessToken, refreshToken);

    return Response.ok(
      jsonEncode({
        'success': true,
        'user': user.toJson(),
        'access_token': accessToken,
        'refresh_token': refreshToken,
        'expires_in': 900, // 15 min
      }),
      headers: {'Content-Type': 'application/json'},
    );
  } on AppException catch (e) {
    return Response(
      e.httpCode,
      body: jsonEncode({'success': false, 'error': e.toJson()}),
      headers: {'Content-Type': 'application/json'},
    );
  } catch (e) {
    LoggingService().logError('Login error', e, StackTrace.current);
    return Response.internalServerError(
      body: jsonEncode({
        'success': false,
        'error': {'code': 'INTERNAL_ERROR', 'message': e.toString()}
      }),
      headers: {'Content-Type': 'application/json'},
    );
  }
}

4️⃣ DATABASE RELIABILITY (Jour 4-5)

4.1 Migrations System

Fichier: bin/database/migrations.dart (NEW)

final List<Migration> migrations = [
  Migration(
    version: 1,
    name: 'Initial schema',
    up: '''
      -- Users
      CREATE TABLE IF NOT EXISTS users (
        id TEXT PRIMARY KEY,
        username TEXT UNIQUE NOT NULL,
        password_hash TEXT NOT NULL,
        is_admin INTEGER DEFAULT 0,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        updated_at TEXT DEFAULT CURRENT_TIMESTAMP
      );

      -- Playlists
      CREATE TABLE IF NOT EXISTS playlists (
        id TEXT PRIMARY KEY,
        user_id TEXT NOT NULL,
        name TEXT NOT NULL,
        server_url TEXT NOT NULL,
        username TEXT NOT NULL,
        password TEXT NOT NULL,
        dns TEXT,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
      );

      -- Sessions
      CREATE TABLE IF NOT EXISTS sessions (
        id TEXT PRIMARY KEY,
        user_id TEXT NOT NULL,
        access_token TEXT UNIQUE NOT NULL,
        refresh_token TEXT UNIQUE NOT NULL,
        expires_at TEXT NOT NULL,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
      );

      -- Indexes
      CREATE INDEX idx_sessions_token ON sessions(access_token);
      CREATE INDEX idx_sessions_expires ON sessions(expires_at);
      CREATE INDEX idx_playlists_user ON playlists(user_id);
    ''',
  ),
  Migration(
    version: 2,
    name: 'Add 2FA support',
    up: '''
      ALTER TABLE users ADD COLUMN totp_secret TEXT;
      ALTER TABLE users ADD COLUMN totp_enabled INTEGER DEFAULT 0;
    ''',
  ),
];

class Migration {
  final int version;
  final String name;
  final String up;
  final String? down;

  Migration({
    required this.version,
    required this.name,
    required this.up,
    this.down,
  });
}

class MigrationRunner {
  late Database _db;

  Future<void> init(Database db) async {
    _db = db;
    await _runMigrations();
  }

  Future<void> _runMigrations() async {
    // Get current schema version
    int currentVersion = 0;
    try {
      final result = _db.select('PRAGMA user_version');
      currentVersion = (result.first['user_version'] as int?) ?? 0;
    } catch (e) {
      // Table doesn't exist
      currentVersion = 0;
    }

    for (final migration in migrations) {
      if (migration.version > currentVersion) {
        print('Running migration v${migration.version}: ${migration.name}');
        _db.execute(migration.up);
        _db.execute('PRAGMA user_version = ${migration.version}');
      }
    }
  }
}

4.2 Backup Service

Fichier: bin/services/backup_service.dart

import 'dart:io';

class BackupService {
  static final BackupService _instance = BackupService._internal();
  factory BackupService() => _instance;

  BackupService._internal();

  Future<String> createBackup() async {
    final backupDir = Directory('/app/backups');
    if (!backupDir.existsSync()) {
      backupDir.createSync(recursive: true);
    }

    final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-');
    final backupPath = '${backupDir.path}/xtremflow_$timestamp.db';

    try {
      final sourceDb = File('/app/data/xtremflow.db');
      await sourceDb.copy(backupPath);

      print('Backup created: $backupPath');
      return backupPath;
    } catch (e) {
      throw Exception('Backup failed: $e');
    }
  }

  Future<void> cleanOldBackups({int maxAge = 30}) async {
    final backupDir = Directory('/app/backups');
    if (!backupDir.existsSync()) return;

    final cutoff = DateTime.now().subtract(Duration(days: maxAge));

    await for (final file in backupDir.list()) {
      if (file is File && file.path.endsWith('.db')) {
        final stat = await file.stat();
        if (stat.modified.isBefore(cutoff)) {
          await file.delete();
          print('Deleted old backup: ${file.path}');
        }
      }
    }
  }
}

5️⃣ INTÉGRER DANS SERVER.DART

Modifié: bin/server.dart (top)

import 'services/logging_service.dart';
import 'middleware/error_handler.dart';
import 'middleware/logging_middleware.dart';
import 'services/jwt_service.dart';
import 'services/backup_service.dart';
import 'database/migrations.dart';

void main(List<String> args) async {
  final parser = ArgParser()
    ..addOption('port', abbr: 'p', defaultsTo: '8089')
    ..addOption('path', defaultsTo: '/app/web');

  final result = parser.parse(args);
  final port = int.parse(result['port']);
  final webPath = result['path'];

  final logger = LoggingService();
  logger.logInfo('Starting XtremFlow IPTV Server...');

  // Initialize database with migrations
  final db = AppDatabase();
  await db.init();
  final migrationRunner = MigrationRunner();
  await migrationRunner.init(db._db); // Access internal db
  await db.seedAdmin();

  // Backup daily
  final backupService = BackupService();
  Timer.periodic(const Duration(hours: 24), (_) async {
    await backupService.createBackup();
    await backupService.cleanOldBackups();
  });

  logger.logInfo('Database initialized');

  // ... rest of server setup (line 157+)

  // REMPLACER la Pipeline:
  final pipeline = const Pipeline()
      .addMiddleware(logRequests())  // Keep shelf's logRequests
      .addMiddleware(loggingMiddleware())  // Our custom logging
      .addMiddleware(errorHandlerMiddleware())  // Error handling
      .addMiddleware(securityHeadersMiddleware())
      .addMiddleware(honeypotMiddleware())
      .addMiddleware(rateLimitMiddleware())
      .addMiddleware(_corsMiddleware())
      .addHandler(handler);

  final server = await shelf_io.serve(
    pipeline,
    InternetAddress.anyIPv4,
    port,
  );

  logger.logInfo('Server started on port ${server.port}');
}

📋 CHECKLIST D'IMPLÉMENTATION

  • Jour 1: Logger + LoggingService
  • Jour 1: AppException hierarchy
  • Jour 1: ErrorHandler middleware
  • Jour 2: Bcrypt password hasher
  • Jour 3: JWT service + token generation
  • Jour 3: Auth handler update
  • Jour 4: Migrations system
  • Jour 5: Backup service
  • Jour 5: Testing complet
  • Integration dans server.dart
  • Tests fonctionnels validant tout

🧪 TESTS À AJOUTER

// test/unit/auth_test.dart
void main() {
  test('Password hashing with bcrypt', () {
    final password = 'test123';
    final hash = PasswordHasher.hash(password);
    expect(PasswordHasher.verify(password, hash), isTrue);
    expect(PasswordHasher.verify('wrong', hash), isFalse);
  });

  test('JWT token generation and verification', () {
    final user = User(id: 'test', username: 'john', passwordHash: 'xxx');
    final jwt = JwtService();
    final token = jwt.generateAccessToken(user);
    final decoded = jwt.verifyToken(token);
    expect(decoded.payload['username'], 'john');
  });

  test('Token expiration check', () {
    final jwt = JwtService();
    // Create expired token
    expect(jwt.isTokenExpired('expired_token'), isTrue);
  });
}

Ce guide fournit le code exact à implémenter pour la semaine 1