Migrate android profile manager

This commit is contained in:
Theodor S. Midtlien
2026-06-02 09:58:28 +02:00
parent 33b1cc5449
commit df0717dc16
6 changed files with 50 additions and 57 deletions

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
log "github.com/sirupsen/logrus"
@@ -24,6 +23,7 @@ const (
// Profile represents a profile for gomobile
type Profile struct {
ID string
Name string
IsActive bool
}
@@ -99,6 +99,7 @@ func (pm *ProfileManager) ListProfiles() (*ProfileArray, error) {
var profiles []*Profile
for _, p := range internalProfiles {
profiles = append(profiles, &Profile{
ID: p.ID,
Name: p.Name,
IsActive: p.IsActive,
})
@@ -108,55 +109,65 @@ func (pm *ProfileManager) ListProfiles() (*ProfileArray, error) {
}
// GetActiveProfile returns the currently active profile name
func (pm *ProfileManager) GetActiveProfile() (string, error) {
func (pm *ProfileManager) GetActiveProfile() (*Profile, error) {
// Use ServiceManager to stay consistent with ListProfiles
// ServiceManager uses active_profile.json
activeState, err := pm.serviceMgr.GetActiveProfileState()
if err != nil {
return "", fmt.Errorf("failed to get active profile: %w", err)
return nil, fmt.Errorf("failed to get active profile: %w", err)
}
return activeState.Name, nil
// ActiveProfileState only stores the ID (and username), not the display
// name. Resolve the ID to the full profile so callers get the real Name.
prof, err := pm.serviceMgr.ResolveProfile(activeState.ID, androidUsername)
if err != nil {
return nil, fmt.Errorf("failed to resolve active profile %q: %w", activeState.ID, err)
}
return &Profile{ID: prof.ID, Name: prof.Name, IsActive: true}, nil
}
// SwitchProfile switches to a different profile
func (pm *ProfileManager) SwitchProfile(profileName string) error {
func (pm *ProfileManager) SwitchProfile(id string) error {
// Use ServiceManager to stay consistent with ListProfiles
// ServiceManager uses active_profile.json
err := pm.serviceMgr.SetActiveProfileState(&profilemanager.ActiveProfileState{
Name: profileName,
ID: id,
Username: androidUsername,
})
if err != nil {
return fmt.Errorf("failed to switch profile: %w", err)
}
log.Infof("switched to profile: %s", profileName)
log.Infof("switched to profile: %s", id)
return nil
}
// AddProfile creates a new profile
func (pm *ProfileManager) AddProfile(profileName string) error {
// Use ServiceManager (creates profile in profiles/ directory)
if err := pm.serviceMgr.AddProfile(profileName, androidUsername); err != nil {
profile, err := pm.serviceMgr.AddProfile(profileName, androidUsername)
if err != nil {
return fmt.Errorf("failed to add profile: %w", err)
}
log.Infof("created new profile: %s", profileName)
log.Infof("created new profile: %s", profile.ID)
return nil
}
// LogoutProfile logs out from a profile (clears authentication)
func (pm *ProfileManager) LogoutProfile(profileName string) error {
profileName = sanitizeProfileName(profileName)
configPath, err := pm.getProfileConfigPath(profileName)
func (pm *ProfileManager) LogoutProfile(id string) error {
configPath, err := pm.getProfileConfigPath(id)
if err != nil {
return err
}
if !profilemanager.IsValidProfileFilenameStem(id) {
return fmt.Errorf("id '%s' is not valid", id)
}
// Check if profile exists
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return fmt.Errorf("profile '%s' does not exist", profileName)
return fmt.Errorf("profile '%s' does not exist", id)
}
// Read current config using internal profilemanager
@@ -174,35 +185,32 @@ func (pm *ProfileManager) LogoutProfile(profileName string) error {
return fmt.Errorf("failed to save config: %w", err)
}
log.Infof("logged out from profile: %s", profileName)
log.Infof("logged out from profile: %s", id)
return nil
}
// RemoveProfile deletes a profile
func (pm *ProfileManager) RemoveProfile(profileName string) error {
func (pm *ProfileManager) RemoveProfile(id string) error {
// Use ServiceManager (removes profile from profiles/ directory)
if err := pm.serviceMgr.RemoveProfile(profileName, androidUsername); err != nil {
if err := pm.serviceMgr.RemoveProfile(id, androidUsername); err != nil {
return fmt.Errorf("failed to remove profile: %w", err)
}
log.Infof("removed profile: %s", profileName)
log.Infof("removed profile: %s", id)
return nil
}
// getProfileConfigPath returns the config file path for a profile
// This is needed for Android-specific path handling (netbird.cfg for default profile)
func (pm *ProfileManager) getProfileConfigPath(profileName string) (string, error) {
if profileName == "" || profileName == profilemanager.DefaultProfileName {
func (pm *ProfileManager) getProfileConfigPath(id string) (string, error) {
if id == "" || id == profilemanager.DefaultProfileName {
// Android uses netbird.cfg for default profile instead of default.json
// Default profile is stored in root configDir, not in profiles/
return filepath.Join(pm.configDir, defaultConfigFilename), nil
}
// Non-default profiles are stored in profiles subdirectory
// This matches the Java Preferences.java expectation
profileName = sanitizeProfileName(profileName)
profilesDir := filepath.Join(pm.configDir, profilesSubdir)
return filepath.Join(profilesDir, profileName+".json"), nil
return filepath.Join(profilesDir, id+".json"), nil
}
// GetConfigPath returns the config file path for a given profile
@@ -213,14 +221,13 @@ func (pm *ProfileManager) GetConfigPath(profileName string) (string, error) {
// GetStateFilePath returns the state file path for a given profile
// Java should call this instead of constructing paths with Preferences.stateFile()
func (pm *ProfileManager) GetStateFilePath(profileName string) (string, error) {
if profileName == "" || profileName == profilemanager.DefaultProfileName {
func (pm *ProfileManager) GetStateFilePath(id string) (string, error) {
if id == "" || id == profilemanager.DefaultProfileName {
return filepath.Join(pm.configDir, "state.json"), nil
}
profileName = sanitizeProfileName(profileName)
profilesDir := filepath.Join(pm.configDir, profilesSubdir)
return filepath.Join(profilesDir, profileName+".state.json"), nil
return filepath.Join(profilesDir, id+".state.json"), nil
}
// GetActiveConfigPath returns the config file path for the currently active profile
@@ -230,7 +237,7 @@ func (pm *ProfileManager) GetActiveConfigPath() (string, error) {
if err != nil {
return "", fmt.Errorf("failed to get active profile: %w", err)
}
return pm.GetConfigPath(activeProfile)
return pm.GetConfigPath(activeProfile.ID)
}
// GetActiveStateFilePath returns the state file path for the currently active profile
@@ -240,18 +247,5 @@ func (pm *ProfileManager) GetActiveStateFilePath() (string, error) {
if err != nil {
return "", fmt.Errorf("failed to get active profile: %w", err)
}
return pm.GetStateFilePath(activeProfile)
}
// sanitizeProfileName removes invalid characters from profile name
func sanitizeProfileName(name string) string {
// Keep only alphanumeric, underscore, and hyphen
var result strings.Builder
for _, r := range name {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') || r == '_' || r == '-' {
result.WriteRune(r)
}
}
return result.String()
return pm.GetStateFilePath(activeProfile.ID)
}

View File

@@ -40,9 +40,9 @@ func generateProfileID() (string, error) {
return hex.EncodeToString(buf), nil
}
// isValidProfileFilenameStem reports whether s is safe to use as the stem
// IsValidProfileFilenameStem reports whether s is safe to use as the stem
// of a profile JSON filename.
func isValidProfileFilenameStem(s string) bool {
func IsValidProfileFilenameStem(s string) bool {
if s == "" || len(s) > maxProfileIDLen {
return false
}

View File

@@ -50,7 +50,7 @@ func (p *Profile) FilePath() (string, error) {
return DefaultConfigPath, nil
}
if !isValidProfileFilenameStem(id) {
if !IsValidProfileFilenameStem(id) {
return "", fmt.Errorf("invalid profile ID: %q", id)
}
@@ -99,7 +99,7 @@ func (pm *ProfileManager) GetActiveProfile() (*Profile, error) {
// supplied handle to a concrete ID, so the value written here is always a
// valid profile filename stem.
func (pm *ProfileManager) SwitchProfile(id string) error {
if id != defaultProfileName && !isValidProfileFilenameStem(id) {
if id != defaultProfileName && !IsValidProfileFilenameStem(id) {
return fmt.Errorf("invalid profile ID: %q", id)
}

View File

@@ -105,7 +105,7 @@ func (a *ActiveProfileState) FilePath() (string, error) {
return DefaultConfigPath, nil
}
if !isValidProfileFilenameStem(a.ID) {
if !IsValidProfileFilenameStem(a.ID) {
return "", fmt.Errorf("invalid profile ID: %q", a.ID)
}
@@ -266,7 +266,7 @@ func (s *ServiceManager) SetActiveProfileState(a *ActiveProfileState) error {
return fmt.Errorf("username must be set for non-default profiles, got: %s", a.ID)
}
if a.ID != defaultProfileName && !isValidProfileFilenameStem(a.ID) {
if a.ID != defaultProfileName && !IsValidProfileFilenameStem(a.ID) {
return fmt.Errorf("invalid profile ID: %q", a.ID)
}
@@ -341,7 +341,7 @@ func (s *ServiceManager) RemoveProfile(id, username string) error {
if id == defaultProfileName {
return fmt.Errorf("cannot remove profile with reserved name: %s", defaultProfileName)
}
if !isValidProfileFilenameStem(id) {
if !IsValidProfileFilenameStem(id) {
return fmt.Errorf("invalid profile ID: %q", id)
}
@@ -408,7 +408,7 @@ func (s *ServiceManager) GetStatePath() string {
return defaultStatePath
}
if !isValidProfileFilenameStem(activeProf.ID) {
if !IsValidProfileFilenameStem(activeProf.ID) {
log.Warnf("invalid active profile ID %q, using default state path", activeProf.ID)
return defaultStatePath
}
@@ -478,7 +478,7 @@ func (s *ServiceManager) loadAllProfiles(username string) ([]Profile, error) {
// default lives at the top-level config dir, not under /<user>
continue
}
if !isValidProfileFilenameStem(stem) {
if !IsValidProfileFilenameStem(stem) {
continue
}
path := filepath.Join(configDir, base)
@@ -498,8 +498,7 @@ func (s *ServiceManager) loadAllProfiles(username string) ([]Profile, error) {
if fileProfiles[i].Name != fileProfiles[j].Name {
return fileProfiles[i].Name < fileProfiles[j].Name
}
// Stable tie-break on ID so duplicate names always render in
// the same order across calls.
// Sort tie-break on ID so duplicate names always render in the same order.
return fileProfiles[i].ID < fileProfiles[j].ID
})
profiles = append(profiles, fileProfiles...)

View File

@@ -218,7 +218,7 @@ func TestIsValidProfileFilenameStem(t *testing.T) {
{strings.Repeat("a", maxProfileIDLen+1), false},
}
for _, tc := range cases {
got := isValidProfileFilenameStem(tc.in)
got := IsValidProfileFilenameStem(tc.in)
assert.Equal(t, tc.want, got, "case %q", tc.in)
}
}

View File

@@ -22,7 +22,7 @@ func (pm *ProfileManager) GetProfileState(id string) (*ProfileState, error) {
return nil, fmt.Errorf("get config directory: %w", err)
}
if id != defaultProfileName && !isValidProfileFilenameStem(id) {
if id != defaultProfileName && !IsValidProfileFilenameStem(id) {
return nil, fmt.Errorf("invalid profile ID: %q", id)
}
@@ -59,7 +59,7 @@ func (pm *ProfileManager) SetActiveProfileState(state *ProfileState) error {
}
id := activeProf.ID
if id != defaultProfileName && !isValidProfileFilenameStem(id) {
if id != defaultProfileName && !IsValidProfileFilenameStem(id) {
return fmt.Errorf("invalid active profile ID: %q", id)
}