mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-18 12:49: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
@@ -0,0 +1,83 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'dart:html' as html;
|
||||
|
||||
/// API Client for communicating with the backend
|
||||
class ApiClient {
|
||||
static final ApiClient _instance = ApiClient._internal();
|
||||
factory ApiClient() => _instance;
|
||||
|
||||
late final Dio _dio;
|
||||
String? _token;
|
||||
|
||||
ApiClient._internal() {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: _getBaseUrl(),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
));
|
||||
|
||||
// Add interceptor for logging
|
||||
_dio.interceptors.add(LogInterceptor(
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
));
|
||||
}
|
||||
|
||||
/// Get base URL (same origin for production)
|
||||
String _getBaseUrl() {
|
||||
// Use current origin for API calls
|
||||
return '';
|
||||
}
|
||||
|
||||
/// Set authentication token
|
||||
void setToken(String? token) {
|
||||
_token = token;
|
||||
if (token != null) {
|
||||
_dio.options.headers['Authorization'] = 'Bearer $token';
|
||||
// Store in localStorage for persistence
|
||||
html.window.localStorage['auth_token'] = token;
|
||||
} else {
|
||||
_dio.options.headers.remove('Authorization');
|
||||
html.window.localStorage.remove('auth_token');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get stored token from localStorage
|
||||
String? getStoredToken() {
|
||||
return html.window.localStorage['auth_token'];
|
||||
}
|
||||
|
||||
/// Restore token from localStorage
|
||||
void restoreToken() {
|
||||
final storedToken = getStoredToken();
|
||||
if (storedToken != null) {
|
||||
setToken(storedToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear token
|
||||
void clearToken() {
|
||||
setToken(null);
|
||||
}
|
||||
|
||||
/// GET request
|
||||
Future<Response> get(String path) async {
|
||||
return _dio.get(path);
|
||||
}
|
||||
|
||||
/// POST request
|
||||
Future<Response> post(String path, {dynamic data}) async {
|
||||
return _dio.post(path, data: data);
|
||||
}
|
||||
|
||||
/// PUT request
|
||||
Future<Response> put(String path, {dynamic data}) async {
|
||||
return _dio.put(path, data: data);
|
||||
}
|
||||
|
||||
/// DELETE request
|
||||
Future<Response> delete(String path) async {
|
||||
return _dio.delete(path);
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,20 @@ 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',
|
||||
refreshListenable: RouterRefreshNotifier(ref),
|
||||
redirect: (context, state) {
|
||||
final isLoggedIn = authState.isAuthenticated;
|
||||
final isLoginRoute = state.matchedLocation == '/login';
|
||||
|
||||
// Wait for initial auth check to complete
|
||||
if (!authState.isInitialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
if (!isLoggedIn && !isLoginRoute) {
|
||||
return '/login';
|
||||
@@ -67,3 +72,14 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
/// Notifier that triggers router refresh when auth state changes
|
||||
class RouterRefreshNotifier extends ChangeNotifier {
|
||||
RouterRefreshNotifier(this._ref) {
|
||||
_ref.listen(authProvider, (_, __) {
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
final Ref _ref;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import '../api/api_client.dart';
|
||||
import '../models/app_user.dart';
|
||||
|
||||
/// Service for authentication via API
|
||||
class AuthApiService {
|
||||
final ApiClient _api = ApiClient();
|
||||
|
||||
/// Login with username and password
|
||||
Future<AuthResult> login(String username, String password) async {
|
||||
try {
|
||||
final response = await _api.post('/api/auth/login', data: {
|
||||
'username': username,
|
||||
'password': password,
|
||||
});
|
||||
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
if (data['success'] == true) {
|
||||
final token = data['token'] as String;
|
||||
final userData = data['user'] as Map<String, dynamic>;
|
||||
|
||||
_api.setToken(token);
|
||||
|
||||
return AuthResult(
|
||||
success: true,
|
||||
user: AppUser(
|
||||
id: userData['id'] as String,
|
||||
username: userData['username'] as String,
|
||||
passwordHash: '', // Not needed for API auth
|
||||
isAdmin: userData['isAdmin'] as bool? ?? false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
token: token,
|
||||
);
|
||||
} else {
|
||||
return AuthResult(
|
||||
success: false,
|
||||
error: data['error'] as String? ?? 'Login failed',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
return AuthResult(
|
||||
success: false,
|
||||
error: 'Network error: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Logout
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
await _api.post('/api/auth/logout');
|
||||
} finally {
|
||||
_api.clearToken();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current user from token
|
||||
Future<AppUser?> getCurrentUser() async {
|
||||
_api.restoreToken();
|
||||
|
||||
if (_api.getStoredToken() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _api.get('/api/auth/me');
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final userData = data['user'] as Map<String, dynamic>;
|
||||
|
||||
return AppUser(
|
||||
id: userData['id'] as String,
|
||||
username: userData['username'] as String,
|
||||
passwordHash: '',
|
||||
isAdmin: userData['isAdmin'] as bool? ?? false,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
} catch (e) {
|
||||
_api.clearToken();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of authentication attempt
|
||||
class AuthResult {
|
||||
final bool success;
|
||||
final AppUser? user;
|
||||
final String? token;
|
||||
final String? error;
|
||||
|
||||
AuthResult({
|
||||
required this.success,
|
||||
this.user,
|
||||
this.token,
|
||||
this.error,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import '../api/api_client.dart';
|
||||
import '../models/playlist_config.dart';
|
||||
|
||||
/// Service for managing playlists via API
|
||||
class PlaylistApiService {
|
||||
final ApiClient _api = ApiClient();
|
||||
|
||||
/// Get all playlists for current user
|
||||
Future<List<PlaylistConfig>> getPlaylists() async {
|
||||
try {
|
||||
final response = await _api.get('/api/playlists');
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final playlistsData = data['playlists'] as List<dynamic>;
|
||||
|
||||
return playlistsData.map((p) {
|
||||
final playlist = p as Map<String, dynamic>;
|
||||
return PlaylistConfig(
|
||||
id: playlist['id'] as String,
|
||||
name: playlist['name'] as String,
|
||||
dns: playlist['serverUrl'] as String? ?? playlist['dns'] as String,
|
||||
username: playlist['username'] as String,
|
||||
password: playlist['password'] as String,
|
||||
createdAt: DateTime.tryParse(playlist['createdAt'] as String? ?? '') ?? DateTime.now(),
|
||||
);
|
||||
}).toList();
|
||||
} catch (e) {
|
||||
print('Error fetching playlists: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new playlist
|
||||
Future<PlaylistConfig?> createPlaylist({
|
||||
required String name,
|
||||
required String dns,
|
||||
required String username,
|
||||
required String password,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _api.post('/api/playlists', data: {
|
||||
'name': name,
|
||||
'serverUrl': dns,
|
||||
'username': username,
|
||||
'password': password,
|
||||
'dns': dns,
|
||||
});
|
||||
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
if (data['success'] == true) {
|
||||
final playlist = data['playlist'] as Map<String, dynamic>;
|
||||
return PlaylistConfig(
|
||||
id: playlist['id'] as String,
|
||||
name: playlist['name'] as String,
|
||||
dns: playlist['serverUrl'] as String? ?? playlist['dns'] as String,
|
||||
username: playlist['username'] as String,
|
||||
password: playlist['password'] as String,
|
||||
createdAt: DateTime.tryParse(playlist['createdAt'] as String? ?? '') ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Error creating playlist: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a playlist
|
||||
Future<PlaylistConfig?> updatePlaylist({
|
||||
required String id,
|
||||
required String name,
|
||||
required String dns,
|
||||
required String username,
|
||||
required String password,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _api.put('/api/playlists/$id', data: {
|
||||
'name': name,
|
||||
'serverUrl': dns,
|
||||
'username': username,
|
||||
'password': password,
|
||||
'dns': dns,
|
||||
});
|
||||
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
if (data['success'] == true) {
|
||||
final playlist = data['playlist'] as Map<String, dynamic>;
|
||||
return PlaylistConfig(
|
||||
id: playlist['id'] as String,
|
||||
name: playlist['name'] as String,
|
||||
dns: playlist['serverUrl'] as String? ?? playlist['dns'] as String,
|
||||
username: playlist['username'] as String,
|
||||
password: playlist['password'] as String,
|
||||
createdAt: DateTime.tryParse(playlist['createdAt'] as String? ?? '') ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Error updating playlist: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a playlist
|
||||
Future<bool> deletePlaylist(String id) async {
|
||||
try {
|
||||
final response = await _api.delete('/api/playlists/$id');
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
return data['success'] == true;
|
||||
} catch (e) {
|
||||
print('Error deleting playlist: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user