Browse Source

resolve merge conflicts

abhishek9686 1 year ago
parent
commit
8466cd4bb3

+ 1 - 0
.github/ISSUE_TEMPLATE/bug-report.yml

@@ -31,6 +31,7 @@ body:
       label: Version
       label: Version
       description: What version are you running?
       description: What version are you running?
       options:
       options:
+        - v0.23.1
         - v0.23.0
         - v0.23.0
         - v0.22.0
         - v0.22.0
         - v0.21.2
         - v0.21.2

+ 1 - 1
README.md

@@ -16,7 +16,7 @@
 
 
 <p align="center">
 <p align="center">
   <a href="https://github.com/gravitl/netmaker/releases">
   <a href="https://github.com/gravitl/netmaker/releases">
-    <img src="https://img.shields.io/badge/Version-0.23.0-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-0.23.1-informational?style=flat-square" />
   </a>
   </a>
   <a href="https://hub.docker.com/r/gravitl/netmaker/tags">
   <a href="https://hub.docker.com/r/gravitl/netmaker/tags">
     <img src="https://img.shields.io/docker/pulls/gravitl/netmaker?label=downloads" />
     <img src="https://img.shields.io/docker/pulls/gravitl/netmaker?label=downloads" />

+ 25 - 6
auth/auth.go

@@ -155,7 +155,7 @@ func HandleAuthLogin(w http.ResponseWriter, r *http.Request) {
 
 
 // IsOauthUser - returns
 // IsOauthUser - returns
 func IsOauthUser(user *models.User) error {
 func IsOauthUser(user *models.User) error {
-	var currentValue, err = fetchPassValue("")
+	var currentValue, err = FetchPassValue("")
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -245,12 +245,11 @@ func addUser(email string) error {
 		slog.Error("error checking for existence of admin user during OAuth login for", "email", email, "error", err)
 		slog.Error("error checking for existence of admin user during OAuth login for", "email", email, "error", err)
 		return err
 		return err
 	} // generate random password to adapt to current model
 	} // generate random password to adapt to current model
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
-		logger.Log(0, "failed to get password: ", err.Error())
+		slog.Error("failed to get password", "error", err.Error())
 		return fetchErr
 		return fetchErr
 	}
 	}
