@ -11,6 +11,7 @@ import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.UserID
import com.tailscale.ipn.ui.model.UserID
import com.tailscale.ipn.ui.model.deepCopy
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.util.AdvertisedRoutesHelper
import com.tailscale.ipn.ui.util.AdvertisedRoutesHelper
import com.tailscale.ipn.ui.util.LoadingIndicator
import com.tailscale.ipn.ui.util.LoadingIndicator
@ -144,52 +145,54 @@ open class IpnViewModel : ViewModel() {
// Login/Logout
// Login/Logout
/ * *
* Order of operations :
* 1. editPrefs ( ) with maskedPrefs ( to allow ControlURL override ) , WantRunning = true , LoggedOut = false if AuthKey != null
* 2. start ( ) starts the LocalBackend state machine
* 3. startLoginInteractive ( ) is currently required for bother interactive and non - interactive ( using auth key ) login
*
* Any failure short ‑ circuits the chain and invokes completionHandler once .
* /
fun login (
fun login (
maskedPrefs : Ipn . MaskedPrefs ? = null ,
maskedPrefs : Ipn . MaskedPrefs ? = null ,
authKey : String ? = null ,
authKey : String ? = null ,
completionHandler : ( Result < Unit > ) -> Unit = { }
completionHandler : ( Result < Unit > ) -> Unit = { }
) {
) {
val client = Client ( viewModelScope )
val loginAction = {
val finalMaskedPrefs = maskedPrefs ?. deepCopy ( ) ?: Ipn . MaskedPrefs ( )
Client ( viewModelScope ) . startLoginInteractive { result ->
finalMaskedPrefs . WantRunning = true
result
if ( authKey != null ) {
. onSuccess { TSLog . d ( TAG , " Login started: $it " ) }
finalMaskedPrefs . LoggedOut = false
. onFailure { TSLog . e ( TAG , " Error starting login: ${it.message} " ) }
completionHandler ( result )
}
}
// Need to stop running before logging in to clear routes:
// https://linear.app/tailscale/issue/ENG-3441/routesdns-is-not-cleared-when-switching-profiles-or-reauthenticating
val stopThenLogin = {
Client ( viewModelScope ) . editPrefs ( Ipn . MaskedPrefs ( ) . apply { WantRunning = false } ) { result ->
result
. onSuccess { loginAction ( ) }
. onFailure { TSLog . e ( TAG , " Error setting wantRunning to false: ${it.message} " ) }
}
}
val startAction = {
Client ( viewModelScope ) . start ( Ipn . Options ( AuthKey = authKey ) ) { start ->
start . onFailure { completionHandler ( Result . failure ( it ) ) } . onSuccess { stopThenLogin ( ) }
}
}
}
// If an MDM control URL is set, we will always use that in lieu of anything the user sets.
client . editPrefs ( finalMaskedPrefs ) { editResult ->
var prefs = maskedPrefs
editResult
val mdmControlURL = MDMSettings . loginURL . flow . value . value
. onFailure {
TSLog . e ( TAG , " editPrefs() failed: ${it.message} " )
if ( mdmControlURL != null ) {
completionHandler ( Result . failure ( it ) )
prefs = prefs ?: Ipn . MaskedPrefs ( )
}
prefs . ControlURL = mdmControlURL
. onSuccess {
TSLog . d ( TAG , " Overriding control URL with MDM value: $mdmControlURL " )
val opts = Ipn . Options ( UpdatePrefs = editResult . getOrThrow ( ) , AuthKey = authKey )
client . start ( opts ) { startResult ->
startResult
. onFailure {
TSLog . e ( TAG , " start() failed: ${it.message} " )
completionHandler ( Result . failure ( it ) )
}
. onSuccess {
client . startLoginInteractive { loginResult ->
loginResult
. onFailure {
TSLog . e ( TAG , " startLoginInteractive() failed: ${it.message} " )
completionHandler ( Result . failure ( it ) )
}
. onSuccess { completionHandler ( Result . success ( Unit ) ) }
}
}
}
}
}
}
prefs ?. let {
Client ( viewModelScope ) . editPrefs ( it ) { result ->
result . onFailure { completionHandler ( Result . failure ( it ) ) } . onSuccess { startAction ( ) }
}
} ?: run { startAction ( ) }
}
}
fun loginWithAuthKey ( authKey : String , completionHandler : ( Result < Unit > ) -> Unit = { } ) {
fun loginWithAuthKey ( authKey : String , completionHandler : ( Result < Unit > ) -> Unit = { } ) {