Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="vikunja-flutter" android:host="callback" />
</intent-filter>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
Expand Down
27 changes: 18 additions & 9 deletions integration_test/concurrent_refresh_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,21 @@ class _RefreshEvent {

class MockSettingsDatasource implements SettingsDatasource {
String? _token;
String? _refreshCookie;
String? _refreshToken;

@override
Future<String?> getUserToken() async => _token;
@override
Future<String?> getRefreshCookie() async => _refreshCookie;
Future<String?> getRefreshToken() async => _refreshToken;
@override
Future<void> saveUserToken(String? token) async => _token = token;
@override
Future<void> saveRefreshCookie(String? cookie) async =>
_refreshCookie = cookie;
Future<void> saveRefreshToken(String? token) async => _refreshToken = token;
@override
Future<void> clearAuthData() async {
_token = null;
_refreshToken = null;
}

@override
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
Expand All @@ -91,7 +95,7 @@ class FileMockSettingsDatasource implements SettingsDatasource {
@override
Future<String?> getUserToken() async => (await _read())['token'];
@override
Future<String?> getRefreshCookie() async => (await _read())['cookie'];
Future<String?> getRefreshToken() async => (await _read())['refreshToken'];

@override
Future<void> saveUserToken(String? token) async {
Expand All @@ -101,12 +105,17 @@ class FileMockSettingsDatasource implements SettingsDatasource {
}

@override
Future<void> saveRefreshCookie(String? cookie) async {
Future<void> saveRefreshToken(String? token) async {
final data = await _read();
data['cookie'] = cookie;
data['refreshToken'] = token;
await _write(data);
}

@override
Future<void> clearAuthData() async {
await _write({});
}

@override
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
Expand Down Expand Up @@ -304,7 +313,7 @@ void main() {
HttpOverrides.global = TestHttpOverrides();
settings = MockSettingsDatasource()
.._token = _oldToken
.._refreshCookie = _oldCookie;
.._refreshToken = _oldCookie;
});

tearDown(() => HttpOverrides.global = null);
Expand Down Expand Up @@ -372,7 +381,7 @@ void main() {

final fileSettings = FileMockSettingsDatasource(settingsFile);
await fileSettings.saveUserToken(_oldToken);
await fileSettings.saveRefreshCookie(_oldCookie);
await fileSettings.saveRefreshToken(_oldCookie);

final client = _createClient(fileSettings);

Expand Down
11 changes: 11 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>vikunja-flutter</string>
</array>
</dict>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
Expand Down
4 changes: 2 additions & 2 deletions lib/core/background_work.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ void callbackDispatcher() {
Future<bool> updateTasks() async {
var datasource = SettingsDatasource(FlutterSecureStorage());
var base = await datasource.getServer();
var refreshCookie = await datasource.getRefreshCookie();
var refreshToken = await datasource.getRefreshToken();

if (refreshCookie == null || base == null) {
if (refreshToken == null || base == null) {
return Future.value(true);
}

Expand Down
100 changes: 50 additions & 50 deletions lib/core/network/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import 'package:logging/logging.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:vikunja_app/core/network/response.dart';
import 'package:vikunja_app/core/network/token_lock.dart';
import 'package:vikunja_app/core/utils/network.dart';
import 'package:vikunja_app/core/utils/constants.dart';
import 'package:vikunja_app/data/data_sources/settings_data_source.dart';
import 'package:vikunja_app/main.dart';
import 'package:vikunja_app/presentation/widgets/string_extension.dart';
Expand All @@ -32,14 +32,17 @@ class Client {

late http.Client _httpClient;

String get base => _base;
String get apiBase => '$_base/api/v1';

Client({required String base}) {
base = base.replaceAll(" ", "");
if (base.endsWith("/")) {
base = base.substring(0, base.length - 1);
}
_base = base.endsWith('/api/v1') ? base : '$base/api/v1';
if (base.endsWith('/api/v1')) {
Comment thread
kolaente marked this conversation as resolved.
base = base.substring(0, base.length - '/api/v1'.length);
}
_base = base;

_httpClient = createClient();
}
Expand Down Expand Up @@ -75,18 +78,12 @@ class Client {
HttpOverrides.global = IgnoreCertHttpOverrides(ignoreCertificates);
}

Future<Map<String, String>> getHeaders([bool? refresh = false]) async {
var headers = {
'Content-Type': 'application/json',
'User-Agent': 'Vikunja Mobile App',
};

if (refresh == true) {
var refreshCookie = await settingsDatasource.getRefreshCookie();
headers['Cookie'] = 'vikunja_refresh_token=$refreshCookie';
} else {
var token = await settingsDatasource.getUserToken();
headers['Authorization'] = token != '' ? 'Bearer $token' : '';
Future<Map<String, String>> getHeaders() async {
var headers = {'Content-Type': 'application/json', 'User-Agent': userAgent};

var token = await settingsDatasource.getUserToken();
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}

return headers;
Expand All @@ -98,7 +95,7 @@ class Client {
Map<String, List<String>>? queryParameters,
}) async {
try {
Uri uri = Uri.tryParse('$base$url')!;
Uri uri = Uri.tryParse('$apiBase$url')!;

uri = Uri(
scheme: uri.scheme,
Expand Down Expand Up @@ -126,7 +123,7 @@ class Client {
try {
return _handleResponseWithRefresh(mapper, () async {
return _httpClient.delete(
'$base$url'.toUri()!,
'$apiBase$url'.toUri()!,
headers: await getHeaders(),
);
});
Expand All @@ -144,7 +141,7 @@ class Client {
var encodedBody = _encoder.convert(body);
return _handleResponseWithRefresh(mapper, () async {
return _httpClient.post(
'$base$url'.toUri()!,
'$apiBase$url'.toUri()!,
headers: await getHeaders(),
body: encodedBody,
);
Expand All @@ -163,7 +160,7 @@ class Client {
var encodedBody = _encoder.convert(body);
return _handleResponseWithRefresh(mapper, () async {
return _httpClient.put(
'$base$url'.toUri()!,
'$apiBase$url'.toUri()!,
headers: await getHeaders(),
body: encodedBody,
);
Expand All @@ -173,6 +170,17 @@ class Client {
}
}

Future<http.Response> postUnauthenticated({
required String url,
dynamic body,
}) async {
return _httpClient.post(
'$apiBase$url'.toUri()!,
headers: {'Content-Type': 'application/json', 'User-Agent': userAgent},
body: _encoder.convert(body),
);
}

io_client.IOClient _createIOClient() {
final httpClient = HttpClient();
if (ignoreCertificates) {
Expand All @@ -181,26 +189,6 @@ class Client {
return io_client.IOClient(httpClient);
}

Future<Response<T>> postWithCookies<T>({
required String url,
T Function(dynamic body)? mapper,
dynamic body,
}) async {
final cookieClient = _createIOClient();
try {
var response = await cookieClient.post(
'$base$url'.toUri()!,
headers: await getHeaders(),
body: _encoder.convert(body),
);
return _handleResponse(response, mapper);
} catch (e, s) {
return _handleException(e, s);
} finally {
cookieClient.close();
}
}

Future<Response<T>> _handleResponse<T>(
http.Response response,
T Function(dynamic body)? mapper,
Expand Down Expand Up @@ -248,24 +236,36 @@ class Client {
return await TokenLock.synchronized(() async {
final refreshClient = _createIOClient();
try {
var refreshToken = await settingsDatasource.getRefreshToken();
if (refreshToken == null || refreshToken.isEmpty) {
return false;
}

var response = await refreshClient.post(
'$base/user/token/refresh'.toUri()!,
headers: await getHeaders(true),
'$apiBase/oauth/token'.toUri()!,
headers: {
'Content-Type': 'application/json',
'User-Agent': userAgent,
},
body: _encoder.convert({
'grant_type': 'refresh_token',
'refresh_token': refreshToken,
}),
);

if (response.statusCode >= 200 && response.statusCode < 400) {
var body = _decoder.convert(utf8.decode(response.bodyBytes));
var newToken = body['token'] as String?;

if (newToken != null && newToken.isNotEmpty) {
var newRefreshCookie = extractRefreshCookie(response.headers);

await settingsDatasource.saveUserToken(newToken);
await settingsDatasource.saveRefreshCookie(newRefreshCookie);

var newAccessToken = body['access_token'] as String?;
var newRefreshToken = body['refresh_token'] as String?;

if (newAccessToken != null && newAccessToken.isNotEmpty) {
await settingsDatasource.saveUserToken(newAccessToken);
if (newRefreshToken != null && newRefreshToken.isNotEmpty) {
await settingsDatasource.saveRefreshToken(newRefreshToken);
}
return true;
}
} else {}
}
} finally {
refreshClient.close();
}
Expand Down
Loading
Loading