Skip to content

Commit

Permalink
fix(auth): Fix failed to login
Browse files Browse the repository at this point in the history
  • Loading branch information
realth000 committed Aug 11, 2024
1 parent 5dc2c8a commit cba5f7d
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:tsdm_client/features/authentication/repository/models/models.dar
import 'package:tsdm_client/features/settings/repositories/settings_repository.dart';
import 'package:tsdm_client/instance.dart';
import 'package:tsdm_client/shared/models/models.dart';
import 'package:tsdm_client/shared/providers/cookie_provider/models/cookie_data.dart';
import 'package:tsdm_client/shared/providers/net_client_provider/net_client_provider.dart';
import 'package:tsdm_client/shared/providers/storage_provider/storage_provider.dart';
import 'package:tsdm_client/utils/logger.dart';
Expand Down Expand Up @@ -148,7 +149,13 @@ class AuthenticationRepository with LoggerMixin {
};

debug('login with user info: $userLoginInfo');
final netClient = NetClientProvider.build(userLoginInfo: userLoginInfo);
// Here the userLoginInfo is incomplete:
//
// Only contains one login field: Username, uid or email.
final netClient = NetClientProvider.build(
userLoginInfo: userLoginInfo,
startLogin: true,
);
final resp = await netClient.postForm(target, data: credential.toJson());
if (resp.statusCode != HttpStatus.ok) {
throw HttpRequestFailedException(resp.statusCode);
Expand Down Expand Up @@ -177,14 +184,16 @@ class AuthenticationRepository with LoggerMixin {
throw LoginUserInfoNotFoundException();
}

// Mark login progress has ended.
await CookieData.endLogin(fullUserInfo);

// Here we get complete user info.
await _saveLoggedUserInfo(fullUserInfo);

_authedUser = UserLoginInfo(
username: fullUserInfo.username,
uid: fullUserInfo.uid,
email: fullUserInfo.email,
);
debug('login finished');

_controller.add(AuthenticationStatus.authenticated);

return;
}

Expand Down Expand Up @@ -216,14 +225,15 @@ class AuthenticationRepository with LoggerMixin {
final fullUserInfo =
_parseUserInfoFromDocument(fullInfoDoc, parseEmail: true);
if (fullUserInfo == null || !fullUserInfo.isComplete) {
error('failed to: parse login result: email not foudn');
error('failed to: parse login result: email not found');
return;
}

// Here we get complete user info.
await _saveLoggedUserInfo(fullUserInfo);
_controller.add(AuthenticationStatus.authenticated);

_authedUser = fullUserInfo;
debug('login with document: user $userInfo');
_controller.add(AuthenticationStatus.authenticated);
}