-	logger.Log(0, "fetched new pass: ", newPass, email)
 	var newUser = models.User{
 	var newUser = models.User{
 		UserName: email,
 		UserName: email,
 		Password: newPass,
 		Password: newPass,
@@ -266,7 +265,7 @@ func addUser(email string) error {
 		// TODO: add ability to add users with preemptive permissions
 		// TODO: add ability to add users with preemptive permissions
 		newUser.IsAdmin = false
 		newUser.IsAdmin = false
 		if err = logic.CreateUser(&newUser); err != nil {
 		if err = logic.CreateUser(&newUser); err != nil {
-			logger.Log(1, "error creating user,", email, "; user not added", "error", err.Error())
+			logger.Log(0, "error creating user,", email, "; user not added", "error", err.Error())
 		} else {
 		} else {
 			logger.Log(0, "user created from ", email)
 			logger.Log(0, "user created from ", email)
 		}
 		}
@@ -274,7 +273,7 @@ func addUser(email string) error {
 	return nil
 	return nil
 }
 }
 
 
-func fetchPassValue(newValue string) (string, error) {
+func FetchPassValue(newValue string) (string, error) {
 
 
 	type valueHolder struct {
 	type valueHolder struct {
 		Value string `json:"value" bson:"value"`
 		Value string `json:"value" bson:"value"`
@@ -328,3 +327,23 @@ func isStateCached(state string) bool {
 	_, err := netcache.Get(state)
 	_, err := netcache.Get(state)
 	return err == nil || strings.Contains(err.Error(), "expired")
 	return err == nil || strings.Contains(err.Error(), "expired")
 }
 }
+
+// isEmailAllowed - checks if email is allowed to signup
+func isEmailAllowed(email string) bool {
+	allowedDomains := servercfg.GetAllowedEmailDomains()
+	domains := strings.Split(allowedDomains, ",")
+	if len(domains) == 1 && domains[0] == "*" {
+		return true
+	}
+	emailParts := strings.Split(email, "@")
+	if len(emailParts) < 2 {
+		return false
+	}
+	baseDomainOfEmail := emailParts[1]
+	for _, domain := range domains {
+		if domain == baseDomainOfEmail {
+			return true
+		}
+	}
+	return false
+}

+ 24 - 3
auth/azure-ad.go

@@ -7,6 +7,7 @@ import (
 	"io"
 	"io"
 	"net/http"
 	"net/http"
 
 
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
@@ -60,9 +61,29 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
+	if !isEmailAllowed(content.UserPrincipalName) {
+		handleOauthUserNotAllowedToSignUp(w)
+		return
+	}
+	// check if user approval is already pending
+	if logic.IsPendingUser(content.UserPrincipalName) {
+		handleOauthUserSignUpApprovalPending(w)
+		return
+	}
 	_, err = logic.GetUser(content.UserPrincipalName)
 	_, err = logic.GetUser(content.UserPrincipalName)
-	if err != nil { // user must not exists, so try to make one
-		if err = addUser(content.UserPrincipalName); err != nil {
+	if err != nil {
+		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
+			err = logic.InsertPendingUser(&models.User{
+				UserName: content.UserPrincipalName,
+			})
+			if err != nil {
+				handleSomethingWentWrong(w)
+				return
+			}
+			handleFirstTimeOauthUserSignUp(w)
+			return
+		} else {
+			handleSomethingWentWrong(w)
 			return
 			return
 		}
 		}
 	}
 	}
@@ -75,7 +96,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		handleOauthUserNotAllowed(w)
 		return
 		return
 	}
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}

+ 51 - 1
auth/error.go

@@ -12,17 +12,44 @@ const oauthNotConfigured = `<!DOCTYPE html><html>
 
 
 const userNotAllowed = `<!DOCTYPE html><html>
 const userNotAllowed = `<!DOCTYPE html><html>
 <body>
 <body>
-<h3>Only Admins are allowed to access Dashboard.</h3>
+<h3>Only administrators can access the Dashboard. Please contact your administrator to elevate your account.</h3>
 <p>Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/pro/rac.html" target="_blank" rel="noopener">RemoteAccessClient.</a></p>
 <p>Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/pro/rac.html" target="_blank" rel="noopener">RemoteAccessClient.</a></p>
 </body>
 </body>
 </html>
 </html>
 `
 `
+
+const userFirstTimeSignUp = `<!DOCTYPE html><html>
+<body>
+<h3>Thank you for signing up. Please contact your administrator for access.</h3>
+</body>
+</html>
+`
+
+const userSignUpApprovalPending = `<!DOCTYPE html><html>
+<body>
+<h3>Your account is yet to be approved. Please contact your administrator for access.</h3>
+</body>
+</html>
+`
+
 const userNotFound = `<!DOCTYPE html><html>
 const userNotFound = `<!DOCTYPE html><html>
 <body>
 <body>
 <h3>User Not Found.</h3>
 <h3>User Not Found.</h3>
 </body>
 </body>
 </html>`
 </html>`
 
 
+const somethingwentwrong = `<!DOCTYPE html><html>
+<body>
+<h3>Something went wrong. Contact Admin.</h3>
+</body>
+</html>`
+
+const notallowedtosignup = `<!DOCTYPE html><html>
+<body>
+<h3>Your email is not allowed. Please contact your administrator.</h3>
+</body>
+</html>`
+
 func handleOauthUserNotFound(response http.ResponseWriter) {
 func handleOauthUserNotFound(response http.ResponseWriter) {
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")
 	response.WriteHeader(http.StatusNotFound)
 	response.WriteHeader(http.StatusNotFound)
@@ -34,6 +61,23 @@ func handleOauthUserNotAllowed(response http.ResponseWriter) {
 	response.WriteHeader(http.StatusForbidden)
 	response.WriteHeader(http.StatusForbidden)
 	response.Write([]byte(userNotAllowed))
 	response.Write([]byte(userNotAllowed))
 }
 }
+func handleFirstTimeOauthUserSignUp(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusForbidden)
+	response.Write([]byte(userFirstTimeSignUp))
+}
+
+func handleOauthUserSignUpApprovalPending(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusForbidden)
+	response.Write([]byte(userSignUpApprovalPending))
+}
+
+func handleOauthUserNotAllowedToSignUp(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusForbidden)
+	response.Write([]byte(notallowedtosignup))
+}
 
 
 // handleOauthNotConfigured - returns an appropriate html page when oauth is not configured on netmaker server but an oauth login was attempted
 // handleOauthNotConfigured - returns an appropriate html page when oauth is not configured on netmaker server but an oauth login was attempted
 func handleOauthNotConfigured(response http.ResponseWriter) {
 func handleOauthNotConfigured(response http.ResponseWriter) {
@@ -41,3 +85,9 @@ func handleOauthNotConfigured(response http.ResponseWriter) {
 	response.WriteHeader(http.StatusInternalServerError)
 	response.WriteHeader(http.StatusInternalServerError)
 	response.Write([]byte(oauthNotConfigured))
 	response.Write([]byte(oauthNotConfigured))
 }
 }
+
+func handleSomethingWentWrong(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusInternalServerError)
+	response.Write([]byte(somethingwentwrong))
+}

+ 24 - 3
auth/github.go

@@ -7,6 +7,7 @@ import (
 	"io"
 	"io"
 	"net/http"
 	"net/http"
 
 
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
@@ -60,9 +61,29 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
+	if !isEmailAllowed(content.Login) {
+		handleOauthUserNotAllowedToSignUp(w)
+		return
+	}
+	// check if user approval is already pending
+	if logic.IsPendingUser(content.Login) {
+		handleOauthUserSignUpApprovalPending(w)
+		return
+	}
 	_, err = logic.GetUser(content.Login)
 	_, err = logic.GetUser(content.Login)
-	if err != nil { // user must not exist, so try to make one
-		if err = addUser(content.Login); err != nil {
+	if err != nil {
+		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
+			err = logic.InsertPendingUser(&models.User{
+				UserName: content.Login,
+			})
+			if err != nil {
+				handleSomethingWentWrong(w)
+				return
+			}
+			handleFirstTimeOauthUserSignUp(w)
+			return
+		} else {
+			handleSomethingWentWrong(w)
 			return
 			return
 		}
 		}
 	}
 	}
@@ -75,7 +96,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		handleOauthUserNotAllowed(w)
 		return
 		return
 	}
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}

+ 24 - 3
auth/google.go

@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"net/http"
 	"time"
 	"time"
 
 
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
@@ -62,9 +63,29 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
+	if !isEmailAllowed(content.Email) {
+		handleOauthUserNotAllowedToSignUp(w)
+		return
+	}
+	// check if user approval is already pending
+	if logic.IsPendingUser(content.Email) {
+		handleOauthUserSignUpApprovalPending(w)
+		return
+	}
 	_, err = logic.GetUser(content.Email)
 	_, err = logic.GetUser(content.Email)
-	if err != nil { // user must not exists, so try to make one
-		if err = addUser(content.Email); err != nil {
+	if err != nil {
+		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
+			err = logic.InsertPendingUser(&models.User{
+				UserName: content.Email,
+			})
+			if err != nil {
+				handleSomethingWentWrong(w)
+				return
+			}
+			handleFirstTimeOauthUserSignUp(w)
+			return
+		} else {
+			handleSomethingWentWrong(w)
 			return
 			return
 		}
 		}
 	}
 	}
@@ -78,7 +99,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		handleOauthUserNotAllowed(w)
 		return
 		return
 	}
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}

+ 13 - 8
auth/headless_callback.go

@@ -50,19 +50,24 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	_, err = logic.GetUser(userClaims.getUserName())
-	if err != nil { // user must not exists, so try to make one
-		if err = addUser(userClaims.getUserName()); err != nil {
-			logger.Log(1, "could not create new user: ", userClaims.getUserName())
-			return
-		}
+	// check if user approval is already pending
+	if logic.IsPendingUser(userClaims.getUserName()) {
+		handleOauthUserNotAllowed(w)
+		return
+	}
+	user, err := logic.GetUser(userClaims.getUserName())
+	if err != nil {
+		response := returnErrTemplate("", "user not found", state, reqKeyIf)
+		w.WriteHeader(http.StatusForbidden)
+		w.Write(response)
+		return
 	}
 	}
-	newPass, fetchErr := fetchPassValue("")
+	newPass, fetchErr := FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}
 	jwt, jwtErr := logic.VerifyAuthRequest(models.UserAuthParams{
 	jwt, jwtErr := logic.VerifyAuthRequest(models.UserAuthParams{
-		UserName: userClaims.getUserName(),
+		UserName: user.UserName,
 		Password: newPass,
 		Password: newPass,
 	})
 	})
 	if jwtErr != nil {
 	if jwtErr != nil {

+ 24 - 3
auth/oidc.go

@@ -7,6 +7,7 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/coreos/go-oidc/v3/oidc"
 	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
@@ -73,9 +74,29 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		handleOauthNotConfigured(w)
 		return
 		return
 	}
 	}
+	if !isEmailAllowed(content.Email) {
+		handleOauthUserNotAllowedToSignUp(w)
+		return
+	}
+	// check if user approval is already pending
+	if logic.IsPendingUser(content.Email) {
+		handleOauthUserSignUpApprovalPending(w)
+		return
+	}
 	_, err = logic.GetUser(content.Email)
 	_, err = logic.GetUser(content.Email)
-	if err != nil { // user must not exists, so try to make one
-		if err = addUser(content.Email); err != nil {
+	if err != nil {
+		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
+			err = logic.InsertPendingUser(&models.User{
+				UserName: content.Email,
+			})
+			if err != nil {
+				handleSomethingWentWrong(w)
+				return
+			}
+			handleFirstTimeOauthUserSignUp(w)
+			return
+		} else {
+			handleSomethingWentWrong(w)
 			return
 			return
 		}
 		}
 	}
 	}
@@ -88,7 +109,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotAllowed(w)
 		handleOauthUserNotAllowed(w)
 		return
 		return
 	}
 	}
-	var newPass, fetchErr = fetchPassValue("")
+	var newPass, fetchErr = FetchPassValue("")
 	if fetchErr != nil {
 	if fetchErr != nil {
 		return
 		return
 	}
 	}

+ 20 - 0
cli/cmd/failover/disable.go

@@ -0,0 +1,20 @@
+package failover
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var disableFailoverCmd = &cobra.Command{
+	Use:   "disable [NODE ID]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Disable failover for a given Node",
+	Long:  `Disable failover for a given Node`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.DisableNodeFailover(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(disableFailoverCmd)
+}

+ 20 - 0
cli/cmd/failover/enable.go

@@ -0,0 +1,20 @@
+package failover
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var enableFailoverCmd = &cobra.Command{
+	Use:   "enable [NODE ID]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Enable failover for a given Node",
+	Long:  `Enable failover for a given Node`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.EnableNodeFailover(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(enableFailoverCmd)
+}

+ 28 - 0
cli/cmd/failover/root.go

@@ -0,0 +1,28 @@
+package failover
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "failover",
+	Short: "Enable/Disable failover for a node associated with a network",
+	Long:  `Enable/Disable failover for a node associated with a network`,
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}

+ 5 - 4
cli/cmd/node/create_ingress.go

@@ -6,10 +6,11 @@ import (
 )
 )
 
 
 var nodeCreateIngressCmd = &cobra.Command{
 var nodeCreateIngressCmd = &cobra.Command{
-	Use:   "create_ingress [NETWORK NAME] [NODE ID]",
-	Args:  cobra.ExactArgs(2),
-	Short: "Turn a Node into a Ingress",
-	Long:  `Turn a Node into a Ingress`,
+	Use:     "create_remote_access_gateway [NETWORK NAME] [NODE ID]",
+	Args:    cobra.ExactArgs(2),
+	Short:   "Turn a Node into a Remote Access Gateway (Ingress)",
+	Long:    `Turn a Node into a Remote Access Gateway (Ingress) for a Network.`,
+	Aliases: []string{"create_rag"},
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.CreateIngress(args[0], args[1], failover))
 		functions.PrettyPrint(functions.CreateIngress(args[0], args[1], failover))
 	},
 	},

+ 5 - 4
cli/cmd/node/delete_ingress.go

@@ -6,10 +6,11 @@ import (
 )
 )
 
 
 var nodeDeleteIngressCmd = &cobra.Command{
 var nodeDeleteIngressCmd = &cobra.Command{
-	Use:   "delete_ingress [NETWORK NAME] [NODE ID]",
-	Args:  cobra.ExactArgs(2),
-	Short: "Delete Ingress role from a Node",
-	Long:  `Delete Ingress role from a Node`,
+	Use:     "delete_remote_access_gateway [NETWORK NAME] [NODE ID]",
+	Args:    cobra.ExactArgs(2),
+	Short:   "Delete Remote Access Gateway role from a Node",
+	Long:    `Delete Remote Access Gateway role from a Node`,
+	Aliases: []string{"delete_rag"},
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.DeleteIngress(args[0], args[1]))
 		functions.PrettyPrint(functions.DeleteIngress(args[0], args[1]))
 	},
 	},

+ 1 - 1
cli/cmd/node/list.go

@@ -29,7 +29,7 @@ var nodeListCmd = &cobra.Command{
 			functions.PrettyPrint(data)
 			functions.PrettyPrint(data)
 		default:
 		default:
 			table := tablewriter.NewWriter(os.Stdout)
 			table := tablewriter.NewWriter(os.Stdout)
-			table.SetHeader([]string{"ID", "Addresses", "Network", "Egress", "Ingress", "Relay"})
+			table.SetHeader([]string{"ID", "Addresses", "Network", "Egress", "Remote Access Gateway", "Relay"})
 			for _, d := range data {
 			for _, d := range data {
 				addresses := ""
 				addresses := ""
 				if d.Address != "" {
 				if d.Address != "" {

+ 2 - 0
cli/cmd/root.go

@@ -9,6 +9,7 @@ import (
 	"github.com/gravitl/netmaker/cli/cmd/dns"
 	"github.com/gravitl/netmaker/cli/cmd/dns"
 	"github.com/gravitl/netmaker/cli/cmd/enrollment_key"
 	"github.com/gravitl/netmaker/cli/cmd/enrollment_key"
 	"github.com/gravitl/netmaker/cli/cmd/ext_client"
 	"github.com/gravitl/netmaker/cli/cmd/ext_client"
+	"github.com/gravitl/netmaker/cli/cmd/failover"
 	"github.com/gravitl/netmaker/cli/cmd/host"
 	"github.com/gravitl/netmaker/cli/cmd/host"
 	"github.com/gravitl/netmaker/cli/cmd/metrics"
 	"github.com/gravitl/netmaker/cli/cmd/metrics"
 	"github.com/gravitl/netmaker/cli/cmd/network"
 	"github.com/gravitl/netmaker/cli/cmd/network"
@@ -53,4 +54,5 @@ func init() {
 	rootCmd.AddCommand(metrics.GetRoot())
 	rootCmd.AddCommand(metrics.GetRoot())
 	rootCmd.AddCommand(host.GetRoot())
 	rootCmd.AddCommand(host.GetRoot())
 	rootCmd.AddCommand(enrollment_key.GetRoot())
 	rootCmd.AddCommand(enrollment_key.GetRoot())
+	rootCmd.AddCommand(failover.GetRoot())
 }
 }

+ 1 - 1
cli/functions/acl.go

@@ -14,5 +14,5 @@ func GetACL(networkName string) *acls.ACLContainer {
 
 
 // UpdateACL - update an ACL
 // UpdateACL - update an ACL
 func UpdateACL(networkName string, payload *acls.ACLContainer) *acls.ACLContainer {
 func UpdateACL(networkName string, payload *acls.ACLContainer) *acls.ACLContainer {
-	return request[acls.ACLContainer](http.MethodPut, fmt.Sprintf("/api/networks/%s/acls", networkName), payload)
+	return request[acls.ACLContainer](http.MethodPut, fmt.Sprintf("/api/networks/%s/acls/v2", networkName), payload)
 }
 }

+ 18 - 0
cli/functions/failover.go

@@ -0,0 +1,18 @@
+package functions
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/gravitl/netmaker/models"
+)
+
+// EnableNodeFailover - Enable failover for a given Node
+func EnableNodeFailover(nodeID string) *models.SuccessResponse {
+	return request[models.SuccessResponse](http.MethodPost, fmt.Sprintf("/api/v1/node/%s/failover", nodeID), nil)
+}
+
+// DisableNodeFailover - Disable failover for a given Node
+func DisableNodeFailover(nodeID string) *models.SuccessResponse {
+	return request[models.SuccessResponse](http.MethodDelete, fmt.Sprintf("/api/v1/node/%s/failover", nodeID), nil)
+}

+ 1 - 1
compose/docker-compose.netclient.yml

@@ -3,7 +3,7 @@ version: "3.4"
 services:
 services:
   netclient:
   netclient:
     container_name: netclient
     container_name: netclient
-    image: 'gravitl/netclient:v0.23.0'
+    image: 'gravitl/netclient:v0.23.1'
     hostname: netmaker-1
     hostname: netmaker-1
     network_mode: host
     network_mode: host
     restart: on-failure
     restart: on-failure

+ 2 - 0
config/config.go

@@ -92,6 +92,8 @@ type ServerConfig struct {
 	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration"`
 	JwtValidityDuration        time.Duration `yaml:"jwt_validity_duration"`
 	RacAutoDisable             bool          `yaml:"rac_auto_disable"`
 	RacAutoDisable             bool          `yaml:"rac_auto_disable"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
 	CacheEnabled               string        `yaml:"caching_enabled"`
+	EndpointDetection          bool          `json:"endpoint_detection"`
+	AllowedEmailDomains        string        `yaml:"allowed_email_domains"`
 }
 }
 
 
 // SQLConfig - Generic SQL Config
 // SQLConfig - Generic SQL Config

+ 32 - 4
controllers/docs.go

@@ -10,7 +10,7 @@
 //
 //
 //	Schemes: https
 //	Schemes: https
 //	BasePath: /
 //	BasePath: /
-//	Version: 0.23.0
+//	Version: 0.23.1
 //	Host: api.demo.netmaker.io
 //	Host: api.demo.netmaker.io
 //
 //
 //	Consumes:
 //	Consumes:
@@ -49,6 +49,12 @@ type hasAdmin struct {
 	Admin bool
 	Admin bool
 }
 }
 
 
+// swagger:response apiHostSliceResponse
+type apiHostSliceResponse struct {
+	// in: body
+	Host []models.ApiHost
+}
+
 // swagger:response apiHostResponse
 // swagger:response apiHostResponse
 type apiHostResponse struct {
 type apiHostResponse struct {
 	// in: body
 	// in: body
@@ -251,7 +257,7 @@ type networkBodyResponse struct {
 	Network models.Network `json:"network"`
 	Network models.Network `json:"network"`
 }
 }
 
 
-// swagger:parameters updateNetworkACL getNetworkACL
+// swagger:parameters updateNetworkACL
 type aclContainerBodyParam struct {
 type aclContainerBodyParam struct {
 	// ACL Container
 	// ACL Container
 	// in: body
 	// in: body
@@ -269,7 +275,7 @@ type aclContainerResponse struct {
 type nodeSliceResponse struct {
 type nodeSliceResponse struct {
 	// Nodes
 	// Nodes
 	// in: body
 	// in: body
-	Nodes []models.LegacyNode `json:"nodes"`
+	Nodes []models.ApiNode `json:"nodes"`
 }
 }
 
 
 // swagger:response nodeResponse
 // swagger:response nodeResponse
@@ -348,8 +354,26 @@ type HostFromNetworkParams struct {
 	Network string `json:"network"`
 	Network string `json:"network"`
 }
 }
 
 
+// swagger:parameters createEnrollmentKey
+type createEnrollmentKeyParams struct {
+	// APIEnrollmentKey
+	// in: body
+	Body models.APIEnrollmentKey `json:"body"`
+}
+
+// swagger:parameters updateEnrollmentKey
+type updateEnrollmentKeyParams struct {
+	// KeyID
+	// in: path
+	KeyID string `json:"keyid"`
+
+	// APIEnrollmentKey
+	// in: body
+	Body models.APIEnrollmentKey `json:"body"`
+}
+
 // swagger:parameters deleteEnrollmentKey
 // swagger:parameters deleteEnrollmentKey
-type DeleteEnrollmentKeyParam struct {
+type deleteEnrollmentKeyParam struct {
 	// in: path
 	// in: path
 	KeyID string `json:"keyid"`
 	KeyID string `json:"keyid"`
 }
 }
@@ -435,6 +459,7 @@ func useUnused() bool {
 	_ = userAuthBodyParam{}
 	_ = userAuthBodyParam{}
 	_ = usernamePathParam{}
 	_ = usernamePathParam{}
 	_ = hasAdmin{}
 	_ = hasAdmin{}
+	_ = apiHostSliceResponse{}
 	_ = apiHostResponse{}
 	_ = apiHostResponse{}
 	_ = fileResponse{}
 	_ = fileResponse{}
 	_ = extClientConfParams{}
 	_ = extClientConfParams{}
@@ -443,5 +468,8 @@ func useUnused() bool {
 	_ = signal{}
 	_ = signal{}
 	_ = filenameToGet{}
 	_ = filenameToGet{}
 	_ = dnsNetworkPathParam{}
 	_ = dnsNetworkPathParam{}
+	_ = createEnrollmentKeyParams{}
+	_ = updateEnrollmentKeyParams{}
+	_ = deleteEnrollmentKeyParam{}
 	return false
 	return false
 }
 }

+ 1 - 1
controllers/enrollmentkeys.go

@@ -180,7 +180,7 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(newEnrollmentKey)
 	json.NewEncoder(w).Encode(newEnrollmentKey)
 }
 }
 
 
-// swagger:route PUT /api/v1/enrollment-keys/:id enrollmentKeys updateEnrollmentKey
+// swagger:route PUT /api/v1/enrollment-keys/{keyid} enrollmentKeys updateEnrollmentKey
 //
 //
 // Updates an EnrollmentKey for hosts to use on Netmaker server. Updates only the relay to use.
 // Updates an EnrollmentKey for hosts to use on Netmaker server. Updates only the relay to use.
 //
 //

+ 45 - 12
controllers/hosts.go

@@ -32,6 +32,7 @@ func hostHandlers(r *mux.Router) {
 	r.HandleFunc("/api/v1/host", Authorize(true, false, "host", http.HandlerFunc(pull))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/host", Authorize(true, false, "host", http.HandlerFunc(pull))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/host/{hostid}/signalpeer", Authorize(true, false, "host", http.HandlerFunc(signalPeer))).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/host/{hostid}/signalpeer", Authorize(true, false, "host", http.HandlerFunc(signalPeer))).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/fallback/host/{hostid}", Authorize(true, false, "host", http.HandlerFunc(hostUpdateFallback))).Methods(http.MethodPut)
 	r.HandleFunc("/api/v1/fallback/host/{hostid}", Authorize(true, false, "host", http.HandlerFunc(hostUpdateFallback))).Methods(http.MethodPut)
+	r.HandleFunc("/api/emqx/hosts", logic.SecurityCheck(true, http.HandlerFunc(delEmqxHosts))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/v1/auth-register/host", socketHandler)
 	r.HandleFunc("/api/v1/auth-register/host", socketHandler)
 }
 }
 
 
@@ -61,7 +62,7 @@ func upgradeHost(w http.ResponseWriter, r *http.Request) {
 //	  		oauth
 //	  		oauth
 //
 //
 //			Responses:
 //			Responses:
-//				200: apiHostResponse
+//				200: apiHostSliceResponse
 func getHosts(w http.ResponseWriter, r *http.Request) {
 func getHosts(w http.ResponseWriter, r *http.Request) {
 	currentHosts, err := logic.GetAllHosts()
 	currentHosts, err := logic.GetAllHosts()
 	if err != nil {
 	if err != nil {
@@ -133,17 +134,18 @@ func pull(w http.ResponseWriter, r *http.Request) {
 
 
 	serverConf.TrafficKey = key
 	serverConf.TrafficKey = key
 	response := models.HostPull{
 	response := models.HostPull{
-		Host:            *host,
-		Nodes:           logic.GetHostNodes(host),
-		ServerConfig:    serverConf,
-		Peers:           hPU.Peers,
-		PeerIDs:         hPU.PeerIDs,
-		HostNetworkInfo: hPU.HostNetworkInfo,
-		EgressRoutes:    hPU.EgressRoutes,
-		FwUpdate:        hPU.FwUpdate,
-		ChangeDefaultGw: hPU.ChangeDefaultGw,
-		DefaultGwIp:     hPU.DefaultGwIp,
-		IsInternetGw:    hPU.IsInternetGw,
+		Host:              *host,
+		Nodes:             logic.GetHostNodes(host),
+		ServerConfig:      serverConf,
+		Peers:             hPU.Peers,
+		PeerIDs:           hPU.PeerIDs,
+		HostNetworkInfo:   hPU.HostNetworkInfo,
+		EgressRoutes:      hPU.EgressRoutes,
+		FwUpdate:          hPU.FwUpdate,
+		ChangeDefaultGw:   hPU.ChangeDefaultGw,
+		DefaultGwIp:       hPU.DefaultGwIp,
+		IsInternetGw:      hPU.IsInternetGw,
+		EndpointDetection: servercfg.IsEndpointDetectionEnabled(),
 	}
 	}
 
 
 	logger.Log(1, hostID, "completed a pull")
 	logger.Log(1, hostID, "completed a pull")
@@ -749,3 +751,34 @@ func syncHost(w http.ResponseWriter, r *http.Request) {
 	slog.Info("requested host pull", "user", r.Header.Get("user"), "host", host.ID)
 	slog.Info("requested host pull", "user", r.Header.Get("user"), "host", host.ID)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }
+
+// swagger:route DELETE /api/emqx/hosts hosts delEmqxHosts
+//
+// Lists all hosts.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: apiHostResponse
+func delEmqxHosts(w http.ResponseWriter, r *http.Request) {
+	currentHosts, err := logic.GetAllHosts()
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, host := range currentHosts {
+		// delete EMQX credentials for host
+		if err := mq.GetEmqxHandler().DeleteEmqxUser(host.ID.String()); err != nil {
+			slog.Error("failed to remove host credentials from EMQX", "id", host.ID, "error", err)
+		}
+	}
+	err = mq.GetEmqxHandler().DeleteEmqxUser(servercfg.GetMqUserName())
+	if err != nil {
+		slog.Error("failed to remove server credentials from EMQX", "user", servercfg.GetMqUserName(), "error", err)
+	}
+	logic.ReturnSuccessResponse(w, r, "deleted hosts data on emqx")
+}

+ 139 - 0
controllers/user.go

@@ -9,6 +9,7 @@ import (
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/websocket"
 	"github.com/gorilla/websocket"
 	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/auth"
+	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
@@ -35,6 +36,11 @@ func userHandlers(r *mux.Router) {
 	r.HandleFunc("/api/oauth/callback", auth.HandleAuthCallback).Methods(http.MethodGet)
 	r.HandleFunc("/api/oauth/callback", auth.HandleAuthCallback).Methods(http.MethodGet)
 	r.HandleFunc("/api/oauth/headless", auth.HandleHeadlessSSO)
 	r.HandleFunc("/api/oauth/headless", auth.HandleHeadlessSSO)
 	r.HandleFunc("/api/oauth/register/{regKey}", auth.RegisterHostSSO).Methods(http.MethodGet)
 	r.HandleFunc("/api/oauth/register/{regKey}", auth.RegisterHostSSO).Methods(http.MethodGet)
+	r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(getPendingUsers))).Methods(http.MethodGet)
+	r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(deleteAllPendingUsers))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(deletePendingUser))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(approvePendingUser))).Methods(http.MethodPost)
+
 }
 }
 
 
 // swagger:route POST /api/users/adm/authenticate authenticate authenticateUser
 // swagger:route POST /api/users/adm/authenticate authenticate authenticateUser
@@ -583,3 +589,136 @@ func socketHandler(w http.ResponseWriter, r *http.Request) {
 	// Start handling the session
 	// Start handling the session
 	go auth.SessionHandler(conn)
 	go auth.SessionHandler(conn)
 }
 }
+
+// swagger:route GET /api/users_pending user getPendingUsers
+//
+// Get all pending users.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func getPendingUsers(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+
+	users, err := logic.ListPendingUsers()
+	if err != nil {
+		logger.Log(0, "failed to fetch users: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	logic.SortUsers(users[:])
+	logger.Log(2, r.Header.Get("user"), "fetched pending users")
+	json.NewEncoder(w).Encode(users)
+}
+
+// swagger:route POST /api/users_pending/user/{username} user approvePendingUser
+//
+// approve pending user.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func approvePendingUser(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	username := params["username"]
+	users, err := logic.ListPendingUsers()
+
+	if err != nil {
+		logger.Log(0, "failed to fetch users: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, user := range users {
+		if user.UserName == username {
+			var newPass, fetchErr = auth.FetchPassValue("")
+			if fetchErr != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fetchErr, "internal"))
+				return
+			}
+			if err = logic.CreateUser(&models.User{
+				UserName: user.UserName,
+				Password: newPass,
+			}); err != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to create user: %s", err), "internal"))
+				return
+			}
+			err = logic.DeletePendingUser(username)
+			if err != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete pending user: %s", err), "internal"))
+				return
+			}
+			break
+		}
+	}
+	logic.ReturnSuccessResponse(w, r, "approved "+username)
+}
+
+// swagger:route DELETE /api/users_pending/user/{username} user deletePendingUser
+//
+// delete pending user.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func deletePendingUser(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	var params = mux.Vars(r)
+	username := params["username"]
+	users, err := logic.ListPendingUsers()
+
+	if err != nil {
+		logger.Log(0, "failed to fetch users: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	for _, user := range users {
+		if user.UserName == username {
+			err = logic.DeletePendingUser(username)
+			if err != nil {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete pending user: %s", err), "internal"))
+				return
+			}
+			break
+		}
+	}
+	logic.ReturnSuccessResponse(w, r, "deleted pending "+username)
+}
+
+// swagger:route DELETE /api/users_pending/{username}/pending user deleteAllPendingUsers
+//
+// delete all pending users.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func deleteAllPendingUsers(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	err := database.DeleteAllRecords(database.PENDING_USERS_TABLE_NAME)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to delete all pending users "+err.Error()), "internal"))
+		return
+	}
+	logic.ReturnSuccessResponse(w, r, "cleared all pending users")
+}

+ 3 - 1
database/database.go

@@ -61,7 +61,8 @@ const (
 	ENROLLMENT_KEYS_TABLE_NAME = "enrollmentkeys"
 	ENROLLMENT_KEYS_TABLE_NAME = "enrollmentkeys"
 	// HOST_ACTIONS_TABLE_NAME - table name for enrollmentkeys
 	// HOST_ACTIONS_TABLE_NAME - table name for enrollmentkeys
 	HOST_ACTIONS_TABLE_NAME = "hostactions"
 	HOST_ACTIONS_TABLE_NAME = "hostactions"
-
+	// PENDING_USERS_TABLE_NAME - table name for pending users
+	PENDING_USERS_TABLE_NAME = "pending_users"
 	// == ERROR CONSTS ==
 	// == ERROR CONSTS ==
 	// NO_RECORD - no singular result found
 	// NO_RECORD - no singular result found
 	NO_RECORD = "no result found"
 	NO_RECORD = "no result found"
@@ -144,6 +145,7 @@ func createTables() {
 	CreateTable(HOSTS_TABLE_NAME)
 	CreateTable(HOSTS_TABLE_NAME)
 	CreateTable(ENROLLMENT_KEYS_TABLE_NAME)
 	CreateTable(ENROLLMENT_KEYS_TABLE_NAME)
 	CreateTable(HOST_ACTIONS_TABLE_NAME)
 	CreateTable(HOST_ACTIONS_TABLE_NAME)
+	CreateTable(PENDING_USERS_TABLE_NAME)
 }
 }
 
 
 func CreateTable(tableName string) error {
 func CreateTable(tableName string) error {

+ 7 - 7
go.mod

@@ -4,7 +4,7 @@ go 1.19
 
 
 require (
 require (
 	github.com/eclipse/paho.mqtt.golang v1.4.3
 	github.com/eclipse/paho.mqtt.golang v1.4.3
-	github.com/go-playground/validator/v10 v10.18.0
+	github.com/go-playground/validator/v10 v10.19.0
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/golang-jwt/jwt/v4 v4.5.0
 	github.com/google/uuid v1.6.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/handlers v1.5.2
@@ -13,12 +13,12 @@ require (
 	github.com/mattn/go-sqlite3 v1.14.22
 	github.com/mattn/go-sqlite3 v1.14.22
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
-	github.com/stretchr/testify v1.8.4
+	github.com/stretchr/testify v1.9.0
 	github.com/txn2/txeh v1.5.5
 	github.com/txn2/txeh v1.5.5
-	golang.org/x/crypto v0.19.0
-	golang.org/x/net v0.21.0 // indirect
-	golang.org/x/oauth2 v0.17.0
-	golang.org/x/sys v0.17.0 // indirect
+	golang.org/x/crypto v0.21.0
+	golang.org/x/net v0.22.0 // indirect
+	golang.org/x/oauth2 v0.18.0
+	golang.org/x/sys v0.18.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	google.golang.org/protobuf v1.31.0 // indirect
 	google.golang.org/protobuf v1.31.0 // indirect
@@ -38,7 +38,7 @@ require (
 )
 )
 
 
 require (
 require (
-	github.com/go-jose/go-jose/v3 v3.0.1
+	github.com/go-jose/go-jose/v3 v3.0.3
 	github.com/guumaster/tablewriter v0.0.10
 	github.com/guumaster/tablewriter v0.0.10
 	github.com/matryer/is v1.4.1
 	github.com/matryer/is v1.4.1
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/olekukonko/tablewriter v0.0.5

+ 27 - 17
go.sum

@@ -20,24 +20,24 @@ github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBd
 github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
 github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
 github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
 github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
-github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
-github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
+github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
-github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
+github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
 github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
@@ -85,9 +85,9 @@ github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyh
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/txn2/txeh v1.5.5 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4=
 github.com/txn2/txeh v1.5.5 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4=
 github.com/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4=
 github.com/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4=
 github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
@@ -95,44 +95,54 @@ github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEAB
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
-golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
+golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=

+ 1 - 1
k8s/client/netclient-daemonset.yaml

@@ -16,7 +16,7 @@ spec:
       hostNetwork: true
       hostNetwork: true
       containers:
       containers:
       - name: netclient
       - name: netclient
-        image: gravitl/netclient:v0.23.0
+        image: gravitl/netclient:v0.23.1
         env:
         env:
         - name: TOKEN
         - name: TOKEN
           value: "TOKEN_VALUE"
           value: "TOKEN_VALUE"

+ 1 - 1
k8s/client/netclient.yaml

@@ -28,7 +28,7 @@ spec:
       #           - "<node label value>"
       #           - "<node label value>"
       containers:
       containers:
       - name: netclient
       - name: netclient
-        image: gravitl/netclient:v0.23.0
+        image: gravitl/netclient:v0.23.1
         env:
         env:
         - name: TOKEN
         - name: TOKEN
           value: "TOKEN_VALUE"
           value: "TOKEN_VALUE"

+ 1 - 1
k8s/server/netmaker-ui.yaml

@@ -15,7 +15,7 @@ spec:
     spec:
     spec:
       containers:
       containers:
       - name: netmaker-ui
       - name: netmaker-ui
-        image: gravitl/netmaker-ui:v0.23.0
+        image: gravitl/netmaker-ui:v0.23.1
         ports:
         ports:
         - containerPort: 443
         - containerPort: 443
         env:
         env:

+ 0 - 1
logic/jwts.go

@@ -106,7 +106,6 @@ func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin
 		if err != nil {
 		if err != nil {
 			return "", false, false, err
 			return "", false, false, err
 		}
 		}
-
 		if user.UserName != "" {
 		if user.UserName != "" {
 			return user.UserName, user.IsSuperAdmin, user.IsAdmin, nil
 			return user.UserName, user.IsSuperAdmin, user.IsAdmin, nil
 		}
 		}

+ 5 - 4
logic/peers.go

@@ -72,10 +72,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		FwUpdate: models.FwUpdate{
 		FwUpdate: models.FwUpdate{
 			EgressInfo: make(map[string]models.EgressInfo),
 			EgressInfo: make(map[string]models.EgressInfo),
 		},
 		},
-		PeerIDs:         make(models.PeerMap, 0),
-		Peers:           []wgtypes.PeerConfig{},
-		NodePeers:       []wgtypes.PeerConfig{},
-		HostNetworkInfo: models.HostInfoMap{},
+		PeerIDs:           make(models.PeerMap, 0),
+		Peers:             []wgtypes.PeerConfig{},
+		NodePeers:         []wgtypes.PeerConfig{},
+		HostNetworkInfo:   models.HostInfoMap{},
+		EndpointDetection: servercfg.IsEndpointDetectionEnabled(),
 	}
 	}
 
 
 	slog.Debug("peer update for host", "hostId", host.ID.String())
 	slog.Debug("peer update for host", "hostId", host.ID.String())

+ 7 - 15
logic/telemetry.go

@@ -39,10 +39,7 @@ func sendTelemetry() error {
 		return err
 		return err
 	}
 	}
 	// get telemetry data
 	// get telemetry data
-	d, err := FetchTelemetryData()
-	if err != nil {
-		slog.Error("error fetching telemetry data", "error", err)
-	}
+	d := FetchTelemetryData()
 	// get tenant admin email
 	// get tenant admin email
 	adminEmail := os.Getenv("NM_EMAIL")
 	adminEmail := os.Getenv("NM_EMAIL")
 	client, err := posthog.NewWithConfig(posthog_pub_key, posthog.Config{Endpoint: posthog_endpoint})
 	client, err := posthog.NewWithConfig(posthog_pub_key, posthog.Config{Endpoint: posthog_endpoint})
@@ -82,7 +79,7 @@ func sendTelemetry() error {
 }
 }
 
 
 // FetchTelemetryData - fetches telemetry data: count of various object types in DB
 // FetchTelemetryData - fetches telemetry data: count of various object types in DB
-func FetchTelemetryData() (telemetryData, error) {
+func FetchTelemetryData() telemetryData {
 	var data telemetryData
 	var data telemetryData
 
 
 	data.IsPro = servercfg.IsPro
 	data.IsPro = servercfg.IsPro
@@ -92,21 +89,16 @@ func FetchTelemetryData() (telemetryData, error) {
 	data.Hosts = getDBLength(database.HOSTS_TABLE_NAME)
 	data.Hosts = getDBLength(database.HOSTS_TABLE_NAME)
 	data.Version = servercfg.GetVersion()
 	data.Version = servercfg.GetVersion()
 	data.Servers = getServerCount()
 	data.Servers = getServerCount()
-	nodes, err := GetAllNodes()
-	if err == nil {
-		data.Nodes = len(nodes)
-		data.Count = getClientCount(nodes)
-	}
-	endDate, err := GetTrialEndDate()
-	if err != nil {
-		logger.Log(0, "error getting trial end date", err.Error())
-	}
+	nodes, _ := GetAllNodes()
+	data.Nodes = len(nodes)
+	data.Count = getClientCount(nodes)
+	endDate, _ := GetTrialEndDate()
 	data.ProTrialEndDate = endDate
 	data.ProTrialEndDate = endDate
 	if endDate.After(time.Now()) {
 	if endDate.After(time.Now()) {
 		data.IsProTrial = true
 		data.IsProTrial = true
 	}
 	}
 	data.IsSaasTenant = servercfg.DeployedByOperator()
 	data.IsSaasTenant = servercfg.DeployedByOperator()
-	return data, err
+	return data
 }
 }
 
 
 // getServerCount returns number of servers from database
 // getServerCount returns number of servers from database

+ 44 - 0
logic/users.go

@@ -75,3 +75,47 @@ func GetSuperAdmin() (models.ReturnUser, error) {
 	}
 	}
 	return models.ReturnUser{}, errors.New("superadmin not found")
 	return models.ReturnUser{}, errors.New("superadmin not found")
 }
 }
+
+func InsertPendingUser(u *models.User) error {
+	data, err := json.Marshal(u)
+	if err != nil {
+		return err
+	}
+	return database.Insert(u.UserName, string(data), database.PENDING_USERS_TABLE_NAME)
+}
+
+func DeletePendingUser(username string) error {
+	return database.DeleteRecord(database.PENDING_USERS_TABLE_NAME, username)
+}
+
+func IsPendingUser(username string) bool {
+	records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
+	if err != nil {
+		return false
+
+	}
+	for _, record := range records {
+		u := models.ReturnUser{}
+		err := json.Unmarshal([]byte(record), &u)
+		if err == nil && u.UserName == username {
+			return true
+		}
+	}
+	return false
+}
+
+func ListPendingUsers() ([]models.ReturnUser, error) {
+	pendingUsers := []models.ReturnUser{}
+	records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return pendingUsers, err
+	}
+	for _, record := range records {
+		u := models.ReturnUser{}
+		err = json.Unmarshal([]byte(record), &u)
+		if err == nil {
+			pendingUsers = append(pendingUsers, u)
+		}
+	}
+	return pendingUsers, nil
+}

+ 1 - 1
main.go

@@ -28,7 +28,7 @@ import (
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 )
 )
 
 
-var version = "v0.23.0"
+var version = "v0.23.1"
 
 
 // Start DB Connection and start API Request Handler
 // Start DB Connection and start API Request Handler
 func main() {
 func main() {

+ 1 - 1
migrate/migrate.go

@@ -177,7 +177,7 @@ func removeInterGw(egressRanges []string) ([]string, bool) {
 func updateAcls() {
 func updateAcls() {
 	// get all networks
 	// get all networks
 	networks, err := logic.GetNetworks()
 	networks, err := logic.GetNetworks()
-	if err != nil {
+	if err != nil && !database.IsEmptyRecord(err) {
 		slog.Error("acls migration failed. error getting networks", "error", err)
 		slog.Error("acls migration failed. error getting networks", "error", err)
 		return
 		return
 	}
 	}

+ 33 - 23
models/api_host.go

@@ -8,27 +8,34 @@ import (
 
 
 // ApiHost - the host struct for API usage
 // ApiHost - the host struct for API usage
 type ApiHost struct {
 type ApiHost struct {
-	ID                  string   `json:"id"`
-	Verbosity           int      `json:"verbosity"`
-	FirewallInUse       string   `json:"firewallinuse"`
-	Version             string   `json:"version"`
-	Name                string   `json:"name"`
-	OS                  string   `json:"os"`
-	Debug               bool     `json:"debug"`
-	IsStatic            bool     `json:"isstatic"`
-	ListenPort          int      `json:"listenport"`
-	WgPublicListenPort  int      `json:"wg_public_listen_port" yaml:"wg_public_listen_port"`
-	MTU                 int      `json:"mtu"                   yaml:"mtu"`
-	Interfaces          []Iface  `json:"interfaces"            yaml:"interfaces"`
-	DefaultInterface    string   `json:"defaultinterface"      yaml:"defautlinterface"`
-	EndpointIP          string   `json:"endpointip"            yaml:"endpointip"`
-	PublicKey           string   `json:"publickey"`
-	MacAddress          string   `json:"macaddress"`
-	Nodes               []string `json:"nodes"`
-	IsDefault           bool     `json:"isdefault"             yaml:"isdefault"`
-	NatType             string   `json:"nat_type"              yaml:"nat_type"`
-	PersistentKeepalive int      `json:"persistentkeepalive"   yaml:"persistentkeepalive"`
-	AutoUpdate          bool     `json:"autoupdate"              yaml:"autoupdate"`
+	ID                  string     `json:"id"`
+	Verbosity           int        `json:"verbosity"`
+	FirewallInUse       string     `json:"firewallinuse"`
+	Version             string     `json:"version"`
+	Name                string     `json:"name"`
+	OS                  string     `json:"os"`
+	Debug               bool       `json:"debug"`
+	IsStatic            bool       `json:"isstatic"`
+	ListenPort          int        `json:"listenport"`
+	WgPublicListenPort  int        `json:"wg_public_listen_port" yaml:"wg_public_listen_port"`
+	MTU                 int        `json:"mtu"                   yaml:"mtu"`
+	Interfaces          []ApiIface `json:"interfaces"            yaml:"interfaces"`
+	DefaultInterface    string     `json:"defaultinterface"      yaml:"defautlinterface"`
+	EndpointIP          string     `json:"endpointip"            yaml:"endpointip"`
+	PublicKey           string     `json:"publickey"`
+	MacAddress          string     `json:"macaddress"`
+	Nodes               []string   `json:"nodes"`
+	IsDefault           bool       `json:"isdefault"             yaml:"isdefault"`
+	NatType             string     `json:"nat_type"              yaml:"nat_type"`
+	PersistentKeepalive int        `json:"persistentkeepalive"   yaml:"persistentkeepalive"`
+	AutoUpdate          bool       `json:"autoupdate"              yaml:"autoupdate"`
+}
+
+// ApiIface - the interface struct for API usage
+// The original Iface struct contains a net.Address, which does not get marshalled correctly
+type ApiIface struct {
+	Name          string `json:"name"`
+	AddressString string `json:"addressString"`
 }
 }
 
 
 // Host.ConvertNMHostToAPI - converts a Netmaker host to an API editable host
 // Host.ConvertNMHostToAPI - converts a Netmaker host to an API editable host
@@ -38,9 +45,12 @@ func (h *Host) ConvertNMHostToAPI() *ApiHost {
 	a.EndpointIP = h.EndpointIP.String()
 	a.EndpointIP = h.EndpointIP.String()
 	a.FirewallInUse = h.FirewallInUse
 	a.FirewallInUse = h.FirewallInUse
 	a.ID = h.ID.String()
 	a.ID = h.ID.String()
-	a.Interfaces = h.Interfaces
+	a.Interfaces = make([]ApiIface, len(h.Interfaces))
 	for i := range a.Interfaces {
 	for i := range a.Interfaces {
-		a.Interfaces[i].AddressString = a.Interfaces[i].Address.String()
+		a.Interfaces[i] = ApiIface{
+			Name:          h.Interfaces[i].Name,
+			AddressString: h.Interfaces[i].Address.String(),
+		}
 	}
 	}
 	a.DefaultInterface = h.DefaultInterface
 	a.DefaultInterface = h.DefaultInterface
 	a.IsStatic = h.IsStatic
 	a.IsStatic = h.IsStatic

+ 16 - 15
models/mqtt.go

@@ -8,21 +8,22 @@ import (
 
 
 // HostPeerUpdate - struct for host peer updates
 // HostPeerUpdate - struct for host peer updates
 type HostPeerUpdate struct {
 type HostPeerUpdate struct {
-	Host            Host                 `json:"host" bson:"host" yaml:"host"`
-	ChangeDefaultGw bool                 `json:"change_default_gw"`
-	DefaultGwIp     net.IP               `json:"default_gw_ip"`
-	IsInternetGw    bool                 `json:"is_inet_gw"`
-	NodeAddrs       []net.IPNet          `json:"nodes_addrs" yaml:"nodes_addrs"`
-	Server          string               `json:"server" bson:"server" yaml:"server"`
-	ServerVersion   string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
-	ServerAddrs     []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
-	NodePeers       []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
-	Peers           []wgtypes.PeerConfig
-	PeerIDs         PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
-	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty" bson:"host_network_info,omitempty" yaml:"host_network_info,omitempty"`
-	EgressRoutes    []EgressNetworkRoutes `json:"egress_network_routes"`
-	FwUpdate        FwUpdate              `json:"fw_update"`
-	ReplacePeers    bool                  `json:"replace_peers"`
+	Host              Host                 `json:"host" bson:"host" yaml:"host"`
+	ChangeDefaultGw   bool                 `json:"change_default_gw"`
+	DefaultGwIp       net.IP               `json:"default_gw_ip"`
+	IsInternetGw      bool                 `json:"is_inet_gw"`
+	NodeAddrs         []net.IPNet          `json:"nodes_addrs" yaml:"nodes_addrs"`
+	Server            string               `json:"server" bson:"server" yaml:"server"`
+	ServerVersion     string               `json:"serverversion" bson:"serverversion" yaml:"serverversion"`
+	ServerAddrs       []ServerAddr         `json:"serveraddrs" bson:"serveraddrs" yaml:"serveraddrs"`
+	NodePeers         []wgtypes.PeerConfig `json:"peers" bson:"peers" yaml:"peers"`
+	Peers             []wgtypes.PeerConfig
+	PeerIDs           PeerMap               `json:"peerids" bson:"peerids" yaml:"peerids"`
+	HostNetworkInfo   HostInfoMap           `json:"host_network_info,omitempty" bson:"host_network_info,omitempty" yaml:"host_network_info,omitempty"`
+	EgressRoutes      []EgressNetworkRoutes `json:"egress_network_routes"`
+	FwUpdate          FwUpdate              `json:"fw_update"`
+	ReplacePeers      bool                  `json:"replace_peers"`
+	EndpointDetection bool                  `json:"endpoint_detection"`
 }
 }
 
 
 // IngressInfo - struct for ingress info
 // IngressInfo - struct for ingress info

+ 12 - 11
models/structs.go

@@ -232,17 +232,18 @@ type TrafficKeys struct {
 
 
 // HostPull - response of a host's pull
 // HostPull - response of a host's pull
 type HostPull struct {
 type HostPull struct {
-	Host            Host                  `json:"host" yaml:"host"`
-	Nodes           []Node                `json:"nodes" yaml:"nodes"`
-	Peers           []wgtypes.PeerConfig  `json:"peers" yaml:"peers"`
-	ServerConfig    ServerConfig          `json:"server_config" yaml:"server_config"`
-	PeerIDs         PeerMap               `json:"peer_ids,omitempty" yaml:"peer_ids,omitempty"`
-	HostNetworkInfo HostInfoMap           `json:"host_network_info,omitempty"  yaml:"host_network_info,omitempty"`
-	EgressRoutes    []EgressNetworkRoutes `json:"egress_network_routes"`
-	FwUpdate        FwUpdate              `json:"fw_update"`
-	ChangeDefaultGw bool                  `json:"change_default_gw"`
-	DefaultGwIp     net.IP                `json:"default_gw_ip"`
-	IsInternetGw    bool                  `json:"is_inet_gw"`
+	Host              Host                  `json:"host" yaml:"host"`
+	Nodes             []Node                `json:"nodes" yaml:"nodes"`
+	Peers             []wgtypes.PeerConfig  `json:"peers" yaml:"peers"`
+	ServerConfig      ServerConfig          `json:"server_config" yaml:"server_config"`
+	PeerIDs           PeerMap               `json:"peer_ids,omitempty" yaml:"peer_ids,omitempty"`
+	HostNetworkInfo   HostInfoMap           `json:"host_network_info,omitempty"  yaml:"host_network_info,omitempty"`
+	EgressRoutes      []EgressNetworkRoutes `json:"egress_network_routes"`
+	FwUpdate          FwUpdate              `json:"fw_update"`
+	ChangeDefaultGw   bool                  `json:"change_default_gw"`
+	DefaultGwIp       net.IP                `json:"default_gw_ip"`
+	IsInternetGw      bool                  `json:"is_inet_gw"`
+	EndpointDetection bool                  `json:"endpoint_detection"`
 }
 }
 
 
 type DefaultGwInfo struct {
 type DefaultGwInfo struct {

+ 2 - 1
mq/mq.go

@@ -33,8 +33,9 @@ func setMqOptions(user, password string, opts *mqtt.ClientOptions) {
 	opts.SetPassword(password)
 	opts.SetPassword(password)
 	opts.SetAutoReconnect(true)
 	opts.SetAutoReconnect(true)
 	opts.SetConnectRetry(true)
 	opts.SetConnectRetry(true)
-	opts.SetConnectRetryInterval(time.Second << 2)
+	opts.SetConnectRetryInterval(time.Second * 4)
 	opts.SetKeepAlive(time.Minute)
 	opts.SetKeepAlive(time.Minute)
+	opts.SetCleanSession(true)
 	opts.SetWriteTimeout(time.Minute)
 	opts.SetWriteTimeout(time.Minute)
 }
 }
 
 

+ 5 - 2
pro/controllers/metrics.go

@@ -2,9 +2,11 @@ package controllers
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
-	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"net/http"
 	"net/http"
 
 
+	proLogic "github.com/gravitl/netmaker/pro/logic"
+	"golang.org/x/exp/slog"
+
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
@@ -122,12 +124,13 @@ func getNetworkExtMetrics(w http.ResponseWriter, r *http.Request) {
 				continue
 				continue
 			}
 			}
 			// if metrics for that client have been reported, append them
 			// if metrics for that client have been reported, append them
-			if len(ingressMetrics.Connectivity[clients[j].ClientID].NodeName) > 0 {
+			if _, ok := ingressMetrics.Connectivity[clients[j].ClientID]; ok {
 				networkMetrics.Connectivity[clients[j].ClientID] = ingressMetrics.Connectivity[clients[j].ClientID]
 				networkMetrics.Connectivity[clients[j].ClientID] = ingressMetrics.Connectivity[clients[j].ClientID]
 			}
 			}
 		}
 		}
 	}
 	}
 
 
