275 lines
7.6 KiB
Dart
275 lines
7.6 KiB
Dart
import 'package:bloc/bloc.dart';
|
|
import '../session/session_bloc.dart';
|
|
import '../session/session_event.dart';
|
|
import '../session/session_state.dart';
|
|
import 'auth_event.dart';
|
|
import 'auth_state.dart';
|
|
import '../../services/api_client.dart';
|
|
import '../../models/api_error.dart';
|
|
|
|
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|
final ApiClient apiClient;
|
|
final SessionBloc sessionBloc;
|
|
|
|
AuthBloc({required this.apiClient, required this.sessionBloc})
|
|
: super(AuthInitial()) {
|
|
on<SignupStarted>(_onSignupStarted);
|
|
on<RegistrationChallengeRequested>(_onRegistrationChallengeRequested);
|
|
on<RegistrationResponseSubmitted>(_onRegistrationResponseSubmitted);
|
|
on<LoginRequested>(_onLoginRequested);
|
|
on<PasswordLoginRequested>(_onPasswordLoginRequested);
|
|
on<AuthenticationChallengeRequested>(_onAuthenticationChallengeRequested);
|
|
on<AuthenticationResponseSubmitted>(_onAuthenticationResponseSubmitted);
|
|
on<LogoutRequested>(_onLogoutRequested);
|
|
on<CheckAuthRequested>(_onCheckAuthRequested);
|
|
}
|
|
|
|
Future<void> _onSignupStarted(
|
|
SignupStarted event,
|
|
Emitter<AuthState> emit,
|
|
) async {
|
|
emit(AuthLoading(message: 'Creating account...'));
|
|
try {
|
|
final response = await apiClient.post(
|
|
'/auth/signup',
|
|
data: {
|
|
'username': event.username,
|
|
'email': event.email,
|
|
'displayName': event.displayName,
|
|
'password': event.password ?? '',
|
|
},
|
|
fromJson: (data) => data,
|
|
);
|
|
|
|
final userId = response['userId'];
|
|
emit(
|
|
SignupInProgress(
|
|
username: event.username,
|
|
email: event.email,
|
|
displayName: event.displayName,
|
|
),
|
|
);
|
|
|
|
// Trigger registration challenge request
|
|
add(RegistrationChallengeRequested(userId: userId));
|
|
} catch (e) {
|
|
final errorMessage = _extractErrorMessage(e);
|
|
emit(AuthFailure(errorMessage));
|
|
}
|
|
}
|
|
|
|
Future<void> _onRegistrationChallengeRequested(
|
|
RegistrationChallengeRequested event,
|
|
Emitter<AuthState> emit,
|
|
) async {
|
|
emit(AuthLoading(message: 'Getting registration challenge...'));
|
|
try {
|
|
final response = await apiClient.post(
|
|
'/auth/registration-challenge',
|
|
data: {'userId': event.userId},
|
|
fromJson: (data) => data,
|
|
);
|
|
|
|
emit(
|
|
RegistrationChallengeReceived(
|
|
userId: event.userId,
|
|
challenge: response['challenge'],
|
|
rpName: response['rp']['name'],
|
|
rpId: response['rp']['id'],
|
|
),
|
|
);
|
|
} catch (e) {
|
|
final errorMessage = _extractErrorMessage(e);
|
|
emit(AuthFailure(errorMessage));
|
|
}
|
|
}
|
|
|
|
Future<void> _onRegistrationResponseSubmitted(
|
|
RegistrationResponseSubmitted event,
|
|
Emitter<AuthState> emit,
|
|
) async {
|
|
emit(AuthLoading(message: 'Verifying registration...'));
|
|
try {
|
|
final response = await apiClient.post(
|
|
'/auth/registration-verify',
|
|
data: {
|
|
'userId': event.userId,
|
|
'challenge': event.challenge,
|
|
'credentialId': event.credentialId,
|
|
'publicKey': event.publicKey,
|
|
'clientDataJSON': event.clientDataJSON,
|
|
'attestationObject': event.attestationObject,
|
|
},
|
|
fromJson: (data) => data,
|
|
);
|
|
|
|
final token = response['token'];
|
|
final user = response['user'];
|
|
|
|
sessionBloc.add(SessionStarted(token));
|
|
|
|
emit(
|
|
AuthAuthenticated(
|
|
token: token,
|
|
userId: user['id'],
|
|
username: user['username'],
|
|
email: user['email'],
|
|
),
|
|
);
|
|
} catch (e) {
|
|
final errorMessage = _extractErrorMessage(e);
|
|
emit(AuthFailure(errorMessage));
|
|
}
|
|
}
|
|
|
|
Future<void> _onLoginRequested(
|
|
LoginRequested event,
|
|
Emitter<AuthState> emit,
|
|
) async {
|
|
emit(AuthLoading(message: 'Requesting authentication challenge...'));
|
|
try {
|
|
add(AuthenticationChallengeRequested(username: event.username));
|
|
} catch (e) {
|
|
final errorMessage = _extractErrorMessage(e);
|
|
emit(AuthFailure(errorMessage));
|
|
}
|
|
}
|
|
|
|
Future<void> _onAuthenticationChallengeRequested(
|
|
AuthenticationChallengeRequested event,
|
|
Emitter<AuthState> emit,
|
|
) async {
|
|
try {
|
|
final response = await apiClient.post(
|
|
'/auth/authentication-challenge',
|
|
data: {'username': event.username},
|
|
fromJson: (data) => data,
|
|
);
|
|
|
|
final credentialIds =
|
|
(response['allowCredentials'] as List?)
|
|
?.map((id) => id.toString())
|
|
.toList() ??
|
|
[];
|
|
|
|
emit(
|
|
AuthenticationChallengeReceived(
|
|
challenge: response['challenge'],
|
|
credentialIds: credentialIds,
|
|
),
|
|
);
|
|
} catch (e) {
|
|
final errorMessage = _extractErrorMessage(e);
|
|
emit(AuthFailure(errorMessage));
|
|
}
|
|
}
|
|
|
|
Future<void> _onAuthenticationResponseSubmitted(
|
|
AuthenticationResponseSubmitted event,
|
|
Emitter<AuthState> emit,
|
|
) async {
|
|
emit(AuthLoading(message: 'Verifying authentication...'));
|
|
try {
|
|
final response = await apiClient.post(
|
|
'/auth/authentication-verify',
|
|
data: {
|
|
'username': event.username,
|
|
'challenge': event.challenge,
|
|
'credentialId': event.credentialId,
|
|
'authenticatorData': event.authenticatorData,
|
|
'clientDataJSON': event.clientDataJSON,
|
|
'signature': event.signature,
|
|
},
|
|
fromJson: (data) => data,
|
|
);
|
|
|
|
final token = response['token'];
|
|
final user = response['user'];
|
|
|
|
sessionBloc.add(SessionStarted(token));
|
|
|
|
emit(
|
|
AuthAuthenticated(
|
|
token: token,
|
|
userId: user['id'],
|
|
username: user['username'],
|
|
email: user['email'],
|
|
),
|
|
);
|
|
} catch (e) {
|
|
final errorMessage = _extractErrorMessage(e);
|
|
emit(AuthFailure(errorMessage));
|
|
}
|
|
}
|
|
|
|
Future<void> _onLogoutRequested(
|
|
LogoutRequested event,
|
|
Emitter<AuthState> emit,
|
|
) async {
|
|
emit(AuthLoading(message: 'Logging out...'));
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
sessionBloc.add(SessionExpired());
|
|
emit(AuthUnauthenticated());
|
|
}
|
|
|
|
Future<void> _onPasswordLoginRequested(
|
|
PasswordLoginRequested event,
|
|
Emitter<AuthState> emit,
|
|
) async {
|
|
emit(AuthLoading(message: 'Authenticating...'));
|
|
try {
|
|
final response = await apiClient.post(
|
|
'/auth/password-login',
|
|
data: {'username': event.username, 'password': event.password},
|
|
fromJson: (data) => data,
|
|
);
|
|
|
|
final token = response['token'];
|
|
final user = response['user'];
|
|
|
|
sessionBloc.add(SessionStarted(token));
|
|
emit(
|
|
AuthAuthenticated(
|
|
token: token,
|
|
userId: user['id'],
|
|
username: user['username'],
|
|
email: user['email'],
|
|
),
|
|
);
|
|
} catch (e) {
|
|
final errorMessage = _extractErrorMessage(e);
|
|
emit(AuthFailure(errorMessage));
|
|
}
|
|
}
|
|
|
|
String _extractErrorMessage(dynamic error) {
|
|
if (error is ApiError) {
|
|
return error.message;
|
|
}
|
|
return error.toString();
|
|
}
|
|
|
|
Future<void> _onCheckAuthRequested(
|
|
CheckAuthRequested event,
|
|
Emitter<AuthState> emit,
|
|
) async {
|
|
// Check if session is active from persistent storage
|
|
final sessionState = sessionBloc.state;
|
|
|
|
if (sessionState is SessionActive) {
|
|
// Session already active - emit authenticated state with minimal info
|
|
// The full user info will be fetched when needed
|
|
emit(
|
|
AuthAuthenticated(
|
|
token: sessionState.token,
|
|
userId: '',
|
|
username: '',
|
|
email: '',
|
|
),
|
|
);
|
|
} else {
|
|
emit(AuthUnauthenticated());
|
|
}
|
|
}
|
|
}
|