refactor(authentication)!: split data sources (cache, session, external)

This commit is contained in:
Hugo Pointcheval 2023-03-08 12:57:24 +01:00
parent 5a7930550d
commit d53e7b80da
Signed by: hugo
GPG Key ID: 3AAC487E131E00BC
15 changed files with 371 additions and 69 deletions

View File

@ -14,4 +14,5 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'remote/authentication_firebase_data_source_impl.dart';
export 'local/local.dart';
export 'remote/remote.dart';

View File

@ -0,0 +1,56 @@
// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_authentication_bloc/src/domain/data_sources/local/authentication_cache_data_source.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
/// {@template authentication_firebase_cache_data_source_impl}
/// A data source that manages the cache strategy.
/// This implementation uses Firebase.
/// {@endtemplate}
class AuthenticationFirebaseCacheDataSourceImpl<Data>
extends AuthenticationCacheDataSource<Data> {
/// {@macro authentication_firebase_cache_data_source_impl}
AuthenticationFirebaseCacheDataSourceImpl({
FirebaseAuth? firebaseAuth,
}) : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance;
final FirebaseAuth _firebaseAuth;
// Already done by Firebase
@override
Future<void> cacheAccount(Account account) => Future.value();
@override
Future<Account?> getCachedAccount() async {
final currentUser = _firebaseAuth.currentUser;
if (currentUser == null) {
return null;
}
final jwt = await currentUser.getIdToken(true);
final currentAccount = AccountModel.fromFirebaseUser(
currentUser,
accessToken: jwt,
);
return currentAccount;
}
// Already done by Firebase
@override
Future<void> removeCachedAccount() => Future.value();
}

View File

@ -0,0 +1,45 @@
// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:async';
import 'package:rxdart/subjects.dart';
import 'package:wyatt_authentication_bloc/src/domain/data_sources/local/authentication_session_data_source.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/auth_session.dart';
/// {@template authentication_session_data_source_impl}
/// A data source that manages the current session.
/// {@endtemplate}
class AuthenticationSessionDataSourceImpl<Data>
extends AuthenticationSessionDataSource<Data> {
/// {@macro authentication_session_data_source_impl}
AuthenticationSessionDataSourceImpl();
final StreamController<AuthenticationSession<Data>> _sessionStream =
BehaviorSubject();
@override
void addSession(AuthenticationSession<Data> session) {
_sessionStream.add(session);
}
@override
Future<AuthenticationSession<Data>> currentSession() => sessionStream().last;
@override
Stream<AuthenticationSession<Data>> sessionStream() =>
_sessionStream.stream.asBroadcastStream();
}

View File

@ -0,0 +1,18 @@
// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'authentication_firebase_cache_data_source_impl.dart';
export 'authentication_session_data_source_impl.dart';

View File

