Skip to content

Commit

Permalink
refactor(authentication): Migrate to bloc - authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
realth000 committed Jan 20, 2024
1 parent 331fbcd commit ee9a07a
Show file tree
Hide file tree
Showing 38 changed files with 747 additions and 58 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- 更新隐藏区域卡片上的按钮样式,更明显。
- 如果帖子的评分消息中评分理由为空,不显示评分理由。
- 登录后,加载首页的时机推迟到访问首页时,而不是登录后直接加载。

## [0.3.0] - 2023-12-30

Expand Down
2 changes: 1 addition & 1 deletion lib/app.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:tsdm_client/features/authentication/repository/authentication_repository.dart';
import 'package:tsdm_client/features/forum/repository/forum_repository.dart';
import 'package:tsdm_client/features/theme/cubit/theme_cubit.dart';
import 'package:tsdm_client/features/upgrade/repository/upgrade_repository.dart';
import 'package:tsdm_client/generated/i18n/strings.g.dart';
import 'package:tsdm_client/routes/app_routes.dart';
import 'package:tsdm_client/shared/repositories/authentication_repository/authentication_repository.dart';
import 'package:tsdm_client/shared/repositories/cache_repository/cache_repository.dart';
import 'package:tsdm_client/shared/repositories/forum_home_repository/forum_home_repository.dart';
import 'package:tsdm_client/shared/repositories/fragments_repository/fragments_repository.dart';
Expand Down
64 changes: 64 additions & 0 deletions lib/features/authentication/bloc/authentication_bloc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tsdm_client/exceptions/exceptions.dart';
import 'package:tsdm_client/features/authentication/repository/authentication_repository.dart';
import 'package:tsdm_client/features/authentication/repository/exceptions/exceptions.dart';
import 'package:tsdm_client/features/authentication/repository/models/hash.dart';
import 'package:tsdm_client/features/authentication/repository/models/user_credential.dart';
import 'package:tsdm_client/utils/debug.dart';

part 'authentication_event.dart';
part 'authentication_state.dart';

typedef AuthenticationEmitter = Emitter<AuthenticationState>;

class AuthenticationBloc
extends Bloc<AuthenticationEvent, AuthenticationState> {
AuthenticationBloc(
{required AuthenticationRepository authenticationRepository})
: _authenticationRepository = authenticationRepository,
super(const AuthenticationState()) {
on<AuthenticationFetchLoginHashRequested>(
_onAuthenticationFetchLoginHashRequested);
on<AuthenticationLoginRequested>(_onAuthenticationLoginRequested);
}

final AuthenticationRepository _authenticationRepository;

Future<void> _onAuthenticationFetchLoginHashRequested(
AuthenticationFetchLoginHashRequested event,
AuthenticationEmitter emit,
) async {
emit(state.copyWith(status: AuthenticationStatus.fetchingHash));
try {
final loginHash = await _authenticationRepository.fetchHash();
emit(state.copyWith(
status: AuthenticationStatus.gotHash, loginHash: loginHash));
} on HttpRequestFailedException catch (e) {
debug('failed to fetch login hash: $e');
emit(state.copyWith(status: AuthenticationStatus.failed));
} on LoginException catch (e) {
debug('failed to fetch login hash: $e');
emit(state.copyWith(
status: AuthenticationStatus.failed, loginException: e));
}
}

Future<void> _onAuthenticationLoginRequested(
AuthenticationLoginRequested event,
AuthenticationEmitter emit,
) async {
emit(state.copyWith(status: AuthenticationStatus.loggingIn));
try {
await _authenticationRepository.loginWithPassword(event.userCredential);
emit(state.copyWith(status: AuthenticationStatus.success));
} on HttpRequestFailedException catch (e) {
debug('failed to login: $e');
emit(state.copyWith(status: AuthenticationStatus.failed));
} on LoginException catch (e) {
debug('failed to login: $e');
emit(state.copyWith(
status: AuthenticationStatus.failed, loginException: e));
}
}
}
16 changes: 16 additions & 0 deletions lib/features/authentication/bloc/authentication_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
part of 'authentication_bloc.dart';

sealed class AuthenticationEvent extends Equatable {
const AuthenticationEvent();

@override
List<Object?> get props => [];
}

/// Call this event to fetch hash data required in login process before login.
final class AuthenticationFetchLoginHashRequested extends AuthenticationEvent {}

final class AuthenticationLoginRequested extends AuthenticationEvent {
const AuthenticationLoginRequested(this.userCredential) : super();
final UserCredential userCredential;
}
49 changes: 49 additions & 0 deletions lib/features/authentication/bloc/authentication_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
part of 'authentication_bloc.dart';

