Files
Termind/Frameworks/TermindApplication/FeatureModels/HostCustomIdentityDraft.swift
T
Kaede Akino d2cce7d2ce feat: make core features free and add host-based Pro limits
Make AI, MCP, Metrics, and Wake-on-LAN free while retaining iCloud sync as a Pro feature. Limit free accounts to three saved hosts and preserve excess hosts with remote-operation locks.

Gate host creation, import, and runtime access; preserve stable host admission across persistence and sync. Add upgrade states on macOS and iOS, open the paywall from locked Wake actions, and use wrench icons for MCP.

Include regression coverage, opt-in UI review fixtures, localized copy, and the updated pricing website revision.
2026-10-17 06:13:38 +08:00

282 lines
12 KiB
Swift

import Foundation
import TermindDomain
import struct TermindDomain.Host
/// Memory-only Custom Identity fields shared by the native Host editors.
///
/// Passwords live here only for the lifetime of an editor. This value is never
/// persisted, synchronized, logged, or given a textual representation.
@MainActor
public struct HostCustomIdentityDraft {
public var sshUsername = ""
public var sshPassword = ""
public var sshKeyID: SSHKeyRef.ID?
public var telnetUsername = ""
public var telnetPassword = ""
public private(set) var usesCustomTelnetSelection = false
public var usesCustomProxy = false
public var proxyPassword = ""
public var usesCustomTelnetProxy = false
public var telnetProxyPassword = ""
private var sshIdentityID: Identity.ID?
private var telnetIdentityID: Identity.ID?
private var sshHasConfiguredPassword = false
private var telnetHasConfiguredPassword = false
private var proxyHasConfiguredPassword = false
private var telnetProxyHasConfiguredPassword = false
private var sshPasswordAvailability: SecretAvailability = .notConfigured
private var telnetPasswordAvailability: SecretAvailability = .notConfigured
private var proxyPasswordAvailability: SecretAvailability = .notConfigured
private var telnetProxyPasswordAvailability: SecretAvailability = .notConfigured
public init() {}
public init(host: Host, library: HostLibraryModel) {
let ssh = library.customIdentity(for: host.id, purpose: .ssh)
sshIdentityID = ssh?.id
sshUsername = ssh?.username ?? ""
sshKeyID = ssh?.keyID
sshHasConfiguredPassword = ssh?.usesPassword == true
if let ssh {
sshPasswordAvailability = library.passwordAvailability(for: ssh)
if sshPasswordAvailability == .available {
sshPassword = (try? library.editablePassword(for: ssh)) ?? ""
}
}
let telnet = library.customIdentity(for: host.id, purpose: .telnet)
telnetIdentityID = telnet?.id
telnetUsername = telnet?.username ?? ""
telnetHasConfiguredPassword = telnet?.usesPassword == true
if let telnet {
telnetPasswordAvailability = library.passwordAvailability(for: telnet)
if telnetPasswordAvailability == .available {
telnetPassword = (try? library.editablePassword(for: telnet)) ?? ""
}
}
usesCustomTelnetSelection = host.telnetIdentityID == telnet?.id && telnet != nil
let proxy = library.customIdentity(for: host.id, purpose: .proxy)
proxyHasConfiguredPassword = proxy?.usesPassword == true
if let proxy {
proxyPasswordAvailability = library.passwordAvailability(for: proxy)
if host.proxy?.identityID == proxy.id {
usesCustomProxy = true
if proxyPasswordAvailability == .available {
proxyPassword = (try? library.editablePassword(for: proxy)) ?? ""
}
}
}
let telnetProxy = library.customIdentity(for: host.id, purpose: .telnetProxy)
telnetProxyHasConfiguredPassword = telnetProxy?.usesPassword == true
if let telnetProxy {
telnetProxyPasswordAvailability = library.passwordAvailability(for: telnetProxy)
if host.telnetProxy?.identityID == telnetProxy.id {
usesCustomTelnetProxy = true
if telnetProxyPasswordAvailability == .available {
telnetProxyPassword = (try? library.editablePassword(for: telnetProxy)) ?? ""
}
}
}
}
public var sshPasswordIsEditable: Bool {
!sshHasConfiguredPassword || sshPasswordAvailability == .available
}
public var proxyPasswordIsEditable: Bool {
!proxyHasConfiguredPassword || proxyPasswordAvailability == .available
}
public var telnetPasswordIsEditable: Bool {
!telnetHasConfiguredPassword || telnetPasswordAvailability == .available
}
public var telnetProxyPasswordIsEditable: Bool {
!telnetProxyHasConfiguredPassword || telnetProxyPasswordAvailability == .available
}
public func usesCustomSSH(for host: Host) -> Bool {
guard let identityID = host.identityID else { return true }
return identityID == sshIdentityID
}
public func selectCustomSSH(on host: inout Host) {
host.identityID = sshIdentityID
}
public func selectSSHIdentity(_ identity: Identity, on host: inout Host) {
host.identityID = identity.id
}
public func usesCustomTelnet(for host: Host) -> Bool {
usesCustomTelnetSelection && (host.telnetIdentityID == nil || host.telnetIdentityID == telnetIdentityID)
}
public mutating func selectCustomTelnet(on host: inout Host) {
usesCustomTelnetSelection = true
host.telnetIdentityID = telnetIdentityID
}
public mutating func clearTelnetIdentity(on host: inout Host) {
usesCustomTelnetSelection = false
host.telnetIdentityID = nil
}
public mutating func selectTelnetIdentity(_ identity: Identity, on host: inout Host) {
usesCustomTelnetSelection = false
host.telnetIdentityID = identity.id
}
public mutating func clearProxyIdentity(on host: inout Host) {
usesCustomProxy = false
host.proxy?.identityID = nil
host.proxy?.username = ""
}
public mutating func selectCustomProxy(on host: inout Host, using library: HostLibraryModel) {
usesCustomProxy = true
let identity = library.customIdentity(for: host.id, purpose: .proxy)
proxyHasConfiguredPassword = identity?.usesPassword == true
proxyPasswordAvailability = identity.map(library.passwordAvailability) ?? .notConfigured
host.proxy?.identityID = identity?.id
host.proxy?.username = identity?.username ?? ""
proxyPassword = ""
if let identity, proxyPasswordAvailability == .available {
proxyPassword = (try? library.editablePassword(for: identity)) ?? ""
}
}
public mutating func selectProxyIdentity(_ identity: Identity, on host: inout Host) {
usesCustomProxy = false
host.proxy?.identityID = identity.id
host.proxy?.username = identity.username
}
public mutating func clearTelnetProxyIdentity(on host: inout Host) {
usesCustomTelnetProxy = false
host.telnetProxy?.identityID = nil
host.telnetProxy?.username = ""
}
public mutating func selectCustomTelnetProxy(on host: inout Host, using library: HostLibraryModel) {
usesCustomTelnetProxy = true
let identity = library.customIdentity(for: host.id, purpose: .telnetProxy)
telnetProxyHasConfiguredPassword = identity?.usesPassword == true
telnetProxyPasswordAvailability = identity.map(library.passwordAvailability) ?? .notConfigured
host.telnetProxy?.identityID = identity?.id
host.telnetProxy?.username = identity?.username ?? ""
telnetProxyPassword = ""
if let identity, telnetProxyPasswordAvailability == .available {
telnetProxyPassword = (try? library.editablePassword(for: identity)) ?? ""
}
}
public mutating func selectTelnetProxyIdentity(_ identity: Identity, on host: inout Host) {
usesCustomTelnetProxy = false
host.telnetProxy?.identityID = identity.id
host.telnetProxy?.username = identity.username
}
public func isSSHValid(for host: Host) -> Bool {
guard host.sshEnabled else { return true }
return !usesCustomSSH(for: host)
|| (!sshUsername.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|| (sshPassword.isEmpty && sshKeyID == nil))
}
public func isProxyValid(for host: Host) -> Bool {
guard host.sshEnabled else { return true }
guard let proxy = host.proxy else { return true }
guard usesCustomProxy else { return proxy.isValid }
let username = proxy.username.trimmingCharacters(in: .whitespacesAndNewlines)
let hasPassword = !proxyPassword.isEmpty || proxyHasConfiguredPassword
let hasAuthentication = !username.isEmpty || hasPassword
return !proxy.hostname.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
&& (1...65_535).contains(proxy.port)
&& (!hasAuthentication || (!username.isEmpty && hasPassword))
}
public func isTelnetValid(for host: Host) -> Bool {
guard host.telnetEnabled else { return true }
guard (1...65_535).contains(host.telnetPort) else { return false }
guard usesCustomTelnet(for: host) else { return true }
let username = telnetUsername.trimmingCharacters(in: .whitespacesAndNewlines)
return !username.isEmpty && (!telnetPassword.isEmpty || telnetHasConfiguredPassword)
}
public func isTelnetProxyValid(for host: Host) -> Bool {
guard host.telnetEnabled, let proxy = host.telnetProxy else { return true }
guard usesCustomTelnetProxy else { return proxy.isValid }
let username = proxy.username.trimmingCharacters(in: .whitespacesAndNewlines)
let hasPassword = !telnetProxyPassword.isEmpty || telnetProxyHasConfiguredPassword
let hasAuthentication = !username.isEmpty || hasPassword
return !proxy.hostname.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
&& (1...65_535).contains(proxy.port)
&& (!hasAuthentication || (!username.isEmpty && hasPassword))
}
public mutating func save(into host: inout Host, using library: HostLibraryModel) throws {
try library.requireSavingHosts([host])
if usesCustomSSH(for: host) {
let identity = try library.saveCustomIdentity(
ownerHostID: host.id, purpose: .ssh, displayName: host.name,
username: sshUsername, password: sshPassword, keyID: sshKeyID)
host.identityID = identity?.id
sshIdentityID = identity?.id
sshHasConfiguredPassword = identity?.usesPassword == true
sshPasswordAvailability = identity.map(library.passwordAvailability) ?? .notConfigured
}
if usesCustomTelnet(for: host), host.telnetEnabled {
let identity = try library.saveCustomIdentity(
ownerHostID: host.id, purpose: .telnet, displayName: "\(host.name) Telnet",
username: telnetUsername, password: telnetPassword, keyID: nil)
host.telnetIdentityID = identity?.id
telnetIdentityID = identity?.id
telnetHasConfiguredPassword = identity?.usesPassword == true
telnetPasswordAvailability = identity.map(library.passwordAvailability) ?? .notConfigured
}
if usesCustomProxy, let proxy = host.proxy {
let identity = try library.saveCustomIdentity(
ownerHostID: host.id, purpose: .proxy, displayName: "\(host.name) Proxy",
username: proxy.username, password: proxyPassword, keyID: nil)
host.proxy?.identityID = identity?.id
proxyHasConfiguredPassword = identity?.usesPassword == true
proxyPasswordAvailability = identity.map(library.passwordAvailability) ?? .notConfigured
} else if host.proxy == nil {
_ = try library.saveCustomIdentity(
ownerHostID: host.id, purpose: .proxy, displayName: "",
username: "", password: "", keyID: nil)
proxyHasConfiguredPassword = false
proxyPasswordAvailability = .notConfigured
}
if usesCustomTelnetProxy, let proxy = host.telnetProxy {
let identity = try library.saveCustomIdentity(
ownerHostID: host.id, purpose: .telnetProxy,
displayName: "\(host.name) Telnet Proxy",
username: proxy.username, password: telnetProxyPassword, keyID: nil)
host.telnetProxy?.identityID = identity?.id
telnetProxyHasConfiguredPassword = identity?.usesPassword == true
telnetProxyPasswordAvailability = identity.map(library.passwordAvailability) ?? .notConfigured
} else if host.telnetProxy == nil {
_ = try library.saveCustomIdentity(
ownerHostID: host.id, purpose: .telnetProxy, displayName: "",
username: "", password: "", keyID: nil)
telnetProxyHasConfiguredPassword = false
telnetProxyPasswordAvailability = .notConfigured
}
guard library.saveHost(host) else {
throw TermindSSHError.library(
library.storageError ?? String(localized: "The host could not be saved."))
}
}
}