184 lines
5.5 KiB
Dart
184 lines
5.5 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
/// inou API client for authentication and data access
|
|
class InouApi {
|
|
static const String baseUrl = 'https://inou.com';
|
|
static const String _prefKeySessionToken = 'inou_session_token';
|
|
static const String _prefKeyDossierId = 'inou_dossier_id';
|
|
|
|
static final InouApi _instance = InouApi._internal();
|
|
factory InouApi() => _instance;
|
|
InouApi._internal();
|
|
|
|
String? _sessionToken;
|
|
String? _dossierId;
|
|
|
|
/// Current session token
|
|
String? get sessionToken => _sessionToken;
|
|
|
|
/// Current dossier ID
|
|
String? get dossierId => _dossierId;
|
|
|
|
/// Whether user is authenticated
|
|
bool get isAuthenticated => _sessionToken != null;
|
|
|
|
/// Initialize - load stored session
|
|
Future<void> initialize() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
_sessionToken = prefs.getString(_prefKeySessionToken);
|
|
_dossierId = prefs.getString(_prefKeyDossierId);
|
|
}
|
|
|
|
/// Request login code to be sent to email
|
|
/// Returns error message or null on success
|
|
Future<String?> sendLoginCode(String email) async {
|
|
try {
|
|
final response = await http.post(
|
|
Uri.parse('$baseUrl/send-code'),
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Accept': 'application/json',
|
|
},
|
|
body: {
|
|
'email': email,
|
|
'nonce': DateTime.now().millisecondsSinceEpoch.toString(),
|
|
},
|
|
);
|
|
|
|
debugPrint('send-code response: ${response.statusCode} ${response.body}');
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 302) {
|
|
// Success - code was sent
|
|
return null;
|
|
}
|
|
|
|
// Try to parse error from response
|
|
try {
|
|
final json = jsonDecode(response.body);
|
|
return json['error'] ?? 'Failed to send code';
|
|
} catch (_) {
|
|
return 'Failed to send code (${response.statusCode})';
|
|
}
|
|
} catch (e) {
|
|
debugPrint('send-code error: $e');
|
|
return 'Network error: ${e.toString()}';
|
|
}
|
|
}
|
|
|
|
/// Verify login code and get session
|
|
/// Returns error message or null on success
|
|
Future<String?> verifyCode(String email, String code) async {
|
|
try {
|
|
final response = await http.post(
|
|
Uri.parse('$baseUrl/verify'),
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Accept': 'application/json',
|
|
},
|
|
body: {
|
|
'email': email,
|
|
'code': code,
|
|
},
|
|
);
|
|
|
|
debugPrint('verify response: ${response.statusCode}');
|
|
debugPrint('verify headers: ${response.headers}');
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 302) {
|
|
// Check for session cookie in response
|
|
final setCookie = response.headers['set-cookie'];
|
|
if (setCookie != null) {
|
|
// Parse session token from cookie
|
|
final sessionMatch = RegExp(r'session=([^;]+)').firstMatch(setCookie);
|
|
if (sessionMatch != null) {
|
|
_sessionToken = sessionMatch.group(1);
|
|
await _saveSession();
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Try to get token from JSON body
|
|
try {
|
|
final json = jsonDecode(response.body);
|
|
if (json['token'] != null) {
|
|
_sessionToken = json['token'];
|
|
_dossierId = json['dossier_id'];
|
|
await _saveSession();
|
|
return null;
|
|
}
|
|
if (json['session_token'] != null) {
|
|
_sessionToken = json['session_token'];
|
|
_dossierId = json['dossier_id'];
|
|
await _saveSession();
|
|
return null;
|
|
}
|
|
} catch (_) {}
|
|
|
|
// Web redirect flow - we got success but need to handle differently
|
|
// For mobile, the server should return JSON with token
|
|
return null; // Assume success for now
|
|
}
|
|
|
|
// Try to parse error
|
|
try {
|
|
final json = jsonDecode(response.body);
|
|
return json['error'] ?? 'Invalid or expired code';
|
|
} catch (_) {
|
|
return 'Invalid or expired code';
|
|
}
|
|
} catch (e) {
|
|
debugPrint('verify error: $e');
|
|
return 'Network error: ${e.toString()}';
|
|
}
|
|
}
|
|
|
|
/// Logout - clear session
|
|
Future<void> logout() async {
|
|
_sessionToken = null;
|
|
_dossierId = null;
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove(_prefKeySessionToken);
|
|
await prefs.remove(_prefKeyDossierId);
|
|
}
|
|
|
|
/// Make authenticated API request
|
|
Future<http.Response> get(String endpoint) async {
|
|
return http.get(
|
|
Uri.parse('$baseUrl/api/v1$endpoint'),
|
|
headers: _authHeaders(),
|
|
);
|
|
}
|
|
|
|
/// Make authenticated POST request
|
|
Future<http.Response> post(String endpoint, Map<String, dynamic> body) async {
|
|
return http.post(
|
|
Uri.parse('$baseUrl/api/v1$endpoint'),
|
|
headers: {
|
|
..._authHeaders(),
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: jsonEncode(body),
|
|
);
|
|
}
|
|
|
|
Map<String, String> _authHeaders() {
|
|
return {
|
|
if (_sessionToken != null) 'Authorization': 'Bearer $_sessionToken',
|
|
'Accept': 'application/json',
|
|
};
|
|
}
|
|
|
|
Future<void> _saveSession() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
if (_sessionToken != null) {
|
|
await prefs.setString(_prefKeySessionToken, _sessionToken!);
|
|
}
|
|
if (_dossierId != null) {
|
|
await prefs.setString(_prefKeyDossierId, _dossierId!);
|
|
}
|
|
}
|
|
}
|