Skip to content

Compass App: WIP Auth logic refactor #2394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 16 additions & 19 deletions compass_app/app/lib/config/dependencies.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import 'package:provider/single_child_widget.dart';
import 'package:provider/provider.dart';

import '../domain/components/auth/auth_login_component.dart';
import '../domain/components/auth/auth_logout_component.dart';
import '../data/repositories/auth/auth_repository.dart';
import '../data/repositories/auth/auth_repository_dev.dart';
import '../data/repositories/auth/auth_repository_remote.dart';
import '../data/services/auth_api_client.dart';
import '../data/services/shared_preferences_service.dart';
import '../data/repositories/activity/activity_repository.dart';
import '../data/repositories/activity/activity_repository_local.dart';
import '../data/repositories/activity/activity_repository_remote.dart';
import '../data/repositories/auth/auth_token_repository.dart';
import '../data/repositories/auth/auth_token_repository_dev.dart';
import '../data/repositories/auth/auth_token_repository_shared_prefs.dart';
import '../data/repositories/continent/continent_repository.dart';
import '../data/repositories/continent/continent_repository_local.dart';
import '../data/repositories/continent/continent_repository_remote.dart';
Expand All @@ -34,30 +34,27 @@ List<SingleChildWidget> _sharedProviders = [
lazy: true,
create: (context) => BookingShareComponent.withSharePlus(),
),
Provider(
lazy: true,
create: (context) => AuthLogoutComponent(
authTokenRepository: context.read(),
itineraryConfigRepository: context.read(),
),
),
];

