f1e0f6b5ef
- Add complete server infrastructure with Gitea, API, PostgreSQL - Implement Obsidian-compatible markdown mindmap converter - Add Flutter service for server synchronization - Configure GitHub Actions for automated build and deploy - Add production docker-compose with health checks - Include VPS setup and deployment scripts - Add comprehensive deployment documentation
361 lines
10 KiB
Dart
361 lines
10 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
/// Сервис для синхронизации с Nanaxe Server
|
|
/// Поддерживает работу с mindmaps через REST API
|
|
class NanaxeServerService {
|
|
static final NanaxeServerService _instance = NanaxeServerService._internal();
|
|
factory NanaxeServerService() => _instance;
|
|
NanaxeServerService._internal();
|
|
|
|
String? _baseUrl;
|
|
String? _authToken;
|
|
|
|
static const String _prefsUrlKey = 'nanaxe_server_url';
|
|
static const String _prefsTokenKey = 'nanaxe_server_token';
|
|
|
|
/// Инициализация сервиса
|
|
Future<void> initialize() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
_baseUrl = prefs.getString(_prefsUrlKey);
|
|
_authToken = prefs.getString(_prefsTokenKey);
|
|
}
|
|
|
|
/// Настройка сервера
|
|
Future<void> configure(String baseUrl, {String? token}) async {
|
|
_baseUrl = baseUrl.replaceAll(RegExp(r'/+$'), ''); // Remove trailing slashes
|
|
_authToken = token;
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(_prefsUrlKey, _baseUrl!);
|
|
if (token != null) {
|
|
await prefs.setString(_prefsTokenKey, token);
|
|
}
|
|
}
|
|
|
|
/// Очистка конфигурации
|
|
Future<void> clearConfiguration() async {
|
|
_baseUrl = null;
|
|
_authToken = null;
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove(_prefsUrlKey);
|
|
await prefs.remove(_prefsTokenKey);
|
|
}
|
|
|
|
/// Проверка, настроен ли сервер
|
|
bool get isConfigured => _baseUrl != null;
|
|
|
|
/// Получение базового URL
|
|
String? get baseUrl => _baseUrl;
|
|
|
|
// ==================== AUTH ====================
|
|
|
|
/// Регистрация нового пользователя
|
|
Future<Map<String, dynamic>> register(String email, String password, String username) async {
|
|
if (_baseUrl == null) throw Exception('Server not configured');
|
|
|
|
final response = await http.post(
|
|
Uri.parse('$_baseUrl/api/auth/register'),
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({
|
|
'email': email,
|
|
'password': password,
|
|
'username': username,
|
|
}),
|
|
);
|
|
|
|
final data = jsonDecode(response.body);
|
|
|
|
if (response.statusCode == 201) {
|
|
_authToken = data['token'];
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(_prefsTokenKey, _authToken!);
|
|
return data;
|
|
} else {
|
|
throw Exception(data['error'] ?? 'Registration failed');
|
|
}
|
|
}
|
|
|
|
/// Вход в систему
|
|
Future<Map<String, dynamic>> login(String email, String password) async {
|
|
if (_baseUrl == null) throw Exception('Server not configured');
|
|
|
|
final response = await http.post(
|
|
Uri.parse('$_baseUrl/api/auth/login'),
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({
|
|
'email': email,
|
|
'password': password,
|
|
}),
|
|
);
|
|
|
|
final data = jsonDecode(response.body);
|
|
|
|
if (response.statusCode == 200) {
|
|
_authToken = data['token'];
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(_prefsTokenKey, _authToken!);
|
|
return data;
|
|
} else {
|
|
throw Exception(data['error'] ?? 'Login failed');
|
|
}
|
|
}
|
|
|
|
/// Проверка валидности токена
|
|
Future<bool> validateToken() async {
|
|
if (_baseUrl == null || _authToken == null) return false;
|
|
|
|
try {
|
|
final response = await http.get(
|
|
Uri.parse('$_baseUrl/api/auth/validate'),
|
|
headers: _getHeaders(),
|
|
);
|
|
|
|
final data = jsonDecode(response.body);
|
|
return data['valid'] == true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ==================== MINDMAPS ====================
|
|
|
|
/// Получение списка всех mindmaps
|
|
Future<List<Map<String, dynamic>>> getMindMaps() async {
|
|
if (_baseUrl == null) throw Exception('Server not configured');
|
|
|
|
final response = await http.get(
|
|
Uri.parse('$_baseUrl/api/mindmaps'),
|
|
headers: _getHeaders(),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = jsonDecode(response.body);
|
|
return List<Map<String, dynamic>>.from(data['mindmaps'] ?? []);
|
|
} else {
|
|
throw Exception('Failed to load mindmaps');
|
|
}
|
|
}
|
|
|
|
/// Получение одной mindmap
|
|
Future<Map<String, dynamic>> getMindMap(String id) async {
|
|
if (_baseUrl == null) throw Exception('Server not configured');
|
|
|
|
final response = await http.get(
|
|
Uri.parse('$_baseUrl/api/mindmaps/$id'),
|
|
headers: _getHeaders(),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
return jsonDecode(response.body);
|
|
} else {
|
|
throw Exception('Failed to load mindmap');
|
|
}
|
|
}
|
|
|
|
/// Создание новой mindmap
|
|
Future<Map<String, dynamic>> createMindMap({
|
|
required String title,
|
|
String? description,
|
|
List<Map<String, dynamic>>? nodes,
|
|
Map<String, dynamic>? settings,
|
|
}) async {
|
|
if (_baseUrl == null) throw Exception('Server not configured');
|
|
|
|
final response = await http.post(
|
|
Uri.parse('$_baseUrl/api/mindmaps'),
|
|
headers: _getHeaders(),
|
|
body: jsonEncode({
|
|
'title': title,
|
|
'description': description ?? '',
|
|
'nodes': nodes ?? [],
|
|
'settings': settings ?? {},
|
|
}),
|
|
);
|
|
|
|
if (response.statusCode == 201) {
|
|
return jsonDecode(response.body);
|
|
} else {
|
|
final data = jsonDecode(response.body);
|
|
throw Exception(data['error'] ?? 'Failed to create mindmap');
|
|
}
|
|
}
|
|
|
|
/// Обновление mindmap
|
|
Future<Map<String, dynamic>> updateMindMap(
|
|
String id, {
|
|
String? title,
|
|
String? description,
|
|
List<Map<String, dynamic>>? nodes,
|
|
Map<String, dynamic>? settings,
|
|
}) async {
|
|
if (_baseUrl == null) throw Exception('Server not configured');
|
|
|
|
final body = <String, dynamic>{};
|
|
if (title != null) body['title'] = title;
|
|
if (description != null) body['description'] = description;
|
|
if (nodes != null) body['nodes'] = nodes;
|
|
if (settings != null) body['settings'] = settings;
|
|
|
|
final response = await http.put(
|
|
Uri.parse('$_baseUrl/api/mindmaps/$id'),
|
|
headers: _getHeaders(),
|
|
body: jsonEncode(body),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
return jsonDecode(response.body);
|
|
} else {
|
|
throw Exception('Failed to update mindmap');
|
|
}
|
|
}
|
|
|
|
/// Удаление mindmap
|
|
Future<void> deleteMindMap(String id) async {
|
|
if (_baseUrl == null) throw Exception('Server not configured');
|
|
|
|
final response = await http.delete(
|
|
Uri.parse('$_baseUrl/api/mindmaps/$id'),
|
|
headers: _getHeaders(),
|
|
);
|
|
|
|
if (response.statusCode != 200) {
|
|
throw Exception('Failed to delete mindmap');
|
|
}
|
|
}
|
|
|
|
/// Получение mindmap как Markdown
|
|
Future<String> getMindMapAsMarkdown(String id) async {
|
|
if (_baseUrl == null) throw Exception('Server not configured');
|
|
|
|
final response = await http.get(
|
|
Uri.parse('$_baseUrl/api/mindmaps/$id/markdown'),
|
|
headers: _getHeaders(),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
return response.body;
|
|
} else {
|
|
throw Exception('Failed to load markdown');
|
|
}
|
|
}
|
|
|
|
/// Сохранение mindmap из Markdown
|
|
Future<Map<String, dynamic>> saveMindMapFromMarkdown(
|
|
String id,
|
|
String markdown,
|
|
) async {
|
|
if (_baseUrl == null) throw Exception('Server not configured');
|
|
|
|
final response = await http.post(
|
|
Uri.parse('$_baseUrl/api/mindmaps/$id/markdown'),
|
|
headers: _getHeaders(),
|
|
body: jsonEncode({'markdown': markdown}),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
return jsonDecode(response.body);
|
|
} else {
|
|
throw Exception('Failed to save markdown');
|
|
}
|
|
}
|
|
|
|
// ==================== SYNC ====================
|
|
|
|
/// Получение статуса синхронизации
|
|
Future<Map<String, dynamic>> getSyncStatus() async {
|
|
if (_baseUrl == null) throw Exception('Server not configured');
|
|
|
|
final response = await http.get(
|
|
Uri.parse('$_baseUrl/api/sync/status'),
|
|
headers: _getHeaders(),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
return jsonDecode(response.body);
|
|
} else {
|
|
throw Exception('Failed to get sync status');
|
|
}
|
|
}
|
|
|
|
/// Push изменений в Gitea
|
|
Future<Map<String, dynamic>> pushToGitea({String? commitMessage}) async {
|
|
if (_baseUrl == null) throw Exception('Server not configured');
|
|
|
|
final response = await http.post(
|
|
Uri.parse('$_baseUrl/api/sync/gitea/push'),
|
|
headers: _getHeaders(),
|
|
body: jsonEncode({
|
|
'commitMessage': commitMessage ?? 'Sync from Nanaxe CRM',
|
|
}),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
return jsonDecode(response.body);
|
|
} else {
|
|
throw Exception('Failed to push to Gitea');
|
|
}
|
|
}
|
|
|
|
/// Pull изменений из Gitea
|
|
Future<Map<String, dynamic>> pullFromGitea() async {
|
|
if (_baseUrl == null) throw Exception('Server not configured');
|
|
|
|
final response = await http.post(
|
|
Uri.parse('$_baseUrl/api/sync/gitea/pull'),
|
|
headers: _getHeaders(),
|
|
body: jsonEncode({}),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
return jsonDecode(response.body);
|
|
} else {
|
|
throw Exception('Failed to pull from Gitea');
|
|
}
|
|
}
|
|
|
|
// ==================== HELPERS ====================
|
|
|
|
Map<String, String> _getHeaders() {
|
|
final headers = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (_authToken != null) {
|
|
headers['Authorization'] = 'Bearer $_authToken';
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
/// Проверка соединения с сервером
|
|
Future<bool> checkConnection() async {
|
|
if (_baseUrl == null) return false;
|
|
|
|
try {
|
|
final response = await http
|
|
.get(Uri.parse('$_baseUrl/health'))
|
|
.timeout(const Duration(seconds: 5));
|
|
return response.statusCode == 200;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Провайдер для работы с сервером
|
|
class ServerConfig {
|
|
final String baseUrl;
|
|
final String? token;
|
|
final bool isConnected;
|
|
|
|
ServerConfig({
|
|
required this.baseUrl,
|
|
this.token,
|
|
this.isConnected = false,
|
|
});
|
|
}
|