mirror of
https://github.com/go-vikunja/app
synced 2024-06-01 02:06:51 +00:00
feat: allow users to specify an X-Client-Token (hidden menu)
Workaround for go-vikunja/app#67
This commit is contained in:
parent
64fa13ce17
commit
6052d5c4a3
|
@ -18,29 +18,34 @@ class Client {
|
|||
final JsonEncoder _encoder = new JsonEncoder();
|
||||
String _token = '';
|
||||
String _base = '';
|
||||
String _xClientToken = '';
|
||||
bool authenticated = false;
|
||||
bool ignoreCertificates = false;
|
||||
bool showSnackBar = true;
|
||||
|
||||
String get base => _base;
|
||||
String get token => _token;
|
||||
String get xClientToken => _xClientToken;
|
||||
|
||||
String? post_body;
|
||||
|
||||
@override
|
||||
bool operator ==(Object otherClient) {
|
||||
if (otherClient is! Client) return false;
|
||||
return otherClient._token == _token;
|
||||
return otherClient._token == _token &&
|
||||
otherClient._xClientToken == _xClientToken;
|
||||
}
|
||||
|
||||
Client(
|
||||
this.global_scaffold_key, {
|
||||
String? token,
|
||||
String? xClientToken,
|
||||
String? base,
|
||||
bool authenticated = false,
|
||||
}) {
|
||||
configure(
|
||||
token: token,
|
||||
xClientToken: xClientToken,
|
||||
base: base,
|
||||
authenticated: authenticated,
|
||||
);
|
||||
|
@ -76,6 +81,7 @@ class Client {
|
|||
'Authorization': _token != '' ? 'Bearer $_token' : '',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Vikunja Mobile App',
|
||||
'X-Client-Token': _xClientToken
|
||||
};
|
||||
|
||||
get headers => _headers;
|
||||
|
@ -87,8 +93,10 @@ class Client {
|
|||
String? token,
|
||||
String? base,
|
||||
bool? authenticated,
|
||||
String? xClientToken,
|
||||
}) {
|
||||
if (token != null) _token = token;
|
||||
if (xClientToken != null) _xClientToken = xClientToken;
|
||||
if (base != null) {
|
||||
base = base.replaceAll(" ", "");
|
||||
if (base.endsWith("/")) base = base.substring(0, base.length - 1);
|
||||
|
@ -98,6 +106,7 @@ class Client {
|
|||
}
|
||||
|
||||
void reset() {
|
||||
_token = _base = _xClientToken = '';
|
||||
authenticated = false;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,13 @@ class UserAPIService extends APIService implements UserService {
|
|||
UserAPIService(Client client) : super(client);
|
||||
|
||||
@override
|
||||
Future<UserTokenPair> login(String username, password,
|
||||
{bool rememberMe = false, String? totp}) async {
|
||||
Future<UserTokenPair> login(
|
||||
String username,
|
||||
password, {
|
||||
bool rememberMe = false,
|
||||
String? totp,
|
||||
String? xClientToken,
|
||||
}) async {
|
||||
var body = {
|
||||
'long_token': rememberMe,
|
||||
'password': password,
|
||||
|
@ -26,7 +31,7 @@ class UserAPIService extends APIService implements UserService {
|
|||
error: response != null ? response.body["code"] : 0,
|
||||
errorString:
|
||||
response != null ? response.body["message"] : "Login error"));
|
||||
client.configure(token: token);
|
||||
client.configure(token: token, xClientToken: xClientToken);
|
||||
return UserAPIService(client)
|
||||
.getCurrentUser()
|
||||
.then((user) => UserTokenPair(user, token));
|
||||
|
|
1
lib/constants.dart
Normal file
1
lib/constants.dart
Normal file
|
@ -0,0 +1 @@
|
|||
const ErrorCodeOtpRequired = 1017;
|
|
@ -102,7 +102,8 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
|||
initialDelay: Duration(seconds: 15),
|
||||
inputData: {
|
||||
"client_token": client.token,
|
||||
"client_base": client.base
|
||||
"client_base": client.base,
|
||||
"x_client_token": client.xClientToken,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -132,7 +133,12 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
|||
});
|
||||
}
|
||||
|
||||
void changeUser(User newUser, {String? token, String? base}) async {
|
||||
void changeUser(
|
||||
User newUser, {
|
||||
String? token,
|
||||
String? base,
|
||||
String? xClientToken,
|
||||
}) async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
|
@ -148,6 +154,16 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
|||
// Write new base to secure storage
|
||||
await _storage.write(key: "${newUser.id.toString()}_base", value: base);
|
||||
}
|
||||
|
||||
if (xClientToken == null) {
|
||||
xClientToken =
|
||||
await _storage.read(key: "${newUser.id.toString()}_x_client_token");
|
||||
} else {
|
||||
// Write new xClientToken to secure storage
|
||||
await _storage.write(
|
||||
key: "${newUser.id.toString()}_x_client_token", value: xClientToken);
|
||||
}
|
||||
|
||||
// Set current user in storage
|
||||
await _storage.write(key: 'currentUser', value: newUser.id.toString());
|
||||
client.configure(token: token, base: base, authenticated: true);
|
||||
|
@ -185,13 +201,20 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
|||
}
|
||||
var token = await _storage.read(key: currentUser);
|
||||
var base = await _storage.read(key: '${currentUser}_base');
|
||||
var xClientToken =
|
||||
await _storage.read(key: '${currentUser}_x_client_token');
|
||||
if (token == null || base == null) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
client.configure(token: token, base: base, authenticated: true);
|
||||
client.configure(
|
||||
token: token,
|
||||
base: base,
|
||||
authenticated: true,
|
||||
xClientToken: xClientToken,
|
||||
);
|
||||
User loadedCurrentUser;
|
||||
try {
|
||||
loadedCurrentUser = await UserAPIService(client).getCurrentUser();
|
||||
|
|
|
@ -35,9 +35,6 @@ class IgnoreCertHttpOverrides extends HttpOverrides {
|
|||
|
||||
@pragma('vm:entry-point')
|
||||
void callbackDispatcher() {
|
||||
if (kIsWeb) {
|
||||
return;
|
||||
}
|
||||
Workmanager().executeTask((task, inputData) async {
|
||||
print(
|
||||
"Native called background task: $task"); //simpleTask will be emitted here.
|
||||
|
@ -45,6 +42,7 @@ void callbackDispatcher() {
|
|||
Client client = Client(null,
|
||||
token: inputData["client_token"],
|
||||
base: inputData["client_base"],
|
||||
xClientToken: inputData["x_client_token"],
|
||||
authenticated: true);
|
||||
tz.initializeTimeZones();
|
||||
|
||||
|
@ -70,13 +68,19 @@ void callbackDispatcher() {
|
|||
return Future.value(true);
|
||||
}
|
||||
var token = await _storage.read(key: currentUser);
|
||||
|
||||
var base = await _storage.read(key: '${currentUser}_base');
|
||||
var xClientToken =
|
||||
await _storage.read(key: '${currentUser}_x_client_token');
|
||||
if (token == null || base == null) {
|
||||
return Future.value(true);
|
||||
}
|
||||
Client client = Client(null);
|
||||
client.configure(token: token, base: base, authenticated: true);
|
||||
client.configure(
|
||||
token: token,
|
||||
base: base,
|
||||
xClientToken: xClientToken,
|
||||
authenticated: true,
|
||||
);
|
||||
// load new token from server to avoid expiration
|
||||
String? newToken = await UserAPIService(client).getToken();
|
||||
if (newToken != null) {
|
||||
|
@ -114,16 +118,15 @@ void main() async {
|
|||
print("Failed to initialize workmanager: $e");
|
||||
}
|
||||
runApp(VikunjaGlobal(
|
||||
child: new VikunjaApp(
|
||||
home: HomePage(),
|
||||
key: UniqueKey(),
|
||||
navkey: globalNavigatorKey,
|
||||
),
|
||||
login: new VikunjaApp(
|
||||
home: LoginPage(),
|
||||
key: UniqueKey(),
|
||||
),
|
||||
));
|
||||
child: new VikunjaApp(
|
||||
home: HomePage(),
|
||||
key: UniqueKey(),
|
||||
navkey: globalNavigatorKey,
|
||||
),
|
||||
login: new VikunjaApp(
|
||||
home: LoginPage(),
|
||||
key: UniqueKey(),
|
||||
)));
|
||||
}
|
||||
|
||||
final ValueNotifier<bool> updateTheme = ValueNotifier(false);
|
||||
|
|
|
@ -4,10 +4,13 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:vikunja_app/api/client.dart';
|
||||
import 'package:vikunja_app/api/user_implementation.dart';
|
||||
import 'package:vikunja_app/constants.dart';
|
||||
import 'package:vikunja_app/global.dart';
|
||||
import 'package:vikunja_app/models/user.dart';
|
||||
import 'package:vikunja_app/pages/user/login_webview.dart';
|
||||
import 'package:vikunja_app/pages/user/register.dart';
|
||||
import 'package:vikunja_app/service/services.dart';
|
||||
import 'package:vikunja_app/theme/button.dart';
|
||||
import 'package:vikunja_app/theme/buttonText.dart';
|
||||
import 'package:vikunja_app/theme/constants.dart';
|
||||
|
@ -26,12 +29,14 @@ class _LoginPageState extends State<LoginPage> {
|
|||
bool _rememberMe = false;
|
||||
bool init = false;
|
||||
List<String> pastServers = [];
|
||||
int amountTaps = 0;
|
||||
DateTime? lastTap;
|
||||
bool _showXClientTokent = false;
|
||||
|
||||
final _serverController = TextEditingController();
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
final _serverSuggestionController = SuggestionsController();
|
||||
final _xClientTokenController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -82,12 +87,15 @@ class _LoginPageState extends State<LoginPage> {
|
|||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 30),
|
||||
child: Image(
|
||||
image: Theme.of(context).brightness == Brightness.dark
|
||||
? AssetImage('assets/vikunja_logo_full_white.png')
|
||||
: AssetImage('assets/vikunja_logo_full.png'),
|
||||
height: 85.0,
|
||||
semanticLabel: 'Vikunja Logo',
|
||||
child: GestureDetector(
|
||||
onTap: _handleLogoTap,
|
||||
child: Image(
|
||||
image: Theme.of(context).brightness == Brightness.dark
|
||||
? AssetImage('assets/vikunja_logo_full_white.png')
|
||||
: AssetImage('assets/vikunja_logo_full.png'),
|
||||
height: 80.0,
|
||||
semanticLabel: 'Vikunja Logo',
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
|
@ -95,9 +103,6 @@ class _LoginPageState extends State<LoginPage> {
|
|||
child: Row(children: [
|
||||
Expanded(
|
||||
child: TypeAheadField(
|
||||
//suggestionsBoxController: _serverSuggestionController,
|
||||
//getImmediateSuggestions: true,
|
||||
//enabled: !_loading,
|
||||
controller: _serverController,
|
||||
builder: (context, controller, focusnode) {
|
||||
return TextFormField(
|
||||
|
@ -116,13 +121,6 @@ class _LoginPageState extends State<LoginPage> {
|
|||
labelText: 'Server Address'),
|
||||
);
|
||||
},
|
||||
/*
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
controller: _serverController,
|
||||
decoration: new InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Server Address'),
|
||||
),*/
|
||||
onSelected: (suggestion) {
|
||||
_serverController.text = suggestion;
|
||||
setState(
|
||||
|
@ -166,22 +164,6 @@ class _LoginPageState extends State<LoginPage> {
|
|||
},
|
||||
),
|
||||
),
|
||||
/*
|
||||
DropdownButton<String>(
|
||||
onChanged: (String? value) {
|
||||
// This is called when the user selects an item.
|
||||
setState(() {
|
||||
if (value != null) _serverController.text = value;
|
||||
});
|
||||
},
|
||||
items: pastServers
|
||||
.map<DropdownMenuItem<String>>((dynamic value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
),*/
|
||||
]),
|
||||
),
|
||||
Padding(
|
||||
|
@ -207,6 +189,17 @@ class _LoginPageState extends State<LoginPage> {
|
|||
obscureText: true,
|
||||
),
|
||||
),
|
||||
if (_showXClientTokent)
|
||||
Padding(
|
||||
padding: vStandardVerticalPadding,
|
||||
child: TextFormField(
|
||||
enabled: !_loading,
|
||||
controller: _xClientTokenController,
|
||||
decoration: new InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'X-Client-Token'),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: vStandardVerticalPadding,
|
||||
child: CheckboxListTile(
|
||||
|
@ -218,14 +211,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||
),
|
||||
Builder(
|
||||
builder: (context) => FancyButton(
|
||||
onPressed: !_loading
|
||||
? () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
Form.of(context).save();
|
||||
_loginUser(context);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
onPressed: !_loading ? _doLogin(context) : null,
|
||||
child: _loading
|
||||
? CircularProgressIndicator()
|
||||
: VikunjaButtonText('Login'),
|
||||
|
@ -239,26 +225,11 @@ class _LoginPageState extends State<LoginPage> {
|
|||
child: VikunjaButtonText('Register'),
|
||||
)),
|
||||
Builder(
|
||||
builder: (context) => FancyButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate() &&
|
||||
_serverController.text.isNotEmpty) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LoginWithWebView(
|
||||
_serverController.text))).then(
|
||||
(btp) {
|
||||
if (btp != null) _loginUserByClientToken(btp);
|
||||
});
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Please enter your frontend url")));
|
||||
}
|
||||
},
|
||||
child: VikunjaButtonText("Login with Frontend"))),
|
||||
builder: (context) => FancyButton(
|
||||
onPressed: _loginWithFrontend,
|
||||
child: VikunjaButtonText("Login with Frontend"),
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: Text("Ignore Certificates"),
|
||||
value: client.ignoreCertificates,
|
||||
|
@ -281,12 +252,41 @@ class _LoginPageState extends State<LoginPage> {
|
|||
);
|
||||
}
|
||||
|
||||
_doLogin(BuildContext context) {
|
||||
return () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
Form.of(context).save();
|
||||
_loginUser(context);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_loginWithFrontend() {
|
||||
if (_formKey.currentState!.validate() &&
|
||||
_serverController.text.isNotEmpty) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
LoginWithWebView(_serverController.text))).then((btp) {
|
||||
if (btp != null) _loginUserByClientToken(btp);
|
||||
});
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Please enter your frontend url"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_loginUser(BuildContext context) async {
|
||||
String _server = _serverController.text;
|
||||
String _username = _usernameController.text;
|
||||
String _password = _passwordController.text;
|
||||
if (_server.isEmpty) return;
|
||||
String _xClientToken = _xClientTokenController.text;
|
||||
|
||||
if (_server.isEmpty) return;
|
||||
if (!pastServers.contains(_server)) pastServers.add(_server);
|
||||
await VikunjaGlobal.of(context).settingsManager.setPastServers(pastServers);
|
||||
|
||||
|
@ -294,16 +294,26 @@ class _LoginPageState extends State<LoginPage> {
|
|||
try {
|
||||
var vGlobal = VikunjaGlobal.of(context);
|
||||
vGlobal.client.showSnackBar = false;
|
||||
vGlobal.client.configure(base: _server);
|
||||
vGlobal.client.configure(base: _server, xClientToken: _xClientToken);
|
||||
Server? info = await vGlobal.serverService.getInfo();
|
||||
if (info == null) throw Exception("Getting server info failed");
|
||||
|
||||
UserTokenPair newUser;
|
||||
|
||||
newUser = await vGlobal.newUserService!
|
||||
.login(_username, _password, rememberMe: this._rememberMe);
|
||||
Client client = Client(
|
||||
vGlobal.snackbarKey,
|
||||
base: _server,
|
||||
xClientToken: _xClientToken,
|
||||
);
|
||||
UserService userService = UserAPIService(client);
|
||||
newUser = await userService.login(
|
||||
_username,
|
||||
_password,
|
||||
rememberMe: this._rememberMe,
|
||||
xClientToken: _xClientToken,
|
||||
);
|
||||
|
||||
if (newUser.error == 1017) {
|
||||
if (newUser.error == ErrorCodeOtpRequired) {
|
||||
TextEditingController totpController = TextEditingController();
|
||||
bool dismissed = true;
|
||||
await showDialog(
|
||||
|
@ -328,35 +338,33 @@ class _LoginPageState extends State<LoginPage> {
|
|||
),
|
||||
);
|
||||
if (!dismissed) {
|
||||
newUser = await vGlobal.newUserService!.login(_username, _password,
|
||||
rememberMe: this._rememberMe, totp: totpController.text);
|
||||
newUser = await userService.login(
|
||||
_username,
|
||||
_password,
|
||||
rememberMe: this._rememberMe,
|
||||
totp: totpController.text,
|
||||
);
|
||||
} else {
|
||||
throw Exception();
|
||||
}
|
||||
}
|
||||
if (newUser.error > 0) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(newUser.errorString)));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(newUser.errorString),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (newUser.error == 0)
|
||||
vGlobal.changeUser(newUser.user!, token: newUser.token, base: _server);
|
||||
vGlobal.changeUser(
|
||||
newUser.user!,
|
||||
token: newUser.token,
|
||||
base: _server,
|
||||
xClientToken: _xClientToken,
|
||||
);
|
||||
} catch (ex) {
|
||||
print(ex);
|
||||
/* log(stacktrace.toString());
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => new AlertDialog(
|
||||
title: Text(
|
||||
'Login failed! Please check your server url and credentials. ' +
|
||||
ex.toString()),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'))
|
||||
],
|
||||
));
|
||||
*/
|
||||
} finally {
|
||||
VikunjaGlobal.of(context).client.showSnackBar = true;
|
||||
setState(() {
|
||||
|
@ -384,4 +392,30 @@ class _LoginPageState extends State<LoginPage> {
|
|||
}
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
|
||||
void _handleLogoTap() {
|
||||
if (lastTap != null &&
|
||||
DateTime.now().difference(lastTap!) < Duration(seconds: 2)) {
|
||||
amountTaps++;
|
||||
} else {
|
||||
amountTaps = 1;
|
||||
}
|
||||
lastTap = DateTime.now();
|
||||
if (amountTaps == 5) {
|
||||
// Show X-Client-Token field
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"X-Client-Token " + (_showXClientTokent ? "hidden" : "shown"),
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_showXClientTokent = !_showXClientTokent;
|
||||
});
|
||||
amountTaps = 0;
|
||||
lastTap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:vikunja_app/api/response.dart';
|
||||
import 'package:vikunja_app/models/label.dart';
|
||||
|
@ -221,8 +220,13 @@ abstract class BucketService {
|
|||
}
|
||||
|
||||
abstract class UserService {
|
||||
Future<UserTokenPair> login(String username, String password,
|
||||
{bool rememberMe = false, String totp});
|
||||
Future<UserTokenPair> login(
|
||||
String username,
|
||||
String password, {
|
||||
bool rememberMe = false,
|
||||
String totp,
|
||||
String? xClientToken,
|
||||
});
|
||||
|
||||
Future<UserTokenPair?> register(String username, email, password);
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user