feat: security hardening, streaming overhaul, design polish, tests

Security:
- Replace unsalted SHA-256 password hashing with bcrypt (lazy rehash on login)
- Add authenticated /api/xtream-api gateway: Xtream credentials are injected
  server-side and never sent to the frontend; /api/playlists no longer
  returns passwords
- Redact credentials from all logs (login body, proxy/FFmpeg/scheduler URLs)
- Add auth to recordings, EPG, season-passes and streaming routes
  (HttpOnly session cookie for hls.js; loopback bypass for local FFmpeg)
- Lock player postMessage to same-origin in both directions
- Vendor and pin hls.js 1.6.7 / mpegts.js 1.7.3 (drop CDN @latest)
- Fix rate limiter (client IP was never resolved), add login rate limit,
  restrict CORS, add CSP Report-Only, block private-IP SSRF targets,
  fix path traversal in recording log retrieval, chmod 777 -> 770
- Remove dead HiveService (seeded admin/admin into IndexedDB with SHA-256)
- Fix authMiddleware not populating 'user' context (getPlaylist ignored the
  logged-in user; admin purge always returned 403)

Streaming:
- New FfmpegSessionManager: process registry, idle reaper (4 min live /
  15 min VOD), orphan cleanup at startup, clean SIGTERM shutdown,
  fast-fail with stderr instead of 30 s timeout
- Quality selection (source/high/medium/low) for live and VOD; source mode
  streams with -c:v copy (zero transcoding); selector wired into the player
- Concurrent recordings (MAX_CONCURRENT_RECORDINGS, default 2); conflicts
  retry on the next tick instead of silently failing
- Lower live latency (HLS window 20 -> 10 segments, liveSync 10 -> 3)
- Fix recording log lookup (.mp4 vs .mkv mismatch)

Design:
- Replace hardcoded colors with AppColors tokens (12 files)
- web/theme.css syncs HTML players with the Flutter palette
- DPAD/keyboard navigation (arrow-key focus, player shortcuts)
- Tooltips on player icon buttons, Semantics on content cards
- Remove 7 dead widgets broken since the Stitch merge

Quality:
- bin/test/: 21 unit tests (bcrypt, redaction, traversal, SSRF, recording
  conflicts) plus a quality-selector widget test
- GitHub Actions CI (analyze + test + build web)
- Archive stale status docs into docs/archive/

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Fable 5 committed 2026-06-10 10:07:18 +02:00
1 parent cb5eca7547
commit 60d3f42901
107 files changed
+4525 -3815

No files matched your search

@@ -5,6 +5,7 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import '../../../core/models/iptv_models.dart';
import '../../../core/models/playlist_config.dart';
import '../../../core/theme/app_colors.dart';
import '../providers/xtream_provider.dart';
import '../providers/settings_provider.dart';
import '../screens/player_screen.dart';
@@ -363,7 +364,7 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
const Icon(
Icons.tv_off,
size: 64,
color: Color(0x1FFFFFFF),
color: AppColors.glassLevel1Border,
),
const SizedBox(height: 16),
Text(
@@ -512,7 +513,7 @@ class _ProgrammeCard extends StatelessWidget {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1E1E2E),
backgroundColor: AppColors.surfaceContainer,
title: Row(
children: [
const Icon(
@@ -823,7 +824,7 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1E1E2E),
backgroundColor: AppColors.surfaceContainer,
title: Text(
title,
style: GoogleFonts.outfit(color: Colors.white, fontSize: 14),
@@ -918,7 +919,7 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
const Icon(
Icons.videocam_off,
size: 64,
color: Color(0x1FFFFFFF),
color: AppColors.glassLevel1Border,
),
const SizedBox(height: 16),
Text(
@@ -1124,7 +1125,7 @@ class _SeasonPassesViewState extends State<_SeasonPassesView> {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1E1E2E),
backgroundColor: AppColors.surfaceContainer,
title: Row(
children: [
const Icon(Icons.repeat, color: Colors.purpleAccent),
@@ -1282,7 +1283,7 @@ class _SeasonPassesViewState extends State<_SeasonPassesView> {
const Icon(
Icons.repeat,
size: 64,
color: Color(0x1FFFFFFF),
color: AppColors.glassLevel1Border,
),
const SizedBox(height: 16),
Text(