@ -23,55 +23,27 @@ import 'package:wyatt_authentication_bloc/src/core/exceptions/exceptions.dart';
import 'package:wyatt_authentication_bloc/src/data/models/models.dart';
import 'package:wyatt_authentication_bloc/src/domain/data_sources/remote/authentication_remote_data_source.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/auth_session.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/authentication_change_event/authentication_change_event.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
/// {@template authentication_firebase_data_source_impl}
/// Implementation of [AuthenticationRemoteDataSource] using Firebase.
/// {@endtemplate}
class AuthenticationFirebaseDataSourceImpl<Data>
extends AuthenticationRemoteDataSource<Data> {
/// {@macro authentication_firebase_data_source_impl}
AuthenticationFirebaseDataSourceImpl({
FirebaseAuth? firebaseAuth,
GoogleSignIn? googleSignIn,
}) : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance,
_googleSignIn = googleSignIn ?? GoogleSignIn() {
_latestCredentials = BehaviorSubject();
_sessionStream = BehaviorSubject();
// Check for account in memory (persistence)
_checkForCachedAccount();
}
late StreamController<AuthenticationSession<Data>> _sessionStream;
late StreamController<UserCredential?> _latestCredentials;
final FirebaseAuth _firebaseAuth;
final GoogleSignIn _googleSignIn;
Future<void> _checkForCachedAccount() async {
final currentUser = _firebaseAuth.currentUser;
if (currentUser == null) {
_sessionStream.add(
const AuthenticationSession(
latestEvent: UnknownAuthenticationEvent(),
),
);
return;
}
final jwt = await currentUser.getIdToken(true);
final currentAccount = AccountModel.fromFirebaseUser(
currentUser,
accessToken: jwt,
);
_sessionStream.add(
AuthenticationSession.fromEvent(
SignedInFromCacheEvent(account: currentAccount),
),
);
return;
}
Future<Account> _addToCredentialStream(
UserCredential userCredential,
) async {
@ -87,23 +59,6 @@ class AuthenticationFirebaseDataSourceImpl<Data>
return account;
}
// Session related methods ===================================================
/// {@macro add_session}
@override
void addSession(AuthenticationSession<Data> session) {
_sessionStream.add(session);
}
/// {@macro session_stream}
@override
Stream<AuthenticationSession<Data>> sessionStream() =>
_sessionStream.stream.asBroadcastStream();
/// {@macro current_session}
@override
Future<AuthenticationSession<Data>> currentSession() => sessionStream().last;
// SignUp/SignIn methods ====================================================
/// {@macro signup_pwd}

View File

@ -0,0 +1,17 @@
// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'authentication_firebase_data_source_impl.dart';

View File

@ -16,17 +16,22 @@
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/src/core/utils/forms.dart';
import 'package:wyatt_authentication_bloc/src/domain/data_sources/remote/authentication_remote_data_source.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/auth_session.dart';
import 'package:wyatt_authentication_bloc/src/domain/repositories/authentication_repository.dart';
import 'package:wyatt_authentication_bloc/src/domain/data_sources/local/authentication_cache_data_source.dart';
import 'package:wyatt_authentication_bloc/src/domain/data_sources/local/authentication_session_data_source.dart';
import 'package:wyatt_authentication_bloc/src/domain/domain.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
/// {@template authentication_repository_impl}
/// The default implementation of [AuthenticationRepository].
/// {@endtemplate}
class AuthenticationRepositoryImpl<Data extends Object>
extends AuthenticationRepository<Data> {
/// {@macro authentication_repository_impl}
AuthenticationRepositoryImpl({
required this.authenticationRemoteDataSource,
required this.authenticationCacheDataSource,
required this.authenticationSessionDataSource,
FormRepository? formRepository,
// ignore: strict_raw_type
List<FormInput>? extraSignUpInputs,
@ -66,24 +71,57 @@ class AuthenticationRepositoryImpl<Data extends Object>
);
}
/// The remote data source used to perform the authentication process.
final AuthenticationRemoteDataSource<Data> authenticationRemoteDataSource;
/// The cache data source used to cache the current account.
final AuthenticationCacheDataSource<Data> authenticationCacheDataSource;
/// The session data source used to manage the current session.
final AuthenticationSessionDataSource<Data> authenticationSessionDataSource;
late FormRepository _formRepository;
/// {@macro form_repo}
@override
FormRepository get formRepository => _formRepository;
// Cache related methods ====================================================
/// {@macro check_cache_account}
@override
Future<void> checkForCachedAccount() async {
final cachedAccount =
await authenticationCacheDataSource.getCachedAccount();
if (cachedAccount == null) {
addSession(
const AuthenticationSession(
latestEvent: UnknownAuthenticationEvent(),
),
);
return;
}
addSession(
AuthenticationSession.fromEvent(
SignedInFromCacheEvent(account: cachedAccount),
),
);
return;
}
// Session related methods ===================================================
/// {@macro add_session}
@override
void addSession(AuthenticationSession<Data> session) =>
authenticationRemoteDataSource.addSession(session);
authenticationSessionDataSource.addSession(session);
/// {@macro session_stream}
@override
Stream<AuthenticationSession<Data>> sessionStream() =>
authenticationRemoteDataSource.sessionStream();
authenticationSessionDataSource.sessionStream();
/// {@macro current_session}
@override
@ -91,7 +129,9 @@ class AuthenticationRepositoryImpl<Data extends Object>
Result.tryCatchAsync<AuthenticationSession<Data>, AppException,
AppException>(
() async {
final session = await authenticationRemoteDataSource.currentSession();
final session =
await authenticationSessionDataSource.currentSession();
return session;
},
(error) => error,

View File

@ -14,4 +14,5 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'remote/authentication_remote_data_source.dart';
export 'local/local.dart';
export 'remote/remote.dart';

View File

@ -0,0 +1,40 @@
// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart';
/// {@template authentication_cache_data_source}
/// A data source that manages the cache strategy.
/// {@endtemplate}
abstract class AuthenticationCacheDataSource<Data> extends BaseLocalDataSource {
/// {@macro authentication_cache_data_source}
const AuthenticationCacheDataSource();
/// Returns the cached account if it exists.
Future<Account?> getCachedAccount();
/// Adds the current account to the cache.
///
/// If an account is already cached, it will be replaced.
Future<void> cacheAccount(Account account);
/// Removes the current account from the cache.
///
/// The cache will be empty after this operation.
/// If no account is cached, nothing will happen.
Future<void> removeCachedAccount();
}

View File

@ -0,0 +1,36 @@
// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
/// {@template authentication_session_data_source}
/// A data source that manages the current session.
/// {@endtemplate}
abstract class AuthenticationSessionDataSource<Data>
extends BaseLocalDataSource {
/// {@macro authentication_session_data_source}
const AuthenticationSessionDataSource();
/// Adds a new session to the data source.
void addSession(AuthenticationSession<Data> session);
/// Returns a stream of sessions.
Stream<AuthenticationSession<Data>> sessionStream();
/// Returns the current session.
Future<AuthenticationSession<Data>> currentSession();
}

View File

@ -0,0 +1,18 @@
// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'authentication_cache_data_source.dart';
export 'authentication_session_data_source.dart';

View File

@ -16,49 +16,71 @@
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/auth_session.dart';
/// Is responsible for abstracting the provenance of the data.
/// {@template authentication_remote_data_source}
/// A remote data source for authentication.
/// It is responsible for all the external communication with the authentication
/// providers.
/// {@endtemplate}
abstract class AuthenticationRemoteDataSource<Data>
extends BaseRemoteDataSource {
// Session related methods ===================================================
void addSession(AuthenticationSession<Data> session);
Stream<AuthenticationSession<Data>> sessionStream();
Future<AuthenticationSession<Data>> currentSession();
/// {@macro authentication_remote_data_source}
const AuthenticationRemoteDataSource();
// SignUp/SignIn methods ====================================================
/// Sign up with email and password.
Future<Account> signUpWithEmailAndPassword({
required String email,
required String password,
});
/// Sign in with email and password.
Future<Account> signInWithEmailAndPassword({
required String email,
required String password,
});
/// Sign in anonymously.
Future<Account> signInAnonymously();
/// Sign in with Google.
Future<Account> signInWithGoogle();
/// Sign out.
Future<void> signOut();
// Account management methods ===============================================
// Future<void> linkCurrentUserWith(AuthenticationProvider anotherProvider);
/// Refresh the current account.
Future<Account> refresh();
/// Reauthenticate the current account.
Future<Account> reauthenticate();
/// Update the current account's email.
Future<Account> updateEmail({required String email});
/// Update the current account's password.
Future<Account> updatePassword({required String password});
/// Delete the current account.
Future<void> delete();
// Email related stuff ======================================================
/// Send an email verification.
Future<void> sendEmailVerification();
/// Send a password reset email.
Future<void> sendPasswordResetEmail({required String email});
/// Confirm password reset.
Future<void> confirmPasswordReset({
required String code,
required String newPassword,
});
/// Verify password reset code.
Future<bool> verifyPasswordResetCode({required String code});
}

View File

@ -0,0 +1,17 @@
// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'authentication_remote_data_source.dart';

View File

@ -19,13 +19,25 @@ import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/auth_session.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
/// {@template auth_repo}
/// Authentication repository interface.
/// {@endtemplate}
abstract class AuthenticationRepository<Data> extends BaseRepository {
/// {@template form_repo}
/// Form repository used in different authentication cubits/blocs
/// {@endtemplate}
FormRepository get formRepository;
// Stream related methods ===================================================
// Cache related methods ====================================================
/// {@template check_cache_account}
/// Checks if there is a cached account.
/// And if there is, it will sign in the user automatically by
/// emitting an event.
/// {@endtemplate}
Future<void> checkForCachedAccount();
// Session related methods ===================================================
/// {@template add_session}
/// Add a new authentication event.

View File

@ -27,27 +27,45 @@ import 'package:wyatt_type_utils/wyatt_type_utils.dart';
part 'authentication_state.dart';
/// {@template authentication_cubit}
/// Abstract authentication cubit class needs to be implemented in application.
///
/// This cubit is in charge of managing the global authentication state of
/// the application.
///
/// Its here you can override every callbacks and add your custom logic.
/// {@endtemplate}
abstract class AuthenticationCubit<Data>
extends Cubit<AuthenticationState<Data>> {
/// {@macro authentication_cubit}
AuthenticationCubit({
required this.authenticationRepository,
}) : super(const AuthenticationState.unknown()) {
_listenForAuthenticationChanges();
_init();
}
/// The authentication repository.
final AuthenticationRepository<Data> authenticationRepository;
/// The latest session.
AuthenticationSession<Data>? _latestSession;
/// Method that is called when the cubit is initialized.
Future<void> _init() async {
/// Setup listeners.
_listenForAuthenticationChanges();
/// Check if there is a cached account.
await authenticationRepository.checkForCachedAccount();
}
void _listenForAuthenticationChanges() {
authenticationRepository.sessionStream().asyncMap((session) async {
final event = session.latestEvent;
/// If the session is signed in from cache.
if (event is SignedInFromCacheEvent) {
/// Call the custom routine.
final customRoutineResult = await onSignInFromCache(session);
if (customRoutineResult.isOk) {
@ -64,12 +82,18 @@ abstract class AuthenticationCubit<Data>
}
return session;
}).listen((session) async {
/// Save the latest session.
_latestSession = session;
/// If there is an account: emit authenticated state.
if (session.account != null) {
emit(AuthenticationState<Data>.authenticated(session));
return;
}
/// If there is no account: emit unauthenticated state.
emit(AuthenticationState<Data>.unauthenticated());
return;
});
}