inou-mobile/lib/services/inou_api.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!);
}
}
}