diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 33108f7d..a8140823 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -53,6 +53,16 @@ "@invalidUsername": { "description": "Error message when the user enters an invalid username" }, + "apitoken": "API Key", + "@apitoken": {}, + "invalidApitoken": "Please enter a valid API key", + "@invalidApitoken": { + "description": "Error message when the user enters an invalid API key" + }, + "apitokenValidChars": "A API key may only contain valid hex, a-f and 0-9 and should be 40 characters long", + "@apitokenValidChars": { + "description": "Error message when the user tries to input a API key with forbidden characters" + }, "customServerUrl": "URL of the wger instance", "@customServerUrl": { "description": "Label in the form where the users can enter their own wger instance" diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index 1bfbdca1..1d8977f1 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -48,6 +48,7 @@ class AuthProvider with ChangeNotifier { static const SERVER_VERSION_URL = 'version'; static const REGISTRATION_URL = 'register'; static const LOGIN_URL = 'login'; + static const TEST_URL = 'userprofile'; late http.Client client; @@ -129,7 +130,7 @@ class AuthProvider with ChangeNotifier { throw WgerHttpException(response.body); } - return login(username, password, serverUrl); + return login(username, password, serverUrl, null); } /// Authenticates a user @@ -137,21 +138,43 @@ class AuthProvider with ChangeNotifier { String username, String password, String serverUrl, + String? apiToken, ) async { await logout(shouldNotify: false); - final response = await client.post( - makeUri(serverUrl, LOGIN_URL), - headers: { - HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', - HttpHeaders.userAgentHeader: getAppNameHeader(), - }, - body: json.encode({'username': username, 'password': password}), - ); - final responseData = json.decode(response.body); - - if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + String token; + + if (apiToken != null) { + final response = await client.get( + makeUri(serverUrl, TEST_URL), + headers: { + HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', + HttpHeaders.userAgentHeader: getAppNameHeader(), + HttpHeaders.authorizationHeader: 'Token ${apiToken}', + }, + ); + + if (response.statusCode != 200) { + throw WgerHttpException(response.body); + } + + token = apiToken; + } else { + final response = await client.post( + makeUri(serverUrl, LOGIN_URL), + headers: { + HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', + HttpHeaders.userAgentHeader: getAppNameHeader(), + }, + body: json.encode({'username': username, 'password': password}), + ); + final responseData = json.decode(response.body); + + if (response.statusCode >= 400) { + throw WgerHttpException(response.body); + } + + token = responseData['token']; } await initVersions(serverUrl); @@ -162,7 +185,6 @@ class AuthProvider with ChangeNotifier { } // Log user in - token = responseData['token']; notifyListeners(); // store login data in shared preferences diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart index aa61caba..b0079fb8 100644 --- a/lib/screens/auth_screen.dart +++ b/lib/screens/auth_screen.dart @@ -120,6 +120,7 @@ class _AuthCardState extends State { 'email': '', 'password': '', 'serverUrl': '', + 'apitoken': '', }; var _isLoading = false; final _usernameController = TextEditingController(); @@ -129,6 +130,7 @@ class _AuthCardState extends State { final _serverUrlController = TextEditingController( text: kDebugMode ? DEFAULT_SERVER_TEST : DEFAULT_SERVER_PROD, ); + final _apitokenController = TextEditingController(); @override void dispose() { @@ -137,6 +139,7 @@ class _AuthCardState extends State { _password2Controller.dispose(); _emailController.dispose(); _serverUrlController.dispose(); + _apitokenController.dispose(); super.dispose(); } @@ -162,6 +165,7 @@ class _AuthCardState extends State { void _resetTextfields() { _usernameController.clear(); _passwordController.clear(); + _apitokenController.clear(); } void _submit(BuildContext context) async { @@ -182,6 +186,7 @@ class _AuthCardState extends State { _authData['username']!, _authData['password']!, _authData['serverUrl']!, + _authData['apitoken']!, ); // Register new user @@ -271,6 +276,10 @@ class _AuthCardState extends State { textInputAction: TextInputAction.next, keyboardType: TextInputType.emailAddress, validator: (value) { + if(!_apitokenController.text.isEmpty) { + return null; + } + if (value == null || value.isEmpty) { return AppLocalizations.of(context).invalidUsername; } @@ -329,6 +338,10 @@ class _AuthCardState extends State { controller: _passwordController, textInputAction: TextInputAction.next, validator: (value) { + if(!_apitokenController.text.isEmpty) { + return null; + } + if (value!.isEmpty || value.length < 8) { return AppLocalizations.of(context).passwordTooShort; } @@ -367,6 +380,58 @@ class _AuthCardState extends State { : null, ); }), + if (_authMode != AuthMode.Signup) + Row(children: [ + Expanded( + child: new Container( + margin: const EdgeInsets.only(left: 10.0, right: 20.0), + child: Divider( + color: Colors.black, + height: 36, + )), + ), + Text("OR"), + Expanded( + child: new Container( + margin: const EdgeInsets.only(left: 20.0, right: 10.0), + child: Divider( + color: Colors.black, + height: 36, + )), + ), + ]), + if (_authMode != AuthMode.Signup) + TextFormField( + key: const Key('inputApitoken'), + decoration: InputDecoration( + labelText: AppLocalizations.of(context).apitoken, + errorMaxLines: 2, + prefixIcon: const Icon(Icons.password), + ), + // autofillHints: const [AutofillHints.apitoken], + controller: _apitokenController, + textInputAction: TextInputAction.next, + keyboardType: TextInputType.emailAddress, + validator: (value) { + if(!_usernameController.text.isEmpty || !_passwordController.text.isEmpty) { + return null; + } + if (value == null || value.isEmpty) { + return AppLocalizations.of(context).invalidApitoken; + } + if (!RegExp(r'^[a-f0-9]{40}$').hasMatch(value)) { + return AppLocalizations.of(context).apitokenValidChars; + } + + return null; + }, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(r'\s\b|\b\s')), + ], + onSaved: (value) { + _authData['apitoken'] = value!; + }, + ), // Off-stage widgets are kept in the tree, otherwise the server URL // would not be saved to _authData Offstage(