Replace API key login with username and password login: a valid API key will automatically be created on login

This commit is contained in:
Varakh 2021-02-13 15:09:40 +01:00
parent 9c6340b2c3
commit f2422816f4
9 changed files with 102 additions and 18 deletions

View file

@ -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"
},

View file

@ -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<String, String> data;
CreateApiKeyResponse({this.status, this.data});
// JSON Init
factory CreateApiKeyResponse.fromJson(Map<String, dynamic> json) => _$CreateApiKeyResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$CreateApiKeyResponseToJson(this);
}

View file

@ -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<Api>();
Future<CreateApiKeyResponse> 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));
}
}

View file

@ -0,0 +1,12 @@
import 'dart:async';
import '../../locator.dart';
import '../repositories/user_repository.dart';
class UserService {
final UserRepository _userRepository = locator<UserRepository>();
Future createApiKey(String url, String username, String password, String accessLevel, String comment) async {
return await _userRepository.createApiKey(url, username, password, accessLevel, comment);
}
}

View file

@ -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<SessionService>();
final StorageService _storageService = locator<StorageService>();
final FileService _configService = locator<FileService>();
final UserService _userService = locator<UserService>();
final FileService _fileService = locator<FileService>();
final Logger _logger = getLogger();
String errorMessage;
@ -48,10 +54,11 @@ class LoginModel extends BaseModel {
}
}
Future<bool> login(String url, String apiKey) async {
Future<bool> 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');
}

View file

@ -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());

View file

@ -68,7 +68,8 @@ class _LoginViewState extends State<LoginView> {
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<LoginView> {
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);
}

View file

@ -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),
]);
}
}

View file

@ -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"