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
This commit is contained in:
2026-04-19 22:18:25 +05:00
parent 52aea9ac87
commit f1e0f6b5ef
56 changed files with 8892 additions and 397 deletions
+178
View File
@@ -0,0 +1,178 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:uuid/uuid.dart';
import '../models/mindmap_model.dart';
import '../core/admin_access_service.dart';
import '../core/deepseek_service.dart';
// Provider для получения текущего email пользователя
final currentUserEmailProvider = Provider<String?>((ref) {
return FirebaseAuth.instance.currentUser?.email;
});
// Provider проверки является ли пользователь админом
final isAdminProvider = Provider<bool>((ref) {
final email = ref.watch(currentUserEmailProvider);
if (email == null) return false;
return AdminAccessService.adminEmails.contains(email.toLowerCase());
});
// Provider списка mind maps
final mindMapsProvider = StreamProvider<List<MindMapModel>>((ref) {
return FirebaseFirestore.instance
.collection('mind_maps')
.orderBy('createdAt', descending: true)
.snapshots()
.map((snapshot) {
return snapshot.docs.map((doc) {
return MindMapModel.fromFirestore(doc);
}).toList();
});
});
// Notifier для управления mind maps
class MindMapsNotifier extends StateNotifier<AsyncValue<List<MindMapModel>>> {
final Ref ref;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
MindMapsNotifier(this.ref) : super(const AsyncValue.loading()) {
_listenToMindMaps();
}
void _listenToMindMaps() {
_firestore
.collection('mind_maps')
.orderBy('createdAt', descending: true)
.snapshots()
.listen(
(snapshot) {
final mindMaps = snapshot.docs.map((doc) {
return MindMapModel.fromFirestore(doc);
}).toList();
state = AsyncValue.data(mindMaps);
},
onError: (error, st) {
state = AsyncValue.error(error, st);
},
);
}
// Создание новой mind map
Future<String> createMindMap({
required String title,
String description = '',
required List<Map<String, dynamic>> nodes,
bool isMain = false,
}) async {
try {
final userEmail = _auth.currentUser?.email ?? 'unknown';
final now = DateTime.now();
final id = const Uuid().v4();
final mindMap = MindMapModel(
id: id,
title: title,
description: description,
ownerEmail: userEmail,
isMain: isMain,
nodes: nodes,
createdAt: now,
updatedAt: now,
);
await _firestore.collection('mind_maps').doc(id).set(mindMap.toFirestore());
return id;
} catch (e, st) {
state = AsyncValue.error(e, st);
rethrow;
}
}
// Удаление mind map (админы могут удалять любые, обычные - только свои)
Future<void> deleteMindMap(String id, String ownerEmail) async {
try {
final isAdmin = ref.read(isAdminProvider);
final currentUserEmail = _auth.currentUser?.email ?? '';
// Проверка прав
if (!isAdmin && ownerEmail != currentUserEmail) {
throw Exception('Нет прав для удаления этой mind map');
}
await _firestore.collection('mind_maps').doc(id).delete();
} catch (e, st) {
state = AsyncValue.error(e, st);
rethrow;
}
}
// Обновление mind map
Future<void> updateMindMap(String id, MindMapModel mindMap) async {
try {
final isAdmin = ref.read(isAdminProvider);
final currentUserEmail = _auth.currentUser?.email ?? '';
// Проверка прав
if (!isAdmin && mindMap.ownerEmail != currentUserEmail) {
throw Exception('Нет прав для редактирования этой mind map');
}
await _firestore
.collection('mind_maps')
.doc(id)
.update(mindMap.copyWith(updatedAt: DateTime.now()).toFirestore());
} catch (e, st) {
state = AsyncValue.error(e, st);
rethrow;
}
}
// AI генерация mind map
Future<String> generateWithAI({
required String title,
required String description,
}) async {
try {
final nodesData = await deepSeekService.generateMindMap(
projectName: title,
description: description,
);
final nodes = <Map<String, dynamic>>[];
for (var i = 0; i < nodesData.length; i++) {
final nodeData = nodesData[i];
nodes.add({
'id': 'node_$i',
'level': nodeData['level'] ?? 0,
'parentId': nodeData['parentId'],
'title': nodeData['title'] ?? '',
'color': _getColorForLevel(nodeData['level'] ?? 0),
});
}
return await createMindMap(
title: title,
description: description,
nodes: nodes,
);
} catch (e, st) {
state = AsyncValue.error(e, st);
rethrow;
}
}
String _getColorForLevel(int level) {
final colors = [
'#2196F3', '#4CAF50', '#FF9800', '#E91E63', '#9C27B0', '#00BCD4'
];
return colors[level % colors.length];
}
}
final mindMapsNotifierProvider =
StateNotifierProvider<MindMapsNotifier, AsyncValue<List<MindMapModel>>>(
(ref) => MindMapsNotifier(ref),
);