mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 10:25:02 +00:00
Generalize Yattee Server credentials manager to BasicAuthCredentialsManager
Renames YatteeServerCredentialsManager → BasicAuthCredentialsManager so the same Keychain-backed username/password storage can be reused for any instance type that sits behind a reverse proxy requiring HTTP Basic Auth. Adds a one-time migration that moves existing items from the legacy 'com.yattee.yatteeserver' Keychain service to 'com.yattee.basicauth', preserving the iCloud-sync attribute. No behavior change for end users.
This commit is contained in:
@@ -47,7 +47,7 @@ final class AppEnvironment {
|
|||||||
let handoffManager: HandoffManager
|
let handoffManager: HandoffManager
|
||||||
let invidiousCredentialsManager: InvidiousCredentialsManager
|
let invidiousCredentialsManager: InvidiousCredentialsManager
|
||||||
let pipedCredentialsManager: PipedCredentialsManager
|
let pipedCredentialsManager: PipedCredentialsManager
|
||||||
let yatteeServerCredentialsManager: YatteeServerCredentialsManager
|
let basicAuthCredentialsManager: BasicAuthCredentialsManager
|
||||||
let homeInstanceCache: HomeInstanceCache
|
let homeInstanceCache: HomeInstanceCache
|
||||||
let invidiousAPI: InvidiousAPI
|
let invidiousAPI: InvidiousAPI
|
||||||
let pipedAPI: PipedAPI
|
let pipedAPI: PipedAPI
|
||||||
@@ -82,12 +82,12 @@ final class AppEnvironment {
|
|||||||
instances.setSettingsManager(settings)
|
instances.setSettingsManager(settings)
|
||||||
self.instancesManager = instances
|
self.instancesManager = instances
|
||||||
|
|
||||||
// Initialize Yattee Server Credentials Manager early (needed for ContentService)
|
// Initialize Basic Auth Credentials Manager early (needed for ContentService)
|
||||||
let yatteeServerCreds = YatteeServerCredentialsManager()
|
let basicAuthCreds = BasicAuthCredentialsManager()
|
||||||
yatteeServerCreds.settingsManager = settings
|
basicAuthCreds.settingsManager = settings
|
||||||
self.yatteeServerCredentialsManager = yatteeServerCreds
|
self.basicAuthCredentialsManager = basicAuthCreds
|
||||||
|
|
||||||
let contentSvc = ContentService(httpClient: client, yatteeServerCredentialsManager: yatteeServerCreds)
|
let contentSvc = ContentService(httpClient: client, basicAuthCredentialsManager: basicAuthCreds)
|
||||||
self.contentService = contentSvc
|
self.contentService = contentSvc
|
||||||
self.instanceDetector = InstanceDetector(httpClient: client)
|
self.instanceDetector = InstanceDetector(httpClient: client)
|
||||||
self.navigationCoordinator = navigationCoordinator ?? NavigationCoordinator()
|
self.navigationCoordinator = navigationCoordinator ?? NavigationCoordinator()
|
||||||
@@ -374,7 +374,7 @@ final class AppEnvironment {
|
|||||||
case .piped:
|
case .piped:
|
||||||
return pipedCredentialsManager
|
return pipedCredentialsManager
|
||||||
case .yatteeServer:
|
case .yatteeServer:
|
||||||
return yatteeServerCredentialsManager
|
return basicAuthCredentialsManager
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ actor ContentService: ContentServiceProtocol {
|
|||||||
private let defaultPeerTubeAPI: PeerTubeAPI
|
private let defaultPeerTubeAPI: PeerTubeAPI
|
||||||
private let defaultYatteeServerAPI: YatteeServerAPI
|
private let defaultYatteeServerAPI: YatteeServerAPI
|
||||||
|
|
||||||
/// Credentials manager for fetching Yattee Server auth headers on demand.
|
/// Credentials manager for fetching basic auth headers on demand.
|
||||||
private let yatteeServerCredentialsManager: YatteeServerCredentialsManager?
|
private let basicAuthCredentialsManager: BasicAuthCredentialsManager?
|
||||||
|
|
||||||
init(httpClient: HTTPClient, yatteeServerCredentialsManager: YatteeServerCredentialsManager? = nil) {
|
init(httpClient: HTTPClient, basicAuthCredentialsManager: BasicAuthCredentialsManager? = nil) {
|
||||||
// Legacy init - create factory internally
|
// Legacy init - create factory internally
|
||||||
self.httpClientFactory = HTTPClientFactory()
|
self.httpClientFactory = HTTPClientFactory()
|
||||||
self.defaultHTTPClient = httpClient
|
self.defaultHTTPClient = httpClient
|
||||||
@@ -52,10 +52,10 @@ actor ContentService: ContentServiceProtocol {
|
|||||||
self.defaultPipedAPI = PipedAPI(httpClient: httpClient)
|
self.defaultPipedAPI = PipedAPI(httpClient: httpClient)
|
||||||
self.defaultPeerTubeAPI = PeerTubeAPI(httpClient: httpClient)
|
self.defaultPeerTubeAPI = PeerTubeAPI(httpClient: httpClient)
|
||||||
self.defaultYatteeServerAPI = YatteeServerAPI(httpClient: httpClient)
|
self.defaultYatteeServerAPI = YatteeServerAPI(httpClient: httpClient)
|
||||||
self.yatteeServerCredentialsManager = yatteeServerCredentialsManager
|
self.basicAuthCredentialsManager = basicAuthCredentialsManager
|
||||||
}
|
}
|
||||||
|
|
||||||
init(httpClientFactory: HTTPClientFactory, yatteeServerCredentialsManager: YatteeServerCredentialsManager? = nil) {
|
init(httpClientFactory: HTTPClientFactory, basicAuthCredentialsManager: BasicAuthCredentialsManager? = nil) {
|
||||||
self.httpClientFactory = httpClientFactory
|
self.httpClientFactory = httpClientFactory
|
||||||
// Create default client for instances that don't need insecure SSL
|
// Create default client for instances that don't need insecure SSL
|
||||||
self.defaultHTTPClient = httpClientFactory.createClient(allowInvalidCertificates: false)
|
self.defaultHTTPClient = httpClientFactory.createClient(allowInvalidCertificates: false)
|
||||||
@@ -63,7 +63,7 @@ actor ContentService: ContentServiceProtocol {
|
|||||||
self.defaultPipedAPI = PipedAPI(httpClient: defaultHTTPClient)
|
self.defaultPipedAPI = PipedAPI(httpClient: defaultHTTPClient)
|
||||||
self.defaultPeerTubeAPI = PeerTubeAPI(httpClient: defaultHTTPClient)
|
self.defaultPeerTubeAPI = PeerTubeAPI(httpClient: defaultHTTPClient)
|
||||||
self.defaultYatteeServerAPI = YatteeServerAPI(httpClient: defaultHTTPClient)
|
self.defaultYatteeServerAPI = YatteeServerAPI(httpClient: defaultHTTPClient)
|
||||||
self.yatteeServerCredentialsManager = yatteeServerCredentialsManager
|
self.basicAuthCredentialsManager = basicAuthCredentialsManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Routing
|
// MARK: - Routing
|
||||||
@@ -114,7 +114,7 @@ actor ContentService: ContentServiceProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch auth header directly from credentials manager (avoids race condition on app startup)
|
// Fetch auth header directly from credentials manager (avoids race condition on app startup)
|
||||||
let authHeader = await yatteeServerCredentialsManager?.basicAuthHeader(for: instance)
|
let authHeader = await basicAuthCredentialsManager?.basicAuthHeader(for: instance)
|
||||||
await api.setAuthHeader(authHeader)
|
await api.setAuthHeader(authHeader)
|
||||||
|
|
||||||
return api
|
return api
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ final class BackgroundFeedRefresher {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let yatteeServerAPI = YatteeServerAPI(httpClient: HTTPClient())
|
let yatteeServerAPI = YatteeServerAPI(httpClient: HTTPClient())
|
||||||
let authHeader = appEnvironment.yatteeServerCredentialsManager.basicAuthHeader(for: yatteeServer)
|
let authHeader = appEnvironment.basicAuthCredentialsManager.basicAuthHeader(for: yatteeServer)
|
||||||
await yatteeServerAPI.setAuthHeader(authHeader)
|
await yatteeServerAPI.setAuthHeader(authHeader)
|
||||||
let response = try await yatteeServerAPI.postFeed(
|
let response = try await yatteeServerAPI.postFeed(
|
||||||
channels: channelRequests,
|
channels: channelRequests,
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
//
|
//
|
||||||
// YatteeServerCredentialsManager.swift
|
// BasicAuthCredentialsManager.swift
|
||||||
// Yattee
|
// Yattee
|
||||||
//
|
//
|
||||||
// Manages Yattee Server credentials (username/password) stored securely in the Keychain.
|
// Manages HTTP Basic Auth credentials (username/password) stored securely in the Keychain.
|
||||||
|
// Works for any instance type (Invidious, Piped, PeerTube, Yattee Server) — used when an
|
||||||
|
// instance sits behind a reverse proxy that requires HTTP Basic Auth.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Security
|
import Security
|
||||||
|
|
||||||
/// Credential structure for Yattee Server basic authentication.
|
/// Credential structure for HTTP Basic authentication.
|
||||||
struct YatteeServerCredential: Codable {
|
struct BasicAuthCredential: Codable {
|
||||||
let username: String
|
let username: String
|
||||||
let password: String
|
let password: String
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manages Yattee Server credentials stored securely in the Keychain.
|
/// Manages HTTP Basic Auth credentials stored securely in the Keychain.
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
final class YatteeServerCredentialsManager: InstanceCredentialsManager {
|
final class BasicAuthCredentialsManager: InstanceCredentialsManager {
|
||||||
private let keychainServiceName = "com.yattee.yatteeserver"
|
private let keychainServiceName = "com.yattee.basicauth"
|
||||||
|
|
||||||
|
/// Legacy Keychain service name from when basic auth was Yattee-Server-only.
|
||||||
|
/// We migrate items from this service to `keychainServiceName` on init.
|
||||||
|
private let legacyKeychainServiceName = "com.yattee.yatteeserver"
|
||||||
|
|
||||||
/// Tracks which instances have stored credentials (for reactive UI updates)
|
/// Tracks which instances have stored credentials (for reactive UI updates)
|
||||||
private(set) var loggedInInstanceIDs: Set<UUID> = []
|
private(set) var loggedInInstanceIDs: Set<UUID> = []
|
||||||
@@ -31,22 +37,24 @@ final class YatteeServerCredentialsManager: InstanceCredentialsManager {
|
|||||||
settingsManager?.iCloudSyncEnabled == true && settingsManager?.syncInstances == true
|
settingsManager?.iCloudSyncEnabled == true && settingsManager?.syncInstances == true
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {}
|
init() {
|
||||||
|
migrateLegacyKeychainItems()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Public API
|
// MARK: - Public API
|
||||||
|
|
||||||
/// Stores credentials for a Yattee Server instance.
|
/// Stores credentials for an instance.
|
||||||
/// Credentials sync to iCloud Keychain when iCloud sync is enabled for instances.
|
/// Credentials sync to iCloud Keychain when iCloud sync is enabled for instances.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - username: The username for basic auth
|
/// - username: The username for basic auth
|
||||||
/// - password: The password for basic auth
|
/// - password: The password for basic auth
|
||||||
/// - instance: The Yattee Server instance
|
/// - instance: The instance
|
||||||
func setCredentials(username: String, password: String, for instance: Instance) {
|
func setCredentials(username: String, password: String, for instance: Instance) {
|
||||||
let account = instance.id.uuidString
|
let account = instance.id.uuidString
|
||||||
let credential = YatteeServerCredential(username: username, password: password)
|
let credential = BasicAuthCredential(username: username, password: password)
|
||||||
|
|
||||||
guard let data = try? JSONEncoder().encode(credential) else {
|
guard let data = try? JSONEncoder().encode(credential) else {
|
||||||
LoggingService.shared.error("Failed to encode Yattee Server credentials", category: .keychain)
|
LoggingService.shared.error("Failed to encode basic auth credentials", category: .keychain)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +82,7 @@ final class YatteeServerCredentialsManager: InstanceCredentialsManager {
|
|||||||
|
|
||||||
if status == errSecSuccess {
|
if status == errSecSuccess {
|
||||||
LoggingService.shared.info(
|
LoggingService.shared.info(
|
||||||
"Stored credentials for Yattee Server instance",
|
"Stored basic auth credentials for instance",
|
||||||
category: .keychain,
|
category: .keychain,
|
||||||
details: "instanceID=\(instance.id), iCloudSync=\(syncEnabled)"
|
details: "instanceID=\(instance.id), iCloudSync=\(syncEnabled)"
|
||||||
)
|
)
|
||||||
@@ -82,18 +90,18 @@ final class YatteeServerCredentialsManager: InstanceCredentialsManager {
|
|||||||
loggedInInstanceIDs.insert(instance.id)
|
loggedInInstanceIDs.insert(instance.id)
|
||||||
} else {
|
} else {
|
||||||
LoggingService.shared.error(
|
LoggingService.shared.error(
|
||||||
"Failed to store credentials for Yattee Server instance",
|
"Failed to store basic auth credentials for instance",
|
||||||
category: .keychain,
|
category: .keychain,
|
||||||
details: "instanceID=\(instance.id), status=\(status)"
|
details: "instanceID=\(instance.id), status=\(status)"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves credentials for a Yattee Server instance.
|
/// Retrieves credentials for an instance.
|
||||||
/// Searches both synced and non-synced items.
|
/// Searches both synced and non-synced items.
|
||||||
/// - Parameter instance: The Yattee Server instance
|
/// - Parameter instance: The instance
|
||||||
/// - Returns: The credentials if stored, nil otherwise
|
/// - Returns: The credentials if stored, nil otherwise
|
||||||
func credentials(for instance: Instance) -> YatteeServerCredential? {
|
func credentials(for instance: Instance) -> BasicAuthCredential? {
|
||||||
let account = instance.id.uuidString
|
let account = instance.id.uuidString
|
||||||
|
|
||||||
let query: [String: Any] = [
|
let query: [String: Any] = [
|
||||||
@@ -110,9 +118,9 @@ final class YatteeServerCredentialsManager: InstanceCredentialsManager {
|
|||||||
|
|
||||||
guard status == errSecSuccess,
|
guard status == errSecSuccess,
|
||||||
let data = result as? Data,
|
let data = result as? Data,
|
||||||
let credential = try? JSONDecoder().decode(YatteeServerCredential.self, from: data) else {
|
let credential = try? JSONDecoder().decode(BasicAuthCredential.self, from: data) else {
|
||||||
LoggingService.shared.debug(
|
LoggingService.shared.debug(
|
||||||
"No credentials found for Yattee Server instance",
|
"No basic auth credentials found for instance",
|
||||||
category: .keychain,
|
category: .keychain,
|
||||||
details: "instanceID=\(instance.id), status=\(status)"
|
details: "instanceID=\(instance.id), status=\(status)"
|
||||||
)
|
)
|
||||||
@@ -124,7 +132,7 @@ final class YatteeServerCredentialsManager: InstanceCredentialsManager {
|
|||||||
|
|
||||||
/// Deletes credentials for an instance.
|
/// Deletes credentials for an instance.
|
||||||
/// Deletes both synced and non-synced items.
|
/// Deletes both synced and non-synced items.
|
||||||
/// - Parameter instance: The Yattee Server instance
|
/// - Parameter instance: The instance
|
||||||
func deleteCredentials(for instance: Instance) {
|
func deleteCredentials(for instance: Instance) {
|
||||||
let account = instance.id.uuidString
|
let account = instance.id.uuidString
|
||||||
|
|
||||||
@@ -139,13 +147,13 @@ final class YatteeServerCredentialsManager: InstanceCredentialsManager {
|
|||||||
|
|
||||||
if status == errSecSuccess || status == errSecItemNotFound {
|
if status == errSecSuccess || status == errSecItemNotFound {
|
||||||
LoggingService.shared.info(
|
LoggingService.shared.info(
|
||||||
"Deleted credentials for Yattee Server instance",
|
"Deleted basic auth credentials for instance",
|
||||||
category: .keychain,
|
category: .keychain,
|
||||||
details: "instanceID=\(instance.id)"
|
details: "instanceID=\(instance.id)"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LoggingService.shared.error(
|
LoggingService.shared.error(
|
||||||
"Failed to delete credentials for Yattee Server instance",
|
"Failed to delete basic auth credentials for instance",
|
||||||
category: .keychain,
|
category: .keychain,
|
||||||
details: "instanceID=\(instance.id), status=\(status)"
|
details: "instanceID=\(instance.id), status=\(status)"
|
||||||
)
|
)
|
||||||
@@ -156,7 +164,7 @@ final class YatteeServerCredentialsManager: InstanceCredentialsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if an instance has stored credentials.
|
/// Checks if an instance has stored credentials.
|
||||||
/// - Parameter instance: The Yattee Server instance
|
/// - Parameter instance: The instance
|
||||||
/// - Returns: true if credentials exist, false otherwise
|
/// - Returns: true if credentials exist, false otherwise
|
||||||
func hasCredentials(for instance: Instance) -> Bool {
|
func hasCredentials(for instance: Instance) -> Bool {
|
||||||
// Check tracked set first for performance
|
// Check tracked set first for performance
|
||||||
@@ -184,15 +192,15 @@ final class YatteeServerCredentialsManager: InstanceCredentialsManager {
|
|||||||
// MARK: - InstanceCredentialsManager Protocol
|
// MARK: - InstanceCredentialsManager Protocol
|
||||||
|
|
||||||
/// Stores a credential for an instance (protocol conformance).
|
/// Stores a credential for an instance (protocol conformance).
|
||||||
/// The credential is expected to be a JSON-encoded YatteeServerCredential.
|
/// The credential is expected to be a JSON-encoded BasicAuthCredential.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - credential: JSON string containing {"username": "...", "password": "..."}
|
/// - credential: JSON string containing {"username": "...", "password": "..."}
|
||||||
/// - instance: The instance to associate the credential with
|
/// - instance: The instance to associate the credential with
|
||||||
func setCredential(_ credential: String, for instance: Instance) {
|
func setCredential(_ credential: String, for instance: Instance) {
|
||||||
guard let data = credential.data(using: .utf8),
|
guard let data = credential.data(using: .utf8),
|
||||||
let creds = try? JSONDecoder().decode(YatteeServerCredential.self, from: data) else {
|
let creds = try? JSONDecoder().decode(BasicAuthCredential.self, from: data) else {
|
||||||
LoggingService.shared.error(
|
LoggingService.shared.error(
|
||||||
"Failed to decode credential string for Yattee Server",
|
"Failed to decode basic auth credential string",
|
||||||
category: .keychain
|
category: .keychain
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -229,7 +237,7 @@ final class YatteeServerCredentialsManager: InstanceCredentialsManager {
|
|||||||
// MARK: - Basic Auth Header Generation
|
// MARK: - Basic Auth Header Generation
|
||||||
|
|
||||||
/// Generates the HTTP Basic Auth header value for an instance.
|
/// Generates the HTTP Basic Auth header value for an instance.
|
||||||
/// - Parameter instance: The Yattee Server instance
|
/// - Parameter instance: The instance
|
||||||
/// - Returns: The Authorization header value (e.g., "Basic dXNlcjpwYXNz") or nil if no credentials
|
/// - Returns: The Authorization header value (e.g., "Basic dXNlcjpwYXNz") or nil if no credentials
|
||||||
func basicAuthHeader(for instance: Instance) -> String? {
|
func basicAuthHeader(for instance: Instance) -> String? {
|
||||||
guard let creds = credentials(for: instance) else { return nil }
|
guard let creds = credentials(for: instance) else { return nil }
|
||||||
@@ -237,4 +245,81 @@ final class YatteeServerCredentialsManager: InstanceCredentialsManager {
|
|||||||
guard let data = credentials.data(using: .utf8) else { return nil }
|
guard let data = credentials.data(using: .utf8) else { return nil }
|
||||||
return "Basic \(data.base64EncodedString())"
|
return "Basic \(data.base64EncodedString())"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Legacy Migration
|
||||||
|
|
||||||
|
/// One-time migration of credentials from the legacy Yattee-Server-only Keychain service
|
||||||
|
/// (`com.yattee.yatteeserver`) to the generic basic-auth service (`com.yattee.basicauth`).
|
||||||
|
/// Items are copied (preserving sync attribute) and then deleted from the legacy service.
|
||||||
|
private func migrateLegacyKeychainItems() {
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: legacyKeychainServiceName,
|
||||||
|
kSecAttrSynchronizable as String: kSecAttrSynchronizableAny,
|
||||||
|
kSecReturnAttributes as String: true,
|
||||||
|
kSecReturnData as String: true,
|
||||||
|
kSecMatchLimit as String: kSecMatchLimitAll
|
||||||
|
]
|
||||||
|
|
||||||
|
var result: AnyObject?
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||||
|
|
||||||
|
guard status == errSecSuccess, let items = result as? [[String: Any]], !items.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var migrated = 0
|
||||||
|
for item in items {
|
||||||
|
guard let account = item[kSecAttrAccount as String] as? String,
|
||||||
|
let data = item[kSecValueData as String] as? Data else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let synchronizable = (item[kSecAttrSynchronizable as String] as? Bool) ?? false
|
||||||
|
|
||||||
|
// Skip add if a new-style item already exists for this account.
|
||||||
|
let existsQuery: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: keychainServiceName,
|
||||||
|
kSecAttrAccount as String: account,
|
||||||
|
kSecAttrSynchronizable as String: kSecAttrSynchronizableAny,
|
||||||
|
kSecMatchLimit as String: kSecMatchLimitOne
|
||||||
|
]
|
||||||
|
if SecItemCopyMatching(existsQuery as CFDictionary, nil) != errSecSuccess {
|
||||||
|
let addQuery: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: keychainServiceName,
|
||||||
|
kSecAttrAccount as String: account,
|
||||||
|
kSecAttrSynchronizable as String: synchronizable,
|
||||||
|
kSecValueData as String: data
|
||||||
|
]
|
||||||
|
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
||||||
|
if addStatus != errSecSuccess {
|
||||||
|
LoggingService.shared.error(
|
||||||
|
"Failed to migrate legacy basic auth credential",
|
||||||
|
category: .keychain,
|
||||||
|
details: "account=\(account), status=\(addStatus)"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
migrated += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the legacy item for this account
|
||||||
|
let deleteQuery: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: legacyKeychainServiceName,
|
||||||
|
kSecAttrAccount as String: account,
|
||||||
|
kSecAttrSynchronizable as String: kSecAttrSynchronizableAny
|
||||||
|
]
|
||||||
|
SecItemDelete(deleteQuery as CFDictionary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if migrated > 0 {
|
||||||
|
LoggingService.shared.info(
|
||||||
|
"Migrated legacy basic auth credentials to generic Keychain service",
|
||||||
|
category: .keychain,
|
||||||
|
details: "count=\(migrated)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -492,7 +492,7 @@ final class SubscriptionFeedCache {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let yatteeServerAPI = YatteeServerAPI(httpClient: HTTPClient())
|
let yatteeServerAPI = YatteeServerAPI(httpClient: HTTPClient())
|
||||||
let authHeader = appEnvironment.yatteeServerCredentialsManager.basicAuthHeader(for: instance)
|
let authHeader = appEnvironment.basicAuthCredentialsManager.basicAuthHeader(for: instance)
|
||||||
await yatteeServerAPI.setAuthHeader(authHeader)
|
await yatteeServerAPI.setAuthHeader(authHeader)
|
||||||
LoggingService.shared.debug("refreshFromStatelessServer: Calling postFeed for \(channelRequests.count) channels", category: .general)
|
LoggingService.shared.debug("refreshFromStatelessServer: Calling postFeed for \(channelRequests.count) channels", category: .general)
|
||||||
let response = try await yatteeServerAPI.postFeed(
|
let response = try await yatteeServerAPI.postFeed(
|
||||||
@@ -556,7 +556,7 @@ final class SubscriptionFeedCache {
|
|||||||
StatelessChannelStatusRequest(channelId: $0.channelId, site: $0.site)
|
StatelessChannelStatusRequest(channelId: $0.channelId, site: $0.site)
|
||||||
}
|
}
|
||||||
let yatteeServerAPI = YatteeServerAPI(httpClient: HTTPClient())
|
let yatteeServerAPI = YatteeServerAPI(httpClient: HTTPClient())
|
||||||
let authHeader = appEnvironment.yatteeServerCredentialsManager.basicAuthHeader(for: instance)
|
let authHeader = appEnvironment.basicAuthCredentialsManager.basicAuthHeader(for: instance)
|
||||||
await yatteeServerAPI.setAuthHeader(authHeader)
|
await yatteeServerAPI.setAuthHeader(authHeader)
|
||||||
|
|
||||||
let maxRetries = 5
|
let maxRetries = 5
|
||||||
|
|||||||
@@ -76,13 +76,13 @@ struct InstanceBrowseView: View {
|
|||||||
/// Auth header for Yattee Server instances (when browsing a Yattee Server directly)
|
/// Auth header for Yattee Server instances (when browsing a Yattee Server directly)
|
||||||
private var yatteeServerAuthHeader: String? {
|
private var yatteeServerAuthHeader: String? {
|
||||||
guard instance.type == .yatteeServer else { return nil }
|
guard instance.type == .yatteeServer else { return nil }
|
||||||
return appEnvironment?.yatteeServerCredentialsManager.basicAuthHeader(for: instance)
|
return appEnvironment?.basicAuthCredentialsManager.basicAuthHeader(for: instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auth header for avatar loading (uses Yattee Server for YouTube channel avatars)
|
/// Auth header for avatar loading (uses Yattee Server for YouTube channel avatars)
|
||||||
private var avatarAuthHeader: String? {
|
private var avatarAuthHeader: String? {
|
||||||
guard let server = yatteeServer else { return nil }
|
guard let server = yatteeServer else { return nil }
|
||||||
return appEnvironment?.yatteeServerCredentialsManager.basicAuthHeader(for: server)
|
return appEnvironment?.basicAuthCredentialsManager.basicAuthHeader(for: server)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BrowseTab: String, CaseIterable, Identifiable {
|
enum BrowseTab: String, CaseIterable, Identifiable {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ struct UnifiedTabView: View {
|
|||||||
|
|
||||||
private var yatteeServerAuthHeader: String? {
|
private var yatteeServerAuthHeader: String? {
|
||||||
guard let server = appEnvironment?.instancesManager.enabledYatteeServerInstances.first else { return nil }
|
guard let server = appEnvironment?.instancesManager.enabledYatteeServerInstances.first else { return nil }
|
||||||
return appEnvironment?.yatteeServerCredentialsManager.basicAuthHeader(for: server)
|
return appEnvironment?.basicAuthCredentialsManager.basicAuthHeader(for: server)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -329,7 +329,7 @@ struct UnifiedTabView: View {
|
|||||||
|
|
||||||
private var yatteeServerAuthHeader: String? {
|
private var yatteeServerAuthHeader: String? {
|
||||||
guard let server = appEnvironment?.instancesManager.enabledYatteeServerInstances.first else { return nil }
|
guard let server = appEnvironment?.instancesManager.enabledYatteeServerInstances.first else { return nil }
|
||||||
return appEnvironment?.yatteeServerCredentialsManager.basicAuthHeader(for: server)
|
return appEnvironment?.basicAuthCredentialsManager.basicAuthHeader(for: server)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -554,7 +554,7 @@ struct UnifiedTabView: View {
|
|||||||
|
|
||||||
private var yatteeServerAuthHeader: String? {
|
private var yatteeServerAuthHeader: String? {
|
||||||
guard let server = appEnvironment?.instancesManager.enabledYatteeServerInstances.first else { return nil }
|
guard let server = appEnvironment?.instancesManager.enabledYatteeServerInstances.first else { return nil }
|
||||||
return appEnvironment?.yatteeServerCredentialsManager.basicAuthHeader(for: server)
|
return appEnvironment?.basicAuthCredentialsManager.basicAuthHeader(for: server)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -529,7 +529,7 @@ struct AddRemoteServerView: View {
|
|||||||
allowInvalidCertificates: allowInvalidCertificates
|
allowInvalidCertificates: allowInvalidCertificates
|
||||||
)
|
)
|
||||||
|
|
||||||
appEnvironment.yatteeServerCredentialsManager.setCredentials(
|
appEnvironment.basicAuthCredentialsManager.setCredentials(
|
||||||
username: yatteeServerUsername,
|
username: yatteeServerUsername,
|
||||||
password: yatteeServerPassword,
|
password: yatteeServerPassword,
|
||||||
for: instance
|
for: instance
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ private struct EditRemoteServerContent: View {
|
|||||||
|
|
||||||
// Load existing Yattee Server credentials
|
// Load existing Yattee Server credentials
|
||||||
if instance.type == .yatteeServer,
|
if instance.type == .yatteeServer,
|
||||||
let credentials = appEnvironment?.yatteeServerCredentialsManager.credentials(for: instance) {
|
let credentials = appEnvironment?.basicAuthCredentialsManager.credentials(for: instance) {
|
||||||
yatteeServerUsername = credentials.username
|
yatteeServerUsername = credentials.username
|
||||||
yatteeServerPassword = credentials.password
|
yatteeServerPassword = credentials.password
|
||||||
}
|
}
|
||||||
@@ -438,7 +438,7 @@ private struct EditRemoteServerContent: View {
|
|||||||
|
|
||||||
// Save Yattee Server credentials if provided
|
// Save Yattee Server credentials if provided
|
||||||
if instance.type == .yatteeServer && !yatteeServerUsername.isEmpty && !yatteeServerPassword.isEmpty {
|
if instance.type == .yatteeServer && !yatteeServerUsername.isEmpty && !yatteeServerPassword.isEmpty {
|
||||||
appEnvironment?.yatteeServerCredentialsManager.setCredentials(
|
appEnvironment?.basicAuthCredentialsManager.setCredentials(
|
||||||
username: yatteeServerUsername,
|
username: yatteeServerUsername,
|
||||||
password: yatteeServerPassword,
|
password: yatteeServerPassword,
|
||||||
for: instance
|
for: instance
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ private struct ChannelNotificationToggle: View {
|
|||||||
|
|
||||||
private var authHeader: String? {
|
private var authHeader: String? {
|
||||||
guard let server = yatteeServer else { return nil }
|
guard let server = yatteeServer else { return nil }
|
||||||
return appEnvironment?.yatteeServerCredentialsManager.basicAuthHeader(for: server)
|
return appEnvironment?.basicAuthCredentialsManager.basicAuthHeader(for: server)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var toggleBinding: Binding<Bool> {
|
private var toggleBinding: Binding<Bool> {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ struct ManageChannelsView: View {
|
|||||||
private var yatteeServerURL: URL? { yatteeServer?.url }
|
private var yatteeServerURL: URL? { yatteeServer?.url }
|
||||||
private var yatteeServerAuthHeader: String? {
|
private var yatteeServerAuthHeader: String? {
|
||||||
guard let server = yatteeServer else { return nil }
|
guard let server = yatteeServer else { return nil }
|
||||||
return appEnvironment?.yatteeServerCredentialsManager.basicAuthHeader(for: server)
|
return appEnvironment?.basicAuthCredentialsManager.basicAuthHeader(for: server)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Channels filtered by search query and sorted by selected order.
|
/// Channels filtered by search query and sorted by selected order.
|
||||||
@@ -439,7 +439,7 @@ struct ManageChannelsView: View {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let api = YatteeServerAPI(httpClient: HTTPClient())
|
let api = YatteeServerAPI(httpClient: HTTPClient())
|
||||||
let authHeader = appEnvironment.yatteeServerCredentialsManager.basicAuthHeader(for: yatteeServer)
|
let authHeader = appEnvironment.basicAuthCredentialsManager.basicAuthHeader(for: yatteeServer)
|
||||||
await api.setAuthHeader(authHeader)
|
await api.setAuthHeader(authHeader)
|
||||||
let response = try await api.channelsMetadata(channelIDs: channelIDs, instance: yatteeServer)
|
let response = try await api.channelsMetadata(channelIDs: channelIDs, instance: yatteeServer)
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ struct SubscriptionsView: View {
|
|||||||
private var yatteeServerURL: URL? { yatteeServer?.url }
|
private var yatteeServerURL: URL? { yatteeServer?.url }
|
||||||
private var yatteeServerAuthHeader: String? {
|
private var yatteeServerAuthHeader: String? {
|
||||||
guard let server = yatteeServer else { return nil }
|
guard let server = yatteeServer else { return nil }
|
||||||
return appEnvironment?.yatteeServerCredentialsManager.basicAuthHeader(for: server)
|
return appEnvironment?.basicAuthCredentialsManager.basicAuthHeader(for: server)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates a unique ID based on instances configuration.
|
/// Generates a unique ID based on instances configuration.
|
||||||
|
|||||||
Reference in New Issue
Block a user