mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 15:26:40 +00:00
[client] Feat: Support Multiple Profiles (#3980)
[client] Feat: Support Multiple Profiles (#3980)
This commit is contained in:
BIN
client/ui/assets/connected.png
Normal file
BIN
client/ui/assets/connected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
client/ui/assets/disconnected.png
Normal file
BIN
client/ui/assets/disconnected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -8,8 +8,10 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -34,11 +36,14 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"github.com/netbirdio/netbird/client/iface"
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
"github.com/netbirdio/netbird/client/ui/desktop"
|
||||
"github.com/netbirdio/netbird/client/ui/event"
|
||||
"github.com/netbirdio/netbird/client/ui/process"
|
||||
|
||||
"github.com/netbirdio/netbird/util"
|
||||
|
||||
"github.com/netbirdio/netbird/version"
|
||||
@@ -54,11 +59,11 @@ const (
|
||||
)
|
||||
|
||||
func main() {
|
||||
daemonAddr, showSettings, showNetworks, showLoginURL, showDebug, errorMsg, saveLogsInFile := parseFlags()
|
||||
flags := parseFlags()
|
||||
|
||||
// Initialize file logging if needed.
|
||||
var logFile string
|
||||
if saveLogsInFile {
|
||||
if flags.saveLogsInFile {
|
||||
file, err := initLogFile()
|
||||
if err != nil {
|
||||
log.Errorf("error while initializing log: %v", err)
|
||||
@@ -74,19 +79,28 @@ func main() {
|
||||
a.SetIcon(fyne.NewStaticResource("netbird", iconDisconnected))
|
||||
|
||||
// Show error message window if needed.
|
||||
if errorMsg != "" {
|
||||
showErrorMessage(errorMsg)
|
||||
if flags.errorMsg != "" {
|
||||
showErrorMessage(flags.errorMsg)
|
||||
return
|
||||
}
|
||||
|
||||
// Create the service client (this also builds the settings or networks UI if requested).
|
||||
client := newServiceClient(daemonAddr, logFile, a, showSettings, showNetworks, showLoginURL, showDebug)
|
||||
client := newServiceClient(&newServiceClientArgs{
|
||||
addr: flags.daemonAddr,
|
||||
logFile: logFile,
|
||||
app: a,
|
||||
showSettings: flags.showSettings,
|
||||
showNetworks: flags.showNetworks,
|
||||
showLoginURL: flags.showLoginURL,
|
||||
showDebug: flags.showDebug,
|
||||
showProfiles: flags.showProfiles,
|
||||
})
|
||||
|
||||
// Watch for theme/settings changes to update the icon.
|
||||
go watchSettingsChanges(a, client)
|
||||
|
||||
// Run in window mode if any UI flag was set.
|
||||
if showSettings || showNetworks || showDebug || showLoginURL {
|
||||
if flags.showSettings || flags.showNetworks || flags.showDebug || flags.showLoginURL || flags.showProfiles {
|
||||
a.Run()
|
||||
return
|
||||
}
|
||||
@@ -106,21 +120,35 @@ func main() {
|
||||
systray.Run(client.onTrayReady, client.onTrayExit)
|
||||
}
|
||||
|
||||
type cliFlags struct {
|
||||
daemonAddr string
|
||||
showSettings bool
|
||||
showNetworks bool
|
||||
showProfiles bool
|
||||
showDebug bool
|
||||
showLoginURL bool
|
||||
errorMsg string
|
||||
saveLogsInFile bool
|
||||
}
|
||||
|
||||
// parseFlags reads and returns all needed command-line flags.
|
||||
func parseFlags() (daemonAddr string, showSettings, showNetworks, showLoginURL, showDebug bool, errorMsg string, saveLogsInFile bool) {
|
||||
func parseFlags() *cliFlags {
|
||||
var flags cliFlags
|
||||
|
||||
defaultDaemonAddr := "unix:///var/run/netbird.sock"
|
||||
if runtime.GOOS == "windows" {
|
||||
defaultDaemonAddr = "tcp://127.0.0.1:41731"
|
||||
}
|
||||
flag.StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
|
||||
flag.BoolVar(&showSettings, "settings", false, "run settings window")
|
||||
flag.BoolVar(&showNetworks, "networks", false, "run networks window")
|
||||
flag.BoolVar(&showLoginURL, "login-url", false, "show login URL in a popup window")
|
||||
flag.BoolVar(&showDebug, "debug", false, "run debug window")
|
||||
flag.StringVar(&errorMsg, "error-msg", "", "displays an error message window")
|
||||
flag.BoolVar(&saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", os.TempDir()))
|
||||
flag.StringVar(&flags.daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
|
||||
flag.BoolVar(&flags.showSettings, "settings", false, "run settings window")
|
||||
flag.BoolVar(&flags.showNetworks, "networks", false, "run networks window")
|
||||
flag.BoolVar(&flags.showProfiles, "profiles", false, "run profiles window")
|
||||
flag.BoolVar(&flags.showDebug, "debug", false, "run debug window")
|
||||
flag.StringVar(&flags.errorMsg, "error-msg", "", "displays an error message window")
|
||||
flag.BoolVar(&flags.saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", os.TempDir()))
|
||||
flag.BoolVar(&flags.showLoginURL, "login-url", false, "show login URL in a popup window")
|
||||
flag.Parse()
|
||||
return
|
||||
return &flags
|
||||
}
|
||||
|
||||
// initLogFile initializes logging into a file.
|
||||
@@ -168,6 +196,12 @@ var iconConnectingMacOS []byte
|
||||
//go:embed assets/netbird-systemtray-error-macos.png
|
||||
var iconErrorMacOS []byte
|
||||
|
||||
//go:embed assets/connected.png
|
||||
var iconConnectedDot []byte
|
||||
|
||||
//go:embed assets/disconnected.png
|
||||
var iconDisconnectedDot []byte
|
||||
|
||||
type serviceClient struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@@ -176,9 +210,13 @@ type serviceClient struct {
|
||||
|
||||
eventHandler *eventHandler
|
||||
|
||||
profileManager *profilemanager.ProfileManager
|
||||
|
||||
icAbout []byte
|
||||
icConnected []byte
|
||||
icConnectedDot []byte
|
||||
icDisconnected []byte
|
||||
icDisconnectedDot []byte
|
||||
icUpdateConnected []byte
|
||||
icUpdateDisconnected []byte
|
||||
icConnecting []byte
|
||||
@@ -189,6 +227,7 @@ type serviceClient struct {
|
||||
mUp *systray.MenuItem
|
||||
mDown *systray.MenuItem
|
||||
mSettings *systray.MenuItem
|
||||
mProfile *profileMenu
|
||||
mAbout *systray.MenuItem
|
||||
mGitHub *systray.MenuItem
|
||||
mVersionUI *systray.MenuItem
|
||||
@@ -214,7 +253,6 @@ type serviceClient struct {
|
||||
|
||||
// input elements for settings form
|
||||
iMngURL *widget.Entry
|
||||
iConfigFile *widget.Entry
|
||||
iLogFile *widget.Entry
|
||||
iPreSharedKey *widget.Entry
|
||||
iInterfaceName *widget.Entry
|
||||
@@ -247,6 +285,7 @@ type serviceClient struct {
|
||||
isUpdateIconActive bool
|
||||
showNetworks bool
|
||||
wNetworks fyne.Window
|
||||
wProfiles fyne.Window
|
||||
|
||||
eventManager *event.Manager
|
||||
|
||||
@@ -263,36 +302,50 @@ type menuHandler struct {
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type newServiceClientArgs struct {
|
||||
addr string
|
||||
logFile string
|
||||
app fyne.App
|
||||
showSettings bool
|
||||
showNetworks bool
|
||||
showDebug bool
|
||||
showLoginURL bool
|
||||
showProfiles bool
|
||||
}
|
||||
|
||||
// newServiceClient instance constructor
|
||||
//
|
||||
// This constructor also builds the UI elements for the settings window.
|
||||
func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool, showNetworks bool, showLoginURL bool, showDebug bool) *serviceClient {
|
||||
func newServiceClient(args *newServiceClientArgs) *serviceClient {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := &serviceClient{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
addr: addr,
|
||||
app: a,
|
||||
logFile: logFile,
|
||||
addr: args.addr,
|
||||
app: args.app,
|
||||
logFile: args.logFile,
|
||||
sendNotification: false,
|
||||
|
||||
showAdvancedSettings: showSettings,
|
||||
showNetworks: showNetworks,
|
||||
showAdvancedSettings: args.showSettings,
|
||||
showNetworks: args.showNetworks,
|
||||
update: version.NewUpdate("nb/client-ui"),
|
||||
}
|
||||
|
||||
s.eventHandler = newEventHandler(s)
|
||||
s.profileManager = profilemanager.NewProfileManager()
|
||||
s.setNewIcons()
|
||||
|
||||
switch {
|
||||
case showSettings:
|
||||
case args.showSettings:
|
||||
s.showSettingsUI()
|
||||
case showNetworks:
|
||||
case args.showNetworks:
|
||||
s.showNetworksUI()
|
||||
case showLoginURL:
|
||||
case args.showLoginURL:
|
||||
s.showLoginURL()
|
||||
case showDebug:
|
||||
case args.showDebug:
|
||||
s.showDebugUI()
|
||||
case args.showProfiles:
|
||||
s.showProfilesUI()
|
||||
}
|
||||
|
||||
return s
|
||||
@@ -300,6 +353,8 @@ func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool
|
||||
|
||||
func (s *serviceClient) setNewIcons() {
|
||||
s.icAbout = iconAbout
|
||||
s.icConnectedDot = iconConnectedDot
|
||||
s.icDisconnectedDot = iconDisconnectedDot
|
||||
if s.app.Settings().ThemeVariant() == theme.VariantDark {
|
||||
s.icConnected = iconConnectedDark
|
||||
s.icDisconnected = iconDisconnected
|
||||
@@ -342,8 +397,7 @@ func (s *serviceClient) showSettingsUI() {
|
||||
s.wSettings.SetOnClosed(s.cancel)
|
||||
|
||||
s.iMngURL = widget.NewEntry()
|
||||
s.iConfigFile = widget.NewEntry()
|
||||
s.iConfigFile.Disable()
|
||||
|
||||
s.iLogFile = widget.NewEntry()
|
||||
s.iLogFile.Disable()
|
||||
s.iPreSharedKey = widget.NewPasswordEntry()
|
||||
@@ -368,14 +422,22 @@ func (s *serviceClient) showSettingsUI() {
|
||||
|
||||
// getSettingsForm to embed it into settings window.
|
||||
func (s *serviceClient) getSettingsForm() *widget.Form {
|
||||
|
||||
var activeProfName string
|
||||
activeProf, err := s.profileManager.GetActiveProfile()
|
||||
if err != nil {
|
||||
log.Errorf("get active profile: %v", err)
|
||||
} else {
|
||||
activeProfName = activeProf.Name
|
||||
}
|
||||
return &widget.Form{
|
||||
Items: []*widget.FormItem{
|
||||
{Text: "Profile", Widget: widget.NewLabel(activeProfName)},
|
||||
{Text: "Quantum-Resistance", Widget: s.sRosenpassPermissive},
|
||||
{Text: "Interface Name", Widget: s.iInterfaceName},
|
||||
{Text: "Interface Port", Widget: s.iInterfacePort},
|
||||
{Text: "Management URL", Widget: s.iMngURL},
|
||||
{Text: "Pre-shared Key", Widget: s.iPreSharedKey},
|
||||
{Text: "Config File", Widget: s.iConfigFile},
|
||||
{Text: "Log File", Widget: s.iLogFile},
|
||||
{Text: "Network Monitor", Widget: s.sNetworkMonitor},
|
||||
{Text: "Disable DNS", Widget: s.sDisableDNS},
|
||||
@@ -416,27 +478,67 @@ func (s *serviceClient) getSettingsForm() *widget.Form {
|
||||
s.managementURL = iMngURL
|
||||
s.preSharedKey = s.iPreSharedKey.Text
|
||||
|
||||
loginRequest := proto.LoginRequest{
|
||||
ManagementUrl: iMngURL,
|
||||
IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd",
|
||||
RosenpassPermissive: &s.sRosenpassPermissive.Checked,
|
||||
InterfaceName: &s.iInterfaceName.Text,
|
||||
WireguardPort: &port,
|
||||
NetworkMonitor: &s.sNetworkMonitor.Checked,
|
||||
DisableDns: &s.sDisableDNS.Checked,
|
||||
DisableClientRoutes: &s.sDisableClientRoutes.Checked,
|
||||
DisableServerRoutes: &s.sDisableServerRoutes.Checked,
|
||||
BlockLanAccess: &s.sBlockLANAccess.Checked,
|
||||
}
|
||||
|
||||
if s.iPreSharedKey.Text != censoredPreSharedKey {
|
||||
loginRequest.OptionalPreSharedKey = &s.iPreSharedKey.Text
|
||||
}
|
||||
|
||||
if err := s.restartClient(&loginRequest); err != nil {
|
||||
log.Errorf("restarting client connection: %v", err)
|
||||
currUser, err := user.Current()
|
||||
if err != nil {
|
||||
log.Errorf("get current user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var req proto.SetConfigRequest
|
||||
req.ProfileName = activeProf.Name
|
||||
req.Username = currUser.Username
|
||||
|
||||
if iMngURL != "" {
|
||||
req.ManagementUrl = iMngURL
|
||||
}
|
||||
|
||||
req.RosenpassPermissive = &s.sRosenpassPermissive.Checked
|
||||
req.InterfaceName = &s.iInterfaceName.Text
|
||||
req.WireguardPort = &port
|
||||
req.NetworkMonitor = &s.sNetworkMonitor.Checked
|
||||
req.DisableDns = &s.sDisableDNS.Checked
|
||||
req.DisableClientRoutes = &s.sDisableClientRoutes.Checked
|
||||
req.DisableServerRoutes = &s.sDisableServerRoutes.Checked
|
||||
req.BlockLanAccess = &s.sBlockLANAccess.Checked
|
||||
|
||||
if s.iPreSharedKey.Text != censoredPreSharedKey {
|
||||
req.OptionalPreSharedKey = &s.iPreSharedKey.Text
|
||||
}
|
||||
|
||||
conn, err := s.getSrvClient(failFastTimeout)
|
||||
if err != nil {
|
||||
log.Errorf("get client: %v", err)
|
||||
dialog.ShowError(fmt.Errorf("Failed to connect to the service: %v", err), s.wSettings)
|
||||
return
|
||||
}
|
||||
_, err = conn.SetConfig(s.ctx, &req)
|
||||
if err != nil {
|
||||
log.Errorf("set config: %v", err)
|
||||
dialog.ShowError(fmt.Errorf("Failed to set configuration: %v", err), s.wSettings)
|
||||
return
|
||||
}
|
||||
|
||||
status, err := conn.Status(s.ctx, &proto.StatusRequest{})
|
||||
if err != nil {
|
||||
log.Errorf("get service status: %v", err)
|
||||
dialog.ShowError(fmt.Errorf("Failed to get service status: %v", err), s.wSettings)
|
||||
return
|
||||
}
|
||||
if status.Status == string(internal.StatusConnected) {
|
||||
// run down & up
|
||||
_, err = conn.Down(s.ctx, &proto.DownRequest{})
|
||||
if err != nil {
|
||||
log.Errorf("down service: %v", err)
|
||||
}
|
||||
|
||||
_, err = conn.Up(s.ctx, &proto.UpRequest{})
|
||||
if err != nil {
|
||||
log.Errorf("up service: %v", err)
|
||||
dialog.ShowError(fmt.Errorf("Failed to reconnect: %v", err), s.wSettings)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
OnCancel: func() {
|
||||
@@ -452,8 +554,21 @@ func (s *serviceClient) login(openURL bool) (*proto.LoginResponse, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
activeProf, err := s.profileManager.GetActiveProfile()
|
||||
if err != nil {
|
||||
log.Errorf("get active profile: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
currUser, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get current user: %w", err)
|
||||
}
|
||||
|
||||
loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{
|
||||
IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd",
|
||||
ProfileName: &activeProf.Name,
|
||||
Username: &currUser.Username,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("login to management URL with: %v", err)
|
||||
@@ -461,15 +576,9 @@ func (s *serviceClient) login(openURL bool) (*proto.LoginResponse, error) {
|
||||
}
|
||||
|
||||
if loginResp.NeedsSSOLogin && openURL {
|
||||
err = open.Run(loginResp.VerificationURIComplete)
|
||||
err = s.handleSSOLogin(loginResp, conn)
|
||||
if err != nil {
|
||||
log.Errorf("opening the verification uri in the browser failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = conn.WaitSSOLogin(s.ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
|
||||
if err != nil {
|
||||
log.Errorf("waiting sso login failed with: %v", err)
|
||||
log.Errorf("handle SSO login failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -477,6 +586,34 @@ func (s *serviceClient) login(openURL bool) (*proto.LoginResponse, error) {
|
||||
return loginResp, nil
|
||||
}
|
||||
|
||||
func (s *serviceClient) handleSSOLogin(loginResp *proto.LoginResponse, conn proto.DaemonServiceClient) error {
|
||||
err := open.Run(loginResp.VerificationURIComplete)
|
||||
if err != nil {
|
||||
log.Errorf("opening the verification uri in the browser failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := conn.WaitSSOLogin(s.ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
|
||||
if err != nil {
|
||||
log.Errorf("waiting sso login failed with: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Email != "" {
|
||||
err := s.profileManager.SetActiveProfileState(&profilemanager.ProfileState{
|
||||
Email: resp.Email,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warnf("failed to set profile state: %v", err)
|
||||
} else {
|
||||
s.mProfile.refresh()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *serviceClient) menuUpClick() error {
|
||||
systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting)
|
||||
conn, err := s.getSrvClient(defaultFailTimeout)
|
||||
@@ -575,6 +712,7 @@ func (s *serviceClient) updateStatus() error {
|
||||
}
|
||||
systray.SetTooltip("NetBird (Connected)")
|
||||
s.mStatus.SetTitle("Connected")
|
||||
s.mStatus.SetIcon(s.icConnectedDot)
|
||||
s.mUp.Disable()
|
||||
s.mDown.Enable()
|
||||
s.mNetworks.Enable()
|
||||
@@ -634,6 +772,7 @@ func (s *serviceClient) setDisconnectedStatus() {
|
||||
}
|
||||
systray.SetTooltip("NetBird (Disconnected)")
|
||||
s.mStatus.SetTitle("Disconnected")
|
||||
s.mStatus.SetIcon(s.icDisconnectedDot)
|
||||
s.mDown.Disable()
|
||||
s.mUp.Enable()
|
||||
s.mNetworks.Disable()
|
||||
@@ -658,7 +797,13 @@ func (s *serviceClient) onTrayReady() {
|
||||
|
||||
// setup systray menu items
|
||||
s.mStatus = systray.AddMenuItem("Disconnected", "Disconnected")
|
||||
s.mStatus.SetIcon(s.icDisconnectedDot)
|
||||
s.mStatus.Disable()
|
||||
|
||||
profileMenuItem := systray.AddMenuItem("", "")
|
||||
emailMenuItem := systray.AddMenuItem("", "")
|
||||
s.mProfile = newProfileMenu(s.ctx, s.profileManager, *s.eventHandler, profileMenuItem, emailMenuItem, s.menuDownClick, s.menuUpClick, s.getSrvClient, s.loadSettings)
|
||||
|
||||
systray.AddSeparator()
|
||||
s.mUp = systray.AddMenuItem("Connect", "Connect")
|
||||
s.mDown = systray.AddMenuItem("Disconnect", "Disconnect")
|
||||
@@ -790,7 +935,15 @@ func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonService
|
||||
|
||||
// getSrvConfig from the service to show it in the settings window.
|
||||
func (s *serviceClient) getSrvConfig() {
|
||||
s.managementURL = internal.DefaultManagementURL
|
||||
s.managementURL = profilemanager.DefaultManagementURL
|
||||
|
||||
_, err := s.profileManager.GetActiveProfile()
|
||||
if err != nil {
|
||||
log.Errorf("get active profile: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var cfg *profilemanager.Config
|
||||
|
||||
conn, err := s.getSrvClient(failFastTimeout)
|
||||
if err != nil {
|
||||
@@ -798,48 +951,63 @@ func (s *serviceClient) getSrvConfig() {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{})
|
||||
currUser, err := user.Current()
|
||||
if err != nil {
|
||||
log.Errorf("get current user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
activeProf, err := s.profileManager.GetActiveProfile()
|
||||
if err != nil {
|
||||
log.Errorf("get active profile: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
srvCfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{
|
||||
ProfileName: activeProf.Name,
|
||||
Username: currUser.Username,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("get config settings from server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.ManagementUrl != "" {
|
||||
s.managementURL = cfg.ManagementUrl
|
||||
cfg = protoConfigToConfig(srvCfg)
|
||||
|
||||
if cfg.ManagementURL.String() != "" {
|
||||
s.managementURL = cfg.ManagementURL.String()
|
||||
}
|
||||
s.preSharedKey = cfg.PreSharedKey
|
||||
s.RosenpassPermissive = cfg.RosenpassPermissive
|
||||
s.interfaceName = cfg.InterfaceName
|
||||
s.interfacePort = int(cfg.WireguardPort)
|
||||
s.interfaceName = cfg.WgIface
|
||||
s.interfacePort = cfg.WgPort
|
||||
|
||||
s.networkMonitor = cfg.NetworkMonitor
|
||||
s.disableDNS = cfg.DisableDns
|
||||
s.networkMonitor = *cfg.NetworkMonitor
|
||||
s.disableDNS = cfg.DisableDNS
|
||||
s.disableClientRoutes = cfg.DisableClientRoutes
|
||||
s.disableServerRoutes = cfg.DisableServerRoutes
|
||||
s.blockLANAccess = cfg.BlockLanAccess
|
||||
s.blockLANAccess = cfg.BlockLANAccess
|
||||
|
||||
if s.showAdvancedSettings {
|
||||
s.iMngURL.SetText(s.managementURL)
|
||||
s.iConfigFile.SetText(cfg.ConfigFile)
|
||||
s.iLogFile.SetText(cfg.LogFile)
|
||||
s.iPreSharedKey.SetText(cfg.PreSharedKey)
|
||||
s.iInterfaceName.SetText(cfg.InterfaceName)
|
||||
s.iInterfacePort.SetText(strconv.Itoa(int(cfg.WireguardPort)))
|
||||
s.iInterfaceName.SetText(cfg.WgIface)
|
||||
s.iInterfacePort.SetText(strconv.Itoa(cfg.WgPort))
|
||||
s.sRosenpassPermissive.SetChecked(cfg.RosenpassPermissive)
|
||||
if !cfg.RosenpassEnabled {
|
||||
s.sRosenpassPermissive.Disable()
|
||||
}
|
||||
s.sNetworkMonitor.SetChecked(cfg.NetworkMonitor)
|
||||
s.sDisableDNS.SetChecked(cfg.DisableDns)
|
||||
s.sNetworkMonitor.SetChecked(*cfg.NetworkMonitor)
|
||||
s.sDisableDNS.SetChecked(cfg.DisableDNS)
|
||||
s.sDisableClientRoutes.SetChecked(cfg.DisableClientRoutes)
|
||||
s.sDisableServerRoutes.SetChecked(cfg.DisableServerRoutes)
|
||||
s.sBlockLANAccess.SetChecked(cfg.BlockLanAccess)
|
||||
s.sBlockLANAccess.SetChecked(cfg.BlockLANAccess)
|
||||
}
|
||||
|
||||
if s.mNotifications == nil {
|
||||
return
|
||||
}
|
||||
if cfg.DisableNotifications {
|
||||
if cfg.DisableNotifications != nil && *cfg.DisableNotifications {
|
||||
s.mNotifications.Uncheck()
|
||||
} else {
|
||||
s.mNotifications.Check()
|
||||
@@ -849,6 +1017,58 @@ func (s *serviceClient) getSrvConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
func protoConfigToConfig(cfg *proto.GetConfigResponse) *profilemanager.Config {
|
||||
|
||||
var config profilemanager.Config
|
||||
|
||||
if cfg.ManagementUrl != "" {
|
||||
parsed, err := url.Parse(cfg.ManagementUrl)
|
||||
if err != nil {
|
||||
log.Errorf("parse management URL: %v", err)
|
||||
} else {
|
||||
config.ManagementURL = parsed
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.PreSharedKey != "" {
|
||||
if cfg.PreSharedKey != censoredPreSharedKey {
|
||||
config.PreSharedKey = cfg.PreSharedKey
|
||||
} else {
|
||||
config.PreSharedKey = ""
|
||||
}
|
||||
}
|
||||
if cfg.AdminURL != "" {
|
||||
parsed, err := url.Parse(cfg.AdminURL)
|
||||
if err != nil {
|
||||
log.Errorf("parse admin URL: %v", err)
|
||||
} else {
|
||||
config.AdminURL = parsed
|
||||
}
|
||||
}
|
||||
|
||||
config.WgIface = cfg.InterfaceName
|
||||
if cfg.WireguardPort != 0 {
|
||||
config.WgPort = int(cfg.WireguardPort)
|
||||
} else {
|
||||
config.WgPort = iface.DefaultWgPort
|
||||
}
|
||||
|
||||
config.DisableAutoConnect = cfg.DisableAutoConnect
|
||||
config.ServerSSHAllowed = &cfg.ServerSSHAllowed
|
||||
config.RosenpassEnabled = cfg.RosenpassEnabled
|
||||
config.RosenpassPermissive = cfg.RosenpassPermissive
|
||||
config.DisableNotifications = &cfg.DisableNotifications
|
||||
config.LazyConnectionEnabled = cfg.LazyConnectionEnabled
|
||||
config.BlockInbound = cfg.BlockInbound
|
||||
config.NetworkMonitor = &cfg.NetworkMonitor
|
||||
config.DisableDNS = cfg.DisableDns
|
||||
config.DisableClientRoutes = cfg.DisableClientRoutes
|
||||
config.DisableServerRoutes = cfg.DisableServerRoutes
|
||||
config.BlockLANAccess = cfg.BlockLanAccess
|
||||
|
||||
return &config
|
||||
}
|
||||
|
||||
func (s *serviceClient) onUpdateAvailable() {
|
||||
s.updateIndicationLock.Lock()
|
||||
defer s.updateIndicationLock.Unlock()
|
||||
@@ -880,7 +1100,22 @@ func (s *serviceClient) loadSettings() {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{})
|
||||
currUser, err := user.Current()
|
||||
if err != nil {
|
||||
log.Errorf("get current user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
activeProf, err := s.profileManager.GetActiveProfile()
|
||||
if err != nil {
|
||||
log.Errorf("get active profile: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{
|
||||
ProfileName: activeProf.Name,
|
||||
Username: currUser.Username,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("get config settings from server: %v", err)
|
||||
return
|
||||
@@ -936,41 +1171,37 @@ func (s *serviceClient) updateConfig() error {
|
||||
blockInbound := s.mBlockInbound.Checked()
|
||||
notificationsDisabled := !s.mNotifications.Checked()
|
||||
|
||||
loginRequest := proto.LoginRequest{
|
||||
IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd",
|
||||
activeProf, err := s.profileManager.GetActiveProfile()
|
||||
if err != nil {
|
||||
log.Errorf("get active profile: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
currUser, err := user.Current()
|
||||
if err != nil {
|
||||
log.Errorf("get current user: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := s.getSrvClient(failFastTimeout)
|
||||
if err != nil {
|
||||
log.Errorf("get client: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
req := proto.SetConfigRequest{
|
||||
ProfileName: activeProf.Name,
|
||||
Username: currUser.Username,
|
||||
DisableAutoConnect: &disableAutoStart,
|
||||
ServerSSHAllowed: &sshAllowed,
|
||||
RosenpassEnabled: &rosenpassEnabled,
|
||||
DisableAutoConnect: &disableAutoStart,
|
||||
DisableNotifications: ¬ificationsDisabled,
|
||||
LazyConnectionEnabled: &lazyConnectionEnabled,
|
||||
BlockInbound: &blockInbound,
|
||||
DisableNotifications: ¬ificationsDisabled,
|
||||
}
|
||||
|
||||
if err := s.restartClient(&loginRequest); err != nil {
|
||||
log.Errorf("restarting client connection: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// restartClient restarts the client connection.
|
||||
func (s *serviceClient) restartClient(loginRequest *proto.LoginRequest) error {
|
||||
ctx, cancel := context.WithTimeout(s.ctx, defaultFailTimeout)
|
||||
defer cancel()
|
||||
|
||||
client, err := s.getSrvClient(failFastTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.Login(ctx, loginRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.Up(ctx, &proto.UpRequest{})
|
||||
if err != nil {
|
||||
if _, err := conn.SetConfig(s.ctx, &req); err != nil {
|
||||
log.Errorf("set config settings on server: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
const (
|
||||
settingsMenuDescr = "Settings of the application"
|
||||
profilesMenuDescr = "Manage your profiles"
|
||||
allowSSHMenuDescr = "Allow SSH connections"
|
||||
autoConnectMenuDescr = "Connect automatically when the service starts"
|
||||
quantumResistanceMenuDescr = "Enable post-quantum security via Rosenpass"
|
||||
|
||||
@@ -433,7 +433,7 @@ func (s *serviceClient) collectDebugData(
|
||||
|
||||
var postUpStatusOutput string
|
||||
if postUpStatus != nil {
|
||||
overview := nbstatus.ConvertToStatusOutputOverview(postUpStatus, params.anonymize, "", nil, nil, nil, "")
|
||||
overview := nbstatus.ConvertToStatusOutputOverview(postUpStatus, params.anonymize, "", nil, nil, nil, "", "")
|
||||
postUpStatusOutput = nbstatus.ParseToFullDetailSummary(overview)
|
||||
}
|
||||
headerPostUp := fmt.Sprintf("----- NetBird post-up - Timestamp: %s", time.Now().Format(time.RFC3339))
|
||||
@@ -450,7 +450,7 @@ func (s *serviceClient) collectDebugData(
|
||||
|
||||
var preDownStatusOutput string
|
||||
if preDownStatus != nil {
|
||||
overview := nbstatus.ConvertToStatusOutputOverview(preDownStatus, params.anonymize, "", nil, nil, nil, "")
|
||||
overview := nbstatus.ConvertToStatusOutputOverview(preDownStatus, params.anonymize, "", nil, nil, nil, "", "")
|
||||
preDownStatusOutput = nbstatus.ParseToFullDetailSummary(overview)
|
||||
}
|
||||
headerPreDown := fmt.Sprintf("----- NetBird pre-down - Timestamp: %s - Duration: %s",
|
||||
@@ -581,7 +581,7 @@ func (s *serviceClient) createDebugBundle(anonymize bool, systemInfo bool, uploa
|
||||
|
||||
var statusOutput string
|
||||
if statusResp != nil {
|
||||
overview := nbstatus.ConvertToStatusOutputOverview(statusResp, anonymize, "", nil, nil, nil, "")
|
||||
overview := nbstatus.ConvertToStatusOutputOverview(statusResp, anonymize, "", nil, nil, nil, "", "")
|
||||
statusOutput = nbstatus.ParseToFullDetailSummary(overview)
|
||||
}
|
||||
|
||||
|
||||
601
client/ui/profile.go
Normal file
601
client/ui/profile.go
Normal file
@@ -0,0 +1,601 @@
|
||||
//go:build !(linux && 386)
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/user"
|
||||
"slices"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"fyne.io/systray"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
// showProfilesUI creates and displays the Profiles window with a list of existing profiles,
|
||||
// a button to add new profiles, allows removal, and lets the user switch the active profile.
|
||||
func (s *serviceClient) showProfilesUI() {
|
||||
|
||||
profiles, err := s.getProfiles()
|
||||
if err != nil {
|
||||
log.Errorf("get profiles: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var refresh func()
|
||||
// List widget for profiles
|
||||
list := widget.NewList(
|
||||
func() int { return len(profiles) },
|
||||
func() fyne.CanvasObject {
|
||||
// Each item: Selected indicator, Name, spacer, Select & Remove buttons
|
||||
return container.NewHBox(
|
||||
widget.NewLabel(""), // indicator
|
||||
widget.NewLabel(""), // profile name
|
||||
layout.NewSpacer(),
|
||||
widget.NewButton("Select", nil),
|
||||
widget.NewButton("Remove", nil),
|
||||
)
|
||||
},
|
||||
func(i widget.ListItemID, item fyne.CanvasObject) {
|
||||
// Populate each row
|
||||
row := item.(*fyne.Container)
|
||||
indicator := row.Objects[0].(*widget.Label)
|
||||
nameLabel := row.Objects[1].(*widget.Label)
|
||||
selectBtn := row.Objects[3].(*widget.Button)
|
||||
removeBtn := row.Objects[4].(*widget.Button)
|
||||
|
||||
profile := profiles[i]
|
||||
// Show a checkmark if selected
|
||||
if profile.IsActive {
|
||||
indicator.SetText("✓")
|
||||
} else {
|
||||
indicator.SetText("")
|
||||
}
|
||||
nameLabel.SetText(profile.Name)
|
||||
|
||||
// Configure Select/Active button
|
||||
selectBtn.SetText(func() string {
|
||||
if profile.IsActive {
|
||||
return "Active"
|
||||
}
|
||||
return "Select"
|
||||
}())
|
||||
selectBtn.OnTapped = func() {
|
||||
if profile.IsActive {
|
||||
return // already active
|
||||
}
|
||||
// confirm switch
|
||||
dialog.ShowConfirm(
|
||||
"Switch Profile",
|
||||
fmt.Sprintf("Are you sure you want to switch to '%s'?", profile.Name),
|
||||
func(confirm bool) {
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
// switch
|
||||
err = s.switchProfile(profile.Name)
|
||||
if err != nil {
|
||||
log.Errorf("failed to switch profile: %v", err)
|
||||
dialog.ShowError(errors.New("failed to select profile"), s.wProfiles)
|
||||
return
|
||||
}
|
||||
|
||||
dialog.ShowInformation(
|
||||
"Profile Switched",
|
||||
fmt.Sprintf("Profile '%s' switched successfully", profile.Name),
|
||||
s.wProfiles,
|
||||
)
|
||||
|
||||
conn, err := s.getSrvClient(defaultFailTimeout)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get daemon client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
status, err := conn.Status(context.Background(), &proto.StatusRequest{})
|
||||
if err != nil {
|
||||
log.Errorf("failed to get status after switching profile: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if status.Status == string(internal.StatusConnected) {
|
||||
if err := s.menuDownClick(); err != nil {
|
||||
log.Errorf("failed to handle down click after switching profile: %v", err)
|
||||
dialog.ShowError(fmt.Errorf("failed to handle down click"), s.wProfiles)
|
||||
return
|
||||
}
|
||||
}
|
||||
// update slice flags
|
||||
refresh()
|
||||
},
|
||||
s.wProfiles,
|
||||
)
|
||||
}
|
||||
|
||||
// Remove profile
|
||||
removeBtn.SetText("Remove")
|
||||
removeBtn.OnTapped = func() {
|
||||
dialog.ShowConfirm(
|
||||
"Delete Profile",
|
||||
fmt.Sprintf("Are you sure you want to delete '%s'?", profile.Name),
|
||||
func(confirm bool) {
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
// remove
|
||||
err = s.removeProfile(profile.Name)
|
||||
if err != nil {
|
||||
log.Errorf("failed to remove profile: %v", err)
|
||||
dialog.ShowError(fmt.Errorf("failed to remove profile"), s.wProfiles)
|
||||
return
|
||||
}
|
||||
dialog.ShowInformation(
|
||||
"Profile Removed",
|
||||
fmt.Sprintf("Profile '%s' removed successfully", profile.Name),
|
||||
s.wProfiles,
|
||||
)
|
||||
// update slice
|
||||
refresh()
|
||||
},
|
||||
s.wProfiles,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
refresh = func() {
|
||||
newProfiles, err := s.getProfiles()
|
||||
if err != nil {
|
||||
dialog.ShowError(err, s.wProfiles)
|
||||
return
|
||||
}
|
||||
profiles = newProfiles // update the slice
|
||||
list.Refresh() // tell Fyne to re-call length/update on every visible row
|
||||
}
|
||||
|
||||
// Button to add a new profile
|
||||
newBtn := widget.NewButton("New Profile", func() {
|
||||
nameEntry := widget.NewEntry()
|
||||
nameEntry.SetPlaceHolder("Enter Profile Name")
|
||||
|
||||
formItems := []*widget.FormItem{{Text: "Name:", Widget: nameEntry}}
|
||||
dlg := dialog.NewForm(
|
||||
"New Profile",
|
||||
"Create",
|
||||
"Cancel",
|
||||
formItems,
|
||||
func(confirm bool) {
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
name := nameEntry.Text
|
||||
if name == "" {
|
||||
dialog.ShowError(errors.New("profile name cannot be empty"), s.wProfiles)
|
||||
return
|
||||
}
|
||||
|
||||
// add profile
|
||||
err = s.addProfile(name)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create profile: %v", err)
|
||||
dialog.ShowError(fmt.Errorf("failed to create profile"), s.wProfiles)
|
||||
return
|
||||
}
|
||||
dialog.ShowInformation(
|
||||
"Profile Created",
|
||||
fmt.Sprintf("Profile '%s' created successfully", name),
|
||||
s.wProfiles,
|
||||
)
|
||||
// update slice
|
||||
refresh()
|
||||
},
|
||||
s.wProfiles,
|
||||
)
|
||||
// make dialog wider
|
||||
dlg.Resize(fyne.NewSize(350, 150))
|
||||
dlg.Show()
|
||||
})
|
||||
|
||||
// Assemble window content
|
||||
content := container.NewBorder(nil, newBtn, nil, nil, list)
|
||||
s.wProfiles = s.app.NewWindow("NetBird Profiles")
|
||||
s.wProfiles.SetContent(content)
|
||||
s.wProfiles.Resize(fyne.NewSize(400, 300))
|
||||
s.wProfiles.SetOnClosed(s.cancel)
|
||||
|
||||
s.wProfiles.Show()
|
||||
}
|
||||
|
||||
func (s *serviceClient) addProfile(profileName string) error {
|
||||
conn, err := s.getSrvClient(defaultFailTimeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf(getClientFMT, err)
|
||||
}
|
||||
|
||||
currUser, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get current user: %w", err)
|
||||
}
|
||||
|
||||
_, err = conn.AddProfile(context.Background(), &proto.AddProfileRequest{
|
||||
ProfileName: profileName,
|
||||
Username: currUser.Username,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("add profile: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *serviceClient) switchProfile(profileName string) error {
|
||||
conn, err := s.getSrvClient(defaultFailTimeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf(getClientFMT, err)
|
||||
}
|
||||
|
||||
currUser, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get current user: %w", err)
|
||||
}
|
||||
|
||||
if _, err := conn.SwitchProfile(context.Background(), &proto.SwitchProfileRequest{
|
||||
ProfileName: &profileName,
|
||||
Username: &currUser.Username,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("switch profile failed: %w", err)
|
||||
}
|
||||
|
||||
err = s.profileManager.SwitchProfile(profileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("switch profile: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *serviceClient) removeProfile(profileName string) error {
|
||||
conn, err := s.getSrvClient(defaultFailTimeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf(getClientFMT, err)
|
||||
}
|
||||
|
||||
currUser, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get current user: %w", err)
|
||||
}
|
||||
|
||||
_, err = conn.RemoveProfile(context.Background(), &proto.RemoveProfileRequest{
|
||||
ProfileName: profileName,
|
||||
Username: currUser.Username,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("remove profile: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Profile struct {
|
||||
Name string
|
||||
IsActive bool
|
||||
}
|
||||
|
||||
func (s *serviceClient) getProfiles() ([]Profile, error) {
|
||||
conn, err := s.getSrvClient(defaultFailTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(getClientFMT, err)
|
||||
}
|
||||
|
||||
currUser, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get current user: %w", err)
|
||||
}
|
||||
profilesResp, err := conn.ListProfiles(context.Background(), &proto.ListProfilesRequest{
|
||||
Username: currUser.Username,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list profiles: %w", err)
|
||||
}
|
||||
|
||||
var profiles []Profile
|
||||
|
||||
for _, profile := range profilesResp.Profiles {
|
||||
profiles = append(profiles, Profile{
|
||||
Name: profile.Name,
|
||||
IsActive: profile.IsActive,
|
||||
})
|
||||
}
|
||||
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
type subItem struct {
|
||||
*systray.MenuItem
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type profileMenu struct {
|
||||
mu sync.Mutex
|
||||
ctx context.Context
|
||||
profileManager *profilemanager.ProfileManager
|
||||
eventHandler eventHandler
|
||||
profileMenuItem *systray.MenuItem
|
||||
emailMenuItem *systray.MenuItem
|
||||
profileSubItems []*subItem
|
||||
manageProfilesSubItem *subItem
|
||||
profilesState []Profile
|
||||
downClickCallback func() error
|
||||
upClickCallback func() error
|
||||
getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error)
|
||||
loadSettingsCallback func()
|
||||
}
|
||||
|
||||
func newProfileMenu(ctx context.Context, profileManager *profilemanager.ProfileManager,
|
||||
|
||||
eventHandler eventHandler, profileMenuItem, emailMenuItem *systray.MenuItem,
|
||||
downClickCallback, upClickCallback func() error,
|
||||
getSrvClientCallback func(timeout time.Duration) (proto.DaemonServiceClient, error),
|
||||
loadSettingsCallback func()) *profileMenu {
|
||||
p := profileMenu{
|
||||
ctx: ctx,
|
||||
profileManager: profileManager,
|
||||
eventHandler: eventHandler,
|
||||
profileMenuItem: profileMenuItem,
|
||||
emailMenuItem: emailMenuItem,
|
||||
downClickCallback: downClickCallback,
|
||||
upClickCallback: upClickCallback,
|
||||
getSrvClientCallback: getSrvClientCallback,
|
||||
loadSettingsCallback: loadSettingsCallback,
|
||||
}
|
||||
|
||||
p.emailMenuItem.Disable()
|
||||
p.emailMenuItem.Hide()
|
||||
p.refresh()
|
||||
go p.updateMenu()
|
||||
|
||||
return &p
|
||||
}
|
||||
|
||||
func (p *profileMenu) getProfiles() ([]Profile, error) {
|
||||
conn, err := p.getSrvClientCallback(defaultFailTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(getClientFMT, err)
|
||||
}
|
||||
currUser, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get current user: %w", err)
|
||||
}
|
||||
|
||||
profilesResp, err := conn.ListProfiles(p.ctx, &proto.ListProfilesRequest{
|
||||
Username: currUser.Username,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list profiles: %w", err)
|
||||
}
|
||||
|
||||
var profiles []Profile
|
||||
|
||||
for _, profile := range profilesResp.Profiles {
|
||||
profiles = append(profiles, Profile{
|
||||
Name: profile.Name,
|
||||
IsActive: profile.IsActive,
|
||||
})
|
||||
}
|
||||
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (p *profileMenu) refresh() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
profiles, err := p.getProfiles()
|
||||
if err != nil {
|
||||
log.Errorf("failed to list profiles: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear existing profile items
|
||||
p.clear(profiles)
|
||||
|
||||
currUser, err := user.Current()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get current user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := p.getSrvClientCallback(defaultFailTimeout)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get daemon client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
activeProf, err := conn.GetActiveProfile(p.ctx, &proto.GetActiveProfileRequest{})
|
||||
if err != nil {
|
||||
log.Errorf("failed to get active profile: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if activeProf.ProfileName == "default" || activeProf.Username == currUser.Username {
|
||||
activeProfState, err := p.profileManager.GetProfileState(activeProf.ProfileName)
|
||||
if err != nil {
|
||||
log.Warnf("failed to get active profile state: %v", err)
|
||||
p.emailMenuItem.Hide()
|
||||
} else if activeProfState.Email != "" {
|
||||
p.emailMenuItem.SetTitle(fmt.Sprintf("(%s)", activeProfState.Email))
|
||||
p.emailMenuItem.Show()
|
||||
}
|
||||
}
|
||||
|
||||
for _, profile := range profiles {
|
||||
item := p.profileMenuItem.AddSubMenuItem(profile.Name, "")
|
||||
if profile.IsActive {
|
||||
item.Check()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
p.profileSubItems = append(p.profileSubItems, &subItem{item, ctx, cancel})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return // context cancelled
|
||||
case _, ok := <-item.ClickedCh:
|
||||
if !ok {
|
||||
return // channel closed
|
||||
}
|
||||
|
||||
// Handle profile selection
|
||||
if profile.IsActive {
|
||||
log.Infof("Profile '%s' is already active", profile.Name)
|
||||
return
|
||||
}
|
||||
conn, err := p.getSrvClientCallback(defaultFailTimeout)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get daemon client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = conn.SwitchProfile(ctx, &proto.SwitchProfileRequest{
|
||||
ProfileName: &profile.Name,
|
||||
Username: &currUser.Username,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("failed to switch profile: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = p.profileManager.SwitchProfile(profile.Name)
|
||||
if err != nil {
|
||||
log.Errorf("failed to switch profile '%s': %v", profile.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Switched to profile '%s'", profile.Name)
|
||||
|
||||
status, err := conn.Status(ctx, &proto.StatusRequest{})
|
||||
if err != nil {
|
||||
log.Errorf("failed to get status after switching profile: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if status.Status == string(internal.StatusConnected) {
|
||||
if err := p.downClickCallback(); err != nil {
|
||||
log.Errorf("failed to handle down click after switching profile: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.upClickCallback(); err != nil {
|
||||
log.Errorf("failed to handle up click after switching profile: %v", err)
|
||||
}
|
||||
|
||||
p.refresh()
|
||||
p.loadSettingsCallback()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
manageItem := p.profileMenuItem.AddSubMenuItem("Manage Profiles", "")
|
||||
p.manageProfilesSubItem = &subItem{manageItem, ctx, cancel}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return // context cancelled
|
||||
case _, ok := <-manageItem.ClickedCh:
|
||||
if !ok {
|
||||
return // channel closed
|
||||
}
|
||||
// Handle manage profiles click
|
||||
p.eventHandler.runSelfCommand(p.ctx, "profiles", "true")
|
||||
p.refresh()
|
||||
p.loadSettingsCallback()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if activeProf.ProfileName == "default" || activeProf.Username == currUser.Username {
|
||||
p.profileMenuItem.SetTitle(activeProf.ProfileName)
|
||||
} else {
|
||||
p.profileMenuItem.SetTitle(fmt.Sprintf("Profile: %s (User: %s)", activeProf.ProfileName, activeProf.Username))
|
||||
p.emailMenuItem.Hide()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (p *profileMenu) clear(profiles []Profile) {
|
||||
// Clear existing profile items
|
||||
for _, item := range p.profileSubItems {
|
||||
item.Remove()
|
||||
item.cancel()
|
||||
}
|
||||
p.profileSubItems = make([]*subItem, 0, len(profiles))
|
||||
p.profilesState = profiles
|
||||
|
||||
if p.manageProfilesSubItem != nil {
|
||||
// Remove the manage profiles item if it exists
|
||||
p.manageProfilesSubItem.Remove()
|
||||
p.manageProfilesSubItem.cancel()
|
||||
p.manageProfilesSubItem = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *profileMenu) updateMenu() {
|
||||
// check every second
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
|
||||
// get profilesList
|
||||
profiles, err := p.getProfiles()
|
||||
if err != nil {
|
||||
log.Errorf("failed to list profiles: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
sort.Slice(profiles, func(i, j int) bool {
|
||||
return profiles[i].Name < profiles[j].Name
|
||||
})
|
||||
|
||||
p.mu.Lock()
|
||||
state := p.profilesState
|
||||
p.mu.Unlock()
|
||||
|
||||
sort.Slice(state, func(i, j int) bool {
|
||||
return state[i].Name < state[j].Name
|
||||
})
|
||||
|
||||
if slices.Equal(profiles, state) {
|
||||
continue
|
||||
}
|
||||
|
||||
p.refresh()
|
||||
case <-p.ctx.Done():
|
||||
return // context cancelled
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user