use cookiejar

This commit is contained in:
mertalev 2026-03-09 13:33:15 -05:00
parent a47b232235
commit 737486a2d6
No known key found for this signature in database
GPG Key ID: DF6ABC77AAD98C95
15 changed files with 390 additions and 119 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

@ -8,11 +8,17 @@ import app.alextran.immich.BuildConfig
import app.alextran.immich.NativeBuffer
import okhttp3.Cache
import okhttp3.ConnectionPool
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.Credentials
import okhttp3.Dispatcher
import okhttp3.Headers
import okhttp3.Credentials
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
@ -33,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.
@ -58,6 +66,8 @@ object HttpClientManager {
var headers: Headers = Headers.headersOf()
private set
private val cookieJar = PersistentCookieJar()
val isMtls: Boolean get() = keyChainAlias != null || keyStore.containsAlias(CERT_ALIAS)
fun initialize(context: Context) {
@ -69,16 +79,23 @@ 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 map = Json.decodeFromString<Map<String, String>>(savedHeaders)
val builder = Headers.Builder()
for (key in json.keys()) {
builder.add(key, json.getString(key))
for ((key, value) in map) {
builder.add(key, value)
}
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")
client = build(cacheDir)
initialized = true
@ -158,20 +175,46 @@ object HttpClientManager {
val builder = Headers.Builder()
headerMap.forEach { (key, value) -> builder[key] = value }
val newHeaders = builder.build()
val headersChanged = headers != newHeaders
val newUrl = serverUrls.firstOrNull()
val urlChanged = newUrl != prefs.getString(PREFS_SERVER_URL, null)
if (!headersChanged && !urlChanged) return
headers = newHeaders
prefs.edit {
if (headersChanged) putString(PREFS_HEADERS, JSONObject(headerMap).toString())
if (urlChanged) {
cookieJar.setServerUrls(serverUrls)
if (headersChanged || urlChanged) {
prefs.edit {
putString(PREFS_HEADERS, Json.encodeToString(headerMap))
putString(PREFS_SERVER_URLS, Json.encodeToString(serverUrls))
if (newUrl != null) putString(PREFS_SERVER_URL, newUrl) else remove(PREFS_SERVER_URL)
}
}
}
}
fun bootstrapCookies(token: String, serverUrls: List<String>) {
val url = serverUrls.firstNotNullOfOrNull { it.toHttpUrlOrNull() } ?: return
val expiry = System.currentTimeMillis() + 400L * 24 * 60 * 60 * 1000
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
return cookieJar.loadForRequest(httpUrl).takeIf { it.isNotEmpty() }
?.joinToString("; ") { "${it.name}=${it.value}" }
}
private fun build(cacheDir: File): OkHttpClient {
val connectionPool = ConnectionPool(
maxIdleConnections = KEEP_ALIVE_CONNECTIONS,
@ -188,6 +231,7 @@ object HttpClientManager {
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
return OkHttpClient.Builder()
.cookieJar(cookieJar)
.addInterceptor {
val request = it.request()
val builder = request.newBuilder()
@ -249,4 +293,124 @@ object HttpClientManager {
socket: Socket?
): String? = null
}
/**
* 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>) {
val parsed = urls.mapNotNull { it.toHttpUrlOrNull() }
if (parsed.map { it.host } == serverUrls.map { it.host }) return
serverUrls = parsed
if (duplicateAuthCookies()) persist()
}
@Synchronized
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val changed = cookies.any { new ->
store.none { it.name == new.name && it.domain == new.domain && it.path == new.path && it.value == new.value }
}
store.removeAll { existing ->
cookies.any { it.name == existing.name && it.domain == existing.domain && it.path == existing.path }
}
store.addAll(cookies)
val duplicated = serverUrls.any { it.host == url.host } && duplicateAuthCookies()
if (changed || duplicated) persist()
}
@Synchronized
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val now = System.currentTimeMillis()
store.removeAll { it.expiresAt < now }
return store.filter { it.matches(url) }
}
private fun duplicateAuthCookies(): Boolean {
val sourceCookies = store.filter { it.name in AUTH_COOKIE_NAMES }.associateBy { it.name }
if (sourceCookies.isEmpty()) return false
var changed = false
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))
changed = true
}
}
return changed
}
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

@ -192,6 +192,7 @@ private class CronetImageFetcher(context: Context, cacheDir: File) : ImageFetche
val callback = FetchCallback(onSuccess, onFailure, ::onComplete)
val requestBuilder = engine.newUrlRequestBuilder(url, callback, executor)
HttpClientManager.headers.forEach { (key, value) -> requestBuilder.addHeader(key, value) }
HttpClientManager.loadCookieHeader(url)?.let { requestBuilder.addHeader("Cookie", it) }
url.toHttpUrlOrNull()?.let { httpUrl ->
if (httpUrl.username.isNotEmpty()) {
requestBuilder.addHeader("Authorization", Credentials.basic(httpUrl.username, httpUrl.password))

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

@ -123,7 +123,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
}
Future<bool> saveAuthInfo({required String accessToken}) async {
await _apiService.setAccessToken(accessToken);
await Store.put(StoreKey.accessToken, accessToken);
await _apiService.updateHeaders();
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
@ -145,7 +145,6 @@ class AuthNotifier extends StateNotifier<AuthState> {
user = serverUser;
await Store.put(StoreKey.deviceId, deviceId);
await Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
await Store.put(StoreKey.accessToken, accessToken);
}
} on ApiException catch (error, stackTrace) {
if (error.code == 401) {

View File

@ -11,7 +11,7 @@ import 'package:immich_mobile/utils/url_helper.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
class ApiService implements Authentication {
class ApiService {
late ApiClient _apiClient;
late UsersApi usersApi;
@ -45,7 +45,6 @@ class ApiService implements Authentication {
setEndpoint(endpoint);
}
}
String? _accessToken;
final _log = Logger("ApiService");
Future<void> updateHeaders() async {
@ -54,11 +53,8 @@ class ApiService implements Authentication {
}
setEndpoint(String endpoint) {
_apiClient = ApiClient(basePath: endpoint, authentication: this);
_apiClient = ApiClient(basePath: endpoint);
_apiClient.client = NetworkRepository.client;
if (_accessToken != null) {
setAccessToken(_accessToken!);
}
usersApi = UsersApi(_apiClient);
authenticationApi = AuthenticationApi(_apiClient);
oAuthApi = AuthenticationApi(_apiClient);
@ -157,11 +153,6 @@ class ApiService implements Authentication {
return "";
}
Future<void> setAccessToken(String accessToken) async {
_accessToken = accessToken;
await Store.put(StoreKey.accessToken, accessToken);
}
Future<void> setDeviceInfoHeader() async {
DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
@ -205,28 +196,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;
}
@override
Future<void> applyToParams(List<QueryParam> queryParams, Map<String, String> headerParams) {
return Future.value();
return jsonDecode(customHeadersStr) as Map<String, String>;
}
ApiClient get apiClient => _apiClient;

View File

@ -340,7 +340,6 @@ class BackgroundService {
],
);
await ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken));
await ref.read(authServiceProvider).setOpenApiServiceEndpoint();
dPrint(() => "[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}");

View File

@ -74,7 +74,6 @@ class BackupVerificationService {
final lower = compute(_computeSaveToDelete, (
deleteCandidates: deleteCandidates.slice(0, half),
originals: originals.slice(0, half),
auth: Store.get(StoreKey.accessToken),
endpoint: Store.get(StoreKey.serverEndpoint),
rootIsolateToken: isolateToken,
fileMediaRepository: _fileMediaRepository,
@ -82,7 +81,6 @@ class BackupVerificationService {
final upper = compute(_computeSaveToDelete, (
deleteCandidates: deleteCandidates.slice(half),
originals: originals.slice(half),
auth: Store.get(StoreKey.accessToken),
endpoint: Store.get(StoreKey.serverEndpoint),
rootIsolateToken: isolateToken,
fileMediaRepository: _fileMediaRepository,
@ -92,7 +90,6 @@ class BackupVerificationService {
toDelete = await compute(_computeSaveToDelete, (
deleteCandidates: deleteCandidates,
originals: originals,
auth: Store.get(StoreKey.accessToken),
endpoint: Store.get(StoreKey.serverEndpoint),
rootIsolateToken: isolateToken,
fileMediaRepository: _fileMediaRepository,
@ -105,7 +102,6 @@ class BackupVerificationService {
({
List<Asset> deleteCandidates,
List<Asset> originals,
String auth,
String endpoint,
RootIsolateToken rootIsolateToken,
FileMediaRepository fileMediaRepository,
@ -120,7 +116,6 @@ class BackupVerificationService {
await tuple.fileMediaRepository.enableBackgroundAccess();
final ApiService apiService = ApiService();
apiService.setEndpoint(tuple.endpoint);
await apiService.setAccessToken(tuple.auth);
for (int i = 0; i < tuple.deleteCandidates.length; i++) {
if (await _compareAssets(tuple.deleteCandidates[i], tuple.originals[i], apiService)) {
result.add(tuple.deleteCandidates[i]);

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