diff --git a/lib/apis/auth_info_api.dart b/lib/apis/auth_info_api.dart index 9d0e60f..75a665c 100644 --- a/lib/apis/auth_info_api.dart +++ b/lib/apis/auth_info_api.dart @@ -8,7 +8,7 @@ class AuthInfoApi { Future getAuthInfo() async { String apiUrl = "${Preferences.prefs?.getString("BaseUrl")}/auth/info"; - var headers = BaseApi.getHeaders(); + var headers = await BaseApi.getRefreshedHeaders(); var response = await http.get( Uri.parse(apiUrl), diff --git a/lib/apis/base_api.dart b/lib/apis/base_api.dart index 6ed25c5..e92d056 100644 --- a/lib/apis/base_api.dart +++ b/lib/apis/base_api.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; +import 'package:open_media_server_app/auth/login_manager.dart'; +import 'package:open_media_server_app/globals/auth_globals.dart'; import 'package:open_media_server_app/helpers/preferences.dart'; class BaseApi { @@ -6,4 +9,49 @@ class BaseApi { "Authorization": "Bearer ${Preferences.prefs?.getString("AccessToken")}" }; } + + static Future> getRefreshedHeaders() async { + var token = Preferences.prefs?.getString("AccessToken"); + + if (AuthGlobals.authInfo != null && token != null) { + if (isJwtAboutToExpire(token)) { + LoginManager loginManager = LoginManager(AuthGlobals.authInfo!); + token = await loginManager.refreshAsync(AuthGlobals.authInfo!); + } + } + + return {"Authorization": "Bearer $token"}; + } + + static bool isJwtAboutToExpire(String token, {int bufferInSeconds = 120}) { + try { + // Decode the JWT to get its payload + final parts = token.split('.'); + if (parts.length != 3) { + throw Exception("Invalid token format"); + } + + // Base64 decode the payload part of the token + final payload = json + .decode(utf8.decode(base64Url.decode(base64Url.normalize(parts[1])))); + + // Check if the `exp` field exists in the payload + if (!payload.containsKey('exp')) { + throw Exception("Token does not contain expiration date"); + } + + // `exp` is the expiration time in seconds since epoch + final exp = payload['exp'] as int; + + // Get the current time in seconds since epoch + final currentTimeInSeconds = + DateTime.now().millisecondsSinceEpoch ~/ 1000; + + // Check if the token is about to expire within the buffer period + return exp - currentTimeInSeconds <= bufferInSeconds; + } catch (e) { + print("Error decoding token: $e"); + return true; // Assuming it's expired if there's an error + } + } } diff --git a/lib/apis/inventory_api.dart b/lib/apis/inventory_api.dart index 92644d8..3fc6552 100644 --- a/lib/apis/inventory_api.dart +++ b/lib/apis/inventory_api.dart @@ -12,7 +12,7 @@ class InventoryApi { Future> listItems(String category) async { String apiUrl = "${Preferences.prefs?.getString("BaseUrl")}/api/inventory/items?"; - var headers = BaseApi.getHeaders(); + var headers = await BaseApi.getRefreshedHeaders(); var response = await http.get(Uri.parse("${apiUrl}category=$category"), headers: headers); @@ -28,7 +28,7 @@ class InventoryApi { Future getMovie(String id) async { String apiUrl = "${Preferences.prefs?.getString("BaseUrl")}/api/inventory/movie?"; - var headers = BaseApi.getHeaders(); + var headers = await BaseApi.getRefreshedHeaders(); var response = await http.get(Uri.parse("${apiUrl}id=$id"), headers: headers); @@ -44,7 +44,7 @@ class InventoryApi { Future getShow(String id) async { String apiUrl = "${Preferences.prefs?.getString("BaseUrl")}/api/inventory/show?"; - var headers = BaseApi.getHeaders(); + var headers = await BaseApi.getRefreshedHeaders(); var response = await http.get(Uri.parse("${apiUrl}id=$id"), headers: headers); @@ -60,7 +60,7 @@ class InventoryApi { Future getSeason(String id) async { String apiUrl = "${Preferences.prefs?.getString("BaseUrl")}/api/inventory/season?"; - var headers = BaseApi.getHeaders(); + var headers = await BaseApi.getRefreshedHeaders(); var response = await http.get(Uri.parse("${apiUrl}id=$id"), headers: headers); @@ -76,7 +76,7 @@ class InventoryApi { Future getEpisode(String id) async { String apiUrl = "${Preferences.prefs?.getString("BaseUrl")}/api/inventory/episode?"; - var headers = BaseApi.getHeaders(); + var headers = await BaseApi.getRefreshedHeaders(); var response = await http.get(Uri.parse("${apiUrl}id=$id"), headers: headers); diff --git a/lib/apis/metadata_api.dart b/lib/apis/metadata_api.dart index a1f3946..d525a5f 100644 --- a/lib/apis/metadata_api.dart +++ b/lib/apis/metadata_api.dart @@ -8,7 +8,7 @@ class MetadataApi { Future getMetadata(String id, String category) async { String apiUrl = "${Preferences.prefs?.getString("BaseUrl")}/api/metadata?"; - var headers = BaseApi.getHeaders(); + var headers = await BaseApi.getRefreshedHeaders(); var response = await http .get(Uri.parse("${apiUrl}id=$id&category=$category"), headers: headers); diff --git a/lib/auth/login_manager.dart b/lib/auth/login_manager.dart index 096a218..695e9c4 100644 --- a/lib/auth/login_manager.dart +++ b/lib/auth/login_manager.dart @@ -7,7 +7,6 @@ import 'package:oauth2_client/access_token_response.dart'; import 'package:oauth2_client/interfaces.dart'; import 'package:oauth2_client/oauth2_client.dart'; import 'package:open_media_server_app/auth/device_code.dart'; -import 'package:open_media_server_app/globals/globals.dart'; import 'package:open_media_server_app/globals/platform_globals.dart'; import 'package:open_media_server_app/helpers/preferences.dart'; import 'package:open_media_server_app/models/auth/auth_info.dart'; @@ -80,14 +79,10 @@ class LoginManager { return tknResponse.accessToken; } - Future refreshAsync() async { - String clientId = Preferences.prefs!.getString("ClientId")!; - String clientSecret = Preferences.prefs!.getString("ClientSecret")!; - + Future refreshAsync(AuthInfo authInfo) async { var tknResponse = await client.refreshToken( Preferences.prefs!.getString("RefreshToken")!, - clientId: clientId, - clientSecret: Uri.encodeQueryComponent(clientSecret), + clientId: authInfo.clientId, ); Preferences.prefs?.setString("AccessToken", tknResponse.accessToken!); diff --git a/lib/globals/auth_globals.dart b/lib/globals/auth_globals.dart index 01108de..2c01dc0 100644 --- a/lib/globals/auth_globals.dart +++ b/lib/globals/auth_globals.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:open_media_server_app/helpers/preferences.dart'; +import 'package:open_media_server_app/models/auth/auth_info.dart'; class AuthGlobals { static String get redirectUriWeb { @@ -12,4 +13,5 @@ class AuthGlobals { static String? appLoginCodeRoute; -} \ No newline at end of file + static AuthInfo? authInfo; +} diff --git a/lib/views/login.dart b/lib/views/login.dart index 89abb46..c34a2cf 100644 --- a/lib/views/login.dart +++ b/lib/views/login.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:open_media_server_app/apis/auth_info_api.dart'; import 'package:open_media_server_app/auth/login_manager.dart'; +import 'package:open_media_server_app/globals/auth_globals.dart'; import 'package:open_media_server_app/globals/globals.dart'; import 'package:open_media_server_app/helpers/preferences.dart'; @@ -76,7 +77,8 @@ class LoginView extends StatelessWidget { return 'Please enter some text'; } - if (!value.contains("http://") && !value.contains("https://")) { + if (!value.contains("http://") && + !value.contains("https://")) { return 'You need to specify the host in this format\nExample: https://example.com'; } @@ -120,12 +122,20 @@ class LoginView extends StatelessWidget { } Future authenticate(BuildContext context) async { + var refreshToken = Preferences.prefs?.getString("RefreshToken"); + var accessToken = Preferences.prefs?.getString("AccessToken"); + AuthInfoApi authInfoApi = AuthInfoApi(); var info = await authInfoApi.getAuthInfo(); + AuthGlobals.authInfo = info; LoginManager loginManager = LoginManager(info); - var token = await loginManager.login(info, context); + if (refreshToken != null && accessToken != null) { + var token = await loginManager.refreshAsync(info); + } else { + var token = await loginManager.login(info, context); + } Navigator.pushReplacement( context, diff --git a/lib/views/player.dart b/lib/views/player.dart index 8f85ad1..2c279aa 100644 --- a/lib/views/player.dart +++ b/lib/views/player.dart @@ -6,7 +6,6 @@ import 'package:open_media_server_app/apis/base_api.dart'; import 'package:open_media_server_app/globals/platform_globals.dart'; import 'package:open_media_server_app/widgets/player_controls/material_tv.dart'; import 'package:open_media_server_app/helpers/wrapper.dart'; -import 'package:open_media_server_app/globals/globals.dart'; class PlayerView extends StatefulWidget { const PlayerView({super.key, required this.url});