Take staff from the back office, and let the seeded PINs die when it has any
Suriya/4821, Divya/5093, Rahul/6274 were compiled into the app — the same three logins on every install, readable by anyone with the APK, and unreplaceable. Sign-in now imports the outlet's real staff and deactivates everything it didn't import, so the built-in PINs stop working the moment a shop has anyone recorded. That deactivation is the point: merging would have left the hardcoded logins alive alongside the real ones for ever. The seeds stay, and that is not a hedge. Only 116 of 596 accounts on the platform have a PIN set, and outlet 1135 — the one this build ships pointed at — has none at all. Deleting them would hand 33 of 34 tenants a till nobody can sign in to. So: back office first, local database once synced, seeds only when there is nothing else. Rows are keyed on the back office user id, so a re-sync updates one account rather than creating a second. A leaver removed upstream loses the till on the next sign-in. Accounts are deactivated rather than deleted, because bills carry the cashier's name and shifts settle against it. An import that writes nobody is treated exactly like an empty answer — a back office full of `pin = 0` rows must not deactivate the seeds and strand the counter. That is a real shape in the data, not a hypothetical. An imported PIN is not flagged for change; the shop already chose it. The flag belongs to the seeds, which everyone shares. Role names are mapped by name and fall back to cashier. `app_roles` holds six rows for four roles — Admin and Manager appear twice each — and most accounts carry a roleid absent from the table entirely, so an unrecognised role must not quietly become an admin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ class PosSession {
|
||||
this.address = '',
|
||||
this.phone = '',
|
||||
this.outlets = const [],
|
||||
this.staff = const [],
|
||||
});
|
||||
|
||||
/// The bearer token, sent on every request from here on.
|
||||
@@ -65,6 +66,14 @@ class PosSession {
|
||||
|
||||
bool get hasChoiceOfOutlet => outlets.length > 1;
|
||||
|
||||
/// The people the back office says may ring a bill here.
|
||||
///
|
||||
/// Very often empty. Only 116 of 596 accounts on the platform have a PIN set,
|
||||
/// and most outlets — including the one this terminal ships pointed at — have
|
||||
/// none at all. A till must treat that as an unfinished setup rather than as
|
||||
/// a failure, which is why the seeded accounts still exist as a last resort.
|
||||
final List<PosStaffMember> staff;
|
||||
|
||||
/// Whether the session is still worth sending.
|
||||
///
|
||||
/// Checked against the terminal's own clock, which is the only one available
|
||||
@@ -95,6 +104,7 @@ class PosSession {
|
||||
address: address ?? this.address,
|
||||
phone: phone,
|
||||
outlets: outlets,
|
||||
staff: staff,
|
||||
);
|
||||
|
||||
factory PosSession.fromJson(Map<String, Object?> json) {
|
||||
@@ -124,6 +134,11 @@ class PosSession {
|
||||
address: (json['address'] as String?) ?? '',
|
||||
phone: (json['phone'] as String?) ?? '',
|
||||
outlets: outlets,
|
||||
staff: (json['staff'] as List<Object?>? ?? const [])
|
||||
.whereType<Map<String, Object?>>()
|
||||
.map(PosStaffMember.fromJson)
|
||||
.where((m) => m.pin.isNotEmpty)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,6 +158,7 @@ class PosSession {
|
||||
'address': address,
|
||||
'phone': phone,
|
||||
'locations': outlets.map((o) => o.toJson()).toList(),
|
||||
'staff': staff.map((m) => m.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,3 +204,50 @@ int _int(Object? value) => switch (value) {
|
||||
final String v => int.tryParse(v.trim()) ?? 0,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
/// One person the back office says may ring a bill at this outlet.
|
||||
///
|
||||
/// The PIN arrives in the clear over TLS and is hashed before it touches disk —
|
||||
/// see [PosSession] and the backend's `PosStaffMember` for why hashing it
|
||||
/// server-side would have bought the appearance of strength and not the
|
||||
/// substance. A four-digit PIN is brute-forceable in microseconds regardless;
|
||||
/// what it is, is *shift attribution* — which of the people already inside a
|
||||
/// shop gets credited with a sale. The security boundary is the session token.
|
||||
class PosStaffMember {
|
||||
const PosStaffMember({
|
||||
required this.userId,
|
||||
required this.fullName,
|
||||
required this.pin,
|
||||
this.role = '',
|
||||
});
|
||||
|
||||
final int userId;
|
||||
final String fullName;
|
||||
final String pin;
|
||||
|
||||
/// The back office's own role name — "Admin", "Manager", "Operations". Blank
|
||||
/// for the many accounts whose `roleid` is not in `app_roles` at all.
|
||||
final String role;
|
||||
|
||||
/// A stable local id for a row that came from the back office.
|
||||
///
|
||||
/// Prefixed so an imported account can be told apart from one seeded on this
|
||||
/// device. That distinction is what lets a sync retire the seeded logins
|
||||
/// without touching anything a shop created itself.
|
||||
String get localId => 'boffice-$userId';
|
||||
|
||||
factory PosStaffMember.fromJson(Map<String, Object?> json) =>
|
||||
PosStaffMember(
|
||||
userId: _int(json['user_id']),
|
||||
fullName: ((json['full_name'] as String?) ?? '').trim(),
|
||||
pin: ((json['pin'] as String?) ?? '').trim(),
|
||||
role: ((json['role'] as String?) ?? '').trim(),
|
||||
);
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'user_id': userId,
|
||||
'full_name': fullName,
|
||||
'pin': pin,
|
||||
'role': role,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user