From e08f02dcd1d827aa9ed59404a8374175cefa07c3 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:47:59 -0500 Subject: [PATCH] handle in cookie manager --- mobile/android/app/build.gradle | 1 + .../alextran/immich/core/HttpClientManager.kt | 177 ++++++++++++++---- .../app/alextran/immich/core/Network.g.kt | 20 ++ .../alextran/immich/core/NetworkApiPlugin.kt | 6 +- mobile/ios/Runner/Core/Network.g.swift | 17 ++ mobile/ios/Runner/Core/NetworkApiImpl.swift | 59 +++--- .../ios/Runner/Core/URLSessionManager.swift | 39 ++++ mobile/lib/platform/network_api.g.dart | 127 ++++++++----- mobile/lib/services/api.service.dart | 15 +- mobile/lib/utils/migration.dart | 13 +- mobile/pigeon/network_api.dart | 2 + 11 files changed, 350 insertions(+), 126 deletions(-) diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 0839000dd0..bd90986f60 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -3,6 +3,7 @@ plugins { id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" id 'com.google.devtools.ksp' + id 'org.jetbrains.kotlin.plugin.serialization' id 'org.jetbrains.kotlin.plugin.compose' version '2.0.20' // this version matches your Kotlin version } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/HttpClientManager.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/HttpClientManager.kt index c03c59045d..25c05465fa 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/HttpClientManager.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/HttpClientManager.kt @@ -16,7 +16,9 @@ import okhttp3.Headers import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.OkHttpClient -import org.json.JSONObject +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.io.ByteArrayInputStream import java.io.File import java.net.Socket @@ -37,6 +39,8 @@ private const val PREFS_NAME = "immich.ssl" private const val PREFS_CERT_ALIAS = "immich.client_cert" private const val PREFS_HEADERS = "immich.request_headers" private const val PREFS_SERVER_URL = "immich.server_url" +private const val PREFS_SERVER_URLS = "immich.server_urls" +private const val PREFS_COOKIES = "immich.cookies" /** * Manages a shared OkHttpClient with SSL configuration support. @@ -62,7 +66,7 @@ object HttpClientManager { var headers: Headers = Headers.headersOf() private set - private val cookieJar = InMemoryCookieJar() + private val cookieJar = PersistentCookieJar() val isMtls: Boolean get() = keyChainAlias != null || keyStore.containsAlias(CERT_ALIAS) @@ -75,15 +79,21 @@ object HttpClientManager { prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) keyChainAlias = prefs.getString(PREFS_CERT_ALIAS, null) + cookieJar.init(prefs) + val savedHeaders = prefs.getString(PREFS_HEADERS, null) if (savedHeaders != null) { - val json = JSONObject(savedHeaders) - val headerMap = mutableMapOf() - for (key in json.keys()) { - headerMap[key] = json.getString(key) + val map = Json.decodeFromString>(savedHeaders) + val builder = Headers.Builder() + for ((key, value) in map) { + if (key != "x-immich-user-token") builder.add(key, value) } - val serverUrl = prefs.getString(PREFS_SERVER_URL, null) - applyHeaders(headerMap, listOfNotNull(serverUrl)) + headers = builder.build() + } + + val serverUrlsJson = prefs.getString(PREFS_SERVER_URLS, null) + if (serverUrlsJson != null) { + cookieJar.setServerUrls(Json.decodeFromString>(serverUrlsJson)) } val cacheDir = File(File(context.cacheDir, "okhttp"), "api") @@ -162,48 +172,41 @@ object HttpClientManager { fun setRequestHeaders(headerMap: Map, serverUrls: List) { synchronized(this) { - applyHeaders(headerMap, serverUrls) - val newUrl = serverUrls.firstOrNull() + val builder = Headers.Builder() + headerMap.forEach { (key, value) -> builder[key] = value } + headers = builder.build() + + cookieJar.setServerUrls(serverUrls) + prefs.edit { - putString(PREFS_HEADERS, JSONObject(headerMap).toString()) + putString(PREFS_HEADERS, Json.encodeToString(headerMap)) + putString(PREFS_SERVER_URLS, Json.encodeToString(serverUrls)) + val newUrl = serverUrls.firstOrNull() if (newUrl != null) putString(PREFS_SERVER_URL, newUrl) else remove(PREFS_SERVER_URL) } } } - private fun applyHeaders(headerMap: Map, serverUrls: List) { - val token = headerMap["x-immich-user-token"] - val builder = Headers.Builder() - headerMap.forEach { (key, value) -> - if (key != "x-immich-user-token") builder[key] = value - } - headers = builder.build() - if (token == null) return - + fun bootstrapCookies(token: String, serverUrls: List) { + val url = serverUrls.firstNotNullOfOrNull { it.toHttpUrlOrNull() } ?: return val expiry = System.currentTimeMillis() + 400L * 24 * 60 * 60 * 1000 - for (serverUrl in serverUrls) { - val url = serverUrl.toHttpUrlOrNull() ?: continue - cookieJar.saveFromResponse(url, listOf( - cookie(url, "immich_access_token", token, expiry, httpOnly = true), - cookie(url, "immich_is_authenticated", "true", expiry, httpOnly = false), - cookie(url, "immich_auth_type", "password", expiry, httpOnly = true), - )) - } - } - - private fun cookie(url: HttpUrl, name: String, value: String, expiry: Long, httpOnly: Boolean): Cookie { - return Cookie.Builder().name(name).value(value).domain(url.host).path("/").expiresAt(expiry) - .apply { - if (url.isHttps) secure() - if (httpOnly) httpOnly() - }.build() + fun cookie(name: String, value: String, httpOnly: Boolean) = + Cookie.Builder().name(name).value(value).domain(url.host).path("/").expiresAt(expiry) + .apply { + if (url.isHttps) secure() + if (httpOnly) httpOnly() + }.build() + cookieJar.saveFromResponse(url, listOf( + cookie("immich_access_token", token, httpOnly = true), + cookie("immich_is_authenticated", "true", httpOnly = false), + cookie("immich_auth_type", "password", httpOnly = true), + )) } fun loadCookieHeader(url: String): String? { val httpUrl = url.toHttpUrlOrNull() ?: return null - val cookies = cookieJar.loadForRequest(httpUrl) - if (cookies.isEmpty()) return null - return cookies.joinToString("; ") { "${it.name}=${it.value}" } + return cookieJar.loadForRequest(httpUrl).takeIf { it.isNotEmpty() } + ?.joinToString("; ") { "${it.name}=${it.value}" } } private fun build(cacheDir: File): OkHttpClient { @@ -285,8 +288,31 @@ object HttpClientManager { ): String? = null } - private class InMemoryCookieJar : CookieJar { + /** + * Persistent CookieJar that duplicates auth cookies across equivalent server URLs. + * When the server sets cookies for one domain, copies are created for all other known + * server domains (for URL switching between local/remote endpoints of the same server). + */ + private class PersistentCookieJar : CookieJar { private val store = mutableListOf() + private var serverUrls = listOf() + private var prefs: SharedPreferences? = null + + companion object { + val AUTH_COOKIE_NAMES = setOf("immich_access_token", "immich_is_authenticated", "immich_auth_type") + } + + fun init(prefs: SharedPreferences) { + this.prefs = prefs + restore() + } + + @Synchronized + fun setServerUrls(urls: List) { + serverUrls = urls.mapNotNull { it.toHttpUrlOrNull() } + duplicateAuthCookies() + persist() + } @Synchronized override fun saveFromResponse(url: HttpUrl, cookies: List) { @@ -294,6 +320,8 @@ object HttpClientManager { cookies.any { it.name == existing.name && it.domain == existing.domain && it.path == existing.path } } store.addAll(cookies) + if (serverUrls.any { it.host == url.host }) duplicateAuthCookies() + persist() } @Synchronized @@ -302,5 +330,74 @@ object HttpClientManager { store.removeAll { it.expiresAt < now } return store.filter { it.matches(url) } } + + private fun duplicateAuthCookies() { + val sourceCookies = store.filter { it.name in AUTH_COOKIE_NAMES }.associateBy { it.name } + if (sourceCookies.isEmpty()) return + + for (url in serverUrls) { + for ((_, source) in sourceCookies) { + if (store.any { it.name == source.name && it.domain == url.host && it.value == source.value }) continue + store.removeAll { it.name == source.name && it.domain == url.host } + store.add(rebuildCookie(source, url)) + } + } + } + + private fun rebuildCookie(source: Cookie, url: HttpUrl): Cookie { + return Cookie.Builder() + .name(source.name).value(source.value) + .domain(url.host).path("/") + .expiresAt(source.expiresAt) + .apply { + if (url.isHttps) secure() + if (source.httpOnly) httpOnly() + } + .build() + } + + private fun persist() { + val p = prefs ?: return + p.edit { putString(PREFS_COOKIES, Json.encodeToString(store.map { SerializedCookie.from(it) })) } + } + + private fun restore() { + val p = prefs ?: return + val jsonStr = p.getString(PREFS_COOKIES, null) ?: return + try { + store.addAll(Json.decodeFromString>(jsonStr).map { it.toCookie() }) + } catch (_: Exception) { + store.clear() + } + } + } + + @Serializable + private data class SerializedCookie( + val name: String, + val value: String, + val domain: String, + val path: String, + val expiresAt: Long, + val secure: Boolean, + val httpOnly: Boolean, + val hostOnly: Boolean, + ) { + fun toCookie(): Cookie = Cookie.Builder() + .name(name).value(value).path(path).expiresAt(expiresAt) + .apply { + if (hostOnly) hostOnlyDomain(domain) else domain(domain) + if (secure) secure() + if (httpOnly) httpOnly() + } + .build() + + companion object { + fun from(cookie: Cookie) = SerializedCookie( + name = cookie.name, value = cookie.value, domain = cookie.domain, + path = cookie.path, expiresAt = cookie.expiresAt, secure = cookie.secure, + httpOnly = cookie.httpOnly, hostOnly = cookie.hostOnly, + ) + } } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt index 5e48d7fef5..f40f29cb5b 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt @@ -185,6 +185,7 @@ interface NetworkApi { fun hasCertificate(): Boolean fun getClientPointer(): Long fun setRequestHeaders(headers: Map, serverUrls: List) + fun bootstrapCookies(token: String, serverUrls: List) companion object { /** The codec used by NetworkApi. */ @@ -299,6 +300,25 @@ interface NetworkApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.bootstrapCookies$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val tokenArg = args[0] as String + val serverUrlsArg = args[1] as List + val wrapped: List = try { + api.bootstrapCookies(tokenArg, serverUrlsArg) + listOf(null) + } catch (exception: Throwable) { + NetworkPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } } } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt index 384c94cce9..57b2cc9397 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt @@ -39,7 +39,7 @@ class NetworkApiPlugin : FlutterPlugin, ActivityAware { } } -private class NetworkApiImpl() : NetworkApi { +private class NetworkApiImpl : NetworkApi { var activity: Activity? = null override fun addCertificate(clientData: ClientCertData, callback: (Result) -> Unit) { @@ -82,4 +82,8 @@ private class NetworkApiImpl() : NetworkApi { override fun setRequestHeaders(headers: Map, serverUrls: List) { HttpClientManager.setRequestHeaders(headers, serverUrls) } + + override fun bootstrapCookies(token: String, serverUrls: List) { + HttpClientManager.bootstrapCookies(token, serverUrls) + } } diff --git a/mobile/ios/Runner/Core/Network.g.swift b/mobile/ios/Runner/Core/Network.g.swift index 96294c1cd4..ecd5153378 100644 --- a/mobile/ios/Runner/Core/Network.g.swift +++ b/mobile/ios/Runner/Core/Network.g.swift @@ -226,6 +226,7 @@ protocol NetworkApi { func hasCertificate() throws -> Bool func getClientPointer() throws -> Int64 func setRequestHeaders(headers: [String: String], serverUrls: [String]) throws + func bootstrapCookies(token: String, serverUrls: [String]) throws } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -325,5 +326,21 @@ class NetworkApiSetup { } else { setRequestHeadersChannel.setMessageHandler(nil) } + let bootstrapCookiesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.bootstrapCookies\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + bootstrapCookiesChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let tokenArg = args[0] as! String + let serverUrlsArg = args[1] as! [String] + do { + try api.bootstrapCookies(token: tokenArg, serverUrls: serverUrlsArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + bootstrapCookiesChannel.setMessageHandler(nil) + } } } diff --git a/mobile/ios/Runner/Core/NetworkApiImpl.swift b/mobile/ios/Runner/Core/NetworkApiImpl.swift index 480286b2af..4b30db7644 100644 --- a/mobile/ios/Runner/Core/NetworkApiImpl.swift +++ b/mobile/ios/Runner/Core/NetworkApiImpl.swift @@ -59,41 +59,42 @@ class NetworkApiImpl: NetworkApi { } func setRequestHeaders(headers: [String : String], serverUrls: [String]) throws { - var headers = headers - if let token = headers.removeValue(forKey: "x-immich-user-token") { - for serverUrl in serverUrls { - guard let url = URL(string: serverUrl), let domain = url.host else { continue } - let isSecure = serverUrl.hasPrefix("https") - let cookies: [(String, String, Bool)] = [ - ("immich_access_token", token, true), - ("immich_is_authenticated", "true", false), - ("immich_auth_type", "password", true), - ] - let expiry = Date().addingTimeInterval(400 * 24 * 60 * 60) - for (name, value, httpOnly) in cookies { - var properties: [HTTPCookiePropertyKey: Any] = [ - .name: name, - .value: value, - .domain: domain, - .path: "/", - .expires: expiry, - ] - if isSecure { properties[.secure] = "TRUE" } - if httpOnly { properties[.init("HttpOnly")] = "TRUE" } - if let cookie = HTTPCookie(properties: properties) { - URLSessionManager.cookieStorage.setCookie(cookie) - } - } - } - } - if serverUrls.first != UserDefaults.group.string(forKey: SERVER_URL_KEY) { UserDefaults.group.set(serverUrls.first, forKey: SERVER_URL_KEY) } + URLSessionManager.duplicateAuthCookies(serverUrls: serverUrls) + if headers != UserDefaults.group.dictionary(forKey: HEADERS_KEY) as? [String: String] { UserDefaults.group.set(headers, forKey: HEADERS_KEY) - URLSessionManager.shared.recreateSession() // Recreate session to apply custom headers without app restart + URLSessionManager.shared.recreateSession() + } + } + + func bootstrapCookies(token: String, serverUrls: [String]) throws { + let expiry = Date().addingTimeInterval(400 * 24 * 60 * 60) + for serverUrl in serverUrls { + guard let url = URL(string: serverUrl), let domain = url.host else { continue } + let isSecure = serverUrl.hasPrefix("https") + let cookies: [(String, String, Bool)] = [ + ("immich_access_token", token, true), + ("immich_is_authenticated", "true", false), + ("immich_auth_type", "password", true), + ] + for (name, value, httpOnly) in cookies { + var properties: [HTTPCookiePropertyKey: Any] = [ + .name: name, + .value: value, + .domain: domain, + .path: "/", + .expires: expiry, + ] + if isSecure { properties[.secure] = "TRUE" } + if httpOnly { properties[.init("HttpOnly")] = "TRUE" } + if let cookie = HTTPCookie(properties: properties) { + URLSessionManager.cookieStorage.setCookie(cookie) + } + } } } } diff --git a/mobile/ios/Runner/Core/URLSessionManager.swift b/mobile/ios/Runner/Core/URLSessionManager.swift index 411b828ea1..b183f2b1a1 100644 --- a/mobile/ios/Runner/Core/URLSessionManager.swift +++ b/mobile/ios/Runner/Core/URLSessionManager.swift @@ -49,6 +49,45 @@ class URLSessionManager: NSObject { session = Self.buildSession(delegate: delegate) } + static func duplicateAuthCookies(serverUrls: [String]) { + let authCookieNames: Set = ["immich_access_token", "immich_is_authenticated", "immich_auth_type"] + let allCookies = cookieStorage.cookies ?? [] + + var sourceCookies: [String: HTTPCookie] = [:] + for cookie in allCookies { + if authCookieNames.contains(cookie.name) { + sourceCookies[cookie.name] = cookie + } + } + + guard !sourceCookies.isEmpty else { return } + + for serverUrl in serverUrls { + guard let url = URL(string: serverUrl), let domain = url.host else { continue } + let isSecure = serverUrl.hasPrefix("https") + + for (_, source) in sourceCookies { + if allCookies.contains(where: { $0.name == source.name && $0.domain == domain && $0.value == source.value }) { + continue + } + + var properties: [HTTPCookiePropertyKey: Any] = [ + .name: source.name, + .value: source.value, + .domain: domain, + .path: "/", + .expires: source.expiresDate ?? Date().addingTimeInterval(400 * 24 * 60 * 60), + ] + if isSecure { properties[.secure] = "TRUE" } + if source.isHTTPOnly { properties[.init("HttpOnly")] = "TRUE" } + + if let cookie = HTTPCookie(properties: properties) { + cookieStorage.setCookie(cookie) + } + } + } + } + private static func buildSession(delegate: URLSessionManagerDelegate) -> URLSession { let config = URLSessionConfiguration.default config.urlCache = urlCache diff --git a/mobile/lib/platform/network_api.g.dart b/mobile/lib/platform/network_api.g.dart index 314a943f7d..84050fd08a 100644 --- a/mobile/lib/platform/network_api.g.dart +++ b/mobile/lib/platform/network_api.g.dart @@ -14,39 +14,47 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } - bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { - return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && - a.entries.every( - (MapEntry entry) => - (b as Map).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key]), - ); + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); } return a == b; } + class ClientCertData { - ClientCertData({required this.data, required this.password}); + ClientCertData({ + required this.data, + required this.password, + }); Uint8List data; String password; List _toList() { - return [data, password]; + return [ + data, + password, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static ClientCertData decode(Object result) { result as List; - return ClientCertData(data: result[0]! as Uint8List, password: result[1]! as String); + return ClientCertData( + data: result[0]! as Uint8List, + password: result[1]! as String, + ); } @override @@ -63,11 +71,17 @@ class ClientCertData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class ClientCertPrompt { - ClientCertPrompt({required this.title, required this.message, required this.cancel, required this.confirm}); + ClientCertPrompt({ + required this.title, + required this.message, + required this.cancel, + required this.confirm, + }); String title; @@ -78,12 +92,16 @@ class ClientCertPrompt { String confirm; List _toList() { - return [title, message, cancel, confirm]; + return [ + title, + message, + cancel, + confirm, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static ClientCertPrompt decode(Object result) { result as List; @@ -109,9 +127,11 @@ class ClientCertPrompt { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -119,10 +139,10 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is ClientCertData) { + } else if (value is ClientCertData) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is ClientCertPrompt) { + } else if (value is ClientCertPrompt) { buffer.putUint8(130); writeValue(buffer, value.encode()); } else { @@ -133,9 +153,9 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: return ClientCertData.decode(readValue(buffer)!); - case 130: + case 130: return ClientCertPrompt.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -148,8 +168,8 @@ class NetworkApi { /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. NetworkApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -157,15 +177,15 @@ class NetworkApi { final String pigeonVar_messageChannelSuffix; Future addCertificate(ClientCertData clientData) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate$pigeonVar_messageChannelSuffix'; + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([clientData]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -180,15 +200,15 @@ class NetworkApi { } Future selectCertificate(ClientCertPrompt promptText) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$pigeonVar_messageChannelSuffix'; + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([promptText]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -203,15 +223,15 @@ class NetworkApi { } Future removeCertificate() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate$pigeonVar_messageChannelSuffix'; + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -226,15 +246,15 @@ class NetworkApi { } Future hasCertificate() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.hasCertificate$pigeonVar_messageChannelSuffix'; + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.hasCertificate$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -254,15 +274,15 @@ class NetworkApi { } Future getClientPointer() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.getClientPointer$pigeonVar_messageChannelSuffix'; + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.getClientPointer$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -282,15 +302,38 @@ class NetworkApi { } Future setRequestHeaders(Map headers, List serverUrls) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.setRequestHeaders$pigeonVar_messageChannelSuffix'; + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.setRequestHeaders$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([headers, serverUrls]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future bootstrapCookies(String token, List serverUrls) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.bootstrapCookies$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([token, serverUrls]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index 566ec7aa31..9a0a8f5d81 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -205,23 +205,12 @@ class ApiService implements Authentication { } static Map getRequestHeaders() { - var accessToken = Store.get(StoreKey.accessToken, ""); var customHeadersStr = Store.get(StoreKey.customHeaders, ""); - var header = {}; - if (accessToken.isNotEmpty) { - header['x-immich-user-token'] = accessToken; - } - if (customHeadersStr.isEmpty) { - return header; + return const {}; } - var customHeaders = jsonDecode(customHeadersStr) as Map; - customHeaders.forEach((key, value) { - header[key] = value; - }); - - return header; + return jsonDecode(customHeadersStr) as Map; } @override diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 6b6f1b251b..e6302764b7 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -27,6 +27,7 @@ import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/network_api.g.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/datetime_helpers.dart'; import 'package:immich_mobile/utils/debug_print.dart'; @@ -35,7 +36,7 @@ import 'package:isar/isar.dart'; // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; -const int targetVersion = 24; +const int targetVersion = 25; Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { final hasVersion = Store.tryGet(StoreKey.version) != null; @@ -109,6 +110,16 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { await _applyLocalAssetOrientation(drift); } + if (version < 25) { + final accessToken = Store.tryGet(StoreKey.accessToken); + if (accessToken != null && accessToken.isNotEmpty) { + final serverUrls = ApiService.getServerUrls(); + if (serverUrls.isNotEmpty) { + await networkApi.bootstrapCookies(accessToken, serverUrls); + } + } + } + if (version < 22 && !Store.isBetaTimelineEnabled) { await Store.put(StoreKey.needBetaMigration, true); } diff --git a/mobile/pigeon/network_api.dart b/mobile/pigeon/network_api.dart index 3ea29052d9..4878c627e4 100644 --- a/mobile/pigeon/network_api.dart +++ b/mobile/pigeon/network_api.dart @@ -44,4 +44,6 @@ abstract class NetworkApi { int getClientPointer(); void setRequestHeaders(Map headers, List serverUrls); + + void bootstrapCookies(String token, List serverUrls); }