handle in cookie manager

This commit is contained in:
mertalev 2026-03-09 15:47:59 -05:00
parent 112b75087a
commit e08f02dcd1
No known key found for this signature in database
GPG Key ID: DF6ABC77AAD98C95
11 changed files with 350 additions and 126 deletions

View File

@ -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
}

View File

@ -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<String, String>()
for (key in json.keys()) {
headerMap[key] = json.getString(key)
val map = Json.decodeFromString<Map<String, String>>(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<List<String>>(serverUrlsJson))
}
val cacheDir = File(File(context.cacheDir, "okhttp"), "api")
@ -162,48 +172,41 @@ object HttpClientManager {
fun setRequestHeaders(headerMap: Map<String, String>, serverUrls: List<String>) {
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<String, String>, serverUrls: List<String>) {
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<String>) {
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<Cookie>()
private var serverUrls = listOf<HttpUrl>()
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<String>) {
serverUrls = urls.mapNotNull { it.toHttpUrlOrNull() }
duplicateAuthCookies()
persist()
}
@Synchronized
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
@ -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<List<SerializedCookie>>(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,
)
}
}
}

View File

@ -185,6 +185,7 @@ interface NetworkApi {
fun hasCertificate(): Boolean
fun getClientPointer(): Long
fun setRequestHeaders(headers: Map<String, String>, serverUrls: List<String>)
fun bootstrapCookies(token: String, serverUrls: List<String>)
companion object {
/** The codec used by NetworkApi. */
@ -299,6 +300,25 @@ interface NetworkApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.bootstrapCookies$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val tokenArg = args[0] as String
val serverUrlsArg = args[1] as List<String>
val wrapped: List<Any?> = try {
api.bootstrapCookies(tokenArg, serverUrlsArg)
listOf(null)
} catch (exception: Throwable) {
NetworkPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@ -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>) -> Unit) {
@ -82,4 +82,8 @@ private class NetworkApiImpl() : NetworkApi {
override fun setRequestHeaders(headers: Map<String, String>, serverUrls: List<String>) {
HttpClientManager.setRequestHeaders(headers, serverUrls)
}
override fun bootstrapCookies(token: String, serverUrls: List<String>) {
HttpClientManager.bootstrapCookies(token, serverUrls)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -49,6 +49,45 @@ class URLSessionManager: NSObject {
session = Self.buildSession(delegate: delegate)
}
static func duplicateAuthCookies(serverUrls: [String]) {
let authCookieNames: Set<String> = ["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

View File

@ -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<Object?, Object?> entry) =>
(b as Map<Object?, Object?>).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key]),
);
return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
(b as Map<Object?, Object?>).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<Object?> _toList() {
return <Object?>[data, password];
return <Object?>[
data,
password,
];
}
Object encode() {
return _toList();
}
return _toList(); }
static ClientCertData decode(Object result) {
result as List<Object?>;
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<Object?> _toList() {
return <Object?>[title, message, cancel, confirm];
return <Object?>[
title,
message,
cancel,
confirm,
];
}
Object encode() {
return _toList();
}
return _toList(); }
static ClientCertPrompt decode(Object result) {
result as List<Object?>;
@ -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<Object?> pigeonChannelCodec = _PigeonCodec();
@ -157,15 +177,15 @@ class NetworkApi {
final String pigeonVar_messageChannelSuffix;
Future<void> 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<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[clientData]);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
@ -180,15 +200,15 @@ class NetworkApi {
}
Future<void> 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<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[promptText]);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
@ -203,15 +223,15 @@ class NetworkApi {
}
Future<void> 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<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
@ -226,15 +246,15 @@ class NetworkApi {
}
Future<bool> 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<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
@ -254,15 +274,15 @@ class NetworkApi {
}
Future<int> 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<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
@ -282,15 +302,38 @@ class NetworkApi {
}
Future<void> setRequestHeaders(Map<String, String> headers, List<String> 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<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[headers, serverUrls]);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
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<void> bootstrapCookies(String token, List<String> serverUrls) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NetworkApi.bootstrapCookies$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[token, serverUrls]);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {

View File

@ -205,23 +205,12 @@ class ApiService implements Authentication {
}
static Map<String, String> getRequestHeaders() {
var accessToken = Store.get(StoreKey.accessToken, "");
var customHeadersStr = Store.get(StoreKey.customHeaders, "");
var header = <String, String>{};
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<String, String>;
}
@override

View File

@ -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<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
final hasVersion = Store.tryGet(StoreKey.version) != null;
@ -109,6 +110,16 @@ Future<void> 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);
}

View File

@ -44,4 +44,6 @@ abstract class NetworkApi {
int getClientPointer();
void setRequestHeaders(Map<String, String> headers, List<String> serverUrls);
void bootstrapCookies(String token, List<String> serverUrls);
}