mirror of
https://github.com/go-vikunja/app
synced 2024-06-02 18:49:47 +00:00
feat: add auth-token
This commit is contained in:
parent
fb5b2de493
commit
58202352aa
|
@ -18,22 +18,37 @@ 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;
|
||||
|
||||
bool operator ==(dynamic otherClient) {
|
||||
return otherClient._token == _token;
|
||||
@override
|
||||
bool operator ==(Object otherClient) {
|
||||
if (otherClient is! Client) return false;
|
||||
return otherClient._token == _token &&
|
||||
otherClient._xClientToken == _xClientToken;
|
||||
}
|
||||
|
||||
Client(this.global_scaffold_key,
|
||||
{String? token, String? base, bool authenticated = false}) {
|
||||
configure(token: token, base: base, authenticated: authenticated);
|
||||
Client(
|
||||
this.global_scaffold_key, {
|
||||
String? token,
|
||||
String? xClientToken,
|
||||
String? base,
|
||||
bool authenticated = false,
|
||||
}) {
|
||||
configure(
|
||||
token: token,
|
||||
xClientToken: xClientToken,
|
||||
base: base,
|
||||
authenticated: authenticated,
|
||||
);
|
||||
}
|
||||
|
||||
http.Client get httpClient {
|
||||
|
@ -65,7 +80,8 @@ class Client {
|
|||
get _headers => {
|
||||
'Authorization': _token != '' ? 'Bearer $_token' : '',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Vikunja Mobile App'
|
||||
'User-Agent': 'Vikunja Mobile App',
|
||||
'X-Client-Token': _xClientToken
|
||||
};
|
||||
|
||||
get headers => _headers;
|
||||
|
@ -73,8 +89,14 @@ class Client {
|
|||
@override
|
||||
int get hashCode => _token.hashCode;
|
||||
|
||||
void configure({String? token, String? base, bool? authenticated}) {
|
||||
void configure({
|
||||
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);
|
||||
|
@ -84,7 +106,7 @@ class Client {
|
|||
}
|
||||
|
||||
void reset() {
|
||||
_token = _base = '';
|
||||
_token = _base = _xClientToken = '';
|
||||
authenticated = false;
|
||||
}
|
||||
|
||||
|
@ -201,7 +223,7 @@ class Client {
|
|||
}
|
||||
|
||||
Response? _handleResponse(http.Response response) {
|
||||
Error? error = _handleResponseErrors(response);
|
||||
_handleResponseErrors(response);
|
||||
return Response(
|
||||
_decoder.convert(response.body), response.statusCode, response.headers);
|
||||
}
|
||||
|
|
|
@ -9,22 +9,29 @@ 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,
|
||||
'username': username,
|
||||
};
|
||||
if(totp != null) {
|
||||
if (totp != null) {
|
||||
body['totp_passcode'] = totp;
|
||||
}
|
||||
}
|
||||
var response = await client.post('/login', body: body);
|
||||
var token = response?.body["token"];
|
||||
if(token == null || response == null || response.error != null)
|
||||
if (token == null || response == null || response.error != null)
|
||||
return Future.value(UserTokenPair(null, null,
|
||||
error: response != null ? response.body["code"] : 0,
|
||||
errorString: response != null ? response.body["message"] : "Login error"));
|
||||
client.configure(token: token);
|
||||
errorString:
|
||||
response != null ? response.body["message"] : "Login error"));
|
||||
client.configure(token: token, xClientToken: xClientToken);
|
||||
return UserAPIService(client)
|
||||
.getCurrentUser()
|
||||
.then((user) => UserTokenPair(user, token));
|
||||
|
@ -46,9 +53,12 @@ class UserAPIService extends APIService implements UserService {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<UserSettings?> setCurrentUserSettings(UserSettings userSettings) async {
|
||||
return client.post('/user/settings/general', body: userSettings.toJson()).then((response) {
|
||||
if(response == null) return null;
|
||||
Future<UserSettings?> setCurrentUserSettings(
|
||||
UserSettings userSettings) async {
|
||||
return client
|
||||
.post('/user/settings/general', body: userSettings.toJson())
|
||||
.then((response) {
|
||||
if (response == null) return null;
|
||||
return userSettings;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -46,95 +46,109 @@ class TaskBottomSheetState extends State<TaskBottomSheet> {
|
|||
Widget build(BuildContext context) {
|
||||
ThemeData theme = Theme.of(context);
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.9,
|
||||
child: Padding(
|
||||
height: MediaQuery.of(context).size.height * 0.9,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(20, 10, 10, 20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
children: <Widget>[
|
||||
Row(
|
||||
// Title and edit button
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(_currentTask.title, style: theme.textTheme.headlineLarge),
|
||||
IconButton(onPressed: () {
|
||||
Navigator.push<Task>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (buildContext) => TaskEditPage(
|
||||
task: _currentTask,
|
||||
taskState: widget.taskState,
|
||||
),
|
||||
),
|
||||
)
|
||||
.then((task) => setState(() {
|
||||
if (task != null) _currentTask = task;
|
||||
}))
|
||||
.whenComplete(() => widget.onEdit());
|
||||
}, icon: Icon(Icons.edit)),
|
||||
],
|
||||
),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
children: _currentTask.labels.map((Label label) {
|
||||
return LabelComponent(
|
||||
label: label,
|
||||
);
|
||||
}).toList()),
|
||||
|
||||
// description with html rendering
|
||||
Text("Description", style: theme.textTheme.headlineSmall),
|
||||
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0),
|
||||
child: HtmlWidget(_currentTask.description.isNotEmpty ? _currentTask.description : "No description"),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
// Title and edit button
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(_currentTask.title,
|
||||
style: theme.textTheme.headlineLarge),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Navigator.push<Task>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (buildContext) => TaskEditPage(
|
||||
task: _currentTask,
|
||||
taskState: widget.taskState,
|
||||
),
|
||||
),
|
||||
)
|
||||
.then((task) => setState(() {
|
||||
if (task != null) _currentTask = task;
|
||||
}))
|
||||
.whenComplete(() => widget.onEdit());
|
||||
},
|
||||
icon: Icon(Icons.edit)),
|
||||
],
|
||||
),
|
||||
// Due date
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.access_time),
|
||||
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
|
||||
Text(_currentTask.dueDate != null ? vDateFormatShort.format(_currentTask.dueDate!.toLocal()) : "No due date"),
|
||||
],
|
||||
),
|
||||
// start date
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.play_arrow_rounded),
|
||||
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
|
||||
Text(_currentTask.startDate != null ? vDateFormatShort.format(_currentTask.startDate!.toLocal()) : "No start date"),
|
||||
],
|
||||
),
|
||||
// end date
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.stop_rounded),
|
||||
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
|
||||
Text(_currentTask.endDate != null ? vDateFormatShort.format(_currentTask.endDate!.toLocal()) : "No end date"),
|
||||
],
|
||||
),
|
||||
// priority
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.priority_high),
|
||||
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
|
||||
Text(_currentTask.priority != null ? priorityToString(_currentTask.priority) : "No priority"),
|
||||
],
|
||||
),
|
||||
// progress
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.percent),
|
||||
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
|
||||
Text(_currentTask.percent_done != null ? (_currentTask.percent_done! * 100).toInt().toString() + "%" : "Unset"),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
children: _currentTask.labels.map((Label label) {
|
||||
return LabelComponent(
|
||||
label: label,
|
||||
);
|
||||
}).toList()),
|
||||
|
||||
);
|
||||
// description with html rendering
|
||||
Text("Description", style: theme.textTheme.headlineSmall),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(10, 0, 0, 0),
|
||||
child: HtmlWidget(_currentTask.description.isNotEmpty
|
||||
? _currentTask.description
|
||||
: "No description"),
|
||||
),
|
||||
// Due date
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.access_time),
|
||||
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
|
||||
Text(_currentTask.dueDate != null
|
||||
? vDateFormatShort.format(_currentTask.dueDate!.toLocal())
|
||||
: "No due date"),
|
||||
],
|
||||
),
|
||||
// start date
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.play_arrow_rounded),
|
||||
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
|
||||
Text(_currentTask.startDate != null
|
||||
? vDateFormatShort
|
||||
.format(_currentTask.startDate!.toLocal())
|
||||
: "No start date"),
|
||||
],
|
||||
),
|
||||
// end date
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.stop_rounded),
|
||||
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
|
||||
Text(_currentTask.endDate != null
|
||||
? vDateFormatShort.format(_currentTask.endDate!.toLocal())
|
||||
: "No end date"),
|
||||
],
|
||||
),
|
||||
// priority
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.priority_high),
|
||||
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
|
||||
Text(_currentTask.priority != null
|
||||
? priorityToString(_currentTask.priority)
|
||||
: "No priority"),
|
||||
],
|
||||
),
|
||||
// progress
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.percent),
|
||||
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
|
||||
Text(_currentTask.percent_done != null
|
||||
? (_currentTask.percent_done! * 100).toInt().toString() +
|
||||
"%"
|
||||
: "Unset"),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
1
lib/constants.dart
Normal file
1
lib/constants.dart
Normal file
|
@ -0,0 +1 @@
|
|||
const ErrorCodeOtpRequired = 1017;
|
|
@ -98,7 +98,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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ void callbackDispatcher() {
|
|||
Client client = Client(null,
|
||||
token: inputData["client_token"],
|
||||
base: inputData["client_base"],
|
||||
xClientToken: inputData["x_client_token"],
|
||||
authenticated: true);
|
||||
tz.initializeTimeZones();
|
||||
|
||||
|
@ -66,13 +67,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) {
|
||||
|
|
|
@ -41,36 +41,35 @@ class _NamespaceOverviewPageState extends State<NamespaceOverviewPage>
|
|||
onTap: () => _onSelectItem(i),
|
||||
)));
|
||||
|
||||
if(_selectedDrawerIndex > -1) {
|
||||
if (_selectedDrawerIndex > -1) {
|
||||
return new WillPopScope(
|
||||
child: NamespacePage(namespace: _namespaces[_selectedDrawerIndex]),
|
||||
onWillPop: () async {setState(() {
|
||||
_selectedDrawerIndex = -2;
|
||||
onWillPop: () async {
|
||||
setState(() {
|
||||
_selectedDrawerIndex = -2;
|
||||
});
|
||||
return false;
|
||||
});
|
||||
return false;});
|
||||
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body:
|
||||
this._loading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
:
|
||||
RefreshIndicator(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: ListTile.divideTiles(
|
||||
context: context, tiles: namespacesList)
|
||||
.toList()),
|
||||
onRefresh: _loadNamespaces,
|
||||
),
|
||||
floatingActionButton: Builder(
|
||||
builder: (context) => FloatingActionButton(
|
||||
onPressed: () => _addNamespaceDialog(context),
|
||||
child: const Icon(Icons.add))),
|
||||
appBar: AppBar(
|
||||
body: this._loading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: RefreshIndicator(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: ListTile.divideTiles(
|
||||
context: context, tiles: namespacesList)
|
||||
.toList()),
|
||||
onRefresh: _loadNamespaces,
|
||||
),
|
||||
floatingActionButton: Builder(
|
||||
builder: (context) => FloatingActionButton(
|
||||
onPressed: () => _addNamespaceDialog(context),
|
||||
child: const Icon(Icons.add))),
|
||||
appBar: AppBar(
|
||||
title: Text("Namespaces"),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -84,11 +83,13 @@ class _NamespaceOverviewPageState extends State<NamespaceOverviewPage>
|
|||
}
|
||||
|
||||
_onSelectItem(int index) {
|
||||
Navigator.push(context,
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (buildContext) => NamespacePage(
|
||||
namespace: _namespaces[index],
|
||||
),));
|
||||
),
|
||||
));
|
||||
//setState(() => _selectedDrawerIndex = index);
|
||||
}
|
||||
|
||||
|
|
|
@ -153,7 +153,11 @@ class _ProjectOverviewPageState extends State<ProjectOverviewPage>
|
|||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('The project was created successfully!'),
|
||||
));
|
||||
}).catchError((error) => showDialog(
|
||||
context: context, builder: (context) => ErrorDialog(error: error)));
|
||||
}).catchError(
|
||||
(error) => showDialog(
|
||||
context: context,
|
||||
builder: (context) => ErrorDialog(error: error),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,10 +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 _xClientTokenController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -80,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(
|
||||
|
@ -93,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(
|
||||
|
@ -114,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(
|
||||
|
@ -164,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(
|
||||
|
@ -205,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(
|
||||
|
@ -216,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'),
|
||||
|
@ -237,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,
|
||||
|
@ -279,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);
|
||||
|
||||
|
@ -292,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(
|
||||
|
@ -326,35 +338,28 @@ 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);
|
||||
} 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(() {
|
||||
|
@ -382,4 +387,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -136,7 +136,6 @@ class MockedListService implements ListService {
|
|||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
void setDefaultList(int? listId) {
|
||||
// TODO: implement setDefaultList
|
||||
}
|
||||
|
@ -190,7 +189,8 @@ class MockedTaskService implements TaskService {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<Response?> getAllByProject(int projectId, [Map<String, List<String>>? queryParameters]) {
|
||||
Future<Response?> getAllByProject(int projectId,
|
||||
[Map<String, List<String>>? queryParameters]) {
|
||||
// TODO: implement getAllByProject
|
||||
return Future.value(new Response(_tasks.values.toList(), 200, {}));
|
||||
}
|
||||
|
@ -198,7 +198,8 @@ class MockedTaskService implements TaskService {
|
|||
|
||||
class MockedUserService implements UserService {
|
||||
@override
|
||||
Future<UserTokenPair> login(String username, password, {bool rememberMe = false, String? totp}) {
|
||||
Future<UserTokenPair> login(String username, password,
|
||||
{bool rememberMe = false, String? totp, String? xClientToken}) {
|
||||
return Future.value(UserTokenPair(_users[1]!, 'abcdefg'));
|
||||
}
|
||||
|
||||
|
@ -223,6 +224,4 @@ class MockedUserService implements UserService {
|
|||
// TODO: implement getToken
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -59,9 +59,9 @@ class TaskServiceOption<T> {
|
|||
dynamic defValue;
|
||||
|
||||
TaskServiceOption(this.name, dynamic input_values) {
|
||||
if(input_values is List<String>) {
|
||||
if (input_values is List<String>) {
|
||||
valueList = input_values;
|
||||
} else if(input_values is String) {
|
||||
} else if (input_values is String) {
|
||||
value = input_values;
|
||||
}
|
||||
}
|
||||
|
@ -81,20 +81,14 @@ class TaskServiceOption<T> {
|
|||
|
||||
final List<TaskServiceOption> defaultOptions = [
|
||||
TaskServiceOption<TaskServiceOptionSortBy>("sort_by",
|
||||
[TaskServiceOptionSortBy.due_date,
|
||||
TaskServiceOptionSortBy.id]),
|
||||
[TaskServiceOptionSortBy.due_date, TaskServiceOptionSortBy.id]),
|
||||
TaskServiceOption<TaskServiceOptionOrderBy>(
|
||||
"order_by", TaskServiceOptionOrderBy.asc),
|
||||
TaskServiceOption<TaskServiceOptionFilterBy>("filter_by", [
|
||||
TaskServiceOptionFilterBy.done,
|
||||
TaskServiceOptionFilterBy.due_date
|
||||
]),
|
||||
TaskServiceOption<TaskServiceOptionFilterValue>("filter_value", [
|
||||
TaskServiceOptionFilterValue.enum_false,
|
||||
'1970-01-01T00:00:00.000Z'
|
||||
]),
|
||||
TaskServiceOption<TaskServiceOptionFilterComparator>(
|
||||
"filter_comparator", [
|
||||
TaskServiceOption<TaskServiceOptionFilterBy>("filter_by",
|
||||
[TaskServiceOptionFilterBy.done, TaskServiceOptionFilterBy.due_date]),
|
||||
TaskServiceOption<TaskServiceOptionFilterValue>("filter_value",
|
||||
[TaskServiceOptionFilterValue.enum_false, '1970-01-01T00:00:00.000Z']),
|
||||
TaskServiceOption<TaskServiceOptionFilterComparator>("filter_comparator", [
|
||||
TaskServiceOptionFilterComparator.equals,
|
||||
TaskServiceOptionFilterComparator.greater
|
||||
]),
|
||||
|
@ -105,13 +99,14 @@ final List<TaskServiceOption> defaultOptions = [
|
|||
class TaskServiceOptions {
|
||||
List<TaskServiceOption> options = [];
|
||||
|
||||
TaskServiceOptions({List<TaskServiceOption>? newOptions, bool clearOther = false}) {
|
||||
if(!clearOther)
|
||||
options = new List<TaskServiceOption>.from(defaultOptions);
|
||||
TaskServiceOptions(
|
||||
{List<TaskServiceOption>? newOptions, bool clearOther = false}) {
|
||||
if (!clearOther) options = new List<TaskServiceOption>.from(defaultOptions);
|
||||
if (newOptions != null) {
|
||||
for (TaskServiceOption custom_option in newOptions) {
|
||||
int index = options.indexWhere((element) => element.name == custom_option.name);
|
||||
if(index > -1) {
|
||||
int index =
|
||||
options.indexWhere((element) => element.name == custom_option.name);
|
||||
if (index > -1) {
|
||||
options.removeAt(index);
|
||||
} else {
|
||||
index = options.length;
|
||||
|
@ -121,13 +116,12 @@ class TaskServiceOptions {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
Map<String, List<String>> getOptions() {
|
||||
Map<String, List<String>> queryparams = {};
|
||||
for (TaskServiceOption option in options) {
|
||||
dynamic value = option.getValue();
|
||||
if (value is List) {
|
||||
queryparams[option.name+"[]"] = value as List<String>;
|
||||
queryparams[option.name + "[]"] = value as List<String>;
|
||||
//for (dynamic valueEntry in value) {
|
||||
// result += '&' + option.name + '[]=' + valueEntry;
|
||||
//}
|
||||
|
@ -151,14 +145,12 @@ abstract class ProjectService {
|
|||
Future<Project?> update(Project p);
|
||||
Future delete(int projectId);
|
||||
|
||||
|
||||
Future<String?> getDisplayDoneTasks(int listId);
|
||||
void setDisplayDoneTasks(int listId, String value);
|
||||
//Future<String?> getDefaultList();
|
||||
//void setDefaultList(int? listId);
|
||||
}
|
||||
|
||||
|
||||
abstract class NamespaceService {
|
||||
Future<List<Namespace>?> getAll();
|
||||
|
||||
|
@ -228,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);
|
||||
|
||||
|
@ -289,7 +286,6 @@ class SettingsManager {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
SettingsManager(this._storage) {
|
||||
applydefaults();
|
||||
}
|
||||
|
@ -297,35 +293,45 @@ class SettingsManager {
|
|||
Future<String?> getIgnoreCertificates() {
|
||||
return _storage.read(key: "ignore-certificates");
|
||||
}
|
||||
|
||||
void setIgnoreCertificates(bool value) {
|
||||
_storage.write(key: "ignore-certificates", value: value ? "1" : "0");
|
||||
}
|
||||
|
||||
Future<bool> getLandingPageOnlyDueDateTasks() {
|
||||
return _storage.read(key: "landing-page-due-date-tasks").then((value) => value == "1");
|
||||
}
|
||||
Future<void> setLandingPageOnlyDueDateTasks(bool value) {
|
||||
return _storage.write(key: "landing-page-due-date-tasks", value: value ? "1" : "0");
|
||||
return _storage
|
||||
.read(key: "landing-page-due-date-tasks")
|
||||
.then((value) => value == "1");
|
||||
}
|
||||
|
||||
Future<void> setLandingPageOnlyDueDateTasks(bool value) {
|
||||
return _storage.write(
|
||||
key: "landing-page-due-date-tasks", value: value ? "1" : "0");
|
||||
}
|
||||
|
||||
Future<String?> getVersionNotifications() {
|
||||
return _storage.read(key: "get-version-notifications");
|
||||
}
|
||||
|
||||
void setVersionNotifications(bool value) {
|
||||
_storage.write(key: "get-version-notifications", value: value ? "1" : "0");
|
||||
}
|
||||
|
||||
|
||||
Future<Duration> getWorkmanagerDuration() {
|
||||
return _storage.read(key: "workmanager-duration").then((value) => Duration(minutes: int.parse(value ?? "0")));
|
||||
return _storage
|
||||
.read(key: "workmanager-duration")
|
||||
.then((value) => Duration(minutes: int.parse(value ?? "0")));
|
||||
}
|
||||
|
||||
Future<void> setWorkmanagerDuration(Duration duration) {
|
||||
return _storage.write(key: "workmanager-duration", value: duration.inMinutes.toString());
|
||||
return _storage.write(
|
||||
key: "workmanager-duration", value: duration.inMinutes.toString());
|
||||
}
|
||||
|
||||
Future<List<String>?> getPastServers() {
|
||||
return _storage.read(key: "recent-servers").then((value) => (jsonDecode(value!) as List<dynamic>).cast<String>());
|
||||
return _storage
|
||||
.read(key: "recent-servers")
|
||||
.then((value) => (jsonDecode(value!) as List<dynamic>).cast<String>());
|
||||
}
|
||||
|
||||
Future<void> setPastServers(List<String>? server) {
|
||||
|
@ -336,18 +342,17 @@ class SettingsManager {
|
|||
|
||||
Future<FlutterThemeMode> getThemeMode() async {
|
||||
String? theme_mode = await _storage.read(key: "theme_mode");
|
||||
if(theme_mode == null)
|
||||
setThemeMode(FlutterThemeMode.system);
|
||||
switch(theme_mode) {
|
||||
if (theme_mode == null) setThemeMode(FlutterThemeMode.system);
|
||||
switch (theme_mode) {
|
||||
case "system":
|
||||
return FlutterThemeMode.system;
|
||||
case "light":
|
||||
return FlutterThemeMode.light;
|
||||
case "dark":
|
||||
return FlutterThemeMode.dark;
|
||||
case "materialYouLight":
|
||||
case "materialYouLight":
|
||||
return FlutterThemeMode.materialYouLight;
|
||||
case "materialYouDark":
|
||||
case "materialYouDark":
|
||||
return FlutterThemeMode.materialYouDark;
|
||||
default:
|
||||
return FlutterThemeMode.system;
|
||||
|
@ -355,9 +360,9 @@ class SettingsManager {
|
|||
}
|
||||
|
||||
Future<void> setThemeMode(FlutterThemeMode newMode) async {
|
||||
await _storage.write(key: "theme_mode", value: newMode.toString().split('.').last);
|
||||
await _storage.write(
|
||||
key: "theme_mode", value: newMode.toString().split('.').last);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum FlutterThemeMode {
|
||||
|
@ -366,4 +371,4 @@ enum FlutterThemeMode {
|
|||
dark,
|
||||
materialYouLight,
|
||||
materialYouDark,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -761,7 +761,7 @@ packages:
|
|||
source: hosted
|
||||
version: "2.1.0"
|
||||
package_info_plus:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
|
||||
|
|
Loading…
Reference in New Issue
Block a user