+	slog.Debug("sending collected client metrics", "metrics", networkMetrics.Connectivity)
 	logger.Log(1, r.Header.Get("user"), "fetched ext client metrics for network", network)
 	logger.Log(1, r.Header.Get("user"), "fetched ext client metrics for network", network)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(networkMetrics.Connectivity)
 	json.NewEncoder(w).Encode(networkMetrics.Connectivity)

+ 13 - 4
pro/controllers/middleware.go

@@ -1,16 +1,25 @@
 package controllers
 package controllers
 
 
 import (
 import (
+	"net/http"
+
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
-	"net/http"
 )
 )
 
 
+var limitedApis = map[string]struct{}{
+	"/api/server/status":          {},
+	"/api/emqx/hosts":             {},
+	"/api/users/adm/authenticate": {},
+}
+
 func OnlyServerAPIWhenUnlicensedMiddleware(handler http.Handler) http.Handler {
 func OnlyServerAPIWhenUnlicensedMiddleware(handler http.Handler) http.Handler {
 	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
 	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
-		if servercfg.ErrLicenseValidation != nil && request.URL.Path != "/api/server/status" {
-			logic.ReturnErrorResponse(writer, request, logic.FormatError(servercfg.ErrLicenseValidation, "forbidden"))
-			return
+		if servercfg.ErrLicenseValidation != nil {
+			if _, ok := limitedApis[request.URL.Path]; !ok {
+				logic.ReturnErrorResponse(writer, request, logic.FormatError(servercfg.ErrLicenseValidation, "forbidden"))
+				return
+			}
 		}
 		}
 		handler.ServeHTTP(writer, request)
 		handler.ServeHTTP(writer, request)
 	})
 	})

