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; final String? avatarUrl; const ApiUser({ required this.id, this.email, this.displayName, this.avatarUrl, }); ApiUser copyWith({ String? id, String? email, String? displayName, String? avatarUrl, }) { return ApiUser( id: id ?? this.id, email: email ?? this.email, displayName: displayName ?? this.displayName, avatarUrl: avatarUrl ?? this.avatarUrl, ); } factory ApiUser.fromJson(Map json) { final metadata = json['user_metadata'] as Map?; final avatar = json['avatar_url'] as String?; final avatarTrimmed = avatar?.trim(); return ApiUser( id: json['id'] as String? ?? '', email: json['email'] as String?, displayName: json['display_name'] as String? ?? metadata?['display_name'] as String?, avatarUrl: (avatarTrimmed != null && avatarTrimmed.isNotEmpty) ? avatarTrimmed : null, ); } Map toJson() { return { 'id': id, if (email != null) 'email': email, if (displayName != null) 'display_name': displayName, if (avatarUrl != null) 'avatar_url': avatarUrl, }; } } /// 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 currentUser = ValueNotifier(null); String? _token; Future get _prefs async => await SharedPreferences.getInstance(); /// Call after app start to restore session from stored token. Future 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; currentUser.value = ApiUser.fromJson(map); } catch (_) { await logout(); } } if (_token != null && currentUser.value == null) { final ok = await _fetchSession(); if (!ok) await logout(); } if (_token != null && currentUser.value != null) { await _refreshProfile(); } } Future _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; final userJson = data['user'] as Map?; 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; } } /// GET /api/auth/profile — fetches profile (display_name, email, avatar_url) and updates currentUser. Future _refreshProfile() async { if (_token == null) return; try { final uri = Uri.parse('$apiBaseUrl/api/auth/profile'); final res = await http.get( uri, headers: {'Authorization': 'Bearer $_token'}, ); if (res.statusCode != 200) return; final data = jsonDecode(res.body) as Map; final id = data['id'] as String? ?? currentUser.value?.id ?? ''; final displayName = data['display_name'] as String?; final email = data['email'] as String?; final avatarUrl = data['avatar_url'] as String?; final trimmedAvatar = avatarUrl?.trim(); final newUser = (currentUser.value ?? ApiUser(id: id)).copyWith( displayName: displayName ?? currentUser.value?.displayName, email: email ?? currentUser.value?.email, avatarUrl: (trimmedAvatar != null && trimmedAvatar.isNotEmpty) ? trimmedAvatar : currentUser.value?.avatarUrl, ); currentUser.value = newUser; final prefs = await _prefs; await prefs.setString(_userKey, jsonEncode(newUser.toJson())); } catch (_) { // Keep existing user if profile fetch fails } } /// 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 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?; return data?['error'] as String? ?? data?['message'] as String? ?? 'Invalid email or password.'; } if (res.statusCode != 200) { final data = jsonDecode(res.body) as Map?; return data?['error'] as String? ?? data?['message'] as String? ?? 'Login failed. Please try again.'; } final data = jsonDecode(res.body) as Map; _token = data['access_token'] as String?; final userJson = data['user'] as Map?; 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)); await _refreshProfile(); 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 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?; return data?['message'] as String? ?? 'Registration failed.'; } if (res.statusCode != 200) { return 'Registration failed. Please try again.'; } final data = jsonDecode(res.body) as Map; _token = data['access_token'] as String?; final userJson = data['user'] as Map?; 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)); await _refreshProfile(); return null; } catch (e) { return connectionErrorMessage(e); } } /// Clears session (token + user) and persists. Call this to log out. Future logout() async { final prefs = await _prefs; await prefs.remove(_tokenKey); await prefs.remove(_userKey); _token = null; currentUser.value = null; } }