Files
Avtex/lib/core/nanaxe_server_service.dart
T
Alexyz f1e0f6b5ef feat: Add Nanaxe server with Docker, GitHub Actions CI/CD, and auto-deployment
- 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
2026-04-19 22:18:25 +05:00

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,
});
}