mirror of https://github.com/flutter/samples.git
Compass App: Basic auth (#2385)
This PR introduces basic auth implementation between the app and the server as part of the architectural example. This PR is a big bigger than the previous ones so I hope this explanation helps: ### Server implementation The server introduces a new endpoint `/login` to perform login requests, which accepts login requests defined in the `LoginRequest` data class, with an email and password. The login process "simulates" checking on the email and password and responds with a "token" and user ID, defined by the `LoginResponse` data class. This is a simple hard-coded check and in any way a guide on how to implement authentication, just a way to demonstrate an architectural example. The server also implements a middleware in `server/lib/middleware/auth.dart`. This checks that the requests between the app and the server carry a valid authorization token in the headers, responding with an unauthorized error otherwise. ### App implementation The app introduces the following new parts: - `AuthTokenRepository`: In charge of storing the auth token. - `AuthLoginComponent`: In charge of performing login. - `AuthLogoutComponent`: In charge of performing logout. - `LoginScreen` with `LoginViewModel`: Displays the login screen. - `LogoutButton` with `LogoutViewModel`: Displays a logout button. The `AuthTokenRepository` acts as the source of truth to decide if the user is logged in or not. If the repository contains a token, it means the user is logged in, otherwise if the token is null, it means that the user is logged out. This repository is also a `ChangeNotifier`, which allows listening to change in it. The `GoRouter` has been modified so it listens to changes in the `AuthTokenRepository` using the `refreshListenable` property. It also implements a `redirect`, so if the token is set to `null` in the repository, the router will redirect users automatically to the login screen. This follows the example found in https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/redirection.dart On app start, `GoRouter` checks the `AuthTokenRepository`, if a token exists the user stays in `/`, if not, the user is redirected to `/login`. The `ApiClient` has also been modified, so it reads the stored token from the repository when performing network calls, and adds it to the auth headers. The two new components implement basic login and logout functionality. The `AuthLoginComponent` will send the request using the `ApiClient`, and then store the token from the response. The `AuthLogoutComponent` clears the stored token from the repository, and as well clears any existing itinerary configuration, effectively cleaning the app state. Performing logout redirects the user to the login screen, as explained. The `LoginScreen` uses the `AuthLoginComponent` internally, it displays two text fields and a login button, plus the application logo on top. A successful login redirects the user to `/`. The `LogoutButton` replaces the home button at the `/`, and on tap it will perform logout using the `AuthLogoutComponent`. **Development target app** The development target app works slightly different compared to the staging build. In this case, the `AuthTokenRepository` always contains a fake token, so the app believes it is always logged in. Auth is only used in the staging build when the server is involved. ## Screenshots <details> <summary>Screenshots</summary> The logout button in the top right corner: ![Screenshot from 2024-08-14 15-28-54](https://github.com/user-attachments/assets/1c5a37dc-9fa1-4950-917e-0c7272896780) The login screen: ![Screenshot from 2024-08-14 15-28-12](https://github.com/user-attachments/assets/3c26ccc2-8e3b-42d2-a230-d31048af6960) </details> ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-devrel channel on [Discord]. <!-- Links --> [Flutter Style Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md [Contributors Guide]: https://github.com/flutter/samples/blob/main/CONTRIBUTING.mdpull/2389/head
parent
0c88289339
commit
e0f25da42b
After Width: | Height: | Size: 9.9 KiB |
@ -0,0 +1,21 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
|
||||
/// Repository to save and get auth token.
|
||||
/// Notifies listeners when the token changes e.g. user logs out.
|
||||
abstract class AuthTokenRepository extends ChangeNotifier {
|
||||
/// Get the token.
|
||||
/// If the value is null, usually means that the user is logged out.
|
||||
Future<Result<String?>> getToken();
|
||||
|
||||
/// Store the token.
|
||||
/// Will notifiy listeners.
|
||||
Future<Result<void>> saveToken(String? token);
|
||||
|
||||
/// Returns true when the token exists, otherwise false.
|
||||
Future<bool> hasToken() async {
|
||||
final result = await getToken();
|
||||
return result is Ok<String?> && result.value != null;
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import '../../../utils/result.dart';
|
||||
import 'auth_token_repository.dart';
|
||||
|
||||
/// Development [AuthTokenRepository] that always returns a fake token
|
||||
class AuthTokenRepositoryDev extends AuthTokenRepository {
|
||||
@override
|
||||
Future<Result<String?>> getToken() async {
|
||||
return Result.ok('token');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> saveToken(String? token) async {
|
||||
notifyListeners();
|
||||
return Result.ok(null);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
import 'auth_token_repository.dart';
|
||||
|
||||
/// [AuthTokenRepository] that stores the token in Shared Preferences.
|
||||
/// Provided for demo purposes, consider using a secure store instead.
|
||||
class AuthTokenRepositorySharedPrefs extends AuthTokenRepository {
|
||||
static const _tokenKey = 'TOKEN';
|
||||
String? cachedToken;
|
||||
|
||||
@override
|
||||
Future<Result<String?>> getToken() async {
|
||||
if (cachedToken != null) return Result.ok(cachedToken);
|
||||
|
||||
try {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final token = sharedPreferences.getString(_tokenKey);
|
||||
return Result.ok(token);
|
||||
} on Exception catch (e) {
|
||||
return Result.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> saveToken(String? token) async {
|
||||
try {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
if (token == null) {
|
||||
await sharedPreferences.remove(_tokenKey);
|
||||
} else {
|
||||
await sharedPreferences.setString(_tokenKey, token);
|
||||
}
|
||||
cachedToken = token;
|
||||
notifyListeners();
|
||||
return Result.ok(null);
|
||||
} on Exception catch (e) {
|
||||
return Result.error(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
import '../../../data/repositories/auth/auth_token_repository.dart';
|
||||
import '../../../data/services/api_client.dart';
|
||||
|
||||
/// Performs user login.
|
||||
class AuthLoginComponent {
|
||||
AuthLoginComponent({
|
||||
required AuthTokenRepository authTokenRepository,
|
||||
required ApiClient apiClient,
|
||||
}) : _authTokenRepository = authTokenRepository,
|
||||
_apiClient = apiClient;
|
||||
|
||||
final AuthTokenRepository _authTokenRepository;
|
||||
final ApiClient _apiClient;
|
||||
final _log = Logger('AuthLoginComponent');
|
||||
|
||||
/// Login with username and password.
|
||||
/// Performs login with the server and stores the obtained auth token.
|
||||
Future<Result<void>> login({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
final result = await _apiClient.login(
|
||||
LoginRequest(
|
||||
email: email,
|
||||
password: password,
|
||||
),
|
||||
);
|
||||
switch (result) {
|
||||
case Ok<LoginResponse>():
|
||||
_log.info('User logged int');
|
||||
return await _authTokenRepository.saveToken(result.value.token);
|
||||
case Error<LoginResponse>():
|
||||
_log.warning('Error logging in: ${result.error}');
|
||||
return Result.error(result.error);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
import '../../../data/repositories/auth/auth_token_repository.dart';
|
||||
import '../../../data/repositories/itinerary_config/itinerary_config_repository.dart';
|
||||
|
||||
/// Performs user logout.
|
||||
class AuthLogoutComponent {
|
||||
AuthLogoutComponent({
|
||||
required AuthTokenRepository authTokenRepository,
|
||||
required ItineraryConfigRepository itineraryConfigRepository,
|
||||
}) : _authTokenRepository = authTokenRepository,
|
||||
_itineraryConfigRepository = itineraryConfigRepository;
|
||||
|
||||
final AuthTokenRepository _authTokenRepository;
|
||||
final ItineraryConfigRepository _itineraryConfigRepository;
|
||||
final _log = Logger('AuthLogoutComponent');
|
||||
|
||||
/// Perform user logout.
|
||||
///
|
||||
/// 1. Clears the stored ItineraryConfig.
|
||||
/// 2. Clears the stored auth token.
|
||||
///
|
||||
/// GoRouter will automatically redirect the user to /login
|
||||
Future<Result<void>> logout() async {
|
||||
_log.info('User logged out');
|
||||
|
||||
// Clear stored ItineraryConfig
|
||||
var result = await _itineraryConfigRepository
|
||||
.setItineraryConfig(const ItineraryConfig());
|
||||
if (result is Error<void>) {
|
||||
_log.severe('Failed to clear stored ItineraryConfig');
|
||||
return result;
|
||||
}
|
||||
|
||||
// Clear stored auth token
|
||||
result = await _authTokenRepository.saveToken(null);
|
||||
if (result is Error<void>) {
|
||||
_log.severe('Failed to clear stored auth token');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../../../domain/components/auth/auth_login_component.dart';
|
||||
import '../../../../utils/command.dart';
|
||||
import '../../../../utils/result.dart';
|
||||
|
||||
class LoginViewModel {
|
||||
LoginViewModel({
|
||||
required AuthLoginComponent authLoginComponent,
|
||||
}) : _authLoginComponent = authLoginComponent {
|
||||
login = Command1<void, (String email, String password)>(_login);
|
||||
}
|
||||
|
||||
final AuthLoginComponent _authLoginComponent;
|
||||
final _log = Logger('LoginViewModel');
|
||||
|
||||
late Command1 login;
|
||||
|
||||
Future<Result<void>> _login((String, String) credentials) async {
|
||||
final (email, password) = credentials;
|
||||
final result = await _authLoginComponent.login(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
if (result is Error<void>) {
|
||||
_log.warning('Login failed! ${result.error}');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../core/localization/applocalization.dart';
|
||||
import '../../../core/themes/dimens.dart';
|
||||
import '../view_models/login_viewmodel.dart';
|
||||
import 'tilted_cards.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final LoginViewModel viewModel;
|
||||
|
||||
@override
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
final TextEditingController _email =
|
||||
TextEditingController(text: 'email@example.com');
|
||||
final TextEditingController _password =
|
||||
TextEditingController(text: 'password');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.viewModel.login.addListener(_onResult);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant LoginScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
oldWidget.viewModel.login.removeListener(_onResult);
|
||||
widget.viewModel.login.addListener(_onResult);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.viewModel.login.removeListener(_onResult);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const TiltedCards(),
|
||||
Padding(
|
||||
padding: Dimens.of(context).edgeInsetsScreenSymmetric,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _email,
|
||||
),
|
||||
const SizedBox(height: Dimens.paddingVertical),
|
||||
TextField(
|
||||
controller: _password,
|
||||
obscureText: true,
|
||||
),
|
||||
const SizedBox(height: Dimens.paddingVertical),
|
||||
ListenableBuilder(
|
||||
listenable: widget.viewModel.login,
|
||||
builder: (context, _) {
|
||||
return FilledButton(
|
||||
onPressed: () {
|
||||
widget.viewModel.login
|
||||
.execute((_email.value.text, _password.value.text));
|
||||
},
|
||||
child: Text(AppLocalization.of(context).login),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onResult() {
|
||||
if (widget.viewModel.login.completed) {
|
||||
widget.viewModel.login.clearResult();
|
||||
context.go('/');
|
||||
}
|
||||
|
||||
if (widget.viewModel.login.error) {
|
||||
widget.viewModel.login.clearResult();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalization.of(context).errorWhileLogin),
|
||||
action: SnackBarAction(
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: () => widget.viewModel.login
|
||||
.execute((_email.value.text, _password.value.text)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class TiltedCards extends StatelessWidget {
|
||||
const TiltedCards({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
child: const AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
child: _Card(
|
||||
imageUrl: 'https://rstr.in/google/tripedia/g2i0BsYPKW-',
|
||||
width: 200,
|
||||
height: 273,
|
||||
tilt: -3.83 / 360,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
child: _Card(
|
||||
imageUrl: 'https://rstr.in/google/tripedia/980sqNgaDRK',
|
||||
width: 180,
|
||||
height: 230,
|
||||
tilt: 3.46 / 360,
|
||||
),
|
||||
),
|
||||
_Card(
|
||||
imageUrl: 'https://rstr.in/google/tripedia/pHfPmf3o5NU',
|
||||
width: 225,
|
||||
height: 322,
|
||||
tilt: 0,
|
||||
showTitle: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Card extends StatelessWidget {
|
||||
const _Card({
|
||||
required this.imageUrl,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.tilt,
|
||||
this.showTitle = false,
|
||||
});
|
||||
|
||||
final double tilt;
|
||||
final double width;
|
||||
final double height;
|
||||
final String imageUrl;
|
||||
final bool showTitle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RotationTransition(
|
||||
turns: AlwaysStoppedAnimation(tilt),
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
color: showTitle ? Colors.black.withOpacity(0.5) : null,
|
||||
colorBlendMode: showTitle ? BlendMode.darken : null,
|
||||
),
|
||||
if (showTitle) Center(child: SvgPicture.asset('assets/logo.svg')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import '../../../../domain/components/auth/auth_logout_component.dart';
|
||||
import '../../../../utils/command.dart';
|
||||
import '../../../../utils/result.dart';
|
||||
|
||||
class LogoutViewModel {
|
||||
LogoutViewModel({
|
||||
required AuthLogoutComponent authLogoutComponent,
|
||||
}) : _authLogoutComponent = authLogoutComponent {
|
||||
logout = Command0(_logout);
|
||||
}
|
||||
final AuthLogoutComponent _authLogoutComponent;
|
||||
late Command0 logout;
|
||||
|
||||
Future<Result> _logout() => _authLogoutComponent.logout();
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/localization/applocalization.dart';
|
||||
import '../../../core/themes/colors.dart';
|
||||
import '../view_models/logout_viewmodel.dart';
|
||||
|
||||
class LogoutButton extends StatefulWidget {
|
||||
const LogoutButton({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final LogoutViewModel viewModel;
|
||||
|
||||
@override
|
||||
State<LogoutButton> createState() => _LogoutButtonState();
|
||||
}
|
||||
|
||||
class _LogoutButtonState extends State<LogoutButton> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.viewModel.logout.addListener(_onResult);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant LogoutButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
oldWidget.viewModel.logout.removeListener(_onResult);
|
||||
widget.viewModel.logout.addListener(_onResult);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.viewModel.logout.removeListener(_onResult);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 40.0,
|
||||
width: 40.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.grey1),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: InkResponse(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
onTap: () {
|
||||
widget.viewModel.logout.execute();
|
||||
},
|
||||
child: Center(
|
||||
child: Icon(
|
||||
size: 24.0,
|
||||
Icons.logout,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onResult() {
|
||||
// We do not need to navigate to `/login` on logout,
|
||||
// it is done automatically by GoRouter.
|
||||
|
||||
if (widget.viewModel.logout.error) {
|
||||
widget.viewModel.logout.clearResult();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalization.of(context).errorWhileLogout),
|
||||
action: SnackBarAction(
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: widget.viewModel.logout.execute,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
import 'package:compass_app/domain/components/auth/auth_login_component.dart';
|
||||
import 'package:compass_app/data/services/api_client.dart';
|
||||
import 'package:compass_app/utils/result.dart';
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../../testing/fakes/repositories/fake_auth_token_repository.dart';
|
||||
import '../../../../testing/fakes/services/fake_api_client.dart';
|
||||
|
||||
void main() {
|
||||
group('AuthLoginComponent test', () {
|
||||
late AuthLoginComponent authLoginComponent;
|
||||
late FakeAuthTokenRepository authTokenRepository;
|
||||
late ApiClient apiClient;
|
||||
|
||||
setUp(() {
|
||||
authTokenRepository = FakeAuthTokenRepository();
|
||||
apiClient = _ApiClient();
|
||||
authLoginComponent = AuthLoginComponent(
|
||||
authTokenRepository: authTokenRepository,
|
||||
apiClient: apiClient,
|
||||
);
|
||||
});
|
||||
|
||||
test('should perform login', () async {
|
||||
// Pass valid email and password
|
||||
final result = await authLoginComponent.login(
|
||||
email: 'EMAIL',
|
||||
password: 'PASSWORD',
|
||||
);
|
||||
// Got good response
|
||||
expect(result, isA<Ok<void>>());
|
||||
expect(authTokenRepository.token, 'TOKEN');
|
||||
});
|
||||
|
||||
test('should fail to login', () async {
|
||||
// Pass wrong email and password
|
||||
final result = await authLoginComponent.login(
|
||||
email: 'WRONG',
|
||||
password: 'WRONG',
|
||||
);
|
||||
// Got bad response
|
||||
expect(result, isA<Error<void>>());
|
||||
expect(result.asError.error.toString(), 'Exception: ERROR');
|
||||
expect(authTokenRepository.token, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _ApiClient extends FakeApiClient {
|
||||
@override
|
||||
Future<Result<LoginResponse>> login(LoginRequest loginRequest) async {
|
||||
if (loginRequest.email == 'EMAIL' && loginRequest.password == 'PASSWORD') {
|
||||
return Result.ok(const LoginResponse(token: 'TOKEN', userId: '1234'));
|
||||
} else {
|
||||
return Result.error(Exception('ERROR'));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import 'package:compass_app/domain/components/auth/auth_logout_component.dart';
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../../testing/fakes/repositories/fake_auth_token_repository.dart';
|
||||
import '../../../../testing/fakes/repositories/fake_itinerary_config_repository.dart';
|
||||
|
||||
void main() {
|
||||
group('AuthLogoutComponent test', () {
|
||||
late AuthLogoutComponent authLogoutComponent;
|
||||
late FakeAuthTokenRepository authTokenRepository;
|
||||
late FakeItineraryConfigRepository itineraryConfigRepository;
|
||||
|
||||
setUp(() {
|
||||
authTokenRepository = FakeAuthTokenRepository();
|
||||
itineraryConfigRepository = FakeItineraryConfigRepository(
|
||||
itineraryConfig: const ItineraryConfig(continent: 'CONTINENT'),
|
||||
);
|
||||
authLogoutComponent = AuthLogoutComponent(
|
||||
authTokenRepository: authTokenRepository,
|
||||
itineraryConfigRepository: itineraryConfigRepository,
|
||||
);
|
||||
});
|
||||
|
||||
test('should perform logout', () async {
|
||||
await authLogoutComponent.logout();
|
||||
// Token should be removed
|
||||
expect(authTokenRepository.token, null);
|
||||
// Itinerary config should be cleared
|
||||
expect(
|
||||
itineraryConfigRepository.itineraryConfig,
|
||||
const ItineraryConfig(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
import 'package:compass_app/ui/booking/components/booking_create_component.dart';
|
||||
import 'package:compass_app/domain/components/booking/booking_create_component.dart';
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../testing/fakes/repositories/fake_activities_repository.dart';
|
||||
import '../../../testing/fakes/repositories/fake_destination_repository.dart';
|
||||
import '../../../testing/models/activity.dart';
|
||||
import '../../../testing/models/booking.dart';
|
||||
import '../../../testing/models/destination.dart';
|
||||
import '../../../../testing/fakes/repositories/fake_activities_repository.dart';
|
||||
import '../../../../testing/fakes/repositories/fake_destination_repository.dart';
|
||||
import '../../../../testing/models/activity.dart';
|
||||
import '../../../../testing/models/booking.dart';
|
||||
import '../../../../testing/models/destination.dart';
|
||||
|
||||
void main() {
|
||||
group('BookingCreateComponent tests', () {
|
@ -1,9 +1,9 @@
|
||||
import 'package:compass_app/ui/booking/components/booking_share_component.dart';
|
||||
import 'package:compass_app/domain/components/booking/booking_share_component.dart';
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../testing/models/activity.dart';
|
||||
import '../../../testing/models/destination.dart';
|
||||
import '../../../../testing/models/activity.dart';
|
||||
import '../../../../testing/models/destination.dart';
|
||||
|
||||
void main() {
|
||||
group('BookingShareComponent tests', () {
|
@ -0,0 +1,64 @@
|
||||
import 'package:compass_app/domain/components/auth/auth_login_component.dart';
|
||||
import 'package:compass_app/ui/auth/login/view_models/login_viewmodel.dart';
|
||||
import 'package:compass_app/ui/auth/login/widgets/login_screen.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:mocktail_image_network/mocktail_image_network.dart';
|
||||
|
||||
import '../../../testing/app.dart';
|
||||
import '../../../testing/fakes/repositories/fake_auth_token_repository.dart';
|
||||
import '../../../testing/fakes/services/fake_api_client.dart';
|
||||
import '../../../testing/mocks.dart';
|
||||
|
||||
void main() {
|
||||
group('LoginScreen test', () {
|
||||
late LoginViewModel viewModel;
|
||||
late MockGoRouter goRouter;
|
||||
late FakeAuthTokenRepository fakeAuthTokenRepository;
|
||||
|
||||
setUp(() {
|
||||
fakeAuthTokenRepository = FakeAuthTokenRepository();
|
||||
viewModel = LoginViewModel(
|
||||
authLoginComponent: AuthLoginComponent(
|
||||
authTokenRepository: fakeAuthTokenRepository,
|
||||
apiClient: FakeApiClient(),
|
||||
),
|
||||
);
|
||||
goRouter = MockGoRouter();
|
||||
});
|
||||
|
||||
Future<void> loadScreen(WidgetTester tester) async {
|
||||
await testApp(
|
||||
tester,
|
||||
LoginScreen(viewModel: viewModel),
|
||||
goRouter: goRouter,
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('should load screen', (WidgetTester tester) async {
|
||||
await mockNetworkImages(() async {
|
||||
await loadScreen(tester);
|
||||
expect(find.byType(LoginScreen), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('should perform login', (WidgetTester tester) async {
|
||||
await mockNetworkImages(() async {
|
||||
await loadScreen(tester);
|
||||
|
||||
// Repo should have no key
|
||||
expect(fakeAuthTokenRepository.token, null);
|
||||
|
||||
// Perform login
|
||||
await tester.tap(find.text('Login'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Repo should have key
|
||||
expect(fakeAuthTokenRepository.token, 'TOKEN');
|
||||
|
||||
// Should navigate to home screen
|
||||
verify(() => goRouter.go('/')).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import 'package:compass_app/domain/components/auth/auth_logout_component.dart';
|
||||
import 'package:compass_app/ui/auth/logout/view_models/logout_viewmodel.dart';
|
||||
import 'package:compass_app/ui/auth/logout/widgets/logout_button.dart';
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail_image_network/mocktail_image_network.dart';
|
||||
|
||||
import '../../../testing/app.dart';
|
||||
import '../../../testing/fakes/repositories/fake_auth_token_repository.dart';
|
||||
import '../../../testing/fakes/repositories/fake_itinerary_config_repository.dart';
|
||||
import '../../../testing/mocks.dart';
|
||||
|
||||
void main() {
|
||||
group('LogoutButton test', () {
|
||||
late MockGoRouter goRouter;
|
||||
late FakeAuthTokenRepository fakeAuthTokenRepository;
|
||||
late FakeItineraryConfigRepository fakeItineraryConfigRepository;
|
||||
late LogoutViewModel viewModel;
|
||||
|
||||
setUp(() {
|
||||
goRouter = MockGoRouter();
|
||||
fakeAuthTokenRepository = FakeAuthTokenRepository();
|
||||
// Setup a token, should be cleared after logout
|
||||
fakeAuthTokenRepository.token = 'TOKEN';
|
||||
// Setup an ItineraryConfig with some data, should be cleared after logout
|
||||
fakeItineraryConfigRepository = FakeItineraryConfigRepository(
|
||||
itineraryConfig: const ItineraryConfig(continent: 'CONTINENT'));
|
||||
viewModel = LogoutViewModel(
|
||||
authLogoutComponent: AuthLogoutComponent(
|
||||
authTokenRepository: fakeAuthTokenRepository,
|
||||
itineraryConfigRepository: fakeItineraryConfigRepository,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
Future<void> loadScreen(WidgetTester tester) async {
|
||||
await testApp(
|
||||
tester,
|
||||
LogoutButton(viewModel: viewModel),
|
||||
goRouter: goRouter,
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('should load widget', (WidgetTester tester) async {
|
||||
await mockNetworkImages(() async {
|
||||
await loadScreen(tester);
|
||||
expect(find.byType(LogoutButton), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('should perform logout', (WidgetTester tester) async {
|
||||
await mockNetworkImages(() async {
|
||||
await loadScreen(tester);
|
||||
|
||||
// Repo should have a key
|
||||
expect(fakeAuthTokenRepository.token, 'TOKEN');
|
||||
// Itinerary config should have data
|
||||
expect(
|
||||
fakeItineraryConfigRepository.itineraryConfig,
|
||||
const ItineraryConfig(continent: 'CONTINENT'),
|
||||
);
|
||||
|
||||
// // Perform logout
|
||||
await tester.tap(find.byType(LogoutButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Repo should have no key
|
||||
expect(fakeAuthTokenRepository.token, null);
|
||||
// Itinerary config should be cleared
|
||||
expect(
|
||||
fakeItineraryConfigRepository.itineraryConfig,
|
||||
const ItineraryConfig(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import 'package:compass_app/data/repositories/auth/auth_token_repository.dart';
|
||||
import 'package:compass_app/utils/result.dart';
|
||||
|
||||
class FakeAuthTokenRepository extends AuthTokenRepository {
|
||||
String? token;
|
||||
|
||||
@override
|
||||
Future<Result<String?>> getToken() async {
|
||||
return Result.ok(token);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> saveToken(String? token) async {
|
||||
this.token = token;
|
||||
notifyListeners();
|
||||
return Result.ok(null);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'login_request.freezed.dart';
|
||||
|
||||
part 'login_request.g.dart';
|
||||
|
||||
/// Simple data class to hold login request data.
|
||||
@freezed
|
||||
class LoginRequest with _$LoginRequest {
|
||||
const factory LoginRequest({
|
||||
/// Email address.
|
||||
required String email,
|
||||
|
||||
/// Plain text password.
|
||||
required String password,
|
||||
}) = _LoginRequest;
|
||||
|
||||
factory LoginRequest.fromJson(Map<String, Object?> json) =>
|
||||
_$LoginRequestFromJson(json);
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'login_request.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
LoginRequest _$LoginRequestFromJson(Map<String, dynamic> json) {
|
||||
return _LoginRequest.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$LoginRequest {
|
||||
/// Email address.
|
||||
String get email => throw _privateConstructorUsedError;
|
||||
|
||||
/// Plain text password.
|
||||
String get password => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this LoginRequest to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of LoginRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$LoginRequestCopyWith<LoginRequest> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $LoginRequestCopyWith<$Res> {
|
||||
factory $LoginRequestCopyWith(
|
||||
LoginRequest value, $Res Function(LoginRequest) then) =
|
||||
_$LoginRequestCopyWithImpl<$Res, LoginRequest>;
|
||||
@useResult
|
||||
$Res call({String email, String password});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$LoginRequestCopyWithImpl<$Res, $Val extends LoginRequest>
|
||||
implements $LoginRequestCopyWith<$Res> {
|
||||
_$LoginRequestCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of LoginRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? email = null,
|
||||
Object? password = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
email: null == email
|
||||
? _value.email
|
||||
: email // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
password: null == password
|
||||
? _value.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$LoginRequestImplCopyWith<$Res>
|
||||
implements $LoginRequestCopyWith<$Res> {
|
||||
factory _$$LoginRequestImplCopyWith(
|
||||
_$LoginRequestImpl value, $Res Function(_$LoginRequestImpl) then) =
|
||||
__$$LoginRequestImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String email, String password});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$LoginRequestImplCopyWithImpl<$Res>
|
||||
extends _$LoginRequestCopyWithImpl<$Res, _$LoginRequestImpl>
|
||||
implements _$$LoginRequestImplCopyWith<$Res> {
|
||||
__$$LoginRequestImplCopyWithImpl(
|
||||
_$LoginRequestImpl _value, $Res Function(_$LoginRequestImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of LoginRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? email = null,
|
||||
Object? password = null,
|
||||
}) {
|
||||
return _then(_$LoginRequestImpl(
|
||||
email: null == email
|
||||
? _value.email
|
||||
: email // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
password: null == password
|
||||
? _value.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$LoginRequestImpl implements _LoginRequest {
|
||||
const _$LoginRequestImpl({required this.email, required this.password});
|
||||
|
||||
factory _$LoginRequestImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$LoginRequestImplFromJson(json);
|
||||
|
||||
/// Email address.
|
||||
@override
|
||||
final String email;
|
||||
|
||||
/// Plain text password.
|
||||
@override
|
||||
final String password;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoginRequest(email: $email, password: $password)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$LoginRequestImpl &&
|
||||
(identical(other.email, email) || other.email == email) &&
|
||||
(identical(other.password, password) ||
|
||||
other.password == password));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, email, password);
|
||||
|
||||
/// Create a copy of LoginRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$LoginRequestImplCopyWith<_$LoginRequestImpl> get copyWith =>
|
||||
__$$LoginRequestImplCopyWithImpl<_$LoginRequestImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$LoginRequestImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _LoginRequest implements LoginRequest {
|
||||
const factory _LoginRequest(
|
||||
{required final String email,
|
||||
required final String password}) = _$LoginRequestImpl;
|
||||
|
||||
factory _LoginRequest.fromJson(Map<String, dynamic> json) =
|
||||
_$LoginRequestImpl.fromJson;
|
||||
|
||||
/// Email address.
|
||||
@override
|
||||
String get email;
|
||||
|
||||
/// Plain text password.
|
||||
@override
|
||||
String get password;
|
||||
|
||||
/// Create a copy of LoginRequest
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$LoginRequestImplCopyWith<_$LoginRequestImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'login_request.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$LoginRequestImpl _$$LoginRequestImplFromJson(Map<String, dynamic> json) =>
|
||||
_$LoginRequestImpl(
|
||||
email: json['email'] as String,
|
||||
password: json['password'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$LoginRequestImplToJson(_$LoginRequestImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'email': instance.email,
|
||||
'password': instance.password,
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'login_response.freezed.dart';
|
||||
|
||||
part 'login_response.g.dart';
|
||||
|
||||
/// LoginResponse model.
|
||||
@freezed
|
||||
class LoginResponse with _$LoginResponse {
|
||||
const factory LoginResponse({
|
||||
/// The token to be used for authentication.
|
||||
required String token,
|
||||
|
||||
/// The user id.
|
||||
required String userId,
|
||||
}) = _LoginResponse;
|
||||
|
||||
factory LoginResponse.fromJson(Map<String, Object?> json) =>
|
||||
_$LoginResponseFromJson(json);
|
||||
}
|
@ -0,0 +1,191 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'login_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
LoginResponse _$LoginResponseFromJson(Map<String, dynamic> json) {
|
||||
return _LoginResponse.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$LoginResponse {
|
||||
/// The token to be used for authentication.
|
||||
String get token => throw _privateConstructorUsedError;
|
||||
|
||||
/// The user id.
|
||||
String get userId => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this LoginResponse to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of LoginResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$LoginResponseCopyWith<LoginResponse> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $LoginResponseCopyWith<$Res> {
|
||||
factory $LoginResponseCopyWith(
|
||||
LoginResponse value, $Res Function(LoginResponse) then) =
|
||||
_$LoginResponseCopyWithImpl<$Res, LoginResponse>;
|
||||
@useResult
|
||||
$Res call({String token, String userId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$LoginResponseCopyWithImpl<$Res, $Val extends LoginResponse>
|
||||
implements $LoginResponseCopyWith<$Res> {
|
||||
_$LoginResponseCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of LoginResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? token = null,
|
||||
Object? userId = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
token: null == token
|
||||
? _value.token
|
||||
: token // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
userId: null == userId
|
||||
? _value.userId
|
||||
: userId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$LoginResponseImplCopyWith<$Res>
|
||||
implements $LoginResponseCopyWith<$Res> {
|
||||
factory _$$LoginResponseImplCopyWith(
|
||||
_$LoginResponseImpl value, $Res Function(_$LoginResponseImpl) then) =
|
||||
__$$LoginResponseImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String token, String userId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$LoginResponseImplCopyWithImpl<$Res>
|
||||
extends _$LoginResponseCopyWithImpl<$Res, _$LoginResponseImpl>
|
||||
implements _$$LoginResponseImplCopyWith<$Res> {
|
||||
__$$LoginResponseImplCopyWithImpl(
|
||||
_$LoginResponseImpl _value, $Res Function(_$LoginResponseImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of LoginResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? token = null,
|
||||
Object? userId = null,
|
||||
}) {
|
||||
return _then(_$LoginResponseImpl(
|
||||
token: null == token
|
||||
? _value.token
|
||||
: token // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
userId: null == userId
|
||||
? _value.userId
|
||||
: userId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$LoginResponseImpl implements _LoginResponse {
|
||||
const _$LoginResponseImpl({required this.token, required this.userId});
|
||||
|
||||
factory _$LoginResponseImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$LoginResponseImplFromJson(json);
|
||||
|
||||
/// The token to be used for authentication.
|
||||
@override
|
||||
final String token;
|
||||
|
||||
/// The user id.
|
||||
@override
|
||||
final String userId;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoginResponse(token: $token, userId: $userId)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$LoginResponseImpl &&
|
||||
(identical(other.token, token) || other.token == token) &&
|
||||
(identical(other.userId, userId) || other.userId == userId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, token, userId);
|
||||
|
||||
/// Create a copy of LoginResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$LoginResponseImplCopyWith<_$LoginResponseImpl> get copyWith =>
|
||||
__$$LoginResponseImplCopyWithImpl<_$LoginResponseImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$LoginResponseImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _LoginResponse implements LoginResponse {
|
||||
const factory _LoginResponse(
|
||||
{required final String token,
|
||||
required final String userId}) = _$LoginResponseImpl;
|
||||
|
||||
factory _LoginResponse.fromJson(Map<String, dynamic> json) =
|
||||
_$LoginResponseImpl.fromJson;
|
||||
|
||||
/// The token to be used for authentication.
|
||||
@override
|
||||
String get token;
|
||||
|
||||
/// The user id.
|
||||
@override
|
||||
String get userId;
|
||||
|
||||
/// Create a copy of LoginResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$LoginResponseImplCopyWith<_$LoginResponseImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'login_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$LoginResponseImpl _$$LoginResponseImplFromJson(Map<String, dynamic> json) =>
|
||||
_$LoginResponseImpl(
|
||||
token: json['token'] as String,
|
||||
userId: json['userId'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$LoginResponseImplToJson(_$LoginResponseImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'token': instance.token,
|
||||
'userId': instance.userId,
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
class Constants {
|
||||
/// Email for the hardcoded login.
|
||||
static const email = 'email@example.com';
|
||||
|
||||
/// Password for the hardcoded login.
|
||||
static const password = 'password';
|
||||
|
||||
/// Token to be returned on successful login.
|
||||
static const token =
|
||||
' e1c37dfd973353b78bb71df050e2c6e72d53034e148920383968ae49b96f1fd2';
|
||||
|
||||
/// User id to be returned on successful login.
|
||||
static const userId = '123';
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import 'package:shelf/shelf.dart';
|
||||
|
||||
import '../config/constants.dart';
|
||||
|
||||
/// Implements a simple auth Middleware.
|
||||
///
|
||||
/// This is provided as an example for Flutter architectural purposes only
|
||||
/// and shouldn't be used as example on how to implement authentication
|
||||
/// in production.
|
||||
///
|
||||
/// This Middleware checks if the token is present in the request headers,
|
||||
/// otherwise returns a 401 Unauthorized response.
|
||||
///
|
||||
/// This token does not expire and is not secure.
|
||||
Middleware authRequests() => (innerHandler) {
|
||||
return (Request request) async {
|
||||
if (request.url.path != 'login' &&
|
||||
request.headers['Authorization'] != 'Bearer ${Constants.token}') {
|
||||
// If the request is not a login request and the token is not present,
|
||||
// return a 401 Unauthorized response.
|
||||
return Response.unauthorized('Unauthorized');
|
||||
}
|
||||
|
||||
return innerHandler(request);
|
||||
};
|
||||
};
|
@ -0,0 +1,44 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:compass_server/config/constants.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
/// Implements a simple login API.
|
||||
///
|
||||
/// This is provided as an example for Flutter architectural purposes only
|
||||
/// and shouldn't be used as example on how to implement authentication
|
||||
/// in production.
|
||||
///
|
||||
/// This API only accepts a fixed email and password for demonstration purposes,
|
||||
/// then returns a hardcoded token and a user id.
|
||||
///
|
||||
/// This token does not expire and is not secure.
|
||||
class LoginApi {
|
||||
Router get router {
|
||||
final router = Router();
|
||||
|
||||
router.post('/', (Request request) async {
|
||||
final body = await request.readAsString();
|
||||
final loginRequest = LoginRequest.fromJson(json.decode(body));
|
||||
|
||||
if (loginRequest.email == Constants.email &&
|
||||
loginRequest.password == Constants.password) {
|
||||
return Response.ok(
|
||||
json.encode(
|
||||
LoginResponse(
|
||||
token: Constants.token,
|
||||
userId: Constants.userId,
|
||||
),
|
||||
),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
}
|
||||
|
||||
return Response.unauthorized('Invalid credentials');
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
Loading…
Reference in new issue