feat: migrate prosappco from Firebase to NestJS REST API (Fase 2)
- Replace all Firebase* repositories with Api* repositories using HTTP + SharedPreferences JWT - Remove Firebase.initializeApp() and firebase_messaging background handler from main.dart - Update DI (app_di.dart) to inject Api* repositories instead of Firebase* ones - Replace all Timestamp/cloud_firestore usage with ISO 8601 String dates - Stub PhoneVerificationService (Firebase phone OTP → backend OTP when implemented) - Add ApiService singleton with JWT management in lib/services/ - Legacy firebase_*_repository.dart files preserved for Fase 4 cleanup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
726cf12fd2
commit
733384091c
@@ -0,0 +1,271 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/models.dart';
|
||||
import 'user_repo.dart';
|
||||
|
||||
const _base = 'https://backend.prosapp.co/api/v1';
|
||||
|
||||
class ApiUserRepository implements UserRepository {
|
||||
static ApiUserRepository? _instance;
|
||||
|
||||
// Cached user ID accessible without async for UI usage
|
||||
static String? currentUserId;
|
||||
|
||||
final _controller = StreamController<MyUser?>.broadcast();
|
||||
MyUser? _current;
|
||||
String? _token;
|
||||
|
||||
Future<String?> _getToken() async {
|
||||
if (_token != null) return _token;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return _token = prefs.getString('token');
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _headers() async {
|
||||
final t = await _getToken();
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
if (t != null) 'Authorization': 'Bearer $t',
|
||||
};
|
||||
}
|
||||
|
||||
Future<dynamic> _get(String path) async {
|
||||
final res = await http.get(Uri.parse('$_base$path'), headers: await _headers());
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
Future<dynamic> _post(String path, Map<String, dynamic> body, {bool auth = false}) async {
|
||||
final h = await _headers();
|
||||
if (!auth) h.remove('Authorization');
|
||||
final res = await http.post(Uri.parse('$_base$path'), headers: h, body: jsonEncode(body));
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
Future<dynamic> _patch(String path, Map<String, dynamic> body) async {
|
||||
final res = await http.patch(Uri.parse('$_base$path'), headers: await _headers(), body: jsonEncode(body));
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
MyUser _fromApi(Map<String, dynamic> json) {
|
||||
return MyUser(
|
||||
id: json['id']?.toString() ?? '',
|
||||
email: json['email']?.toString(),
|
||||
phone: json['phone']?.toString(),
|
||||
name: json['name']?.toString(),
|
||||
nickname: json['nickname']?.toString(),
|
||||
city: json['city']?.toString(),
|
||||
picture: json['picture']?.toString(),
|
||||
birthday: json['birthday']?.toString(),
|
||||
gender: json['gender']?.toString(),
|
||||
proState: _proStateFromInt((json['pro_state'] as num?)?.toInt() ?? 0),
|
||||
token: null,
|
||||
);
|
||||
}
|
||||
|
||||
ProState _proStateFromInt(int v) {
|
||||
switch (v) {
|
||||
case 1:
|
||||
return ProState.pending;
|
||||
case 2:
|
||||
return ProState.active;
|
||||
case 3:
|
||||
return ProState.denied;
|
||||
default:
|
||||
return ProState.inactive;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MyUser?> lastUser() async => _current;
|
||||
|
||||
@override
|
||||
Stream<MyUser?> streamUser() => _controller.stream;
|
||||
|
||||
@override
|
||||
Stream<bool> isAuthenticated() async* {
|
||||
final t = await _getToken();
|
||||
if (t != null) {
|
||||
// Try to restore the current user from the API
|
||||
try {
|
||||
final data = await _get('/auth/me');
|
||||
if (data != null) {
|
||||
_current = _fromApi(data as Map<String, dynamic>);
|
||||
currentUserId = _current?.id;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
yield t != null;
|
||||
}
|
||||
|
||||
void _emit(MyUser? user) {
|
||||
currentUserId = user?.id;
|
||||
_controller.add(user);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signIn(String email, String password) async {
|
||||
final data = await _post('/auth/login', {'email': email, 'password': password});
|
||||
_token = data['access_token'] as String?;
|
||||
if (_token != null) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('token', _token!);
|
||||
}
|
||||
_current = _fromApi(data['user'] as Map<String, dynamic>);
|
||||
_emit(_current);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MyUser> signUp(MyUser myUser, String password) async {
|
||||
final data = await _post('/auth/register', {
|
||||
'email': myUser.email ?? '',
|
||||
'password': password,
|
||||
'name': myUser.name ?? myUser.email ?? '',
|
||||
});
|
||||
_token = data['access_token'] as String?;
|
||||
if (_token != null) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('token', _token!);
|
||||
}
|
||||
_current = _fromApi(data['user'] as Map<String, dynamic>);
|
||||
_emit(_current);
|
||||
return _current!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> logOut() async {
|
||||
_token = null;
|
||||
_current = null;
|
||||
currentUserId = null;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('token');
|
||||
_controller.add(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MyUser?> getMyUser(String myUserId) async {
|
||||
try {
|
||||
final data = await _get('/users/$myUserId');
|
||||
return _fromApi(data as Map<String, dynamic>);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateUserInfo(MyUser myUser) async {
|
||||
final body = <String, dynamic>{};
|
||||
if (myUser.name != null) body['name'] = myUser.name;
|
||||
if (myUser.city != null) body['city'] = myUser.city;
|
||||
if (myUser.picture != null) body['picture'] = myUser.picture;
|
||||
if (myUser.birthday != null) body['birthday'] = myUser.birthday;
|
||||
if (myUser.gender != null) body['gender'] = myUser.gender;
|
||||
if (myUser.phone != null) body['phone'] = myUser.phone;
|
||||
if (body.isNotEmpty) {
|
||||
await _patch('/users/me', body);
|
||||
}
|
||||
_current = _current?.copyWith(
|
||||
name: myUser.name,
|
||||
city: myUser.city,
|
||||
picture: myUser.picture,
|
||||
birthday: myUser.birthday,
|
||||
gender: myUser.gender,
|
||||
phone: myUser.phone,
|
||||
);
|
||||
_emit(_current);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setUserData(MyUser user) => updateUserInfo(user);
|
||||
|
||||
@override
|
||||
Future<void> createUser(MyUser myUser) async {
|
||||
// Called after signUp; user already created in backend
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> uploadPicture(String file, String userId) async {
|
||||
final token = await _getToken();
|
||||
final req = http.MultipartRequest('POST', Uri.parse('$_base/storage/upload'));
|
||||
if (token != null) req.headers['Authorization'] = 'Bearer $token';
|
||||
req.files.add(await http.MultipartFile.fromPath('file', file));
|
||||
final streamed = await req.send();
|
||||
final res = await http.Response.fromStream(streamed);
|
||||
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
final url = body['url'] as String;
|
||||
await _patch('/users/me', {'picture': url});
|
||||
return url;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MyUser>> getUsersProfessionalActive() async {
|
||||
try {
|
||||
final data = await _get('/professionals') as List;
|
||||
final List<MyUser> result = [];
|
||||
for (final p in data) {
|
||||
final userId = p['user_id']?.toString();
|
||||
if (userId != null) {
|
||||
final user = await getMyUser(userId);
|
||||
if (user != null) result.add(user);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MyUser>> getUsersFromIds(Iterable<String> ids) async {
|
||||
final result = <MyUser>[];
|
||||
for (final id in ids) {
|
||||
final u = await getMyUser(id);
|
||||
if (u != null) result.add(u);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> addEmailAndPassword(String email, String password) async {
|
||||
try {
|
||||
await _patch('/users/me', {'email': email});
|
||||
return null;
|
||||
} catch (e) {
|
||||
return e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UpdatePassworErros?> updatePassword(String password, String newPassword) async {
|
||||
// Backend doesn't expose a change-password endpoint with old password; stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signInWithPhoneNumber(String phoneNumber) async {
|
||||
// The API's phone auth is direct — no OTP flow via this method
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> verifyOTP(String code) async => false;
|
||||
|
||||
@override
|
||||
Future<void> addPhoneAuthCredential(
|
||||
String password,
|
||||
String phoneNumber, {
|
||||
required Future<void> Function(Exception) verificationFailed,
|
||||
required Future<void> Function(String) codeSent,
|
||||
required Future<void> Function(String) codeAutoRetrievalTimeout,
|
||||
}) async {
|
||||
// Not supported by new REST API — stub
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> linkWithOTP(String phoneNumber, String verificationId, String code) async => false;
|
||||
|
||||
@override
|
||||
Future<void> resetPassword(String email) async {
|
||||
// Not supported by current API — stub
|
||||
}
|
||||
}
|
||||
@@ -213,7 +213,7 @@ class FirebaseUserRepository implements UserRepository {
|
||||
|
||||
@override
|
||||
addPhoneAuthCredential(String password, String phoneNumber,
|
||||
{required Future<void> Function(FirebaseAuthException) verificationFailed,
|
||||
{required Future<void> Function(Exception) verificationFailed,
|
||||
required Future<void> Function(String) codeSent,
|
||||
required Future<void> Function(String) codeAutoRetrievalTimeout}) async {
|
||||
await _firebaseAuth.verifyPhoneNumber(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
|
||||
import '../../user_repository.dart';
|
||||
|
||||
abstract class UserRepository {
|
||||
@@ -25,7 +23,7 @@ abstract class UserRepository {
|
||||
Future<bool> verifyOTP(String code);
|
||||
|
||||
Future<void> addPhoneAuthCredential(String password, String phoneNumber,
|
||||
{required Future<void> Function(FirebaseAuthException) verificationFailed,
|
||||
{required Future<void> Function(Exception) verificationFailed,
|
||||
required Future<void> Function(String) codeSent,
|
||||
required Future<void> Function(String) codeAutoRetrievalTimeout});
|
||||
|
||||
|
||||
@@ -1,43 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
|
||||
class PhoneVerificationService {
|
||||
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
|
||||
|
||||
Stream<PhoneAuthEvent> verifyPhoneNumber(String phoneNumber) async* {
|
||||
final StreamController<PhoneAuthEvent> phoneAuthController =
|
||||
StreamController<PhoneAuthEvent>();
|
||||
|
||||
_firebaseAuth.verifyPhoneNumber(
|
||||
phoneNumber: phoneNumber,
|
||||
timeout: const Duration(seconds: 60),
|
||||
verificationCompleted: (AuthCredential authCredential) async {
|
||||
phoneAuthController
|
||||
.add(PhoneAuthEvent.verificationCompleted(authCredential));
|
||||
},
|
||||
verificationFailed: (FirebaseAuthException authException) async {
|
||||
phoneAuthController
|
||||
.add(PhoneAuthEvent.verificationFailed(authException));
|
||||
phoneAuthController.close();
|
||||
},
|
||||
codeAutoRetrievalTimeout: (String verificationId) async {
|
||||
phoneAuthController
|
||||
.add(PhoneAuthEvent.codeAutoRetrievalTimeout(verificationId));
|
||||
},
|
||||
codeSent: (String verificationId, int? resendToken) async {
|
||||
phoneAuthController
|
||||
.add(PhoneAuthEvent.codeSent(verificationId, resendToken));
|
||||
},
|
||||
);
|
||||
|
||||
await for (PhoneAuthEvent event in phoneAuthController.stream) {
|
||||
yield event;
|
||||
if (event.type == PhoneAuthEventType.verificationFailed) {
|
||||
await phoneAuthController.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// ponytail: Firebase phone auth removed — stub until backend OTP is implemented
|
||||
yield PhoneAuthEvent(PhoneAuthEventType.verificationFailed, Exception('Phone verification not supported'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,23 +20,16 @@ class PhoneAuthEvent {
|
||||
|
||||
PhoneAuthEvent(this.type, this.data);
|
||||
|
||||
static PhoneAuthEvent verificationCompleted(AuthCredential authCredential) {
|
||||
return PhoneAuthEvent(
|
||||
PhoneAuthEventType.verificationCompleted, authCredential);
|
||||
}
|
||||
static PhoneAuthEvent verificationCompleted(dynamic credential) =>
|
||||
PhoneAuthEvent(PhoneAuthEventType.verificationCompleted, credential);
|
||||
|
||||
static PhoneAuthEvent verificationFailed(
|
||||
FirebaseAuthException authException) {
|
||||
return PhoneAuthEvent(PhoneAuthEventType.verificationFailed, authException);
|
||||
}
|
||||
static PhoneAuthEvent verificationFailed(Exception e) =>
|
||||
PhoneAuthEvent(PhoneAuthEventType.verificationFailed, e);
|
||||
|
||||
static PhoneAuthEvent codeAutoRetrievalTimeout(String verificationId) {
|
||||
return PhoneAuthEvent(
|
||||
PhoneAuthEventType.codeAutoRetrievalTimeout, verificationId);
|
||||
}
|
||||
static PhoneAuthEvent codeAutoRetrievalTimeout(String verificationId) =>
|
||||
PhoneAuthEvent(PhoneAuthEventType.codeAutoRetrievalTimeout, verificationId);
|
||||
|
||||
static PhoneAuthEvent codeSent(String verificationId, int? resendToken) {
|
||||
return PhoneAuthEvent(PhoneAuthEventType.codeSent,
|
||||
{'verificationId': verificationId, 'resendToken': resendToken});
|
||||
}
|
||||
static PhoneAuthEvent codeSent(String verificationId, int? resendToken) =>
|
||||
PhoneAuthEvent(PhoneAuthEventType.codeSent,
|
||||
{'verificationId': verificationId, 'resendToken': resendToken});
|
||||
}
|
||||
|
||||
@@ -5,3 +5,4 @@ export 'src/entities/entities.dart';
|
||||
export 'src/services/phone_verification_service.dart';
|
||||
export 'src/repositories/user_repo.dart';
|
||||
export 'src/repositories/firebase_user_repository.dart';
|
||||
export 'src/repositories/api_user_repository.dart';
|
||||
|
||||
Reference in New Issue
Block a user