Merge pull request #1 from v4rakh/feature/create-apikey-on-login
Feature/create apikey on login
This commit is contained in:
commit
37d1b11b5a
10 changed files with 105 additions and 18 deletions
|
@ -1,5 +1,8 @@
|
|||
# CHANGELOG
|
||||
|
||||
## 1.1.0+5
|
||||
* Replace API key login with username and password login: a valid API key will automatically be created on login
|
||||
|
||||
## 1.0.0+3 and 1.0.0+4
|
||||
* Fixed launch bug related to Android 4.0.1 gradle plugin
|
||||
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
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/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');
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue