diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt index 42f5fb4b1b..1851392861 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/ImmichAPI.kt @@ -3,6 +3,7 @@ package app.alextran.immich.widget import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.util.Log import app.alextran.immich.widget.model.* import com.google.gson.Gson import com.google.gson.reflect.TypeToken @@ -24,14 +25,23 @@ class ImmichAPI(cfg: ServerConfig) { val serverURL = prefs.getString("widget_server_url", "") ?: "" val sessionKey = prefs.getString("widget_auth_token", "") ?: "" + val customHeadersJSON = prefs.getString("widget_custom_headers", "") ?: "" if (serverURL.isBlank() || sessionKey.isBlank()) { return null } + var customHeaders: Map = HashMap() + + if (customHeadersJSON.isNotBlank()) { + val stringMapType = object : TypeToken>() {}.type + customHeaders = Gson().fromJson(customHeadersJSON, stringMapType) + } + return ServerConfig( serverURL, - sessionKey + sessionKey, + customHeaders ) } } @@ -55,6 +65,12 @@ class ImmichAPI(cfg: ServerConfig) { val connection = (url.openConnection() as HttpURLConnection).apply { requestMethod = "POST" setRequestProperty("Content-Type", "application/json") + + // Custom Headers + serverConfig.customHeaders.forEach { (key, value) -> + setRequestProperty(key, value) + } + doOutput = true } @@ -75,6 +91,11 @@ class ImmichAPI(cfg: ServerConfig) { val url = buildRequestURL("/memories", listOf("for" to iso8601)) val connection = (url.openConnection() as HttpURLConnection).apply { requestMethod = "GET" + + // Custom Headers + serverConfig.customHeaders.forEach { (key, value) -> + setRequestProperty(key, value) + } } val response = connection.inputStream.bufferedReader().readText() @@ -94,6 +115,11 @@ class ImmichAPI(cfg: ServerConfig) { val url = buildRequestURL("/albums") val connection = (url.openConnection() as HttpURLConnection).apply { requestMethod = "GET" + + // Custom Headers + serverConfig.customHeaders.forEach { (key, value) -> + setRequestProperty(key, value) + } } val response = connection.inputStream.bufferedReader().readText() diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt index 9595a3b696..545a1edc59 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/widget/model/Model.kt @@ -55,7 +55,11 @@ data class WidgetEntry ( val deeplink: String? ) -data class ServerConfig(val serverEndpoint: String, val sessionKey: String) +data class ServerConfig( + val serverEndpoint: String, + val sessionKey: String, + val customHeaders: Map +) // MARK: Widget State Keys val kImageUUID = stringPreferencesKey("uuid") diff --git a/mobile/ios/WidgetExtension/ImmichAPI.swift b/mobile/ios/WidgetExtension/ImmichAPI.swift index 36758b824c..907063655f 100644 --- a/mobile/ios/WidgetExtension/ImmichAPI.swift +++ b/mobile/ios/WidgetExtension/ImmichAPI.swift @@ -104,10 +104,13 @@ struct Album: Codable, Equatable { // MARK: API class ImmichAPI { + typealias CustomHeaders = [String:String] struct ServerConfig { let serverEndpoint: String let sessionKey: String + let customHeaders: CustomHeaders } + let serverConfig: ServerConfig init() async throws { @@ -122,10 +125,20 @@ class ImmichAPI { if serverURL == "" || sessionKey == "" { throw WidgetError.noLogin } + + // custom headers come in the form of KV pairs in JSON + var customHeadersJSON = (defaults.string(forKey: "widget_custom_headers") ?? "") + var customHeaders: CustomHeaders = [:] + + if customHeadersJSON != "", + let parsedHeaders = try? JSONDecoder().decode(CustomHeaders.self, from: customHeadersJSON.data(using: .utf8)!) { + customHeaders = parsedHeaders + } serverConfig = ServerConfig( serverEndpoint: serverURL, - sessionKey: sessionKey + sessionKey: sessionKey, + customHeaders: customHeaders ) } @@ -155,6 +168,12 @@ class ImmichAPI { return components?.url } + + func applyHeaders(for request: inout URLRequest) { + for (header, value) in serverConfig.customHeaders { + request.addValue(value, forHTTPHeaderField: header) + } + } func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter) async throws @@ -174,7 +193,8 @@ class ImmichAPI { request.httpMethod = "POST" request.httpBody = try JSONEncoder().encode(filters) request.setValue("application/json", forHTTPHeaderField: "Content-Type") - + applyHeaders(for: &request) + let (data, _) = try await URLSession.shared.data(for: request) // decode data @@ -196,6 +216,7 @@ class ImmichAPI { var request = URLRequest(url: searchURL) request.httpMethod = "GET" + applyHeaders(for: &request) let (data, _) = try await URLSession.shared.data(for: request) @@ -254,7 +275,8 @@ class ImmichAPI { var request = URLRequest(url: searchURL) request.httpMethod = "GET" - + applyHeaders(for: &request) + let (data, _) = try await URLSession.shared.data(for: request) // decode data diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index b3d9d138c4..d984b468d7 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -30,9 +30,10 @@ const int kTimelineAssetLoadBatchSize = 256; const int kTimelineAssetLoadOppositeSize = 64; // Widget keys +const String appShareGroupId = "group.app.immich.share"; const String kWidgetAuthToken = "widget_auth_token"; const String kWidgetServerEndpoint = "widget_server_url"; -const String appShareGroupId = "group.app.immich.share"; +const String kWidgetCustomHeaders = "widget_custom_headers"; // add widget identifiers here for new widgets // these are used to force a widget refresh diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index 02f7920d6f..017b9a454e 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -121,7 +121,9 @@ class AuthNotifier extends StateNotifier { Future saveAuthInfo({required String accessToken}) async { await _apiService.setAccessToken(accessToken); - await _widgetService.writeCredentials(Store.get(StoreKey.serverEndpoint), accessToken); + final serverEndpoint = Store.get(StoreKey.serverEndpoint); + final customHeaders = Store.get(StoreKey.customHeaders); + await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders); // Get the deviceid from the store if it exists, otherwise generate a new one String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; diff --git a/mobile/lib/services/widget.service.dart b/mobile/lib/services/widget.service.dart index 8c5d21b389..4ff18b56e6 100644 --- a/mobile/lib/services/widget.service.dart +++ b/mobile/lib/services/widget.service.dart @@ -11,10 +11,11 @@ class WidgetService { const WidgetService(this._repository); - Future writeCredentials(String serverURL, String sessionKey) async { + Future writeCredentials(String serverURL, String sessionKey, String customHeaders) async { await _repository.setAppGroupId(appShareGroupId); await _repository.saveData(kWidgetServerEndpoint, serverURL); await _repository.saveData(kWidgetAuthToken, sessionKey); + await _repository.saveData(kWidgetCustomHeaders, customHeaders); // wait 3 seconds to ensure the widget is updated, dont block Future.delayed(const Duration(seconds: 3), refreshWidgets); @@ -24,6 +25,7 @@ class WidgetService { await _repository.setAppGroupId(appShareGroupId); await _repository.saveData(kWidgetServerEndpoint, ""); await _repository.saveData(kWidgetAuthToken, ""); + await _repository.saveData(kWidgetCustomHeaders, ""); // wait 3 seconds to ensure the widget is updated, dont block Future.delayed(const Duration(seconds: 3), refreshWidgets);