/// Logout the current user.
Expand All @@ -242,7 +252,14 @@ class AuthenticationRepository with LoggerMixin {
if (_authedUser == null) {
return;
}
final netClient = getIt.get<NetClientProvider>();
final netClient = NetClientProvider.build(
userLoginInfo: UserLoginInfo(
username: _authedUser!.username,
uid: _authedUser!.uid,
email: _authedUser!.email,
),
logout: true,
);
final resp = await netClient.get(_checkAuthUrl);
if (resp.statusCode != HttpStatus.ok) {
throw HttpRequestFailedException(resp.statusCode);
Expand Down Expand Up @@ -368,5 +385,7 @@ class AuthenticationRepository with LoggerMixin {
SettingsKeys.loginEmail,
userInfo.email!,
);

_authedUser = userInfo;
}
}
33 changes: 30 additions & 3 deletions lib/shared/providers/cookie_provider/cookie_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,25 @@ class CookieProvider with LoggerMixin {
/// * First look in storage and find the cached cookie related to
/// [userLoginInfo].
/// * Return empty cookie if not found in cache.
CookieData build({UserLoginInfo? userLoginInfo}) {
// Specified user override.
CookieData build({
UserLoginInfo? userLoginInfo,
bool startLogin = false,
bool logout = false,
}) {
assert(
!(startLogin && logout),
'startLogin and logout can NOT be true at the same time',
);

if (userLoginInfo != null) {
if (startLogin) {
assert(
!userLoginInfo.isEmpty,
'A UserLoginInfo intend to start login can NOT be empty',
);
return CookieData.startLogin(userLoginInfo);
}

final username = userLoginInfo.username;
final uid = userLoginInfo.uid;
final email = userLoginInfo.email;
Expand All @@ -66,7 +82,18 @@ class CookieProvider with LoggerMixin {
cookie = Map.castFrom(databaseCookie);
}

debug('load cookie with user info');
if (logout) {
assert(
userLoginInfo.isComplete,
'A UserLoginInfo intend to start login can MUST be complete',
);
return CookieData.logout(
userLoginInfo: userLoginInfo,
cookie: cookie,
);
}

debug('load cookie with user info : $userLoginInfo');
return CookieData.withUserInfo(
userLoginInfo: userLoginInfo,
cookie: cookie,
Expand Down
112 changes: 101 additions & 11 deletions lib/shared/providers/cookie_provider/models/cookie_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import 'package:tsdm_client/shared/models/models.dart';
import 'package:tsdm_client/shared/providers/storage_provider/storage_provider.dart';
import 'package:tsdm_client/utils/logger.dart';

part 'current_user_cookie.dart';

/// Cookie stored to use in [PersistCookieJar] as replacement of [FileStorage]
/// that save in database.
///
Expand All @@ -16,6 +18,26 @@ import 'package:tsdm_client/utils/logger.dart';
///
/// Use the [getIt] to access database.
class CookieData with LoggerMixin implements Storage {
/// Constructor.
///
/// Use this constructor when logout.
CookieData.logout({
required UserLoginInfo userLoginInfo,
required Cookie cookie,
}) : _userLoginInfo = userLoginInfo,
_cookieMap = Map.castFrom(cookie) {
debug('logout progress with userinfo $_userLoginInfo');

// We may logout another user, or current user.
//
// Only clear static cookie cache when current user logout.
// This progress is prepared for the multiuser feature.
if (userLoginInfo == _currentUserCookie.userLoginInfo) {
debug('clear temporary cookie static cache for current login user');
_currentUserCookie = _CurrentUserCookie.empty();
}
}

/// Construct with [userLoginInfo] and [cookie].
///
/// [userLoginInfo] can be partly filled. What means is during login progress,
Expand All @@ -26,9 +48,54 @@ class CookieData with LoggerMixin implements Storage {
/// * Password.
CookieData.withUserInfo({
required UserLoginInfo userLoginInfo,
required Cookie cookie,
Cookie? cookie,
}) : _userLoginInfo = userLoginInfo,
_cookieMap = Map.castFrom(cookie);
_cookieMap = cookie != null
? Map.castFrom(cookie)
: _currentUserCookie.cookie ?? {} {
if (userLoginInfo.isComplete && cookie != null) {
// User info is complete, means already has user login.
_currentUserCookie.copyWith(
userLoginInfo: userLoginInfo,
cookie: Map.castFrom(cookie),
);
}
}

/// Construct with [userLoginInfo].
///
/// Use this constructor when starting a new login progress.
CookieData.startLogin(UserLoginInfo userLoginInfo)
: _userLoginInfo = userLoginInfo,
_cookieMap = {} {
// Clear current user cookie when going to start a login progress.
debug('start login progress with userinfo $userLoginInfo');
_currentUserCookie =
_CurrentUserCookie.empty().copyWith(userLoginInfo: userLoginInfo);
}

// FIXME: This static method is bad practise.
/// Call when a login progress is going to end.
///
/// **Note that [userLoginInfo] MUST be completed**.
static Future<void> endLogin(UserLoginInfo userLoginInfo) async {
assert(
userLoginInfo.isComplete,
'The UserLoginInfo intend to finish a '
'login progress MUST be complete',
);
// Fulfill user info.
talker.debug('end login progress with userinfo $userLoginInfo');
_currentUserCookie =
_currentUserCookie.copyWith(userLoginInfo: userLoginInfo);

await getIt.get<StorageProvider>().saveCookie(
username: userLoginInfo.username!,
uid: userLoginInfo.uid!,
email: userLoginInfo.email!,
cookie: _currentUserCookie.cookie!,
);
}

final UserLoginInfo? _userLoginInfo;

Expand All @@ -42,6 +109,33 @@ class CookieData with LoggerMixin implements Storage {
bool _isUserInfoComplete() =>
_userLoginInfo != null && _userLoginInfo.isComplete;

/// Temporary cookie data to save current user cookie.
///
/// We have a principle, that we only have the complete info about a user
/// when the user has logged in. So that during the login progress, we only
/// have part of the user info: It's username, uid or email, and with some
/// cookie that received from server.
///
/// Another principle: Only complete userinfo together with cookie can be
/// saved in storage.
///
/// The problem is, the cookie received MUST be saved somewhere till we
/// successfully login and get complete user info, then the cookie becomes
/// usable and need to save in database.
///
/// So during login progress, save the info here.
/// When auth repo get complete user info, call some method to fulfill user
/// info.
///
/// 1. Try to login, this time incomplete user info passed in, save user info
/// here. Now cookie is empty.
/// 2. During login progress, get cookie (already authed) and save here. Now
/// cookie is ok but user info still incomplete.
/// 3. Auth repo (or auth provider else) get full user info using the cookie
/// we save in step 2, call some method to save full user info.
/// 4. Check user info is complete and save data into storage.
static _CurrentUserCookie _currentUserCookie = _CurrentUserCookie.empty();

/// Save cookie in database.
///
/// This function is private because saving a cookie should only be triggered
Expand All @@ -56,13 +150,10 @@ class CookieData with LoggerMixin implements Storage {
// This shall not happen.
if (!_isUserInfoComplete()) {
info('only save cookie in memory: user info incomplete: $_userLoginInfo');

await getIt.get<StorageProvider>().saveCookieInCache(
username: _userLoginInfo?.username,
uid: _userLoginInfo?.uid,
email: _userLoginInfo?.email,
cookie: _cookieMap,
);
if (_cookieMap.toString().contains('s_gkr8_682f_auth')) {
// Only save cookie when cookie is authed.
_currentUserCookie = _currentUserCookie.copyWith(cookie: _cookieMap);
}
return false;
}
debug('save complete cookie in memory:'
Expand Down Expand Up @@ -94,8 +185,7 @@ class CookieData with LoggerMixin implements Storage {
}

if (_userLoginInfo != null) {
debug('CookieData $hashCode: delete '
'cookie for uid: $_userLoginInfo');
debug('CookieData $hashCode: delete cookie for uid: $_userLoginInfo');
await getIt.get<StorageProvider>().deleteCookieByUserInfo(_userLoginInfo);
}
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
part of 'cookie_data.dart';

/// Temporary data class for recording current user's info and credential.
///
/// Can be used in login progress, where we have incomplete user info and need
/// to update.
final class _CurrentUserCookie {
const _CurrentUserCookie({
this.userLoginInfo,
this.cookie,
});

factory _CurrentUserCookie.empty() => const _CurrentUserCookie();

final UserLoginInfo? userLoginInfo;
final Map<String, String>? cookie;

_CurrentUserCookie copyWith({
UserLoginInfo? userLoginInfo,
Map<String, String>? cookie,
}) =>
_CurrentUserCookie(
userLoginInfo: this.userLoginInfo?.copyWith(
username: userLoginInfo?.username,
uid: userLoginInfo?.uid,
email: userLoginInfo?.email,
) ??
this.userLoginInfo,
cookie: cookie ?? this.cookie,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ final class NetClientProvider with LoggerMixin {
factory NetClientProvider.build({
Dio? dio,
UserLoginInfo? userLoginInfo,
bool startLogin = false,
bool logout = false,
}) {
talker.debug('build cookie with user info: $userLoginInfo');
final d = dio ?? getIt.get<SettingsRepository>().buildDefaultDio();
final cookie =
getIt.get<CookieProvider>().build(userLoginInfo: userLoginInfo);
final cookie = getIt.get<CookieProvider>().build(
userLoginInfo: userLoginInfo,
startLogin: startLogin,
logout: logout,
);
final cookieJar = PersistCookieJar(
ignoreExpires: true,
storage: cookie,
Expand Down
13 changes: 1 addition & 12 deletions lib/shared/providers/storage_provider/storage_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class StorageProvider with LoggerMixin {
_cookieCache[userInfo] = allCookie;

if (!allCookie.toString().contains('s_gkr8_682f_auth')) {
// Only save cookie when cookie is authed.
info('refuse to save not authed cookie');
return;
}
Expand All @@ -132,18 +133,6 @@ class StorageProvider with LoggerMixin {
);
}

/// Only save cookie in memory cache.
Future<void> saveCookieInCache({
required Cookie cookie,
String? username,
int? uid,
String? email,
}) async {
// Update cookie cache.
final userInfo = UserLoginInfo(username: username, uid: uid, email: email);
_cookieCache[userInfo] = cookie;
}

/// Delete cookie for [uid].
Future<bool> deleteCookieByUid(int uid) async {
// Update cookie cache.
Expand Down

0 comments on commit cba5f7d

Please sign in to comment.