/// Configure dependencies for remote data.
/// This dependency list uses repositories that connect to a remote server.
List<SingleChildWidget> get providersRemote {
return [
ChangeNotifierProvider.value(
value: AuthTokenRepositorySharedPrefs() as AuthTokenRepository,
Provider(
create: (context) => AuthApiClient(),
),
Provider(
create: (context) => ApiClient(authTokenRepository: context.read()),
create: (context) => ApiClient(),
),
Provider(
create: (context) => AuthLoginComponent(
authTokenRepository: context.read(),
create: (context) => SharedPreferencesService(),
),
ChangeNotifierProvider(
create: (context) => AuthRepositoryRemote(
authApiClient: context.read(),
apiClient: context.read(),
),
sharedPreferencesService: context.read(),
) as AuthRepository,
),
Provider(
create: (context) => DestinationRepositoryRemote(
Expand Down Expand Up @@ -87,7 +84,7 @@ List<SingleChildWidget> get providersRemote {
List<SingleChildWidget> get providersLocal {
return [
ChangeNotifierProvider.value(
value: AuthTokenRepositoryDev() as AuthTokenRepository,
value: AuthRepositoryDev() as AuthRepository,
),
Provider.value(
value: DestinationRepositoryLocal() as DestinationRepository,
Expand Down
18 changes: 18 additions & 0 deletions compass_app/app/lib/data/repositories/auth/auth_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:flutter/foundation.dart';

import '../../../utils/result.dart';

abstract class AuthRepository extends ChangeNotifier {
/// Returns true when the user is logged in
/// Returns [Future] because it will load a stored auth state the first time.
Future<bool> get isAuthenticated;

/// Perform login
Future<Result<void>> login({
required String email,
required String password,
});

/// Perform logout
Future<Result<void>> logout();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import '../../../utils/result.dart';
import 'auth_repository.dart';

class AuthRepositoryDev extends AuthRepository {
/// User is always authenticated in dev scenarios
@override
Future<bool> get isAuthenticated => Future.value(true);

/// Login is always successful in dev scenarios
@override
Future<Result<void>> login({
required String email,
required String password,
}) async {
return Result.ok(null);
}

/// Logout is always successful in dev scenarios
@override
Future<Result<void>> logout() async {
return Result.ok(null);
}
}
107 changes: 107 additions & 0 deletions compass_app/app/lib/data/repositories/auth/auth_repository_remote.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import 'package:compass_model/model.dart';
import 'package:logging/logging.dart';

import '../../../utils/result.dart';
import '../../services/api_client.dart';
import '../../services/auth_api_client.dart';
import '../../services/shared_preferences_service.dart';
import 'auth_repository.dart';

class AuthRepositoryRemote extends AuthRepository {
AuthRepositoryRemote({
required ApiClient apiClient,
required AuthApiClient authApiClient,
required SharedPreferencesService sharedPreferencesService,
}) : _apiClient = apiClient,
_authApiClient = authApiClient,
_sharedPreferencesService = sharedPreferencesService {
_apiClient.authHeaderProvider = _authHeaderProvider;
}

final AuthApiClient _authApiClient;
final ApiClient _apiClient;
final SharedPreferencesService _sharedPreferencesService;

bool? _isAuthenticated;
String? _authToken;
final _log = Logger('AuthRepositoryRemote');

/// Fetch token from shared preferences
Future<void> _fetch() async {
final result = await _sharedPreferencesService.fetchToken();
switch (result) {
case Ok<String?>():
_authToken = result.value;
_isAuthenticated = result.value != null;
case Error<String?>():
_log.severe(
'Failed to fech Token from SharedPreferences',
result.error,
);
}
}

@override
Future<bool> get isAuthenticated async {
// Status is cached
if (_isAuthenticated != null) {
return _isAuthenticated!;
}
// No status cached, fetch from storage
await _fetch();
return _isAuthenticated ?? false;
}

@override
Future<Result<void>> login({
required String email,
required String password,
}) async {
try {
final result = await _authApiClient.login(
LoginRequest(
email: email,
password: password,
),
);
switch (result) {
case Ok<LoginResponse>():
_log.info('User logged int');
// Set auth status
_isAuthenticated = true;
_authToken = result.value.token;
// Store in Shared preferences
return await _sharedPreferencesService.saveToken(result.value.token);
case Error<LoginResponse>():
_log.warning('Error logging in: ${result.error}');
return Result.error(result.error);
}
} finally {
notifyListeners();
}
}

@override
Future<Result<void>> logout() async {
_log.info('User logged out');
try {
// Clear stored auth token
final result = await _sharedPreferencesService.saveToken(null);
if (result is Error<void>) {
_log.severe('Failed to clear stored auth token');
}

// Clear token in ApiClient
_authToken = null;

// Clear authenticated status
_isAuthenticated = false;
return result;
} finally {
notifyListeners();
}
}

String? _authHeaderProvider() =>
_authToken != null ? 'Bearer $_authToken' : null;
}

This file was deleted.

This file was deleted.

40 changes: 10 additions & 30 deletions compass_app/app/lib/data/services/api_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,24 @@ import 'dart:io';
import 'package:compass_model/model.dart';

import '../../utils/result.dart';
import '../repositories/auth/auth_token_repository.dart';

typedef AuthTokenProvider = Future<String?> Function();
/// Adds the `Authentication` header to a header configuration.
typedef AuthHeaderProvider = String? Function();

// TODO: Configurable baseurl/host/port
class ApiClient {
ApiClient({
required AuthTokenRepository authTokenRepository,
}) : _authTokenRepository = authTokenRepository;
ApiClient();

/// Provides the auth token to be used in the request
final AuthTokenRepository _authTokenRepository;
AuthHeaderProvider? _authHeaderProvider;

Future<void> _authHeader(HttpHeaders headers) async {
final result = await _authTokenRepository.getToken();
if (result is Ok<String?>) {
if (result.value != null) {
headers.add(HttpHeaders.authorizationHeader, 'Bearer ${result.value}');
}
}
set authHeaderProvider(AuthHeaderProvider authHeaderProvider) {
_authHeaderProvider = authHeaderProvider;
}

Future<Result<LoginResponse>> login(LoginRequest loginRequest) async {
final client = HttpClient();
try {
final request = await client.post('localhost', 8080, '/login');
request.write(jsonEncode(loginRequest));
final response = await request.close();
if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join();
return Result.ok(LoginResponse.fromJson(jsonDecode(stringData)));
} else {
return Result.error(const HttpException("Login error"));
}
} on Exception catch (error) {
return Result.error(error);
} finally {
client.close();
Future<void> _authHeader(HttpHeaders headers) async {
final header = _authHeaderProvider?.call();
if (header != null) {
headers.add(HttpHeaders.authorizationHeader, header);
}
}

Expand Down
28 changes: 28 additions & 0 deletions compass_app/app/lib/data/services/auth_api_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// TODO: Configurable baseurl/host/port
import 'dart:convert';
import 'dart:io';

import 'package:compass_model/model.dart';

import '../../utils/result.dart';

class AuthApiClient {
Future<Result<LoginResponse>> login(LoginRequest loginRequest) async {
final client = HttpClient();
try {
final request = await client.post('localhost', 8080, '/login');
request.write(jsonEncode(loginRequest));
final response = await request.close();
if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join();
return Result.ok(LoginResponse.fromJson(jsonDecode(stringData)));
} else {
return Result.error(const HttpException("Login error"));
}
} on Exception catch (error) {
return Result.error(error);
} finally {
client.close();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,36 @@
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../../../utils/result.dart';
import 'auth_token_repository.dart';
import '../../utils/result.dart';

/// [AuthTokenRepository] that stores the token in Shared Preferences.
/// Provided for demo purposes, consider using a secure store instead.
class AuthTokenRepositorySharedPrefs extends AuthTokenRepository {
class SharedPreferencesService {
static const _tokenKey = 'TOKEN';
String? cachedToken;

@override
Future<Result<String?>> getToken() async {
if (cachedToken != null) return Result.ok(cachedToken);
final _log = Logger('SharedPreferencesService');

Future<Result<String?>> fetchToken() async {
try {
final sharedPreferences = await SharedPreferences.getInstance();
final token = sharedPreferences.getString(_tokenKey);
return Result.ok(token);
_log.finer('Got token from SharedPreferences');
return Result.ok(sharedPreferences.getString(_tokenKey));
} on Exception catch (e) {
_log.warning('Failed to get token', e);
return Result.error(e);
}
}

@override
Future<Result<void>> saveToken(String? token) async {
try {
final sharedPreferences = await SharedPreferences.getInstance();
if (token == null) {
_log.finer('Removed token');
await sharedPreferences.remove(_tokenKey);
} else {
_log.finer('Replaced token');
await sharedPreferences.setString(_tokenKey, token);
}
cachedToken = token;
notifyListeners();
return Result.ok(null);
} on Exception catch (e) {
_log.warning('Failed to set token', e);
return Result.error(e);
}
}
Expand Down
Loading