489 lines
17 KiB
Dart
489 lines
17 KiB
Dart
import 'dart:convert';
|
|
import 'dart:math';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
class ApiService {
|
|
static const String baseUrl = 'https://posyandu.oyi.web.id/api';
|
|
static const Duration _defaultTimeout = Duration(seconds: 10);
|
|
static const bool _enableLogging = true;
|
|
|
|
// Cache untuk mengurangi panggilan berulang
|
|
static final Map<String, dynamic> _responseCache = {};
|
|
static const bool _enableCacheForPerkembangan = false;
|
|
|
|
String? _token;
|
|
bool _isLoadingToken = false;
|
|
|
|
// Singleton pattern
|
|
static final ApiService _instance = ApiService._internal();
|
|
|
|
factory ApiService() {
|
|
return _instance;
|
|
}
|
|
|
|
ApiService._internal() {
|
|
// Load token saat instance dibuat
|
|
loadToken();
|
|
}
|
|
|
|
/// Generic request method that handles all HTTP methods
|
|
Future<Map<String, dynamic>> request({
|
|
required String method,
|
|
required String endpoint,
|
|
Map<String, dynamic>? data,
|
|
}) async {
|
|
try {
|
|
// Dapatkan token dan SharedPreferences di awal
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
if (_token == null && !_isLoadingToken) {
|
|
await loadToken();
|
|
print('Loaded token: ${_token != null ? 'Token ditemukan' : 'Token tidak ditemukan'}');
|
|
}
|
|
|
|
final url = Uri.parse('$baseUrl/$endpoint');
|
|
final headers = {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
// Tambahkan token Bearer jika tersedia
|
|
if (_token != null) {
|
|
headers['Authorization'] = 'Bearer $_token';
|
|
}
|
|
|
|
// Tambahkan header identifikasi
|
|
if (prefs.getString('nik') != null) {
|
|
headers['X-User-NIK'] = prefs.getString('nik')!;
|
|
}
|
|
|
|
if (prefs.getInt('user_id') != null) {
|
|
headers['X-User-ID'] = prefs.getInt('user_id').toString();
|
|
}
|
|
|
|
// Tambahkan Basic Auth sebagai fallback
|
|
String? basicAuth;
|
|
if (_token == null && prefs.getString('nik') != null && prefs.getInt('user_id') != null) {
|
|
String nik = prefs.getString('nik')!;
|
|
String userId = prefs.getInt('user_id').toString();
|
|
basicAuth = 'Basic ' + base64Encode(utf8.encode('$nik:$userId'));
|
|
headers['Authorization'] = basicAuth;
|
|
print('Using basic auth as fallback');
|
|
}
|
|
|
|
// Log request if enabled
|
|
if (_enableLogging) {
|
|
print('$method Request to: $url');
|
|
print('Headers: ${headers.toString()}');
|
|
if (data != null) print('Body: ${json.encode(data)}');
|
|
}
|
|
|
|
http.Response response;
|
|
|
|
// Gunakan timeout yang lebih panjang untuk server yang mungkin lambat
|
|
final timeout = Duration(seconds: 30);
|
|
|
|
switch (method.toLowerCase()) {
|
|
case 'get':
|
|
// Check cache for GET requests
|
|
if (_responseCache.containsKey(endpoint) &&
|
|
!endpoint.startsWith('perkembangan') &&
|
|
_enableCacheForPerkembangan) {
|
|
if (_enableLogging) print('Using cached response for $endpoint');
|
|
return _responseCache[endpoint];
|
|
}
|
|
|
|
response = await http.get(url, headers: headers)
|
|
.timeout(timeout);
|
|
break;
|
|
|
|
case 'post':
|
|
response = await http.post(
|
|
url,
|
|
headers: headers,
|
|
body: data != null ? json.encode(data) : null,
|
|
).timeout(timeout);
|
|
break;
|
|
|
|
case 'put':
|
|
response = await http.put(
|
|
url,
|
|
headers: headers,
|
|
body: data != null ? json.encode(data) : null,
|
|
).timeout(timeout);
|
|
break;
|
|
|
|
case 'delete':
|
|
response = await http.delete(url, headers: headers)
|
|
.timeout(timeout);
|
|
break;
|
|
|
|
default:
|
|
throw Exception('Unsupported HTTP method: $method');
|
|
}
|
|
|
|
if (_enableLogging) {
|
|
print('Response status: ${response.statusCode}');
|
|
print('Response body: ${response.body.length > 300 ? response.body.substring(0, 300) + '...' : response.body}');
|
|
}
|
|
|
|
// Handle 401 Unauthorized dengan mencoba beberapa alternatif autentikasi
|
|
if (response.statusCode == 401) {
|
|
print('401 Unauthorized - akan mencoba kredensial alternatif untuk: $endpoint');
|
|
|
|
// *** Khusus endpoint anak: coba dengan format token yang berbeda ***
|
|
if (endpoint.startsWith('anak')) {
|
|
if (_token != null) {
|
|
print('Mencoba format token khusus untuk endpoint anak');
|
|
|
|
Map<String, String> anakEndpointHeaders = {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
// Jangan gunakan 'Bearer ' prefix, hanya token saja
|
|
'Authorization': _token!,
|
|
'X-User-NIK': prefs.getString('nik') ?? '',
|
|
'X-User-ID': prefs.getInt('user_id')?.toString() ?? '',
|
|
};
|
|
|
|
try {
|
|
// Coba request dengan format token khusus
|
|
http.Response anakResponse;
|
|
|
|
switch (method.toLowerCase()) {
|
|
case 'get':
|
|
anakResponse = await http.get(url, headers: anakEndpointHeaders).timeout(timeout);
|
|
break;
|
|
case 'post':
|
|
anakResponse = await http.post(
|
|
url,
|
|
headers: anakEndpointHeaders,
|
|
body: data != null ? json.encode(data) : null,
|
|
).timeout(timeout);
|
|
break;
|
|
default:
|
|
anakResponse = await http.get(url, headers: anakEndpointHeaders).timeout(timeout);
|
|
}
|
|
|
|
if (anakResponse.statusCode >= 200 && anakResponse.statusCode < 300) {
|
|
print('Format token khusus berhasil: ${anakResponse.statusCode}');
|
|
response = anakResponse;
|
|
|
|
// Jika ini berfungsi, jangan lakukan alternatif lain
|
|
return json.decode(response.body);
|
|
} else {
|
|
print('Format token khusus gagal: ${anakResponse.statusCode}');
|
|
}
|
|
} catch (e) {
|
|
print('Error pada format token khusus: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Metode alternatif 1: Tanpa token, hanya header X-User
|
|
Map<String, String> altHeaders1 = {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (prefs.getString('nik') != null) {
|
|
altHeaders1['X-User-NIK'] = prefs.getString('nik')!;
|
|
}
|
|
|
|
if (prefs.getInt('user_id') != null) {
|
|
altHeaders1['X-User-ID'] = prefs.getInt('user_id').toString();
|
|
}
|
|
|
|
print('Alternatif 1: Request dengan X-User headers saja: $altHeaders1');
|
|
|
|
// Coba request dengan Alternatif 1
|
|
http.Response alt1Response;
|
|
try {
|
|
switch (method.toLowerCase()) {
|
|
case 'get':
|
|
alt1Response = await http.get(url, headers: altHeaders1).timeout(timeout);
|
|
break;
|
|
case 'post':
|
|
alt1Response = await http.post(
|
|
url,
|
|
headers: altHeaders1,
|
|
body: data != null ? json.encode(data) : null,
|
|
).timeout(timeout);
|
|
break;
|
|
default:
|
|
alt1Response = await http.get(url, headers: altHeaders1).timeout(timeout);
|
|
}
|
|
|
|
if (alt1Response.statusCode >= 200 && alt1Response.statusCode < 300) {
|
|
print('Alternatif 1 berhasil: ${alt1Response.statusCode}');
|
|
response = alt1Response;
|
|
} else {
|
|
print('Alternatif 1 gagal: ${alt1Response.statusCode}');
|
|
}
|
|
} catch (e) {
|
|
print('Error pada Alternatif 1: $e');
|
|
}
|
|
|
|
// Metode alternatif 2: Basic Auth
|
|
if (response.statusCode == 401 && basicAuth != null) {
|
|
Map<String, String> altHeaders2 = {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'Authorization': basicAuth,
|
|
};
|
|
|
|
print('Alternatif 2: Request dengan Basic Auth: $altHeaders2');
|
|
|
|
// Coba request dengan Alternatif 2
|
|
http.Response alt2Response;
|
|
try {
|
|
switch (method.toLowerCase()) {
|
|
case 'get':
|
|
alt2Response = await http.get(url, headers: altHeaders2).timeout(timeout);
|
|
break;
|
|
case 'post':
|
|
alt2Response = await http.post(
|
|
url,
|
|
headers: altHeaders2,
|
|
body: data != null ? json.encode(data) : null,
|
|
).timeout(timeout);
|
|
break;
|
|
default:
|
|
alt2Response = await http.get(url, headers: altHeaders2).timeout(timeout);
|
|
}
|
|
|
|
if (alt2Response.statusCode >= 200 && alt2Response.statusCode < 300) {
|
|
print('Alternatif 2 berhasil: ${alt2Response.statusCode}');
|
|
response = alt2Response;
|
|
} else {
|
|
print('Alternatif 2 gagal: ${alt2Response.statusCode}');
|
|
}
|
|
} catch (e) {
|
|
print('Error pada Alternatif 2: $e');
|
|
}
|
|
}
|
|
|
|
// ** PENTING: Jika ini adalah endpoint anak, jangan hapus token meskipun gagal **
|
|
if (response.statusCode == 401) {
|
|
if (endpoint.startsWith('anak')) {
|
|
print('Gagal autentikasi untuk endpoint anak, tetapi TIDAK menghapus token');
|
|
|
|
// Tetap kembalikan error, tetapi jangan hapus token
|
|
if (response.body.isNotEmpty) {
|
|
try {
|
|
return json.decode(response.body);
|
|
} catch (e) {
|
|
return {'status': 'error', 'message': 'Gagal autentikasi untuk endpoint anak'};
|
|
}
|
|
} else {
|
|
return {'status': 'error', 'message': 'Gagal autentikasi untuk endpoint anak'};
|
|
}
|
|
} else {
|
|
// Hapus token hanya jika bukan endpoint anak
|
|
print('Semua metode autentikasi gagal untuk endpoint: $endpoint');
|
|
print('Aplikasi mungkin perlu login ulang');
|
|
|
|
await clearToken();
|
|
clearAuthCache();
|
|
throw Exception('Session expired. Please login again.');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
final responseData = json.decode(response.body);
|
|
|
|
// Cache GET responses (except for perkembangan if disabled)
|
|
if (method.toLowerCase() == 'get' &&
|
|
!endpoint.startsWith('perkembangan') &&
|
|
_enableCacheForPerkembangan) {
|
|
_responseCache[endpoint] = responseData;
|
|
}
|
|
|
|
// Invalidate cache for write operations
|
|
if (method.toLowerCase() != 'get') {
|
|
_invalidateRelatedCache(endpoint);
|
|
}
|
|
|
|
return responseData;
|
|
} else {
|
|
throw Exception('HTTP Error: ${response.statusCode} - ${response.body}');
|
|
}
|
|
} catch (e) {
|
|
if (_enableLogging) {
|
|
print('API Error: $e');
|
|
if (e.toString().contains('TimeoutException')) {
|
|
print('!!! KONEKSI KE SERVER API TIMEOUT !!!');
|
|
print('!!! Pastikan server API berjalan di $baseUrl !!!');
|
|
} else if (e.toString().contains('SocketException')) {
|
|
print('!!! SERVER API TIDAK DAPAT DIJANGKAU !!!');
|
|
print('!!! Pastikan server API berjalan di $baseUrl !!!');
|
|
}
|
|
}
|
|
throw Exception('API Error: $e');
|
|
}
|
|
}
|
|
|
|
// Convenience methods that use the request method
|
|
Future<Map<String, dynamic>> get(String endpoint, {Map<String, dynamic>? queryParameters}) async {
|
|
String finalEndpoint = endpoint;
|
|
|
|
if (queryParameters != null && queryParameters.isNotEmpty) {
|
|
// Tambahkan parameter query ke endpoint
|
|
final queryString = queryParameters.entries
|
|
.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value.toString())}')
|
|
.join('&');
|
|
finalEndpoint = '$endpoint?$queryString';
|
|
|
|
if (_enableLogging) {
|
|
print('GET request with query parameters: $finalEndpoint');
|
|
}
|
|
}
|
|
|
|
final response = await request(method: 'get', endpoint: finalEndpoint);
|
|
|
|
// Khusus endpoint artikel, berikan debug log tambahan
|
|
if (endpoint.startsWith('artikel')) {
|
|
print('Endpoint artikel: $finalEndpoint');
|
|
print('Response format untuk artikel: ${response.keys}');
|
|
|
|
if (response.containsKey('data')) {
|
|
if (response['data'] is List) {
|
|
print('Data adalah List dengan ${(response['data'] as List).length} items');
|
|
} else if (response['data'] is Map) {
|
|
print('Data adalah Map dengan keys: ${(response['data'] as Map).keys}');
|
|
|
|
if ((response['data'] as Map).containsKey('data')) {
|
|
final innerData = (response['data'] as Map)['data'];
|
|
if (innerData is List) {
|
|
print('Inner data adalah List dengan ${innerData.length} items');
|
|
} else {
|
|
print('Inner data bukan List: ${innerData.runtimeType}');
|
|
}
|
|
}
|
|
} else {
|
|
print('Data bukan List atau Map: ${response['data'].runtimeType}');
|
|
}
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) async {
|
|
return request(method: 'post', endpoint: endpoint, data: data);
|
|
}
|
|
|
|
Future<Map<String, dynamic>> put(String endpoint, Map<String, dynamic> data) async {
|
|
return request(method: 'put', endpoint: endpoint, data: data);
|
|
}
|
|
|
|
Future<Map<String, dynamic>> delete(String endpoint) async {
|
|
return request(method: 'delete', endpoint: endpoint);
|
|
}
|
|
|
|
// Token management
|
|
Future<void> setToken(String token) async {
|
|
_token = token;
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString('token', token);
|
|
if (_enableLogging) print('Token saved successfully to SharedPreferences: ${token.substring(0, min(10, token.length))}...');
|
|
}
|
|
|
|
Future<void> clearToken() async {
|
|
_token = null;
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove('token');
|
|
if (_enableLogging) print('Token cleared from SharedPreferences');
|
|
}
|
|
|
|
Future<void> loadToken() async {
|
|
if (_isLoadingToken) return;
|
|
|
|
try {
|
|
_isLoadingToken = true;
|
|
final prefs = await SharedPreferences.getInstance();
|
|
_token = prefs.getString('token');
|
|
if (_enableLogging) {
|
|
print('Token dari SharedPreferences: ${_token != null ? _token!.substring(0, min(10, _token!.length)) + "..." : "tidak ditemukan"}');
|
|
}
|
|
} catch (e) {
|
|
print('Error loading token: $e');
|
|
_token = null;
|
|
} finally {
|
|
_isLoadingToken = false;
|
|
}
|
|
}
|
|
|
|
// Cache management
|
|
void _invalidateRelatedCache(String endpoint) {
|
|
if (endpoint.startsWith('anak')) {
|
|
_responseCache.removeWhere((key, _) => key.startsWith('anak'));
|
|
} else if (endpoint.startsWith('perkembangan')) {
|
|
_responseCache.removeWhere((key, _) => key.startsWith('perkembangan'));
|
|
} else if (endpoint.startsWith('auth') || endpoint.startsWith('profile')) {
|
|
_responseCache.removeWhere((key, _) =>
|
|
key.startsWith('auth') ||
|
|
key.startsWith('profile') ||
|
|
key.startsWith('user')
|
|
);
|
|
}
|
|
}
|
|
|
|
void clearCache() {
|
|
_responseCache.clear();
|
|
if (_enableLogging) print('All API cache cleared');
|
|
}
|
|
|
|
void clearAuthCache() {
|
|
_responseCache.removeWhere((key, _) =>
|
|
key.startsWith('auth') ||
|
|
key.startsWith('profile') ||
|
|
key.startsWith('user')
|
|
);
|
|
if (_enableLogging) print('Auth-related cache cleared');
|
|
}
|
|
|
|
void clearChildCache() {
|
|
_responseCache.removeWhere((key, _) =>
|
|
key.startsWith('anak') ||
|
|
key.contains('orangtua') ||
|
|
key.contains('pengguna')
|
|
);
|
|
if (_enableLogging) print('Child-related cache cleared');
|
|
}
|
|
|
|
/// Memeriksa apakah server API dapat dijangkau
|
|
Future<bool> isServerReachable() async {
|
|
try {
|
|
if (_enableLogging) {
|
|
print('Memeriksa koneksi ke server API: $baseUrl');
|
|
}
|
|
|
|
final response = await http.get(
|
|
Uri.parse('$baseUrl/ping'),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
},
|
|
).timeout(Duration(seconds: 5));
|
|
|
|
if (_enableLogging) {
|
|
print('Status koneksi server: ${response.statusCode}');
|
|
}
|
|
|
|
return response.statusCode >= 200 && response.statusCode < 300;
|
|
} catch (e) {
|
|
if (_enableLogging) {
|
|
print('Gagal terhubung ke server API: $e');
|
|
if (e.toString().contains('TimeoutException')) {
|
|
print('!!! KONEKSI KE SERVER API TIMEOUT !!!');
|
|
} else if (e.toString().contains('SocketException')) {
|
|
print('!!! SERVER API TIDAK DAPAT DIJANGKAU !!!');
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|