161 lines
5.3 KiB
Dart
161 lines
5.3 KiB
Dart
import 'dart:io';
|
|
import 'dart:async';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:local_auth/local_auth.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:login_vault_app/constants.dart';
|
|
|
|
/// Handles app-level authentication, session, and local identity.
|
|
class AuthService {
|
|
final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
|
final LocalAuthentication _localAuth = LocalAuthentication();
|
|
|
|
// ---------- Identity (email + app password) ----------
|
|
|
|
Future<String?> getEmail() async =>
|
|
await _storage.read(key: AppConstants.secureKeyEmail);
|
|
|
|
Future<String?> getAppPassword() async =>
|
|
await _storage.read(key: AppConstants.secureKeyAppPassword);
|
|
|
|
Future<bool> isRegistered() async => (await getEmail()) != null;
|
|
|
|
Future<void> registerWithEmail(String email, String appPassword) async {
|
|
await _storage.write(key: AppConstants.secureKeyEmail, value: email);
|
|
await _storage.write(
|
|
key: AppConstants.secureKeyAppPassword, value: appPassword);
|
|
}
|
|
|
|
/// Verify the entered app password against the stored one.
|
|
Future<bool> verifyPassword(String password) async {
|
|
final stored = await getAppPassword();
|
|
if (stored == null) return false;
|
|
return stored == password;
|
|
}
|
|
|
|
Future<void> changePassword(String newPassword) async {
|
|
await _storage.write(
|
|
key: AppConstants.secureKeyAppPassword, value: newPassword);
|
|
}
|
|
|
|
// ---------- Biometric (fingerprint) ----------
|
|
|
|
/// Can the device authenticate with biometrics (fingerprint etc.)?
|
|
Future<bool> canUseBiometrics() async {
|
|
try {
|
|
final supported = await _localAuth.isDeviceSupported();
|
|
if (!supported) return false;
|
|
final available = await _localAuth.getAvailableBiometrics();
|
|
return available.isNotEmpty;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Prompt the OS biometric dialog.
|
|
/// Returns (success, errorMessage). errorMessage is null on success.
|
|
Future<(bool, String?)> authenticateWithBiometrics() async {
|
|
String? lastErr;
|
|
// Prefer strict biometric (fingerprint). If that fails, fall back to
|
|
// device credentials (PIN/pattern) so the user isn't hard-blocked.
|
|
for (final opt in [
|
|
const AuthenticationOptions(
|
|
biometricOnly: true,
|
|
stickyAuth: true,
|
|
sensitiveTransaction: false,
|
|
useErrorDialogs: true),
|
|
const AuthenticationOptions(
|
|
biometricOnly: false,
|
|
stickyAuth: true,
|
|
sensitiveTransaction: false,
|
|
useErrorDialogs: true),
|
|
]) {
|
|
try {
|
|
final ok = await _localAuth.authenticate(
|
|
localizedReason: 'Gunakan biometrik untuk membuka Login Vault',
|
|
options: opt,
|
|
);
|
|
if (ok) return (true, null);
|
|
} on PlatformException catch (e) {
|
|
lastErr = '${e.code}: ${e.message}';
|
|
// user cancel / negative button -> stop retrying
|
|
if (e.code == 'PasscodeNotSet' ||
|
|
e.code == 'NotAvailable' ||
|
|
e.code == 'NotEnrolled' ||
|
|
e.code == 'LockedOut' ||
|
|
e.code == 'UserCancel' ||
|
|
e.code == 'Canceled' ||
|
|
e.code == 'UserFallback') {
|
|
return (false, lastErr);
|
|
}
|
|
} catch (e) {
|
|
lastErr = e.toString();
|
|
}
|
|
}
|
|
return (false, lastErr);
|
|
}
|
|
|
|
// ---------- Session ----------
|
|
|
|
/// Persist that a session was just opened (with timestamp).
|
|
Future<void> openSession() async {
|
|
await _storage.write(
|
|
key: AppConstants.secureKeySession, value: DateTime.now().toIso8601String());
|
|
}
|
|
|
|
Future<void> closeSession() async {
|
|
await _storage.delete(key: AppConstants.secureKeySession);
|
|
}
|
|
|
|
/// Returns true if a valid, non-expired session exists.
|
|
/// [timeoutMinutes] = 0 means "stay logged in until manual logout".
|
|
Future<bool> hasValidSession(int timeoutMinutes) async {
|
|
if (timeoutMinutes <= 0) {
|
|
// never auto-expire; just check a session was opened
|
|
final v = await _storage.read(key: AppConstants.secureKeySession);
|
|
return v != null;
|
|
}
|
|
final v = await _storage.read(key: AppConstants.secureKeySession);
|
|
if (v == null) return false;
|
|
final opened = DateTime.tryParse(v);
|
|
if (opened == null) return false;
|
|
final diff = DateTime.now().difference(opened).inMinutes;
|
|
return diff < timeoutMinutes;
|
|
}
|
|
|
|
// ---------- Timeout setting ----------
|
|
|
|
/// Get configured session timeout (minutes). Default 3.
|
|
Future<int> getTimeoutMinutes() async {
|
|
final sp = await _prefs();
|
|
return sp.getInt(AppConstants.prefTimeout) ?? 3;
|
|
}
|
|
|
|
Future<void> setTimeoutMinutes(int minutes) async {
|
|
final sp = await _prefs();
|
|
await sp.setInt(AppConstants.prefTimeout, minutes);
|
|
}
|
|
|
|
// ---------- Biometric toggle setting ----------
|
|
|
|
Future<bool> isBiometricEnabled() async {
|
|
final sp = await _prefs();
|
|
return sp.getBool(AppConstants.prefBiometric) ?? false;
|
|
}
|
|
|
|
Future<void> setBiometricEnabled(bool on) async {
|
|
final sp = await _prefs();
|
|
await sp.setBool(AppConstants.prefBiometric, on);
|
|
}
|
|
|
|
Future<SharedPreferences> _prefs() async =>
|
|
await SharedPreferences.getInstance();
|
|
|
|
Future<void> reset() async {
|
|
await _storage.delete(key: AppConstants.secureKeyEmail);
|
|
await _storage.delete(key: AppConstants.secureKeyAppPassword);
|
|
await _storage.delete(key: AppConstants.secureKeySession);
|
|
}
|
|
}
|