You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
187 lines
6.0 KiB
187 lines
6.0 KiB
import 'dart:convert';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../env.dart';
|
|
import '../utils/connection_error.dart';
|
|
|
|
/// Logged-in user from the API.
|
|
class ApiUser {
|
|
final String id;
|
|
final String? email;
|
|
final String? displayName;
|
|
|
|
const ApiUser({required this.id, this.email, this.displayName});
|
|
|
|
factory ApiUser.fromJson(Map<String, dynamic> json) {
|
|
final metadata = json['user_metadata'] as Map<String, dynamic>?;
|
|
return ApiUser(
|
|
id: json['id'] as String? ?? '',
|
|
email: json['email'] as String?,
|
|
displayName: json['display_name'] as String? ??
|
|
metadata?['display_name'] as String?,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Auth service that uses the backend REST API (login, token storage).
|
|
class ApiAuthService {
|
|
ApiAuthService._();
|
|
static final ApiAuthService _instance = ApiAuthService._();
|
|
static ApiAuthService get instance => _instance;
|
|
|
|
static const String _tokenKey = 'api_auth_token';
|
|
static const String _userKey = 'api_auth_user';
|
|
|
|
final ValueNotifier<ApiUser?> currentUser = ValueNotifier<ApiUser?>(null);
|
|
String? _token;
|
|
|
|
Future<SharedPreferences> get _prefs async =>
|
|
await SharedPreferences.getInstance();
|
|
|
|
/// Call after app start to restore session from stored token.
|
|
Future<void> init() async {
|
|
final prefs = await _prefs;
|
|
_token = prefs.getString(_tokenKey);
|
|
final userJson = prefs.getString(_userKey);
|
|
if (_token != null && userJson != null) {
|
|
try {
|
|
final map = jsonDecode(userJson) as Map<String, dynamic>;
|
|
currentUser.value = ApiUser.fromJson(map);
|
|
} catch (_) {
|
|
await logout();
|
|
}
|
|
}
|
|
if (_token != null && currentUser.value == null) {
|
|
final ok = await _fetchSession();
|
|
if (!ok) await logout();
|
|
}
|
|
}
|
|
|
|
Future<bool> _fetchSession() async {
|
|
if (_token == null) return false;
|
|
try {
|
|
final uri = Uri.parse('$apiBaseUrl/api/auth/session');
|
|
final res = await http.get(
|
|
uri,
|
|
headers: {'Authorization': 'Bearer $_token'},
|
|
);
|
|
if (res.statusCode != 200) return false;
|
|
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
|
final userJson = data['user'] as Map<String, dynamic>?;
|
|
if (userJson == null) return false;
|
|
currentUser.value = ApiUser.fromJson(userJson);
|
|
final prefs = await _prefs;
|
|
await prefs.setString(_userKey, jsonEncode(userJson));
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Returns the current Bearer token for API requests, or null if not logged in.
|
|
String? get token => _token;
|
|
|
|
/// Login with email or username and password.
|
|
/// Returns null on success, or an error message string.
|
|
Future<String?> login(String emailOrUsername, String password) async {
|
|
final trimmed = emailOrUsername.trim();
|
|
if (trimmed.isEmpty) return 'Enter your email or username.';
|
|
if (password.isEmpty) return 'Enter your password.';
|
|
|
|
try {
|
|
final uri = Uri.parse('$apiBaseUrl/api/auth/login');
|
|
final res = await http.post(
|
|
uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({
|
|
'email_or_username': trimmed,
|
|
'password': password,
|
|
}),
|
|
);
|
|
|
|
if (res.statusCode == 401) {
|
|
final data = jsonDecode(res.body) as Map<String, dynamic>?;
|
|
return data?['error'] as String? ??
|
|
data?['message'] as String? ??
|
|
'Invalid email or password.';
|
|
}
|
|
if (res.statusCode != 200) {
|
|
final data = jsonDecode(res.body) as Map<String, dynamic>?;
|
|
return data?['error'] as String? ??
|
|
data?['message'] as String? ??
|
|
'Login failed. Please try again.';
|
|
}
|
|
|
|
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
|
_token = data['access_token'] as String?;
|
|
final userJson = data['user'] as Map<String, dynamic>?;
|
|
if (_token == null || userJson == null) {
|
|
return 'Invalid response from server.';
|
|
}
|
|
|
|
currentUser.value = ApiUser.fromJson(userJson);
|
|
final prefs = await _prefs;
|
|
await prefs.setString(_tokenKey, _token!);
|
|
await prefs.setString(_userKey, jsonEncode(userJson));
|
|
return null;
|
|
} catch (e) {
|
|
return connectionErrorMessage(e);
|
|
}
|
|
}
|
|
|
|
/// Register with email, password, and optional display name.
|
|
/// Returns null on success, or an error message string.
|
|
Future<String?> register(String email, String password, String displayName) async {
|
|
final trimmedEmail = email.trim();
|
|
if (trimmedEmail.isEmpty) return 'Enter your email.';
|
|
if (password.isEmpty) return 'Enter your password.';
|
|
|
|
try {
|
|
final uri = Uri.parse('$apiBaseUrl/api/auth/register');
|
|
final res = await http.post(
|
|
uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({
|
|
'email': trimmedEmail,
|
|
'password': password,
|
|
'displayName': displayName.trim().isEmpty ? null : displayName.trim(),
|
|
}),
|
|
);
|
|
|
|
if (res.statusCode == 400 || res.statusCode == 422) {
|
|
final data = jsonDecode(res.body) as Map<String, dynamic>?;
|
|
return data?['message'] as String? ?? 'Registration failed.';
|
|
}
|
|
if (res.statusCode != 200) {
|
|
return 'Registration failed. Please try again.';
|
|
}
|
|
|
|
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
|
_token = data['access_token'] as String?;
|
|
final userJson = data['user'] as Map<String, dynamic>?;
|
|
if (_token == null || userJson == null) {
|
|
return 'Invalid response from server.';
|
|
}
|
|
|
|
currentUser.value = ApiUser.fromJson(userJson);
|
|
final prefs = await _prefs;
|
|
await prefs.setString(_tokenKey, _token!);
|
|
await prefs.setString(_userKey, jsonEncode(userJson));
|
|
return null;
|
|
} catch (e) {
|
|
return connectionErrorMessage(e);
|
|
}
|
|
}
|
|
|
|
Future<void> logout() async {
|
|
_token = null;
|
|
currentUser.value = null;
|
|
final prefs = await _prefs;
|
|
await prefs.remove(_tokenKey);
|
|
await prefs.remove(_userKey);
|
|
}
|
|
}
|