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);
}
}
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 {
try {
await authRepository.refresh(token);
await authRepository.refreshSession(token);
return true;
} catch (_) {
return false;
+15 -17
View File
@@ -9,47 +9,45 @@ class AuthApi {
AuthApi(this._client);
Future<SignupStartResponse> signupStart(SignupStartRequest request) async {
Future<VerificationCreateResponse> 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<AuthResponse> signupVerify(SignupVerifyRequest request) async {
Future<AuthResponse> verifyVerification(SignupVerifyRequest request) async {
final response = await _client.post(
'$_prefix/signup/verify',
'$_prefix/verifications/verify',
data: request.toJson(),
);
return AuthResponse.fromJson(response.data);
}
Future<SignupResendResponse> signupResend(SignupResendRequest request) async {
final response = await _client.post(
'$_prefix/signup/resend',
data: request.toJson(),
);
return SignupResendResponse.fromJson(response.data);
Future<void> resendVerification(SignupResendRequest request) async {
await _client.post('$_prefix/verifications/resend', data: request.toJson());
}
Future<AuthResponse> login(LoginRequest request) async {
Future<AuthResponse> createSession(LoginRequest request) async {
final response = await _client.post(
'$_prefix/login',
'$_prefix/sessions',
data: request.toJson(),
);
return AuthResponse.fromJson(response.data);
}
Future<AuthResponse> refresh(RefreshRequest request) async {
Future<AuthResponse> refreshSession(RefreshRequest request) async {
final response = await _client.post(
'$_prefix/refresh',
'$_prefix/sessions/refresh',
data: request.toJson(),
);
return AuthResponse.fromJson(response.data);
}
Future<void> logout(LogoutRequest request) async {
await _client.post('$_prefix/logout', data: request.toJson());
Future<void> deleteSession(LogoutRequest request) async {
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';
abstract class AuthRepository {
Future<SignupStartResponse> signupStart(SignupStartRequest request);
Future<AuthResponse> signupVerify(SignupVerifyRequest request);
Future<SignupResendResponse> signupResend(SignupResendRequest request);
Future<AuthResponse> login(LoginRequest request);
Future<AuthResponse> refresh(String refreshToken);
Future<void> logout();
Future<VerificationCreateResponse> createVerification(
SignupStartRequest request,
);
Future<AuthResponse> verifyVerification(SignupVerifyRequest request);
Future<void> resendVerification(SignupResendRequest request);
Future<AuthResponse> createSession(LoginRequest request);
Future<AuthResponse> refreshSession(String refreshToken);
Future<void> deleteSession();
Future<String?> getAccessToken();
Future<String?> getRefreshToken();
Future<bool> isAuthenticated();
@@ -14,13 +14,15 @@ class AuthRepositoryImpl implements AuthRepository {
_tokenStorage = tokenStorage;
@override
Future<SignupStartResponse> signupStart(SignupStartRequest request) {
return _api.signupStart(request);
Future<VerificationCreateResponse> createVerification(
SignupStartRequest request,
) {
return _api.createVerification(request);
}
@override
Future<AuthResponse> signupVerify(SignupVerifyRequest request) async {
final response = await _api.signupVerify(request);
Future<AuthResponse> 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<SignupResendResponse> signupResend(SignupResendRequest request) {
return _api.signupResend(request);
Future<void> resendVerification(SignupResendRequest request) {
return _api.resendVerification(request);
}
@override
Future<AuthResponse> login(LoginRequest request) async {
final response = await _api.login(request);
Future<AuthResponse> 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<AuthResponse> refresh(String refreshToken) async {
final response = await _api.refresh(
Future<AuthResponse> 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<void> logout() async {
Future<void> 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();
}
@@ -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<String, dynamic> 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<String, dynamic> json) {
return SignupResendResponse(message: json['message'] as String);
factory VerificationCreateResponse.fromJson(Map<String, dynamic> json) {
return VerificationCreateResponse(email: json['email'] as String);
}
}
@@ -17,7 +17,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
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<AuthEvent, AuthState> {
);
return;
} catch (_) {
await _repository.logout();
await _repository.deleteSession();
}
}
emit(AuthUnauthenticated());
@@ -39,7 +39,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthLoggedOut event,
Emitter<AuthState> emit,
) async {
await _repository.logout();
await _repository.deleteSession();
emit(AuthUnauthenticated());
}
}
@@ -59,7 +59,7 @@ class LoginCubit extends Cubit<LoginState> {
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));
@@ -99,7 +99,7 @@ class RegisterCubit extends Cubit<RegisterState> {
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<RegisterState> {
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<RegisterState> {
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<RegisterState> {
);
try {
final response = await _repository.signupStart(
final response = await _repository.createVerification(
SignupStartRequest(
username: state.username.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', () {
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(
@@ -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<AuthBloc, AuthState>(
'emits [AuthUnauthenticated] when AuthLoggedOut',
build: () {
when(() => mockRepository.logout()).thenAnswer((_) async {});
when(() => mockRepository.deleteSession()).thenAnswer((_) async {});
return authBloc;
},
seed: () => AuthAuthenticated(
@@ -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<RegisterCubit, RegisterState>(
'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(),