mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-28 18:41:30 +02:00
* [client] Add iOS NetBirdSDK profile manager binding Mirror the Android profile manager in the iOS gomobile binding so the core's ID-based profilemanager.ServiceManager owns profile state on iOS too, instead of a parallel Swift reimplementation. Adds client/ios/NetBirdSDK/profile_manager.go (//go:build ios): an ID-based ProfileManager wrapping ServiceManager with iOS-specific path handling (default profile at the container-root netbird.cfg, others as profiles/<id>.json) and a gomobile-friendly API: List/Add/Switch/Rename/ Logout/Remove plus active config/state path accessors. The default profile keeps the reserved "default" id and is never assigned a hex id. * fix(ios): preserve profile name when saving config during auth NewAuth built a fresh in-memory config from only the management URL, so the SSO/setup-key save (DirectWriteOutConfig) overwrote the profile config file the profile manager had just written, wiping the display name to "" and forcing the UI to fall back to the profile ID. Load the existing config when present and override only the management URL, keeping the name and keys. * [client] Extract the mobile profile manager into client/mobile The Android and iOS gomobile bindings carried two near-identical copies of the profile manager. Move the shared implementation into a new client/mobile package and reduce both bindings to thin adapters that only translate to gomobile-friendly types (gomobile binds per package, so the Profile / ProfileArray wrappers have to stay platform-side). Also bring the account-email layer over to the shared package: an SSO login records the account under <stem>.account.json so the next login can pass it as an OIDC login_hint. Logout keeps it, profile removal drops it. The suffix deliberately differs from .state.json, which the engine's state manager owns in the same directory on mobile. Adds profilemanager.Prefs (namespaced per-profile preference store) and its cleanup in ServiceManager.RemoveProfile, exposed through the shared manager as ProfilePrefs.
105 lines
3.5 KiB
Go
105 lines
3.5 KiB
Go
package mobile
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"github.com/netbirdio/netbird/util"
|
|
)
|
|
|
|
const (
|
|
// profileAccountSuffix names the file holding the profile's account email.
|
|
// Deliberately not ".state.json", which desktop uses for the same data:
|
|
// there the email and the engine's state manager live in different
|
|
// directories, but on mobile both resolve under configDir, so sharing the
|
|
// name would have the two overwrite each other — the state manager rewrites
|
|
// the whole file from its own keys (see statemanager.Manager.PersistState),
|
|
// and this package's writer does the same in reverse.
|
|
profileAccountSuffix = ".account.json"
|
|
)
|
|
|
|
// profileAccountPathFor derives the account file path from a profile's config
|
|
// path: netbird.cfg -> netbird.account.json, <id>.json -> <id>.account.json.
|
|
//
|
|
// Deriving from the config path rather than resolving the active profile keeps
|
|
// the write on the profile the login actually ran for: login flows run in a
|
|
// goroutine, so the active profile can change under a flow already in flight.
|
|
func profileAccountPathFor(configPath string) (string, error) {
|
|
if configPath == "" {
|
|
return "", fmt.Errorf("empty config path")
|
|
}
|
|
|
|
base := filepath.Base(configPath)
|
|
stem := strings.TrimSuffix(base, filepath.Ext(base))
|
|
if stem == "" || stem == "." {
|
|
return "", fmt.Errorf("config path %q has no filename stem", configPath)
|
|
}
|
|
|
|
return filepath.Join(filepath.Dir(configPath), stem+profileAccountSuffix), nil
|
|
}
|
|
|
|
// ReadProfileEmail returns the account email stored for the profile whose
|
|
// config lives at configPath. A missing or unreadable file yields "", which
|
|
// leaves the account choice to the IdP.
|
|
func ReadProfileEmail(configPath string) string {
|
|
accountPath, err := profileAccountPathFor(configPath)
|
|
if err != nil {
|
|
log.Debugf("no profile account path for login hint: %v", err)
|
|
return ""
|
|
}
|
|
|
|
var state profilemanager.ProfileState
|
|
if _, err := util.ReadJson(accountPath, &state); err != nil {
|
|
if !os.IsNotExist(err) {
|
|
log.Debugf("failed to read profile account for login hint: %v", err)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
return state.Email
|
|
}
|
|
|
|
// WriteProfileEmail records the account email for the profile whose config
|
|
// lives at configPath, so later logins can pass it as an OIDC login_hint. An
|
|
// empty email is ignored rather than blanking what is already stored.
|
|
func WriteProfileEmail(configPath string, email string) error {
|
|
if email == "" {
|
|
return nil
|
|
}
|
|
|
|
accountPath, err := profileAccountPathFor(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve profile account path: %w", err)
|
|
}
|
|
|
|
state := profilemanager.ProfileState{Email: email}
|
|
if err := util.WriteJsonWithRestrictedPermission(context.Background(), accountPath, state); err != nil {
|
|
return fmt.Errorf("write profile account: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// removeProfileEmail drops the stored account email. Called on profile removal,
|
|
// not on logout: a logged-out profile keeps its email so the next login passes
|
|
// it as the login_hint, matching the desktop and CLI semantics. Mirrors the
|
|
// desktop UI's RemoveProfileState call.
|
|
func removeProfileEmail(configPath string) error {
|
|
accountPath, err := profileAccountPathFor(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve profile account path: %w", err)
|
|
}
|
|
|
|
if err := os.Remove(accountPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove profile account: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|