mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-16 19:58:54 +02:00
feat: Add initial backend server with user authentication, session management, and playlist CRUD functionality.
This commit is contained in:
1 parent
a1ce183cd3
commit
ac4f274031
19 files changed
+1324
-75
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/database/hive_service.dart';
|
||||
import '../../../core/services/auth_api_service.dart';
|
||||
import '../../../core/models/app_user.dart';
|
||||
|
||||
/// Auth state
|
||||
@@ -7,11 +7,13 @@ class AuthState {
|
||||
final AppUser? currentUser;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
final bool isInitialized;
|
||||
|
||||
const AuthState({
|
||||
this.currentUser,
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
this.isInitialized = false,
|
||||
});
|
||||
|
||||
bool get isAuthenticated => currentUser != null;
|
||||
@@ -21,52 +23,83 @@ class AuthState {
|
||||
AppUser? currentUser,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
bool? isInitialized,
|
||||
bool clearUser = false,
|
||||
}) {
|
||||
return AuthState(
|
||||
currentUser: currentUser ?? this.currentUser,
|
||||
currentUser: clearUser ? null : (currentUser ?? this.currentUser),
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage,
|
||||
isInitialized: isInitialized ?? this.isInitialized,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Auth notifier
|
||||
/// Auth notifier using API
|
||||
class AuthNotifier extends StateNotifier<AuthState> {
|
||||
AuthNotifier() : super(const AuthState());
|
||||
final AuthApiService _authService = AuthApiService();
|
||||
|
||||
AuthNotifier() : super(const AuthState()) {
|
||||
// Auto-check for existing session
|
||||
checkSession();
|
||||
}
|
||||
|
||||
/// Check if there's an existing valid session
|
||||
Future<void> checkSession() async {
|
||||
state = state.copyWith(isLoading: true);
|
||||
|
||||
try {
|
||||
final user = await _authService.getCurrentUser();
|
||||
if (user != null) {
|
||||
state = AuthState(
|
||||
currentUser: user,
|
||||
isLoading: false,
|
||||
isInitialized: true,
|
||||
);
|
||||
} else {
|
||||
state = const AuthState(isInitialized: true);
|
||||
}
|
||||
} catch (e) {
|
||||
state = const AuthState(isInitialized: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Login with username and password
|
||||
Future<bool> login(String username, String password) async {
|
||||
state = state.copyWith(isLoading: true, errorMessage: null);
|
||||
|
||||
try {
|
||||
final usersBox = HiveService.usersBox;
|
||||
final result = await _authService.login(username, password);
|
||||
|
||||
// Search for user by username
|
||||
final user = usersBox.values.firstWhere(
|
||||
(user) => user.username == username,
|
||||
orElse: () => throw Exception('User not found'),
|
||||
);
|
||||
|
||||
// Verify password using salt-based hashing
|
||||
if (!HiveService.verifyPassword(password, user.passwordHash)) {
|
||||
throw Exception('Invalid password');
|
||||
if (result.success && result.user != null) {
|
||||
state = AuthState(
|
||||
currentUser: result.user,
|
||||
isLoading: false,
|
||||
isInitialized: true,
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
state = AuthState(
|
||||
isLoading: false,
|
||||
isInitialized: true,
|
||||
errorMessage: result.error ?? 'Login failed',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
state = AuthState(currentUser: user, isLoading: false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = AuthState(
|
||||
isLoading: false,
|
||||
errorMessage: 'Invalid username or password',
|
||||
isInitialized: true,
|
||||
errorMessage: 'Network error: $e',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Logout current user
|
||||
void logout() {
|
||||
state = const AuthState();
|
||||
Future<void> logout() async {
|
||||
await _authService.logout();
|
||||
state = const AuthState(isInitialized: true);
|
||||
}
|
||||
|
||||
/// Get current user
|
||||
|
||||
@@ -2,24 +2,23 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../core/database/hive_service.dart';
|
||||
import '../../../core/services/playlist_api_service.dart';
|
||||
import '../../../core/models/playlist_config.dart';
|
||||
import '../../auth/providers/auth_provider.dart';
|
||||
|
||||
/// Provider for fetching playlists from API
|
||||
final playlistsProvider = FutureProvider<List<PlaylistConfig>>((ref) async {
|
||||
final service = PlaylistApiService();
|
||||
return service.getPlaylists();
|
||||
});
|
||||
|
||||
class PlaylistSelectionScreen extends ConsumerWidget {
|
||||
const PlaylistSelectionScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentUser = ref.watch(authProvider).currentUser;
|
||||
final playlistsBox = HiveService.playlistsBox;
|
||||
|
||||
// Filter playlists assigned to current user (or all if admin)
|
||||
final availablePlaylists = playlistsBox.values.where((playlist) {
|
||||
if (currentUser == null) return false;
|
||||
if (currentUser.isAdmin) return playlist.isActive;
|
||||
return currentUser.assignedPlaylistIds.contains(playlist.id) &&
|
||||
playlist.isActive;
|
||||
}).toList();
|
||||
final playlistsAsync = ref.watch(playlistsProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -36,16 +35,36 @@ class PlaylistSelectionScreen extends ConsumerWidget {
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () {
|
||||
ref.read(authProvider.notifier).logout();
|
||||
context.go('/login');
|
||||
onPressed: () async {
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
if (context.mounted) {
|
||||
context.go('/login');
|
||||
}
|
||||
},
|
||||
tooltip: 'Logout',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: availablePlaylists.isEmpty
|
||||
? Center(
|
||||
body: playlistsAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 64, color: Colors.red.shade400),
|
||||
const SizedBox(height: 16),
|
||||
Text('Error loading playlists', style: GoogleFonts.roboto(fontSize: 18)),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.refresh(playlistsProvider),
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (playlists) {
|
||||
if (playlists.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -74,32 +93,36 @@ class PlaylistSelectionScreen extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 1.5,
|
||||
),
|
||||
itemCount: availablePlaylists.length,
|
||||
itemBuilder: (context, index) {
|
||||
final playlist = availablePlaylists[index];
|
||||
return _PlaylistCard(
|
||||
playlist: playlist,
|
||||
onTap: () {
|
||||
context.go('/dashboard', extra: playlist);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 1.5,
|
||||
),
|
||||
itemCount: playlists.length,
|
||||
itemBuilder: (context, index) {
|
||||
final playlist = playlists[index];
|
||||
return _PlaylistCard(
|
||||
playlist: playlist,
|
||||
onTap: () {
|
||||
context.go('/dashboard', extra: playlist);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlaylistCard extends StatelessWidget {
|
||||
final dynamic playlist;
|
||||
final PlaylistConfig playlist;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _PlaylistCard({
|
||||
@@ -151,7 +174,7 @@ class _PlaylistCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
Uri.parse(playlist.dns).host,
|
||||
Uri.tryParse(playlist.dns)?.host ?? playlist.dns,
|
||||
style: GoogleFonts.roboto(
|
||||
fontSize: 12,
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
|
||||
Reference in new issue
Block a user