diff --git a/apps/lib/core/api/api_client.dart b/apps/lib/core/api/api_client.dart index da3da79..70ec78c 100644 --- a/apps/lib/core/api/api_client.dart +++ b/apps/lib/core/api/api_client.dart @@ -63,4 +63,28 @@ class ApiClient { throw ApiException.fromDioError(e); } } + + Future> patch( + String path, { + dynamic data, + Options? options, + }) async { + try { + return await _dio.patch(path, data: data, options: options); + } on DioException catch (e) { + throw ApiException.fromDioError(e); + } + } + + Future> delete( + String path, { + dynamic data, + Options? options, + }) async { + try { + return await _dio.delete(path, data: data, options: options); + } on DioException catch (e) { + throw ApiException.fromDioError(e); + } + } } diff --git a/apps/lib/core/di/injection.dart b/apps/lib/core/di/injection.dart index d09ad0c..4005bb2 100644 --- a/apps/lib/core/di/injection.dart +++ b/apps/lib/core/di/injection.dart @@ -38,7 +38,7 @@ Future configureDependencies() async { apiClient.setRefreshCallback((token) async { try { - await authRepository.refresh(token); + await authRepository.refreshSession(token); return true; } catch (_) { return false; diff --git a/apps/lib/features/auth/data/auth_api.dart b/apps/lib/features/auth/data/auth_api.dart index 7d15d03..2798c7a 100644 --- a/apps/lib/features/auth/data/auth_api.dart +++ b/apps/lib/features/auth/data/auth_api.dart @@ -9,47 +9,45 @@ class AuthApi { AuthApi(this._client); - Future signupStart(SignupStartRequest request) async { + Future createVerification( + SignupStartRequest request, + ) async { final response = await _client.post( - '$_prefix/signup/start', + '$_prefix/verifications', data: request.toJson(), ); - return SignupStartResponse.fromJson(response.data); + return VerificationCreateResponse.fromJson(response.data); } - Future signupVerify(SignupVerifyRequest request) async { + Future verifyVerification(SignupVerifyRequest request) async { final response = await _client.post( - '$_prefix/signup/verify', + '$_prefix/verifications/verify', data: request.toJson(), ); return AuthResponse.fromJson(response.data); } - Future signupResend(SignupResendRequest request) async { - final response = await _client.post( - '$_prefix/signup/resend', - data: request.toJson(), - ); - return SignupResendResponse.fromJson(response.data); + Future resendVerification(SignupResendRequest request) async { + await _client.post('$_prefix/verifications/resend', data: request.toJson()); } - Future login(LoginRequest request) async { + Future createSession(LoginRequest request) async { final response = await _client.post( - '$_prefix/login', + '$_prefix/sessions', data: request.toJson(), ); return AuthResponse.fromJson(response.data); } - Future refresh(RefreshRequest request) async { + Future refreshSession(RefreshRequest request) async { final response = await _client.post( - '$_prefix/refresh', + '$_prefix/sessions/refresh', data: request.toJson(), ); return AuthResponse.fromJson(response.data); } - Future logout(LogoutRequest request) async { - await _client.post('$_prefix/logout', data: request.toJson()); + Future deleteSession(LogoutRequest request) async { + await _client.delete('$_prefix/sessions', data: request.toJson()); } } diff --git a/apps/lib/features/auth/data/auth_repository.dart b/apps/lib/features/auth/data/auth_repository.dart index 2bf372e..e7850d5 100644 --- a/apps/lib/features/auth/data/auth_repository.dart +++ b/apps/lib/features/auth/data/auth_repository.dart @@ -3,12 +3,14 @@ import 'package:social_app/features/auth/data/models/login_request.dart'; import 'package:social_app/features/auth/data/models/auth_response.dart'; abstract class AuthRepository { - Future signupStart(SignupStartRequest request); - Future signupVerify(SignupVerifyRequest request); - Future signupResend(SignupResendRequest request); - Future login(LoginRequest request); - Future refresh(String refreshToken); - Future logout(); + Future createVerification( + SignupStartRequest request, + ); + Future verifyVerification(SignupVerifyRequest request); + Future resendVerification(SignupResendRequest request); + Future createSession(LoginRequest request); + Future refreshSession(String refreshToken); + Future deleteSession(); Future getAccessToken(); Future getRefreshToken(); Future isAuthenticated(); diff --git a/apps/lib/features/auth/data/auth_repository_impl.dart b/apps/lib/features/auth/data/auth_repository_impl.dart index 23bf352..6dc079c 100644 --- a/apps/lib/features/auth/data/auth_repository_impl.dart +++ b/apps/lib/features/auth/data/auth_repository_impl.dart @@ -14,13 +14,15 @@ class AuthRepositoryImpl implements AuthRepository { _tokenStorage = tokenStorage; @override - Future signupStart(SignupStartRequest request) { - return _api.signupStart(request); + Future createVerification( + SignupStartRequest request, + ) { + return _api.createVerification(request); } @override - Future signupVerify(SignupVerifyRequest request) async { - final response = await _api.signupVerify(request); + Future verifyVerification(SignupVerifyRequest request) async { + final response = await _api.verifyVerification(request); await _tokenStorage.saveTokens( access: response.accessToken, refresh: response.refreshToken, @@ -29,13 +31,13 @@ class AuthRepositoryImpl implements AuthRepository { } @override - Future signupResend(SignupResendRequest request) { - return _api.signupResend(request); + Future resendVerification(SignupResendRequest request) { + return _api.resendVerification(request); } @override - Future login(LoginRequest request) async { - final response = await _api.login(request); + Future createSession(LoginRequest request) async { + final response = await _api.createSession(request); await _tokenStorage.saveTokens( access: response.accessToken, refresh: response.refreshToken, @@ -44,8 +46,8 @@ class AuthRepositoryImpl implements AuthRepository { } @override - Future refresh(String refreshToken) async { - final response = await _api.refresh( + Future refreshSession(String refreshToken) async { + final response = await _api.refreshSession( RefreshRequest(refreshToken: refreshToken), ); await _tokenStorage.saveTokens( @@ -56,10 +58,10 @@ class AuthRepositoryImpl implements AuthRepository { } @override - Future logout() async { + Future deleteSession() async { final refreshToken = await _tokenStorage.getRefreshToken(); if (refreshToken != null) { - await _api.logout(LogoutRequest(refreshToken: refreshToken)); + await _api.deleteSession(LogoutRequest(refreshToken: refreshToken)); } await _tokenStorage.clear(); } diff --git a/apps/lib/features/auth/data/models/auth_response.dart b/apps/lib/features/auth/data/models/auth_response.dart index 71e9a76..db1bc70 100644 --- a/apps/lib/features/auth/data/models/auth_response.dart +++ b/apps/lib/features/auth/data/models/auth_response.dart @@ -35,32 +35,12 @@ class AuthResponse { } } -class SignupStartResponse { - final String status; +class VerificationCreateResponse { final String email; - final String message; - const SignupStartResponse({ - required this.status, - required this.email, - required this.message, - }); + const VerificationCreateResponse({required this.email}); - factory SignupStartResponse.fromJson(Map json) { - return SignupStartResponse( - status: json['status'] as String, - email: json['email'] as String, - message: json['message'] as String, - ); - } -} - -class SignupResendResponse { - final String message; - - const SignupResendResponse({required this.message}); - - factory SignupResendResponse.fromJson(Map json) { - return SignupResendResponse(message: json['message'] as String); + factory VerificationCreateResponse.fromJson(Map json) { + return VerificationCreateResponse(email: json['email'] as String); } } diff --git a/apps/lib/features/auth/presentation/bloc/auth_bloc.dart b/apps/lib/features/auth/presentation/bloc/auth_bloc.dart index 3b3d790..ec89dd9 100644 --- a/apps/lib/features/auth/presentation/bloc/auth_bloc.dart +++ b/apps/lib/features/auth/presentation/bloc/auth_bloc.dart @@ -17,7 +17,7 @@ class AuthBloc extends Bloc { final refreshToken = await _repository.getRefreshToken(); if (refreshToken != null) { try { - final response = await _repository.refresh(refreshToken); + final response = await _repository.refreshSession(refreshToken); emit( AuthAuthenticated( user: AuthUser(id: response.user.id, email: response.user.email), @@ -25,7 +25,7 @@ class AuthBloc extends Bloc { ); return; } catch (_) { - await _repository.logout(); + await _repository.deleteSession(); } } emit(AuthUnauthenticated()); @@ -39,7 +39,7 @@ class AuthBloc extends Bloc { AuthLoggedOut event, Emitter emit, ) async { - await _repository.logout(); + await _repository.deleteSession(); emit(AuthUnauthenticated()); } } diff --git a/apps/lib/features/auth/presentation/cubits/login_cubit.dart b/apps/lib/features/auth/presentation/cubits/login_cubit.dart index e9cb9ca..b493af8 100644 --- a/apps/lib/features/auth/presentation/cubits/login_cubit.dart +++ b/apps/lib/features/auth/presentation/cubits/login_cubit.dart @@ -59,7 +59,7 @@ class LoginCubit extends Cubit { emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); try { - final response = await _repository.login( + final response = await _repository.createSession( LoginRequest(email: state.email.value, password: state.password.value), ); emit(state.copyWith(status: FormzSubmissionStatus.success)); diff --git a/apps/lib/features/auth/presentation/cubits/register_cubit.dart b/apps/lib/features/auth/presentation/cubits/register_cubit.dart index 3b71f3b..7de8f69 100644 --- a/apps/lib/features/auth/presentation/cubits/register_cubit.dart +++ b/apps/lib/features/auth/presentation/cubits/register_cubit.dart @@ -99,7 +99,7 @@ class RegisterCubit extends Cubit { emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); try { - final response = await _repository.signupStart( + final response = await _repository.createVerification( SignupStartRequest( username: state.username.value, email: state.email.value, @@ -132,7 +132,7 @@ class RegisterCubit extends Cubit { emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); try { - final response = await _repository.signupVerify( + final response = await _repository.verifyVerification( SignupVerifyRequest( email: state.pendingEmail!, token: state.verificationCode.value, @@ -166,7 +166,7 @@ class RegisterCubit extends Cubit { emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); try { - await _repository.signupResend( + await _repository.resendVerification( SignupResendRequest(email: state.pendingEmail!), ); emit( @@ -197,7 +197,7 @@ class RegisterCubit extends Cubit { ); try { - final response = await _repository.signupStart( + final response = await _repository.createVerification( SignupStartRequest( username: state.username.value, email: state.email.value, diff --git a/apps/lib/features/users/data/models/user_response.dart b/apps/lib/features/users/data/models/user_response.dart new file mode 100644 index 0000000..fb23cc0 --- /dev/null +++ b/apps/lib/features/users/data/models/user_response.dart @@ -0,0 +1,38 @@ +class UserResponse { + final String id; + final String username; + final String? avatarUrl; + final String? bio; + + const UserResponse({ + required this.id, + required this.username, + this.avatarUrl, + this.bio, + }); + + factory UserResponse.fromJson(Map json) { + return UserResponse( + id: json['id'] as String, + username: json['username'] as String, + avatarUrl: json['avatar_url'] as String?, + bio: json['bio'] as String?, + ); + } +} + +class UserUpdateRequest { + final String? username; + final String? avatarUrl; + final String? bio; + + const UserUpdateRequest({this.username, this.avatarUrl, this.bio}); + + Map toJson() { + return { + if (username != null) 'username': username, + if (avatarUrl != null) 'avatar_url': avatarUrl, + if (bio != null) 'bio': bio, + }; + } +} diff --git a/apps/lib/features/users/data/users_api.dart b/apps/lib/features/users/data/users_api.dart new file mode 100644 index 0000000..fc69065 --- /dev/null +++ b/apps/lib/features/users/data/users_api.dart @@ -0,0 +1,24 @@ +import 'package:social_app/core/api/api_client.dart'; +import 'models/user_response.dart'; + +class UsersApi { + final ApiClient _client; + static const _prefix = '/api/v1/users'; + + UsersApi(this._client); + + Future getMe() async { + final response = await _client.get('$_prefix/me'); + return UserResponse.fromJson(response.data); + } + + Future updateMe(UserUpdateRequest request) async { + final response = await _client.patch('$_prefix/me', data: request.toJson()); + return UserResponse.fromJson(response.data); + } + + Future getByUsername(String username) async { + final response = await _client.get('$_prefix/$username'); + return UserResponse.fromJson(response.data); + } +} diff --git a/apps/lib/features/users/data/users_repository.dart b/apps/lib/features/users/data/users_repository.dart new file mode 100644 index 0000000..f62313e --- /dev/null +++ b/apps/lib/features/users/data/users_repository.dart @@ -0,0 +1,7 @@ +import 'models/user_response.dart'; + +abstract class UsersRepository { + Future getMe(); + Future updateMe(UserUpdateRequest request); + Future getByUsername(String username); +} diff --git a/apps/lib/features/users/data/users_repository_impl.dart b/apps/lib/features/users/data/users_repository_impl.dart new file mode 100644 index 0000000..4376560 --- /dev/null +++ b/apps/lib/features/users/data/users_repository_impl.dart @@ -0,0 +1,24 @@ +import 'users_api.dart'; +import 'users_repository.dart'; +import 'models/user_response.dart'; + +class UsersRepositoryImpl implements UsersRepository { + final UsersApi _api; + + UsersRepositoryImpl({required UsersApi api}) : _api = api; + + @override + Future getMe() { + return _api.getMe(); + } + + @override + Future updateMe(UserUpdateRequest request) { + return _api.updateMe(request); + } + + @override + Future getByUsername(String username) { + return _api.getByUsername(username); + } +} diff --git a/apps/test/features/auth/data/auth_repository_test.dart b/apps/test/features/auth/data/auth_repository_test.dart index 011b69c..d3f9178 100644 --- a/apps/test/features/auth/data/auth_repository_test.dart +++ b/apps/test/features/auth/data/auth_repository_test.dart @@ -34,16 +34,13 @@ void main() { }); group('AuthRepositoryImpl', () { - test('signupStart calls api and returns response', () async { - when(() => mockApi.signupStart(any())).thenAnswer( - (_) async => const SignupStartResponse( - status: 'pending_verification', - email: 'test@example.com', - message: 'Verification code sent', - ), + test('createVerification calls api and returns response', () async { + when(() => mockApi.createVerification(any())).thenAnswer( + (_) async => + const VerificationCreateResponse(email: 'test@example.com'), ); - final result = await repository.signupStart( + final result = await repository.createVerification( const SignupStartRequest( username: 'testuser', email: 'test@example.com', @@ -51,12 +48,12 @@ void main() { ), ); - expect(result.status, 'pending_verification'); - verify(() => mockApi.signupStart(any())).called(1); + expect(result.email, 'test@example.com'); + verify(() => mockApi.createVerification(any())).called(1); }); - test('login calls api and saves tokens', () async { - when(() => mockApi.login(any())).thenAnswer( + test('createSession calls api and saves tokens', () async { + when(() => mockApi.createSession(any())).thenAnswer( (_) async => AuthResponse( accessToken: 'access_token', refreshToken: 'refresh_token', @@ -72,7 +69,7 @@ void main() { ), ).thenAnswer((_) async {}); - final result = await repository.login( + final result = await repository.createSession( const LoginRequest(email: 'test@example.com', password: 'password123'), ); @@ -85,21 +82,24 @@ void main() { ).called(1); }); - test('logout calls api with refresh token and clears storage', () async { - when( - () => mockStorage.getRefreshToken(), - ).thenAnswer((_) async => 'refresh_token'); - when(() => mockApi.logout(any())).thenAnswer((_) async {}); - when(() => mockStorage.clear()).thenAnswer((_) async {}); + test( + 'deleteSession calls api with refresh token and clears storage', + () async { + when( + () => mockStorage.getRefreshToken(), + ).thenAnswer((_) async => 'refresh_token'); + when(() => mockApi.deleteSession(any())).thenAnswer((_) async {}); + when(() => mockStorage.clear()).thenAnswer((_) async {}); - await repository.logout(); + await repository.deleteSession(); - verify(() => mockApi.logout(any())).called(1); - verify(() => mockStorage.clear()).called(1); - }); + verify(() => mockApi.deleteSession(any())).called(1); + verify(() => mockStorage.clear()).called(1); + }, + ); - test('refresh saves new tokens', () async { - when(() => mockApi.refresh(any())).thenAnswer( + test('refreshSession saves new tokens', () async { + when(() => mockApi.refreshSession(any())).thenAnswer( (_) async => AuthResponse( accessToken: 'new_access', refreshToken: 'new_refresh', @@ -115,7 +115,7 @@ void main() { ), ).thenAnswer((_) async {}); - final result = await repository.refresh('old_refresh'); + final result = await repository.refreshSession('old_refresh'); expect(result.accessToken, 'new_access'); verify( diff --git a/apps/test/features/auth/presentation/bloc/auth_bloc_test.dart b/apps/test/features/auth/presentation/bloc/auth_bloc_test.dart index 38244f7..2ae18e0 100644 --- a/apps/test/features/auth/presentation/bloc/auth_bloc_test.dart +++ b/apps/test/features/auth/presentation/bloc/auth_bloc_test.dart @@ -41,7 +41,7 @@ void main() { when( () => mockRepository.getRefreshToken(), ).thenAnswer((_) async => 'valid_refresh'); - when(() => mockRepository.refresh('valid_refresh')).thenAnswer( + when(() => mockRepository.refreshSession('valid_refresh')).thenAnswer( (_) async => AuthResponse( accessToken: 'new_access', refreshToken: 'new_refresh', @@ -63,9 +63,9 @@ void main() { () => mockRepository.getRefreshToken(), ).thenAnswer((_) async => 'expired_refresh'); when( - () => mockRepository.refresh('expired_refresh'), + () => mockRepository.refreshSession('expired_refresh'), ).thenThrow(Exception('Invalid refresh token')); - when(() => mockRepository.logout()).thenAnswer((_) async {}); + when(() => mockRepository.deleteSession()).thenAnswer((_) async {}); return authBloc; }, act: (bloc) => bloc.add(AuthStarted()), @@ -86,7 +86,7 @@ void main() { blocTest( 'emits [AuthUnauthenticated] when AuthLoggedOut', build: () { - when(() => mockRepository.logout()).thenAnswer((_) async {}); + when(() => mockRepository.deleteSession()).thenAnswer((_) async {}); return authBloc; }, seed: () => AuthAuthenticated( diff --git a/apps/test/features/auth/presentation/cubits/register_cubit_test.dart b/apps/test/features/auth/presentation/cubits/register_cubit_test.dart index c7a73db..00f99b6 100644 --- a/apps/test/features/auth/presentation/cubits/register_cubit_test.dart +++ b/apps/test/features/auth/presentation/cubits/register_cubit_test.dart @@ -65,12 +65,8 @@ void main() { password: const Password.dirty('password123'), ), setUp: () { - when(() => mockRepository.signupStart(any())).thenAnswer( - (_) async => SignupStartResponse( - status: 'ok', - email: 'test@example.com', - message: 'Code sent', - ), + when(() => mockRepository.createVerification(any())).thenAnswer( + (_) async => VerificationCreateResponse(email: 'test@example.com'), ); }, act: (c) => c.sendCodeSilently(), @@ -85,7 +81,7 @@ void main() { ), ], verify: (_) { - verify(() => mockRepository.signupStart(any())).called(1); + verify(() => mockRepository.createVerification(any())).called(1); }, ); @@ -99,7 +95,7 @@ void main() { ), setUp: () { when( - () => mockRepository.signupStart(any()), + () => mockRepository.createVerification(any()), ).thenThrow(ServerException('Network error')); }, act: (c) => c.sendCodeSilently(), @@ -113,12 +109,12 @@ void main() { ), ], verify: (_) { - verify(() => mockRepository.signupStart(any())).called(1); + verify(() => mockRepository.createVerification(any())).called(1); }, ); blocTest( - 'does not call signupStart when input is invalid', + 'does not call createVerification when input is invalid', build: () => cubit, seed: () => RegisterState( username: const Username.dirty(''), @@ -128,7 +124,7 @@ void main() { act: (c) => c.sendCodeSilently(), expect: () => [], verify: (_) { - verifyNever(() => mockRepository.signupStart(any())); + verifyNever(() => mockRepository.createVerification(any())); }, ); @@ -144,7 +140,7 @@ void main() { act: (c) => c.sendCodeSilently(), expect: () => [], verify: (_) { - verifyNever(() => mockRepository.signupStart(any())); + verifyNever(() => mockRepository.createVerification(any())); }, ); }); @@ -161,7 +157,7 @@ void main() { .having((s) => s.errorMessage, 'errorMessage', '验证码发送失败,请返回上一步重试'), ], verify: (_) { - verifyNever(() => mockRepository.signupResend(any())); + verifyNever(() => mockRepository.resendVerification(any())); }, ); @@ -171,8 +167,8 @@ void main() { seed: () => RegisterState(pendingEmail: 'test@example.com'), setUp: () { when( - () => mockRepository.signupResend(any()), - ).thenAnswer((_) async => SignupResendResponse(message: 'Code sent')); + () => mockRepository.resendVerification(any()), + ).thenAnswer((_) async {}); }, act: (c) => c.resendCode(), expect: () => [ @@ -188,7 +184,7 @@ void main() { ), ], verify: (_) { - verify(() => mockRepository.signupResend(any())).called(1); + verify(() => mockRepository.resendVerification(any())).called(1); }, ); @@ -198,7 +194,7 @@ void main() { seed: () => RegisterState(pendingEmail: 'test@example.com'), setUp: () { when( - () => mockRepository.signupResend(any()), + () => mockRepository.resendVerification(any()), ).thenThrow(ServerException('Network error')); }, act: (c) => c.resendCode(),