337 lines
11 KiB
Dart
337 lines
11 KiB
Dart
// Backend API endpoints:
|
|
// POST /auth/login body: {"username":"...","secret":"..."}
|
|
// → {"access_token":"...","token_type":"bearer"}
|
|
// GET /groups/my header: Authorization: Bearer TOKEN
|
|
// → [{"id":"uuid","name":"...","type":"...","is_active":true}]
|
|
// POST /groups/ body: {"name":"..."}
|
|
// → GroupResponse
|
|
// GET /groups/{id}/members → [{"user_id":"...","username":"...","role":"...","is_online":bool}]
|
|
// POST /groups/{id}/invite body: {"username":"..."}
|
|
// → {"message":"...","notification_id":"..."}
|
|
// DELETE /groups/{id}/members/{user_id} → {"message":"..."}
|
|
// GET /notifications/ → [NotificationResponse]
|
|
// POST /notifications/{id}/respond body: {"is_accepted":bool}
|
|
//
|
|
// LiveKit token از طریق WebSocket دریافت میشود:
|
|
// WS /ws/groups/{id}?token={jwt}
|
|
|
|
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
import '../config/app_config.dart';
|
|
import '../models/channel.dart';
|
|
import '../models/group_member.dart';
|
|
import '../models/app_notification.dart';
|
|
import 'auth_service.dart';
|
|
|
|
// ── Fake data (only used when AppConfig.debug == true) ────────────────────
|
|
const _fakeChannels = [
|
|
{'id': '1', 'name': 'تیم آلفا', 'type': 'PUBLIC', 'is_active': true},
|
|
{'id': '2', 'name': 'پشتیبانی میدانی', 'type': 'PRIVATE', 'is_active': true},
|
|
{'id': '3', 'name': 'فرماندهی', 'type': 'PUBLIC', 'is_active': true},
|
|
{'id': '4', 'name': 'گروه لجستیک', 'type': 'PRIVATE', 'is_active': true},
|
|
{'id': '5', 'name': 'واحد امنیت', 'type': 'PUBLIC', 'is_active': true},
|
|
];
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
class ApiService {
|
|
final AuthService _auth;
|
|
|
|
ApiService(this._auth);
|
|
|
|
Future<Map<String, String>> _headers() async {
|
|
final token = await _auth.getToken();
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
if (token != null) 'Authorization': 'Bearer $token',
|
|
};
|
|
}
|
|
|
|
// استخراج user_id از JWT token (بدون نیاز به کتابخانه)
|
|
String? _extractUserIdFromJwt(String token) {
|
|
try {
|
|
final parts = token.split('.');
|
|
if (parts.length < 2) return null;
|
|
var payload = parts[1];
|
|
while (payload.length % 4 != 0) {
|
|
payload += '=';
|
|
}
|
|
final decoded = utf8.decode(base64Url.decode(payload));
|
|
final map = jsonDecode(decoded) as Map<String, dynamic>;
|
|
return map['sub']?.toString();
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ── Auth ──────────────────────────────────────────────────────────────
|
|
|
|
Future<bool> login(String username, String secret) async {
|
|
if (AppConfig.debug) {
|
|
await Future.delayed(const Duration(milliseconds: 600));
|
|
await _auth.saveAuth(token: 'debug_jwt', userId: 'debug_user');
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
final res = await http
|
|
.post(
|
|
Uri.parse('${AppConfig.baseUrl}/auth/login'),
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({'username': username, 'secret': secret}),
|
|
)
|
|
.timeout(const Duration(seconds: 10));
|
|
|
|
if (res.statusCode == 200) {
|
|
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
|
final accessToken = data['access_token'] as String?;
|
|
if (accessToken == null) return false;
|
|
final userId = _extractUserIdFromJwt(accessToken);
|
|
await _auth.saveAuth(token: accessToken, userId: userId);
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ── Groups ────────────────────────────────────────────────────────────
|
|
|
|
Future<List<Channel>> getChannels() async {
|
|
if (AppConfig.debug) {
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
return _fakeChannels
|
|
.map((e) => Channel.fromJson(e as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
try {
|
|
final res = await http
|
|
.get(
|
|
Uri.parse('${AppConfig.baseUrl}/groups/my'),
|
|
headers: await _headers(),
|
|
)
|
|
.timeout(const Duration(seconds: 10));
|
|
|
|
if (res.statusCode == 200) {
|
|
final list = jsonDecode(res.body) as List;
|
|
return list
|
|
.map((e) => Channel.fromJson(e as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
return [];
|
|
} catch (_) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
Future<Channel?> createGroup(String name) async {
|
|
if (AppConfig.debug) {
|
|
await Future.delayed(const Duration(milliseconds: 400));
|
|
return Channel(
|
|
id: 'fake_${DateTime.now().millisecondsSinceEpoch}',
|
|
name: name,
|
|
);
|
|
}
|
|
|
|
try {
|
|
final res = await http
|
|
.post(
|
|
Uri.parse('${AppConfig.baseUrl}/groups/'),
|
|
headers: await _headers(),
|
|
body: jsonEncode({'name': name}),
|
|
)
|
|
.timeout(const Duration(seconds: 10));
|
|
|
|
if (res.statusCode == 200 || res.statusCode == 201) {
|
|
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
|
return Channel.fromJson(data);
|
|
}
|
|
return null;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<List<GroupMember>> getGroupMembers(String groupId) async {
|
|
if (AppConfig.debug) {
|
|
await Future.delayed(const Duration(milliseconds: 400));
|
|
return [
|
|
const GroupMember(
|
|
userId: 'u1',
|
|
username: 'علی',
|
|
role: 'MANAGER',
|
|
isOnline: true,
|
|
),
|
|
const GroupMember(
|
|
userId: 'u2',
|
|
username: 'رضا',
|
|
role: 'MEMBER',
|
|
isOnline: false,
|
|
),
|
|
const GroupMember(
|
|
userId: 'u3',
|
|
username: 'مریم',
|
|
role: 'MEMBER',
|
|
isOnline: true,
|
|
),
|
|
];
|
|
}
|
|
|
|
try {
|
|
final res = await http
|
|
.get(
|
|
Uri.parse('${AppConfig.baseUrl}/groups/$groupId/members'),
|
|
headers: await _headers(),
|
|
)
|
|
.timeout(const Duration(seconds: 10));
|
|
|
|
if (res.statusCode == 200) {
|
|
final list = jsonDecode(res.body) as List;
|
|
return list
|
|
.map((e) => GroupMember.fromJson(e as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
return [];
|
|
} catch (_) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
Future<String?> inviteMember(String groupId, String username) async {
|
|
if (AppConfig.debug) {
|
|
await Future.delayed(const Duration(milliseconds: 400));
|
|
return null; // success
|
|
}
|
|
|
|
try {
|
|
final res = await http
|
|
.post(
|
|
Uri.parse('${AppConfig.baseUrl}/groups/$groupId/invite'),
|
|
headers: await _headers(),
|
|
body: jsonEncode({'username': username}),
|
|
)
|
|
.timeout(const Duration(seconds: 10));
|
|
|
|
if (res.statusCode == 200 || res.statusCode == 201) {
|
|
return null; // success, no error
|
|
}
|
|
// برگرداندن پیام خطا از بکند
|
|
try {
|
|
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
|
return (data['detail'] as String?) ?? 'خطا در ارسال دعوت';
|
|
} catch (_) {
|
|
return 'خطا در ارسال دعوت';
|
|
}
|
|
} catch (_) {
|
|
return 'خطا در اتصال به سرور';
|
|
}
|
|
}
|
|
|
|
Future<String?> removeMember(String groupId, String userId) async {
|
|
if (AppConfig.debug) {
|
|
await Future.delayed(const Duration(milliseconds: 400));
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
final res = await http
|
|
.delete(
|
|
Uri.parse('${AppConfig.baseUrl}/groups/$groupId/members/$userId'),
|
|
headers: await _headers(),
|
|
)
|
|
.timeout(const Duration(seconds: 10));
|
|
|
|
if (res.statusCode == 200) return null;
|
|
try {
|
|
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
|
return (data['detail'] as String?) ?? 'خطا در حذف عضو';
|
|
} catch (_) {
|
|
return 'خطا در حذف عضو';
|
|
}
|
|
} catch (_) {
|
|
return 'خطا در اتصال به سرور';
|
|
}
|
|
}
|
|
|
|
// ── Notifications ─────────────────────────────────────────────────────
|
|
|
|
Future<List<AppNotification>> getNotifications() async {
|
|
if (AppConfig.debug) {
|
|
await Future.delayed(const Duration(milliseconds: 400));
|
|
return [
|
|
AppNotification(
|
|
id: 'n1',
|
|
title: 'دعوت به گروه',
|
|
description: 'شما به گروه "تیم آلفا" دعوت شدهاید',
|
|
type: 'JOIN_REQUEST',
|
|
groupId: '1',
|
|
isAccepted: null,
|
|
receiverId: 'debug_user',
|
|
senderId: 'u1',
|
|
),
|
|
AppNotification(
|
|
id: 'n2',
|
|
title: 'اطلاعیه سیستم',
|
|
description: 'سیستم بهروزرسانی شد',
|
|
type: 'PUBLIC',
|
|
isAccepted: null,
|
|
receiverId: 'debug_user',
|
|
),
|
|
];
|
|
}
|
|
|
|
try {
|
|
final res = await http
|
|
.get(
|
|
Uri.parse('${AppConfig.baseUrl}/notifications/'),
|
|
headers: await _headers(),
|
|
)
|
|
.timeout(const Duration(seconds: 10));
|
|
|
|
if (res.statusCode == 200) {
|
|
final list = jsonDecode(res.body) as List;
|
|
print(list);
|
|
return list
|
|
.map((e) => AppNotification.fromJson(e as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
return [];
|
|
} catch (_) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
Future<String?> respondToNotification(
|
|
String notificationId,
|
|
bool isAccepted,
|
|
) async {
|
|
if (AppConfig.debug) {
|
|
await Future.delayed(const Duration(milliseconds: 400));
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
final res = await http
|
|
.post(
|
|
Uri.parse(
|
|
'${AppConfig.baseUrl}/notifications/$notificationId/respond',
|
|
),
|
|
headers: await _headers(),
|
|
body: jsonEncode({'is_accepted': isAccepted}),
|
|
)
|
|
.timeout(const Duration(seconds: 10));
|
|
|
|
if (res.statusCode == 200) return null;
|
|
try {
|
|
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
|
return (data['detail'] as String?) ?? 'خطا در پاسخ به اعلان';
|
|
} catch (_) {
|
|
return 'خطا در پاسخ به اعلان';
|
|
}
|
|
} catch (_) {
|
|
return 'خطا در اتصال به سرور';
|
|
}
|
|
}
|
|
}
|