+ 8 - 19
pro/logic/metrics.go

@@ -104,7 +104,6 @@ func MQUpdateMetrics(client mqtt.Client, msg mqtt.Message) {
 }
 }
 
 
 func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) {
 func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) {
-
 	oldMetrics, err := logic.GetMetrics(currentNode.ID.String())
 	oldMetrics, err := logic.GetMetrics(currentNode.ID.String())
 	if err != nil {
 	if err != nil {
 		slog.Error("error finding old metrics for node", "id", currentNode.ID, "error", err)
 		slog.Error("error finding old metrics for node", "id", currentNode.ID, "error", err)
@@ -121,21 +120,13 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) {
 	if newMetrics.Connectivity == nil {
 	if newMetrics.Connectivity == nil {
 		newMetrics.Connectivity = make(map[string]models.Metric)
 		newMetrics.Connectivity = make(map[string]models.Metric)
 	}
 	}
-	if len(attachedClients) > 0 {
-		// associate ext clients with IDs
-		for i := range attachedClients {
-			extMetric := newMetrics.Connectivity[attachedClients[i].PublicKey]
-			if len(extMetric.NodeName) == 0 &&
-				len(newMetrics.Connectivity[attachedClients[i].ClientID].NodeName) > 0 { // cover server clients
-				extMetric = newMetrics.Connectivity[attachedClients[i].ClientID]
-				if extMetric.TotalReceived > 0 && extMetric.TotalSent > 0 {
-					extMetric.Connected = true
-				}
-			}
-			extMetric.NodeName = attachedClients[i].ClientID
-			delete(newMetrics.Connectivity, attachedClients[i].PublicKey)
-			newMetrics.Connectivity[attachedClients[i].ClientID] = extMetric
-		}
+	for i := range attachedClients {
+		slog.Debug("[metrics] processing attached client", "client", attachedClients[i].ClientID, "public key", attachedClients[i].PublicKey)
+		clientMetric := newMetrics.Connectivity[attachedClients[i].PublicKey]
+		clientMetric.NodeName = attachedClients[i].ClientID
+		newMetrics.Connectivity[attachedClients[i].ClientID] = clientMetric
+		delete(newMetrics.Connectivity, attachedClients[i].PublicKey)
+		slog.Debug("[metrics] attached client metric", "metric", clientMetric)
 	}
 	}
 
 
 	// run through metrics for each peer
 	// run through metrics for each peer
@@ -168,7 +159,5 @@ func updateNodeMetrics(currentNode *models.Node, newMetrics *models.Metrics) {
 
 
 	}
 	}
 
 
-	for k := range oldMetrics.Connectivity { // cleanup any left over data, self healing
-		delete(newMetrics.Connectivity, k)
-	}
+	slog.Debug("[metrics] node metrics data", "node ID", currentNode.ID, "metrics", newMetrics)
 }
 }

