refactor(frontend): adapt to RESTful API routes

This commit is contained in:
qzl
2026-02-26 14:28:58 +08:00
parent 5b8b584013
commit d635d9a5e0
16 changed files with 210 additions and 115 deletions
+24
View File
@@ -63,4 +63,28 @@ class ApiClient {
throw ApiException.fromDioError(e); throw ApiException.fromDioError(e);
} }
} }
Future<Response<T>> patch<T>(
String path, {
dynamic data,
Options? options,
}) async {
try {
return await _dio.patch<T>(path, data: data, options: options);
} on DioException catch (e) {
throw ApiException.fromDioError(e);
}
}
Future<Response<T>> delete<T>(
String path, {
dynamic data,
Options? options,
}) async {
try {
return await _dio.delete<T>(path, data: data, options: options);
} on DioException catch (e) {
throw ApiException.fromDioError(e);
}
}
} }
+1 -1
View File
@@ -38,7 +38,7 @@ Future<void> configureDependencies() async {
apiClient.setRefreshCallback((token) async { apiClient.setRefreshCallback((token) async {
try { try {
await authRepository.refresh(token); await authRepository.refreshSession(token);
return true; return true;
} catch (_) { } catch (_) {
return false; return false;
+15 -17
View File
@@ -9,47 +9,45 @@ class AuthApi {
AuthApi(this._client); AuthApi(this._client);
Future<SignupStartResponse> signupStart(SignupStartRequest request) async { Future<VerificationCreateResponse> createVerification(
SignupStartRequest request,
) async {
final response = await _client.post( final response = await _client.post(
'$_prefix/signup/start', '$_prefix/verifications',
data: request.toJson(), data: request.toJson(),
); );
return SignupStartResponse.fromJson(response.data); return VerificationCreateResponse.fromJson(response.data);
} }
Future<AuthResponse> signupVerify(SignupVerifyRequest request) async { Future<AuthResponse> verifyVerification(SignupVerifyRequest request) async {
final response = await _client.post( final response = await _client.post(
'$_prefix/signup/verify', '$_prefix/verifications/verify',
data: request.toJson(), data: request.toJson(),
); );
return AuthResponse.fromJson(response.data); return AuthResponse.fromJson(response.data);
} }
Future<SignupResendResponse> signupResend(SignupResendRequest request) async { Future<void> resendVerification(SignupResendRequest request) async {
final response = await _client.post( await _client.post('$_prefix/verifications/resend', data: request.toJson());
'$_prefix/signup/resend',
data: request.toJson(),
);
return SignupResendResponse.fromJson(response.data);
} }
Future<AuthResponse> login(LoginRequest request) async { Future<AuthResponse> createSession(LoginRequest request) async {
final response = await _client.post( final response = await _client.post(
'$_prefix/login', '$_prefix/sessions',
data: request.toJson(), data: request.toJson(),
); );
return AuthResponse.fromJson(response.data); return AuthResponse.fromJson(response.data);
} }
Future<AuthResponse> refresh(RefreshRequest request) async { Future<AuthResponse> refreshSession(RefreshRequest request) async {
final response = await _client.post( final response = await _client.post(
'$_prefix/refresh', '$_prefix/sessions/refresh',
data: request.toJson(), data: request.toJson(),
); );
return AuthResponse.fromJson(response.data); return AuthResponse.fromJson(response.data);
} }
Future<void> logout(LogoutRequest request) async { Future<void> deleteSession(LogoutRequest request) async {
await _client.post('$_prefix/logout', data: request.toJson()); await _client.delete('$_prefix/sessions', data: request.toJson());
} }
} }
@@ -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'; import 'package:social_app/features/auth/data/models/auth_response.dart';
abstract class AuthRepository { abstract class AuthRepository {
Future<SignupStartResponse> signupStart(SignupStartRequest request); Future<VerificationCreateResponse> createVerification(
Future<AuthResponse> signupVerify(SignupVerifyRequest request); SignupStartRequest request,
Future<SignupResendResponse> signupResend(SignupResendRequest request); );
Future<AuthResponse> login(LoginRequest request); Future<AuthResponse> verifyVerification(SignupVerifyRequest request);
Future<AuthResponse> refresh(String refreshToken); Future<void> resendVerification(SignupResendRequest request);
Future<void> logout(); Future<AuthResponse> createSession(LoginRequest request);
Future<AuthResponse> refreshSession(String refreshToken);
Future<void> deleteSession();
Future<String?> getAccessToken(); Future<String?> getAccessToken();
Future<String?> getRefreshToken(); Future<String?> getRefreshToken();
Future<bool> isAuthenticated(); Future<bool> isAuthenticated();
@@ -14,13 +14,15 @@ class AuthRepositoryImpl implements AuthRepository {
_tokenStorage = tokenStorage; _tokenStorage = tokenStorage;
@override @override
Future<SignupStartResponse> signupStart(SignupStartRequest request) { Future<VerificationCreateResponse> createVerification(
return _api.signupStart(request); SignupStartRequest request,
) {
return _api.createVerification(request);
} }
@override @override
Future<AuthResponse> signupVerify(SignupVerifyRequest request) async { Future<AuthResponse> verifyVerification(SignupVerifyRequest request) async {
final response = await _api.signupVerify(request); final response = await _api.verifyVerification(request);
await _tokenStorage.saveTokens( await _tokenStorage.saveTokens(
access: response.accessToken, access: response.accessToken,
refresh: response.refreshToken, refresh: response.refreshToken,
@@ -29,13 +31,13 @@ class AuthRepositoryImpl implements AuthRepository {
} }
@override @override
Future<SignupResendResponse> signupResend(SignupResendRequest request) { Future<void> resendVerification(SignupResendRequest request) {
return _api.signupResend(request); return _api.resendVerification(request);
} }
@override @override
Future<AuthResponse> login(LoginRequest request) async { Future<AuthResponse> createSession(LoginRequest request) async {
final response = await _api.login(request); final response = await _api.createSession(request);
await _tokenStorage.saveTokens( await _tokenStorage.saveTokens(
access: response.accessToken, access: response.accessToken,
refresh: response.refreshToken, refresh: response.refreshToken,
@@ -44,8 +46,8 @@ class AuthRepositoryImpl implements AuthRepository {
} }
@override @override
Future<AuthResponse> refresh(String refreshToken) async { Future<AuthResponse> refreshSession(String refreshToken) async {
final response = await _api.refresh( final response = await _api.refreshSession(
RefreshRequest(refreshToken: refreshToken), RefreshRequest(refreshToken: refreshToken),
); );
await _tokenStorage.saveTokens( await _tokenStorage.saveTokens(
@@ -56,10 +58,10 @@ class AuthRepositoryImpl implements AuthRepository {
} }
@override @override
Future<void> logout() async { Future<void> deleteSession() async {
final refreshToken = await _tokenStorage.getRefreshToken(); final refreshToken = await _tokenStorage.getRefreshToken();
if (refreshToken != null) { if (refreshToken != null) {
await _api.logout(LogoutRequest(refreshToken: refreshToken)); await _api.deleteSession(LogoutRequest(refreshToken: refreshToken));
} }
await _tokenStorage.clear(); await _tokenStorage.clear();
} }
@@ -35,32 +35,12 @@ class AuthResponse {
} }
} }
class SignupStartResponse { class VerificationCreateResponse {
final String status;
final String email; final String email;
final String message;
const SignupStartResponse({ const VerificationCreateResponse({required this.email});
required this.status,
required this.email,
required this.message,
});
factory SignupStartResponse.fromJson(Map<String, dynamic> json) { factory VerificationCreateResponse.fromJson(Map<String, dynamic> json) {
return SignupStartResponse( return VerificationCreateResponse(email: json['email'] as String);
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<String, dynamic> json) {
return SignupResendResponse(message: json['message'] as String);
} }
} }
@@ -17,7 +17,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
final refreshToken = await _repository.getRefreshToken(); final refreshToken = await _repository.getRefreshToken();
if (refreshToken != null) { if (refreshToken != null) {
try { try {
final response = await _repository.refresh(refreshToken); final response = await _repository.refreshSession(refreshToken);
emit( emit(
AuthAuthenticated( AuthAuthenticated(
user: AuthUser(id: response.user.id, email: response.user.email), user: AuthUser(id: response.user.id, email: response.user.email),
@@ -25,7 +25,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
); );
return; return;
} catch (_) { } catch (_) {
await _repository.logout(); await _repository.deleteSession();
} }
} }
emit(AuthUnauthenticated()); emit(AuthUnauthenticated());
@@ -39,7 +39,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthLoggedOut event, AuthLoggedOut event,
Emitter<AuthState> emit, Emitter<AuthState> emit,
) async { ) async {
await _repository.logout(); await _repository.deleteSession();
emit(AuthUnauthenticated()); emit(AuthUnauthenticated());
} }
} }
@@ -59,7 +59,7 @@ class LoginCubit extends Cubit<LoginState> {
emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); emit(state.copyWith(status: FormzSubmissionStatus.inProgress));
try { try {
final response = await _repository.login( final response = await _repository.createSession(
LoginRequest(email: state.email.value, password: state.password.value), LoginRequest(email: state.email.value, password: state.password.value),
); );
emit(state.copyWith(status: FormzSubmissionStatus.success)); emit(state.copyWith(status: FormzSubmissionStatus.success));
@@ -99,7 +99,7 @@ class RegisterCubit extends Cubit<RegisterState> {
emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); emit(state.copyWith(status: FormzSubmissionStatus.inProgress));
try { try {
final response = await _repository.signupStart( final response = await _repository.createVerification(
SignupStartRequest( SignupStartRequest(
username: state.username.value, username: state.username.value,
email: state.email.value, email: state.email.value,
@@ -132,7 +132,7 @@ class RegisterCubit extends Cubit<RegisterState> {
emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); emit(state.copyWith(status: FormzSubmissionStatus.inProgress));
try { try {
final response = await _repository.signupVerify( final response = await _repository.verifyVerification(
SignupVerifyRequest( SignupVerifyRequest(
email: state.pendingEmail!, email: state.pendingEmail!,
token: state.verificationCode.value, token: state.verificationCode.value,
@@ -166,7 +166,7 @@ class RegisterCubit extends Cubit<RegisterState> {
emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); emit(state.copyWith(status: FormzSubmissionStatus.inProgress));
try { try {
await _repository.signupResend( await _repository.resendVerification(
SignupResendRequest(email: state.pendingEmail!), SignupResendRequest(email: state.pendingEmail!),
); );
emit( emit(
@@ -197,7 +197,7 @@ class RegisterCubit extends Cubit<RegisterState> {
); );
try { try {
final response = await _repository.signupStart( final response = await _repository.createVerification(
SignupStartRequest( SignupStartRequest(
username: state.username.value, username: state.username.value,
email: state.email.value, email: state.email.value,
@@ -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<String, dynamic> 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<String, dynamic> toJson() {
return {
if (username != null) 'username': username,
if (avatarUrl != null) 'avatar_url': avatarUrl,
if (bio != null) 'bio': bio,
};
}
}
@@ -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<UserResponse> getMe() async {
final response = await _client.get('$_prefix/me');
return UserResponse.fromJson(response.data);
}
Future<UserResponse> updateMe(UserUpdateRequest request) async {
final response = await _client.patch('$_prefix/me', data: request.toJson());
return UserResponse.fromJson(response.data);
}
Future<UserResponse> getByUsername(String username) async {
final response = await _client.get('$_prefix/$username');
return UserResponse.fromJson(response.data);
}
}
@@ -0,0 +1,7 @@
import 'models/user_response.dart';
abstract class UsersRepository {
Future<UserResponse> getMe();
Future<UserResponse> updateMe(UserUpdateRequest request);
Future<UserResponse> getByUsername(String username);
}
@@ -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<UserResponse> getMe() {
return _api.getMe();
}
@override
Future<UserResponse> updateMe(UserUpdateRequest request) {
return _api.updateMe(request);
}
@override
Future<UserResponse> getByUsername(String username) {
return _api.getByUsername(username);
}
}
@@ -34,16 +34,13 @@ void main() {
}); });
group('AuthRepositoryImpl', () { group('AuthRepositoryImpl', () {
test('signupStart calls api and returns response', () async { test('createVerification calls api and returns response', () async {
when(() => mockApi.signupStart(any())).thenAnswer( when(() => mockApi.createVerification(any())).thenAnswer(
(_) async => const SignupStartResponse( (_) async =>
status: 'pending_verification', const VerificationCreateResponse(email: 'test@example.com'),
email: 'test@example.com',
message: 'Verification code sent',
),
); );
final result = await repository.signupStart( final result = await repository.createVerification(
const SignupStartRequest( const SignupStartRequest(
username: 'testuser', username: 'testuser',
email: 'test@example.com', email: 'test@example.com',
@@ -51,12 +48,12 @@ void main() {
), ),
); );
expect(result.status, 'pending_verification'); expect(result.email, 'test@example.com');
verify(() => mockApi.signupStart(any())).called(1); verify(() => mockApi.createVerification(any())).called(1);
}); });
test('login calls api and saves tokens', () async { test('createSession calls api and saves tokens', () async {
when(() => mockApi.login(any())).thenAnswer( when(() => mockApi.createSession(any())).thenAnswer(
(_) async => AuthResponse( (_) async => AuthResponse(
accessToken: 'access_token', accessToken: 'access_token',
refreshToken: 'refresh_token', refreshToken: 'refresh_token',
@@ -72,7 +69,7 @@ void main() {
), ),
).thenAnswer((_) async {}); ).thenAnswer((_) async {});
final result = await repository.login( final result = await repository.createSession(
const LoginRequest(email: 'test@example.com', password: 'password123'), const LoginRequest(email: 'test@example.com', password: 'password123'),
); );
@@ -85,21 +82,24 @@ void main() {
).called(1); ).called(1);
}); });
test('logout calls api with refresh token and clears storage', () async { test(
when( 'deleteSession calls api with refresh token and clears storage',
() => mockStorage.getRefreshToken(), () async {
).thenAnswer((_) async => 'refresh_token'); when(
when(() => mockApi.logout(any())).thenAnswer((_) async {}); () => mockStorage.getRefreshToken(),
when(() => mockStorage.clear()).thenAnswer((_) async {}); ).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(() => mockApi.deleteSession(any())).called(1);
verify(() => mockStorage.clear()).called(1); verify(() => mockStorage.clear()).called(1);
}); },
);
test('refresh saves new tokens', () async { test('refreshSession saves new tokens', () async {
when(() => mockApi.refresh(any())).thenAnswer( when(() => mockApi.refreshSession(any())).thenAnswer(
(_) async => AuthResponse( (_) async => AuthResponse(
accessToken: 'new_access', accessToken: 'new_access',
refreshToken: 'new_refresh', refreshToken: 'new_refresh',
@@ -115,7 +115,7 @@ void main() {
), ),
).thenAnswer((_) async {}); ).thenAnswer((_) async {});
final result = await repository.refresh('old_refresh'); final result = await repository.refreshSession('old_refresh');
expect(result.accessToken, 'new_access'); expect(result.accessToken, 'new_access');
verify( verify(
@@ -41,7 +41,7 @@ void main() {
when( when(
() => mockRepository.getRefreshToken(), () => mockRepository.getRefreshToken(),
).thenAnswer((_) async => 'valid_refresh'); ).thenAnswer((_) async => 'valid_refresh');
when(() => mockRepository.refresh('valid_refresh')).thenAnswer( when(() => mockRepository.refreshSession('valid_refresh')).thenAnswer(
(_) async => AuthResponse( (_) async => AuthResponse(
accessToken: 'new_access', accessToken: 'new_access',
refreshToken: 'new_refresh', refreshToken: 'new_refresh',
@@ -63,9 +63,9 @@ void main() {
() => mockRepository.getRefreshToken(), () => mockRepository.getRefreshToken(),
).thenAnswer((_) async => 'expired_refresh'); ).thenAnswer((_) async => 'expired_refresh');
when( when(
() => mockRepository.refresh('expired_refresh'), () => mockRepository.refreshSession('expired_refresh'),
).thenThrow(Exception('Invalid refresh token')); ).thenThrow(Exception('Invalid refresh token'));
when(() => mockRepository.logout()).thenAnswer((_) async {}); when(() => mockRepository.deleteSession()).thenAnswer((_) async {});
return authBloc; return authBloc;
}, },
act: (bloc) => bloc.add(AuthStarted()), act: (bloc) => bloc.add(AuthStarted()),
@@ -86,7 +86,7 @@ void main() {
blocTest<AuthBloc, AuthState>( blocTest<AuthBloc, AuthState>(
'emits [AuthUnauthenticated] when AuthLoggedOut', 'emits [AuthUnauthenticated] when AuthLoggedOut',
build: () { build: () {
when(() => mockRepository.logout()).thenAnswer((_) async {}); when(() => mockRepository.deleteSession()).thenAnswer((_) async {});
return authBloc; return authBloc;
}, },
seed: () => AuthAuthenticated( seed: () => AuthAuthenticated(
@@ -65,12 +65,8 @@ void main() {
password: const Password.dirty('password123'), password: const Password.dirty('password123'),
), ),
setUp: () { setUp: () {
when(() => mockRepository.signupStart(any())).thenAnswer( when(() => mockRepository.createVerification(any())).thenAnswer(
(_) async => SignupStartResponse( (_) async => VerificationCreateResponse(email: 'test@example.com'),
status: 'ok',
email: 'test@example.com',
message: 'Code sent',
),
); );
}, },
act: (c) => c.sendCodeSilently(), act: (c) => c.sendCodeSilently(),
@@ -85,7 +81,7 @@ void main() {
), ),
], ],
verify: (_) { verify: (_) {
verify(() => mockRepository.signupStart(any())).called(1); verify(() => mockRepository.createVerification(any())).called(1);
}, },
); );
@@ -99,7 +95,7 @@ void main() {
), ),
setUp: () { setUp: () {
when( when(
() => mockRepository.signupStart(any()), () => mockRepository.createVerification(any()),
).thenThrow(ServerException('Network error')); ).thenThrow(ServerException('Network error'));
}, },
act: (c) => c.sendCodeSilently(), act: (c) => c.sendCodeSilently(),
@@ -113,12 +109,12 @@ void main() {
), ),
], ],
verify: (_) { verify: (_) {
verify(() => mockRepository.signupStart(any())).called(1); verify(() => mockRepository.createVerification(any())).called(1);
}, },
); );
blocTest<RegisterCubit, RegisterState>( blocTest<RegisterCubit, RegisterState>(
'does not call signupStart when input is invalid', 'does not call createVerification when input is invalid',
build: () => cubit, build: () => cubit,
seed: () => RegisterState( seed: () => RegisterState(
username: const Username.dirty(''), username: const Username.dirty(''),
@@ -128,7 +124,7 @@ void main() {
act: (c) => c.sendCodeSilently(), act: (c) => c.sendCodeSilently(),
expect: () => [], expect: () => [],
verify: (_) { verify: (_) {
verifyNever(() => mockRepository.signupStart(any())); verifyNever(() => mockRepository.createVerification(any()));
}, },
); );
@@ -144,7 +140,7 @@ void main() {
act: (c) => c.sendCodeSilently(), act: (c) => c.sendCodeSilently(),
expect: () => [], expect: () => [],
verify: (_) { verify: (_) {
verifyNever(() => mockRepository.signupStart(any())); verifyNever(() => mockRepository.createVerification(any()));
}, },
); );
}); });
@@ -161,7 +157,7 @@ void main() {
.having((s) => s.errorMessage, 'errorMessage', '验证码发送失败,请返回上一步重试'), .having((s) => s.errorMessage, 'errorMessage', '验证码发送失败,请返回上一步重试'),
], ],
verify: (_) { verify: (_) {
verifyNever(() => mockRepository.signupResend(any())); verifyNever(() => mockRepository.resendVerification(any()));
}, },
); );
@@ -171,8 +167,8 @@ void main() {
seed: () => RegisterState(pendingEmail: 'test@example.com'), seed: () => RegisterState(pendingEmail: 'test@example.com'),
setUp: () { setUp: () {
when( when(
() => mockRepository.signupResend(any()), () => mockRepository.resendVerification(any()),
).thenAnswer((_) async => SignupResendResponse(message: 'Code sent')); ).thenAnswer((_) async {});
}, },
act: (c) => c.resendCode(), act: (c) => c.resendCode(),
expect: () => [ expect: () => [
@@ -188,7 +184,7 @@ void main() {
), ),
], ],
verify: (_) { 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'), seed: () => RegisterState(pendingEmail: 'test@example.com'),
setUp: () { setUp: () {
when( when(
() => mockRepository.signupResend(any()), () => mockRepository.resendVerification(any()),
).thenThrow(ServerException('Network error')); ).thenThrow(ServerException('Network error'));
}, },
act: (c) => c.resendCode(), act: (c) => c.resendCode(),