This commit is contained in:
2026-08-07 14:34:25 +05:30
parent 829e5a8188
commit 0988d39d8b
19 changed files with 850 additions and 932 deletions

View File

@@ -1,149 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../domain/entities/pos_session.dart';
/// Raised when the back office refuses or cannot answer a sign-in.
///
/// Carries a message meant to be shown to whoever is standing at the till, so
/// it is written for them rather than for a log: what happened, and what they
/// can do about it.
class PosAuthException implements Exception {
const PosAuthException(this.message, {this.isCredentialFailure = false});
final String message;
/// Whether the details were wrong, as opposed to the back office being
/// unreachable. The till reacts differently: a bad password is worth
/// re-typing, an unreachable server is worth waiting for.
final bool isCredentialFailure;
@override
String toString() => message;
}
/// Signs a terminal in against the back office.
///
/// Talks to the same `app_users` accounts as the web console, so a manager who
/// can open the back office can open the till with the same details — one
/// account store means deactivating a leaver closes both doors at once.
///
/// ```
/// POST {base}/login
/// { "authname": "…", "password": "…", "terminal_id": "T5EDD" }
/// ```
///
/// answered with `{ code, status, details: { token, store_id, locations, … } }`.
class PosAuthApi {
PosAuthApi({required this.baseUrl, http.Client? client})
: _client = client ?? http.Client();
final String baseUrl;
final http.Client _client;
/// Generous, because this runs on a shop's connection while somebody watches.
/// Short enough that a dead endpoint is reported rather than hung on.
static const _timeout = Duration(seconds: 20);
/// Exchanges credentials for a session.
///
/// [locationId] is only meaningful for an account entitled to several
/// outlets: it says which one this terminal is standing in. It is a request,
/// not an assertion — the back office checks it against what the account may
/// actually reach, and that check is the whole point of the endpoint.
Future<PosSession> login({
required String authname,
required String password,
String? terminalId,
String? deviceId,
int? locationId,
int? configId,
}) async {
if (baseUrl.isEmpty) {
throw const PosAuthException(
'This terminal has no back office configured. Set the endpoint in '
'Settings → Connectivity & sync.',
);
}
final body = <String, Object?>{
'authname': authname.trim(),
'password': password,
if (terminalId != null && terminalId.isNotEmpty) 'terminal_id': terminalId,
if (deviceId != null && deviceId.isNotEmpty) 'device_id': deviceId,
if (locationId != null && locationId > 0) 'location_id': locationId,
// Sent only when known. The backend infers it when absent, and a shop
// has no way to find out what its configid is.
if (configId != null && configId > 0) 'configid': configId,
};
final http.Response response;
try {
response = await _client
.post(
Uri.parse('$baseUrl/login'),
headers: const {'Content-Type': 'application/json'},
body: jsonEncode(body),
)
.timeout(_timeout);
} on TimeoutException {
throw const PosAuthException(
'The back office did not answer in time. Check the connection and try '
'again.',
);
} on Object {
throw const PosAuthException(
'Could not reach the back office. Check the connection and try again.',
);
}
Map<String, Object?> decoded;
try {
decoded = jsonDecode(response.body) as Map<String, Object?>;
} on Object {
throw PosAuthException(
'The back office answered with something this terminal could not read '
'(HTTP ${response.statusCode}).',
);
}
if (response.statusCode != 200) {
throw PosAuthException(
(decoded['message'] as String?) ??
'Sign-in was refused (HTTP ${response.statusCode}).',
// 401 is a wrong email or password; 403 is a real account that may not
// open this till. Only the first is worth re-typing.
isCredentialFailure: response.statusCode == 401,
);
}
final details = decoded['details'];
if (details is! Map<String, Object?>) {
throw const PosAuthException(
'The back office accepted the sign-in but returned no session.',
);
}
final session = PosSession.fromJson(details);
// A session with no token cannot authenticate anything, and one with no
// outlet cannot bill. Refused here rather than being saved and failing
// later against every request, which would be much harder to diagnose.
if (session.token.isEmpty) {
throw const PosAuthException(
'The back office returned a session with no token.',
);
}
if (session.locationId <= 0) {
throw const PosAuthException(
'This account is not attached to an outlet, so it cannot open a till.',
);
}
return session;
}
void dispose() => _client.close();
}