+ 1 - 1
pro/trial.go

@@ -42,7 +42,7 @@ const trial_data_key = "trialdata"
 
 
 // stores trial end date
 // stores trial end date
 func initTrial() error {
 func initTrial() error {
-	telData, _ := logic.FetchTelemetryData()
+	telData := logic.FetchTelemetryData()
 	if telData.Hosts > 0 || telData.Networks > 0 || telData.Users > 0 {
 	if telData.Hosts > 0 || telData.Networks > 0 || telData.Users > 0 {
 		return nil // database is already populated, so skip creating trial
 		return nil // database is already populated, so skip creating trial
 	}
 	}

+ 1 - 1
release.md

@@ -1,4 +1,4 @@
-# Netmaker v0.23.0
+# Netmaker v0.23.1
 
 
 ## Whats New ✨
 ## Whats New ✨
 
 

+ 4 - 0
scripts/netmaker.default.env

@@ -53,6 +53,8 @@ TELEMETRY=on
 # OAuth section
 # OAuth section
 #
 #
 ###
 ###
+# only mentioned domains will be allowded to signup using oauth, by default all domains are allowed
+ALLOWED_EMAIL_DOMAINS=*
 # "<azure-ad|github|google|oidc>"
 # "<azure-ad|github|google|oidc>"
 AUTH_PROVIDER=
 AUTH_PROVIDER=
 # "<client id of your oauth provider>"
 # "<client id of your oauth provider>"
@@ -71,3 +73,5 @@ JWT_VALIDITY_DURATION=43200
 RAC_AUTO_DISABLE=true
 RAC_AUTO_DISABLE=true
 # if turned on data will be cached on to improve performance significantly (IMPORTANT: If HA set to `false` )
 # if turned on data will be cached on to improve performance significantly (IMPORTANT: If HA set to `false` )
 CACHING_ENABLED=true
 CACHING_ENABLED=true
+# if turned on netclient checks if peers are reachable over private/LAN address, and choose that as peer endpoint
+ENDPOINT_DETECTION=true

+ 32 - 4
scripts/nm-quick.sh

@@ -248,8 +248,8 @@ save_config() { (
 	local toCopy=("SERVER_HOST" "MASTER_KEY" "MQ_USERNAME" "MQ_PASSWORD"
 	local toCopy=("SERVER_HOST" "MASTER_KEY" "MQ_USERNAME" "MQ_PASSWORD"
 		"INSTALL_TYPE" "NODE_ID" "DNS_MODE" "NETCLIENT_AUTO_UPDATE" "API_PORT"
 		"INSTALL_TYPE" "NODE_ID" "DNS_MODE" "NETCLIENT_AUTO_UPDATE" "API_PORT"
 		"CORS_ALLOWED_ORIGIN" "DISPLAY_KEYS" "DATABASE" "SERVER_BROKER_ENDPOINT" "VERBOSITY"
 		"CORS_ALLOWED_ORIGIN" "DISPLAY_KEYS" "DATABASE" "SERVER_BROKER_ENDPOINT" "VERBOSITY"
-		"DEBUG_MODE"  "REST_BACKEND" "DISABLE_REMOTE_IP_CHECK" "TELEMETRY" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET"
-		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE" "CACHING_ENABLED")
+		"DEBUG_MODE"  "REST_BACKEND" "DISABLE_REMOTE_IP_CHECK" "TELEMETRY" "ALLOWED_EMAIL_DOMAINS" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET"
+		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE" "CACHING_ENABLED" "ENDPOINT_DETECTION")
 	for name in "${toCopy[@]}"; do
 	for name in "${toCopy[@]}"; do
 		save_config_item $name "${!name}"
 		save_config_item $name "${!name}"
 	done
 	done
@@ -694,7 +694,12 @@ upgrade() {
 	unset IMAGE_TAG
 	unset IMAGE_TAG
 	unset BUILD_TAG
 	unset BUILD_TAG
 	IMAGE_TAG=$UI_IMAGE_TAG
 	IMAGE_TAG=$UI_IMAGE_TAG
-	BUILD_TAG=$UI_IMAGE_TAG
+	semver=$(chsv_check_version_ex "$UI_IMAGE_TAG")
+  	if [[ ! "$semver" ]]; then
+		BUILD_TAG=$LATEST
+	else
+		BUILD_TAG=$UI_IMAGE_TAG
+  	fi
 	echo "-----------------------------------------------------"
 	echo "-----------------------------------------------------"
 	echo "Provide Details for pro installation:"
 	echo "Provide Details for pro installation:"
 	echo "    1. Log into https://app.netmaker.io"
 	echo "    1. Log into https://app.netmaker.io"
@@ -720,7 +725,13 @@ downgrade () {
 	unset IMAGE_TAG
 	unset IMAGE_TAG
 	unset BUILD_TAG
 	unset BUILD_TAG
 	IMAGE_TAG=$UI_IMAGE_TAG
 	IMAGE_TAG=$UI_IMAGE_TAG
-	BUILD_TAG=$UI_IMAGE_TAG
+
+	semver=$(chsv_check_version_ex "$UI_IMAGE_TAG")
+  	if [[ ! "$semver" ]]; then
+		BUILD_TAG=$LATEST
+	else
+		BUILD_TAG=$UI_IMAGE_TAG
+  	fi
 	save_config
 	save_config
 	if [ -a "$SCRIPT_DIR"/docker-compose.override.yml ]; then
 	if [ -a "$SCRIPT_DIR"/docker-compose.override.yml ]; then
 		rm -f "$SCRIPT_DIR"/docker-compose.override.yml
 		rm -f "$SCRIPT_DIR"/docker-compose.override.yml
@@ -730,6 +741,23 @@ downgrade () {
 	install_netmaker
 	install_netmaker
 }
 }
 
 
+function chsv_check_version() {
+  if [[ $1 =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then
+    echo "$1"
+  else
+    echo ""
+  fi
+}
+
+function chsv_check_version_ex() {
+  if [[ $1 =~ ^v.+$ ]]; then
+    chsv_check_version "${1:1}"
+  else
+    chsv_check_version "${1}"
+  fi
+}
+
+
 
 
 main (){
 main (){
 
 

+ 23 - 1
servercfg/serverconf.go

@@ -441,7 +441,7 @@ func GetPublicIP() (string, error) {
 	endpoint := ""
 	endpoint := ""
 	var err error
 	var err error
 
 
-	iplist := []string{"https://ip.server.gravitl.com", "https://ifconfig.me", "https://api.ipify.org", "https://ipinfo.io/ip"}
+	iplist := []string{"https://ifconfig.me", "https://api.ipify.org", "https://ipinfo.io/ip"}
 	publicIpService := os.Getenv("PUBLIC_IP_SERVICE")
 	publicIpService := os.Getenv("PUBLIC_IP_SERVICE")
 	if publicIpService != "" {
 	if publicIpService != "" {
 		// prepend the user-specified service so it's checked first
 		// prepend the user-specified service so it's checked first
@@ -674,6 +674,17 @@ func DeployedByOperator() bool {
 	return config.Config.Server.DeployedByOperator
 	return config.Config.Server.DeployedByOperator
 }
 }
 
 
+// IsEndpointDetectionEnabled - returns true if endpoint detection enabled
+func IsEndpointDetectionEnabled() bool {
+	var enabled = true //default
+	if os.Getenv("ENDPOINT_DETECTION") != "" {
+		enabled = os.Getenv("ENDPOINT_DETECTION") == "true"
+	} else {
+		enabled = config.Config.Server.EndpointDetection
+	}
+	return enabled
+}
+
 // GetEnvironment returns the environment the server is running in (e.g. dev, staging, prod...)
 // GetEnvironment returns the environment the server is running in (e.g. dev, staging, prod...)
 func GetEnvironment() string {
 func GetEnvironment() string {
 	if env := os.Getenv("ENVIRONMENT"); env != "" {
 	if env := os.Getenv("ENVIRONMENT"); env != "" {
@@ -703,3 +714,14 @@ func GetEmqxAppID() string {
 func GetEmqxAppSecret() string {
 func GetEmqxAppSecret() string {
 	return os.Getenv("EMQX_APP_SECRET")
 	return os.Getenv("EMQX_APP_SECRET")
 }
 }
+
+// GetAllowedEmailDomains - gets the allowed email domains for oauth signup
+func GetAllowedEmailDomains() string {
+	allowedDomains := "*"
+	if os.Getenv("ALLOWED_EMAIL_DOMAINS") != "" {
+		allowedDomains = os.Getenv("ALLOWED_EMAIL_DOMAINS")
+	} else if config.Config.Server.AllowedEmailDomains != "" {
+		allowedDomains = config.Config.Server.AllowedEmailDomains
+	}
+	return allowedDomains
+}

+ 465 - 30
swagger.yml

@@ -15,9 +15,43 @@ definitions:
         description: ACLContainer - the total list of all node's ACL in a given network
         description: ACLContainer - the total list of all node's ACL in a given network
         type: object
         type: object
         x-go-package: github.com/gravitl/netmaker/logic/acls
         x-go-package: github.com/gravitl/netmaker/logic/acls
+    APIEnrollmentKey:
+        description: APIEnrollmentKey - used to create enrollment keys via API
+        properties:
+            expiration:
+                format: int64
+                type: integer
+                x-go-name: Expiration
+            networks:
+                items:
+                    type: string
+                type: array
+                x-go-name: Networks
+            relay:
+                type: string
+                x-go-name: Relay
+            tags:
+                items:
+                    type: string
+                type: array
+                x-go-name: Tags
+            type:
+                $ref: '#/definitions/KeyType'
+            unlimited:
+                type: boolean
+                x-go-name: Unlimited
+            uses_remaining:
+                format: int64
+                type: integer
+                x-go-name: UsesRemaining
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
     ApiHost:
     ApiHost:
         description: ApiHost - the host struct for API usage
         description: ApiHost - the host struct for API usage
         properties:
         properties:
+            autoupdate:
+                type: boolean
+                x-go-name: AutoUpdate
             debug:
             debug:
                 type: boolean
                 type: boolean
                 x-go-name: Debug
                 x-go-name: Debug
@@ -35,18 +69,12 @@ definitions:
                 x-go-name: ID
                 x-go-name: ID
             interfaces:
             interfaces:
                 items:
                 items:
-                    $ref: '#/definitions/Iface'
+                    $ref: '#/definitions/ApiIface'
                 type: array
                 type: array
                 x-go-name: Interfaces
                 x-go-name: Interfaces
             isdefault:
             isdefault:
                 type: boolean
                 type: boolean
                 x-go-name: IsDefault
                 x-go-name: IsDefault
-            isrelay:
-                type: boolean
-                x-go-name: IsRelay
-            isrelayed:
-                type: boolean
-                x-go-name: IsRelayed
             isstatic:
             isstatic:
                 type: boolean
                 type: boolean
                 x-go-name: IsStatic
                 x-go-name: IsStatic
@@ -75,17 +103,13 @@ definitions:
             os:
             os:
                 type: string
                 type: string
                 x-go-name: OS
                 x-go-name: OS
+            persistentkeepalive:
+                format: int64
+                type: integer
+                x-go-name: PersistentKeepalive
             publickey:
             publickey:
                 type: string
                 type: string
                 x-go-name: PublicKey
                 x-go-name: PublicKey
-            relay_hosts:
-                items:
-                    type: string
-                type: array
-                x-go-name: RelayedHosts
-            relayed_by:
-                type: string
-                x-go-name: RelayedBy
             verbosity:
             verbosity:
                 format: int64
                 format: int64
                 type: integer
                 type: integer
@@ -99,6 +123,139 @@ definitions:
                 x-go-name: WgPublicListenPort
                 x-go-name: WgPublicListenPort
         type: object
         type: object
         x-go-package: github.com/gravitl/netmaker/models
         x-go-package: github.com/gravitl/netmaker/models
+    ApiIface:
+        description: |-
+            ApiIface - the interface struct for API usage
+            The original Iface struct contains a net.Address, which does not get marshalled correctly
+        properties:
+            addressString:
+                type: string
+                x-go-name: AddressString
+            name:
+                type: string
+                x-go-name: Name
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
+    ApiNode:
+        description: ApiNode is a stripped down Node DTO that exposes only required fields to external systems
+        properties:
+            address:
+                type: string
+                x-go-name: Address
+            address6:
+                type: string
+                x-go-name: Address6
+            allowedips:
+                items:
+                    type: string
+                type: array
+                x-go-name: AllowedIPs
+            connected:
+                type: boolean
+                x-go-name: Connected
+            defaultacl:
+                description: == PRO ==
+                type: string
+                x-go-name: DefaultACL
+            dnson:
+                type: boolean
+                x-go-name: DNSOn
+            egressgatewaynatenabled:
+                type: boolean
+                x-go-name: EgressGatewayNatEnabled
+            egressgatewayranges:
+                items:
+                    type: string
+                type: array
+                x-go-name: EgressGatewayRanges
+            expdatetime:
+                format: int64
+                type: integer
+                x-go-name: ExpirationDateTime
+            fail_over_peers:
+                additionalProperties:
+                    type: object
+                type: object
+                x-go-name: FailOverPeers
+            failed_over_by:
+                format: uuid
+                type: string
+                x-go-name: FailedOverBy
+            hostid:
+                type: string
+                x-go-name: HostID
+            id:
+                type: string
+                x-go-name: ID
+            inet_node_req:
+                $ref: '#/definitions/InetNodeReq'
+            ingressdns:
+                type: string
+                x-go-name: IngressDns
+            internetgw_node_id:
+                type: string
+                x-go-name: InternetGwID
+            is_fail_over:
+                type: boolean
+                x-go-name: IsFailOver
+            isegressgateway:
+                type: boolean
+                x-go-name: IsEgressGateway
+            isingressgateway:
+                type: boolean
+                x-go-name: IsIngressGateway
+            isinternetgateway:
+                type: boolean
+                x-go-name: IsInternetGateway
+            isrelay:
+                type: boolean
+                x-go-name: IsRelay
+            isrelayed:
+                type: boolean
+                x-go-name: IsRelayed
+            lastcheckin:
+                format: int64
+                type: integer
+                x-go-name: LastCheckIn
+            lastmodified:
+                format: int64
+                type: integer
+                x-go-name: LastModified
+            lastpeerupdate:
+                format: int64
+                type: integer
+                x-go-name: LastPeerUpdate
+            localaddress:
+                type: string
+                x-go-name: LocalAddress
+            metadata:
+                type: string
+                x-go-name: Metadata
+            network:
+                type: string
+                x-go-name: Network
+            networkrange:
+                type: string
+                x-go-name: NetworkRange
+            networkrange6:
+                type: string
+                x-go-name: NetworkRange6
+            pendingdelete:
+                type: boolean
+                x-go-name: PendingDelete
+            relayedby:
+                type: string
+                x-go-name: RelayedBy
+            relaynodes:
+                items:
+                    type: string
+                type: array
+                x-go-name: RelayedNodes
+            server:
+                type: string
+                x-go-name: Server
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
     AuthParams:
     AuthParams:
         description: AuthParams - struct for auth params
         description: AuthParams - struct for auth params
         properties:
         properties:
@@ -135,6 +292,12 @@ definitions:
                     type: string
                     type: string
                 type: array
                 type: array
                 x-go-name: ExtraAllowedIPs
                 x-go-name: ExtraAllowedIPs
+            postdown:
+                type: string
+                x-go-name: PostDown
+            postup:
+                type: string
+                x-go-name: PostUp
             publickey:
             publickey:
                 type: string
                 type: string
                 x-go-name: PublicKey
                 x-go-name: PublicKey
@@ -187,6 +350,32 @@ definitions:
                 x-go-name: Ranges
                 x-go-name: Ranges
         type: object
         type: object
         x-go-package: github.com/gravitl/netmaker/models
         x-go-package: github.com/gravitl/netmaker/models
+    EgressInfo:
+        description: EgressInfo - struct for egress info
+        properties:
+            egress_gateway_cfg:
+                $ref: '#/definitions/EgressGatewayRequest'
+            egress_gw_addr:
+                $ref: '#/definitions/IPNet'
+            egress_id:
+                type: string
+                x-go-name: EgressID
+            network:
+                $ref: '#/definitions/IPNet'
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
+    EgressNetworkRoutes:
+        description: EgressNetworkRoutes - struct for egress network routes for adding routes to peer's interface
+        properties:
+            egress_ranges:
+                items:
+                    type: string
+                type: array
+                x-go-name: EgressRanges
+            node_addr:
+                $ref: '#/definitions/IPNet'
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
     EnrollmentKey:
     EnrollmentKey:
         description: EnrollmentKey - the key used to register hosts and join them to specific networks
         description: EnrollmentKey - the key used to register hosts and join them to specific networks
         properties:
         properties:
@@ -199,6 +388,10 @@ definitions:
                     type: string
                     type: string
                 type: array
                 type: array
                 x-go-name: Networks
                 x-go-name: Networks
+            relay:
+                format: uuid
+                type: string
+                x-go-name: Relay
             tags:
             tags:
                 items:
                 items:
                     type: string
                     type: string
@@ -230,6 +423,11 @@ definitions:
             address6:
             address6:
                 type: string
                 type: string
                 x-go-name: Address6
                 x-go-name: Address6
+            allowed_ips:
+                items:
+                    type: string
+                type: array
+                x-go-name: AllowedIPs
             clientid:
             clientid:
                 type: string
                 type: string
                 x-go-name: ClientID
                 x-go-name: ClientID
@@ -265,6 +463,12 @@ definitions:
             ownerid:
             ownerid:
                 type: string
                 type: string
                 x-go-name: OwnerID
                 x-go-name: OwnerID
+            postdown:
+                type: string
+                x-go-name: PostDown
+            postup:
+                type: string
+                x-go-name: PostUp
             privatekey:
             privatekey:
                 type: string
                 type: string
                 x-go-name: PrivateKey
                 x-go-name: PrivateKey
@@ -280,6 +484,19 @@ definitions:
         title: File represents an open file descriptor.
         title: File represents an open file descriptor.
         type: object
         type: object
         x-go-package: os
         x-go-package: os
+    FwUpdate:
+        description: FwUpdate - struct for firewall updates
+        properties:
+            egress_info:
+                additionalProperties:
+                    $ref: '#/definitions/EgressInfo'
+                type: object
+                x-go-name: EgressInfo
+            is_egress_gw:
+                type: boolean
+                x-go-name: IsEgressGw
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
     HardwareAddr:
     HardwareAddr:
         items:
         items:
             format: uint8
             format: uint8
@@ -362,6 +579,8 @@ definitions:
             os:
             os:
                 type: string
                 type: string
                 x-go-name: OS
                 x-go-name: OS
+            persistentkeepalive:
+                $ref: '#/definitions/Duration'
             publickey:
             publickey:
                 $ref: '#/definitions/Key'
                 $ref: '#/definitions/Key'
             traffickeypublic:
             traffickeypublic:
@@ -386,11 +605,52 @@ definitions:
                 x-go-name: WgPublicListenPort
                 x-go-name: WgPublicListenPort
         type: object
         type: object
         x-go-package: github.com/gravitl/netmaker/models
         x-go-package: github.com/gravitl/netmaker/models
+    HostInfoMap:
+        additionalProperties:
+            $ref: '#/definitions/HostNetworkInfo'
+        description: HostInfoMap - map of host public keys to host networking info
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
+    HostNetworkInfo:
+        description: HostNetworkInfo - holds info related to host networking (used for client side peer calculations)
+        properties:
+            interfaces:
+                items:
+                    $ref: '#/definitions/Iface'
+                type: array
+                x-go-name: Interfaces
+            is_static:
+                type: boolean
+                x-go-name: IsStatic
+            listen_port:
+                format: int64
+                type: integer
+                x-go-name: ListenPort
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
     HostPull:
     HostPull:
         description: HostPull - response of a host's pull
         description: HostPull - response of a host's pull
         properties:
         properties:
+            change_default_gw:
+                type: boolean
+                x-go-name: ChangeDefaultGw
+            default_gw_ip:
+                type: string
+                x-go-name: DefaultGwIp
+            egress_network_routes:
+                items:
+                    $ref: '#/definitions/EgressNetworkRoutes'
+                type: array
+                x-go-name: EgressRoutes
+            fw_update:
+                $ref: '#/definitions/FwUpdate'
             host:
             host:
                 $ref: '#/definitions/Host'
                 $ref: '#/definitions/Host'
+            host_network_info:
+                $ref: '#/definitions/HostInfoMap'
+            is_inet_gw:
+                type: boolean
+                x-go-name: IsInternetGw
             nodes:
             nodes:
                 items:
                 items:
                     $ref: '#/definitions/Node'
                     $ref: '#/definitions/Node'
@@ -413,6 +673,9 @@ definitions:
             address:
             address:
                 type: string
                 type: string
                 x-go-name: Address
                 x-go-name: Address
+            host_id:
+                type: string
+                x-go-name: HostID
             id:
             id:
                 type: string
                 type: string
                 x-go-name: ID
                 x-go-name: ID
@@ -466,6 +729,16 @@ definitions:
                 x-go-name: Name
                 x-go-name: Name
         type: object
         type: object
         x-go-package: github.com/gravitl/netmaker/models
         x-go-package: github.com/gravitl/netmaker/models
+    InetNodeReq:
+        description: InetNodeReq - exit node request struct
+        properties:
+            inet_node_client_ids:
+                items:
+                    type: string
+                type: array
+                x-go-name: InetNodeClientIDs
+        type: object
+        x-go-package: github.com/gravitl/netmaker/models
     Key:
     Key:
         description: |-
         description: |-
             A Key is a public, private, or pre-shared secret key.  The Key constructor
             A Key is a public, private, or pre-shared secret key.  The Key constructor
@@ -758,13 +1031,15 @@ definitions:
                 format: date-time
                 format: date-time
                 type: string
                 type: string
                 x-go-name: ExpirationDateTime
                 x-go-name: ExpirationDateTime
-            failover:
-                type: boolean
-                x-go-name: Failover
-            failovernode:
+            fail_over_peers:
+                additionalProperties:
+                    type: object
+                type: object
+                x-go-name: FailOverPeers
+            failed_over_by:
                 format: uuid
                 format: uuid
                 type: string
                 type: string
-                x-go-name: FailoverNode
+                x-go-name: FailedOverBy
             hostid:
             hostid:
                 format: uuid
                 format: uuid
                 type: string
                 type: string
@@ -773,6 +1048,8 @@ definitions:
                 format: uuid
                 format: uuid
                 type: string
                 type: string
                 x-go-name: ID
                 x-go-name: ID
+            inet_node_req:
+                $ref: '#/definitions/InetNodeReq'
             ingressdns:
             ingressdns:
                 type: string
                 type: string
                 x-go-name: IngressDNS
                 x-go-name: IngressDNS
@@ -782,14 +1059,21 @@ definitions:
             ingressgatewayrange6:
             ingressgatewayrange6:
                 type: string
                 type: string
                 x-go-name: IngressGatewayRange6
                 x-go-name: IngressGatewayRange6
-            internetgateway:
-                $ref: '#/definitions/UDPAddr'
+            internetgw_node_id:
+                type: string
+                x-go-name: InternetGwID
+            is_fail_over:
+                type: boolean
+                x-go-name: IsFailOver
             isegressgateway:
             isegressgateway:
                 type: boolean
                 type: boolean
                 x-go-name: IsEgressGateway
                 x-go-name: IsEgressGateway
             isingressgateway:
             isingressgateway:
                 type: boolean
                 type: boolean
                 x-go-name: IsIngressGateway
                 x-go-name: IsIngressGateway
+            isinternetgateway:
+                type: boolean
+                x-go-name: IsInternetGateway
             isrelay:
             isrelay:
                 type: boolean
                 type: boolean
                 x-go-name: IsRelay
                 x-go-name: IsRelay
@@ -810,6 +1094,9 @@ definitions:
                 x-go-name: LastPeerUpdate
                 x-go-name: LastPeerUpdate
             localaddress:
             localaddress:
                 $ref: '#/definitions/IPNet'
                 $ref: '#/definitions/IPNet'
+            metadata:
+                type: string
+                x-go-name: Metadata
             network:
             network:
                 type: string
                 type: string
                 x-go-name: Network
                 x-go-name: Network
@@ -823,8 +1110,6 @@ definitions:
             pendingdelete:
             pendingdelete:
                 type: boolean
                 type: boolean
                 x-go-name: PendingDelete
                 x-go-name: PendingDelete
-            persistentkeepalive:
-                $ref: '#/definitions/Duration'
             relayedby:
             relayedby:
                 type: string
                 type: string
                 x-go-name: RelayedBy
                 x-go-name: RelayedBy
@@ -930,6 +1215,8 @@ definitions:
                 type: string
                 type: string
             BrokerType:
             BrokerType:
                 type: string
                 type: string
+            CacheEnabled:
+                type: string
             ClientID:
             ClientID:
                 type: string
                 type: string
             ClientSecret:
             ClientSecret:
@@ -965,6 +1252,8 @@ definitions:
             IsEE:
             IsEE:
                 type: string
                 type: string
                 x-go-name: IsPro
                 x-go-name: IsPro
+            JwtValidityDuration:
+                $ref: '#/definitions/Duration'
             LicenseValue:
             LicenseValue:
                 type: string
                 type: string
             MQPassword:
             MQPassword:
@@ -997,6 +1286,8 @@ definitions:
                 type: string
                 type: string
             PublicIPService:
             PublicIPService:
                 type: string
                 type: string
+            RacAutoDisable:
+                type: boolean
             RestBackend:
             RestBackend:
                 type: string
                 type: string
             SQLConn:
             SQLConn:
@@ -1040,9 +1331,18 @@ definitions:
         properties:
         properties:
             action:
             action:
                 $ref: '#/definitions/SignalAction'
                 $ref: '#/definitions/SignalAction'
+            from_host_id:
+                type: string
+                x-go-name: FromHostID
             from_host_pubkey:
             from_host_pubkey:
                 type: string
                 type: string
                 x-go-name: FromHostPubKey
                 x-go-name: FromHostPubKey
+            from_node_id:
+                type: string
+                x-go-name: FromNodeID
+            is_pro:
+                type: boolean
+                x-go-name: IsPro
             reply:
             reply:
                 type: boolean
                 type: boolean
                 x-go-name: Reply
                 x-go-name: Reply
@@ -1053,12 +1353,15 @@ definitions:
                 format: int64
                 format: int64
                 type: integer
                 type: integer
                 x-go-name: TimeStamp
                 x-go-name: TimeStamp
+            to_host_id:
+                type: string
+                x-go-name: ToHostID
             to_host_pubkey:
             to_host_pubkey:
                 type: string
                 type: string
                 x-go-name: ToHostPubKey
                 x-go-name: ToHostPubKey
-            turn_relay_addr:
+            to_node_id:
                 type: string
                 type: string
-                x-go-name: TurnRelayEndpoint
+                x-go-name: ToNodeID
         type: object
         type: object
         x-go-package: github.com/gravitl/netmaker/models
         x-go-package: github.com/gravitl/netmaker/models
     SignalAction:
     SignalAction:
@@ -1114,6 +1417,10 @@ definitions:
             issuperadmin:
             issuperadmin:
                 type: boolean
                 type: boolean
                 x-go-name: IsSuperAdmin
                 x-go-name: IsSuperAdmin
+            last_login_time:
+                format: date-time
+                type: string
+                x-go-name: LastLoginTime
             password:
             password:
                 type: string
                 type: string
                 x-go-name: Password
                 x-go-name: Password
@@ -1149,7 +1456,7 @@ info:
 
 
         API calls must be authenticated via a header of the format -H “Authorization: Bearer <YOUR_SECRET_KEY>” There are two methods to obtain YOUR_SECRET_KEY: 1. Using the masterkey. By default, this value is “secret key,” but you should change this on your instance and keep it secure. This value can be set via env var at startup or in a config file (config/environments/< env >.yaml). See the [Netmaker](https://docs.netmaker.org/index.html) documentation for more details. 2. Using a JWT received for a node. This can be retrieved by calling the /api/nodes/<network>/authenticate endpoint, as documented below.
         API calls must be authenticated via a header of the format -H “Authorization: Bearer <YOUR_SECRET_KEY>” There are two methods to obtain YOUR_SECRET_KEY: 1. Using the masterkey. By default, this value is “secret key,” but you should change this on your instance and keep it secure. This value can be set via env var at startup or in a config file (config/environments/< env >.yaml). See the [Netmaker](https://docs.netmaker.org/index.html) documentation for more details. 2. Using a JWT received for a node. This can be retrieved by calling the /api/nodes/<network>/authenticate endpoint, as documented below.
     title: Netmaker
     title: Netmaker
-    version: 0.23.0
+    version: 0.23.1
 paths:
 paths:
     /api/dns:
     /api/dns:
         get:
         get:
@@ -1468,7 +1775,7 @@ paths:
             operationId: getHosts
             operationId: getHosts
             responses:
             responses:
                 "200":
                 "200":
-                    $ref: '#/responses/apiHostResponse'
+                    $ref: '#/responses/apiHostSliceResponse'
             schemes:
             schemes:
                 - https
                 - https
             summary: Lists all hosts.
             summary: Lists all hosts.
@@ -1721,6 +2028,23 @@ paths:
     /api/networks/{networkname}/acls:
     /api/networks/{networkname}/acls:
         get:
         get:
             operationId: getNetworkACL
             operationId: getNetworkACL
+            parameters:
+                - description: 'name: network name'
+                  in: path
+                  name: networkname
+                  required: true
+                  type: string
+                  x-go-name: Networkname
+            responses:
+                "200":
+                    $ref: '#/responses/aclContainerResponse'
+            schemes:
+                - https
+            summary: Get a network ACL (Access Control List).
+            tags:
+                - networks
+        put:
+            operationId: updateNetworkACL
             parameters:
             parameters:
                 - description: 'name: network name'
                 - description: 'name: network name'
                   in: path
                   in: path
@@ -1739,9 +2063,10 @@ paths:
                     $ref: '#/responses/aclContainerResponse'
                     $ref: '#/responses/aclContainerResponse'
             schemes:
             schemes:
                 - https
                 - https
-            summary: Get a network ACL (Access Control List).
+            summary: Update a network ACL (Access Control List).
             tags:
             tags:
                 - networks
                 - networks
+    /api/networks/{networkname}/acls/v2:
         put:
         put:
             operationId: updateNetworkACL
             operationId: updateNetworkACL
             parameters:
             parameters:
@@ -1765,6 +2090,17 @@ paths:
             summary: Update a network ACL (Access Control List).
             summary: Update a network ACL (Access Control List).
             tags:
             tags:
                 - networks
                 - networks
+    /api/node/{nodeid}/failOverME:
+        post:
+            operationId: failOver_me
+            responses:
+                "200":
+                    $ref: '#/responses/nodeResponse'
+            schemes:
+                - https
+            summary: Create a relay.
+            tags:
+                - node
     /api/nodes:
     /api/nodes:
         get:
         get:
             operationId: getAllNodes
             operationId: getAllNodes
@@ -2014,6 +2350,37 @@ paths:
             summary: Remove a relay.
             summary: Remove a relay.
             tags:
             tags:
                 - nodes
                 - nodes
+    /api/nodes/{network}/{nodeid}/inet_gw:
+        delete:
+            operationId: deleteInternetGw
+            responses:
+                "200":
+                    $ref: '#/responses/nodeResponse'
+            schemes:
+                - https
+            summary: Delete an internet gw.
+            tags:
+                - nodes
+        post:
+            operationId: createInternetGw
+            responses:
+                "200":
+                    $ref: '#/responses/nodeResponse'
+            schemes:
+                - https
+            summary: Create an inet node.
+            tags:
+                - nodes
+        put:
+            operationId: updateInternetGw
+            responses:
+                "200":
+                    $ref: '#/responses/nodeResponse'
+            schemes:
+                - https
+            summary: update an inet node.
+            tags:
+                - nodes
     /api/nodes/{network}/{nodeid}/ingress/users:
     /api/nodes/{network}/{nodeid}/ingress/users:
         get:
         get:
             operationId: ingressGatewayUsers
             operationId: ingressGatewayUsers
@@ -2300,6 +2667,13 @@ paths:
                 - enrollmentKeys
                 - enrollmentKeys
         post:
         post:
             operationId: createEnrollmentKey
             operationId: createEnrollmentKey
+            parameters:
+                - description: APIEnrollmentKey
+                  in: body
+                  name: body
+                  schema:
+                    $ref: '#/definitions/APIEnrollmentKey'
+                  x-go-name: Body
             responses:
             responses:
                 "200":
                 "200":
                     $ref: '#/responses/EnrollmentKey'
                     $ref: '#/responses/EnrollmentKey'
@@ -2325,6 +2699,29 @@ paths:
             summary: Deletes an EnrollmentKey from Netmaker server.
             summary: Deletes an EnrollmentKey from Netmaker server.
             tags:
             tags:
                 - enrollmentKeys
                 - enrollmentKeys
+        put:
+            operationId: updateEnrollmentKey
+            parameters:
+                - description: KeyID
+                  in: path
+                  name: keyid
+                  required: true
+                  type: string
+                  x-go-name: KeyID
+                - description: APIEnrollmentKey
+                  in: body
+                  name: body
+                  schema:
+                    $ref: '#/definitions/APIEnrollmentKey'
+                  x-go-name: Body
+            responses:
+                "200":
+                    $ref: '#/responses/EnrollmentKey'
+            schemes:
+                - https
+            summary: Updates an EnrollmentKey for hosts to use on Netmaker server. Updates only the relay to use.
+            tags:
+                - enrollmentKeys
     /api/v1/enrollment-keys/{token}:
     /api/v1/enrollment-keys/{token}:
         post:
         post:
             operationId: handleHostRegister
             operationId: handleHostRegister
@@ -2347,6 +2744,17 @@ paths:
             summary: Handles a Netclient registration with server and add nodes accordingly.
             summary: Handles a Netclient registration with server and add nodes accordingly.
             tags:
             tags:
                 - enrollmentKeys
                 - enrollmentKeys
+    /api/v1/fallback/host/{hostid}:
+        put:
+            operationId: hostUpdateFallback
+            responses:
+                "200":
+                    $ref: '#/responses/apiHostResponse'
+            schemes:
+                - https
+            summary: Updates a Netclient host on Netmaker server.
+            tags:
+                - hosts
     /api/v1/host:
     /api/v1/host:
         get:
         get:
             description: Used by clients for "pull" command
             description: Used by clients for "pull" command
@@ -2369,6 +2777,27 @@ paths:
             summary: Delete all legacy nodes from DB.
             summary: Delete all legacy nodes from DB.
             tags:
             tags:
                 - nodes
                 - nodes
+    /api/v1/node/failover:
+        delete:
+            operationId: deletefailOver
+            responses:
+                "200":
+                    $ref: '#/responses/nodeResponse'
+            schemes:
+                - https
+            summary: Create a relay.
+            tags:
+                - node
+        post:
+            operationId: createfailOver
+            responses:
+                "200":
+                    $ref: '#/responses/nodeResponse'
+            schemes:
+                - https
+            summary: Create a relay.
+            tags:
+                - node
     /api/v1/nodes/migrate:
     /api/v1/nodes/migrate:
         put:
         put:
             operationId: migrateData
             operationId: migrateData
@@ -2423,6 +2852,12 @@ responses:
         description: ""
         description: ""
         schema:
         schema:
             $ref: '#/definitions/ApiHost'
             $ref: '#/definitions/ApiHost'
+    apiHostSliceResponse:
+        description: ""
+        schema:
+            items:
+                $ref: '#/definitions/ApiHost'
+            type: array
     byteArrayResponse:
     byteArrayResponse:
         description: ""
         description: ""
         schema:
         schema:
@@ -2474,7 +2909,7 @@ responses:
         description: ""
         description: ""
         schema:
         schema:
             items:
             items:
-                $ref: '#/definitions/LegacyNode'
+                $ref: '#/definitions/ApiNode'
             type: array
             type: array
     okResponse:
     okResponse:
         description: ""
         description: ""