NIM_E31222534/Androidnya/lib/services/api_service.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;
}
}
}