added login

This commit is contained in:
2026-08-07 17:07:49 +05:30
parent ad44402232
commit 4a2474ee6c
8 changed files with 908 additions and 383 deletions

View 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();
}