import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/services/auth_token_store.dart'; import '../../../../core/services/http_client.dart'; import '../../../../core/services/runtime_env.dart'; import 'package:userfront/i18n.dart'; import 'models.dart'; String get _baseUrl => runtimeBackendUrl(); Future _fetchAuthTimelinePage({String? cursor}) async { final queryParameters = {'limit': '20'}; if (cursor != null && cursor.isNotEmpty) { queryParameters['cursor'] = cursor; } final url = Uri.parse( '$_baseUrl/api/v1/audit/auth/timeline', ).replace(queryParameters: queryParameters); final useCookie = AuthTokenStore.usesCookie(); final token = AuthTokenStore.getToken(); final client = createHttpClient(withCredentials: useCookie); final headers = {'Content-Type': 'application/json'}; if (!useCookie && token != null) { headers['Authorization'] = 'Bearer $token'; } try { final response = await client.get(url, headers: headers); if (response.statusCode != 200) { throw Exception('Failed to load audit logs'); } final body = jsonDecode(response.body) as Map; final items = (body['items'] as List?) ?? []; final nextCursor = body['next_cursor']?.toString(); final logs = []; for (final item in items) { if (item is Map) { logs.add(AuditLogEntry.fromJson(Map.from(item))); } } return AuditPage(items: logs, nextCursor: nextCursor); } finally { client.close(); } } typedef AuthTimelineFetcher = Future Function({String? cursor}); final authTimelineFetcherProvider = Provider((ref) { return _fetchAuthTimelinePage; }); class AuthTimelineState { final List items; final String? nextCursor; final bool isLoading; final bool isLoadingMore; final String? error; const AuthTimelineState({ required this.items, this.nextCursor, this.isLoading = false, this.isLoadingMore = false, this.error, }); const AuthTimelineState.initial() : items = const [], nextCursor = null, isLoading = false, isLoadingMore = false, error = null; AuthTimelineState copyWith({ List? items, String? nextCursor, bool? isLoading, bool? isLoadingMore, String? error, }) { return AuthTimelineState( items: items ?? this.items, nextCursor: nextCursor ?? this.nextCursor, isLoading: isLoading ?? this.isLoading, isLoadingMore: isLoadingMore ?? this.isLoadingMore, error: error, ); } } class AuthTimelineNotifier extends Notifier { late final AuthTimelineFetcher _fetchPage; bool _hasLoaded = false; @override AuthTimelineState build() { _fetchPage = ref.watch(authTimelineFetcherProvider); if (!_hasLoaded) { _hasLoaded = true; Future.microtask(_loadInitial); } return const AuthTimelineState.initial(); } Future refresh() async { if (state.isLoading) { return; } state = state.copyWith( items: const [], nextCursor: null, isLoading: true, error: null, ); await _loadPage(reset: true); } Future loadMore() async { if (state.isLoading || state.isLoadingMore) { return; } final nextCursor = state.nextCursor; if (nextCursor == null || nextCursor.isEmpty) { return; } state = state.copyWith(isLoadingMore: true, error: null); await _loadPage(reset: false); } Future _loadInitial() async { if (state.items.isNotEmpty || state.isLoading) { return; } state = state.copyWith(isLoading: true, error: null); await _loadPage(reset: true); } Future _loadPage({required bool reset}) async { try { final page = await _fetchPage(cursor: reset ? null : state.nextCursor); if (reset) { state = state.copyWith( items: page.items, nextCursor: page.nextCursor, isLoading: false, isLoadingMore: false, error: null, ); } else { state = state.copyWith( items: [...state.items, ...page.items], nextCursor: page.nextCursor, isLoading: false, isLoadingMore: false, error: null, ); } } catch (e) { state = state.copyWith( isLoading: false, isLoadingMore: false, error: tr('msg.userfront.dashboard.timeline.load_error'), ); } } } final authTimelineProvider = NotifierProvider( AuthTimelineNotifier.new, );