1
0
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:
Denys Vitali 2024-04-05 15:53:10 +02:00
parent 64fa13ce17
commit 6052d5c4a3
No known key found for this signature in database
GPG Key ID: 5227C664145098BC
7 changed files with 190 additions and 111 deletions

View File

@ -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;
}

View File

@ -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
View File

@ -0,0 +1 @@
const ErrorCodeOtpRequired = 1017;

View File

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

View File

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

View File

@ -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;
}
}
}

View File

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