mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-18 04:39:41 +02:00
feat: Initialize XtremFlow project with core structure, IPTV features, authentication, and video player.
This commit is contained in:
commit
2a28eb44d8
28 files changed
+3279
No files matched your search
@@ -0,0 +1,119 @@
|
||||
import 'dart:convert';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../models/app_user.dart';
|
||||
import '../models/playlist_config.dart';
|
||||
|
||||
class HiveService {
|
||||
static const String _usersBoxName = 'users';
|
||||
static const String _playlistsBoxName = 'playlists';
|
||||
static const String _encryptionKeyName = 'hive_encryption_key';
|
||||
|
||||
static const _storage = FlutterSecureStorage();
|
||||
static bool _initialized = false;
|
||||
|
||||
/// Initialize Hive for Web with encryption
|
||||
static Future<void> init() async {
|
||||
if (_initialized) return;
|
||||
|
||||
// Initialize Hive for Web (uses IndexedDB)
|
||||
await Hive.initFlutter();
|
||||
|
||||
// Generate or retrieve encryption key
|
||||
final encryptionKey = await _getOrCreateEncryptionKey();
|
||||
|
||||
// Register adapters
|
||||
Hive.registerAdapter(AppUserAdapter());
|
||||
Hive.registerAdapter(PlaylistConfigAdapter());
|
||||
|
||||
// Open encrypted boxes
|
||||
await Hive.openBox<AppUser>(
|
||||
_usersBoxName,
|
||||
encryptionCipher: HiveAesCipher(encryptionKey),
|
||||
);
|
||||
|
||||
await Hive.openBox<PlaylistConfig>(
|
||||
_playlistsBoxName,
|
||||
encryptionCipher: HiveAesCipher(encryptionKey),
|
||||
);
|
||||
|
||||
// Seed default admin if no users exist
|
||||
await _seedDefaultAdmin();
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// Get or create 256-bit encryption key
|
||||
static Future<List<int>> _getOrCreateEncryptionKey() async {
|
||||
try {
|
||||
final existingKey = await _storage.read(key: _encryptionKeyName);
|
||||
|
||||
if (existingKey != null) {
|
||||
return base64Decode(existingKey);
|
||||
}
|
||||
} catch (e) {
|
||||
// Storage might not be available in some web contexts
|
||||
// Fall back to session-based key (not persisted)
|
||||
}
|
||||
|
||||
// Generate new 256-bit key
|
||||
final key = Hive.generateSecureKey();
|
||||
|
||||
try {
|
||||
await _storage.write(
|
||||
key: _encryptionKeyName,
|
||||
value: base64Encode(key),
|
||||
);
|
||||
} catch (e) {
|
||||
// Ignore if storage fails - key will be regenerated next session
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/// Seed default admin user (admin/admin)
|
||||
static Future<void> _seedDefaultAdmin() async {
|
||||
final usersBox = Hive.box<AppUser>(_usersBoxName);
|
||||
|
||||
if (usersBox.isEmpty) {
|
||||
final adminId = const Uuid().v4();
|
||||
final passwordHash = _hashPassword('admin');
|
||||
|
||||
final admin = AppUser(
|
||||
id: adminId,
|
||||
username: 'admin',
|
||||
passwordHash: passwordHash,
|
||||
isAdmin: true,
|
||||
assignedPlaylistIds: [],
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await usersBox.put(adminId, admin);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash password using SHA-256
|
||||
static String _hashPassword(String password) {
|
||||
final bytes = utf8.encode(password);
|
||||
final digest = sha256.convert(bytes);
|
||||
return digest.toString();
|
||||
}
|
||||
|
||||
/// Public method to hash passwords (used by auth)
|
||||
static String hashPassword(String password) => _hashPassword(password);
|
||||
|
||||
/// Get users box
|
||||
static Box<AppUser> get usersBox => Hive.box<AppUser>(_usersBoxName);
|
||||
|
||||
/// Get playlists box
|
||||
static Box<PlaylistConfig> get playlistsBox =>
|
||||
Hive.box<PlaylistConfig>(_playlistsBoxName);
|
||||
|
||||
/// Close all boxes (cleanup)
|
||||
static Future<void> dispose() async {
|
||||
await Hive.close();
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'app_user.g.dart';
|
||||
|
||||
@HiveType(typeId: 0)
|
||||
class AppUser {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
|
||||
@HiveField(1)
|
||||
final String username;
|
||||
|
||||
@HiveField(2)
|
||||
final String passwordHash; // SHA-256 hash
|
||||
|
||||
@HiveField(3)
|
||||
final bool isAdmin;
|
||||
|
||||
@HiveField(4)
|
||||
final List<String> assignedPlaylistIds;
|
||||
|
||||
@HiveField(5)
|
||||
final DateTime createdAt;
|
||||
|
||||
const AppUser({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.passwordHash,
|
||||
required this.isAdmin,
|
||||
required this.assignedPlaylistIds,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
AppUser copyWith({
|
||||
String? id,
|
||||
String? username,
|
||||
String? passwordHash,
|
||||
bool? isAdmin,
|
||||
List<String>? assignedPlaylistIds,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
return AppUser(
|
||||
id: id ?? this.id,
|
||||
username: username ?? this.username,
|
||||
passwordHash: passwordHash ?? this.passwordHash,
|
||||
isAdmin: isAdmin ?? this.isAdmin,
|
||||
assignedPlaylistIds: assignedPlaylistIds ?? this.assignedPlaylistIds,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_user.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class AppUserAdapter extends TypeAdapter<AppUser> {
|
||||
@override
|
||||
final int typeId = 0;
|
||||
|
||||
@override
|
||||
AppUser read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return AppUser(
|
||||
id: fields[0] as String,
|
||||
username: fields[1] as String,
|
||||
passwordHash: fields[2] as String,
|
||||
isAdmin: fields[3] as bool,
|
||||
assignedPlaylistIds: (fields[4] as List).cast<String>(),
|
||||
createdAt: fields[5] as DateTime,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AppUser obj) {
|
||||
writer
|
||||
..writeByte(6)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.username)
|
||||
..writeByte(2)
|
||||
..write(obj.passwordHash)
|
||||
..writeByte(3)
|
||||
..write(obj.isAdmin)
|
||||
..writeByte(4)
|
||||
..write(obj.assignedPlaylistIds)
|
||||
..writeByte(5)
|
||||
..write(obj.createdAt);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AppUserAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'playlist_config.g.dart';
|
||||
|
||||
@HiveType(typeId: 1)
|
||||
class PlaylistConfig {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
|
||||
@HiveField(1)
|
||||
final String name;
|
||||
|
||||
@HiveField(2)
|
||||
final String dns; // Xtream server URL (without trailing slash)
|
||||
|
||||
@HiveField(3)
|
||||
final String username;
|
||||
|
||||
@HiveField(4)
|
||||
final String password; // Will be encrypted by Hive
|
||||
|
||||
@HiveField(5)
|
||||
final DateTime createdAt;
|
||||
|
||||
@HiveField(6)
|
||||
final bool isActive;
|
||||
|
||||
const PlaylistConfig({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.dns,
|
||||
required this.username,
|
||||
required this.password,
|
||||
required this.createdAt,
|
||||
this.isActive = true,
|
||||
});
|
||||
|
||||
PlaylistConfig copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? dns,
|
||||
String? username,
|
||||
String? password,
|
||||
DateTime? createdAt,
|
||||
bool? isActive,
|
||||
}) {
|
||||
return PlaylistConfig(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
dns: dns ?? this.dns,
|
||||
username: username ?? this.username,
|
||||
password: password ?? this.password,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
isActive: isActive ?? this.isActive,
|
||||
);
|
||||
}
|
||||
|
||||
/// Generate Xtream API base URL
|
||||
String get apiBaseUrl => '$dns/player_api.php';
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'playlist_config.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class PlaylistConfigAdapter extends TypeAdapter<PlaylistConfig> {
|
||||
@override
|
||||
final int typeId = 1;
|
||||
|
||||
@override
|
||||
PlaylistConfig read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return PlaylistConfig(
|
||||
id: fields[0] as String,
|
||||
name: fields[1] as String,
|
||||
dns: fields[2] as String,
|
||||
username: fields[3] as String,
|
||||
password: fields[4] as String,
|
||||
createdAt: fields[5] as DateTime,
|
||||
isActive: fields[6] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, PlaylistConfig obj) {
|
||||
writer
|
||||
..writeByte(7)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.dns)
|
||||
..writeByte(3)
|
||||
..write(obj.username)
|
||||
..writeByte(4)
|
||||
..write(obj.password)
|
||||
..writeByte(5)
|
||||
..write(obj.createdAt)
|
||||
..writeByte(6)
|
||||
..write(obj.isActive);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PlaylistConfigAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../features/auth/providers/auth_provider.dart';
|
||||
import '../../features/auth/screens/login_screen.dart';
|
||||
import '../../features/iptv/screens/playlist_selection_screen.dart';
|
||||
import '../../features/iptv/screens/dashboard_screen.dart';
|
||||
import '../../features/admin/screens/admin_panel.dart';
|
||||
import '../models/playlist_config.dart';
|
||||
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
final authNotifier = ref.watch(authProvider.notifier);
|
||||
final authState = ref.watch(authProvider);
|
||||
|
||||
return GoRouter(
|
||||
initialLocation: '/login',
|
||||
redirect: (context, state) {
|
||||
final isLoggedIn = authState.isAuthenticated;
|
||||
final isLoginRoute = state.matchedLocation == '/login';
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
if (!isLoggedIn && !isLoginRoute) {
|
||||
return '/login';
|
||||
}
|
||||
|
||||
// Redirect to playlists if already logged in and trying to access login
|
||||
if (isLoggedIn && isLoginRoute) {
|
||||
return '/playlists';
|
||||
}
|
||||
|
||||
// Admin guard for /admin route
|
||||
if (state.matchedLocation == '/admin' && !authState.isAdmin) {
|
||||
return '/playlists';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/login',
|
||||
builder: (context, state) => const LoginScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/playlists',
|
||||
builder: (context, state) => const PlaylistSelectionScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/dashboard',
|
||||
builder: (context, state) {
|
||||
final playlist = state.extra as PlaylistConfig?;
|
||||
if (playlist == null) {
|
||||
// Redirect back if no playlist provided
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/playlists');
|
||||
});
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
return DashboardScreen(playlist: playlist);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin',
|
||||
builder: (context, state) => const AdminPanel(),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user