fix(mobile): logout on upgrade (#26827)

* use cookiejar

* cookie duping hook

* remove old pref

* handle network switching on logout

* remove bootstrapCookies

* dead code

* fix cast

* use constants

* use new event name

* update api
This commit is contained in:
Mert
2026-03-11 12:07:27 -05:00
committed by GitHub
parent e7db3b220d
commit c403e03a42
17 changed files with 340 additions and 89 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,16 @@ 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.json.Json
import java.io.ByteArrayInputStream
import java.io.File
import java.net.Socket
@@ -32,7 +37,19 @@ private const val CERT_ALIAS = "client_cert"
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"
private const val COOKIE_EXPIRY_DAYS = 400L
private enum class AuthCookie(val cookieName: String, val httpOnly: Boolean) {
ACCESS_TOKEN("immich_access_token", httpOnly = true),
IS_AUTHENTICATED("immich_is_authenticated", httpOnly = false),
AUTH_TYPE("immich_auth_type", httpOnly = true);
companion object {
val names = entries.map { it.cookieName }.toSet()
}
}
/**
* Manages a shared OkHttpClient with SSL configuration support.
@@ -58,6 +75,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 +88,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
@@ -153,25 +179,50 @@ object HttpClientManager {
synchronized(this) { clientChangedListeners.add(listener) }
}
fun setRequestHeaders(headerMap: Map<String, String>, serverUrls: List<String>) {
fun setRequestHeaders(headerMap: Map<String, String>, serverUrls: List<String>, token: String?) {
synchronized(this) {
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
val urlsChanged = Json.encodeToString(serverUrls) != prefs.getString(PREFS_SERVER_URLS, null)
headers = newHeaders
prefs.edit {
if (headersChanged) putString(PREFS_HEADERS, JSONObject(headerMap).toString())
if (urlChanged) {
if (newUrl != null) putString(PREFS_SERVER_URL, newUrl) else remove(PREFS_SERVER_URL)
cookieJar.setServerUrls(serverUrls)
if (headersChanged || urlsChanged) {
prefs.edit {
putString(PREFS_HEADERS, Json.encodeToString(headerMap))
putString(PREFS_SERVER_URLS, Json.encodeToString(serverUrls))
}
}
if (token != null) {
val url = serverUrls.firstNotNullOfOrNull { it.toHttpUrlOrNull() } ?: return
val expiry = System.currentTimeMillis() + COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000
val values = mapOf(
AuthCookie.ACCESS_TOKEN to token,
AuthCookie.IS_AUTHENTICATED to "true",
AuthCookie.AUTH_TYPE to "password",
)
cookieJar.saveFromResponse(url, values.map { (cookie, value) ->
Cookie.Builder().name(cookie.cookieName).value(value).domain(url.host).path("/").expiresAt(expiry)
.apply {
if (url.isHttps) secure()
if (cookie.httpOnly) httpOnly()
}.build()
})
}
}
}
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 +239,7 @@ object HttpClientManager {
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
return OkHttpClient.Builder()
.cookieJar(cookieJar)
.addInterceptor {
val request = it.request()
val builder = request.newBuilder()
@@ -249,4 +301,131 @@ 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
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 (syncAuthCookies()) 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 synced = serverUrls.any { it.host == url.host } && syncAuthCookies()
if (changed || synced) persist()
}
@Synchronized
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val now = System.currentTimeMillis()
if (store.removeAll { it.expiresAt < now }) {
syncAuthCookies()
persist()
}
return store.filter { it.matches(url) }
}
private fun syncAuthCookies(): Boolean {
val serverHosts = serverUrls.map { it.host }.toSet()
val now = System.currentTimeMillis()
val sourceCookies = store
.filter { it.name in AuthCookie.names && it.domain in serverHosts && it.expiresAt > now }
.associateBy { it.name }
if (sourceCookies.isEmpty()) {
return store.removeAll { it.name in AuthCookie.names && it.domain in serverHosts }
}
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

@@ -184,7 +184,7 @@ interface NetworkApi {
fun removeCertificate(callback: (Result<Unit>) -> Unit)
fun hasCertificate(): Boolean
fun getClientPointer(): Long
fun setRequestHeaders(headers: Map<String, String>, serverUrls: List<String>)
fun setRequestHeaders(headers: Map<String, String>, serverUrls: List<String>, token: String?)
companion object {
/** The codec used by NetworkApi. */
@@ -287,8 +287,9 @@ interface NetworkApi {
val args = message as List<Any?>
val headersArg = args[0] as Map<String, String>
val serverUrlsArg = args[1] as List<String>
val tokenArg = args[2] as String?
val wrapped: List<Any?> = try {
api.setRequestHeaders(headersArg, serverUrlsArg)
api.setRequestHeaders(headersArg, serverUrlsArg, tokenArg)
listOf(null)
} catch (exception: Throwable) {
NetworkPigeonUtils.wrapError(exception)

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) {
@@ -79,7 +79,7 @@ private class NetworkApiImpl() : NetworkApi {
return HttpClientManager.getClientPointer()
}
override fun setRequestHeaders(headers: Map<String, String>, serverUrls: List<String>) {
HttpClientManager.setRequestHeaders(headers, serverUrls)
override fun setRequestHeaders(headers: Map<String, String>, serverUrls: List<String>, token: String?) {
HttpClientManager.setRequestHeaders(headers, serverUrls, token)
}
}

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

@@ -225,7 +225,7 @@ protocol NetworkApi {
func removeCertificate(completion: @escaping (Result<Void, Error>) -> Void)
func hasCertificate() throws -> Bool
func getClientPointer() throws -> Int64
func setRequestHeaders(headers: [String: String], serverUrls: [String]) throws
func setRequestHeaders(headers: [String: String], serverUrls: [String], token: String?) throws
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -315,8 +315,9 @@ class NetworkApiSetup {
let args = message as! [Any?]
let headersArg = args[0] as! [String: String]
let serverUrlsArg = args[1] as! [String]
let tokenArg: String? = nilOrValue(args[2])
do {
try api.setRequestHeaders(headers: headersArg, serverUrls: serverUrlsArg)
try api.setRequestHeaders(headers: headersArg, serverUrls: serverUrlsArg, token: tokenArg)
reply(wrapResult(nil))
} catch {
reply(wrapError(error))

View File

@@ -58,42 +58,39 @@ class NetworkApiImpl: NetworkApi {
return Int64(Int(bitPattern: pointer))
}
func setRequestHeaders(headers: [String : String], serverUrls: [String]) throws {
var headers = headers
if let token = headers.removeValue(forKey: "x-immich-user-token") {
func setRequestHeaders(headers: [String : String], serverUrls: [String], token: String?) throws {
URLSessionManager.setServerUrls(serverUrls)
if let token = token {
let expiry = Date().addingTimeInterval(COOKIE_EXPIRY_DAYS * 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),
let values: [AuthCookie: String] = [
.accessToken: token,
.isAuthenticated: "true",
.authType: "password",
]
let expiry = Date().addingTimeInterval(400 * 24 * 60 * 60)
for (name, value, httpOnly) in cookies {
for (cookie, value) in values {
var properties: [HTTPCookiePropertyKey: Any] = [
.name: name,
.name: cookie.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 cookie.httpOnly { properties[.init("HttpOnly")] = "TRUE" }
if let httpCookie = HTTPCookie(properties: properties) {
URLSessionManager.cookieStorage.setCookie(httpCookie)
}
}
}
}
if serverUrls.first != UserDefaults.group.string(forKey: SERVER_URL_KEY) {
UserDefaults.group.set(serverUrls.first, forKey: SERVER_URL_KEY)
}
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()
}
}
}

View File

@@ -3,8 +3,30 @@ import native_video_player
let CLIENT_CERT_LABEL = "app.alextran.immich.client_identity"
let HEADERS_KEY = "immich.request_headers"
let SERVER_URL_KEY = "immich.server_url"
let SERVER_URLS_KEY = "immich.server_urls"
let APP_GROUP = "group.app.immich.share"
let COOKIE_EXPIRY_DAYS: TimeInterval = 400
enum AuthCookie: CaseIterable {
case accessToken, isAuthenticated, authType
var name: String {
switch self {
case .accessToken: return "immich_access_token"
case .isAuthenticated: return "immich_is_authenticated"
case .authType: return "immich_auth_type"
}
}
var httpOnly: Bool {
switch self {
case .accessToken, .authType: return true
case .isAuthenticated: return false
}
}
static let names: Set<String> = Set(allCases.map(\.name))
}
extension UserDefaults {
static let group = UserDefaults(suiteName: APP_GROUP)!
@@ -34,21 +56,94 @@ class URLSessionManager: NSObject {
return "Immich_iOS_\(version)"
}()
static let cookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: APP_GROUP)
private static var serverUrls: [String] = []
private static var isSyncing = false
var sessionPointer: UnsafeMutableRawPointer {
Unmanaged.passUnretained(session).toOpaque()
}
private override init() {
delegate = URLSessionManagerDelegate()
session = Self.buildSession(delegate: delegate)
super.init()
Self.serverUrls = UserDefaults.group.stringArray(forKey: SERVER_URLS_KEY) ?? []
NotificationCenter.default.addObserver(
Self.self,
selector: #selector(Self.cookiesDidChange),
name: NSNotification.Name.NSHTTPCookieManagerCookiesChanged,
object: Self.cookieStorage
)
}
func recreateSession() {
session = Self.buildSession(delegate: delegate)
}
static func setServerUrls(_ urls: [String]) {
guard urls != serverUrls else { return }
serverUrls = urls
UserDefaults.group.set(urls, forKey: SERVER_URLS_KEY)
syncAuthCookies()
}
@objc private static func cookiesDidChange(_ notification: Notification) {
guard !isSyncing, !serverUrls.isEmpty else { return }
syncAuthCookies()
}
private static func syncAuthCookies() {
let serverHosts = Set(serverUrls.compactMap { URL(string: $0)?.host })
let allCookies = cookieStorage.cookies ?? []
let now = Date()
let serverAuthCookies = allCookies.filter {
AuthCookie.names.contains($0.name) && serverHosts.contains($0.domain)
}
var sourceCookies: [String: HTTPCookie] = [:]
for cookie in serverAuthCookies {
if cookie.expiresDate.map({ $0 > now }) ?? true {
sourceCookies[cookie.name] = cookie
}
}
isSyncing = true
defer { isSyncing = false }
if sourceCookies.isEmpty {
for cookie in serverAuthCookies {
cookieStorage.deleteCookie(cookie)
}
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(COOKIE_EXPIRY_DAYS * 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

@@ -26,8 +26,8 @@ class NetworkRepository {
}
}
static Future<void> setHeaders(Map<String, String> headers, List<String> serverUrls) async {
await networkApi.setRequestHeaders(headers, serverUrls);
static Future<void> setHeaders(Map<String, String> headers, List<String> serverUrls, {String? token}) async {
await networkApi.setRequestHeaders(headers, serverUrls, token);
if (Platform.isIOS) {
await init();
}

View File

@@ -281,7 +281,7 @@ class NetworkApi {
}
}
Future<void> setRequestHeaders(Map<String, String> headers, List<String> serverUrls) async {
Future<void> setRequestHeaders(Map<String, String> headers, List<String> serverUrls, String? token) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NetworkApi.setRequestHeaders$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
@@ -289,7 +289,7 @@ class NetworkApi {
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[headers, serverUrls]);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[headers, serverUrls, token]);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);

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

@@ -38,10 +38,6 @@ class AuthRepository extends DatabaseRepository {
});
}
String getAccessToken() {
return Store.get(StoreKey.accessToken);
}
bool getEndpointSwitchingFeature() {
return Store.tryGet(StoreKey.autoEndpointSwitching) ?? false;
}

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).cast<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

@@ -25,8 +25,10 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.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 +37,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 +111,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 NetworkRepository.setHeaders(ApiService.getRequestHeaders(), serverUrls, token: accessToken);
}
}
}
if (version < 22 && !Store.isBetaTimelineEnabled) {
await Store.put(StoreKey.needBetaMigration, true);
}

View File

@@ -43,5 +43,5 @@ abstract class NetworkApi {
int getClientPointer();
void setRequestHeaders(Map<String, String> headers, List<String> serverUrls);
void setRequestHeaders(Map<String, String> headers, List<String> serverUrls, String? token);
}