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.
decky/lib/services/api_auth_service.dart

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);
}
}

Powered by TurnKey Linux.