MIF_E31231623/android/wisata_app/lib/services/auth_service.dart

345 lines
10 KiB
Dart

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../config/api_config.dart';
import '../models/app_user.dart';
class AuthException implements Exception {
const AuthException(this.message, {this.errors = const {}});
final String message;
final Map<String, List<String>> errors;
@override
String toString() => message;
}
class AuthService {
static const String _loggedInKey = 'auth_logged_in';
static const String _tokenKey = 'auth_access_token';
static const String _nameKey = 'auth_user_name';
static const String _emailKey = 'auth_user_email';
Future<String?> get savedName async {
final preferences = await SharedPreferences.getInstance();
return _normalizeName(preferences.getString(_nameKey));
}
Future<AppUser> register({
required String name,
required String email,
required String password,
required String passwordConfirmation,
}) async {
if (password != passwordConfirmation) {
throw const AuthException('Konfirmasi password tidak sesuai.');
}
try {
final response = await _post('/register', body: {
'name': name.trim(),
'email': email.trim(),
'password': password,
'password_confirmation': passwordConfirmation,
});
final payload = _decodeResponse(response);
_throwIfUnsuccessful(response, payload);
final responseUser = _userFromPayload(payload);
final user = responseUser.copyWith(
name: _normalizeName(responseUser.name),
email: responseUser.email.isEmpty ? email.trim() : responseUser.email,
);
await _saveUser(user, loggedIn: false);
return user;
} on AuthException {
rethrow;
} catch (_) {
throw const AuthException(
'Tidak bisa terhubung ke server. Periksa koneksi dan alamat API.',
);
}
}
Future<AppUser> login({
required String email,
required String password,
}) async {
try {
final response = await _post('/login', body: {
'email': email.trim(),
'password': password,
});
final payload = _decodeResponse(response);
_throwIfUnsuccessful(response, payload);
final user = _userFromPayload(payload);
final token = _stringAt(payload, ['data', 'token']);
if (token.isEmpty) {
throw const AuthException('Token login tidak ditemukan dari server.');
}
await _saveUser(user, token: token, loggedIn: true);
return user;
} on AuthException {
rethrow;
} catch (_) {
throw const AuthException(
'Tidak bisa terhubung ke server. Periksa koneksi dan alamat API.',
);
}
}
Future<AppUser?> profile() async {
final preferences = await SharedPreferences.getInstance();
final loggedIn = preferences.getBool(_loggedInKey) ?? false;
if (!loggedIn) return null;
final token = preferences.getString(_tokenKey);
final email = preferences.getString(_emailKey);
final fallbackUser = email == null
? null
: AppUser(
id: 0,
name: _normalizeName(preferences.getString(_nameKey)),
email: email,
);
if (token == null || token.isEmpty) return fallbackUser;
try {
final response = await http
.get(
ApiConfig.apiUri('/profile'),
headers: _headers(token: token),
)
.timeout(const Duration(seconds: 10));
final payload = _decodeResponse(response);
_throwIfUnsuccessful(response, payload);
final user = _userFromPayload(payload);
await _saveUser(user, token: token, loggedIn: true);
return user;
} on AuthException {
rethrow;
} catch (_) {
return fallbackUser;
}
}
Future<void> logout() async {
final preferences = await SharedPreferences.getInstance();
final token = preferences.getString(_tokenKey);
if (token != null && token.isNotEmpty) {
try {
await _post('/logout', token: token);
} catch (_) {
// Session lokal tetap dibersihkan meski server logout gagal.
}
}
await preferences.setBool(_loggedInKey, false);
await preferences.remove(_tokenKey);
}
Future<void> clearSession() async {
final preferences = await SharedPreferences.getInstance();
await preferences.setBool(_loggedInKey, false);
await preferences.remove(_tokenKey);
}
Future<bool> checkApiHealth() async => true;
Future<bool> checkLaravelApiHealth() async {
try {
final response = await http
.get(ApiConfig.apiUri('/health'))
.timeout(const Duration(seconds: 3));
return response.statusCode >= 200 && response.statusCode < 500;
} catch (_) {
return false;
}
}
Future<void> _saveUser(
AppUser user, {
String? token,
required bool loggedIn,
}) async {
final preferences = await SharedPreferences.getInstance();
await preferences.setBool(_loggedInKey, loggedIn);
await preferences.setString(_nameKey, _normalizeName(user.name));
await preferences.setString(_emailKey, user.email);
if (token != null && token.isNotEmpty) {
await preferences.setString(_tokenKey, token);
}
}
Future<http.Response> _post(
String path, {
Map<String, dynamic>? body,
String? token,
}) async {
final url = ApiConfig.apiUri(path);
final isLoginRequest = path == '/login' || path == 'login';
if (isLoginRequest) {
debugPrint('LOGIN REQUEST METHOD: POST');
debugPrint('LOGIN REQUEST URL: $url');
}
try {
final response = await http
.post(
url,
headers: _headers(token: token),
body: jsonEncode(body ?? const <String, dynamic>{}),
)
.timeout(const Duration(seconds: 15));
if (isLoginRequest) {
debugPrint('LOGIN RESPONSE STATUS: ${response.statusCode}');
debugPrint('LOGIN RESPONSE BODY: ${response.body}');
}
return response;
} catch (error) {
if (isLoginRequest) {
debugPrint('LOGIN REQUEST ERROR: $error');
}
rethrow;
}
}
Map<String, String> _headers({String? token}) {
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token',
};
}
Map<String, dynamic> _decodeResponse(http.Response response) {
if (response.body.trim().isEmpty) return <String, dynamic>{};
final decoded = jsonDecode(response.body);
if (decoded is Map<String, dynamic>) return decoded;
throw const AuthException('Format response server tidak valid.');
}
void _throwIfUnsuccessful(
http.Response response,
Map<String, dynamic> payload,
) {
if (response.statusCode >= 200 && response.statusCode < 300) return;
throw AuthException(
_messageFromPayload(payload),
errors: _errorsFromPayload(payload),
);
}
AppUser _userFromPayload(Map<String, dynamic> payload) {
final data = payload['data'];
final userJson = data is Map<String, dynamic> ? data['user'] : null;
if (userJson is! Map<String, dynamic>) {
throw const AuthException('Data user tidak ditemukan dari server.');
}
final user = AppUser.fromJson(userJson);
return user.copyWith(name: _normalizeName(user.name));
}
String _messageFromPayload(Map<String, dynamic> payload) {
final errors = _errorsFromPayload(payload);
if (errors.isNotEmpty) {
return _localizeAuthMessage(errors.values.first.first);
}
final message = payload['message']?.toString().trim();
if (message != null && message.isNotEmpty && message != 'Success') {
return _localizeAuthMessage(message);
}
return 'Permintaan gagal diproses. Silakan coba lagi.';
}
Map<String, List<String>> _errorsFromPayload(Map<String, dynamic> payload) {
final errors = payload['errors'];
if (errors is! Map) return const {};
return errors.map((key, value) {
final messages = value is List
? value
.map((message) => _localizeAuthMessage(message.toString()))
.toList()
: <String>[_localizeAuthMessage(value.toString())];
return MapEntry(key.toString(), messages);
});
}
String _localizeAuthMessage(String message) {
final text = message.trim();
final normalized = text.toLowerCase();
if (normalized.contains('invalid credentials') ||
normalized.contains('incorrect password') ||
normalized.contains('wrong password') ||
normalized.contains('password is incorrect') ||
normalized.contains('these credentials do not match') ||
normalized.contains('the provided credentials are incorrect')) {
return 'Email atau password yang Anda masukkan salah.';
}
if (normalized.contains('user not found') ||
normalized.contains('email not found') ||
normalized.contains('no user found') ||
normalized.contains('account not found')) {
return 'Akun dengan email tersebut tidak ditemukan.';
}
if (normalized.contains('unauthenticated') ||
normalized.contains('unauthorized')) {
return 'Sesi Anda tidak valid. Silakan login kembali.';
}
if (normalized.contains('too many login attempts') ||
normalized.contains('too many attempts')) {
return 'Terlalu banyak percobaan login. Silakan coba lagi nanti.';
}
if (normalized.contains('email field is required') ||
normalized.contains('email is required')) {
return 'Email wajib diisi.';
}
if (normalized.contains('password field is required') ||
normalized.contains('password is required')) {
return 'Password wajib diisi.';
}
return text;
}
String _stringAt(Map<String, dynamic> payload, List<String> path) {
Object? current = payload;
for (final key in path) {
if (current is! Map<String, dynamic>) return '';
current = current[key];
}
return current?.toString().trim() ?? '';
}
String _normalizeName(String? value) {
final name = value?.trim() ?? '';
return name.isEmpty ? 'Wisatawan' : name;
}
}