// 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 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 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 get _authHeaders => { if (config.token.isNotEmpty) 'Authorization': 'Bearer ${config.token}', }; Future> 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; } Future 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; config = config.copyWith( username: username, password: password, token: body['token'] as String? ?? '', userId: body['user_id'] as String? ?? '', ); return config; } Future> 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; } void close() => _http.close(); }