added login
This commit is contained in:
163
lib/data/remote/pos_auth_api.dart
Normal file
163
lib/data/remote/pos_auth_api.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../domain/entities/pos_session.dart';
|
||||
|
||||
/// A sign-in that did not produce a session.
|
||||
///
|
||||
/// Carries a message written for the person at the counter, not a status code.
|
||||
/// [isCredentialFailure] separates "you typed the wrong password" from "the
|
||||
/// shop's internet is down", because the first is the operator's problem to
|
||||
/// fix and the second is not.
|
||||
class AuthApiException implements Exception {
|
||||
const AuthApiException(
|
||||
this.message, {
|
||||
this.isCredentialFailure = false,
|
||||
this.statusCode,
|
||||
});
|
||||
|
||||
final String message;
|
||||
final bool isCredentialFailure;
|
||||
final int? statusCode;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
/// Signs the terminal in against the back office.
|
||||
///
|
||||
/// ```
|
||||
/// POST {base}/login
|
||||
/// Content-Type: application/json
|
||||
///
|
||||
/// { "authname": …, "password": …, "device_id": …, "configid": 1 }
|
||||
/// ```
|
||||
///
|
||||
/// answering
|
||||
///
|
||||
/// ```json
|
||||
/// { "code": 200, "status": true, "message": "Login successful",
|
||||
/// "details": { "token": …, "role": "Supervisor", … } }
|
||||
/// ```
|
||||
///
|
||||
/// The envelope is checked rather than the HTTP status alone: this API answers
|
||||
/// `200` with `status: false` for a rejected credential, so trusting the
|
||||
/// status code would sign a terminal in on a failed login.
|
||||
class PosAuthApi {
|
||||
PosAuthApi({
|
||||
required this.baseUrl,
|
||||
this.configId = 1,
|
||||
http.Client? client,
|
||||
}) : _client = client ?? http.Client();
|
||||
|
||||
/// Same base as the catalogue and order endpoints, e.g.
|
||||
/// `https://fiesta.nearle.app/live/api/v1/pos`.
|
||||
final String baseUrl;
|
||||
|
||||
/// Which back-office configuration this terminal belongs to.
|
||||
final int configId;
|
||||
|
||||
final http.Client _client;
|
||||
|
||||
static const Duration _timeout = Duration(seconds: 20);
|
||||
|
||||
Future<PosSession> login({
|
||||
required String authname,
|
||||
required String password,
|
||||
required String deviceId,
|
||||
}) async {
|
||||
if (baseUrl.isEmpty) {
|
||||
throw const AuthApiException(
|
||||
'No back-office URL is configured for this terminal. Set one in '
|
||||
'Settings → Connectivity & sync → Configure.',
|
||||
);
|
||||
}
|
||||
|
||||
final uri = Uri.parse('${baseUrl.replaceAll(RegExp(r'/+$'), '')}/login');
|
||||
|
||||
http.Response response;
|
||||
try {
|
||||
response = await _client
|
||||
.post(
|
||||
uri,
|
||||
headers: const {
|
||||
'content-type': 'application/json',
|
||||
'accept': 'application/json',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'authname': authname.trim(),
|
||||
'password': password,
|
||||
// This device's own identity, minted on first run. Two terminals
|
||||
// must never sign in as the same device — the back office keys
|
||||
// sessions on it.
|
||||
'device_id': deviceId,
|
||||
'configid': configId,
|
||||
}),
|
||||
)
|
||||
.timeout(_timeout);
|
||||
} on TimeoutException {
|
||||
throw const AuthApiException(
|
||||
'The back office did not answer in time. Check the connection and '
|
||||
'try again.',
|
||||
);
|
||||
} on http.ClientException {
|
||||
// DNS failure, refused connection, dropped socket — the shop's line
|
||||
// rather than the operator's credentials.
|
||||
throw const AuthApiException(
|
||||
'Could not reach the back office. Check this terminal\'s internet '
|
||||
'connection.',
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
throw AuthApiException('Could not reach the back office: $e');
|
||||
}
|
||||
|
||||
Map<String, Object?> body;
|
||||
try {
|
||||
body = jsonDecode(response.body) as Map<String, Object?>;
|
||||
} on Object {
|
||||
throw AuthApiException(
|
||||
'The back office answered with something this terminal could not '
|
||||
'read (${response.statusCode}).',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
|
||||
final ok = body['status'] == true && response.statusCode < 300;
|
||||
|
||||
if (!ok) {
|
||||
final raw = body['message'];
|
||||
final message = raw is String ? raw.trim() : '';
|
||||
const rejectedCodes = {400, 401, 403, 422};
|
||||
|
||||
throw AuthApiException(
|
||||
// The server's own wording, when it gave one. It knows whether the
|
||||
// account is disabled, the device is unregistered or the password is
|
||||
// simply wrong, and a generic message would throw that away.
|
||||
message.isEmpty ? 'Sign-in failed (${response.statusCode}).' : message,
|
||||
isCredentialFailure: rejectedCodes.contains(response.statusCode),
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
|
||||
final details = body['details'];
|
||||
if (details is! Map<String, Object?>) {
|
||||
throw const AuthApiException(
|
||||
'The back office accepted the sign-in but sent no session back.',
|
||||
);
|
||||
}
|
||||
|
||||
final session = PosSession.fromDetails(details, authname: authname.trim());
|
||||
|
||||
if (session.token.isEmpty) {
|
||||
throw const AuthApiException(
|
||||
'The back office accepted the sign-in but issued no token.',
|
||||
);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
void dispose() => _client.close();
|
||||
}
|
||||
Reference in New Issue
Block a user