140 lines
4.4 KiB
Dart
140 lines
4.4 KiB
Dart
|
|
// HTTP client for the optional self-hosted BadNote Server (/api/v1).
|
||
|
|
// WebDAV remains the primary NAS sync path; this client covers health, auth,
|
||
|
|
// vault manifest assist, and OCR job submit/poll.
|
||
|
|
|
||
|
|
import 'dart:convert';
|
||
|
|
|
||
|
|
import 'package:http/http.dart' as http;
|
||
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
||
|
|
|
||
|
|
class BadNoteServerConfig {
|
||
|
|
const BadNoteServerConfig({
|
||
|
|
this.baseUrl = '',
|
||
|
|
this.username = '',
|
||
|
|
this.password = '',
|
||
|
|
this.token = '',
|
||
|
|
this.userId = '',
|
||
|
|
});
|
||
|
|
|
||
|
|
final String baseUrl;
|
||
|
|
final String username;
|
||
|
|
final String password;
|
||
|
|
final String token;
|
||
|
|
final String userId;
|
||
|
|
|
||
|
|
bool get isConfigured => baseUrl.trim().isNotEmpty;
|
||
|
|
bool get isLoggedIn => token.isNotEmpty;
|
||
|
|
|
||
|
|
Uri? uri(String path) {
|
||
|
|
final base = baseUrl.trim().replaceAll(RegExp(r'/+$'), '');
|
||
|
|
if (base.isEmpty) return null;
|
||
|
|
final p = path.startsWith('/') ? path : '/$path';
|
||
|
|
return Uri.parse('$base$p');
|
||
|
|
}
|
||
|
|
|
||
|
|
BadNoteServerConfig copyWith({
|
||
|
|
String? baseUrl,
|
||
|
|
String? username,
|
||
|
|
String? password,
|
||
|
|
String? token,
|
||
|
|
String? userId,
|
||
|
|
}) =>
|
||
|
|
BadNoteServerConfig(
|
||
|
|
baseUrl: baseUrl ?? this.baseUrl,
|
||
|
|
username: username ?? this.username,
|
||
|
|
password: password ?? this.password,
|
||
|
|
token: token ?? this.token,
|
||
|
|
userId: userId ?? this.userId,
|
||
|
|
);
|
||
|
|
|
||
|
|
static const _kBase = 'badnote.server.baseUrl';
|
||
|
|
static const _kUser = 'badnote.server.username';
|
||
|
|
static const _kPass = 'badnote.server.password';
|
||
|
|
static const _kToken = 'badnote.server.token';
|
||
|
|
static const _kUid = 'badnote.server.userId';
|
||
|
|
|
||
|
|
static Future<BadNoteServerConfig> load(SharedPreferences prefs) async {
|
||
|
|
return BadNoteServerConfig(
|
||
|
|
baseUrl: prefs.getString(_kBase) ?? '',
|
||
|
|
username: prefs.getString(_kUser) ?? '',
|
||
|
|
password: prefs.getString(_kPass) ?? '',
|
||
|
|
token: prefs.getString(_kToken) ?? '',
|
||
|
|
userId: prefs.getString(_kUid) ?? '',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> save(SharedPreferences prefs) async {
|
||
|
|
await prefs.setString(_kBase, baseUrl);
|
||
|
|
await prefs.setString(_kUser, username);
|
||
|
|
await prefs.setString(_kPass, password);
|
||
|
|
await prefs.setString(_kToken, token);
|
||
|
|
await prefs.setString(_kUid, userId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class BadNoteServerClient {
|
||
|
|
BadNoteServerClient(this.config, {http.Client? httpClient})
|
||
|
|
: _http = httpClient ?? http.Client();
|
||
|
|
|
||
|
|
BadNoteServerConfig config;
|
||
|
|
final http.Client _http;
|
||
|
|
|
||
|
|
Map<String, String> get _authHeaders => {
|
||
|
|
if (config.token.isNotEmpty) 'Authorization': 'Bearer ${config.token}',
|
||
|
|
};
|
||
|
|
|
||
|
|
Future<Map<String, dynamic>> health() async {
|
||
|
|
final uri = config.uri('/api/v1/health');
|
||
|
|
if (uri == null) throw StateError('server URL not set');
|
||
|
|
final res = await _http.get(uri).timeout(const Duration(seconds: 8));
|
||
|
|
if (res.statusCode != 200) {
|
||
|
|
throw StateError('health ${res.statusCode}: ${res.body}');
|
||
|
|
}
|
||
|
|
return jsonDecode(res.body) as Map<String, dynamic>;
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<BadNoteServerConfig> registerOrLogin({
|
||
|
|
required String username,
|
||
|
|
required String password,
|
||
|
|
}) async {
|
||
|
|
final registerUri = config.uri('/api/v1/auth/register');
|
||
|
|
if (registerUri == null) throw StateError('server URL not set');
|
||
|
|
var res = await _http.post(
|
||
|
|
registerUri,
|
||
|
|
headers: {'Content-Type': 'application/json'},
|
||
|
|
body: jsonEncode({'username': username, 'password': password}),
|
||
|
|
);
|
||
|
|
if (res.statusCode == 409) {
|
||
|
|
final loginUri = config.uri('/api/v1/auth/login')!;
|
||
|
|
res = await _http.post(
|
||
|
|
loginUri,
|
||
|
|
headers: {'Content-Type': 'application/json'},
|
||
|
|
body: jsonEncode({'username': username, 'password': password}),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if (res.statusCode != 200 && res.statusCode != 201) {
|
||
|
|
throw StateError('auth ${res.statusCode}: ${res.body}');
|
||
|
|
}
|
||
|
|
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
||
|
|
config = config.copyWith(
|
||
|
|
username: username,
|
||
|
|
password: password,
|
||
|
|
token: body['token'] as String? ?? '',
|
||
|
|
userId: body['user_id'] as String? ?? '',
|
||
|
|
);
|
||
|
|
return config;
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<Map<String, dynamic>> vaultManifest() async {
|
||
|
|
final uri = config.uri('/api/v1/vault/manifest');
|
||
|
|
if (uri == null) throw StateError('server URL not set');
|
||
|
|
final res = await _http.get(uri, headers: _authHeaders);
|
||
|
|
if (res.statusCode != 200) {
|
||
|
|
throw StateError('manifest ${res.statusCode}: ${res.body}');
|
||
|
|
}
|
||
|
|
return jsonDecode(res.body) as Map<String, dynamic>;
|
||
|
|
}
|
||
|
|
|
||
|
|
void close() => _http.close();
|
||
|
|
}
|