enum AuthenticationStatus {
initial,

/// Fetching hash data that need to use in login process.
fetchingHash,

//
gotHash,

/// Polling login request.
loggingIn,

/// Login success.
success,

/// Login failed.
failed,
}

final class AuthenticationState extends Equatable {
const AuthenticationState({
this.status = AuthenticationStatus.initial,
this.loginHash,
this.loginException,
});

final AuthenticationStatus status;

final LoginHash? loginHash;

final LoginException? loginException;

AuthenticationState copyWith({
AuthenticationStatus? status,
LoginHash? loginHash,
LoginException? loginException,
}) {
return AuthenticationState(
status: status ?? this.status,
loginHash: loginHash ?? this.loginHash,
loginException: loginException ?? this.loginException,
);
}

@override
List<Object?> get props => [status];
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import 'package:rxdart/rxdart.dart';
import 'package:tsdm_client/constants/url.dart';
import 'package:tsdm_client/exceptions/exceptions.dart';
import 'package:tsdm_client/extensions/universal_html.dart';
import 'package:tsdm_client/features/authentication/repository/exceptions/exceptions.dart';
import 'package:tsdm_client/features/authentication/repository/internal/login_result.dart';
import 'package:tsdm_client/features/authentication/repository/internal/user_info.dart';
import 'package:tsdm_client/features/authentication/repository/models/hash.dart';
import 'package:tsdm_client/features/authentication/repository/models/user.dart';
import 'package:tsdm_client/features/authentication/repository/models/user_credential.dart';
import 'package:tsdm_client/instance.dart';
import 'package:tsdm_client/shared/providers/net_client_provider/net_client_provider.dart';
import 'package:tsdm_client/shared/providers/settings_provider/settings_provider.dart';
import 'package:tsdm_client/shared/repositories/authentication_repository/exceptions/exceptions.dart';
import 'package:tsdm_client/shared/repositories/authentication_repository/internal/login_result.dart';
import 'package:tsdm_client/shared/repositories/authentication_repository/internal/user_info.dart';
import 'package:tsdm_client/shared/repositories/authentication_repository/models/user.dart';
import 'package:tsdm_client/shared/repositories/authentication_repository/models/user_credential.dart';
import 'package:tsdm_client/utils/debug.dart';
import 'package:universal_html/html.dart' as uh;
import 'package:universal_html/parsing.dart';
Expand Down Expand Up @@ -55,6 +56,9 @@ class AuthenticationRepository {
'$baseUrl/member.php?mod=logging&action=login&loginsubmit=yes&frommessage&loginhash=';
static const _logoutBaseUrl =
'$baseUrl/member.php?mod=logging&action=logout&formhash=';
static const _fakeFormUrl =
'$baseUrl/member.php?mod=logging&action=login&infloat=yes&frommessage&inajax=1&ajaxtarget=messagelogin';
static final _layerLoginRe = RegExp(r'layer_login_(?<Hash>\w+)');
static final _formHashRe = RegExp(r'formhash" value="(?<FormHash>\w+)"');

static String _buildLoginUrl(String formHash) {
Expand All @@ -78,6 +82,44 @@ class AuthenticationRepository {
_controller.close();
}

/// Fetch login hash and form hash for logging in.
///
/// # Exception
///
/// * **HttpRequestFailedException** when http request failed.
/// * **LoginFormHashNotFoundException** when form hash not found.
/// * **LoginInvalidFormHashException** when form hash found but not in the expected format.
Future<LoginHash> fetchHash() async {
// TODO: Parse CDATA.
// 返回的data是xml:
//
// <?xml version="1.0" encoding="utf-8"?>
// <root><![CDATA[
// <div id="main_messaqge_L5hJN">
// <div id="layer_login_L5hJN">
//
// 其中"main_message_"后面的是本次登录的loginHash,登录时需要加到url上
final rawDataResp = await getIt.get<NetClientProvider>().get(_fakeFormUrl);
if (rawDataResp.statusCode != HttpStatus.ok) {
throw HttpRequestFailedException(rawDataResp.statusCode!);
}
final data = rawDataResp.data as String;
final match = _layerLoginRe.firstMatch(data);
final loginHash = match?.namedGroup('Hash');
if (loginHash == null) {
throw LoginFormHashNotFoundException();
}

final formHashMatch = _formHashRe.firstMatch(data);
final formHash = formHashMatch?.namedGroup('FormHash');
if (formHash == null) {
throw LoginInvalidFormHashException();
}

debug('get login hash $loginHash');
return LoginHash(formHash: formHash, loginHash: loginHash);
}

Future<void> loginWithCookie(Map<String, dynamic> cookieMap) async {
throw UnimplementedError();
}
Expand All @@ -96,8 +138,8 @@ class AuthenticationRepository {
Future<void> loginWithPassword(UserCredential credential) async {
final target = _buildLoginUrl(credential.formHash);
// FIXME: Now we indicate the login field in credential is always username.
final netClient = getIt.get<NetClientProvider>();
final resp = await netClient.postForm(target, data: credential);
final netClient = NetClientProvider(username: credential.loginFieldValue);
final resp = await netClient.postForm(target, data: credential.toJson());
if (resp.statusCode != HttpStatus.ok) {
throw HttpRequestFailedException(resp.statusCode!);
}
Expand Down Expand Up @@ -190,7 +232,10 @@ class AuthenticationRepository {
// Here we'd better to check the failed reason, but it's ok without it.
throw LogoutFailedException();
}
await getIt.get<SettingsProvider>().setLoginInfo('', -1);
await getIt
.get<SettingsProvider>()
.deleteCookieByUsername(_authedUser?.username ?? '');
await getIt.get<SettingsProvider>().setLoginInfo(null, null);
_authedUser = null;
_controller.add(AuthenticationStatus.unauthenticated);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
sealed class LoginException implements Exception {}

final class LoginFormHashNotFoundException implements LoginException {}

/// Found form hash, but it's not in the expect format.
final class LoginInvalidFormHashException implements LoginException {}

final class LoginMessageNotFoundException implements LoginException {}

final class LoginIncorrectCaptchaException implements LoginException {}
Expand Down
14 changes: 14 additions & 0 deletions lib/features/authentication/repository/models/hash.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:equatable/equatable.dart';

class LoginHash extends Equatable {
const LoginHash({
required this.formHash,
required this.loginHash,
});

final String formHash;
final String loginHash;

@override
List<Object?> get props => [formHash, loginHash];
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,12 @@ class UserCredential {
'formhash': formHash,
'tsdm_verify': tsdmVerify,
'referer': referer,
'questionid': 0,
'answer': 0,
'questionid': securityQuestion?.questionId ?? 0,
'answer': securityQuestion?.answer ?? 0,
'cookietime': cookieTime,
'loginsubmit': loginSubmit,
};

if (securityQuestion != null) {
m['questionid'] = securityQuestion!.questionId;
m['answer'] = securityQuestion!.answer;
}

return m;
}
}
82 changes: 82 additions & 0 deletions lib/features/authentication/view/login_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:tsdm_client/features/authentication/bloc/authentication_bloc.dart';
import 'package:tsdm_client/features/authentication/repository/exceptions/exceptions.dart';
import 'package:tsdm_client/features/authentication/widgets/login_form.dart';
import 'package:tsdm_client/generated/i18n/strings.g.dart';

class LoginPage extends StatefulWidget {
const LoginPage({this.redirectBackState, super.key});

final GoRouterState? redirectBackState;

@override
State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.t.loginPage.title),
),
body: BlocProvider(
create: (context) => AuthenticationBloc(
authenticationRepository: RepositoryProvider.of(context))
..add(AuthenticationFetchLoginHashRequested()),
child: BlocListener<AuthenticationBloc, AuthenticationState>(
listener: (context, state) {
if (state.status == AuthenticationStatus.failed) {
final errorText = switch (state.loginException) {
LoginFormHashNotFoundException() =>
context.t.loginPage.hashValueNotFound,
LoginInvalidFormHashException() =>
context.t.loginPage.failedToGetFormHash,
LoginMessageNotFoundException() =>
context.t.loginPage.failedToLoginMessageNodeNotFound,
LoginIncorrectCaptchaException() =>
context.t.loginPage.loginResultIncorrectCaptcha,
LoginInvalidCredentialException() =>
context.t.loginPage.loginResultIncorrectUsernameOrPassword,
LoginIncorrectSecurityQuestionException() =>
context.t.loginPage.loginResultIncorrectQuestionOrAnswer,
LoginAttemptLimitException() =>
context.t.loginPage.loginResultTooManyLoginAttempts,
LoginUserInfoNotFoundException() =>
context.t.loginPage.loginFailed,
LoginOtherErrorException() =>
context.t.loginPage.loginResultOtherErrors,
null => context.t.general.failedToLoad,
};
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(errorText)));

context
.read<AuthenticationBloc>()
.add(AuthenticationFetchLoginHashRequested());
}
},
child: Padding(
padding: const EdgeInsets.all(15),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 500,
maxWidth: 500,
),
child: LoginForm(
redirectPath: widget.redirectBackState?.fullPath,
redirectPathParameters:
widget.redirectBackState?.pathParameters,
redirectExtra: widget.redirectBackState?.extra,
),
),
),
),
),
),
);
}
}
Loading

0 comments on commit ee9a07a

Please sign in to comment.