auth: use sessions
This commit is contained in:
@@ -10,39 +10,30 @@ import '../models/change.dart';
|
||||
import '../models/sheet.dart';
|
||||
|
||||
/// HTTP client for communicating with the Sheetless API server.
|
||||
///
|
||||
/// Handles authentication, sheet listing, and PDF downloads.
|
||||
/// Provides caching for PDF files on native platforms.
|
||||
class ApiClient {
|
||||
final _log = Logger('ApiClient');
|
||||
|
||||
final String baseUrl;
|
||||
String? token;
|
||||
String? accessToken; // Short-lived jwt
|
||||
String? refreshToken; // Long lived binary token
|
||||
|
||||
ApiClient({required this.baseUrl, this.token});
|
||||
|
||||
/// Whether the client has an authentication token set.
|
||||
bool get isAuthenticated => token != null;
|
||||
ApiClient({required this.baseUrl, this.refreshToken});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Authentication
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Authenticates with the server and stores the JWT token.
|
||||
///
|
||||
/// Throws an [Exception] if login fails.
|
||||
Future<void> login(String username, String password) async {
|
||||
_log.info('Logging in...');
|
||||
final url = Uri.parse('$baseUrl/auth/login');
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'username': username, 'password': password}),
|
||||
);
|
||||
final response = await post("/auth/login", {
|
||||
'username': username,
|
||||
'password': password,
|
||||
}, authenticateWithAccessToken: false);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = jsonDecode(response.body);
|
||||
token = responseData['token'];
|
||||
accessToken = responseData['access_token'];
|
||||
refreshToken = responseData['refresh_token'];
|
||||
_log.info('Login successful');
|
||||
} else {
|
||||
throw ApiException(
|
||||
@@ -53,9 +44,37 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the authentication token.
|
||||
void logout() {
|
||||
token = null;
|
||||
Future<void> refreshAccessToken() async {
|
||||
_log.info('Refreshing access token...');
|
||||
_log.info('Refresh token is: $refreshToken');
|
||||
final response = await post("/auth/refresh", {
|
||||
"refresh_token": refreshToken,
|
||||
}, authenticateWithAccessToken: false);
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = jsonDecode(response.body);
|
||||
accessToken = responseData['access_token'];
|
||||
_log.info('Token refresh successful -> $accessToken');
|
||||
} else {
|
||||
throw ApiException(
|
||||
'Token refresh failed',
|
||||
statusCode: response.statusCode,
|
||||
body: response.body,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
await post(
|
||||
"/auth/logout",
|
||||
{},
|
||||
authenticateWithAccessToken: true,
|
||||
); // Ignoring response, logging out in any case
|
||||
} catch (e) {
|
||||
_log.fine('Logout failed: $e');
|
||||
}
|
||||
accessToken = null;
|
||||
refreshToken = null;
|
||||
_log.info('Logged out successfully');
|
||||
}
|
||||
|
||||
@@ -63,21 +82,46 @@ class ApiClient {
|
||||
// HTTP Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Map<String, String> _buildHeaders({bool isBinary = false}) {
|
||||
return {
|
||||
'Authorization': 'Bearer $token',
|
||||
Map<String, String> _buildHeaders({
|
||||
bool authenticateWithAccessToken = false,
|
||||
bool isBinary = false,
|
||||
}) {
|
||||
final headers = {
|
||||
if (authenticateWithAccessToken) 'Authorization': 'Bearer $accessToken',
|
||||
if (!isBinary) 'Content-Type': 'application/json',
|
||||
};
|
||||
_log.fine("Header is: $headers");
|
||||
return headers;
|
||||
}
|
||||
|
||||
/// Performs a GET request to the given endpoint.
|
||||
Future<http.Response> get(String endpoint, {bool isBinary = false}) async {
|
||||
Future<http.Response> get(
|
||||
String endpoint, {
|
||||
bool isBinary = false,
|
||||
bool authenticateWithAccessToken = true,
|
||||
bool isRetry = false,
|
||||
}) async {
|
||||
_log.fine("Starting get: $endpoint");
|
||||
final url = Uri.parse('$baseUrl$endpoint');
|
||||
final response = await http.get(
|
||||
url,
|
||||
headers: _buildHeaders(isBinary: isBinary),
|
||||
headers: _buildHeaders(
|
||||
isBinary: isBinary,
|
||||
authenticateWithAccessToken: authenticateWithAccessToken,
|
||||
),
|
||||
);
|
||||
|
||||
// Unauthorized, refresh access token
|
||||
if (authenticateWithAccessToken && response.statusCode == 401 && !isRetry) {
|
||||
await refreshAccessToken();
|
||||
return get(
|
||||
endpoint,
|
||||
isBinary: isBinary,
|
||||
authenticateWithAccessToken: authenticateWithAccessToken,
|
||||
isRetry: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
_log.warning(
|
||||
"GET '$endpoint' failed: ${response.statusCode}\n${response.body}",
|
||||
@@ -93,15 +137,33 @@ class ApiClient {
|
||||
}
|
||||
|
||||
/// Performs a POST request with JSON body.
|
||||
Future<http.Response> post(String endpoint, Map<String, dynamic> body) async {
|
||||
Future<http.Response> post(
|
||||
String endpoint,
|
||||
Map<String, dynamic> body, {
|
||||
bool authenticateWithAccessToken = true,
|
||||
bool isRetry = false,
|
||||
}) async {
|
||||
final url = Uri.parse('$baseUrl$endpoint');
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: _buildHeaders(),
|
||||
headers: _buildHeaders(
|
||||
authenticateWithAccessToken: authenticateWithAccessToken,
|
||||
),
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
|
||||
// Unauthorized, refresh access token
|
||||
if (authenticateWithAccessToken && response.statusCode == 401 && !isRetry) {
|
||||
await refreshAccessToken();
|
||||
return post(
|
||||
endpoint,
|
||||
body,
|
||||
authenticateWithAccessToken: authenticateWithAccessToken,
|
||||
isRetry: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
_log.warning(
|
||||
"POST '$endpoint' failed: ${response.statusCode}\n${response.body}",
|
||||
@@ -117,18 +179,28 @@ class ApiClient {
|
||||
}
|
||||
|
||||
/// Performs a POST request with form-encoded body.
|
||||
Future<http.Response> postFormData(String endpoint, String body) async {
|
||||
Future<http.Response> postFormData(
|
||||
String endpoint,
|
||||
String body, {
|
||||
bool isRetry = false,
|
||||
}) async {
|
||||
final url = Uri.parse('$baseUrl$endpoint');
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Authorization': 'Bearer $accessToken',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: body,
|
||||
);
|
||||
|
||||
// Unauthorized, refresh access token
|
||||
if (response.statusCode == 401 && !isRetry) {
|
||||
await refreshAccessToken();
|
||||
return postFormData(endpoint, body, isRetry: true);
|
||||
}
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
_log.warning(
|
||||
"POST Form '$endpoint' failed: ${response.statusCode}\n${response.body}",
|
||||
@@ -248,10 +320,9 @@ class ApiClient {
|
||||
/// Returns true if the server responds, false otherwise.
|
||||
Future<bool> checkConnection() async {
|
||||
try {
|
||||
final url = Uri.parse('$baseUrl/api/health');
|
||||
final response = await http
|
||||
.get(url, headers: _buildHeaders())
|
||||
.timeout(const Duration(seconds: 5));
|
||||
final response = await get(
|
||||
"/api/health",
|
||||
).timeout(const Duration(seconds: 5));
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
_log.fine('Connection check failed: $e');
|
||||
|
||||
Reference in New Issue
Block a user