check
This commit is contained in:
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user