Replace API key login with username and password login: a valid API key will automatically be created on login
This commit is contained in:
parent
9c6340b2c3
commit
f2422816f4
9 changed files with 102 additions and 18 deletions
|
@ -47,16 +47,18 @@
|
||||||
"help": "Login",
|
"help": "Login",
|
||||||
"compatibility_dialog": {
|
"compatibility_dialog": {
|
||||||
"title": "How to login?",
|
"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",
|
"url_placeholder": "https://paste.domain.tld",
|
||||||
"apikey_placeholder": "API key",
|
"username_placeholder": "Username",
|
||||||
|
"password_placeholder": "Password",
|
||||||
"button": "Login",
|
"button": "Login",
|
||||||
"errors": {
|
"errors": {
|
||||||
"empty_url": "Please provide a FileBin URL",
|
"empty_url": "Please provide a FileBin URL",
|
||||||
"no_protocol": "URLs need to include a valid protocol like http:// or https://",
|
"no_protocol": "URLs need to include a valid protocol like http:// or https://",
|
||||||
"invalid_url": "Please provide a valid FileBin URL",
|
"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",
|
"wrong_credentials": "Credentials are invalid",
|
||||||
"forbidden": "You're not allowed to access this instance"
|
"forbidden": "You're not allowed to access this instance"
|
||||||
}
|
}
|
||||||
|
@ -86,7 +88,7 @@
|
||||||
"headline": "Welcome to FileBin mobile!",
|
"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.",
|
"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_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?",
|
"contact_us": "Feedback? Issues?",
|
||||||
"website": "Main application: https://github.com/Bluewind/filebin\n\nMobile: https://github.com/v4rakh/fbmobile"
|
"website": "Main application: https://github.com/Bluewind/filebin\n\nMobile: https://github.com/v4rakh/fbmobile"
|
||||||
},
|
},
|
||||||
|
|
20
lib/core/models/rest/create_apikey_response.dart
Normal file
20
lib/core/models/rest/create_apikey_response.dart
Normal 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);
|
||||||
|
}
|
18
lib/core/repositories/user_repository.dart
Normal file
18
lib/core/repositories/user_repository.dart
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
12
lib/core/services/user_service.dart
Normal file
12
lib/core/services/user_service.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,20 +15,26 @@ import '../enums/viewstate.dart';
|
||||||
import '../error/rest_service_exception.dart';
|
import '../error/rest_service_exception.dart';
|
||||||
import '../error/service_exception.dart';
|
import '../error/service_exception.dart';
|
||||||
import '../models/rest/config.dart';
|
import '../models/rest/config.dart';
|
||||||
|
import '../models/rest/create_apikey_response.dart';
|
||||||
|
import '../services/user_service.dart';
|
||||||
import '../util/logger.dart';
|
import '../util/logger.dart';
|
||||||
import 'base_model.dart';
|
import 'base_model.dart';
|
||||||
|
|
||||||
class LoginModel extends BaseModel {
|
class LoginModel extends BaseModel {
|
||||||
TextEditingController _uriController = new TextEditingController();
|
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 uriController => _uriController;
|
||||||
|
|
||||||
TextEditingController get apiKeyController => _apiKeyController;
|
TextEditingController get userNameController => _userNameController;
|
||||||
|
|
||||||
|
TextEditingController get passwordController => _passwordController;
|
||||||
|
|
||||||
final SessionService _sessionService = locator<SessionService>();
|
final SessionService _sessionService = locator<SessionService>();
|
||||||
final StorageService _storageService = locator<StorageService>();
|
final StorageService _storageService = locator<StorageService>();
|
||||||
final FileService _configService = locator<FileService>();
|
final UserService _userService = locator<UserService>();
|
||||||
|
final FileService _fileService = locator<FileService>();
|
||||||
final Logger _logger = getLogger();
|
final Logger _logger = getLogger();
|
||||||
|
|
||||||
String errorMessage;
|
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);
|
setState(ViewState.Busy);
|
||||||
|
|
||||||
url = trim(url);
|
url = trim(url);
|
||||||
|
username = trim(username);
|
||||||
|
|
||||||
if (url.isEmpty) {
|
if (url.isEmpty) {
|
||||||
errorMessage = translate('login.errors.empty_url');
|
errorMessage = translate('login.errors.empty_url');
|
||||||
|
@ -72,16 +79,24 @@ class LoginModel extends BaseModel {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiKey.isEmpty) {
|
if (username.isEmpty) {
|
||||||
errorMessage = translate('login.errors.empty_apikey');
|
errorMessage = translate('login.errors.empty_username');
|
||||||
|
setState(ViewState.Idle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.isEmpty) {
|
||||||
|
errorMessage = translate('login.errors.empty_password');
|
||||||
setState(ViewState.Idle);
|
setState(ViewState.Idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var success = false;
|
var success = false;
|
||||||
try {
|
try {
|
||||||
Config config = await _configService.getConfig(url);
|
Config config = await _fileService.getConfig(url);
|
||||||
success = await _sessionService.login(url, apiKey, config);
|
CreateApiKeyResponse apiKeyResponse =
|
||||||
|
await _userService.createApiKey(url, username, password, 'apikey', 'fbmobile');
|
||||||
|
success = await _sessionService.login(url, apiKeyResponse.data['new_key'], config);
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is RestServiceException) {
|
if (e is RestServiceException) {
|
||||||
|
@ -91,6 +106,9 @@ class LoginModel extends BaseModel {
|
||||||
errorMessage = translate('login.errors.forbidden');
|
errorMessage = translate('login.errors.forbidden');
|
||||||
} else if (e.statusCode == HttpStatus.notFound) {
|
} else if (e.statusCode == HttpStatus.notFound) {
|
||||||
errorMessage = translate('api.incompatible_error_not_found');
|
errorMessage = translate('api.incompatible_error_not_found');
|
||||||
|
}
|
||||||
|
if (e.statusCode == HttpStatus.badRequest) {
|
||||||
|
errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message});
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('api.general_rest_error');
|
errorMessage = translate('api.general_rest_error');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
import 'core/repositories/file_repository.dart';
|
import 'core/repositories/file_repository.dart';
|
||||||
|
import 'core/repositories/user_repository.dart';
|
||||||
import 'core/services/api.dart';
|
import 'core/services/api.dart';
|
||||||
import 'core/services/dialog_service.dart';
|
import 'core/services/dialog_service.dart';
|
||||||
import 'core/services/file_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/refresh_service.dart';
|
||||||
import 'core/services/session_service.dart';
|
import 'core/services/session_service.dart';
|
||||||
import 'core/services/storage_service.dart';
|
import 'core/services/storage_service.dart';
|
||||||
|
import 'core/services/user_service.dart';
|
||||||
import 'core/viewmodels/about_model.dart';
|
import 'core/viewmodels/about_model.dart';
|
||||||
import 'core/viewmodels/history_model.dart';
|
import 'core/viewmodels/history_model.dart';
|
||||||
import 'core/viewmodels/home_model.dart';
|
import 'core/viewmodels/home_model.dart';
|
||||||
|
@ -30,9 +32,11 @@ void setupLocator() {
|
||||||
locator.registerLazySingleton(() => Api());
|
locator.registerLazySingleton(() => Api());
|
||||||
|
|
||||||
locator.registerLazySingleton(() => FileRepository());
|
locator.registerLazySingleton(() => FileRepository());
|
||||||
|
locator.registerLazySingleton(() => UserRepository());
|
||||||
|
|
||||||
/// services
|
/// services
|
||||||
locator.registerLazySingleton(() => SessionService());
|
locator.registerLazySingleton(() => SessionService());
|
||||||
|
locator.registerLazySingleton(() => UserService());
|
||||||
locator.registerLazySingleton(() => FileService());
|
locator.registerLazySingleton(() => FileService());
|
||||||
locator.registerLazySingleton(() => LinkService());
|
locator.registerLazySingleton(() => LinkService());
|
||||||
locator.registerLazySingleton(() => PermissionService());
|
locator.registerLazySingleton(() => PermissionService());
|
||||||
|
|
|
@ -68,7 +68,8 @@ class _LoginViewState extends State<LoginView> {
|
||||||
LoginHeaders(
|
LoginHeaders(
|
||||||
validationMessage: model.errorMessage,
|
validationMessage: model.errorMessage,
|
||||||
uriController: model.uriController,
|
uriController: model.uriController,
|
||||||
apiKeyController: model.apiKeyController,
|
usernameController: model.userNameController,
|
||||||
|
passwordController: model.passwordController,
|
||||||
),
|
),
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
@ -78,7 +79,8 @@ class _LoginViewState extends State<LoginView> {
|
||||||
color: primaryAccentColor,
|
color: primaryAccentColor,
|
||||||
child: Text(translate('login.button'), style: TextStyle(color: buttonForegroundColor)),
|
child: Text(translate('login.button'), style: TextStyle(color: buttonForegroundColor)),
|
||||||
onPressed: () async {
|
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) {
|
if (loginSuccess) {
|
||||||
_navigationService.navigateAndReplaceTo(HomeView.routeName);
|
_navigationService.navigateAndReplaceTo(HomeView.routeName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,16 @@ import 'package:flutter_translate/flutter_translate.dart';
|
||||||
|
|
||||||
class LoginHeaders extends StatelessWidget {
|
class LoginHeaders extends StatelessWidget {
|
||||||
final TextEditingController uriController;
|
final TextEditingController uriController;
|
||||||
final TextEditingController apiKeyController;
|
final TextEditingController usernameController;
|
||||||
|
final TextEditingController passwordController;
|
||||||
|
|
||||||
final String validationMessage;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -15,7 +20,10 @@ class LoginHeaders extends StatelessWidget {
|
||||||
this.validationMessage != null ? Text(validationMessage, style: TextStyle(color: Colors.red)) : Container(),
|
this.validationMessage != null ? Text(validationMessage, style: TextStyle(color: Colors.red)) : Container(),
|
||||||
LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link),
|
LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link),
|
||||||
keyboardType: TextInputType.url),
|
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),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ description: A mobile client for FileBin.
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.0.0+4
|
version: 1.1.0+5
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.7.0 <3.0.0"
|
sdk: ">=2.7.0 <3.0.0"
|
||||||
|
|
Loading…
Reference in a new issue