84 lines
2.8 KiB
Dart
84 lines
2.8 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
|
|
import '../../domain/entities/pos_session.dart';
|
|
|
|
/// Where the signed-in session lives between launches.
|
|
///
|
|
/// The platform keystore, not SQLite — Keychain on macOS, Credential Manager
|
|
/// on Windows, the Android Keystore on a tablet. The response carries a bearer
|
|
/// token and every staff PIN in the clear, and the SQLite file sits on a
|
|
/// machine behind a shop counter readable by anything that can open it.
|
|
///
|
|
/// Stored as one blob rather than field by field so [clear] is a single
|
|
/// delete. A sign-out that leaves half a session behind is worse than one that
|
|
/// leaves none.
|
|
class SessionStore {
|
|
SessionStore({FlutterSecureStorage? secureStorage})
|
|
: _secure = secureStorage ?? const FlutterSecureStorage();
|
|
|
|
final FlutterSecureStorage _secure;
|
|
|
|
static const String _key = 'pos.session';
|
|
|
|
/// The stored session, or null if there is none, it cannot be read, or it
|
|
/// has expired.
|
|
///
|
|
/// An expired token is treated as absent and swept: carrying it forward only
|
|
/// moves the failure to the first call that uses it, which is a cashier
|
|
/// discovering it mid-sale rather than at the login screen.
|
|
Future<PosSession?> read() async {
|
|
String? raw;
|
|
try {
|
|
raw = await _secure.read(key: _key);
|
|
} on Object catch (e) {
|
|
// No keystore — a headless test host, or a Linux box with no secret
|
|
// service. The terminal still runs, it just asks for credentials.
|
|
debugPrint('SessionStore: keystore unavailable ($e)');
|
|
return null;
|
|
}
|
|
|
|
if (raw == null || raw.isEmpty) return null;
|
|
|
|
PosSession session;
|
|
try {
|
|
session = PosSession.fromJson(jsonDecode(raw) as Map<String, Object?>);
|
|
} on Object catch (e) {
|
|
// A blob this build cannot parse — an upgrade that changed the shape.
|
|
// Drop it rather than failing every launch from here on.
|
|
debugPrint('SessionStore: unreadable session dropped ($e)');
|
|
await clear();
|
|
return null;
|
|
}
|
|
|
|
if (session.token.isEmpty || session.isExpired) {
|
|
await clear();
|
|
return null;
|
|
}
|
|
|
|
return session;
|
|
}
|
|
|
|
Future<void> save(PosSession session) async {
|
|
try {
|
|
await _secure.write(key: _key, value: jsonEncode(session.toJson()));
|
|
} on Object catch (e) {
|
|
// Not fatal: the session is live in memory and this shift carries on.
|
|
// The next launch just asks for credentials again.
|
|
debugPrint('SessionStore: could not persist session ($e)');
|
|
}
|
|
}
|
|
|
|
/// Removes the session. Called on every sign-out, and on an expired or
|
|
/// rejected token.
|
|
Future<void> clear() async {
|
|
try {
|
|
await _secure.delete(key: _key);
|
|
} on Object catch (e) {
|
|
debugPrint('SessionStore: could not clear session ($e)');
|
|
}
|
|
}
|
|
}
|