Take the terminal's role from the back office, not from which tab was clicked
The role split was right; only its source was wrong. Signing in matched what was typed against two constants compiled into the app — admin@nearle.in and cashier@nearle.in — so which shell a person got was a property of the *build*. A shop could not add a third person, revoke either of the two it had, or stop anyone with the APK reading both passwords out of it. TerminalLogin survives unchanged in shape, because the shape was the good part: one flag the shell reads, a session that decides it, and a cashier sign-out that takes the catalogue with it while a supervisor's leaves it behind. Every consumer — visibleModulesProvider, resolvedModuleProvider, the sidebar, the page header, the sign-out dialog — is untouched. What changed is that the enum is now only constructible from a session the back office signed, so there is no path left where the terminal grants itself a permission the server did not send. It reads `can_manage_staff` rather than the role name or id. app_roles holds six rows for four distinct roles, a great many accounts carry a roleid that is not in the table at all, and the name comes back blank for most of them. Matching on either would mean shipping a copy of the role table in the app and keeping the two in step for ever. One boolean, decided server-side, cannot drift. It defaults to false, which matters on the restore path: a session saved by a build that predates the field comes back as a cashier, never silently as an admin. This also restores the sign-in layer itself — pos_auth_api, pos_session, session_store, the staff import and the bearer token — which an earlier commit removed wholesale from a stale checkout. Its parent was the commit that added them, so the deletion was a bad merge rather than a decision; the terminal has been running on the two constants since. The login screen loses its role tabs and its credential prefill. You do not choose what you are on the way in. The opener is now matched on the back office user id rather than on the first account with a matching role, so the first bill of a shift is attributed to whoever actually signed in. Tests: the smoke suite pinned only the supervisor shell, and it was passing for the wrong reason — the fake session omitted can_manage_staff, and the sidebar it asserted on was there because the role was hardcoded. Both halves are pinned now and the fake is parameterised. widget_test.dart was the stock Flutter counter template, restored by the same bad merge, testing a MyApp that has never existed in this repo. 292 tests pass; analyzer reports no errors and no warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
81
lib/data/local/session_store.dart
Normal file
81
lib/data/local/session_store.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
import '../../domain/entities/pos_session.dart';
|
||||
|
||||
/// Keeps a terminal signed in across restarts.
|
||||
///
|
||||
/// A till is not a browser. It signs in when a shop opens and bills for the
|
||||
/// whole trading day, often on a connection that comes and goes, and it must
|
||||
/// survive being rebooted mid-shift without a queue of customers waiting while
|
||||
/// somebody finds the manager's password.
|
||||
///
|
||||
/// The whole session goes to the platform keystore rather than to SQLite. The
|
||||
/// token is a bearer credential — anything holding it can bill as this shop —
|
||||
/// and SQLite here is a file on a machine behind a shop counter, readable by
|
||||
/// anything that can open it. The rest of the session travels with the token
|
||||
/// because splitting them invites the two halves to disagree about which outlet
|
||||
/// this terminal is.
|
||||
class SessionStore {
|
||||
SessionStore({FlutterSecureStorage? secureStorage})
|
||||
: _secure = secureStorage ?? const FlutterSecureStorage();
|
||||
|
||||
final FlutterSecureStorage _secure;
|
||||
|
||||
static const _key = 'pos.session';
|
||||
|
||||
/// Reads the saved session, or null when there is none worth using.
|
||||
///
|
||||
/// An expired session is treated as absent rather than returned for the
|
||||
/// caller to check. Every caller would have to make the same check, and the
|
||||
/// one that forgot would send a dead token all day and read the resulting
|
||||
/// 401s as a server fault.
|
||||
Future<PosSession?> read({DateTime? now}) async {
|
||||
final 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. Signing in again is the safe way to fail.
|
||||
debugPrint('Secure storage unavailable, session not loaded: $e');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (raw == null || raw.isEmpty) return null;
|
||||
|
||||
try {
|
||||
final session =
|
||||
PosSession.fromJson(jsonDecode(raw) as Map<String, Object?>);
|
||||
if (!session.isValidAt(now ?? DateTime.now())) return null;
|
||||
if (session.token.isEmpty || session.locationId <= 0) return null;
|
||||
return session;
|
||||
} on Object catch (e) {
|
||||
// A stored session this build cannot parse — most likely written by an
|
||||
// older one. Dropped rather than repaired: a half-understood session is
|
||||
// worse than none, and re-authenticating costs one screen.
|
||||
debugPrint('Stored session could not be read, discarding: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> write(PosSession session) async {
|
||||
try {
|
||||
await _secure.write(key: _key, value: jsonEncode(session.toJson()));
|
||||
} on Object catch (e) {
|
||||
// The terminal keeps working on the session it holds in memory; it just
|
||||
// will not survive a restart. Failing the sign-in over this would close a
|
||||
// shop for a keystore problem.
|
||||
debugPrint('Could not persist the session: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
try {
|
||||
await _secure.delete(key: _key);
|
||||
} on Object catch (e) {
|
||||
debugPrint('Could not clear the session: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user