forked from baron/baron-sso
194 lines
4.9 KiB
Dart
194 lines
4.9 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../../../core/services/auth_token_store.dart';
|
|
import '../../../../core/services/http_client.dart';
|
|
import 'package:userfront/i18n.dart';
|
|
import 'models.dart';
|
|
|
|
String _envOrDefault(String key, String fallback) {
|
|
if (!dotenv.isInitialized) {
|
|
return fallback;
|
|
}
|
|
return dotenv.env[key] ?? fallback;
|
|
}
|
|
|
|
String get _baseUrl => _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
|
|
|
Future<AuditPage> _fetchAuthTimelinePage({String? cursor}) async {
|
|
final queryParameters = <String, String>{
|
|
'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 = <String, String>{
|
|
'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<String, dynamic>;
|
|
final items = (body['items'] as List?) ?? [];
|
|
final nextCursor = body['next_cursor']?.toString();
|
|
final logs = <AuditLogEntry>[];
|
|
for (final item in items) {
|
|
if (item is Map) {
|
|
logs.add(AuditLogEntry.fromJson(Map<String, dynamic>.from(item)));
|
|
}
|
|
}
|
|
|
|
return AuditPage(items: logs, nextCursor: nextCursor);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
typedef AuthTimelineFetcher = Future<AuditPage> Function({String? cursor});
|
|
|
|
final authTimelineFetcherProvider = Provider<AuthTimelineFetcher>((ref) {
|
|
return _fetchAuthTimelinePage;
|
|
});
|
|
|
|
class AuthTimelineState {
|
|
final List<AuditLogEntry> 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<AuditLogEntry>? 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<AuthTimelineState> {
|
|
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<void> refresh() async {
|
|
if (state.isLoading) {
|
|
return;
|
|
}
|
|
state = state.copyWith(
|
|
items: const [],
|
|
nextCursor: null,
|
|
isLoading: true,
|
|
error: null,
|
|
);
|
|
await _loadPage(reset: true);
|
|
}
|
|
|
|
Future<void> loadMore() async {
|
|
if (state.isLoading || state.isLoadingMore) {
|
|
return;
|
|
}
|
|
if (state.nextCursor == null || state.nextCursor!.isEmpty) {
|
|
return;
|
|
}
|
|
state = state.copyWith(isLoadingMore: true, error: null);
|
|
await _loadPage(reset: false);
|
|
}
|
|
|
|
Future<void> _loadInitial() async {
|
|
if (state.items.isNotEmpty || state.isLoading) {
|
|
return;
|
|
}
|
|
state = state.copyWith(isLoading: true, error: null);
|
|
await _loadPage(reset: true);
|
|
}
|
|
|
|
Future<void> _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',
|
|
fallback: '접속이력을 불러오지 못했습니다.',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
final authTimelineProvider = NotifierProvider<AuthTimelineNotifier, AuthTimelineState>(
|
|
AuthTimelineNotifier.new,
|
|
);
|