From f2422816f427c92e01a2fe0c450843d747c734dd Mon Sep 17 00:00:00 2001 From: Varakh Date: Sat, 13 Feb 2021 15:09:40 +0100 Subject: [PATCH] Replace API key login with username and password login: a valid API key will automatically be created on login --- assets/i18n/en.json | 10 +++--- .../models/rest/create_apikey_response.dart | 20 +++++++++++ lib/core/repositories/user_repository.dart | 18 ++++++++++ lib/core/services/user_service.dart | 12 +++++++ lib/core/viewmodels/login_model.dart | 34 ++++++++++++++----- lib/locator.dart | 4 +++ lib/ui/views/login_view.dart | 6 ++-- lib/ui/widgets/login_header.dart | 14 ++++++-- pubspec.yaml | 2 +- 9 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 lib/core/models/rest/create_apikey_response.dart create mode 100644 lib/core/repositories/user_repository.dart create mode 100644 lib/core/services/user_service.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 06b663a..7ca5d32 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -47,16 +47,18 @@ "help": "Login", "compatibility_dialog": { "title": "How to login?", - "body": "A FileBin instance >= 3.5.0 and a valid API key with at least access-level 'apikey' is required." + "body": "A FileBin instance >= 3.5.0 and valid credentials for this instance are required." }, "url_placeholder": "https://paste.domain.tld", - "apikey_placeholder": "API key", + "username_placeholder": "Username", + "password_placeholder": "Password", "button": "Login", "errors": { "empty_url": "Please provide a FileBin URL", "no_protocol": "URLs need to include a valid protocol like http:// or https://", "invalid_url": "Please provide a valid FileBin URL", - "empty_apikey": "Please provide an API key", + "empty_username": "Please provide a username", + "empty_password": "Please provide a password", "wrong_credentials": "Credentials are invalid", "forbidden": "You're not allowed to access this instance" } @@ -86,7 +88,7 @@ "headline": "Welcome to FileBin mobile!", "description": "This application is a mobile client for FileBin and it's open source. It helps you to manage your pastes.\n\nIn order to use the application, you need access to a FileBin instance.", "faq_headline": "F.A.Q", - "faq": "- How do I login?\nInsert your instance URL and an API key which you can generate in the web interface of FileBin. You should use 'apikey' access-level in order to show history.\n\n- Why is storage permission required?\nIt's not required, but highly advised to grant it. Otherwise sharing files with the app won't work correctly and you might think that sharing has no effect.\n\n- When I am logged out, sharing files via share with the app won't list all files I selected after I login.\nPlease login before you start using the app. Account information are persisted. You only need to do it once.", + "faq": "- How do I login?\nInsert your instance URL and valid credentials you also use in the web interface of FileBin.\n\n- Why is storage permission required?\nIt's not required, but highly advised to grant it. Otherwise sharing files with the app won't work correctly and you might think that sharing has no effect.\n\n- When I am logged out, sharing files via share with the app won't list all files I selected after I login.\nPlease login before you start using the app. Account information are persisted. You only need to do it once.", "contact_us": "Feedback? Issues?", "website": "Main application: https://github.com/Bluewind/filebin\n\nMobile: https://github.com/v4rakh/fbmobile" }, diff --git a/lib/core/models/rest/create_apikey_response.dart b/lib/core/models/rest/create_apikey_response.dart new file mode 100644 index 0000000..cdd415d --- /dev/null +++ b/lib/core/models/rest/create_apikey_response.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'create_apikey_response.g.dart'; + +@JsonSerializable() +class CreateApiKeyResponse { + @JsonKey(required: true) + final String status; + + @JsonKey(required: true) + final Map data; + + CreateApiKeyResponse({this.status, this.data}); + + // JSON Init + factory CreateApiKeyResponse.fromJson(Map json) => _$CreateApiKeyResponseFromJson(json); + + // JSON Export + Map toJson() => _$CreateApiKeyResponseToJson(this); +} diff --git a/lib/core/repositories/user_repository.dart b/lib/core/repositories/user_repository.dart new file mode 100644 index 0000000..f423816 --- /dev/null +++ b/lib/core/repositories/user_repository.dart @@ -0,0 +1,18 @@ +import 'dart:convert'; + +import '../../locator.dart'; +import '../models/rest/create_apikey_response.dart'; +import '../services/api.dart'; + +class UserRepository { + Api _api = locator(); + + Future createApiKey( + String url, String username, String password, String accessLevel, String comment) async { + _api.setUrl(url); + + var response = await _api.post('/user/create_apikey', + fields: {'username': username, 'password': password, 'access_level': accessLevel, 'comment': comment}); + return CreateApiKeyResponse.fromJson(json.decode(response.body)); + } +} diff --git a/lib/core/services/user_service.dart b/lib/core/services/user_service.dart new file mode 100644 index 0000000..f9e4914 --- /dev/null +++ b/lib/core/services/user_service.dart @@ -0,0 +1,12 @@ +import 'dart:async'; + +import '../../locator.dart'; +import '../repositories/user_repository.dart'; + +class UserService { + final UserRepository _userRepository = locator(); + + Future createApiKey(String url, String username, String password, String accessLevel, String comment) async { + return await _userRepository.createApiKey(url, username, password, accessLevel, comment); + } +} diff --git a/lib/core/viewmodels/login_model.dart b/lib/core/viewmodels/login_model.dart index f2eadc9..0d16ec1 100644 --- a/lib/core/viewmodels/login_model.dart +++ b/lib/core/viewmodels/login_model.dart @@ -15,20 +15,26 @@ import '../enums/viewstate.dart'; import '../error/rest_service_exception.dart'; import '../error/service_exception.dart'; import '../models/rest/config.dart'; +import '../models/rest/create_apikey_response.dart'; +import '../services/user_service.dart'; import '../util/logger.dart'; import 'base_model.dart'; class LoginModel extends BaseModel { TextEditingController _uriController = new TextEditingController(); - final TextEditingController _apiKeyController = new TextEditingController(); + final TextEditingController _userNameController = new TextEditingController(); + final TextEditingController _passwordController = new TextEditingController(); TextEditingController get uriController => _uriController; - TextEditingController get apiKeyController => _apiKeyController; + TextEditingController get userNameController => _userNameController; + + TextEditingController get passwordController => _passwordController; final SessionService _sessionService = locator(); final StorageService _storageService = locator(); - final FileService _configService = locator(); + final UserService _userService = locator(); + final FileService _fileService = locator(); final Logger _logger = getLogger(); String errorMessage; @@ -48,10 +54,11 @@ class LoginModel extends BaseModel { } } - Future login(String url, String apiKey) async { + Future login(String url, String username, String password) async { setState(ViewState.Busy); url = trim(url); + username = trim(username); if (url.isEmpty) { errorMessage = translate('login.errors.empty_url'); @@ -72,16 +79,24 @@ class LoginModel extends BaseModel { return false; } - if (apiKey.isEmpty) { - errorMessage = translate('login.errors.empty_apikey'); + if (username.isEmpty) { + errorMessage = translate('login.errors.empty_username'); + setState(ViewState.Idle); + return false; + } + + if (password.isEmpty) { + errorMessage = translate('login.errors.empty_password'); setState(ViewState.Idle); return false; } var success = false; try { - Config config = await _configService.getConfig(url); - success = await _sessionService.login(url, apiKey, config); + Config config = await _fileService.getConfig(url); + CreateApiKeyResponse apiKeyResponse = + await _userService.createApiKey(url, username, password, 'apikey', 'fbmobile'); + success = await _sessionService.login(url, apiKeyResponse.data['new_key'], config); errorMessage = null; } catch (e) { if (e is RestServiceException) { @@ -91,6 +106,9 @@ class LoginModel extends BaseModel { errorMessage = translate('login.errors.forbidden'); } else if (e.statusCode == HttpStatus.notFound) { errorMessage = translate('api.incompatible_error_not_found'); + } + if (e.statusCode == HttpStatus.badRequest) { + errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message}); } else { errorMessage = translate('api.general_rest_error'); } diff --git a/lib/locator.dart b/lib/locator.dart index 732ae92..97bc207 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -1,6 +1,7 @@ import 'package:get_it/get_it.dart'; import 'core/repositories/file_repository.dart'; +import 'core/repositories/user_repository.dart'; import 'core/services/api.dart'; import 'core/services/dialog_service.dart'; import 'core/services/file_service.dart'; @@ -10,6 +11,7 @@ import 'core/services/permission_service.dart'; import 'core/services/refresh_service.dart'; import 'core/services/session_service.dart'; import 'core/services/storage_service.dart'; +import 'core/services/user_service.dart'; import 'core/viewmodels/about_model.dart'; import 'core/viewmodels/history_model.dart'; import 'core/viewmodels/home_model.dart'; @@ -30,9 +32,11 @@ void setupLocator() { locator.registerLazySingleton(() => Api()); locator.registerLazySingleton(() => FileRepository()); + locator.registerLazySingleton(() => UserRepository()); /// services locator.registerLazySingleton(() => SessionService()); + locator.registerLazySingleton(() => UserService()); locator.registerLazySingleton(() => FileService()); locator.registerLazySingleton(() => LinkService()); locator.registerLazySingleton(() => PermissionService()); diff --git a/lib/ui/views/login_view.dart b/lib/ui/views/login_view.dart index 30f67cd..f343621 100644 --- a/lib/ui/views/login_view.dart +++ b/lib/ui/views/login_view.dart @@ -68,7 +68,8 @@ class _LoginViewState extends State { LoginHeaders( validationMessage: model.errorMessage, uriController: model.uriController, - apiKeyController: model.apiKeyController, + usernameController: model.userNameController, + passwordController: model.passwordController, ), RaisedButton( shape: RoundedRectangleBorder( @@ -78,7 +79,8 @@ class _LoginViewState extends State { color: primaryAccentColor, child: Text(translate('login.button'), style: TextStyle(color: buttonForegroundColor)), onPressed: () async { - var loginSuccess = await model.login(model.uriController.text, model.apiKeyController.text); + var loginSuccess = await model.login( + model.uriController.text, model.userNameController.text, model.passwordController.text); if (loginSuccess) { _navigationService.navigateAndReplaceTo(HomeView.routeName); } diff --git a/lib/ui/widgets/login_header.dart b/lib/ui/widgets/login_header.dart index 64f329b..6ba7a75 100644 --- a/lib/ui/widgets/login_header.dart +++ b/lib/ui/widgets/login_header.dart @@ -3,11 +3,16 @@ import 'package:flutter_translate/flutter_translate.dart'; class LoginHeaders extends StatelessWidget { final TextEditingController uriController; - final TextEditingController apiKeyController; + final TextEditingController usernameController; + final TextEditingController passwordController; final String validationMessage; - LoginHeaders({@required this.uriController, @required this.apiKeyController, this.validationMessage}); + LoginHeaders( + {@required this.uriController, + @required this.usernameController, + @required this.passwordController, + this.validationMessage}); @override Widget build(BuildContext context) { @@ -15,7 +20,10 @@ class LoginHeaders extends StatelessWidget { this.validationMessage != null ? Text(validationMessage, style: TextStyle(color: Colors.red)) : Container(), LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link), keyboardType: TextInputType.url), - LoginTextField(apiKeyController, translate('login.apikey_placeholder'), Icon(Icons.vpn_key), obscureText: true), + LoginTextField(usernameController, translate('login.username_placeholder'), Icon(Icons.person), + keyboardType: TextInputType.name), + LoginTextField(passwordController, translate('login.password_placeholder'), Icon(Icons.vpn_key), + obscureText: true), ]); } } diff --git a/pubspec.yaml b/pubspec.yaml index 5b4dca1..7490299 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: A mobile client for FileBin. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.0+4 +version: 1.1.0+5 environment: sdk: ">=2.7.0 <3.0.0"