Browse Source

Merge pull request #3490 from gravitl/release-v0.99.0

v0.99.0
Abhishek K 3 months ago
parent
commit
8e08bf2585
100 changed files with 4445 additions and 1886 deletions
  1. 1 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 2 2
      .github/workflows/deletedroplets.yml
  3. 2 2
      Dockerfile
  4. 2 2
      Dockerfile-quick
  5. 1 1
      README.md
  6. 36 32
      auth/host_session.go
  7. 43 0
      cli/cmd/access_token/create.go
  8. 23 0
      cli/cmd/access_token/delete.go
  9. 20 0
      cli/cmd/access_token/get.go
  10. 28 0
      cli/cmd/access_token/root.go
  11. 7 4
      cli/cmd/context/set.go
  12. 3 3
      cli/cmd/host/add_network.go
  13. 3 3
      cli/cmd/host/delete.go
  14. 3 3
      cli/cmd/host/delete_network.go
  15. 2 2
      cli/cmd/host/list.go
  16. 4 4
      cli/cmd/host/refresh_keys.go
  17. 4 3
      cli/cmd/host/root.go
  18. 11 11
      cli/cmd/host/update.go
  19. 26 3
      cli/cmd/node/list.go
  20. 4 1
      cli/cmd/root.go
  21. 58 0
      cli/functions/access_tokens.go
  22. 1 1
      cli/functions/http_client.go
  23. 1 1
      compose/docker-compose.netclient.yml
  24. 0 1
      config/config.go
  25. 93 6
      controllers/acls.go
  26. 4 1
      controllers/controller.go
  27. 6 6
      controllers/dns.go
  28. 0 2
      controllers/dns_test.go
  29. 305 0
      controllers/egress.go
  30. 95 16
      controllers/enrollmentkeys.go
  31. 30 3
      controllers/ext_client.go
  32. 42 2
      controllers/gateway.go
  33. 145 9
      controllers/hosts.go
  34. 4 1
      controllers/middleware.go
  35. 5 6
      controllers/migrate.go
  36. 34 5
      controllers/network.go
  37. 23 11
      controllers/node.go
  38. 0 95
      controllers/node_test.go
  39. 91 2
      controllers/server.go
  40. 429 39
      controllers/user.go
  41. 37 90
      database/database.go
  42. 2 2
      database/postgres.go
  43. 2 2
      database/rqlite.go
  44. 2 2
      database/sqlite.go
  45. 0 59
      database/statics.go
  46. 11 0
      database/utils.go
  47. 41 0
      db/connector.go
  48. 112 0
      db/db.go
  49. 117 0
      db/postgres.go
  50. 55 0
      db/sqlite.go
  51. 1 1
      docker/Dockerfile-go-builder
  52. 1 1
      docker/Dockerfile-netclient-multiarch
  53. 2 2
      docker/Dockerfile-netclient-multiarch-userspace
  54. 51 19
      go.mod
  55. 125 80
      go.sum
  56. 1 1
      k8s/client/netclient-daemonset.yaml
  57. 1 1
      k8s/client/netclient.yaml
  58. 1 1
      k8s/server/netmaker-ui.yaml
  59. 541 532
      logic/acls.go
  60. 62 8
      logic/auth.go
  61. 1 2
      logic/dns.go
  62. 221 0
      logic/egress.go
  63. 2 1
      logic/enrollmentkey.go
  64. 13 13
      logic/enrollmentkey_test.go
  65. 16 6
      logic/errors.go
  66. 55 346
      logic/extpeers.go
  67. 10 12
      logic/gateway.go
  68. 27 4
      logic/hosts.go
  69. 59 14
      logic/jwts.go
  70. 1 2
      logic/networks.go
  71. 10 196
      logic/nodes.go
  72. 77 51
      logic/peers.go
  73. 17 1
      logic/relay.go
  74. 15 0
      logic/security.go
  75. 355 0
      logic/settings.go
  76. 8 2
      logic/telemetry.go
  77. 4 20
      logic/user_mgmt.go
  78. 28 9
      logic/users.go
  79. 21 0
      logic/util.go
  80. 0 14
      logic/wireguard.go
  81. 41 2
      logic/zombie.go
  82. 53 4
      main.go
  83. 130 38
      migrate/migrate.go
  84. 183 0
      migrate/migrate_schema.go
  85. 55 8
      models/accessToken.go
  86. 1 0
      models/acl.go
  87. 3 0
      models/api_host.go
  88. 9 13
      models/api_node.go
  89. 14 0
      models/egress.go
  90. 2 0
      models/enrollment_key.go
  91. 78 0
      models/events.go
  92. 1 0
      models/host.go
  93. 1 1
      models/mqtt.go
  94. 20 15
      models/node.go
  95. 47 0
      models/settings.go
  96. 2 0
      models/structs.go
  97. 33 14
      models/user_mgmt.go
  98. 1 1
      mq/publishers.go
  99. 48 8
      pro/auth/auth.go
  100. 27 5
      pro/auth/azure-ad.go

+ 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.99.0
         - v0.90.0
         - v0.90.0
         - v0.30.0
         - v0.30.0
         - v0.26.0
         - v0.26.0

+ 2 - 2
.github/workflows/deletedroplets.yml

@@ -12,7 +12,7 @@ jobs:
     if: ${{ github.event.workflow_run.conclusion == 'success' }}
     if: ${{ github.event.workflow_run.conclusion == 'success' }}
     steps:
     steps:
       - name: get logs
       - name: get logs
-        uses: dawidd6/action-download-artifact@v8
+        uses: dawidd6/action-download-artifact@v9
         with:
         with:
           run_id: ${{ github.event.workflow_run.id}}
           run_id: ${{ github.event.workflow_run.id}}
           if_no_artifact_found: warn
           if_no_artifact_found: warn
@@ -75,7 +75,7 @@ jobs:
     if: ${{ github.event.workflow_run.conclusion == 'failure' }}
     if: ${{ github.event.workflow_run.conclusion == 'failure' }}
     steps:
     steps:
       - name: get logs
       - name: get logs
-        uses: dawidd6/action-download-artifact@v8
+        uses: dawidd6/action-download-artifact@v9
         with:
         with:
           run_id: ${{ github.event.workflow_run.id}}
           run_id: ${{ github.event.workflow_run.id}}
           if_no_artifact_found: warn
           if_no_artifact_found: warn

+ 2 - 2
Dockerfile

@@ -6,12 +6,12 @@ COPY . .
 
 
 RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -w " -tags ${tags} .
 RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -w " -tags ${tags} .
 # RUN go build -tags=ee . -o netmaker main.go
 # RUN go build -tags=ee . -o netmaker main.go
-FROM alpine:3.21.2
+FROM alpine:3.21.3
 
 
 # add a c lib
 # add a c lib
 # set the working directory
 # set the working directory
 WORKDIR /root/
 WORKDIR /root/
-RUN apk update && apk upgrade
+RUN apk upgrade --no-cache
 RUN apk add --no-cache sqlite
 RUN apk add --no-cache sqlite
 RUN mkdir -p /etc/netclient/config
 RUN mkdir -p /etc/netclient/config
 COPY --from=builder /app/netmaker .
 COPY --from=builder /app/netmaker .

+ 2 - 2
Dockerfile-quick

@@ -1,12 +1,12 @@
 #first stage - builder
 #first stage - builder
-FROM alpine:3.21.2
+FROM alpine:3.21.3
 ARG version 
 ARG version 
 WORKDIR /app
 WORKDIR /app
 COPY ./netmaker /root/netmaker
 COPY ./netmaker /root/netmaker
 ENV GO111MODULE=auto
 ENV GO111MODULE=auto
 
 
 # add a c lib
 # add a c lib
-RUN apk add gcompat iptables wireguard-tools
+RUN apk add --no-cache gcompat iptables wireguard-tools
 # set the working directory
 # set the working directory
 WORKDIR /root/
 WORKDIR /root/
 RUN mkdir -p /etc/netclient/config
 RUN mkdir -p /etc/netclient/config

+ 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.90.0-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-0.99.0-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" />

+ 36 - 32
auth/host_session.go

@@ -3,6 +3,8 @@ package auth
 import (
 import (
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
+	"log/slog"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/google/uuid"
 	"github.com/google/uuid"
@@ -14,7 +16,6 @@ import (
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
-	"golang.org/x/exp/slog"
 )
 )
 
 
 // SessionHandler - called by the HTTP router when user
 // SessionHandler - called by the HTTP router when user
@@ -67,7 +68,7 @@ func SessionHandler(conn *websocket.Conn) {
 	if len(registerMessage.User) > 0 { // handle basic auth
 	if len(registerMessage.User) > 0 { // handle basic auth
 		logger.Log(0, "user registration attempted with host:", registerMessage.RegisterHost.Name, "user:", registerMessage.User)
 		logger.Log(0, "user registration attempted with host:", registerMessage.RegisterHost.Name, "user:", registerMessage.User)
 
 
-		if !servercfg.IsBasicAuthEnabled() {
+		if !logic.IsBasicAuthEnabled() {
 			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
 			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
 			if err != nil {
 			if err != nil {
 				logger.Log(0, "error during message writing:", err.Error())
 				logger.Log(0, "error during message writing:", err.Error())
@@ -165,7 +166,7 @@ func SessionHandler(conn *websocket.Conn) {
 					return
 					return
 				}
 				}
 			}
 			}
-			logic.CheckHostPorts(&result.Host)
+			_ = logic.CheckHostPorts(&result.Host)
 			if err := logic.CreateHost(&result.Host); err != nil {
 			if err := logic.CreateHost(&result.Host); err != nil {
 				handleHostRegErr(conn, err)
 				handleHostRegErr(conn, err)
 				return
 				return
@@ -207,7 +208,7 @@ func SessionHandler(conn *websocket.Conn) {
 				netsToAdd = append(netsToAdd, newNet)
 				netsToAdd = append(netsToAdd, newNet)
 			}
 			}
 		}
 		}
-		server := servercfg.GetServerInfo()
+		server := logic.GetServerInfo()
 		server.TrafficKey = key
 		server.TrafficKey = key
 		result.Host.HostPass = ""
 		result.Host.HostPass = ""
 		response := models.RegisterResponse{
 		response := models.RegisterResponse{
@@ -242,37 +243,40 @@ func CheckNetRegAndHostUpdate(networks []string, h *models.Host, relayNodeId uui
 		network := networks[i]
 		network := networks[i]
 		if ok, _ := logic.NetworkExists(network); ok {
 		if ok, _ := logic.NetworkExists(network); ok {
 			newNode, err := logic.UpdateHostNetwork(h, network, true)
 			newNode, err := logic.UpdateHostNetwork(h, network, true)
-			if err != nil {
-				logger.Log(0, "failed to add host to network:", h.ID.String(), h.Name, network, err.Error())
-				continue
-			}
-			if len(tags) > 0 {
-				newNode.Tags = make(map[models.TagID]struct{})
-				for _, tagI := range tags {
-					newNode.Tags[tagI] = struct{}{}
-				}
-				logic.UpsertNode(newNode)
-			}
-
-			if relayNodeId != uuid.Nil && !newNode.IsRelayed {
-				// check if relay node exists and acting as relay
-				relaynode, err := logic.GetNodeByID(relayNodeId.String())
-				if err == nil && relaynode.IsRelay && relaynode.Network == newNode.Network {
-					slog.Info(fmt.Sprintf("adding relayed node %s to relay %s on network %s", newNode.ID.String(), relayNodeId.String(), network))
-					newNode.IsRelayed = true
-					newNode.RelayedBy = relayNodeId.String()
-					updatedRelayNode := relaynode
-					updatedRelayNode.RelayedNodes = append(updatedRelayNode.RelayedNodes, newNode.ID.String())
-					logic.UpdateRelayed(&relaynode, &updatedRelayNode)
-					if err := logic.UpsertNode(&updatedRelayNode); err != nil {
-						slog.Error("failed to update node", "nodeid", relayNodeId.String())
+			if err == nil || strings.Contains(err.Error(), "host already part of network") {
+				if len(tags) > 0 {
+					newNode.Tags = make(map[models.TagID]struct{})
+					for _, tagI := range tags {
+						newNode.Tags[tagI] = struct{}{}
 					}
 					}
-					if err := logic.UpsertNode(newNode); err != nil {
-						slog.Error("failed to update node", "nodeid", relayNodeId.String())
+					logic.UpsertNode(newNode)
+				}
+				if relayNodeId != uuid.Nil && !newNode.IsRelayed {
+					// check if relay node exists and acting as relay
+					relaynode, err := logic.GetNodeByID(relayNodeId.String())
+					if err == nil && relaynode.IsGw && relaynode.Network == newNode.Network {
+						slog.Error(fmt.Sprintf("adding relayed node %s to relay %s on network %s", newNode.ID.String(), relayNodeId.String(), network))
+						newNode.IsRelayed = true
+						newNode.RelayedBy = relayNodeId.String()
+						updatedRelayNode := relaynode
+						updatedRelayNode.RelayedNodes = append(updatedRelayNode.RelayedNodes, newNode.ID.String())
+						logic.UpdateRelayed(&relaynode, &updatedRelayNode)
+						if err := logic.UpsertNode(&updatedRelayNode); err != nil {
+							slog.Error("failed to update node", "nodeid", relayNodeId.String())
+						}
+						if err := logic.UpsertNode(newNode); err != nil {
+							slog.Error("failed to update node", "nodeid", relayNodeId.String())
+						}
+					} else {
+						slog.Error("failed to relay node. maybe specified relay node is actually not a relay? Or the relayed node is not in the same network with relay?", "err", err)
 					}
 					}
-				} else {
-					slog.Error("failed to relay node. maybe specified relay node is actually not a relay? Or the relayed node is not in the same network with relay?", "err", err)
 				}
 				}
+				if err != nil && strings.Contains(err.Error(), "host already part of network") {
+					continue
+				}
+			} else {
+				logger.Log(0, "failed to add host to network:", h.ID.String(), h.Name, network, err.Error())
+				continue
 			}
 			}
 			logger.Log(1, "added new node", newNode.ID.String(), "to host", h.Name)
 			logger.Log(1, "added new node", newNode.ID.String(), "to host", h.Name)
 			hostactions.AddAction(models.HostUpdate{
 			hostactions.AddAction(models.HostUpdate{

+ 43 - 0
cli/cmd/access_token/create.go

@@ -0,0 +1,43 @@
+package access_token
+
+import (
+	"time"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/schema"
+	"github.com/spf13/cobra"
+)
+
+var accessTokenCreateCmd = &cobra.Command{
+	Use:   "create [token-name]",
+	Short: "Create an access token",
+	Long:  `Create an access token for a user`,
+	Args:  cobra.ExactArgs(1),
+	Run: func(cmd *cobra.Command, args []string) {
+		userName, _ := cmd.Flags().GetString("user")
+		expiresAt, _ := cmd.Flags().GetString("expires")
+
+		accessToken := &schema.UserAccessToken{}
+		accessToken.Name = args[0]
+		accessToken.UserName = userName
+
+		expTime := time.Now().Add(time.Hour * 24 * 365) // default to 1 year
+		if expiresAt != "" {
+			var err error
+			expTime, err = time.Parse(time.RFC3339, expiresAt)
+			if err != nil {
+				cmd.PrintErrf("Invalid expiration time format. Please use RFC3339 format (e.g. 2024-01-01T00:00:00Z). Using default 1 year.\n")
+			}
+		}
+		accessToken.ExpiresAt = expTime
+
+		functions.PrettyPrint(functions.CreateAccessToken(accessToken))
+	},
+}
+
+func init() {
+	accessTokenCreateCmd.Flags().String("user", "", "Username to create token for")
+	accessTokenCreateCmd.Flags().String("expires", "", "Expiration time for the token in RFC3339 format (e.g. 2024-01-01T00:00:00Z). Defaults to 1 year from now.")
+	accessTokenCreateCmd.MarkFlagRequired("user")
+	rootCmd.AddCommand(accessTokenCreateCmd)
+}

+ 23 - 0
cli/cmd/access_token/delete.go

@@ -0,0 +1,23 @@
+package access_token
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var accessTokenDeleteCmd = &cobra.Command{
+	Use:   "delete [ACCESS TOKEN ID]",
+	Short: "Delete an access token",
+	Long:  `Delete an access token by ID`,
+	Args:  cobra.ExactArgs(1),
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.DeleteAccessToken(args[0])
+		fmt.Println("Access token deleted successfully")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(accessTokenDeleteCmd)
+}

+ 20 - 0
cli/cmd/access_token/get.go

@@ -0,0 +1,20 @@
+package access_token
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var accessTokenGetCmd = &cobra.Command{
+	Use:   "get [USERNAME]",
+	Short: "Get a user's access token",
+	Long:  `Get a user's access token`,
+	Args:  cobra.ExactArgs(1),
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetAccessToken(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(accessTokenGetCmd)
+}

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

@@ -0,0 +1,28 @@
+package access_token
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "access_token",
+	Short: "Manage Netmaker user access tokens",
+	Long:  `Manage a Netmaker user's access tokens. This command allows you to create, delete, and list access tokens for a user.`,
+}
+
+// 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)
+	}
+}

+ 7 - 4
cli/cmd/context/set.go

@@ -17,6 +17,7 @@ var (
 	sso       bool
 	sso       bool
 	tenantId  string
 	tenantId  string
 	saas      bool
 	saas      bool
+	authToken string
 )
 )
 
 
 var contextSetCmd = &cobra.Command{
 var contextSetCmd = &cobra.Command{
@@ -30,13 +31,14 @@ var contextSetCmd = &cobra.Command{
 			Username:  username,
 			Username:  username,
 			Password:  password,
 			Password:  password,
 			MasterKey: masterKey,
 			MasterKey: masterKey,
+			AuthToken: authToken,
 			SSO:       sso,
 			SSO:       sso,
 			TenantId:  tenantId,
 			TenantId:  tenantId,
 			Saas:      saas,
 			Saas:      saas,
 		}
 		}
 		if !ctx.Saas {
 		if !ctx.Saas {
-			if ctx.Username == "" && ctx.MasterKey == "" && !ctx.SSO {
-				log.Fatal("Either username/password or master key is required")
+			if ctx.Username == "" && ctx.MasterKey == "" && !ctx.SSO && ctx.AuthToken == "" {
+				log.Fatal("Either username/password or master key or auth token is required")
 				cmd.Usage()
 				cmd.Usage()
 			}
 			}
 			if ctx.Endpoint == "" {
 			if ctx.Endpoint == "" {
@@ -49,8 +51,8 @@ var contextSetCmd = &cobra.Command{
 				cmd.Usage()
 				cmd.Usage()
 			}
 			}
 			ctx.Endpoint = fmt.Sprintf(functions.TenantUrlTemplate, tenantId)
 			ctx.Endpoint = fmt.Sprintf(functions.TenantUrlTemplate, tenantId)
-			if ctx.Username == "" && ctx.Password == "" && !ctx.SSO {
-				log.Fatal("Username/password is required for non-SSO SaaS contexts")
+			if ctx.Username == "" && ctx.Password == "" && ctx.AuthToken == "" && !ctx.SSO {
+				log.Fatal("Username/password or authtoken is required for non-SSO SaaS contexts")
 				cmd.Usage()
 				cmd.Usage()
 			}
 			}
 		}
 		}
@@ -62,6 +64,7 @@ func init() {
 	contextSetCmd.Flags().StringVar(&endpoint, "endpoint", "", "Endpoint of the API Server")
 	contextSetCmd.Flags().StringVar(&endpoint, "endpoint", "", "Endpoint of the API Server")
 	contextSetCmd.Flags().StringVar(&username, "username", "", "Username")
 	contextSetCmd.Flags().StringVar(&username, "username", "", "Username")
 	contextSetCmd.Flags().StringVar(&password, "password", "", "Password")
 	contextSetCmd.Flags().StringVar(&password, "password", "", "Password")
+	contextSetCmd.Flags().StringVar(&authToken, "auth_token", "", "Auth Token")
 	contextSetCmd.MarkFlagsRequiredTogether("username", "password")
 	contextSetCmd.MarkFlagsRequiredTogether("username", "password")
 	contextSetCmd.Flags().BoolVar(&sso, "sso", false, "Login via Single Sign On (SSO)?")
 	contextSetCmd.Flags().BoolVar(&sso, "sso", false, "Login via Single Sign On (SSO)?")
 	contextSetCmd.Flags().StringVar(&masterKey, "master_key", "", "Master Key")
 	contextSetCmd.Flags().StringVar(&masterKey, "master_key", "", "Master Key")

+ 3 - 3
cli/cmd/host/add_network.go

@@ -6,10 +6,10 @@ import (
 )
 )
 
 
 var addHostNetworkCmd = &cobra.Command{
 var addHostNetworkCmd = &cobra.Command{
-	Use:   "add_network HostID Network",
+	Use:   "add_network DeviceID/HostID Network",
 	Args:  cobra.ExactArgs(2),
 	Args:  cobra.ExactArgs(2),
-	Short: "Add a network to a host",
-	Long:  `Add a network to a host`,
+	Short: "Add a device to a network",
+	Long:  `Add a device to a network`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.AddHostToNetwork(args[0], args[1]))
 		functions.PrettyPrint(functions.AddHostToNetwork(args[0], args[1]))
 	},
 	},

+ 3 - 3
cli/cmd/host/delete.go

@@ -8,10 +8,10 @@ import (
 var force bool
 var force bool
 
 
 var hostDeleteCmd = &cobra.Command{
 var hostDeleteCmd = &cobra.Command{
-	Use:   "delete HostID",
+	Use:   "delete DeviceID/HostID",
 	Args:  cobra.ExactArgs(1),
 	Args:  cobra.ExactArgs(1),
-	Short: "Delete a host",
-	Long:  `Delete a host`,
+	Short: "Delete a device",
+	Long:  `Delete a device`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.DeleteHost(args[0], force))
 		functions.PrettyPrint(functions.DeleteHost(args[0], force))
 	},
 	},

+ 3 - 3
cli/cmd/host/delete_network.go

@@ -6,10 +6,10 @@ import (
 )
 )
 
 
 var deleteHostNetworkCmd = &cobra.Command{
 var deleteHostNetworkCmd = &cobra.Command{
-	Use:   "delete_network HostID Network",
+	Use:   "delete_network DeviceID/HostID Network",
 	Args:  cobra.ExactArgs(2),
 	Args:  cobra.ExactArgs(2),
-	Short: "Delete a network from a host",
-	Long:  `Delete a network from a host`,
+	Short: "Remove a device from a network",
+	Long:  `Remove a device from a network`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.DeleteHostFromNetwork(args[0], args[1]))
 		functions.PrettyPrint(functions.DeleteHostFromNetwork(args[0], args[1]))
 	},
 	},

+ 2 - 2
cli/cmd/host/list.go

@@ -8,8 +8,8 @@ import (
 var hostListCmd = &cobra.Command{
 var hostListCmd = &cobra.Command{
 	Use:   "list",
 	Use:   "list",
 	Args:  cobra.NoArgs,
 	Args:  cobra.NoArgs,
-	Short: "List all hosts",
-	Long:  `List all hosts`,
+	Short: "List all devices",
+	Long:  `List all devices`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.GetHosts())
 		functions.PrettyPrint(functions.GetHosts())
 	},
 	},

+ 4 - 4
cli/cmd/host/refresh_keys.go

@@ -6,11 +6,11 @@ import (
 )
 )
 
 
 var hostRefreshKeysCmd = &cobra.Command{
 var hostRefreshKeysCmd = &cobra.Command{
-	Use:   "refresh_keys [HOST ID] ",
+	Use:   "refresh_keys [DEVICE ID/HOST ID]",
 	Args:  cobra.MaximumNArgs(1),
 	Args:  cobra.MaximumNArgs(1),
-	Short: "Refresh wireguard keys on host",
-	Long: `Refresh wireguard keys on specified or all hosts
-	If HOSTID is not specified, all hosts will be updated`,
+	Short: "Refresh wireguard keys on device",
+	Long: `Refresh wireguard keys on specified or all devices
+	If DEVICE ID/HOST ID is not specified, all devices will be updated`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		functions.PrettyPrint(functions.RefreshKeys(args[0]))
 		functions.PrettyPrint(functions.RefreshKeys(args[0]))
 	},
 	},

+ 4 - 3
cli/cmd/host/root.go

@@ -8,9 +8,10 @@ import (
 
 
 // rootCmd represents the base command when called without any subcommands
 // rootCmd represents the base command when called without any subcommands
 var rootCmd = &cobra.Command{
 var rootCmd = &cobra.Command{
-	Use:   "host",
-	Short: "Manage hosts",
-	Long:  `Manage hosts`,
+	Use:     "device",
+	Aliases: []string{"host"},
+	Short:   "Manage devices",
+	Long:    `Manage devices`,
 }
 }
 
 
 // GetRoot returns the root subcommand
 // GetRoot returns the root subcommand

+ 11 - 11
cli/cmd/host/update.go

@@ -25,10 +25,10 @@ var (
 )
 )
 
 
 var hostUpdateCmd = &cobra.Command{
 var hostUpdateCmd = &cobra.Command{
-	Use:   "update HostID",
+	Use:   "update DeviceID/HostID",
 	Args:  cobra.ExactArgs(1),
 	Args:  cobra.ExactArgs(1),
-	Short: "Update a host",
-	Long:  `Update a host`,
+	Short: "Update a device",
+	Long:  `Update a device`,
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
 		apiHost := &models.ApiHost{}
 		apiHost := &models.ApiHost{}
 		if apiHostFilePath != "" {
 		if apiHostFilePath != "" {
@@ -57,14 +57,14 @@ var hostUpdateCmd = &cobra.Command{
 
 
 func init() {
 func init() {
 	hostUpdateCmd.Flags().StringVar(&apiHostFilePath, "file", "", "Path to host_definition.json")
 	hostUpdateCmd.Flags().StringVar(&apiHostFilePath, "file", "", "Path to host_definition.json")
-	hostUpdateCmd.Flags().StringVar(&endpoint, "endpoint", "", "Endpoint of the Host")
-	hostUpdateCmd.Flags().StringVar(&endpoint6, "endpoint6", "", "IPv6 Endpoint of the Host")
-	hostUpdateCmd.Flags().StringVar(&name, "name", "", "Host name")
-	hostUpdateCmd.Flags().IntVar(&listenPort, "listen_port", 0, "Listen port of the host")
-	hostUpdateCmd.Flags().IntVar(&mtu, "mtu", 0, "Host MTU size")
+	hostUpdateCmd.Flags().StringVar(&endpoint, "endpoint", "", "Endpoint of the Device")
+	hostUpdateCmd.Flags().StringVar(&endpoint6, "endpoint6", "", "IPv6 Endpoint of the Device")
+	hostUpdateCmd.Flags().StringVar(&name, "name", "", "Device name")
+	hostUpdateCmd.Flags().IntVar(&listenPort, "listen_port", 0, "Listen port of the device")
+	hostUpdateCmd.Flags().IntVar(&mtu, "mtu", 0, "Device MTU size")
 	hostUpdateCmd.Flags().IntVar(&keepAlive, "keep_alive", 0, "Interval (seconds) in which packets are sent to keep connections open with peers")
 	hostUpdateCmd.Flags().IntVar(&keepAlive, "keep_alive", 0, "Interval (seconds) in which packets are sent to keep connections open with peers")
-	hostUpdateCmd.Flags().BoolVar(&isStaticPort, "static_port", false, "Make Host Static Port?")
-	hostUpdateCmd.Flags().BoolVar(&isStatic, "static_endpoint", false, "Make Host Static Endpoint?")
-	hostUpdateCmd.Flags().BoolVar(&isDefault, "default", false, "Make Host Default ?")
+	hostUpdateCmd.Flags().BoolVar(&isStaticPort, "static_port", false, "Make Device Static Port?")
+	hostUpdateCmd.Flags().BoolVar(&isStatic, "static_endpoint", false, "Make Device Static Endpoint?")
+	hostUpdateCmd.Flags().BoolVar(&isDefault, "default", false, "Make Device Default ?")
 	rootCmd.AddCommand(hostUpdateCmd)
 	rootCmd.AddCommand(hostUpdateCmd)
 }
 }

+ 26 - 3
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", "Remote Access Gateway", "Relay"})
+			table.SetHeader([]string{"ID", "Addresses", "Network", "Egress", "Remote Access Gateway", "Relay", "Type"})
 			for _, d := range data {
 			for _, d := range data {
 				addresses := ""
 				addresses := ""
 				if d.Address != "" {
 				if d.Address != "" {
@@ -41,8 +41,31 @@ var nodeListCmd = &cobra.Command{
 					}
 					}
 					addresses += d.Address6
 					addresses += d.Address6
 				}
 				}
-				table.Append([]string{d.ID, addresses, d.Network,
-					strconv.FormatBool(d.IsEgressGateway), strconv.FormatBool(d.IsIngressGateway), strconv.FormatBool(d.IsRelay)})
+				network := d.Network
+				id := d.ID
+				nodeType := "Device"
+
+				if d.IsStatic {
+					id = d.StaticNode.ClientID
+					nodeType = "Static"
+				}
+				if d.IsUserNode {
+					id = d.StaticNode.OwnerID
+					nodeType = "User"
+				}
+				if d.IsStatic || d.IsUserNode {
+					addresses = d.StaticNode.Address
+					if d.StaticNode.Address6 != "" {
+						if addresses != "" {
+							addresses += ", "
+						}
+						addresses += d.StaticNode.Address6
+					}
+					network = d.StaticNode.Network
+				}
+
+				table.Append([]string{id, addresses, network,
+					strconv.FormatBool(d.IsEgressGateway), strconv.FormatBool(d.IsIngressGateway), strconv.FormatBool(d.IsRelay), nodeType})
 			}
 			}
 			table.Render()
 			table.Render()
 		}
 		}

+ 4 - 1
cli/cmd/root.go

@@ -1,9 +1,9 @@
 package cmd
 package cmd
 
 
 import (
 import (
-	"github.com/gravitl/netmaker/cli/cmd/gateway"
 	"os"
 	"os"
 
 
+	"github.com/gravitl/netmaker/cli/cmd/access_token"
 	"github.com/gravitl/netmaker/cli/cmd/acl"
 	"github.com/gravitl/netmaker/cli/cmd/acl"
 	"github.com/gravitl/netmaker/cli/cmd/commons"
 	"github.com/gravitl/netmaker/cli/cmd/commons"
 	"github.com/gravitl/netmaker/cli/cmd/context"
 	"github.com/gravitl/netmaker/cli/cmd/context"
@@ -11,12 +11,14 @@ import (
 	"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/failover"
+	"github.com/gravitl/netmaker/cli/cmd/gateway"
 	"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"
 	"github.com/gravitl/netmaker/cli/cmd/node"
 	"github.com/gravitl/netmaker/cli/cmd/node"
 	"github.com/gravitl/netmaker/cli/cmd/server"
 	"github.com/gravitl/netmaker/cli/cmd/server"
 	"github.com/gravitl/netmaker/cli/cmd/user"
 	"github.com/gravitl/netmaker/cli/cmd/user"
+
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
@@ -57,4 +59,5 @@ func init() {
 	rootCmd.AddCommand(enrollment_key.GetRoot())
 	rootCmd.AddCommand(enrollment_key.GetRoot())
 	rootCmd.AddCommand(failover.GetRoot())
 	rootCmd.AddCommand(failover.GetRoot())
 	rootCmd.AddCommand(gateway.GetRoot())
 	rootCmd.AddCommand(gateway.GetRoot())
+	rootCmd.AddCommand(access_token.GetRoot())
 }
 }

+ 58 - 0
cli/functions/access_tokens.go

@@ -0,0 +1,58 @@
+package functions
+
+import (
+	"encoding/json"
+	"log"
+	"net/http"
+
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
+)
+
+// CreateAccessToken - creates an access token for a user
+func CreateAccessToken(payload *schema.UserAccessToken) *models.SuccessfulUserLoginResponse {
+	res := request[models.SuccessResponse](http.MethodPost, "/api/v1/users/access_token", payload)
+	if res.Code != http.StatusOK {
+		log.Fatalf("Error creating access token: %s", res.Message)
+	}
+
+	var token models.SuccessfulUserLoginResponse
+	responseBytes, err := json.Marshal(res.Response)
+	if err != nil {
+		log.Fatalf("Error marshaling response: %v", err)
+	}
+
+	if err := json.Unmarshal(responseBytes, &token); err != nil {
+		log.Fatalf("Error unmarshaling token: %v", err)
+	}
+
+	return &token
+}
+
+// GetAccessToken - fetch all access tokens per user
+func GetAccessToken(userName string) []schema.UserAccessToken {
+	res := request[models.SuccessResponse](http.MethodGet, "/api/v1/users/access_token?username="+userName, nil)
+	if res.Code != http.StatusOK {
+		log.Fatalf("Error getting access token: %s", res.Message)
+	}
+
+	var tokens []schema.UserAccessToken
+	responseBytes, err := json.Marshal(res.Response)
+	if err != nil {
+		log.Fatalf("Error marshaling response: %v", err)
+	}
+
+	if err := json.Unmarshal(responseBytes, &tokens); err != nil {
+		log.Fatalf("Error unmarshaling tokens: %v", err)
+	}
+
+	return tokens
+}
+
+// DeleteAccessToken - delete an access token
+func DeleteAccessToken(id string) {
+	res := request[models.SuccessResponse](http.MethodDelete, "/api/v1/users/access_token?id="+id, nil)
+	if res.Code != http.StatusOK {
+		log.Fatalf("Error deleting access token: %s", res.Message)
+	}
+}

+ 1 - 1
cli/functions/http_client.go

@@ -192,7 +192,7 @@ retry:
 	body := new(T)
 	body := new(T)
 	if len(resBodyBytes) > 0 {
 	if len(resBodyBytes) > 0 {
 		if err := json.Unmarshal(resBodyBytes, body); err != nil {
 		if err := json.Unmarshal(resBodyBytes, body); err != nil {
-			log.Fatalf("Error unmarshalling JSON: %s", err)
+			log.Fatalf("Error unmarshalling JSON: %s %s", err, string(resBodyBytes))
 		}
 		}
 	}
 	}
 	return body
 	return body

+ 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.90.0'
+    image: 'gravitl/netclient:v0.99.0'
     hostname: netmaker-1
     hostname: netmaker-1
     network_mode: host
     network_mode: host
     restart: on-failure
     restart: on-failure

+ 0 - 1
config/config.go

@@ -75,7 +75,6 @@ type ServerConfig struct {
 	NetmakerTenantID           string        `yaml:"netmaker_tenant_id"`
 	NetmakerTenantID           string        `yaml:"netmaker_tenant_id"`
 	IsPro                      string        `yaml:"is_ee" json:"IsEE"`
 	IsPro                      string        `yaml:"is_ee" json:"IsEE"`
 	StunPort                   int           `yaml:"stun_port"`
 	StunPort                   int           `yaml:"stun_port"`
-	StunList                   string        `yaml:"stun_list"`
 	TurnServer                 string        `yaml:"turn_server"`
 	TurnServer                 string        `yaml:"turn_server"`
 	TurnApiServer              string        `yaml:"turn_api_server"`
 	TurnApiServer              string        `yaml:"turn_api_server"`
 	TurnPort                   int           `yaml:"turn_port"`
 	TurnPort                   int           `yaml:"turn_port"`

+ 93 - 6
controllers/acls.go

@@ -9,15 +9,19 @@ import (
 
 
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/db"
 	"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"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/schema"
 )
 )
 
 
 func aclHandlers(r *mux.Router) {
 func aclHandlers(r *mux.Router) {
 	r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(getAcls))).
 	r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(getAcls))).
 		Methods(http.MethodGet)
 		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/acls/egress", logic.SecurityCheck(true, http.HandlerFunc(getEgressAcls))).
+		Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/acls/policy_types", logic.SecurityCheck(true, http.HandlerFunc(aclPolicyTypes))).
 	r.HandleFunc("/api/v1/acls/policy_types", logic.SecurityCheck(true, http.HandlerFunc(aclPolicyTypes))).
 		Methods(http.MethodGet)
 		Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(createAcl))).
 	r.HandleFunc("/api/v1/acls", logic.SecurityCheck(true, http.HandlerFunc(createAcl))).
@@ -51,7 +55,7 @@ func aclPolicyTypes(w http.ResponseWriter, r *http.Request) {
 		DstGroupTypes: []models.AclGroupType{
 		DstGroupTypes: []models.AclGroupType{
 			models.NodeTagID,
 			models.NodeTagID,
 			models.NodeID,
 			models.NodeID,
-			models.EgressRange,
+			models.EgressID,
 			// models.NetmakerIPAclID,
 			// models.NetmakerIPAclID,
 			// models.NetmakerSubNetRangeAClID,
 			// models.NetmakerSubNetRangeAClID,
 		},
 		},
@@ -171,9 +175,11 @@ func aclDebug(w http.ResponseWriter, r *http.Request) {
 		IsPeerAllowed bool
 		IsPeerAllowed bool
 		Policies      []models.Acl
 		Policies      []models.Acl
 		IngressRules  []models.FwRule
 		IngressRules  []models.FwRule
+		NodeAllPolicy bool
+		EgressNets    map[string]models.Node
 	}
 	}
 
 
-	allowed, ps := logic.IsNodeAllowedToCommunicateV1(node, peer, true)
+	allowed, ps := logic.IsNodeAllowedToCommunicate(node, peer, true)
 	isallowed := logic.IsPeerAllowed(node, peer, true)
 	isallowed := logic.IsPeerAllowed(node, peer, true)
 	re := resp{
 	re := resp{
 		IsNodeAllowed: allowed,
 		IsNodeAllowed: allowed,
@@ -217,6 +223,35 @@ func getAcls(w http.ResponseWriter, r *http.Request) {
 	logic.ReturnSuccessResponseWithJson(w, r, acls, "fetched all acls in the network "+netID)
 	logic.ReturnSuccessResponseWithJson(w, r, acls, "fetched all acls in the network "+netID)
 }
 }
 
 
+// @Summary     List Egress Acls in a network
+// @Router      /api/v1/acls [get]
+// @Tags        ACL
+// @Accept      json
+// @Success     200 {array} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
+func getEgressAcls(w http.ResponseWriter, r *http.Request) {
+	eID := r.URL.Query().Get("egress_id")
+	if eID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("egress id param is missing"), "badrequest"))
+		return
+	}
+	e := schema.Egress{ID: eID}
+	// check if network exists
+	err := e.Get(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	acls, err := logic.ListEgressAcls(eID)
+	if err != nil {
+		logger.Log(0, r.Header.Get("user"), "failed to get all network acl entries: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logic.SortAclEntrys(acls[:])
+	logic.ReturnSuccessResponseWithJson(w, r, acls, "fetched acls for egress"+e.Name)
+}
+
 // @Summary     Create Acl
 // @Summary     Create Acl
 // @Router      /api/v1/acls [post]
 // @Router      /api/v1/acls [post]
 // @Tags        ACL
 // @Tags        ACL
@@ -253,8 +288,8 @@ func createAcl(w http.ResponseWriter, r *http.Request) {
 		acl.Proto = models.ALL
 		acl.Proto = models.ALL
 	}
 	}
 	// validate create acl policy
 	// validate create acl policy
-	if !logic.IsAclPolicyValid(acl) {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid policy"), "badrequest"))
+	if err := logic.IsAclPolicyValid(acl); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
 	err = logic.InsertAcl(acl)
 	err = logic.InsertAcl(acl)
@@ -267,6 +302,22 @@ func createAcl(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   acl.ID,
+			Name: acl.Name,
+			Type: models.AclSub,
+		},
+		NetworkID: acl.NetworkID,
+		Origin:    models.Dashboard,
+	})
 	go mq.PublishPeerUpdate(true)
 	go mq.PublishPeerUpdate(true)
 	logic.ReturnSuccessResponseWithJson(w, r, acl, "created acl successfully")
 	logic.ReturnSuccessResponseWithJson(w, r, acl, "created acl successfully")
 }
 }
@@ -292,8 +343,8 @@ func updateAcl(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
-	if !logic.IsAclPolicyValid(updateAcl.Acl) {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid policy"), "badrequest"))
+	if err := logic.IsAclPolicyValid(updateAcl.Acl); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
 	if updateAcl.Acl.NetworkID != acl.NetworkID {
 	if updateAcl.Acl.NetworkID != acl.NetworkID {
@@ -309,6 +360,26 @@ func updateAcl(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Update,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   acl.ID,
+			Name: acl.Name,
+			Type: models.AclSub,
+		},
+		Diff: models.Diff{
+			Old: acl,
+			New: updateAcl.Acl,
+		},
+		NetworkID: acl.NetworkID,
+		Origin:    models.Dashboard,
+	})
 	go mq.PublishPeerUpdate(true)
 	go mq.PublishPeerUpdate(true)
 	logic.ReturnSuccessResponse(w, r, "updated acl "+acl.Name)
 	logic.ReturnSuccessResponse(w, r, "updated acl "+acl.Name)
 }
 }
@@ -340,6 +411,22 @@ func deleteAcl(w http.ResponseWriter, r *http.Request) {
 			logic.FormatError(errors.New("cannot delete default policy"), "internal"))
 			logic.FormatError(errors.New("cannot delete default policy"), "internal"))
 		return
 		return
 	}
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   acl.ID,
+			Name: acl.Name,
+			Type: models.AclSub,
+		},
+		NetworkID: acl.NetworkID,
+		Origin:    models.Dashboard,
+	})
 	go mq.PublishPeerUpdate(true)
 	go mq.PublishPeerUpdate(true)
 	logic.ReturnSuccessResponse(w, r, "deleted acl "+acl.Name)
 	logic.ReturnSuccessResponse(w, r, "deleted acl "+acl.Name)
 }
 }

+ 4 - 1
controllers/controller.go

@@ -9,6 +9,8 @@ import (
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
+	"github.com/gravitl/netmaker/db"
+
 	"github.com/gorilla/handlers"
 	"github.com/gorilla/handlers"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
@@ -18,6 +20,7 @@ import (
 
 
 // HttpMiddlewares - middleware functions for REST interactions
 // HttpMiddlewares - middleware functions for REST interactions
 var HttpMiddlewares = []mux.MiddlewareFunc{
 var HttpMiddlewares = []mux.MiddlewareFunc{
+	db.Middleware,
 	userMiddleWare,
 	userMiddleWare,
 }
 }
 
 
@@ -35,8 +38,8 @@ var HttpHandlers = []interface{}{
 	loggerHandlers,
 	loggerHandlers,
 	hostHandlers,
 	hostHandlers,
 	enrollmentKeyHandlers,
 	enrollmentKeyHandlers,
-	tagHandlers,
 	aclHandlers,
 	aclHandlers,
+	egressHandlers,
 	legacyHandlers,
 	legacyHandlers,
 }
 }
 
 

+ 6 - 6
controllers/dns.go

@@ -164,9 +164,9 @@ func createDNS(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 	// check if default domain is appended if not append
 	// check if default domain is appended if not append
-	if servercfg.GetDefaultDomain() != "" &&
-		!strings.HasSuffix(entry.Name, servercfg.GetDefaultDomain()) {
-		entry.Name += "." + servercfg.GetDefaultDomain()
+	if logic.GetDefaultDomain() != "" &&
+		!strings.HasSuffix(entry.Name, logic.GetDefaultDomain()) {
+		entry.Name += "." + logic.GetDefaultDomain()
 	}
 	}
 	entry, err = logic.CreateDNS(entry)
 	entry, err = logic.CreateDNS(entry)
 	if err != nil {
 	if err != nil {
@@ -185,7 +185,7 @@ func createDNS(w http.ResponseWriter, r *http.Request) {
 		}
 		}
 	}
 	}
 
 
-	if servercfg.GetManageDNS() {
+	if logic.GetManageDNS() {
 		mq.SendDNSSyncByNetwork(netID)
 		mq.SendDNSSyncByNetwork(netID)
 	}
 	}
 
 
@@ -230,7 +230,7 @@ func deleteDNS(w http.ResponseWriter, r *http.Request) {
 		}
 		}
 	}
 	}
 
 
-	if servercfg.GetManageDNS() {
+	if logic.GetManageDNS() {
 		mq.SendDNSSyncByNetwork(netID)
 		mq.SendDNSSyncByNetwork(netID)
 	}
 	}
 
 
@@ -293,7 +293,7 @@ func pushDNS(w http.ResponseWriter, r *http.Request) {
 func syncDNS(w http.ResponseWriter, r *http.Request) {
 func syncDNS(w http.ResponseWriter, r *http.Request) {
 	// Set header
 	// Set header
 	w.Header().Set("Content-Type", "application/json")
 	w.Header().Set("Content-Type", "application/json")
-	if !servercfg.GetManageDNS() {
+	if !logic.GetManageDNS() {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
 			r,
 			r,

+ 0 - 2
controllers/dns_test.go

@@ -65,7 +65,6 @@ func TestGetNodeDNS(t *testing.T) {
 			ID:      uuid.New(),
 			ID:      uuid.New(),
 			Network: "skynet",
 			Network: "skynet",
 			Address: *ipnet,
 			Address: *ipnet,
-			DNSOn:   true,
 		}
 		}
 		createnode := models.Node{
 		createnode := models.Node{
 			CommonNode: tmpCNode,
 			CommonNode: tmpCNode,
@@ -82,7 +81,6 @@ func TestGetNodeDNS(t *testing.T) {
 			ID:      uuid.New(),
 			ID:      uuid.New(),
 			Network: "skynet",
 			Network: "skynet",
 			Address: *ipnet,
 			Address: *ipnet,
-			DNSOn:   true,
 		}
 		}
 		createnode := models.Node{
 		createnode := models.Node{
 			CommonNode: tmpCNode,
 			CommonNode: tmpCNode,

+ 305 - 0
controllers/egress.go

@@ -0,0 +1,305 @@
+package controller
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"net/http"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/schema"
+	"gorm.io/datatypes"
+)
+
+func egressHandlers(r *mux.Router) {
+	r.HandleFunc("/api/v1/egress", logic.SecurityCheck(true, http.HandlerFunc(createEgress))).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/egress", logic.SecurityCheck(true, http.HandlerFunc(listEgress))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/egress", logic.SecurityCheck(true, http.HandlerFunc(updateEgress))).Methods(http.MethodPut)
+	r.HandleFunc("/api/v1/egress", logic.SecurityCheck(true, http.HandlerFunc(deleteEgress))).Methods(http.MethodDelete)
+}
+
+// @Summary     Create Egress Resource
+// @Router      /api/v1/egress [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.Egress
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func createEgress(w http.ResponseWriter, r *http.Request) {
+
+	var req models.EgressReq
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	var egressRange string
+	if !req.IsInetGw {
+		egressRange, err = logic.NormalizeCIDR(req.Range)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+	} else {
+		egressRange = "*"
+	}
+
+	e := schema.Egress{
+		ID:          uuid.New().String(),
+		Name:        req.Name,
+		Network:     req.Network,
+		Description: req.Description,
+		Range:       egressRange,
+		Nat:         req.Nat,
+		Nodes:       make(datatypes.JSONMap),
+		Tags:        make(datatypes.JSONMap),
+		Status:      true,
+		CreatedBy:   r.Header.Get("user"),
+		CreatedAt:   time.Now().UTC(),
+	}
+	for nodeID, metric := range req.Nodes {
+		e.Nodes[nodeID] = metric
+	}
+	if err := logic.ValidateEgressReq(&e); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = e.Create(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error creating egress resource"+err.Error()), "internal"),
+		)
+		return
+	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   e.ID,
+			Name: e.Name,
+			Type: models.EgressSub,
+		},
+		NetworkID: models.NetworkID(e.Network),
+		Origin:    models.Dashboard,
+	})
+	// for nodeID := range e.Nodes {
+	// 	node, err := logic.GetNodeByID(nodeID)
+	// 	if err != nil {
+	// 		logic.AddEgressInfoToNode(&node, e)
+	// 		logic.UpsertNode(&node)
+	// 	}
+
+	// }
+	go mq.PublishPeerUpdate(false)
+	logic.ReturnSuccessResponseWithJson(w, r, e, "created egress resource")
+}
+
+// @Summary     List Egress Resource
+// @Router      /api/v1/egress [get]
+// @Tags        Auth
+// @Accept      json
+// @Param       query network string
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func listEgress(w http.ResponseWriter, r *http.Request) {
+
+	network := r.URL.Query().Get("network")
+	if network == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network is required"), "badrequest"))
+		return
+	}
+	e := schema.Egress{Network: network}
+	list, err := e.ListByNetwork(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error listing egress resource"+err.Error()), "internal"),
+		)
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, list, "fetched egress resource list")
+}
+
+// @Summary     Update Egress Resource
+// @Router      /api/v1/egress [put]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.Egress
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func updateEgress(w http.ResponseWriter, r *http.Request) {
+
+	var req models.EgressReq
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	var egressRange string
+	if !req.IsInetGw {
+		egressRange, err = logic.NormalizeCIDR(req.Range)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+	} else {
+		egressRange = "*"
+	}
+
+	e := schema.Egress{ID: req.ID}
+	err = e.Get(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	var updateNat bool
+	var updateStatus bool
+	if req.Nat != e.Nat {
+		updateNat = true
+	}
+	if req.Status != e.Status {
+		updateStatus = true
+	}
+	event := &models.Event{
+		Action: models.Update,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   e.ID,
+			Name: e.Name,
+			Type: models.EgressSub,
+		},
+		Diff: models.Diff{
+			Old: e,
+		},
+		NetworkID: models.NetworkID(e.Network),
+		Origin:    models.Dashboard,
+	}
+	e.Nodes = make(datatypes.JSONMap)
+	e.Tags = make(datatypes.JSONMap)
+	for nodeID, metric := range req.Nodes {
+		e.Nodes[nodeID] = metric
+	}
+	e.Range = egressRange
+	e.Description = req.Description
+	e.Name = req.Name
+	e.Nat = req.Nat
+	e.Status = req.Status
+	e.UpdatedAt = time.Now().UTC()
+	if err := logic.ValidateEgressReq(&e); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = e.Update(db.WithContext(context.TODO()))
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error creating egress resource"+err.Error()), "internal"),
+		)
+		return
+	}
+	if updateNat {
+		e.Nat = req.Nat
+		e.UpdateNatStatus(db.WithContext(context.TODO()))
+	}
+	if updateStatus {
+		e.Status = req.Status
+		e.UpdateEgressStatus(db.WithContext(context.TODO()))
+	}
+	event.Diff.New = e
+	logic.LogEvent(event)
+	go mq.PublishPeerUpdate(false)
+	logic.ReturnSuccessResponseWithJson(w, r, e, "updated egress resource")
+}
+
+// @Summary     Delete Egress Resource
+// @Router      /api/v1/egress [delete]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.Egress
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func deleteEgress(w http.ResponseWriter, r *http.Request) {
+
+	id := r.URL.Query().Get("id")
+	if id == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("id is required"), "badrequest"))
+		return
+	}
+	e := schema.Egress{ID: id}
+	err := e.Get(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
+		return
+	}
+	err = e.Delete(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
+		return
+	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   e.ID,
+			Name: e.Name,
+			Type: models.EgressSub,
+		},
+		NetworkID: models.NetworkID(e.Network),
+		Origin:    models.Dashboard,
+	})
+	// delete related acl policies
+	acls := logic.ListAcls()
+	for _, acl := range acls {
+
+		for i := len(acl.Dst) - 1; i >= 0; i-- {
+			if acl.Dst[i].ID == models.EgressID && acl.Dst[i].Value == id {
+				acl.Dst = append(acl.Dst[:i], acl.Dst[i+1:]...)
+			}
+		}
+		if len(acl.Dst) == 0 {
+			logic.DeleteAcl(acl)
+		} else {
+			logic.UpsertAcl(acl)
+		}
+	}
+	go mq.PublishPeerUpdate(false)
+	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted egress resource")
+}

+ 95 - 16
controllers/enrollmentkeys.go

@@ -72,12 +72,32 @@ func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
 func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	params := mux.Vars(r)
 	params := mux.Vars(r)
 	keyID := params["keyID"]
 	keyID := params["keyID"]
-	err := logic.DeleteEnrollmentKey(keyID, false)
+	key, err := logic.GetEnrollmentKey(keyID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	err = logic.DeleteEnrollmentKey(keyID, false)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to remove enrollment key: ", err.Error())
 		logger.Log(0, r.Header.Get("user"), "failed to remove enrollment key: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   keyID,
+			Name: key.Tags[0],
+			Type: models.EnrollmentKeySub,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(2, r.Header.Get("user"), "deleted enrollment key", keyID)
 	logger.Log(2, r.Header.Get("user"), "deleted enrollment key", keyID)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }
@@ -160,6 +180,7 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		enrollmentKeyBody.Unlimited,
 		enrollmentKeyBody.Unlimited,
 		relayId,
 		relayId,
 		false,
 		false,
+		enrollmentKeyBody.AutoEgress,
 	)
 	)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
 		logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
@@ -172,6 +193,21 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   newEnrollmentKey.Value,
+			Name: newEnrollmentKey.Tags[0],
+			Type: models.EnrollmentKeySub,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(2, r.Header.Get("user"), "created enrollment key")
 	logger.Log(2, r.Header.Get("user"), "created enrollment key")
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(newEnrollmentKey)
 	json.NewEncoder(w).Encode(newEnrollmentKey)
@@ -207,6 +243,7 @@ func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 			return
 			return
 		}
 		}
 	}
 	}
+	currKey, _ := logic.GetEnrollmentKey(keyId)
 
 
 	newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId, enrollmentKeyBody.Groups)
 	newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId, enrollmentKeyBody.Groups)
 	if err != nil {
 	if err != nil {
@@ -220,7 +257,25 @@ func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-
+	logic.LogEvent(&models.Event{
+		Action: models.Update,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   newEnrollmentKey.Value,
+			Name: newEnrollmentKey.Tags[0],
+			Type: models.EnrollmentKeySub,
+		},
+		Diff: models.Diff{
+			Old: currKey,
+			New: newEnrollmentKey,
+		},
+		Origin: models.Dashboard,
+	})
 	slog.Info("updated enrollment key", "id", keyId)
 	slog.Info("updated enrollment key", "id", keyId)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(newEnrollmentKey)
 	json.NewEncoder(w).Encode(newEnrollmentKey)
@@ -285,7 +340,7 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 	key, keyErr := logic.RetrievePublicTrafficKey()
 	key, keyErr := logic.RetrievePublicTrafficKey()
 	if keyErr != nil {
 	if keyErr != nil {
 		logger.Log(0, "error retrieving key:", keyErr.Error())
 		logger.Log(0, "error retrieving key:", keyErr.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(keyErr, "internal"))
 		return
 		return
 	}
 	}
 	// use the token
 	// use the token
@@ -301,7 +356,7 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 	if !hostExists {
 	if !hostExists {
 		newHost.PersistentKeepalive = models.DefaultPersistentKeepAlive
 		newHost.PersistentKeepalive = models.DefaultPersistentKeepAlive
 		// register host
 		// register host
-		logic.CheckHostPorts(&newHost)
+		_ = logic.CheckHostPorts(&newHost)
 		// create EMQX credentials and ACLs for host
 		// create EMQX credentials and ACLs for host
 		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 		if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 			if err := mq.GetEmqxHandler().CreateEmqxUser(newHost.ID.String(), newHost.HostPass); err != nil {
 			if err := mq.GetEmqxHandler().CreateEmqxUser(newHost.ID.String(), newHost.HostPass); err != nil {
@@ -325,14 +380,14 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 	} else {
 	} else {
 		// need to revise the list of networks from key
 		// need to revise the list of networks from key
 		// based on the ones host currently has
 		// based on the ones host currently has
-		networksToAdd := []string{}
-		currentNets := logic.GetHostNetworks(newHost.ID.String())
-		for _, newNet := range enrollmentKey.Networks {
-			if !logic.StringSliceContains(currentNets, newNet) {
-				networksToAdd = append(networksToAdd, newNet)
-			}
-		}
-		enrollmentKey.Networks = networksToAdd
+		// networksToAdd := []string{}
+		// currentNets := logic.GetHostNetworks(newHost.ID.String())
+		// for _, newNet := range enrollmentKey.Networks {
+		// 	if !logic.StringSliceContains(currentNets, newNet) {
+		// 		networksToAdd = append(networksToAdd, newNet)
+		// 	}
+		// }
+		// enrollmentKey.Networks = networksToAdd
 		currHost, err := logic.GetHost(newHost.ID.String())
 		currHost, err := logic.GetHost(newHost.ID.String())
 		if err != nil {
 		if err != nil {
 			slog.Error("failed registration", "hostID", newHost.ID.String(), "hostName", newHost.Name, "error", err.Error())
 			slog.Error("failed registration", "hostID", newHost.ID.String(), "hostName", newHost.Name, "error", err.Error())
@@ -347,16 +402,40 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 			return
 			return
 		}
 		}
 	}
 	}
+	host, err := logic.GetHost(newHost.ID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
 	// ready the response
 	// ready the response
-	server := servercfg.GetServerInfo()
+	server := logic.GetServerInfo()
 	server.TrafficKey = key
 	server.TrafficKey = key
 	response := models.RegisterResponse{
 	response := models.RegisterResponse{
 		ServerConf:    server,
 		ServerConf:    server,
-		RequestedHost: newHost,
+		RequestedHost: *host,
+	}
+	for _, netID := range enrollmentKey.Networks {
+		logic.LogEvent(&models.Event{
+			Action: models.JoinHostToNet,
+			Source: models.Subject{
+				ID:   enrollmentKey.Value,
+				Name: enrollmentKey.Tags[0],
+				Type: models.EnrollmentKeySub,
+			},
+			TriggeredBy: r.Header.Get("user"),
+			Target: models.Subject{
+				ID:   newHost.ID.String(),
+				Name: newHost.Name,
+				Type: models.DeviceSub,
+			},
+			NetworkID: models.NetworkID(netID),
+			Origin:    models.Dashboard,
+		})
 	}
 	}
-	logger.Log(0, newHost.Name, newHost.ID.String(), "registered with Netmaker")
+
+	logger.Log(0, host.Name, host.ID.String(), "registered with Netmaker")
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(&response)
 	json.NewEncoder(w).Encode(&response)
 	// notify host of changes, peer and node updates
 	// notify host of changes, peer and node updates
-	go auth.CheckNetRegAndHostUpdate(enrollmentKey.Networks, &newHost, enrollmentKey.Relay, enrollmentKey.Groups)
+	go auth.CheckNetRegAndHostUpdate(enrollmentKey.Networks, host, enrollmentKey.Relay, enrollmentKey.Groups)
 }
 }

+ 30 - 3
controllers/ext_client.go

@@ -1,6 +1,7 @@
 package controller
 package controller
 
 
 import (
 import (
+	"context"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
@@ -13,8 +14,10 @@ import (
 	"github.com/go-playground/validator/v10"
 	"github.com/go-playground/validator/v10"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 
 
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
@@ -174,6 +177,9 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	eli, _ := (&schema.Egress{Network: gwnode.Network}).ListByNetwork(db.WithContext(context.TODO()))
+	acls, _ := logic.ListAclsByNetwork(models.NetworkID(client.Network))
+	logic.GetNodeEgressInfo(&gwnode, eli, acls)
 	host, err := logic.GetHost(gwnode.HostID.String())
 	host, err := logic.GetHost(gwnode.HostID.String())
 	if err != nil {
 	if err != nil {
 		logger.Log(
 		logger.Log(
@@ -261,7 +267,7 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 
 
 	var newAllowedIPs string
 	var newAllowedIPs string
-	if logic.IsInternetGw(gwnode) || gwnode.InternetGwID != "" {
+	if logic.IsInternetGw(gwnode) {
 		egressrange := "0.0.0.0/0"
 		egressrange := "0.0.0.0/0"
 		if gwnode.Address6.IP != nil && client.Address6 != "" {
 		if gwnode.Address6.IP != nil && client.Address6 != "" {
 			egressrange += "," + "::/0"
 			egressrange += "," + "::/0"
@@ -688,7 +694,7 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 	var gateway models.EgressGatewayRequest
 	var gateway models.EgressGatewayRequest
 	gateway.NetID = params["network"]
 	gateway.NetID = params["network"]
 	gateway.Ranges = customExtClient.ExtraAllowedIPs
 	gateway.Ranges = customExtClient.ExtraAllowedIPs
-	err := logic.ValidateEgressRange(gateway)
+	err := logic.ValidateEgressRange(gateway.NetID, gateway.Ranges)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
 		logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
@@ -798,6 +804,27 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 		"clientid",
 		"clientid",
 		extclient.ClientID,
 		extclient.ClientID,
 	)
 	)
+	if extclient.RemoteAccessClientID != "" {
+		// if created by user from client app, log event
+		logic.LogEvent(&models.Event{
+			Action: models.Connect,
+			Source: models.Subject{
+				ID:   userName,
+				Name: userName,
+				Type: models.UserSub,
+			},
+			TriggeredBy: userName,
+			Target: models.Subject{
+				ID:   extclient.Network,
+				Name: extclient.Network,
+				Type: models.NetworkSub,
+				Info: extclient,
+			},
+			NetworkID: models.NetworkID(extclient.Network),
+			Origin:    models.ClientApp,
+		})
+	}
+
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	go func() {
 	go func() {
 		if err := logic.SetClientDefaultACLs(&extclient); err != nil {
 		if err := logic.SetClientDefaultACLs(&extclient); err != nil {
@@ -876,7 +903,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	var gateway models.EgressGatewayRequest
 	var gateway models.EgressGatewayRequest
 	gateway.NetID = params["network"]
 	gateway.NetID = params["network"]
 	gateway.Ranges = update.ExtraAllowedIPs
 	gateway.Ranges = update.ExtraAllowedIPs
-	err = logic.ValidateEgressRange(gateway)
+	err = logic.ValidateEgressRange(gateway.NetID, gateway.Ranges)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
 		logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))

+ 42 - 2
controllers/gateway.go

@@ -39,6 +39,11 @@ func createGateway(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
 	var req models.CreateGwReq
 	var req models.CreateGwReq
 	err = json.NewDecoder(r.Body).Decode(&req)
 	err = json.NewDecoder(r.Body).Decode(&req)
 	if err != nil {
 	if err != nil {
@@ -89,7 +94,23 @@ func createGateway(w http.ResponseWriter, r *http.Request) {
 	)
 	)
 	logic.GetNodeStatus(&relayNode, false)
 	logic.GetNodeStatus(&relayNode, false)
 	apiNode := relayNode.ConvertToAPINode()
 	apiNode := relayNode.ConvertToAPINode()
-
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   node.ID.String(),
+			Name: host.Name,
+			Type: models.GatewaySub,
+		},
+		Origin: models.Dashboard,
+	})
+	host.IsStaticPort = true
+	logic.UpsertHost(host)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 	json.NewEncoder(w).Encode(apiNode)
 	go func() {
 	go func() {
@@ -138,6 +159,11 @@ func deleteGateway(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
+	host, err := logic.GetHost(node.HostID.String())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
 	node.IsGw = false
 	node.IsGw = false
 	logic.UpsertNode(&node)
 	logic.UpsertNode(&node)
 	logger.Log(1, r.Header.Get("user"), "deleted gw", nodeid, "on network", netid)
 	logger.Log(1, r.Header.Get("user"), "deleted gw", nodeid, "on network", netid)
@@ -200,7 +226,21 @@ func deleteGateway(w http.ResponseWriter, r *http.Request) {
 		}
 		}
 
 
 	}()
 	}()
-
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   node.ID.String(),
+			Name: host.Name,
+			Type: models.GatewaySub,
+		},
+		Origin: models.Dashboard,
+	})
 	logic.GetNodeStatus(&node, false)
 	logic.GetNodeStatus(&node, false)
 	apiNode := node.ConvertToAPINode()
 	apiNode := node.ConvertToAPINode()
 	logger.Log(1, r.Header.Get("user"), "deleted ingress gateway", nodeid)
 	logger.Log(1, r.Header.Get("user"), "deleted ingress gateway", nodeid)

+ 145 - 9
controllers/hosts.go

@@ -96,7 +96,21 @@ func upgradeHosts(w http.ResponseWriter, r *http.Request) {
 			}(host)
 			}(host)
 		}
 		}
 	}()
 	}()
-
+	logic.LogEvent(&models.Event{
+		Action: models.UpgradeAll,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   "All Hosts",
+			Name: "All Hosts",
+			Type: models.DeviceSub,
+		},
+		Origin: models.Dashboard,
+	})
 	slog.Info("upgrade all hosts request received", "user", user)
 	slog.Info("upgrade all hosts request received", "user", user)
 	logic.ReturnSuccessResponse(w, r, "upgrade all hosts request received")
 	logic.ReturnSuccessResponse(w, r, "upgrade all hosts request received")
 }
 }
@@ -209,14 +223,14 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	serverConf := servercfg.GetServerInfo()
+	serverConf := logic.GetServerInfo()
 	key, keyErr := logic.RetrievePublicTrafficKey()
 	key, keyErr := logic.RetrievePublicTrafficKey()
 	if keyErr != nil {
 	if keyErr != nil {
 		logger.Log(0, "error retrieving key:", keyErr.Error())
 		logger.Log(0, "error retrieving key:", keyErr.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(keyErr, "internal"))
 		return
 		return
 	}
 	}
-
+	_ = logic.CheckHostPorts(host)
 	serverConf.TrafficKey = key
 	serverConf.TrafficKey = key
 	response := models.HostPull{
 	response := models.HostPull{
 		Host:              *host,
 		Host:              *host,
@@ -230,7 +244,7 @@ func pull(w http.ResponseWriter, r *http.Request) {
 		ChangeDefaultGw:   hPU.ChangeDefaultGw,
 		ChangeDefaultGw:   hPU.ChangeDefaultGw,
 		DefaultGwIp:       hPU.DefaultGwIp,
 		DefaultGwIp:       hPU.DefaultGwIp,
 		IsInternetGw:      hPU.IsInternetGw,
 		IsInternetGw:      hPU.IsInternetGw,
-		EndpointDetection: servercfg.IsEndpointDetectionEnabled(),
+		EndpointDetection: logic.IsEndpointDetectionEnabled(),
 	}
 	}
 
 
 	logger.Log(1, hostID, "completed a pull")
 	logger.Log(1, hostID, "completed a pull")
@@ -294,7 +308,25 @@ func updateHost(w http.ResponseWriter, r *http.Request) {
 			}
 			}
 		}
 		}
 	}()
 	}()
-
+	logic.LogEvent(&models.Event{
+		Action: models.Update,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   currHost.ID.String(),
+			Name: newHost.Name,
+			Type: models.DeviceSub,
+		},
+		Diff: models.Diff{
+			Old: currHost,
+			New: newHost,
+		},
+		Origin: models.Dashboard,
+	})
 	apiHostData := newHost.ConvertNMHostToAPI()
 	apiHostData := newHost.ConvertNMHostToAPI()
 	logger.Log(2, r.Header.Get("user"), "updated host", newHost.ID.String())
 	logger.Log(2, r.Header.Get("user"), "updated host", newHost.ID.String())
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
@@ -420,7 +452,21 @@ func deleteHost(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   currHost.ID.String(),
+			Name: currHost.Name,
+			Type: models.DeviceSub,
+		},
+		Origin: models.Dashboard,
+	})
 	apiHostData := currHost.ConvertNMHostToAPI()
 	apiHostData := currHost.ConvertNMHostToAPI()
 	logger.Log(2, r.Header.Get("user"), "removed host", currHost.Name)
 	logger.Log(2, r.Header.Get("user"), "removed host", currHost.Name)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
@@ -492,6 +538,22 @@ func addHostToNetwork(w http.ResponseWriter, r *http.Request) {
 		r.Header.Get("user"),
 		r.Header.Get("user"),
 		fmt.Sprintf("added host %s to network %s", currHost.Name, network),
 		fmt.Sprintf("added host %s to network %s", currHost.Name, network),
 	)
 	)
+	logic.LogEvent(&models.Event{
+		Action: models.JoinHostToNet,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   currHost.ID.String(),
+			Name: currHost.Name,
+			Type: models.DeviceSub,
+		},
+		NetworkID: models.NetworkID(network),
+		Origin:    models.Dashboard,
+	})
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }
 
 
@@ -623,6 +685,22 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 			logic.SetDNS()
 			logic.SetDNS()
 		}
 		}
 	}()
 	}()
+	logic.LogEvent(&models.Event{
+		Action: models.RemoveHostFromNet,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   currHost.ID.String(),
+			Name: currHost.Name,
+			Type: models.DeviceSub,
+		},
+		NetworkID: models.NetworkID(network),
+		Origin:    models.Dashboard,
+	})
 	logger.Log(
 	logger.Log(
 		2,
 		2,
 		r.Header.Get("user"),
 		r.Header.Get("user"),
@@ -828,6 +906,21 @@ func updateAllKeys(w http.ResponseWriter, r *http.Request) {
 			}
 			}
 		}
 		}
 	}()
 	}()
+	logic.LogEvent(&models.Event{
+		Action: models.RefreshAllKeys,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   "All Devices",
+			Name: "All Devices",
+			Type: models.DeviceSub,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(2, r.Header.Get("user"), "updated keys for all hosts")
 	logger.Log(2, r.Header.Get("user"), "updated keys for all hosts")
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }
@@ -863,6 +956,21 @@ func updateKeys(w http.ResponseWriter, r *http.Request) {
 			logger.Log(0, "failed to send host key update", host.ID.String(), err.Error())
 			logger.Log(0, "failed to send host key update", host.ID.String(), err.Error())
 		}
 		}
 	}()
 	}()
+	logic.LogEvent(&models.Event{
+		Action: models.RefreshKey,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   host.ID.String(),
+			Name: host.Name,
+			Type: models.DeviceSub,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(2, r.Header.Get("user"), "updated key on host", host.Name)
 	logger.Log(2, r.Header.Get("user"), "updated key on host", host.Name)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }
@@ -901,7 +1009,21 @@ func syncHosts(w http.ResponseWriter, r *http.Request) {
 			time.Sleep(time.Millisecond * 100)
 			time.Sleep(time.Millisecond * 100)
 		}
 		}
 	}()
 	}()
-
+	logic.LogEvent(&models.Event{
+		Action: models.SyncAll,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   "All Devices",
+			Name: "All Devices",
+			Type: models.DeviceSub,
+		},
+		Origin: models.Dashboard,
+	})
 	slog.Info("sync all hosts request received", "user", user)
 	slog.Info("sync all hosts request received", "user", user)
 	logic.ReturnSuccessResponse(w, r, "sync all hosts request received")
 	logic.ReturnSuccessResponse(w, r, "sync all hosts request received")
 }
 }
@@ -937,7 +1059,21 @@ func syncHost(w http.ResponseWriter, r *http.Request) {
 			slog.Error("failed to send host pull request", "host", host.ID.String(), "error", err)
 			slog.Error("failed to send host pull request", "host", host.ID.String(), "error", err)
 		}
 		}
 	}()
 	}()
-
+	logic.LogEvent(&models.Event{
+		Action: models.Sync,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   host.ID.String(),
+			Name: host.Name,
+			Type: models.DeviceSub,
+		},
+		Origin: models.Dashboard,
+	})
 	slog.Info("requested host pull", "user", r.Header.Get("user"), "host", host.ID.String())
 	slog.Info("requested host pull", "user", r.Header.Get("user"), "host", host.ID.String())
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 }
 }

+ 4 - 1
controllers/middleware.go

@@ -50,8 +50,11 @@ func userMiddleWare(handler http.Handler) http.Handler {
 		if strings.Contains(route, "createrelay") || strings.Contains(route, "deleterelay") {
 		if strings.Contains(route, "createrelay") || strings.Contains(route, "deleterelay") {
 			r.Header.Set("TARGET_RSRC", models.RelayRsrc.String())
 			r.Header.Set("TARGET_RSRC", models.RelayRsrc.String())
 		}
 		}
-
 		if strings.Contains(route, "gateway") {
 		if strings.Contains(route, "gateway") {
+			r.Header.Set("TARGET_RSRC", models.GatewayRsrc.String())
+		}
+
+		if strings.Contains(route, "egress") {
 			r.Header.Set("TARGET_RSRC", models.EgressGwRsrc.String())
 			r.Header.Set("TARGET_RSRC", models.EgressGwRsrc.String())
 		}
 		}
 		if strings.Contains(route, "networks") {
 		if strings.Contains(route, "networks") {

+ 5 - 6
controllers/migrate.go

@@ -70,11 +70,11 @@ func migrate(w http.ResponseWriter, r *http.Request) {
 				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 				return
 				return
 			}
 			}
-			server = servercfg.GetServerInfo()
+			server = logic.GetServerInfo()
 			key, keyErr := logic.RetrievePublicTrafficKey()
 			key, keyErr := logic.RetrievePublicTrafficKey()
 			if keyErr != nil {
 			if keyErr != nil {
-				slog.Error("retrieving traffickey", "error", err)
-				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+				slog.Error("retrieving traffickey", "error", keyErr)
+				logic.ReturnErrorResponse(w, r, logic.FormatError(keyErr, "internal"))
 				return
 				return
 			}
 			}
 			server.TrafficKey = key
 			server.TrafficKey = key
@@ -134,7 +134,7 @@ func convertLegacyHostNode(legacy models.LegacyNode) (models.Host, models.Node)
 	host := models.Host{}
 	host := models.Host{}
 	host.ID = uuid.New()
 	host.ID = uuid.New()
 	host.IPForwarding = models.ParseBool(legacy.IPForwarding)
 	host.IPForwarding = models.ParseBool(legacy.IPForwarding)
-	host.AutoUpdate = servercfg.AutoUpdateEnabled()
+	host.AutoUpdate = logic.AutoUpdateEnabled()
 	host.Interface = "netmaker"
 	host.Interface = "netmaker"
 	host.ListenPort = int(legacy.ListenPort)
 	host.ListenPort = int(legacy.ListenPort)
 	if host.ListenPort == 0 {
 	if host.ListenPort == 0 {
@@ -207,8 +207,7 @@ func convertLegacyNode(legacy models.LegacyNode, hostID uuid.UUID) models.Node {
 	node.IsRelayed = false
 	node.IsRelayed = false
 	node.IsRelay = false
 	node.IsRelay = false
 	node.RelayedNodes = []string{}
 	node.RelayedNodes = []string{}
-	node.DNSOn = models.ParseBool(legacy.DNSOn)
-	node.LastModified = time.Now()
+	node.LastModified = time.Now().UTC()
 	node.ExpirationDateTime = time.Unix(legacy.ExpirationDateTime, 0)
 	node.ExpirationDateTime = time.Unix(legacy.ExpirationDateTime, 0)
 	node.EgressGatewayNatEnabled = models.ParseBool(legacy.EgressGatewayNatEnabled)
 	node.EgressGatewayNatEnabled = models.ParseBool(legacy.EgressGatewayNatEnabled)
 	node.EgressGatewayRequest = legacy.EgressGatewayRequest
 	node.EgressGatewayRequest = legacy.EgressGatewayRequest

+ 34 - 5
controllers/network.go

@@ -483,9 +483,9 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	err = logic.DeleteNetwork(network, force, doneCh)
 	err = logic.DeleteNetwork(network, force, doneCh)
 	if err != nil {
 	if err != nil {
-		errtype := "badrequest"
+		errtype := logic.BadReq
 		if strings.Contains(err.Error(), "Node check failed") {
 		if strings.Contains(err.Error(), "Node check failed") {
-			errtype = "forbidden"
+			errtype = logic.Forbidden
 		}
 		}
 		logger.Log(0, r.Header.Get("user"),
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to delete network [%s]: %v", network, err))
 			fmt.Sprintf("failed to delete network [%s]: %v", network, err))
@@ -514,6 +514,21 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 			logic.SetDNS()
 			logic.SetDNS()
 		}
 		}
 	}()
 	}()
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   network,
+			Name: network,
+			Type: models.NetworkSub,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(1, r.Header.Get("user"), "deleted network", network)
 	logger.Log(1, r.Header.Get("user"), "deleted network", network)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode("success")
 	json.NewEncoder(w).Encode("success")
@@ -588,8 +603,7 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 	logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(network.NetID))
 	logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(network.NetID))
 	logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID))
 	logic.CreateDefaultAclNetworkPolicies(models.NetworkID(network.NetID))
 	logic.CreateDefaultTags(models.NetworkID(network.NetID))
 	logic.CreateDefaultTags(models.NetworkID(network.NetID))
-
-	go logic.AddNetworkToAllocatedIpMap(network.NetID)
+	logic.AddNetworkToAllocatedIpMap(network.NetID)
 
 
 	go func() {
 	go func() {
 		defaultHosts := logic.GetDefaultHosts()
 		defaultHosts := logic.GetDefaultHosts()
@@ -637,7 +651,22 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 			logger.Log(1, "failed to publish peer update for default hosts after network is added")
 			logger.Log(1, "failed to publish peer update for default hosts after network is added")
 		}
 		}
 	}()
 	}()
-
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   network.NetID,
+			Name: network.NetID,
+			Type: models.NetworkSub,
+			Info: network,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(1, r.Header.Get("user"), "created network", network.NetID)
 	logger.Log(1, r.Header.Get("user"), "created network", network.NetID)
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(network)
 	json.NewEncoder(w).Encode(network)

+ 23 - 11
controllers/node.go

@@ -178,7 +178,7 @@ func Authorize(
 			// check if host instead of user
 			// check if host instead of user
 			if hostAllowed {
 			if hostAllowed {
 				// TODO --- should ensure that node is only operating on itself
 				// TODO --- should ensure that node is only operating on itself
-				if hostID, _, _, err := logic.VerifyHostToken(authToken); err == nil {
+				if hostID, macAddr, _, err := logic.VerifyHostToken(authToken); err == nil && macAddr != "" {
 					r.Header.Set(hostIDHeader, hostID)
 					r.Header.Set(hostIDHeader, hostID)
 					// this indicates request is from a node
 					// this indicates request is from a node
 					// used for failover - if a getNode comes from node, this will trigger a metrics wipe
 					// used for failover - if a getNode comes from node, this will trigger a metrics wipe
@@ -477,7 +477,7 @@ func getNode(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
 	}
 	}
-	server := servercfg.GetServerInfo()
+	server := logic.GetServerInfo()
 	response := models.NodeGet{
 	response := models.NodeGet{
 		Node:         node,
 		Node:         node,
 		Host:         *host,
 		Host:         *host,
@@ -516,7 +516,7 @@ func createEgressGateway(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 	gateway.NetID = params["network"]
 	gateway.NetID = params["network"]
 	gateway.NodeID = params["nodeid"]
 	gateway.NodeID = params["nodeid"]
-	err = logic.ValidateEgressRange(gateway)
+	err = logic.ValidateEgressRange(gateway.NetID, gateway.Ranges)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
 		logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
@@ -638,13 +638,6 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		)
 		)
 		return
 		return
 	}
 	}
-	if newNode.IsInternetGateway != currentNode.IsInternetGateway {
-		if newNode.IsInternetGateway {
-			logic.SetInternetGw(newNode, models.InetNodeReq{})
-		} else {
-			logic.UnsetInternetGw(newNode)
-		}
-	}
 	relayUpdate := logic.RelayUpdates(&currentNode, newNode)
 	relayUpdate := logic.RelayUpdates(&currentNode, newNode)
 	if relayUpdate && newNode.IsRelay {
 	if relayUpdate && newNode.IsRelay {
 		err = logic.ValidateRelay(models.RelayRequest{
 		err = logic.ValidateRelay(models.RelayRequest{
@@ -657,7 +650,7 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 			return
 			return
 		}
 		}
 	}
 	}
-	_, err = logic.GetHost(newNode.HostID.String())
+	host, err := logic.GetHost(newNode.HostID.String())
 	if err != nil {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"),
 		logger.Log(0, r.Header.Get("user"),
 			fmt.Sprintf("failed to get host for node  [ %s ] info: %v", nodeid, err))
 			fmt.Sprintf("failed to get host for node  [ %s ] info: %v", nodeid, err))
@@ -689,6 +682,25 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 		"on network",
 		"on network",
 		currentNode.Network,
 		currentNode.Network,
 	)
 	)
+	logic.LogEvent(&models.Event{
+		Action: models.Update,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   newNode.ID.String(),
+			Name: host.Name,
+			Type: models.NodeSub,
+		},
+		Diff: models.Diff{
+			Old: currentNode,
+			New: newNode,
+		},
+		Origin: models.Dashboard,
+	})
 	w.WriteHeader(http.StatusOK)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 	json.NewEncoder(w).Encode(apiNode)
 	go func(aclUpdate, relayupdate bool, newNode *models.Node) {
 	go func(aclUpdate, relayupdate bool, newNode *models.Node) {

+ 0 - 95
controllers/node_test.go

@@ -18,100 +18,6 @@ import (
 var nonLinuxHost models.Host
 var nonLinuxHost models.Host
 var linuxHost models.Host
 var linuxHost models.Host
 
 
-func TestCreateEgressGateway(t *testing.T) {
-	var gateway models.EgressGatewayRequest
-	gateway.Ranges = []string{"10.100.100.0/24"}
-	gateway.RangesWithMetric = append(gateway.RangesWithMetric, models.EgressRangeMetric{
-		Network:     "10.100.100.0/24",
-		RouteMetric: 256,
-	})
-	gateway.NetID = "skynet"
-	deleteAllNetworks()
-	createNet()
-	t.Run("NoNodes", func(t *testing.T) {
-		node, err := logic.CreateEgressGateway(gateway)
-		assert.Equal(t, models.Node{}, node)
-		assert.EqualError(t, err, "could not find any records")
-	})
-	t.Run("Non-linux node", func(t *testing.T) {
-		createnode := createNodeWithParams("", "")
-		createNodeHosts()
-		createnode.HostID = nonLinuxHost.ID
-		err := logic.AssociateNodeToHost(createnode, &nonLinuxHost)
-		assert.Nil(t, err)
-		gateway.NodeID = createnode.ID.String()
-		node, err := logic.CreateEgressGateway(gateway)
-		assert.Equal(t, models.Node{}, node)
-		assert.EqualError(t, err, "windows is unsupported for egress gateways")
-	})
-	t.Run("Success-Nat-Enabled", func(t *testing.T) {
-		deleteAllNodes()
-		testnode := createTestNode()
-		gateway.NodeID = testnode.ID.String()
-		gateway.NatEnabled = "yes"
-
-		node, err := logic.CreateEgressGateway(gateway)
-		t.Log(node.EgressGatewayNatEnabled)
-		assert.Nil(t, err)
-	})
-	t.Run("Success-Nat-Disabled", func(t *testing.T) {
-		deleteAllNodes()
-		testnode := createTestNode()
-		gateway.NodeID = testnode.ID.String()
-		gateway.NatEnabled = "no"
-
-		node, err := logic.CreateEgressGateway(gateway)
-		t.Log(node.EgressGatewayNatEnabled)
-		assert.Nil(t, err)
-	})
-	t.Run("Success", func(t *testing.T) {
-		var gateway models.EgressGatewayRequest
-		gateway.Ranges = []string{"10.100.100.0/24"}
-		gateway.NetID = "skynet"
-		deleteAllNodes()
-		testnode := createTestNode()
-		gateway.NodeID = testnode.ID.String()
-
-		node, err := logic.CreateEgressGateway(gateway)
-		t.Log(node)
-		assert.Nil(t, err)
-		assert.Equal(t, true, node.IsEgressGateway)
-		assert.Equal(t, gateway.Ranges, node.EgressGatewayRanges)
-	})
-
-}
-func TestDeleteEgressGateway(t *testing.T) {
-	var gateway models.EgressGatewayRequest
-	deleteAllNetworks()
-	createNet()
-	testnode := createTestNode()
-	gateway.Ranges = []string{"10.100.100.0/24"}
-	gateway.NetID = "skynet"
-	gateway.NodeID = testnode.ID.String()
-	t.Run("Success", func(t *testing.T) {
-		node, err := logic.CreateEgressGateway(gateway)
-		assert.Nil(t, err)
-		assert.Equal(t, true, node.IsEgressGateway)
-		assert.Equal(t, []string{"10.100.100.0/24"}, node.EgressGatewayRanges)
-		node, err = logic.DeleteEgressGateway(gateway.NetID, gateway.NodeID)
-		assert.Nil(t, err)
-		assert.Equal(t, false, node.IsEgressGateway)
-		assert.Equal(t, []string([]string{}), node.EgressGatewayRanges)
-	})
-	t.Run("NotGateway", func(t *testing.T) {
-		node, err := logic.DeleteEgressGateway(gateway.NetID, gateway.NodeID)
-		assert.Nil(t, err)
-		assert.Equal(t, false, node.IsEgressGateway)
-		assert.Equal(t, []string([]string{}), node.EgressGatewayRanges)
-	})
-	t.Run("BadNode", func(t *testing.T) {
-		node, err := logic.DeleteEgressGateway(gateway.NetID, "01:02:03")
-		assert.EqualError(t, err, "no result found")
-		assert.Equal(t, models.Node{}, node)
-		deleteAllNodes()
-	})
-}
-
 func TestGetNetworkNodes(t *testing.T) {
 func TestGetNetworkNodes(t *testing.T) {
 	deleteAllNetworks()
 	deleteAllNetworks()
 	createNet()
 	createNet()
@@ -237,7 +143,6 @@ func createNodeWithParams(network, address string) *models.Node {
 		ID:      uuid.New(),
 		ID:      uuid.New(),
 		Network: "skynet",
 		Network: "skynet",
 		Address: *ipnet,
 		Address: *ipnet,
-		DNSOn:   true,
 	}
 	}
 	if len(network) > 0 {
 	if len(network) > 0 {
 		tmpCNode.Network = network
 		tmpCNode.Network = network

+ 91 - 2
controllers/server.go

@@ -2,6 +2,7 @@ package controller
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
+	"errors"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
 	"strings"
 	"strings"
@@ -12,6 +13,7 @@ import (
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"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"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
@@ -41,6 +43,10 @@ func serverHandlers(r *mux.Router) {
 	).Methods(http.MethodPost)
 	).Methods(http.MethodPost)
 	r.HandleFunc("/api/server/getconfig", allowUsers(http.HandlerFunc(getConfig))).
 	r.HandleFunc("/api/server/getconfig", allowUsers(http.HandlerFunc(getConfig))).
 		Methods(http.MethodGet)
 		Methods(http.MethodGet)
+	r.HandleFunc("/api/server/settings", allowUsers(http.HandlerFunc(getSettings))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/server/settings", logic.SecurityCheck(true, http.HandlerFunc(updateSettings))).
+		Methods(http.MethodPut)
 	r.HandleFunc("/api/server/getserverinfo", logic.SecurityCheck(true, http.HandlerFunc(getServerInfo))).
 	r.HandleFunc("/api/server/getserverinfo", logic.SecurityCheck(true, http.HandlerFunc(getServerInfo))).
 		Methods(http.MethodGet)
 		Methods(http.MethodGet)
 	r.HandleFunc("/api/server/status", getStatus).Methods(http.MethodGet)
 	r.HandleFunc("/api/server/status", getStatus).Methods(http.MethodGet)
@@ -207,7 +213,7 @@ func getServerInfo(w http.ResponseWriter, r *http.Request) {
 
 
 	// get params
 	// get params
 
 
-	json.NewEncoder(w).Encode(servercfg.GetServerInfo())
+	json.NewEncoder(w).Encode(logic.GetServerInfo())
 	// w.WriteHeader(http.StatusOK)
 	// w.WriteHeader(http.StatusOK)
 }
 }
 
 
@@ -222,7 +228,7 @@ func getConfig(w http.ResponseWriter, r *http.Request) {
 
 
 	// get params
 	// get params
 
 
-	scfg := servercfg.GetServerConfig()
+	scfg := logic.GetServerConfig()
 	scfg.IsPro = "no"
 	scfg.IsPro = "no"
 	if servercfg.IsPro {
 	if servercfg.IsPro {
 		scfg.IsPro = "yes"
 		scfg.IsPro = "yes"
@@ -230,3 +236,86 @@ func getConfig(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(scfg)
 	json.NewEncoder(w).Encode(scfg)
 	// w.WriteHeader(http.StatusOK)
 	// w.WriteHeader(http.StatusOK)
 }
 }
+
+// @Summary     Get the server settings
+// @Router      /api/server/settings [get]
+// @Tags        Server
+// @Security    oauth2
+// @Success     200 {object} config.ServerSettings
+func getSettings(w http.ResponseWriter, r *http.Request) {
+	scfg := logic.GetServerSettings()
+	scfg.ClientSecret = logic.Mask()
+	logic.ReturnSuccessResponseWithJson(w, r, scfg, "fetched server settings successfully")
+}
+
+// @Summary     Update the server settings
+// @Router      /api/server/settings [put]
+// @Tags        Server
+// @Security    oauth2
+// @Success     200 {object} config.ServerSettings
+func updateSettings(w http.ResponseWriter, r *http.Request) {
+	var req models.ServerSettings
+	force := r.URL.Query().Get("force")
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		logger.Log(0, r.Header.Get("user"), "error decoding request body: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if !logic.ValidateNewSettings(req) {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid settings"), "badrequest"))
+		return
+	}
+	currSettings := logic.GetServerSettings()
+	err := logic.UpsertServerSettings(req)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to udpate server settings "+err.Error()), "internal"))
+		return
+	}
+	logic.LogEvent(&models.Event{
+		Action: models.Update,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   models.SettingSub.String(),
+			Name: models.SettingSub.String(),
+			Type: models.SettingSub,
+		},
+		Diff: models.Diff{
+			Old: currSettings,
+			New: req,
+		},
+		Origin: models.Dashboard,
+	})
+	go reInit(currSettings, req, force == "true")
+	logic.ReturnSuccessResponseWithJson(w, r, req, "updated server settings successfully")
+}
+
+func reInit(curr, new models.ServerSettings, force bool) {
+	logic.SettingsMutex.Lock()
+	defer logic.SettingsMutex.Unlock()
+	logic.ResetAuthProvider()
+	logic.EmailInit()
+	logic.SetVerbosity(int(logic.GetServerSettings().Verbosity))
+	logic.ResetIDPSyncHook()
+	// check if auto update is changed
+	if force {
+		if curr.NetclientAutoUpdate != new.NetclientAutoUpdate {
+			// update all hosts
+			hosts, _ := logic.GetAllHosts()
+			for _, host := range hosts {
+				host.AutoUpdate = new.NetclientAutoUpdate
+				logic.UpsertHost(&host)
+				mq.HostUpdate(&models.HostUpdate{
+					Action: models.UpdateHost,
+					Host:   host,
+				})
+			}
+		}
+	}
+	go mq.PublishPeerUpdate(false)
+
+}

+ 429 - 39
controllers/user.go

@@ -6,7 +6,9 @@ import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
 	"reflect"
 	"reflect"
+	"time"
 
 
+	"github.com/google/uuid"
 	"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"
@@ -14,6 +16,7 @@ import (
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 )
 )
@@ -34,14 +37,19 @@ func userHandlers(r *mux.Router) {
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceUsers, http.HandlerFunc(createUser)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceUsers, http.HandlerFunc(createUser)))).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(deleteUser))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(deleteUser))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser)))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser)))).Methods(http.MethodGet)
+	r.HandleFunc("/api/users/{username}/enable", logic.SecurityCheck(true, http.HandlerFunc(enableUserAccount))).Methods(http.MethodPost)
+	r.HandleFunc("/api/users/{username}/disable", logic.SecurityCheck(true, http.HandlerFunc(disableUserAccount))).Methods(http.MethodPost)
 	r.HandleFunc("/api/v1/users", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserV1)))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/users", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserV1)))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users", logic.SecurityCheck(true, http.HandlerFunc(getUsers))).Methods(http.MethodGet)
 	r.HandleFunc("/api/users", logic.SecurityCheck(true, http.HandlerFunc(getUsers))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/users/roles", logic.SecurityCheck(true, http.HandlerFunc(ListRoles))).Methods(http.MethodGet)
 	r.HandleFunc("/api/v1/users/roles", logic.SecurityCheck(true, http.HandlerFunc(ListRoles))).Methods(http.MethodGet)
-
+	r.HandleFunc("/api/v1/users/access_token", logic.SecurityCheck(true, http.HandlerFunc(createUserAccessToken))).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/users/access_token", logic.SecurityCheck(true, http.HandlerFunc(getUserAccessTokens))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/users/access_token", logic.SecurityCheck(true, http.HandlerFunc(deleteUserAccessTokens))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/users/logout", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(logout)))).Methods(http.MethodPost)
 }
 }
 
 
 // @Summary     Authenticate a user to retrieve an authorization token
 // @Summary     Authenticate a user to retrieve an authorization token
-// @Router      /api/users/adm/authenticate [post]
+// @Router      /api/v1/users/{username}/access_token [post]
 // @Tags        Auth
 // @Tags        Auth
 // @Accept      json
 // @Accept      json
 // @Param       body body models.UserAuthParams true "Authentication parameters"
 // @Param       body body models.UserAuthParams true "Authentication parameters"
@@ -49,24 +57,201 @@ func userHandlers(r *mux.Router) {
 // @Failure     400 {object} models.ErrorResponse
 // @Failure     400 {object} models.ErrorResponse
 // @Failure     401 {object} models.ErrorResponse
 // @Failure     401 {object} models.ErrorResponse
 // @Failure     500 {object} models.ErrorResponse
 // @Failure     500 {object} models.ErrorResponse
-func authenticateUser(response http.ResponseWriter, request *http.Request) {
+func createUserAccessToken(w http.ResponseWriter, r *http.Request) {
 
 
 	// Auth request consists of Mac Address and Password (from node that is authorizing
 	// Auth request consists of Mac Address and Password (from node that is authorizing
 	// in case of Master, auth is ignored and mac is set to "mastermac"
 	// in case of Master, auth is ignored and mac is set to "mastermac"
-	var authRequest models.UserAuthParams
-	var errorResponse = models.ErrorResponse{
-		Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
+	var req schema.UserAccessToken
+
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
+		return
+	}
+	if req.Name == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("name is required"), logic.BadReq))
+		return
+	}
+	if req.UserName == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), logic.BadReq))
+		return
+	}
+	caller, err := logic.GetUser(r.Header.Get("user"))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.UnAuthorized))
+		return
+	}
+	user, err := logic.GetUser(req.UserName)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.UnAuthorized))
+		return
+	}
+	if caller.UserName != user.UserName && caller.PlatformRoleID != models.SuperAdminRole {
+		if caller.PlatformRoleID == models.AdminRole {
+			if user.PlatformRoleID == models.SuperAdminRole || user.PlatformRoleID == models.AdminRole {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("not enough permissions to create token for user "+user.UserName), logic.Forbidden_Msg))
+				return
+			}
+		} else {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("not enough permissions to create token for user "+user.UserName), logic.Forbidden_Msg))
+			return
+		}
 	}
 	}
 
 
-	if !servercfg.IsBasicAuthEnabled() {
+	req.ID = uuid.New().String()
+	req.CreatedBy = r.Header.Get("user")
+	req.CreatedAt = time.Now()
+	jwt, err := logic.CreateUserAccessJwtToken(user.UserName, user.PlatformRoleID, req.ExpiresAt, req.ID)
+	if jwt == "" {
+		// very unlikely that err is !nil and no jwt returned, but handle it anyways.
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
-			response,
-			request,
-			logic.FormatError(fmt.Errorf("basic auth is disabled"), "badrequest"),
+			w,
+			r,
+			logic.FormatError(errors.New("error creating access token "+err.Error()), logic.Internal),
+		)
+		return
+	}
+	err = req.Create(r.Context())
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error creating access token "+err.Error()), logic.Internal),
+		)
+		return
+	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		Source: models.Subject{
+			ID:   caller.UserName,
+			Name: caller.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: caller.UserName,
+		Target: models.Subject{
+			ID:   req.ID,
+			Name: req.Name,
+			Type: models.UserAccessTokenSub,
+			Info: req,
+		},
+		Origin: models.Dashboard,
+	})
+	logic.ReturnSuccessResponseWithJson(w, r, models.SuccessfulUserLoginResponse{
+		AuthToken: jwt,
+		UserName:  req.UserName,
+	}, "api access token has generated for user "+req.UserName)
+}
+
+// @Summary     Authenticate a user to retrieve an authorization token
+// @Router      /api/v1/users/{username}/access_token [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.UserAuthParams true "Authentication parameters"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func getUserAccessTokens(w http.ResponseWriter, r *http.Request) {
+	username := r.URL.Query().Get("username")
+	if username == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), "badrequest"))
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, (&schema.UserAccessToken{UserName: username}).ListByUser(r.Context()), "fetched api access tokens for user "+username)
+}
+
+// @Summary     Authenticate a user to retrieve an authorization token
+// @Router      /api/v1/users/{username}/access_token [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.UserAuthParams true "Authentication parameters"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) {
+	id := r.URL.Query().Get("id")
+	if id == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("id is required"), "badrequest"))
+		return
+	}
+	a := schema.UserAccessToken{
+		ID: id,
+	}
+	err := a.Get(r.Context())
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("token does not exist"), "badrequest"))
+		return
+	}
+	caller, err := logic.GetUser(r.Header.Get("user"))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
+		return
+	}
+	user, err := logic.GetUser(a.UserName)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
+		return
+	}
+	if caller.UserName != user.UserName && caller.PlatformRoleID != models.SuperAdminRole {
+		if caller.PlatformRoleID == models.AdminRole {
+			if user.PlatformRoleID == models.SuperAdminRole || user.PlatformRoleID == models.AdminRole {
+				logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("not enough permissions to delete token of user "+user.UserName), logic.Forbidden_Msg))
+				return
+			}
+		} else {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("not enough permissions to delete token of user "+user.UserName), logic.Forbidden_Msg))
+			return
+		}
+	}
+
+	err = (&schema.UserAccessToken{ID: id}).Delete(r.Context())
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error deleting access token "+err.Error()), "internal"),
 		)
 		)
 		return
 		return
 	}
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		Source: models.Subject{
+			ID:   caller.UserName,
+			Name: caller.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: caller.UserName,
+		Target: models.Subject{
+			ID:   a.ID,
+			Name: a.Name,
+			Type: models.UserAccessTokenSub,
+			Info: a,
+		},
+		Origin: models.Dashboard,
+	})
+	logic.ReturnSuccessResponseWithJson(w, r, nil, "revoked access token")
+}
+
+// @Summary     Authenticate a user to retrieve an authorization token
+// @Router      /api/users/adm/authenticate [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.UserAuthParams true "Authentication parameters"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func authenticateUser(response http.ResponseWriter, request *http.Request) {
 
 
+	// Auth request consists of Mac Address and Password (from node that is authorizing
+	// in case of Master, auth is ignored and mac is set to "mastermac"
+	var authRequest models.UserAuthParams
+	var errorResponse = models.ErrorResponse{
+		Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
+	}
 	decoder := json.NewDecoder(request.Body)
 	decoder := json.NewDecoder(request.Body)
 	decoderErr := decoder.Decode(&authRequest)
 	decoderErr := decoder.Decode(&authRequest)
 	defer request.Body.Close()
 	defer request.Body.Close()
@@ -76,15 +261,36 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 		logic.ReturnErrorResponse(response, request, errorResponse)
 		logic.ReturnErrorResponse(response, request, errorResponse)
 		return
 		return
 	}
 	}
+	user, err := logic.GetUser(authRequest.UserName)
+	if err != nil {
+		logger.Log(0, authRequest.UserName, "user validation failed: ",
+			err.Error())
+		logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
+		return
+	}
+	if logic.IsOauthUser(user) == nil {
+		logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("user is registered via SSO"), "badrequest"))
+		return
+	}
+
+	if user.AccountDisabled {
+		err = errors.New("user account disabled")
+		logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
+		return
+	}
+
+	if !user.IsSuperAdmin && !logic.IsBasicAuthEnabled() {
+		logic.ReturnErrorResponse(
+			response,
+			request,
+			logic.FormatError(fmt.Errorf("basic auth is disabled"), "badrequest"),
+		)
+		return
+	}
+
 	if val := request.Header.Get("From-Ui"); val == "true" {
 	if val := request.Header.Get("From-Ui"); val == "true" {
 		// request came from UI, if normal user block Login
 		// request came from UI, if normal user block Login
-		user, err := logic.GetUser(authRequest.UserName)
-		if err != nil {
-			logger.Log(0, authRequest.UserName, "user validation failed: ",
-				err.Error())
-			logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
-			return
-		}
+
 		role, err := logic.GetRole(user.PlatformRoleID)
 		role, err := logic.GetRole(user.PlatformRoleID)
 		if err != nil {
 		if err != nil {
 			logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
 			logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
@@ -94,16 +300,40 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 			logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
 			logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
 			return
 			return
 		}
 		}
+		// log user activity
+		logic.LogEvent(&models.Event{
+			Action: models.Login,
+			Source: models.Subject{
+				ID:   user.UserName,
+				Name: user.UserName,
+				Type: models.UserSub,
+			},
+			TriggeredBy: user.UserName,
+			Target: models.Subject{
+				ID:   models.DashboardSub.String(),
+				Name: models.DashboardSub.String(),
+				Type: models.DashboardSub,
+			},
+			Origin: models.Dashboard,
+		})
+	} else {
+		logic.LogEvent(&models.Event{
+			Action: models.Login,
+			Source: models.Subject{
+				ID:   user.UserName,
+				Name: user.UserName,
+				Type: models.UserSub,
+			},
+			TriggeredBy: user.UserName,
+			Target: models.Subject{
+				ID:   models.ClientAppSub.String(),
+				Name: models.ClientAppSub.String(),
+				Type: models.ClientAppSub,
+			},
+			Origin: models.ClientApp,
+		})
 	}
 	}
-	user, err := logic.GetUser(authRequest.UserName)
-	if err != nil {
-		logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
-		return
-	}
-	if logic.IsOauthUser(user) == nil {
-		logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("user is registered via SSO"), "badrequest"))
-		return
-	}
+
 	username := authRequest.UserName
 	username := authRequest.UserName
 	jwt, err := logic.VerifyAuthRequest(authRequest)
 	jwt, err := logic.VerifyAuthRequest(authRequest)
 	if err != nil {
 	if err != nil {
@@ -145,7 +375,7 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 	response.Write(successJSONResponse)
 	response.Write(successJSONResponse)
 
 
 	go func() {
 	go func() {
-		if servercfg.IsPro && servercfg.GetRacAutoDisable() {
+		if servercfg.IsPro && logic.GetRacAutoDisable() {
 			// enable all associeated clients for the user
 			// enable all associeated clients for the user
 			clients, err := logic.GetAllExtClients()
 			clients, err := logic.GetAllExtClients()
 			if err != nil {
 			if err != nil {
@@ -225,6 +455,65 @@ func getUser(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(user)
 	json.NewEncoder(w).Encode(user)
 }
 }
 
 
+// @Summary     Enable a user's account
+// @Router      /api/users/{username}/enable [post]
+// @Tags        Users
+// @Param       username path string true "Username of the user to enable"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func enableUserAccount(w http.ResponseWriter, r *http.Request) {
+	username := mux.Vars(r)["username"]
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logger.Log(0, "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	user.AccountDisabled = false
+	err = logic.UpsertUser(*user)
+	if err != nil {
+		logger.Log(0, "failed to enable user account: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+	}
+
+	logic.ReturnSuccessResponse(w, r, "user account enabled")
+}
+
+// @Summary     Disable a user's account
+// @Router      /api/users/{username}/disable [post]
+// @Tags        Users
+// @Param       username path string true "Username of the user to disable"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func disableUserAccount(w http.ResponseWriter, r *http.Request) {
+	username := mux.Vars(r)["username"]
+	user, err := logic.GetUser(username)
+	if err != nil {
+		logger.Log(0, "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+
+	if user.PlatformRoleID == models.SuperAdminRole {
+		err = errors.New("cannot disable super-admin user account")
+		logger.Log(0, err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	user.AccountDisabled = true
+	err = logic.UpsertUser(*user)
+	if err != nil {
+		logger.Log(0, "failed to disable user account: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+	}
+
+	logic.ReturnSuccessResponse(w, r, "user account disabled")
+}
+
 // swagger:route GET /api/v1/users user getUserV1
 // swagger:route GET /api/v1/users user getUserV1
 //
 //
 // Get an individual user with role info.
 // Get an individual user with role info.
@@ -319,7 +608,7 @@ func createSuperAdmin(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	if !servercfg.IsBasicAuthEnabled() {
+	if !logic.IsBasicAuthEnabled() {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
 			r,
 			r,
@@ -367,7 +656,7 @@ func transferSuperAdmin(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only admins can be promoted to superadmin role"), "forbidden"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only admins can be promoted to superadmin role"), "forbidden"))
 		return
 		return
 	}
 	}
-	if !servercfg.IsBasicAuthEnabled() {
+	if !logic.IsBasicAuthEnabled() {
 		logic.ReturnErrorResponse(
 		logic.ReturnErrorResponse(
 			w,
 			w,
 			r,
 			r,
@@ -421,6 +710,10 @@ func createUser(w http.ResponseWriter, r *http.Request) {
 	if !servercfg.IsPro {
 	if !servercfg.IsPro {
 		user.PlatformRoleID = models.AdminRole
 		user.PlatformRoleID = models.AdminRole
 	}
 	}
+	if user.UserName == logic.MasterUser {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username not allowed"), "badrequest"))
+		return
+	}
 
 
 	if user.PlatformRoleID == "" {
 	if user.PlatformRoleID == "" {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("platform role is missing"), "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("platform role is missing"), "badrequest"))
@@ -458,6 +751,22 @@ func createUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		Source: models.Subject{
+			ID:   caller.UserName,
+			Name: caller.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: caller.UserName,
+		Target: models.Subject{
+			ID:   user.UserName,
+			Name: user.UserName,
+			Type: models.UserSub,
+			Info: user,
+		},
+		Origin: models.Dashboard,
+	})
 	logic.DeleteUserInvite(user.UserName)
 	logic.DeleteUserInvite(user.UserName)
 	logic.DeletePendingUser(user.UserName)
 	logic.DeletePendingUser(user.UserName)
 	go mq.PublishPeerUpdate(false)
 	go mq.PublishPeerUpdate(false)
@@ -534,12 +843,12 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 			return
 			return
 		}
 		}
 		if caller.PlatformRoleID == models.AdminRole && user.PlatformRoleID == models.AdminRole {
 		if caller.PlatformRoleID == models.AdminRole && user.PlatformRoleID == models.AdminRole {
-			slog.Error("admin user cannot update another admin", "caller", caller.UserName, "attempted to update admin user", username)
-			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("admin user cannot update another admin"), "forbidden"))
+			slog.Error("an admin user does not have permissions to update another admin user", "caller", caller.UserName, "attempted to update admin user", username)
+			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("an admin user does not have permissions to update another admin user"), "forbidden"))
 			return
 			return
 		}
 		}
 		if caller.PlatformRoleID == models.AdminRole && userchange.PlatformRoleID == models.AdminRole {
 		if caller.PlatformRoleID == models.AdminRole && userchange.PlatformRoleID == models.AdminRole {
-			err = errors.New("admin user cannot update role of an another user to admin")
+			err = errors.New("an admin user does not have permissions to assign the admin role to another user")
 			slog.Error(
 			slog.Error(
 				"failed to update user",
 				"failed to update user",
 				"caller",
 				"caller",
@@ -592,7 +901,30 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
 		return
 		return
 	}
 	}
-
+	logic.AddGlobalNetRolesToAdmins(&userchange)
+	if userchange.PlatformRoleID != user.PlatformRoleID || !logic.CompareMaps(user.UserGroups, userchange.UserGroups) {
+		(&schema.UserAccessToken{UserName: user.UserName}).DeleteAllUserTokens(r.Context())
+	}
+	oldUser := *user
+	e := models.Event{
+		Action: models.Update,
+		Source: models.Subject{
+			ID:   caller.UserName,
+			Name: caller.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: caller.UserName,
+		Target: models.Subject{
+			ID:   user.UserName,
+			Name: user.UserName,
+			Type: models.UserSub,
+		},
+		Diff: models.Diff{
+			Old: oldUser,
+			New: userchange,
+		},
+		Origin: models.Dashboard,
+	}
 	user, err = logic.UpdateUser(&userchange, user)
 	user, err = logic.UpdateUser(&userchange, user)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, username,
 		logger.Log(0, username,
@@ -600,6 +932,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 		return
 	}
 	}
+	logic.LogEvent(&e)
 	go mq.PublishPeerUpdate(false)
 	go mq.PublishPeerUpdate(false)
 	logger.Log(1, username, "was updated")
 	logger.Log(1, username, "was updated")
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
@@ -671,18 +1004,28 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 			return
 			return
 		}
 		}
 	}
 	}
-	success, err := logic.DeleteUser(username)
+	err = logic.DeleteUser(username)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, username,
 		logger.Log(0, username,
 			"failed to delete user: ", err.Error())
 			"failed to delete user: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 		return
-	} else if !success {
-		err := errors.New("delete unsuccessful")
-		logger.Log(0, username, err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
-		return
 	}
 	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		Source: models.Subject{
+			ID:   caller.UserName,
+			Name: caller.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: caller.UserName,
+		Target: models.Subject{
+			ID:   user.UserName,
+			Name: user.UserName,
+			Type: models.UserSub,
+		},
+		Origin: models.Dashboard,
+	})
 	// check and delete extclient with this ownerID
 	// check and delete extclient with this ownerID
 	go func() {
 	go func() {
 		extclients, err := logic.GetAllExtClients()
 		extclients, err := logic.GetAllExtClients()
@@ -748,3 +1091,50 @@ func listRoles(w http.ResponseWriter, r *http.Request) {
 
 
 	logic.ReturnSuccessResponseWithJson(w, r, roles, "successfully fetched user roles permission templates")
 	logic.ReturnSuccessResponseWithJson(w, r, roles, "successfully fetched user roles permission templates")
 }
 }
+
+// swagger:route POST /api/v1/user/logout user logout
+//
+// LogOut user.
+//
+//			Schemes: https
+//
+//			Security:
+//	  		oauth
+//
+//			Responses:
+//				200: userBodyResponse
+func logout(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+	userName := r.URL.Query().Get("username")
+	user, err := logic.GetUser(userName)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
+		return
+	}
+	var target models.SubjectType
+	if val := r.Header.Get("From-Ui"); val == "true" {
+		target = models.DashboardSub
+	} else {
+		target = models.ClientAppSub
+	}
+	if target != "" {
+		logic.LogEvent(&models.Event{
+			Action: models.LogOut,
+			Source: models.Subject{
+				ID:   user.UserName,
+				Name: user.UserName,
+				Type: models.UserSub,
+			},
+			TriggeredBy: user.UserName,
+			Target: models.Subject{
+				ID:   target.String(),
+				Name: target.String(),
+				Type: target,
+			},
+			Origin: models.Origin(target),
+		})
+	}
+
+	logic.ReturnSuccessResponse(w, r, "user logged out")
+}

+ 37 - 90
database/database.go

@@ -1,18 +1,12 @@
 package database
 package database
 
 
 import (
 import (
-	"crypto/rand"
-	"encoding/json"
 	"errors"
 	"errors"
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
-	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
-	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
-	"golang.org/x/crypto/nacl/box"
 )
 )
 
 
 const (
 const (
@@ -73,6 +67,8 @@ const (
 	TAG_TABLE_NAME = "tags"
 	TAG_TABLE_NAME = "tags"
 	// PEER_ACK_TABLE - table for failover peer ack
 	// PEER_ACK_TABLE - table for failover peer ack
 	PEER_ACK_TABLE = "peer_ack"
 	PEER_ACK_TABLE = "peer_ack"
+	// SERVER_SETTINGS - table for server settings
+	SERVER_SETTINGS = "server_settings"
 	// == 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"
@@ -102,6 +98,36 @@ const (
 
 
 var dbMutex sync.RWMutex
 var dbMutex sync.RWMutex
 
 
+var Tables = []string{
+	NETWORKS_TABLE_NAME,
+	NODES_TABLE_NAME,
+	CERTS_TABLE_NAME,
+	DELETED_NODES_TABLE_NAME,
+	USERS_TABLE_NAME,
+	DNS_TABLE_NAME,
+	EXT_CLIENT_TABLE_NAME,
+	PEERS_TABLE_NAME,
+	SERVERCONF_TABLE_NAME,
+	SERVER_UUID_TABLE_NAME,
+	GENERATED_TABLE_NAME,
+	NODE_ACLS_TABLE_NAME,
+	SSO_STATE_CACHE,
+	METRICS_TABLE_NAME,
+	NETWORK_USER_TABLE_NAME,
+	USER_GROUPS_TABLE_NAME,
+	CACHE_TABLE_NAME,
+	HOSTS_TABLE_NAME,
+	ENROLLMENT_KEYS_TABLE_NAME,
+	HOST_ACTIONS_TABLE_NAME,
+	PENDING_USERS_TABLE_NAME,
+	USER_PERMISSIONS_TABLE_NAME,
+	USER_INVITES_TABLE_NAME,
+	TAG_TABLE_NAME,
+	ACLS_TABLE_NAME,
+	PEER_ACK_TABLE,
+	SERVER_SETTINGS,
+}
+
 func getCurrentDB() map[string]interface{} {
 func getCurrentDB() map[string]interface{} {
 	switch servercfg.GetDB() {
 	switch servercfg.GetDB() {
 	case "rqlite":
 	case "rqlite":
@@ -131,71 +157,30 @@ func InitializeDatabase() error {
 		time.Sleep(2 * time.Second)
 		time.Sleep(2 * time.Second)
 	}
 	}
 	createTables()
 	createTables()
-	return initializeUUID()
+	return nil
 }
 }
 
 
 func createTables() {
 func createTables() {
-	CreateTable(NETWORKS_TABLE_NAME)
-	CreateTable(NODES_TABLE_NAME)
-	CreateTable(CERTS_TABLE_NAME)
-	CreateTable(DELETED_NODES_TABLE_NAME)
-	CreateTable(USERS_TABLE_NAME)
-	CreateTable(DNS_TABLE_NAME)
-	CreateTable(EXT_CLIENT_TABLE_NAME)
-	CreateTable(PEERS_TABLE_NAME)
-	CreateTable(SERVERCONF_TABLE_NAME)
-	CreateTable(SERVER_UUID_TABLE_NAME)
-	CreateTable(GENERATED_TABLE_NAME)
-	CreateTable(NODE_ACLS_TABLE_NAME)
-	CreateTable(SSO_STATE_CACHE)
-	CreateTable(METRICS_TABLE_NAME)
-	CreateTable(NETWORK_USER_TABLE_NAME)
-	CreateTable(USER_GROUPS_TABLE_NAME)
-	CreateTable(CACHE_TABLE_NAME)
-	CreateTable(HOSTS_TABLE_NAME)
-	CreateTable(ENROLLMENT_KEYS_TABLE_NAME)
-	CreateTable(HOST_ACTIONS_TABLE_NAME)
-	CreateTable(PENDING_USERS_TABLE_NAME)
-	CreateTable(USER_PERMISSIONS_TABLE_NAME)
-	CreateTable(USER_INVITES_TABLE_NAME)
-	CreateTable(TAG_TABLE_NAME)
-	CreateTable(ACLS_TABLE_NAME)
-	CreateTable(PEER_ACK_TABLE)
+	for _, table := range Tables {
+		_ = CreateTable(table)
+	}
 }
 }
 
 
 func CreateTable(tableName string) error {
 func CreateTable(tableName string) error {
 	return getCurrentDB()[CREATE_TABLE].(func(string) error)(tableName)
 	return getCurrentDB()[CREATE_TABLE].(func(string) error)(tableName)
 }
 }
 
 
-// IsJSONString - checks if valid json
-func IsJSONString(value string) bool {
-	var jsonInt interface{}
-	var nodeInt models.Node
-	return json.Unmarshal([]byte(value), &jsonInt) == nil || json.Unmarshal([]byte(value), &nodeInt) == nil
-}
-
 // Insert - inserts object into db
 // Insert - inserts object into db
 func Insert(key string, value string, tableName string) error {
 func Insert(key string, value string, tableName string) error {
 	dbMutex.Lock()
 	dbMutex.Lock()
 	defer dbMutex.Unlock()
 	defer dbMutex.Unlock()
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		return getCurrentDB()[INSERT].(func(string, string, string) error)(key, value, tableName)
 		return getCurrentDB()[INSERT].(func(string, string, string) error)(key, value, tableName)
 	} else {
 	} else {
 		return errors.New("invalid insert " + key + " : " + value)
 		return errors.New("invalid insert " + key + " : " + value)
 	}
 	}
 }
 }
 
 
-// InsertPeer - inserts peer into db
-func InsertPeer(key string, value string) error {
-	dbMutex.Lock()
-	defer dbMutex.Unlock()
-	if key != "" && value != "" && IsJSONString(value) {
-		return getCurrentDB()[INSERT_PEER].(func(string, string) error)(key, value)
-	} else {
-		return errors.New("invalid peer insert " + key + " : " + value)
-	}
-}
-
 // DeleteRecord - deletes a record from db
 // DeleteRecord - deletes a record from db
 func DeleteRecord(tableName string, key string) error {
 func DeleteRecord(tableName string, key string) error {
 	dbMutex.Lock()
 	dbMutex.Lock()
@@ -237,44 +222,6 @@ func FetchRecords(tableName string) (map[string]string, error) {
 	return getCurrentDB()[FETCH_ALL].(func(string) (map[string]string, error))(tableName)
 	return getCurrentDB()[FETCH_ALL].(func(string) (map[string]string, error))(tableName)
 }
 }
 
 
-// initializeUUID - create a UUID record for server if none exists
-func initializeUUID() error {
-	records, err := FetchRecords(SERVER_UUID_TABLE_NAME)
-	if err != nil {
-		if !IsEmptyRecord(err) {
-			return err
-		}
-	} else if len(records) > 0 {
-		return nil
-	}
-	// setup encryption keys
-	var trafficPubKey, trafficPrivKey, errT = box.GenerateKey(rand.Reader) // generate traffic keys
-	if errT != nil {
-		return errT
-	}
-	tPriv, err := ncutils.ConvertKeyToBytes(trafficPrivKey)
-	if err != nil {
-		return err
-	}
-
-	tPub, err := ncutils.ConvertKeyToBytes(trafficPubKey)
-	if err != nil {
-		return err
-	}
-
-	telemetry := models.Telemetry{
-		UUID:           uuid.NewString(),
-		TrafficKeyPriv: tPriv,
-		TrafficKeyPub:  tPub,
-	}
-	telJSON, err := json.Marshal(&telemetry)
-	if err != nil {
-		return err
-	}
-
-	return Insert(SERVER_UUID_RECORD_KEY, string(telJSON), SERVER_UUID_TABLE_NAME)
-}
-
 // CloseDB - closes a database gracefully
 // CloseDB - closes a database gracefully
 func CloseDB() {
 func CloseDB() {
 	getCurrentDB()[CLOSE_DB].(func())()
 	getCurrentDB()[CLOSE_DB].(func())()

+ 2 - 2
database/postgres.go

@@ -59,7 +59,7 @@ func pgCreateTable(tableName string) error {
 }
 }
 
 
 func pgInsert(key string, value string, tableName string) error {
 func pgInsert(key string, value string, tableName string) error {
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		insertSQL := "INSERT INTO " + tableName + " (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $3;"
 		insertSQL := "INSERT INTO " + tableName + " (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $3;"
 		statement, err := PGDB.Prepare(insertSQL)
 		statement, err := PGDB.Prepare(insertSQL)
 		if err != nil {
 		if err != nil {
@@ -77,7 +77,7 @@ func pgInsert(key string, value string, tableName string) error {
 }
 }
 
 
 func pgInsertPeer(key string, value string) error {
 func pgInsertPeer(key string, value string) error {
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		err := pgInsert(key, value, PEERS_TABLE_NAME)
 		err := pgInsert(key, value, PEERS_TABLE_NAME)
 		if err != nil {
 		if err != nil {
 			return err
 			return err

+ 2 - 2
database/rqlite.go

@@ -43,7 +43,7 @@ func rqliteCreateTable(tableName string) error {
 }
 }
 
 
 func rqliteInsert(key string, value string, tableName string) error {
 func rqliteInsert(key string, value string, tableName string) error {
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		_, err := RQliteDatabase.WriteOne("INSERT OR REPLACE INTO " + tableName + " (key, value) VALUES ('" + key + "', '" + value + "')")
 		_, err := RQliteDatabase.WriteOne("INSERT OR REPLACE INTO " + tableName + " (key, value) VALUES ('" + key + "', '" + value + "')")
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -54,7 +54,7 @@ func rqliteInsert(key string, value string, tableName string) error {
 }
 }
 
 
 func rqliteInsertPeer(key string, value string) error {
 func rqliteInsertPeer(key string, value string) error {
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		_, err := RQliteDatabase.WriteOne("INSERT OR REPLACE INTO " + PEERS_TABLE_NAME + " (key, value) VALUES ('" + key + "', '" + value + "')")
 		_, err := RQliteDatabase.WriteOne("INSERT OR REPLACE INTO " + PEERS_TABLE_NAME + " (key, value) VALUES ('" + key + "', '" + value + "')")
 		if err != nil {
 		if err != nil {
 			return err
 			return err

+ 2 - 2
database/sqlite.go

@@ -61,7 +61,7 @@ func sqliteCreateTable(tableName string) error {
 }
 }
 
 
 func sqliteInsert(key string, value string, tableName string) error {
 func sqliteInsert(key string, value string, tableName string) error {
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		insertSQL := "INSERT OR REPLACE INTO " + tableName + " (key, value) VALUES (?, ?)"
 		insertSQL := "INSERT OR REPLACE INTO " + tableName + " (key, value) VALUES (?, ?)"
 		statement, err := SqliteDB.Prepare(insertSQL)
 		statement, err := SqliteDB.Prepare(insertSQL)
 		if err != nil {
 		if err != nil {
@@ -78,7 +78,7 @@ func sqliteInsert(key string, value string, tableName string) error {
 }
 }
 
 
 func sqliteInsertPeer(key string, value string) error {
 func sqliteInsertPeer(key string, value string) error {
-	if key != "" && value != "" && IsJSONString(value) {
+	if key != "" && value != "" {
 		err := sqliteInsert(key, value, PEERS_TABLE_NAME)
 		err := sqliteInsert(key, value, PEERS_TABLE_NAME)
 		if err != nil {
 		if err != nil {
 			return err
 			return err

+ 0 - 59
database/statics.go

@@ -1,59 +0,0 @@
-package database
-
-import (
-	"encoding/json"
-	"strings"
-)
-
-// SetPeers - sets peers for a network
-func SetPeers(newPeers map[string]string, networkName string) bool {
-	areEqual := PeersAreEqual(newPeers, networkName)
-	if !areEqual {
-		jsonData, err := json.Marshal(newPeers)
-		if err != nil {
-			return false
-		}
-		InsertPeer(networkName, string(jsonData))
-		return true
-	}
-	return !areEqual
-}
-
-// GetPeers - gets peers for a given network
-func GetPeers(networkName string) (map[string]string, error) {
-	record, err := FetchRecord(PEERS_TABLE_NAME, networkName)
-	if err != nil && !IsEmptyRecord(err) {
-		return nil, err
-	}
-	currentDataMap := make(map[string]string)
-	if IsEmptyRecord(err) {
-		return currentDataMap, nil
-	}
-	err = json.Unmarshal([]byte(record), &currentDataMap)
-	return currentDataMap, err
-}
-
-// PeersAreEqual - checks if peers are the same
-func PeersAreEqual(toCompare map[string]string, networkName string) bool {
-	currentDataMap, err := GetPeers(networkName)
-	if err != nil {
-		return false
-	}
-	if len(currentDataMap) != len(toCompare) {
-		return false
-	}
-	for k := range currentDataMap {
-		if toCompare[k] != currentDataMap[k] {
-			return false
-		}
-	}
-	return true
-}
-
-// IsEmptyRecord - checks for if it's an empty record error or not
-func IsEmptyRecord(err error) bool {
-	if err == nil {
-		return false
-	}
-	return strings.Contains(err.Error(), NO_RECORD) || strings.Contains(err.Error(), NO_RECORDS)
-}

+ 11 - 0
database/utils.go

@@ -0,0 +1,11 @@
+package database
+
+import "strings"
+
+// IsEmptyRecord - checks for if it's an empty record error or not
+func IsEmptyRecord(err error) bool {
+	if err == nil {
+		return false
+	}
+	return strings.Contains(err.Error(), NO_RECORD) || strings.Contains(err.Error(), NO_RECORDS)
+}

+ 41 - 0
db/connector.go

@@ -0,0 +1,41 @@
+package db
+
+import (
+	"errors"
+	"os"
+
+	"github.com/gravitl/netmaker/config"
+	"gorm.io/gorm"
+)
+
+var ErrUnsupportedDB = errors.New("unsupported db type")
+
+// connector helps connect to a database,
+// along with any initializations required.
+type connector interface {
+	connect() (*gorm.DB, error)
+}
+
+// GetDB - gets the database type
+func GetDB() string {
+	database := "sqlite"
+	if os.Getenv("DATABASE") != "" {
+		database = os.Getenv("DATABASE")
+	} else if config.Config.Server.Database != "" {
+		database = config.Config.Server.Database
+	}
+	return database
+}
+
+// newConnector detects the database being
+// used and returns the corresponding connector.
+func newConnector() (connector, error) {
+	switch GetDB() {
+	case "sqlite":
+		return &sqliteConnector{}, nil
+	case "postgres":
+		return &postgresConnector{}, nil
+	default:
+		return nil, ErrUnsupportedDB
+	}
+}

+ 112 - 0
db/db.go

@@ -0,0 +1,112 @@
+package db
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"time"
+
+	"gorm.io/gorm"
+)
+
+type ctxKey string
+
+const dbCtxKey ctxKey = "db"
+
+var db *gorm.DB
+
+var ErrDBNotFound = errors.New("no db instance in context")
+
+// InitializeDB initializes a connection to the
+// database (if not already done) and ensures it
+// has the latest schema.
+func InitializeDB(models ...interface{}) error {
+	if db != nil {
+		return nil
+	}
+
+	connector, err := newConnector()
+	if err != nil {
+		return err
+	}
+
+	// DB / LIFE ADVICE: try 5 times before giving up.
+	for i := 0; i < 5; i++ {
+		db, err = connector.connect()
+		if err == nil {
+			break
+		}
+
+		// wait 2s if you have the time.
+		time.Sleep(2 * time.Second)
+	}
+	if err != nil {
+		return err
+	}
+
+	return db.AutoMigrate(models...)
+}
+
+// WithContext returns a new context with the db
+// connection instance.
+//
+// Ensure InitializeDB has been called before using
+// this function.
+//
+// To extract the db connection use the FromContext
+// function.
+func WithContext(ctx context.Context) context.Context {
+	return context.WithValue(ctx, dbCtxKey, db)
+}
+
+// Middleware to auto-inject the db connection instance
+// in a request's context.
+//
+// Ensure InitializeDB has been called before using this
+// middleware.
+func Middleware(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		next.ServeHTTP(w, r.WithContext(WithContext(r.Context())))
+	})
+}
+
+// FromContext extracts the db connection instance from
+// the given context.
+//
+// The function panics, if a connection does not exist.
+func FromContext(ctx context.Context) *gorm.DB {
+	db, ok := ctx.Value(dbCtxKey).(*gorm.DB)
+	if !ok {
+		panic(ErrDBNotFound)
+	}
+
+	return db
+}
+
+func SetPagination(ctx context.Context, page, pageSize int) context.Context {
+	if page < 1 {
+		page = 1
+	}
+	if pageSize < 1 || pageSize > 100 {
+		pageSize = 10
+	}
+	db := FromContext(ctx)
+	offset := (page - 1) * pageSize
+	return context.WithValue(ctx, dbCtxKey, db.Offset(offset).Limit(pageSize))
+}
+
+// BeginTx returns a context with a new transaction.
+// If the context already has a db connection instance,
+// it uses that instance. Otherwise, it uses the
+// connection initialized in the package.
+//
+// Ensure InitializeDB has been called before using
+// this function.
+func BeginTx(ctx context.Context) context.Context {
+	dbInCtx, ok := ctx.Value(dbCtxKey).(*gorm.DB)
+	if !ok {
+		return context.WithValue(ctx, dbCtxKey, db.Begin())
+	}
+
+	return context.WithValue(ctx, dbCtxKey, dbInCtx.Begin())
+}

+ 117 - 0
db/postgres.go

@@ -0,0 +1,117 @@
+package db
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+
+	"github.com/gravitl/netmaker/config"
+	"gorm.io/driver/postgres"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+)
+
+// postgresConnector for initializing and
+// connecting to a postgres database.
+type postgresConnector struct{}
+
+// postgresConnector.connect connects and
+// initializes a connection to postgres.
+func (pg *postgresConnector) connect() (*gorm.DB, error) {
+	pgConf := GetSQLConf()
+	dsn := fmt.Sprintf(
+		"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s connect_timeout=5",
+		pgConf.Host,
+		pgConf.Port,
+		pgConf.Username,
+		pgConf.Password,
+		pgConf.DB,
+		pgConf.SSLMode,
+	)
+
+	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
+		Logger: logger.Default.LogMode(logger.Silent),
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	// ensure netmaker_v1 schema exists.
+	err = db.Exec("CREATE SCHEMA IF NOT EXISTS netmaker_v1").Error
+	if err != nil {
+		return nil, err
+	}
+
+	// set the netmaker_v1 schema as the default schema.
+	err = db.Exec("SET search_path TO netmaker_v1").Error
+	if err != nil {
+		return nil, err
+	}
+
+	return db, nil
+}
+func GetSQLConf() config.SQLConfig {
+	var cfg config.SQLConfig
+	cfg.Host = GetSQLHost()
+	cfg.Port = GetSQLPort()
+	cfg.Username = GetSQLUser()
+	cfg.Password = GetSQLPass()
+	cfg.DB = GetSQLDB()
+	cfg.SSLMode = GetSQLSSLMode()
+	return cfg
+}
+func GetSQLHost() string {
+	host := "localhost"
+	if os.Getenv("SQL_HOST") != "" {
+		host = os.Getenv("SQL_HOST")
+	} else if config.Config.SQL.Host != "" {
+		host = config.Config.SQL.Host
+	}
+	return host
+}
+func GetSQLPort() int32 {
+	port := int32(5432)
+	envport, err := strconv.Atoi(os.Getenv("SQL_PORT"))
+	if err == nil && envport != 0 {
+		port = int32(envport)
+	} else if config.Config.SQL.Port != 0 {
+		port = config.Config.SQL.Port
+	}
+	return port
+}
+func GetSQLUser() string {
+	user := "postgres"
+	if os.Getenv("SQL_USER") != "" {
+		user = os.Getenv("SQL_USER")
+	} else if config.Config.SQL.Username != "" {
+		user = config.Config.SQL.Username
+	}
+	return user
+}
+func GetSQLPass() string {
+	pass := "nopass"
+	if os.Getenv("SQL_PASS") != "" {
+		pass = os.Getenv("SQL_PASS")
+	} else if config.Config.SQL.Password != "" {
+		pass = config.Config.SQL.Password
+	}
+	return pass
+}
+func GetSQLDB() string {
+	db := "netmaker"
+	if os.Getenv("SQL_DB") != "" {
+		db = os.Getenv("SQL_DB")
+	} else if config.Config.SQL.DB != "" {
+		db = config.Config.SQL.DB
+	}
+	return db
+}
+func GetSQLSSLMode() string {
+	sslmode := "disable"
+	if os.Getenv("SQL_SSL_MODE") != "" {
+		sslmode = os.Getenv("SQL_SSL_MODE")
+	} else if config.Config.SQL.SSLMode != "" {
+		sslmode = config.Config.SQL.SSLMode
+	}
+	return sslmode
+}

+ 55 - 0
db/sqlite.go

@@ -0,0 +1,55 @@
+package db
+
+import (
+	"os"
+	"path/filepath"
+
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+)
+
+// sqliteConnector for initializing and
+// connecting to a sqlite database.
+type sqliteConnector struct{}
+
+// sqliteConnector.connect connects and
+// initializes a connection to sqlite.
+func (s *sqliteConnector) connect() (*gorm.DB, error) {
+	// ensure data dir exists.
+	_, err := os.Stat("data")
+	if err != nil {
+		if os.IsNotExist(err) {
+			err = os.Mkdir("data", 0700)
+			if err != nil {
+				return nil, err
+			}
+		} else {
+			return nil, err
+		}
+	}
+
+	dbFilePath := filepath.Join("data", "netmaker.db")
+
+	// ensure netmaker_v1.db exists.
+	_, err = os.Stat(dbFilePath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			file, err := os.Create(dbFilePath)
+			if err != nil {
+				return nil, err
+			}
+
+			err = file.Close()
+			if err != nil {
+				return nil, err
+			}
+		} else {
+			return nil, err
+		}
+	}
+
+	return gorm.Open(sqlite.Open(dbFilePath), &gorm.Config{
+		Logger: logger.Default.LogMode(logger.Silent),
+	})
+}

+ 1 - 1
docker/Dockerfile-go-builder

@@ -1,6 +1,6 @@
 FROM golang:1.23.0-alpine3.20
 FROM golang:1.23.0-alpine3.20
 ARG version 
 ARG version 
-RUN apk add build-base
+RUN apk add --no-cache build-base
 WORKDIR /app
 WORKDIR /app
 COPY go.* ./ 
 COPY go.* ./ 
 RUN go mod download
 RUN go mod download

+ 1 - 1
docker/Dockerfile-netclient-multiarch

@@ -13,7 +13,7 @@ FROM alpine:3.16.2
 
 
 WORKDIR /root/
 WORKDIR /root/
 
 
-RUN apk add --no-cache --update bash libmnl gcompat iptables openresolv iproute2 wireguard-tools 
+RUN apk add --no-cache bash libmnl gcompat iptables openresolv iproute2 wireguard-tools 
 COPY --from=builder /app/netclient-app ./netclient
 COPY --from=builder /app/netclient-app ./netclient
 COPY --from=builder /app/scripts/netclient.sh .
 COPY --from=builder /app/scripts/netclient.sh .
 RUN chmod 0755 netclient && chmod 0755 netclient.sh
 RUN chmod 0755 netclient && chmod 0755 netclient.sh

+ 2 - 2
docker/Dockerfile-netclient-multiarch-userspace

@@ -10,7 +10,7 @@ RUN GOOS=linux CGO_ENABLED=0 /usr/local/go/bin/go build -ldflags="-w -s" -o netc
 
 
 WORKDIR /root/
 WORKDIR /root/
 
 
-RUN apk add --update git build-base libmnl-dev iptables
+RUN apk add --no-cache git build-base libmnl-dev iptables
 
 
 RUN git clone https://git.zx2c4.com/wireguard-go && \
 RUN git clone https://git.zx2c4.com/wireguard-go && \
     cd wireguard-go && \
     cd wireguard-go && \
@@ -28,7 +28,7 @@ FROM alpine:3.16.2
 
 
 WORKDIR /root/
 WORKDIR /root/
 
 
-RUN apk add --no-cache --update bash libmnl gcompat iptables openresolv iproute2
+RUN apk add --no-cache bash libmnl gcompat iptables openresolv iproute2
 COPY --from=builder /usr/bin/wireguard-go /usr/bin/wg* /usr/bin/
 COPY --from=builder /usr/bin/wireguard-go /usr/bin/wg* /usr/bin/
 COPY --from=builder /app/netclient-app ./netclient
 COPY --from=builder /app/netclient-app ./netclient
 COPY --from=builder /app/scripts/netclient.sh .
 COPY --from=builder /app/scripts/netclient.sh .

+ 51 - 19
go.mod

@@ -1,28 +1,30 @@
 module github.com/gravitl/netmaker
 module github.com/gravitl/netmaker
 
 
-go 1.23
+go 1.23.0
+
+toolchain go1.23.7
 
 
 require (
 require (
 	github.com/blang/semver v3.5.1+incompatible
 	github.com/blang/semver v3.5.1+incompatible
-	github.com/eclipse/paho.mqtt.golang v1.4.3
-	github.com/go-playground/validator/v10 v10.24.0
-	github.com/golang-jwt/jwt/v4 v4.5.1
+	github.com/eclipse/paho.mqtt.golang v1.5.0
+	github.com/go-playground/validator/v10 v10.26.0
+	github.com/golang-jwt/jwt/v4 v4.5.2
 	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
 	github.com/gorilla/mux v1.8.1
 	github.com/gorilla/mux v1.8.1
 	github.com/lib/pq v1.10.9
 	github.com/lib/pq v1.10.9
-	github.com/mattn/go-sqlite3 v1.14.24
+	github.com/mattn/go-sqlite3 v1.14.28
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa
 	github.com/seancfoley/ipaddress-go v1.7.0
 	github.com/seancfoley/ipaddress-go v1.7.0
 	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.10.0
 	github.com/stretchr/testify v1.10.0
 	github.com/txn2/txeh v1.5.5
 	github.com/txn2/txeh v1.5.5
 	go.uber.org/automaxprocs v1.6.0
 	go.uber.org/automaxprocs v1.6.0
-	golang.org/x/crypto v0.32.0
-	golang.org/x/net v0.34.0 // indirect
-	golang.org/x/oauth2 v0.24.0
-	golang.org/x/sys v0.29.0 // indirect
-	golang.org/x/text v0.21.0 // indirect
+	golang.org/x/crypto v0.38.0
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/oauth2 v0.30.0
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.25.0 // indirect
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
 	gopkg.in/yaml.v3 v3.0.1
 	gopkg.in/yaml.v3 v3.0.1
 )
 )
@@ -30,44 +32,74 @@ require (
 require (
 require (
 	filippo.io/edwards25519 v1.1.0
 	filippo.io/edwards25519 v1.1.0
 	github.com/c-robinson/iplib v1.0.8
 	github.com/c-robinson/iplib v1.0.8
-	github.com/posthog/posthog-go v1.2.24
+	github.com/posthog/posthog-go v1.5.5
 )
 )
 
 
 require (
 require (
-	github.com/coreos/go-oidc/v3 v3.9.0
+	github.com/coreos/go-oidc/v3 v3.14.1
 	github.com/gorilla/websocket v1.5.3
 	github.com/gorilla/websocket v1.5.3
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
 )
 )
 
 
 require (
 require (
+	github.com/google/go-cmp v0.7.0
 	github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e
 	github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e
 	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
-	github.com/spf13/cobra v1.8.1
+	github.com/spf13/cobra v1.9.1
+	google.golang.org/api v0.229.0
 	gopkg.in/mail.v2 v2.3.1
 	gopkg.in/mail.v2 v2.3.1
+	gorm.io/datatypes v1.2.5
+	gorm.io/driver/postgres v1.5.11
+	gorm.io/driver/sqlite v1.5.7
+	gorm.io/gorm v1.30.0
 )
 )
 
 
 require (
 require (
-	cloud.google.com/go/compute/metadata v0.3.0 // indirect
+	cloud.google.com/go/auth v0.16.0 // indirect
+	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
+	cloud.google.com/go/compute/metadata v0.6.0 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
-	github.com/go-jose/go-jose/v3 v3.0.3 // indirect
+	github.com/go-jose/go-jose/v4 v4.0.5 // indirect
+	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/go-sql-driver/mysql v1.8.1 // indirect
+	github.com/google/s2a-go v0.1.9 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
+	github.com/googleapis/gax-go/v2 v2.14.1 // indirect
+	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/kr/text v0.2.0 // indirect
+	github.com/jackc/pgpassfile v1.0.0 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+	github.com/jackc/pgx/v5 v5.7.2 // indirect
+	github.com/jackc/puddle/v2 v2.2.2 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
+	github.com/rogpeppe/go-internal v1.14.1 // indirect
 	github.com/seancfoley/bintree v1.3.1 // indirect
 	github.com/seancfoley/bintree v1.3.1 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/spf13/pflag v1.0.6 // indirect
+	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
+	go.opentelemetry.io/otel v1.35.0 // indirect
+	go.opentelemetry.io/otel/metric v1.35.0 // indirect
+	go.opentelemetry.io/otel/trace v1.35.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
+	google.golang.org/grpc v1.71.1 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
+	gorm.io/driver/mysql v1.5.6 // indirect
 )
 )
 
 
 require (
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/felixge/httpsnoop v1.0.3 // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/hashicorp/go-version v1.7.0
 	github.com/hashicorp/go-version v1.7.0
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	golang.org/x/sync v0.10.0 // indirect
+	golang.org/x/sync v0.14.0 // indirect
 )
 )

+ 125 - 80
go.sum

@@ -1,40 +1,63 @@
-cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
-cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU=
+cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
+cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
+cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
+cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
+cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/c-robinson/iplib v1.0.8 h1:exDRViDyL9UBLcfmlxxkY5odWX5092nPsQIykHXhIn4=
 github.com/c-robinson/iplib v1.0.8 h1:exDRViDyL9UBLcfmlxxkY5odWX5092nPsQIykHXhIn4=
 github.com/c-robinson/iplib v1.0.8/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
 github.com/c-robinson/iplib v1.0.8/go.mod h1:i3LuuFL1hRT5gFpBRnEydzw8R6yhGkF4szNDIbF8pgo=
-github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
-github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
-github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
+github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
-github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
-github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
-github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o=
+github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
 github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
 github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
 github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
-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-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
+github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 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/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 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.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
-github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
-github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
-github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-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/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
+github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
+github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
+github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
 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/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
+github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
+github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40=
 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40=
 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4=
 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4=
 github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
 github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
@@ -47,10 +70,24 @@ github.com/guumaster/tablewriter v0.0.10 h1:A0HD94yMdt4usgxBjoEceNeE0XMJ027euoHA
 github.com/guumaster/tablewriter v0.0.10/go.mod h1:p4FRFhyfo0UD9ZLmMRbbJooTUsxo6b80qZTERVDWrH8=
 github.com/guumaster/tablewriter v0.0.10/go.mod h1:p4FRFhyfo0UD9ZLmMRbbJooTUsxo6b80qZTERVDWrH8=
 github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
 github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
 github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
+github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@@ -63,19 +100,23 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
-github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
+github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
+github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posthog/posthog-go v1.2.24 h1:A+iG4saBJemo++VDlcWovbYf8KFFNUfrCoJtsc40RPA=
-github.com/posthog/posthog-go v1.2.24/go.mod h1:uYC2l1Yktc8E+9FAHJ9QZG4vQf/NHJPD800Hsm7DzoM=
+github.com/posthog/posthog-go v1.5.5 h1:2o3j7IrHbTIfxRtj4MPaXKeimuTYg49onNzNBZbwksM=
+github.com/posthog/posthog-go v1.5.5/go.mod h1:3RqUmSnPuwmeVj/GYrS75wNGqcAKdpODiwc83xZWgdE=
 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa h1:hxMLFbj+F444JAS5nUQxTDZwUxwCRqg3WkNqhiDzXrM=
 github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa h1:hxMLFbj+F444JAS5nUQxTDZwUxwCRqg3WkNqhiDzXrM=
 github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
 github.com/rqlite/gorqlite v0.0.0-20240122221808-a8a425b1a6aa/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -85,79 +126,83 @@ github.com/seancfoley/ipaddress-go v1.7.0 h1:vWp3SR3k+HkV3aKiNO2vEe6xbVxS0x/Ixw6
 github.com/seancfoley/ipaddress-go v1.7.0/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw=
 github.com/seancfoley/ipaddress-go v1.7.0/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
-github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
-github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
-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/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/stretchr/testify v1.10.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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
+go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
 go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
 go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
 go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
 go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
-golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
+golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
+golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
 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.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-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.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.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
-golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
-golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
-golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
-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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-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-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.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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
-golang.org/x/sys v0.29.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-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.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.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/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-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.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/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
+golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
+golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
+golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
+golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
+google.golang.org/api v0.229.0 h1:p98ymMtqeJ5i3lIBMj5MpR9kzIIgzpHHh8vQ+vgAzx8=
+google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
+google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
 gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
 gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
 gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I=
+gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4=
+gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
+gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
+gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
+gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
+gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
+gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
+gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
+gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
+gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
+gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=

+ 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.90.0
+        image: gravitl/netclient:v0.99.0
         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.90.0
+        image: gravitl/netclient:v0.99.0
         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.90.0
+        image: gravitl/netmaker-ui:v0.99.0
         ports:
         ports:
         - containerPort: 443
         - containerPort: 443
         env:
         env:

File diff suppressed because it is too large
+ 541 - 532
logic/acls.go


+ 62 - 8
logic/auth.go

@@ -1,12 +1,16 @@
 package logic
 package logic
 
 
 import (
 import (
+	"context"
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"time"
 	"time"
 
 
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/schema"
+
 	"github.com/go-playground/validator/v10"
 	"github.com/go-playground/validator/v10"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
@@ -28,6 +32,9 @@ func ClearSuperUserCache() {
 	superUser = models.User{}
 	superUser = models.User{}
 }
 }
 
 
+var ResetAuthProvider = func() {}
+var ResetIDPSyncHook = func() {}
+
 // HasSuperAdmin - checks if server has an superadmin/owner
 // HasSuperAdmin - checks if server has an superadmin/owner
 func HasSuperAdmin() (bool, error) {
 func HasSuperAdmin() (bool, error) {
 
 
@@ -169,6 +176,7 @@ func CreateUser(user *models.User) error {
 	if IsOauthUser(user) == nil {
 	if IsOauthUser(user) == nil {
 		user.AuthType = models.OAuth
 		user.AuthType = models.OAuth
 	}
 	}
+	AddGlobalNetRolesToAdmins(user)
 	_, err = CreateUserJWT(user.UserName, user.PlatformRoleID)
 	_, err = CreateUserJWT(user.UserName, user.PlatformRoleID)
 	if err != nil {
 	if err != nil {
 		logger.Log(0, "failed to generate token", err.Error())
 		logger.Log(0, "failed to generate token", err.Error())
@@ -186,7 +194,6 @@ func CreateUser(user *models.User) error {
 		logger.Log(0, "failed to insert user", err.Error())
 		logger.Log(0, "failed to insert user", err.Error())
 		return err
 		return err
 	}
 	}
-	AddGlobalNetRolesToAdmins(*user)
 	return nil
 	return nil
 }
 }
 
 
@@ -235,7 +242,7 @@ func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) {
 	}
 	}
 
 
 	// update last login time
 	// update last login time
-	result.LastLoginTime = time.Now()
+	result.LastLoginTime = time.Now().UTC()
 	err = UpsertUser(result)
 	err = UpsertUser(result)
 	if err != nil {
 	if err != nil {
 		slog.Error("error upserting user", "error", err)
 		slog.Error("error upserting user", "error", err)
@@ -275,6 +282,10 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) {
 		if _, err := GetUser(userchange.UserName); err == nil {
 		if _, err := GetUser(userchange.UserName); err == nil {
 			return &models.User{}, errors.New("username exists already")
 			return &models.User{}, errors.New("username exists already")
 		}
 		}
+		if userchange.UserName == MasterUser {
+			return &models.User{}, errors.New("username not allowed")
+		}
+
 		user.UserName = userchange.UserName
 		user.UserName = userchange.UserName
 	}
 	}
 	if userchange.Password != "" {
 	if userchange.Password != "" {
@@ -298,14 +309,58 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) {
 	if err := IsNetworkRolesValid(userchange.NetworkRoles); err != nil {
 	if err := IsNetworkRolesValid(userchange.NetworkRoles); err != nil {
 		return userchange, errors.New("invalid network roles: " + err.Error())
 		return userchange, errors.New("invalid network roles: " + err.Error())
 	}
 	}
+
+	if userchange.DisplayName != "" {
+		if user.ExternalIdentityProviderID != "" &&
+			user.DisplayName != userchange.DisplayName {
+			return userchange, errors.New("display name cannot be updated for external user")
+		}
+
+		user.DisplayName = userchange.DisplayName
+	}
+
+	if user.ExternalIdentityProviderID != "" &&
+		userchange.AccountDisabled != user.AccountDisabled {
+		return userchange, errors.New("account status cannot be updated for external user")
+	}
+
 	// Reset Gw Access for service users
 	// Reset Gw Access for service users
 	go UpdateUserGwAccess(*user, *userchange)
 	go UpdateUserGwAccess(*user, *userchange)
 	if userchange.PlatformRoleID != "" {
 	if userchange.PlatformRoleID != "" {
 		user.PlatformRoleID = userchange.PlatformRoleID
 		user.PlatformRoleID = userchange.PlatformRoleID
 	}
 	}
+
+	for groupID := range userchange.UserGroups {
+		_, ok := user.UserGroups[groupID]
+		if !ok {
+			group, err := GetUserGroup(groupID)
+			if err != nil {
+				return userchange, err
+			}
+
+			if group.ExternalIdentityProviderID != "" {
+				return userchange, errors.New("cannot modify membership of external groups")
+			}
+		}
+	}
+
+	for groupID := range user.UserGroups {
+		_, ok := userchange.UserGroups[groupID]
+		if !ok {
+			group, err := GetUserGroup(groupID)
+			if err != nil {
+				return userchange, err
+			}
+
+			if group.ExternalIdentityProviderID != "" {
+				return userchange, errors.New("cannot modify membership of external groups")
+			}
+		}
+	}
+
 	user.UserGroups = userchange.UserGroups
 	user.UserGroups = userchange.UserGroups
 	user.NetworkRoles = userchange.NetworkRoles
 	user.NetworkRoles = userchange.NetworkRoles
-	AddGlobalNetRolesToAdmins(*user)
+	AddGlobalNetRolesToAdmins(user)
 	err := ValidateUser(user)
 	err := ValidateUser(user)
 	if err != nil {
 	if err != nil {
 		return &models.User{}, err
 		return &models.User{}, err
@@ -349,19 +404,18 @@ func ValidateUser(user *models.User) error {
 }
 }
 
 
 // DeleteUser - deletes a given user
 // DeleteUser - deletes a given user
-func DeleteUser(user string) (bool, error) {
+func DeleteUser(user string) error {
 
 
 	if userRecord, err := database.FetchRecord(database.USERS_TABLE_NAME, user); err != nil || len(userRecord) == 0 {
 	if userRecord, err := database.FetchRecord(database.USERS_TABLE_NAME, user); err != nil || len(userRecord) == 0 {
-		return false, errors.New("user does not exist")
+		return errors.New("user does not exist")
 	}
 	}
 
 
 	err := database.DeleteRecord(database.USERS_TABLE_NAME, user)
 	err := database.DeleteRecord(database.USERS_TABLE_NAME, user)
 	if err != nil {
 	if err != nil {
-		return false, err
+		return err
 	}
 	}
 	go RemoveUserFromAclPolicy(user)
 	go RemoveUserFromAclPolicy(user)
-
-	return true, nil
+	return (&schema.UserAccessToken{UserName: user}).DeleteAllUserTokens(db.WithContext(context.TODO()))
 }
 }
 
 
 func SetAuthSecret(secret string) error {
 func SetAuthSecret(secret string) error {

+ 1 - 2
logic/dns.go

@@ -12,7 +12,6 @@ import (
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
-	"github.com/gravitl/netmaker/servercfg"
 	"github.com/txn2/txeh"
 	"github.com/txn2/txeh"
 )
 )
 
 
@@ -106,7 +105,7 @@ func GetNodeDNS(network string) ([]models.DNSEntry, error) {
 	if err != nil {
 	if err != nil {
 		return dns, err
 		return dns, err
 	}
 	}
-	defaultDomain := servercfg.GetDefaultDomain()
+	defaultDomain := GetDefaultDomain()
 	for _, node := range nodes {
 	for _, node := range nodes {
 		if node.Network != network {
 		if node.Network != network {
 			continue
 			continue

+ 221 - 0
logic/egress.go

@@ -0,0 +1,221 @@
+package logic
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"maps"
+
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
+	"github.com/gravitl/netmaker/servercfg"
+)
+
+func ValidateEgressReq(e *schema.Egress) error {
+	if e.Network == "" {
+		return errors.New("network id is empty")
+	}
+	_, err := GetNetwork(e.Network)
+	if err != nil {
+		return errors.New("failed to get network " + err.Error())
+	}
+
+	if !servercfg.IsPro && len(e.Nodes) > 1 {
+		return errors.New("can only set one routing node on CE")
+	}
+
+	if len(e.Nodes) > 0 {
+		for k := range e.Nodes {
+			_, err := GetNodeByID(k)
+			if err != nil {
+				return errors.New("invalid routing node " + err.Error())
+			}
+		}
+	}
+	return nil
+}
+
+func DoesNodeHaveAccessToEgress(node *models.Node, e *schema.Egress, acls []models.Acl) bool {
+	nodeTags := maps.Clone(node.Tags)
+	nodeTags[models.TagID(node.ID.String())] = struct{}{}
+	nodeTags[models.TagID("*")] = struct{}{}
+	for _, acl := range acls {
+		if !acl.Enabled {
+			continue
+		}
+		srcVal := ConvAclTagToValueMap(acl.Src)
+		for _, dstI := range acl.Dst {
+
+			if dstI.ID == models.NodeTagID && dstI.Value == "*" {
+				return true
+			}
+			if dstI.ID == models.EgressID && dstI.Value == e.ID {
+				e := schema.Egress{ID: dstI.Value}
+				err := e.Get(db.WithContext(context.TODO()))
+				if err != nil {
+					continue
+				}
+				if node.IsStatic {
+					if _, ok := srcVal[node.StaticNode.ClientID]; ok {
+						return true
+					}
+				} else {
+					if _, ok := srcVal[node.ID.String()]; ok {
+						return true
+					}
+				}
+
+				for tagID := range nodeTags {
+					if _, ok := srcVal[tagID.String()]; ok {
+						return true
+					}
+				}
+
+			}
+		}
+	}
+	return false
+}
+
+func AddEgressInfoToPeerByAccess(node, targetNode *models.Node, eli []schema.Egress, acls []models.Acl, isDefaultPolicyActive bool) {
+
+	req := models.EgressGatewayRequest{
+		NodeID:     targetNode.ID.String(),
+		NetID:      targetNode.Network,
+		NatEnabled: "yes",
+	}
+	for _, e := range eli {
+		if !e.Status || e.Network != targetNode.Network {
+			continue
+		}
+		if !isDefaultPolicyActive {
+			if !DoesNodeHaveAccessToEgress(node, &e, acls) {
+				if node.IsRelayed && node.RelayedBy == targetNode.ID.String() {
+					if !DoesNodeHaveAccessToEgress(targetNode, &e, acls) {
+						continue
+					}
+				} else {
+					continue
+				}
+
+			}
+		}
+
+		if metric, ok := e.Nodes[targetNode.ID.String()]; ok {
+			m64, err := metric.(json.Number).Int64()
+			if err != nil {
+				m64 = 256
+			}
+			m := uint32(m64)
+			req.Ranges = append(req.Ranges, e.Range)
+			req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+				Network:     e.Range,
+				Nat:         e.Nat,
+				RouteMetric: m,
+			})
+		}
+	}
+	if targetNode.Mutex != nil {
+		targetNode.Mutex.Lock()
+	}
+	if len(req.Ranges) > 0 {
+
+		targetNode.EgressDetails.IsEgressGateway = true
+		targetNode.EgressDetails.EgressGatewayRanges = req.Ranges
+		targetNode.EgressDetails.EgressGatewayRequest = req
+
+	} else {
+		targetNode.EgressDetails = models.EgressDetails{}
+	}
+	if targetNode.Mutex != nil {
+		targetNode.Mutex.Unlock()
+	}
+}
+
+func GetNodeEgressInfo(targetNode *models.Node, eli []schema.Egress, acls []models.Acl) {
+
+	req := models.EgressGatewayRequest{
+		NodeID:     targetNode.ID.String(),
+		NetID:      targetNode.Network,
+		NatEnabled: "yes",
+	}
+	for _, e := range eli {
+		if !e.Status || e.Network != targetNode.Network {
+			continue
+		}
+		if metric, ok := e.Nodes[targetNode.ID.String()]; ok {
+			m64, err := metric.(json.Number).Int64()
+			if err != nil {
+				m64 = 256
+			}
+			m := uint32(m64)
+			req.Ranges = append(req.Ranges, e.Range)
+			req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
+				Network:     e.Range,
+				Nat:         e.Nat,
+				RouteMetric: m,
+			})
+
+		}
+	}
+	if targetNode.Mutex != nil {
+		targetNode.Mutex.Lock()
+	}
+	if len(req.Ranges) > 0 {
+		targetNode.EgressDetails.IsEgressGateway = true
+		targetNode.EgressDetails.EgressGatewayRanges = req.Ranges
+		targetNode.EgressDetails.EgressGatewayRequest = req
+	} else {
+		targetNode.EgressDetails = models.EgressDetails{}
+	}
+	if targetNode.Mutex != nil {
+		targetNode.Mutex.Unlock()
+	}
+}
+
+func RemoveNodeFromEgress(node models.Node) {
+	egs, _ := (&schema.Egress{
+		Network: node.Network,
+	}).ListByNetwork(db.WithContext(context.TODO()))
+	for _, egI := range egs {
+		if _, ok := egI.Nodes[node.ID.String()]; ok {
+			delete(egI.Nodes, node.ID.String())
+			egI.Update(db.WithContext(context.TODO()))
+		}
+	}
+
+}
+
+func GetEgressRanges(netID models.NetworkID) (map[string][]string, map[string]struct{}, error) {
+
+	resultMap := make(map[string]struct{})
+	nodeEgressMap := make(map[string][]string)
+	networkNodes, err := GetNetworkNodes(netID.String())
+	if err != nil {
+		return nil, nil, err
+	}
+	for _, currentNode := range networkNodes {
+		if currentNode.Network != netID.String() {
+			continue
+		}
+		if currentNode.EgressDetails.IsEgressGateway { // add the egress gateway range(s) to the result
+			if len(currentNode.EgressDetails.EgressGatewayRanges) > 0 {
+				nodeEgressMap[currentNode.ID.String()] = currentNode.EgressDetails.EgressGatewayRanges
+				for _, egressRangeI := range currentNode.EgressDetails.EgressGatewayRanges {
+					resultMap[egressRangeI] = struct{}{}
+				}
+			}
+		}
+	}
+	extclients, _ := GetNetworkExtClients(netID.String())
+	for _, extclient := range extclients {
+		if len(extclient.ExtraAllowedIPs) > 0 {
+			nodeEgressMap[extclient.ClientID] = extclient.ExtraAllowedIPs
+			for _, extraAllowedIP := range extclient.ExtraAllowedIPs {
+				resultMap[extraAllowedIP] = struct{}{}
+			}
+		}
+	}
+	return nodeEgressMap, resultMap, nil
+}

+ 2 - 1
logic/enrollmentkey.go

@@ -38,7 +38,7 @@ var (
 )
 )
 
 
 // CreateEnrollmentKey - creates a new enrollment key in db
 // CreateEnrollmentKey - creates a new enrollment key in db
-func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, groups []models.TagID, unlimited bool, relay uuid.UUID, defaultKey bool) (*models.EnrollmentKey, error) {
+func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, groups []models.TagID, unlimited bool, relay uuid.UUID, defaultKey, autoEgress bool) (*models.EnrollmentKey, error) {
 	newKeyID, err := getUniqueEnrollmentID()
 	newKeyID, err := getUniqueEnrollmentID()
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -54,6 +54,7 @@ func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string
 		Relay:         relay,
 		Relay:         relay,
 		Groups:        groups,
 		Groups:        groups,
 		Default:       defaultKey,
 		Default:       defaultKey,
+		AutoEgress:    autoEgress,
 	}
 	}
 	if uses > 0 {
 	if uses > 0 {
 		k.UsesRemaining = uses
 		k.UsesRemaining = uses

+ 13 - 13
logic/enrollmentkey_test.go

@@ -14,35 +14,35 @@ func TestCreateEnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	defer database.CloseDB()
 	t.Run("Can_Not_Create_Key", func(t *testing.T) {
 	t.Run("Can_Not_Create_Key", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, false, uuid.Nil, false)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, false, uuid.Nil, false, false)
 		assert.Nil(t, newKey)
 		assert.Nil(t, newKey)
 		assert.NotNil(t, err)
 		assert.NotNil(t, err)
 		assert.ErrorIs(t, err, models.ErrInvalidEnrollmentKey)
 		assert.ErrorIs(t, err, models.ErrInvalidEnrollmentKey)
 	})
 	})
 	t.Run("Can_Create_Key_Uses", func(t *testing.T) {
 	t.Run("Can_Create_Key_Uses", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false)
+		newKey, err := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false, false)
 		assert.Nil(t, err)
 		assert.Nil(t, err)
 		assert.Equal(t, 1, newKey.UsesRemaining)
 		assert.Equal(t, 1, newKey.UsesRemaining)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 	})
 	})
 	t.Run("Can_Create_Key_Time", func(t *testing.T) {
 	t.Run("Can_Create_Key_Time", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Now().Add(time.Minute), nil, nil, nil, false, uuid.Nil, false)
+		newKey, err := CreateEnrollmentKey(0, time.Now().Add(time.Minute), nil, nil, nil, false, uuid.Nil, false, false)
 		assert.Nil(t, err)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 	})
 	})
 	t.Run("Can_Create_Key_Unlimited", func(t *testing.T) {
 	t.Run("Can_Create_Key_Unlimited", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, true, uuid.Nil, false)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, true, uuid.Nil, false, false)
 		assert.Nil(t, err)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 	})
 	})
 	t.Run("Can_Create_Key_WithNetworks", func(t *testing.T) {
 	t.Run("Can_Create_Key_WithNetworks", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false, false)
 		assert.Nil(t, err)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 		assert.True(t, len(newKey.Networks) == 2)
 		assert.True(t, len(newKey.Networks) == 2)
 	})
 	})
 	t.Run("Can_Create_Key_WithTags", func(t *testing.T) {
 	t.Run("Can_Create_Key_WithTags", func(t *testing.T) {
-		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, []string{"tag1", "tag2"}, nil, true, uuid.Nil, false)
+		newKey, err := CreateEnrollmentKey(0, time.Time{}, nil, []string{"tag1", "tag2"}, nil, true, uuid.Nil, false, false)
 		assert.Nil(t, err)
 		assert.Nil(t, err)
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 		assert.True(t, len(newKey.Tags) == 2)
 		assert.True(t, len(newKey.Tags) == 2)
@@ -62,7 +62,7 @@ func TestCreateEnrollmentKey(t *testing.T) {
 func TestDelete_EnrollmentKey(t *testing.T) {
 func TestDelete_EnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false, false)
 	t.Run("Can_Delete_Key", func(t *testing.T) {
 	t.Run("Can_Delete_Key", func(t *testing.T) {
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 		err := DeleteEnrollmentKey(newKey.Value, false)
 		err := DeleteEnrollmentKey(newKey.Value, false)
@@ -83,7 +83,7 @@ func TestDelete_EnrollmentKey(t *testing.T) {
 func TestDecrement_EnrollmentKey(t *testing.T) {
 func TestDecrement_EnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false)
+	newKey, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false, false)
 	t.Run("Check_initial_uses", func(t *testing.T) {
 	t.Run("Check_initial_uses", func(t *testing.T) {
 		assert.True(t, newKey.IsValid())
 		assert.True(t, newKey.IsValid())
 		assert.Equal(t, newKey.UsesRemaining, 1)
 		assert.Equal(t, newKey.UsesRemaining, 1)
@@ -107,9 +107,9 @@ func TestDecrement_EnrollmentKey(t *testing.T) {
 func TestUsability_EnrollmentKey(t *testing.T) {
 func TestUsability_EnrollmentKey(t *testing.T) {
 	database.InitializeDatabase()
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	defer database.CloseDB()
-	key1, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false)
-	key2, _ := CreateEnrollmentKey(0, time.Now().Add(time.Minute<<4), nil, nil, nil, false, uuid.Nil, false)
-	key3, _ := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, true, uuid.Nil, false)
+	key1, _ := CreateEnrollmentKey(1, time.Time{}, nil, nil, nil, false, uuid.Nil, false, false)
+	key2, _ := CreateEnrollmentKey(0, time.Now().Add(time.Minute<<4), nil, nil, nil, false, uuid.Nil, false, false)
+	key3, _ := CreateEnrollmentKey(0, time.Time{}, nil, nil, nil, true, uuid.Nil, false, false)
 	t.Run("Check if valid use key can be used", func(t *testing.T) {
 	t.Run("Check if valid use key can be used", func(t *testing.T) {
 		assert.Equal(t, key1.UsesRemaining, 1)
 		assert.Equal(t, key1.UsesRemaining, 1)
 		ok := TryToUseEnrollmentKey(key1)
 		ok := TryToUseEnrollmentKey(key1)
@@ -145,7 +145,7 @@ func removeAllEnrollments() {
 func TestTokenize_EnrollmentKeys(t *testing.T) {
 func TestTokenize_EnrollmentKeys(t *testing.T) {
 	database.InitializeDatabase()
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false, false)
 	const defaultValue = "MwE5MwE5MwE5MwE5MwE5MwE5MwE5MwE5"
 	const defaultValue = "MwE5MwE5MwE5MwE5MwE5MwE5MwE5MwE5"
 	const b64value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const b64value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const serverAddr = "api.myserver.com"
 	const serverAddr = "api.myserver.com"
@@ -178,7 +178,7 @@ func TestTokenize_EnrollmentKeys(t *testing.T) {
 func TestDeTokenize_EnrollmentKeys(t *testing.T) {
 func TestDeTokenize_EnrollmentKeys(t *testing.T) {
 	database.InitializeDatabase()
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	defer database.CloseDB()
-	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false)
+	newKey, _ := CreateEnrollmentKey(0, time.Time{}, []string{"mynet", "skynet"}, nil, nil, true, uuid.Nil, false, false)
 	const b64Value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const b64Value = "eyJzZXJ2ZXIiOiJhcGkubXlzZXJ2ZXIuY29tIiwidmFsdWUiOiJNd0U1TXdFNU13RTVNd0U1TXdFNU13RTVNd0U1TXdFNSJ9"
 	const serverAddr = "api.myserver.com"
 	const serverAddr = "api.myserver.com"
 
 

+ 16 - 6
logic/errors.go

@@ -8,20 +8,30 @@ import (
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 )
 )
 
 
+type ApiErrorType string
+
+const (
+	Internal     ApiErrorType = "internal"
+	BadReq       ApiErrorType = "badrequest"
+	NotFound     ApiErrorType = "notfound"
+	UnAuthorized ApiErrorType = "unauthorized"
+	Forbidden    ApiErrorType = "forbidden"
+)
+
 // FormatError - takes ErrorResponse and uses correct code
 // FormatError - takes ErrorResponse and uses correct code
-func FormatError(err error, errType string) models.ErrorResponse {
+func FormatError(err error, errType ApiErrorType) models.ErrorResponse {
 
 
 	var status = http.StatusInternalServerError
 	var status = http.StatusInternalServerError
 	switch errType {
 	switch errType {
-	case "internal":
+	case Internal:
 		status = http.StatusInternalServerError
 		status = http.StatusInternalServerError
-	case "badrequest":
+	case BadReq:
 		status = http.StatusBadRequest
 		status = http.StatusBadRequest
-	case "notfound":
+	case NotFound:
 		status = http.StatusNotFound
 		status = http.StatusNotFound
-	case "unauthorized":
+	case UnAuthorized:
 		status = http.StatusUnauthorized
 		status = http.StatusUnauthorized
-	case "forbidden":
+	case Forbidden:
 		status = http.StatusForbidden
 		status = http.StatusForbidden
 	default:
 	default:
 		status = http.StatusInternalServerError
 		status = http.StatusInternalServerError

+ 55 - 346
logic/extpeers.go

@@ -1,6 +1,7 @@
 package logic
 package logic
 
 
 import (
 import (
+	"context"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
@@ -13,9 +14,11 @@ import (
 
 
 	"github.com/goombaio/namegenerator"
 	"github.com/goombaio/namegenerator"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@@ -71,13 +74,17 @@ func GetEgressRangesOnNetwork(client *models.ExtClient) ([]string, error) {
 	if err != nil {
 	if err != nil {
 		return []string{}, err
 		return []string{}, err
 	}
 	}
+	eli, _ := (&schema.Egress{Network: client.Network}).ListByNetwork(db.WithContext(context.TODO()))
+	acls, _ := ListAclsByNetwork(models.NetworkID(client.Network))
+	// clientNode := client.ConvertToStaticNode()
 	for _, currentNode := range networkNodes {
 	for _, currentNode := range networkNodes {
 		if currentNode.Network != client.Network {
 		if currentNode.Network != client.Network {
 			continue
 			continue
 		}
 		}
-		if currentNode.IsEgressGateway { // add the egress gateway range(s) to the result
-			if len(currentNode.EgressGatewayRanges) > 0 {
-				result = append(result, currentNode.EgressGatewayRanges...)
+		GetNodeEgressInfo(&currentNode, eli, acls)
+		if currentNode.EgressDetails.IsEgressGateway { // add the egress gateway range(s) to the result
+			if len(currentNode.EgressDetails.EgressGatewayRanges) > 0 {
+				result = append(result, currentNode.EgressDetails.EgressGatewayRanges...)
 			}
 			}
 		}
 		}
 	}
 	}
@@ -89,7 +96,30 @@ func GetEgressRangesOnNetwork(client *models.ExtClient) ([]string, error) {
 		result = append(result, extclient.ExtraAllowedIPs...)
 		result = append(result, extclient.ExtraAllowedIPs...)
 	}
 	}
 
 
-	return result, nil
+	return UniqueIPNetStrList(result), nil
+}
+
+// UniqueIPNetList deduplicates and sorts a list of CIDR strings.
+func UniqueIPNetStrList(ipnets []string) []string {
+	uniqueMap := make(map[string]struct{})
+
+	for _, cidr := range ipnets {
+		_, ipnet, err := net.ParseCIDR(cidr)
+		if err != nil {
+			continue // skip invalid CIDR strings
+		}
+		key := ipnet.String() // normalized CIDR
+		uniqueMap[key] = struct{}{}
+	}
+
+	// Convert map keys to slice
+	uniqueList := make([]string, 0, len(uniqueMap))
+	for cidr := range uniqueMap {
+		uniqueList = append(uniqueList, cidr)
+	}
+
+	sort.Strings(uniqueList)
+	return uniqueList
 }
 }
 
 
 // DeleteExtClient - deletes an existing ext client
 // DeleteExtClient - deletes an existing ext client
@@ -116,6 +146,25 @@ func DeleteExtClient(network string, clientid string) error {
 		}
 		}
 		deleteExtClientFromCache(key)
 		deleteExtClientFromCache(key)
 	}
 	}
+	if extClient.RemoteAccessClientID != "" {
+		LogEvent(&models.Event{
+			Action: models.Disconnect,
+			Source: models.Subject{
+				ID:   extClient.OwnerID,
+				Name: extClient.OwnerID,
+				Type: models.UserSub,
+			},
+			TriggeredBy: extClient.OwnerID,
+			Target: models.Subject{
+				ID:   extClient.Network,
+				Name: extClient.Network,
+				Type: models.NetworkSub,
+				Info: extClient,
+			},
+			NetworkID: models.NetworkID(extClient.Network),
+			Origin:    models.ClientApp,
+		})
+	}
 	go RemoveNodeFromAclPolicy(extClient.ConvertToStaticNode())
 	go RemoveNodeFromAclPolicy(extClient.ConvertToStaticNode())
 	return nil
 	return nil
 }
 }
@@ -465,346 +514,6 @@ func ToggleExtClientConnectivity(client *models.ExtClient, enable bool) (models.
 	return newClient, nil
 	return newClient, nil
 }
 }
 
 
-// Sort a slice of net.IP addresses
-func sortIPs(ips []net.IP) {
-	sort.Slice(ips, func(i, j int) bool {
-		ip1, ip2 := ips[i].To16(), ips[j].To16()
-		return string(ip1) < string(ip2) // Compare as byte slices
-	})
-}
-
-func GetStaticNodeIps(node models.Node) (ips []net.IP) {
-	defer func() {
-		sortIPs(ips)
-	}()
-	defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
-	defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
-
-	extclients := GetStaticNodesByNetwork(models.NetworkID(node.Network), false)
-	for _, extclient := range extclients {
-		if extclient.IsUserNode && defaultUserPolicy.Enabled {
-			continue
-		}
-		if !extclient.IsUserNode && defaultDevicePolicy.Enabled {
-			continue
-		}
-		if extclient.StaticNode.Address != "" {
-			ips = append(ips, extclient.StaticNode.AddressIPNet4().IP)
-		}
-		if extclient.StaticNode.Address6 != "" {
-			ips = append(ips, extclient.StaticNode.AddressIPNet6().IP)
-		}
-	}
-	return
-}
-
-func getFwRulesForNodeAndPeerOnGw(node, peer models.Node, allowedPolicies []models.Acl) (rules []models.FwRule) {
-
-	for _, policy := range allowedPolicies {
-		// if static peer dst rule not for ingress node -> skip
-		if node.Address.IP != nil {
-			rules = append(rules, models.FwRule{
-				SrcIP: net.IPNet{
-					IP:   node.Address.IP,
-					Mask: net.CIDRMask(32, 32),
-				},
-				DstIP: net.IPNet{
-					IP:   peer.Address.IP,
-					Mask: net.CIDRMask(32, 32),
-				},
-				AllowedProtocol: policy.Proto,
-				AllowedPorts:    policy.Port,
-				Allow:           true,
-			})
-		}
-
-		if node.Address6.IP != nil {
-			rules = append(rules, models.FwRule{
-				SrcIP: net.IPNet{
-					IP:   node.Address6.IP,
-					Mask: net.CIDRMask(128, 128),
-				},
-				DstIP: net.IPNet{
-					IP:   peer.Address6.IP,
-					Mask: net.CIDRMask(128, 128),
-				},
-				AllowedProtocol: policy.Proto,
-				AllowedPorts:    policy.Port,
-				Allow:           true,
-			})
-		}
-		if policy.AllowedDirection == models.TrafficDirectionBi {
-			if node.Address.IP != nil {
-				rules = append(rules, models.FwRule{
-					SrcIP: net.IPNet{
-						IP:   peer.Address.IP,
-						Mask: net.CIDRMask(32, 32),
-					},
-					DstIP: net.IPNet{
-						IP:   node.Address.IP,
-						Mask: net.CIDRMask(32, 32),
-					},
-					AllowedProtocol: policy.Proto,
-					AllowedPorts:    policy.Port,
-					Allow:           true,
-				})
-			}
-
-			if node.Address6.IP != nil {
-				rules = append(rules, models.FwRule{
-					SrcIP: net.IPNet{
-						IP:   peer.Address6.IP,
-						Mask: net.CIDRMask(128, 128),
-					},
-					DstIP: net.IPNet{
-						IP:   node.Address6.IP,
-						Mask: net.CIDRMask(128, 128),
-					},
-					AllowedProtocol: policy.Proto,
-					AllowedPorts:    policy.Port,
-					Allow:           true,
-				})
-			}
-		}
-		if len(node.StaticNode.ExtraAllowedIPs) > 0 {
-			for _, additionalAllowedIPNet := range node.StaticNode.ExtraAllowedIPs {
-				_, ipNet, err := net.ParseCIDR(additionalAllowedIPNet)
-				if err != nil {
-					continue
-				}
-				if ipNet.IP.To4() != nil && peer.Address.IP != nil {
-					rules = append(rules, models.FwRule{
-						SrcIP: net.IPNet{
-							IP:   peer.Address.IP,
-							Mask: net.CIDRMask(32, 32),
-						},
-						DstIP: *ipNet,
-						Allow: true,
-					})
-				} else if peer.Address6.IP != nil {
-					rules = append(rules, models.FwRule{
-						SrcIP: net.IPNet{
-							IP:   peer.Address6.IP,
-							Mask: net.CIDRMask(128, 128),
-						},
-						DstIP: *ipNet,
-						Allow: true,
-					})
-				}
-
-			}
-
-		}
-		if len(peer.StaticNode.ExtraAllowedIPs) > 0 {
-			for _, additionalAllowedIPNet := range peer.StaticNode.ExtraAllowedIPs {
-				_, ipNet, err := net.ParseCIDR(additionalAllowedIPNet)
-				if err != nil {
-					continue
-				}
-				if ipNet.IP.To4() != nil && node.Address.IP != nil {
-					rules = append(rules, models.FwRule{
-						SrcIP: net.IPNet{
-							IP:   node.Address.IP,
-							Mask: net.CIDRMask(32, 32),
-						},
-						DstIP: *ipNet,
-						Allow: true,
-					})
-				} else if node.Address6.IP != nil {
-					rules = append(rules, models.FwRule{
-						SrcIP: net.IPNet{
-							IP:   node.Address6.IP,
-							Mask: net.CIDRMask(128, 128),
-						},
-						DstIP: *ipNet,
-						Allow: true,
-					})
-				}
-
-			}
-
-		}
-
-		// add egress range rules
-		for _, dstI := range policy.Dst {
-			if dstI.ID == models.EgressRange {
-				ip, cidr, err := net.ParseCIDR(dstI.Value)
-				if err == nil {
-					if ip.To4() != nil {
-						if node.Address.IP != nil {
-							rules = append(rules, models.FwRule{
-								SrcIP: net.IPNet{
-									IP:   node.Address.IP,
-									Mask: net.CIDRMask(32, 32),
-								},
-								DstIP:           *cidr,
-								AllowedProtocol: policy.Proto,
-								AllowedPorts:    policy.Port,
-								Allow:           true,
-							})
-						}
-					} else {
-						if node.Address6.IP != nil {
-							rules = append(rules, models.FwRule{
-								SrcIP: net.IPNet{
-									IP:   node.Address6.IP,
-									Mask: net.CIDRMask(128, 128),
-								},
-								DstIP:           *cidr,
-								AllowedProtocol: policy.Proto,
-								AllowedPorts:    policy.Port,
-								Allow:           true,
-							})
-						}
-					}
-
-				}
-			}
-		}
-	}
-
-	return
-}
-
-func getFwRulesForUserNodesOnGw(node models.Node, nodes []models.Node) (rules []models.FwRule) {
-	defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
-	userNodes := GetStaticUserNodesByNetwork(models.NetworkID(node.Network))
-	for _, userNodeI := range userNodes {
-		for _, peer := range nodes {
-			if peer.IsUserNode {
-				continue
-			}
-
-			if ok, allowedPolicies := IsUserAllowedToCommunicate(userNodeI.StaticNode.OwnerID, peer); ok {
-				if peer.IsStatic {
-					peer = peer.StaticNode.ConvertToStaticNode()
-				}
-				if !defaultUserPolicy.Enabled {
-					for _, policy := range allowedPolicies {
-						if userNodeI.StaticNode.Address != "" {
-							rules = append(rules, models.FwRule{
-								SrcIP: userNodeI.StaticNode.AddressIPNet4(),
-								DstIP: net.IPNet{
-									IP:   peer.Address.IP,
-									Mask: net.CIDRMask(32, 32),
-								},
-								AllowedProtocol: policy.Proto,
-								AllowedPorts:    policy.Port,
-								Allow:           true,
-							})
-						}
-						if userNodeI.StaticNode.Address6 != "" {
-							rules = append(rules, models.FwRule{
-								SrcIP: userNodeI.StaticNode.AddressIPNet6(),
-								DstIP: net.IPNet{
-									IP:   peer.Address6.IP,
-									Mask: net.CIDRMask(128, 128),
-								},
-								AllowedProtocol: policy.Proto,
-								AllowedPorts:    policy.Port,
-								Allow:           true,
-							})
-						}
-
-						// add egress ranges
-						for _, dstI := range policy.Dst {
-							if dstI.ID == models.EgressRange {
-								ip, cidr, err := net.ParseCIDR(dstI.Value)
-								if err == nil {
-									if ip.To4() != nil && userNodeI.StaticNode.Address != "" {
-										rules = append(rules, models.FwRule{
-											SrcIP:           userNodeI.StaticNode.AddressIPNet4(),
-											DstIP:           *cidr,
-											AllowedProtocol: policy.Proto,
-											AllowedPorts:    policy.Port,
-											Allow:           true,
-										})
-									} else if ip.To16() != nil && userNodeI.StaticNode.Address6 != "" {
-										rules = append(rules, models.FwRule{
-											SrcIP:           userNodeI.StaticNode.AddressIPNet6(),
-											DstIP:           *cidr,
-											AllowedProtocol: policy.Proto,
-											AllowedPorts:    policy.Port,
-											Allow:           true,
-										})
-									}
-								}
-							}
-						}
-
-					}
-				}
-
-			}
-		}
-	}
-	return
-}
-
-func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
-	// fetch user access to static clients via policies
-	defer func() {
-		sort.Slice(rules, func(i, j int) bool {
-			if !rules[i].SrcIP.IP.Equal(rules[j].SrcIP.IP) {
-				return string(rules[i].SrcIP.IP.To16()) < string(rules[j].SrcIP.IP.To16())
-			}
-			return string(rules[i].DstIP.IP.To16()) < string(rules[j].DstIP.IP.To16())
-		})
-	}()
-	defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
-	nodes, _ := GetNetworkNodes(node.Network)
-	nodes = append(nodes, GetStaticNodesByNetwork(models.NetworkID(node.Network), true)...)
-	rules = getFwRulesForUserNodesOnGw(node, nodes)
-	if defaultDevicePolicy.Enabled {
-		return
-	}
-	for _, nodeI := range nodes {
-		if !nodeI.IsStatic || nodeI.IsUserNode {
-			continue
-		}
-		// if nodeI.StaticNode.IngressGatewayID != node.ID.String() {
-		// 	continue
-		// }
-		for _, peer := range nodes {
-			if peer.StaticNode.ClientID == nodeI.StaticNode.ClientID || peer.IsUserNode {
-				continue
-			}
-			if nodeI.StaticNode.IngressGatewayID != node.ID.String() &&
-				((!peer.IsStatic && peer.ID.String() != node.ID.String()) ||
-					(peer.IsStatic && peer.StaticNode.IngressGatewayID != node.ID.String())) {
-				continue
-			}
-			if peer.IsStatic {
-				peer = peer.StaticNode.ConvertToStaticNode()
-			}
-			var allowedPolicies1 []models.Acl
-			var ok bool
-			if ok, allowedPolicies1 = IsNodeAllowedToCommunicateV1(nodeI.StaticNode.ConvertToStaticNode(), peer, true); ok {
-				rules = append(rules, getFwRulesForNodeAndPeerOnGw(nodeI.StaticNode.ConvertToStaticNode(), peer, allowedPolicies1)...)
-			}
-			if ok, allowedPolicies2 := IsNodeAllowedToCommunicateV1(peer, nodeI.StaticNode.ConvertToStaticNode(), true); ok {
-				rules = append(rules,
-					getFwRulesForNodeAndPeerOnGw(peer, nodeI.StaticNode.ConvertToStaticNode(),
-						GetUniquePolicies(allowedPolicies1, allowedPolicies2))...)
-			}
-		}
-	}
-	return
-}
-
-func GetUniquePolicies(policies1, policies2 []models.Acl) []models.Acl {
-	policies1Map := make(map[string]struct{})
-	for _, policy1I := range policies1 {
-		policies1Map[policy1I.ID] = struct{}{}
-	}
-	for i := len(policies2) - 1; i >= 0; i-- {
-		if _, ok := policies1Map[policies2[i].ID]; ok {
-			policies2 = append(policies2[:i], policies2[i+1:]...)
-		}
-	}
-	return policies2
-}
-
 func GetExtPeers(node, peer *models.Node) ([]wgtypes.PeerConfig, []models.IDandAddr, []models.EgressNetworkRoutes, error) {
 func GetExtPeers(node, peer *models.Node) ([]wgtypes.PeerConfig, []models.IDandAddr, []models.EgressNetworkRoutes, error) {
 	var peers []wgtypes.PeerConfig
 	var peers []wgtypes.PeerConfig
 	var idsAndAddr []models.IDandAddr
 	var idsAndAddr []models.IDandAddr
@@ -920,7 +629,7 @@ func getExtpeerEgressRanges(node models.Node) (ranges, ranges6 []net.IPNet) {
 		if len(extPeer.ExtraAllowedIPs) == 0 {
 		if len(extPeer.ExtraAllowedIPs) == 0 {
 			continue
 			continue
 		}
 		}
-		if ok, _ := IsNodeAllowedToCommunicateV1(extPeer.ConvertToStaticNode(), node, true); !ok {
+		if ok, _ := IsNodeAllowedToCommunicate(extPeer.ConvertToStaticNode(), node, true); !ok {
 			continue
 			continue
 		}
 		}
 		for _, allowedRange := range extPeer.ExtraAllowedIPs {
 		for _, allowedRange := range extPeer.ExtraAllowedIPs {
@@ -947,7 +656,7 @@ func getExtpeersExtraRoutes(node models.Node) (egressRoutes []models.EgressNetwo
 		if len(extPeer.ExtraAllowedIPs) == 0 {
 		if len(extPeer.ExtraAllowedIPs) == 0 {
 			continue
 			continue
 		}
 		}
-		if ok, _ := IsNodeAllowedToCommunicateV1(extPeer.ConvertToStaticNode(), node, true); !ok {
+		if ok, _ := IsNodeAllowedToCommunicate(extPeer.ConvertToStaticNode(), node, true); !ok {
 			continue
 			continue
 		}
 		}
 		egressRoutes = append(egressRoutes, getExtPeerEgressRoute(node, extPeer)...)
 		egressRoutes = append(egressRoutes, getExtPeerEgressRoute(node, extPeer)...)

+ 10 - 12
logic/gateway.go

@@ -56,7 +56,7 @@ func GetAllEgresses() ([]models.Node, error) {
 	}
 	}
 	egresses := make([]models.Node, 0)
 	egresses := make([]models.Node, 0)
 	for _, node := range nodes {
 	for _, node := range nodes {
-		if node.IsEgressGateway {
+		if node.EgressDetails.IsEgressGateway {
 			egresses = append(egresses, node)
 			egresses = append(egresses, node)
 		}
 		}
 	}
 	}
@@ -133,11 +133,11 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
 	if gateway.Ranges == nil {
 	if gateway.Ranges == nil {
 		gateway.Ranges = make([]string, 0)
 		gateway.Ranges = make([]string, 0)
 	}
 	}
-	node.IsEgressGateway = true
-	node.EgressGatewayRanges = gateway.Ranges
-	node.EgressGatewayNatEnabled = models.ParseBool(gateway.NatEnabled)
+	node.EgressDetails.IsEgressGateway = true
+	node.EgressDetails.EgressGatewayRanges = gateway.Ranges
+	node.EgressDetails.EgressGatewayNatEnabled = models.ParseBool(gateway.NatEnabled)
 
 
-	node.EgressGatewayRequest = gateway // store entire request for use when preserving the egress gateway
+	node.EgressDetails.EgressGatewayRequest = gateway // store entire request for use when preserving the egress gateway
 	node.SetLastModified()
 	node.SetLastModified()
 	if err = UpsertNode(&node); err != nil {
 	if err = UpsertNode(&node); err != nil {
 		return models.Node{}, err
 		return models.Node{}, err
@@ -156,9 +156,9 @@ func DeleteEgressGateway(network, nodeid string) (models.Node, error) {
 	if err != nil {
 	if err != nil {
 		return models.Node{}, err
 		return models.Node{}, err
 	}
 	}
-	node.IsEgressGateway = false
-	node.EgressGatewayRanges = []string{}
-	node.EgressGatewayRequest = models.EgressGatewayRequest{} // remove preserved request as the egress gateway is gone
+	node.EgressDetails.IsEgressGateway = false
+	node.EgressDetails.EgressGatewayRanges = []string{}
+	node.EgressDetails.EgressGatewayRequest = models.EgressGatewayRequest{} // remove preserved request as the egress gateway is gone
 	node.SetLastModified()
 	node.SetLastModified()
 	if err = UpsertNode(&node); err != nil {
 	if err = UpsertNode(&node); err != nil {
 		return models.Node{}, err
 		return models.Node{}, err
@@ -190,9 +190,7 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	}
 	}
 	node.IsIngressGateway = true
 	node.IsIngressGateway = true
 	node.IsGw = true
 	node.IsGw = true
-	if !servercfg.IsPro {
-		node.IsInternetGateway = ingress.IsInternetGateway
-	}
+	node.IsInternetGateway = ingress.IsInternetGateway
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange = network.AddressRange
 	node.IngressGatewayRange6 = network.AddressRange6
 	node.IngressGatewayRange6 = network.AddressRange6
 	node.IngressDNS = ingress.ExtclientDNS
 	node.IngressDNS = ingress.ExtclientDNS
@@ -267,7 +265,7 @@ func DeleteIngressGateway(nodeid string) (models.Node, []models.ExtClient, error
 		return models.Node{}, removedClients, err
 		return models.Node{}, removedClients, err
 	}
 	}
 	logger.Log(3, "deleting ingress gateway")
 	logger.Log(3, "deleting ingress gateway")
-	node.LastModified = time.Now()
+	node.LastModified = time.Now().UTC()
 	node.IsIngressGateway = false
 	node.IsIngressGateway = false
 	if !servercfg.IsPro {
 	if !servercfg.IsPro {
 		node.IsInternetGateway = false
 		node.IsInternetGateway = false

+ 27 - 4
logic/hosts.go

@@ -228,7 +228,13 @@ func CreateHost(h *models.Host) error {
 		return err
 		return err
 	}
 	}
 	h.HostPass = string(hash)
 	h.HostPass = string(hash)
-	h.AutoUpdate = servercfg.AutoUpdateEnabled()
+	h.AutoUpdate = AutoUpdateEnabled()
+
+	if GetServerSettings().ManageDNS {
+		h.DNS = "yes"
+	} else {
+		h.DNS = "no"
+	}
 	checkForZombieHosts(h)
 	checkForZombieHosts(h)
 	return UpsertHost(h)
 	return UpsertHost(h)
 }
 }
@@ -393,7 +399,7 @@ func UpdateHostNetwork(h *models.Host, network string, add bool) (*models.Node,
 			if !add {
 			if !add {
 				return &node, nil
 				return &node, nil
 			} else {
 			} else {
-				return nil, errors.New("host already part of network " + network)
+				return &node, errors.New("host already part of network " + network)
 			}
 			}
 		}
 		}
 	}
 	}
@@ -548,17 +554,29 @@ func GetRelatedHosts(hostID string) []models.Host {
 // CheckHostPort checks host endpoints to ensures that hosts on the same server
 // CheckHostPort checks host endpoints to ensures that hosts on the same server
 // with the same endpoint have different listen ports
 // with the same endpoint have different listen ports
 // in the case of 64535 hosts or more with same endpoint, ports will not be changed
 // in the case of 64535 hosts or more with same endpoint, ports will not be changed
-func CheckHostPorts(h *models.Host) {
+func CheckHostPorts(h *models.Host) (changed bool) {
 	portsInUse := make(map[int]bool, 0)
 	portsInUse := make(map[int]bool, 0)
 	hosts, err := GetAllHosts()
 	hosts, err := GetAllHosts()
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
+	originalPort := h.ListenPort
+	defer func() {
+		if originalPort != h.ListenPort {
+			changed = true
+		}
+	}()
+	if h.EndpointIP == nil {
+		return
+	}
 	for _, host := range hosts {
 	for _, host := range hosts {
 		if host.ID.String() == h.ID.String() {
 		if host.ID.String() == h.ID.String() {
 			// skip self
 			// skip self
 			continue
 			continue
 		}
 		}
+		if host.EndpointIP == nil {
+			continue
+		}
 		if !host.EndpointIP.Equal(h.EndpointIP) {
 		if !host.EndpointIP.Equal(h.EndpointIP) {
 			continue
 			continue
 		}
 		}
@@ -566,11 +584,16 @@ func CheckHostPorts(h *models.Host) {
 	}
 	}
 	// iterate until port is not found or max iteration is reached
 	// iterate until port is not found or max iteration is reached
 	for i := 0; portsInUse[h.ListenPort] && i < maxPort-minPort+1; i++ {
 	for i := 0; portsInUse[h.ListenPort] && i < maxPort-minPort+1; i++ {
-		h.ListenPort++
+		if h.ListenPort == 443 {
+			h.ListenPort = 51821
+		} else {
+			h.ListenPort++
+		}
 		if h.ListenPort > maxPort {
 		if h.ListenPort > maxPort {
 			h.ListenPort = minPort
 			h.ListenPort = minPort
 		}
 		}
 	}
 	}
+	return
 }
 }
 
 
 // HostExists - checks if given host already exists
 // HostExists - checks if given host already exists

+ 59 - 14
logic/jwts.go

@@ -1,6 +1,7 @@
 package logic
 package logic
 
 
 import (
 import (
+	"context"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"strings"
 	"strings"
@@ -8,8 +9,10 @@ import (
 
 
 	"github.com/golang-jwt/jwt/v4"
 	"github.com/golang-jwt/jwt/v4"
 
 
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
@@ -53,17 +56,19 @@ func CreateJWT(uuid string, macAddress string, network string) (response string,
 }
 }
 
 
 // CreateUserJWT - creates a user jwt token
 // CreateUserJWT - creates a user jwt token
-func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) {
-	expirationTime := time.Now().Add(servercfg.GetServerConfig().JwtValidityDuration)
+func CreateUserAccessJwtToken(username string, role models.UserRoleID, d time.Time, tokenID string) (response string, err error) {
 	claims := &models.UserClaims{
 	claims := &models.UserClaims{
 		UserName:       username,
 		UserName:       username,
 		Role:           role,
 		Role:           role,
-		RacAutoDisable: servercfg.GetRacAutoDisable() && (role != models.SuperAdminRole && role != models.AdminRole),
+		TokenType:      models.AccessTokenType,
+		Api:            servercfg.GetAPIHost(),
+		RacAutoDisable: GetRacAutoDisable() && (role != models.SuperAdminRole && role != models.AdminRole),
 		RegisteredClaims: jwt.RegisteredClaims{
 		RegisteredClaims: jwt.RegisteredClaims{
 			Issuer:    "Netmaker",
 			Issuer:    "Netmaker",
 			Subject:   fmt.Sprintf("user|%s", username),
 			Subject:   fmt.Sprintf("user|%s", username),
 			IssuedAt:  jwt.NewNumericDate(time.Now()),
 			IssuedAt:  jwt.NewNumericDate(time.Now()),
-			ExpiresAt: jwt.NewNumericDate(expirationTime),
+			ExpiresAt: jwt.NewNumericDate(d),
+			ID:        tokenID,
 		},
 		},
 	}
 	}
 
 
@@ -75,16 +80,29 @@ func CreateUserJWT(username string, role models.UserRoleID) (response string, er
 	return "", err
 	return "", err
 }
 }
 
 
-// VerifyJWT verifies Auth Header
-func VerifyJWT(bearerToken string) (username string, issuperadmin, isadmin bool, err error) {
-	token := ""
-	tokenSplit := strings.Split(bearerToken, " ")
-	if len(tokenSplit) > 1 {
-		token = tokenSplit[1]
-	} else {
-		return "", false, false, errors.New("invalid auth header")
+// CreateUserJWT - creates a user jwt token
+func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) {
+	settings := GetServerSettings()
+	expirationTime := time.Now().Add(time.Duration(settings.JwtValidityDuration) * time.Minute)
+	claims := &models.UserClaims{
+		UserName:       username,
+		Role:           role,
+		TokenType:      models.UserIDTokenType,
+		RacAutoDisable: settings.RacAutoDisable && (role != models.SuperAdminRole && role != models.AdminRole),
+		RegisteredClaims: jwt.RegisteredClaims{
+			Issuer:    "Netmaker",
+			Subject:   fmt.Sprintf("user|%s", username),
+			IssuedAt:  jwt.NewNumericDate(time.Now()),
+			ExpiresAt: jwt.NewNumericDate(expirationTime),
+		},
+	}
+
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+	tokenString, err := token.SignedString(jwtSecretKey)
+	if err == nil {
+		return tokenString, nil
 	}
 	}
-	return VerifyUserToken(token)
+	return "", err
 }
 }
 
 
 func GetUserNameFromToken(authtoken string) (username string, err error) {
 func GetUserNameFromToken(authtoken string) (username string, err error) {
@@ -107,6 +125,20 @@ func GetUserNameFromToken(authtoken string) (username string, err error) {
 	if err != nil {
 	if err != nil {
 		return "", Unauthorized_Err
 		return "", Unauthorized_Err
 	}
 	}
+	if claims.TokenType == models.AccessTokenType {
+		jti := claims.ID
+		if jti != "" {
+			a := schema.UserAccessToken{ID: jti}
+			// check if access token is active
+			err := a.Get(db.WithContext(context.TODO()))
+			if err != nil {
+				err = errors.New("token revoked")
+				return "", err
+			}
+			a.LastUsed = time.Now().UTC()
+			a.Update(db.WithContext(context.TODO()))
+		}
+	}
 
 
 	if token != nil && token.Valid {
 	if token != nil && token.Valid {
 		var user *models.User
 		var user *models.User
@@ -139,7 +171,20 @@ func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin
 	token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
 	token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
 		return jwtSecretKey, nil
 		return jwtSecretKey, nil
 	})
 	})
-
+	if claims.TokenType == models.AccessTokenType {
+		jti := claims.ID
+		if jti != "" {
+			a := schema.UserAccessToken{ID: jti}
+			// check if access token is active
+			err := a.Get(db.WithContext(context.TODO()))
+			if err != nil {
+				err = errors.New("token revoked")
+				return "", false, false, err
+			}
+			a.LastUsed = time.Now().UTC()
+			a.Update(db.WithContext(context.TODO()))
+		}
+	}
 	if token != nil && token.Valid {
 	if token != nil && token.Valid {
 		var user *models.User
 		var user *models.User
 		// check that user exists
 		// check that user exists

+ 1 - 2
logic/networks.go

@@ -302,6 +302,7 @@ func CreateNetwork(network models.Network) (models.Network, error) {
 		true,
 		true,
 		uuid.Nil,
 		uuid.Nil,
 		true,
 		true,
+		false,
 	)
 	)
 
 
 	return network, nil
 	return network, nil
@@ -521,7 +522,6 @@ func UniqueAddress6DB(networkName string, reverse bool) (net.IP, error) {
 	var network models.Network
 	var network models.Network
 	network, err := GetParentNetwork(networkName)
 	network, err := GetParentNetwork(networkName)
 	if err != nil {
 	if err != nil {
-		fmt.Println("Network Not Found")
 		return add, err
 		return add, err
 	}
 	}
 	if network.IsIPv6 == "no" {
 	if network.IsIPv6 == "no" {
@@ -566,7 +566,6 @@ func UniqueAddress6Cache(networkName string, reverse bool) (net.IP, error) {
 	var network models.Network
 	var network models.Network
 	network, err := GetParentNetwork(networkName)
 	network, err := GetParentNetwork(networkName)
 	if err != nil {
 	if err != nil {
-		fmt.Println("Network Not Found")
 		return add, err
 		return add, err
 	}
 	}
 	if network.IsIPv6 == "no" {
 	if network.IsIPv6 == "no" {

+ 10 - 196
logic/nodes.go

@@ -164,7 +164,7 @@ func UpdateNodeCheckin(node *models.Node) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-
+	node.EgressDetails = models.EgressDetails{}
 	err = database.Insert(node.ID.String(), string(data), database.NODES_TABLE_NAME)
 	err = database.Insert(node.ID.String(), string(data), database.NODES_TABLE_NAME)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -183,6 +183,7 @@ func UpsertNode(newNode *models.Node) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
+	newNode.EgressDetails = models.EgressDetails{}
 	err = database.Insert(newNode.ID.String(), string(data), database.NODES_TABLE_NAME)
 	err = database.Insert(newNode.ID.String(), string(data), database.NODES_TABLE_NAME)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -218,7 +219,7 @@ func UpdateNode(currentNode *models.Node, newNode *models.Node) error {
 				return err
 				return err
 			}
 			}
 		}
 		}
-
+		newNode.EgressDetails = models.EgressDetails{}
 		newNode.SetLastModified()
 		newNode.SetLastModified()
 		if data, err := json.Marshal(newNode); err != nil {
 		if data, err := json.Marshal(newNode); err != nil {
 			return err
 			return err
@@ -320,8 +321,9 @@ func DeleteNode(node *models.Node, purge bool) error {
 	if err := DissasociateNodeFromHost(node, host); err != nil {
 	if err := DissasociateNodeFromHost(node, host); err != nil {
 		return err
 		return err
 	}
 	}
-	go RemoveNodeFromAclPolicy(*node)
 
 
+	go RemoveNodeFromAclPolicy(*node)
+	go RemoveNodeFromEgress(*node)
 	return nil
 	return nil
 }
 }
 
 
@@ -639,14 +641,6 @@ func createNode(node *models.Node) error {
 		return err
 		return err
 	}
 	}
 
 
-	if !node.DNSOn {
-		if servercfg.IsDNSMode() {
-			node.DNSOn = true
-		} else {
-			node.DNSOn = false
-		}
-	}
-
 	SetNodeDefaults(node, true)
 	SetNodeDefaults(node, true)
 
 
 	defaultACLVal := acls.Allowed
 	defaultACLVal := acls.Allowed
@@ -783,16 +777,16 @@ func ValidateNodeIp(currentNode *models.Node, newNode *models.ApiNode) error {
 	return nil
 	return nil
 }
 }
 
 
-func ValidateEgressRange(gateway models.EgressGatewayRequest) error {
-	network, err := GetNetworkSettings(gateway.NetID)
+func ValidateEgressRange(netID string, ranges []string) error {
+	network, err := GetNetworkSettings(netID)
 	if err != nil {
 	if err != nil {
-		slog.Error("error getting network with netid", "error", gateway.NetID, err.Error)
-		return errors.New("error getting network with netid:  " + gateway.NetID + " " + err.Error())
+		slog.Error("error getting network with netid", "error", netID, err.Error)
+		return errors.New("error getting network with netid:  " + netID + " " + err.Error())
 	}
 	}
 	ipv4Net := network.AddressRange
 	ipv4Net := network.AddressRange
 	ipv6Net := network.AddressRange6
 	ipv6Net := network.AddressRange6
 
 
-	for _, v := range gateway.Ranges {
+	for _, v := range ranges {
 		if ipv4Net != "" {
 		if ipv4Net != "" {
 			if ContainsCIDR(ipv4Net, v) {
 			if ContainsCIDR(ipv4Net, v) {
 				slog.Error("egress range should not be the same as or contained in the netmaker network address", "error", v, ipv4Net)
 				slog.Error("egress range should not be the same as or contained in the netmaker network address", "error", v, ipv4Net)
@@ -830,183 +824,3 @@ func GetAllFailOvers() ([]models.Node, error) {
 	}
 	}
 	return igs, nil
 	return igs, nil
 }
 }
-
-func GetTagMapWithNodes() (tagNodesMap map[models.TagID][]models.Node) {
-	tagNodesMap = make(map[models.TagID][]models.Node)
-	nodes, _ := GetAllNodes()
-	for _, nodeI := range nodes {
-		if nodeI.Tags == nil {
-			continue
-		}
-		if nodeI.Mutex != nil {
-			nodeI.Mutex.Lock()
-		}
-		for nodeTagID := range nodeI.Tags {
-			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)
-		}
-		if nodeI.Mutex != nil {
-			nodeI.Mutex.Unlock()
-		}
-
-	}
-	return
-}
-
-func GetTagMapWithNodesByNetwork(netID models.NetworkID, withStaticNodes bool) (tagNodesMap map[models.TagID][]models.Node) {
-	tagNodesMap = make(map[models.TagID][]models.Node)
-	nodes, _ := GetNetworkNodes(netID.String())
-	for _, nodeI := range nodes {
-		tagNodesMap[models.TagID(nodeI.ID.String())] = []models.Node{
-			nodeI,
-		}
-		if nodeI.Tags == nil {
-			continue
-		}
-		if nodeI.Mutex != nil {
-			nodeI.Mutex.Lock()
-		}
-		for nodeTagID := range nodeI.Tags {
-			if nodeTagID == models.TagID(nodeI.ID.String()) {
-				continue
-			}
-			tagNodesMap[nodeTagID] = append(tagNodesMap[nodeTagID], nodeI)
-		}
-		if nodeI.Mutex != nil {
-			nodeI.Mutex.Unlock()
-		}
-	}
-	tagNodesMap["*"] = nodes
-	if !withStaticNodes {
-		return
-	}
-	return AddTagMapWithStaticNodes(netID, tagNodesMap)
-}
-
-func AddTagMapWithStaticNodes(netID models.NetworkID,
-	tagNodesMap map[models.TagID][]models.Node) map[models.TagID][]models.Node {
-	extclients, err := GetNetworkExtClients(netID.String())
-	if err != nil {
-		return tagNodesMap
-	}
-	for _, extclient := range extclients {
-		if extclient.RemoteAccessClientID != "" {
-			continue
-		}
-		tagNodesMap[models.TagID(extclient.ClientID)] = []models.Node{
-			{
-				IsStatic:   true,
-				StaticNode: extclient,
-			},
-		}
-		if extclient.Tags == nil {
-			continue
-		}
-
-		if extclient.Mutex != nil {
-			extclient.Mutex.Lock()
-		}
-		for tagID := range extclient.Tags {
-			if tagID == models.TagID(extclient.ClientID) {
-				continue
-			}
-			tagNodesMap[tagID] = append(tagNodesMap[tagID], extclient.ConvertToStaticNode())
-			tagNodesMap["*"] = append(tagNodesMap["*"], extclient.ConvertToStaticNode())
-		}
-		if extclient.Mutex != nil {
-			extclient.Mutex.Unlock()
-		}
-	}
-	return tagNodesMap
-}
-
-func AddTagMapWithStaticNodesWithUsers(netID models.NetworkID,
-	tagNodesMap map[models.TagID][]models.Node) map[models.TagID][]models.Node {
-	extclients, err := GetNetworkExtClients(netID.String())
-	if err != nil {
-		return tagNodesMap
-	}
-	for _, extclient := range extclients {
-		tagNodesMap[models.TagID(extclient.ClientID)] = []models.Node{
-			{
-				IsStatic:   true,
-				StaticNode: extclient,
-			},
-		}
-		if extclient.Tags == nil {
-			continue
-		}
-		if extclient.Mutex != nil {
-			extclient.Mutex.Lock()
-		}
-		for tagID := range extclient.Tags {
-			tagNodesMap[tagID] = append(tagNodesMap[tagID], extclient.ConvertToStaticNode())
-		}
-		if extclient.Mutex != nil {
-			extclient.Mutex.Unlock()
-		}
-
-	}
-	return tagNodesMap
-}
-
-func GetNodesWithTag(tagID models.TagID) map[string]models.Node {
-	nMap := make(map[string]models.Node)
-	tag, err := GetTag(tagID)
-	if err != nil {
-		return nMap
-	}
-	nodes, _ := GetNetworkNodes(tag.Network.String())
-	for _, nodeI := range nodes {
-		if nodeI.Tags == nil {
-			continue
-		}
-		if nodeI.Mutex != nil {
-			nodeI.Mutex.Lock()
-		}
-		if _, ok := nodeI.Tags[tagID]; ok {
-			nMap[nodeI.ID.String()] = nodeI
-		}
-		if nodeI.Mutex != nil {
-			nodeI.Mutex.Unlock()
-		}
-	}
-	return AddStaticNodesWithTag(tag, nMap)
-}
-
-func AddStaticNodesWithTag(tag models.Tag, nMap map[string]models.Node) map[string]models.Node {
-	extclients, err := GetNetworkExtClients(tag.Network.String())
-	if err != nil {
-		return nMap
-	}
-	for _, extclient := range extclients {
-		if extclient.RemoteAccessClientID != "" {
-			continue
-		}
-		if extclient.Mutex != nil {
-			extclient.Mutex.Lock()
-		}
-		if _, ok := extclient.Tags[tag.ID]; ok {
-			nMap[extclient.ClientID] = extclient.ConvertToStaticNode()
-		}
-		if extclient.Mutex != nil {
-			extclient.Mutex.Unlock()
-		}
-	}
-	return nMap
-}
-
-func GetStaticNodeWithTag(tagID models.TagID) map[string]models.Node {
-	nMap := make(map[string]models.Node)
-	tag, err := GetTag(tagID)
-	if err != nil {
-		return nMap
-	}
-	extclients, err := GetNetworkExtClients(tag.Network.String())
-	if err != nil {
-		return nMap
-	}
-	for _, extclient := range extclients {
-		nMap[extclient.ClientID] = extclient.ConvertToStaticNode()
-	}
-	return nMap
-}

+ 77 - 51
logic/peers.go

@@ -1,15 +1,20 @@
 package logic
 package logic
 
 
 import (
 import (
+	"context"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net"
 	"net"
 	"net/netip"
 	"net/netip"
+	"time"
 
 
+	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/exp/slices"
 	"golang.org/x/exp/slices"
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
@@ -37,7 +42,6 @@ var (
 	CreateFailOver = func(node models.Node) error {
 	CreateFailOver = func(node models.Node) error {
 		return nil
 		return nil
 	}
 	}
-
 	// SetDefaulGw
 	// SetDefaulGw
 	SetDefaultGw = func(node models.Node, peerUpdate models.HostPeerUpdate) models.HostPeerUpdate {
 	SetDefaultGw = func(node models.Node, peerUpdate models.HostPeerUpdate) models.HostPeerUpdate {
 		return peerUpdate
 		return peerUpdate
@@ -106,7 +110,7 @@ func GetHostPeerInfo(host *models.Host) (models.HostPeerInfo, error) {
 				!peer.PendingDelete &&
 				!peer.PendingDelete &&
 				peer.Connected &&
 				peer.Connected &&
 				nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
 				nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
-				(defaultDevicePolicy.Enabled || allowedToComm) {
+				(allowedToComm) {
 
 
 				networkPeersInfo[peerHost.PublicKey.String()] = models.IDandAddr{
 				networkPeersInfo[peerHost.PublicKey.String()] = models.IDandAddr{
 					ID:         peer.ID.String(),
 					ID:         peer.ID.String(),
@@ -157,30 +161,23 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 		Peers:           []wgtypes.PeerConfig{},
 		Peers:           []wgtypes.PeerConfig{},
 		NodePeers:       []wgtypes.PeerConfig{},
 		NodePeers:       []wgtypes.PeerConfig{},
 		HostNetworkInfo: models.HostInfoMap{},
 		HostNetworkInfo: models.HostInfoMap{},
-		ServerConfig:    servercfg.ServerInfo,
+		ServerConfig:    GetServerInfo(),
+	}
+	if host.DNS == "no" {
+		hostPeerUpdate.ManageDNS = false
 	}
 	}
 	defer func() {
 	defer func() {
 		if !hostPeerUpdate.FwUpdate.AllowAll {
 		if !hostPeerUpdate.FwUpdate.AllowAll {
-			aclRule := models.AclRule{
-				ID:              "allowed-network-rules",
-				AllowedProtocol: models.ALL,
-				Direction:       models.TrafficDirectionBi,
-				Allowed:         true,
-			}
-			for _, allowedNet := range hostPeerUpdate.FwUpdate.AllowedNetworks {
-				if allowedNet.IP.To4() != nil {
-					aclRule.IPList = append(aclRule.IPList, allowedNet)
-				} else {
-					aclRule.IP6List = append(aclRule.IP6List, allowedNet)
-				}
-			}
-			hostPeerUpdate.FwUpdate.AclRules["allowed-network-rules"] = aclRule
+
 			hostPeerUpdate.FwUpdate.EgressInfo["allowed-network-rules"] = models.EgressInfo{
 			hostPeerUpdate.FwUpdate.EgressInfo["allowed-network-rules"] = models.EgressInfo{
-				EgressID: "allowed-network-rules",
-				EgressFwRules: map[string]models.AclRule{
-					"allowed-network-rules": aclRule,
-				},
+				EgressID:      "allowed-network-rules",
+				EgressFwRules: make(map[string]models.AclRule),
+			}
+			for _, aclRule := range hostPeerUpdate.FwUpdate.AllowedNetworks {
+				hostPeerUpdate.FwUpdate.AclRules[aclRule.ID] = aclRule
+				hostPeerUpdate.FwUpdate.EgressInfo["allowed-network-rules"].EgressFwRules[aclRule.ID] = aclRule
 			}
 			}
+
 		}
 		}
 	}()
 	}()
 
 
@@ -189,28 +186,41 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 	for _, nodeID := range host.Nodes {
 	for _, nodeID := range host.Nodes {
 		networkAllowAll := true
 		networkAllowAll := true
 		nodeID := nodeID
 		nodeID := nodeID
+		if nodeID == uuid.Nil.String() {
+			continue
+		}
 		node, err := GetNodeByID(nodeID)
 		node, err := GetNodeByID(nodeID)
 		if err != nil {
 		if err != nil {
 			continue
 			continue
 		}
 		}
 
 
-		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE {
+		if !node.Connected || node.PendingDelete || node.Action == models.NODE_DELETE || time.Since(node.LastCheckIn) > time.Hour {
 			continue
 			continue
 		}
 		}
+		acls, _ := ListAclsByNetwork(models.NetworkID(node.Network))
+		eli, _ := (&schema.Egress{Network: node.Network}).ListByNetwork(db.WithContext(context.TODO()))
+		GetNodeEgressInfo(&node, eli, acls)
 		hostPeerUpdate = SetDefaultGw(node, hostPeerUpdate)
 		hostPeerUpdate = SetDefaultGw(node, hostPeerUpdate)
 		if !hostPeerUpdate.IsInternetGw {
 		if !hostPeerUpdate.IsInternetGw {
 			hostPeerUpdate.IsInternetGw = IsInternetGw(node)
 			hostPeerUpdate.IsInternetGw = IsInternetGw(node)
 		}
 		}
 		defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
 		defaultUserPolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.UserPolicy)
 		defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
 		defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
-
-		if (defaultDevicePolicy.Enabled && defaultUserPolicy.Enabled) || (!checkIfAnyPolicyisUniDirectional(node) && !checkIfAnyActiveEgressPolicy(node)) {
-			if node.NetworkRange.IP != nil {
-				hostPeerUpdate.FwUpdate.AllowedNetworks = append(hostPeerUpdate.FwUpdate.AllowedNetworks, node.NetworkRange)
+		if (defaultDevicePolicy.Enabled && defaultUserPolicy.Enabled) ||
+			(!CheckIfAnyPolicyisUniDirectional(node, acls) && !CheckIfAnyActiveEgressPolicy(node, acls)) {
+			aclRule := models.AclRule{
+				ID:              fmt.Sprintf("%s-allowed-network-rules", node.ID.String()),
+				AllowedProtocol: models.ALL,
+				Direction:       models.TrafficDirectionBi,
+				Allowed:         true,
+				IPList:          []net.IPNet{node.NetworkRange},
+				IP6List:         []net.IPNet{node.NetworkRange6},
 			}
 			}
-			if node.NetworkRange6.IP != nil {
-				hostPeerUpdate.FwUpdate.AllowedNetworks = append(hostPeerUpdate.FwUpdate.AllowedNetworks, node.NetworkRange6)
+			if !(defaultDevicePolicy.Enabled && defaultUserPolicy.Enabled) {
+				aclRule.Dst = []net.IPNet{node.NetworkRange}
+				aclRule.Dst6 = []net.IPNet{node.NetworkRange6}
 			}
 			}
+			hostPeerUpdate.FwUpdate.AllowedNetworks = append(hostPeerUpdate.FwUpdate.AllowedNetworks, aclRule)
 		} else {
 		} else {
 			networkAllowAll = false
 			networkAllowAll = false
 			hostPeerUpdate.FwUpdate.AllowAll = false
 			hostPeerUpdate.FwUpdate.AllowAll = false
@@ -247,8 +257,12 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				PersistentKeepaliveInterval: &peerHost.PersistentKeepalive,
 				PersistentKeepaliveInterval: &peerHost.PersistentKeepalive,
 				ReplaceAllowedIPs:           true,
 				ReplaceAllowedIPs:           true,
 			}
 			}
+			GetNodeEgressInfo(&peer, eli, acls)
+			if peer.EgressDetails.IsEgressGateway {
+				AddEgressInfoToPeerByAccess(&node, &peer, eli, acls, defaultDevicePolicy.Enabled)
+			}
 			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
 			_, isFailOverPeer := node.FailOverPeers[peer.ID.String()]
-			if peer.IsEgressGateway {
+			if peer.EgressDetails.IsEgressGateway {
 				peerKey := peerHost.PublicKey.String()
 				peerKey := peerHost.PublicKey.String()
 				if isFailOverPeer && peer.FailedOverBy.String() != node.ID.String() {
 				if isFailOverPeer && peer.FailedOverBy.String() != node.ID.String() {
 					// get relay host
 					// get relay host
@@ -361,8 +375,8 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				!peer.PendingDelete &&
 				!peer.PendingDelete &&
 				peer.Connected &&
 				peer.Connected &&
 				nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
 				nodeacls.AreNodesAllowed(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), nodeacls.NodeID(peer.ID.String())) &&
-				(defaultDevicePolicy.Enabled || allowedToComm) &&
-				(deletedNode == nil || (deletedNode != nil && peer.ID.String() != deletedNode.ID.String())) {
+				(allowedToComm) &&
+				(deletedNode == nil || (peer.ID.String() != deletedNode.ID.String())) {
 				peerConfig.AllowedIPs = GetAllowedIPs(&node, &peer, nil) // only append allowed IPs if valid connection
 				peerConfig.AllowedIPs = GetAllowedIPs(&node, &peer, nil) // only append allowed IPs if valid connection
 			}
 			}
 
 
@@ -435,7 +449,7 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 				logger.Log(1, "error retrieving external clients:", err.Error())
 				logger.Log(1, "error retrieving external clients:", err.Error())
 			}
 			}
 		}
 		}
-		if node.IsEgressGateway && node.EgressGatewayRequest.NatEnabled == "yes" && len(node.EgressGatewayRequest.Ranges) > 0 {
+		if node.EgressDetails.IsEgressGateway && len(node.EgressDetails.EgressGatewayRequest.Ranges) > 0 {
 			hostPeerUpdate.FwUpdate.IsEgressGw = true
 			hostPeerUpdate.FwUpdate.IsEgressGw = true
 			hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()] = models.EgressInfo{
 			hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()] = models.EgressInfo{
 				EgressID: node.ID.String(),
 				EgressID: node.ID.String(),
@@ -449,12 +463,12 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 					IP:   node.Address6.IP,
 					IP:   node.Address6.IP,
 					Mask: getCIDRMaskFromAddr(node.Address6.IP.String()),
 					Mask: getCIDRMaskFromAddr(node.Address6.IP.String()),
 				},
 				},
-				EgressGWCfg:   node.EgressGatewayRequest,
+				EgressGWCfg:   node.EgressDetails.EgressGatewayRequest,
 				EgressFwRules: make(map[string]models.AclRule),
 				EgressFwRules: make(map[string]models.AclRule),
 			}
 			}
 
 
 		}
 		}
-		if node.IsEgressGateway {
+		if node.EgressDetails.IsEgressGateway {
 			if !networkAllowAll {
 			if !networkAllowAll {
 				egressInfo := hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()]
 				egressInfo := hostPeerUpdate.FwUpdate.EgressInfo[node.ID.String()]
 				if egressInfo.EgressFwRules == nil {
 				if egressInfo.EgressFwRules == nil {
@@ -472,7 +486,15 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 			if node.Address6.IP != nil {
 			if node.Address6.IP != nil {
 				egressrange = append(egressrange, "::/0")
 				egressrange = append(egressrange, "::/0")
 			}
 			}
-			hostPeerUpdate.FwUpdate.EgressInfo[fmt.Sprintf("%s-%s", node.ID.String(), "inet")] = models.EgressInfo{
+			rangeWithMetric := []models.EgressRangeMetric{}
+			for _, rangeI := range egressrange {
+				rangeWithMetric = append(rangeWithMetric, models.EgressRangeMetric{
+					Network:     rangeI,
+					RouteMetric: 256,
+					Nat:         true,
+				})
+			}
+			inetEgressInfo := models.EgressInfo{
 				EgressID: fmt.Sprintf("%s-%s", node.ID.String(), "inet"),
 				EgressID: fmt.Sprintf("%s-%s", node.ID.String(), "inet"),
 				Network:  node.PrimaryAddressIPNet(),
 				Network:  node.PrimaryAddressIPNet(),
 				EgressGwAddr: net.IPNet{
 				EgressGwAddr: net.IPNet{
@@ -485,14 +507,18 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
 					Mask: getCIDRMaskFromAddr(node.Address6.IP.String()),
 					Mask: getCIDRMaskFromAddr(node.Address6.IP.String()),
 				},
 				},
 				EgressGWCfg: models.EgressGatewayRequest{
 				EgressGWCfg: models.EgressGatewayRequest{
-					NodeID:     fmt.Sprintf("%s-%s", node.ID.String(), "inet"),
-					NetID:      node.Network,
-					NatEnabled: "yes",
-					Ranges:     egressrange,
+					NodeID:           fmt.Sprintf("%s-%s", node.ID.String(), "inet"),
+					NetID:            node.Network,
+					NatEnabled:       "yes",
+					Ranges:           egressrange,
+					RangesWithMetric: rangeWithMetric,
 				},
 				},
 			}
 			}
+			if !networkAllowAll {
+				inetEgressInfo.EgressFwRules = GetAclRuleForInetGw(node)
+			}
+			hostPeerUpdate.FwUpdate.EgressInfo[fmt.Sprintf("%s-%s", node.ID.String(), "inet")] = inetEgressInfo
 		}
 		}
-
 	}
 	}
 	// == post peer calculations ==
 	// == post peer calculations ==
 	// indicate removal if no allowed IPs were calculated
 	// indicate removal if no allowed IPs were calculated
@@ -549,11 +575,11 @@ func GetPeerListenPort(host *models.Host) int {
 }
 }
 
 
 func filterConflictingEgressRoutes(node, peer models.Node) []string {
 func filterConflictingEgressRoutes(node, peer models.Node) []string {
-	egressIPs := slices.Clone(peer.EgressGatewayRanges)
-	if node.IsEgressGateway {
+	egressIPs := slices.Clone(peer.EgressDetails.EgressGatewayRanges)
+	if node.EgressDetails.IsEgressGateway {
 		// filter conflicting addrs
 		// filter conflicting addrs
 		nodeEgressMap := make(map[string]struct{})
 		nodeEgressMap := make(map[string]struct{})
-		for _, rangeI := range node.EgressGatewayRanges {
+		for _, rangeI := range node.EgressDetails.EgressGatewayRanges {
 			nodeEgressMap[rangeI] = struct{}{}
 			nodeEgressMap[rangeI] = struct{}{}
 		}
 		}
 		for i := len(egressIPs) - 1; i >= 0; i-- {
 		for i := len(egressIPs) - 1; i >= 0; i-- {
@@ -567,11 +593,11 @@ func filterConflictingEgressRoutes(node, peer models.Node) []string {
 }
 }
 
 
 func filterConflictingEgressRoutesWithMetric(node, peer models.Node) []models.EgressRangeMetric {
 func filterConflictingEgressRoutesWithMetric(node, peer models.Node) []models.EgressRangeMetric {
-	egressIPs := slices.Clone(peer.EgressGatewayRequest.RangesWithMetric)
-	if node.IsEgressGateway {
+	egressIPs := slices.Clone(peer.EgressDetails.EgressGatewayRequest.RangesWithMetric)
+	if node.EgressDetails.IsEgressGateway {
 		// filter conflicting addrs
 		// filter conflicting addrs
 		nodeEgressMap := make(map[string]struct{})
 		nodeEgressMap := make(map[string]struct{})
-		for _, rangeI := range node.EgressGatewayRanges {
+		for _, rangeI := range node.EgressDetails.EgressGatewayRanges {
 			nodeEgressMap[rangeI] = struct{}{}
 			nodeEgressMap[rangeI] = struct{}{}
 		}
 		}
 		for i := len(egressIPs) - 1; i >= 0; i-- {
 		for i := len(egressIPs) - 1; i >= 0; i-- {
@@ -623,11 +649,11 @@ func GetEgressIPs(peer *models.Node) []net.IPNet {
 
 
 	// check for internet gateway
 	// check for internet gateway
 	internetGateway := false
 	internetGateway := false
-	if slices.Contains(peer.EgressGatewayRanges, "0.0.0.0/0") || slices.Contains(peer.EgressGatewayRanges, "::/0") {
+	if slices.Contains(peer.EgressDetails.EgressGatewayRanges, "0.0.0.0/0") || slices.Contains(peer.EgressDetails.EgressGatewayRanges, "::/0") {
 		internetGateway = true
 		internetGateway = true
 	}
 	}
 	allowedips := []net.IPNet{}
 	allowedips := []net.IPNet{}
-	for _, iprange := range peer.EgressGatewayRanges { // go through each cidr for egress gateway
+	for _, iprange := range peer.EgressDetails.EgressGatewayRanges { // go through each cidr for egress gateway
 		_, ipnet, err := net.ParseCIDR(iprange) // confirming it's valid cidr
 		_, ipnet, err := net.ParseCIDR(iprange) // confirming it's valid cidr
 		if err != nil {
 		if err != nil {
 			logger.Log(1, "could not parse gateway IP range. Not adding ", iprange)
 			logger.Log(1, "could not parse gateway IP range. Not adding ", iprange)
@@ -669,13 +695,13 @@ func getNodeAllowedIPs(peer, node *models.Node) []net.IPNet {
 		allowedips = append(allowedips, allowed)
 		allowedips = append(allowedips, allowed)
 	}
 	}
 	// handle egress gateway peers
 	// handle egress gateway peers
-	if peer.IsEgressGateway {
+	if peer.EgressDetails.IsEgressGateway {
 		// hasGateway = true
 		// hasGateway = true
 		egressIPs := GetEgressIPs(peer)
 		egressIPs := GetEgressIPs(peer)
-		if node.IsEgressGateway {
+		if node.EgressDetails.IsEgressGateway {
 			// filter conflicting addrs
 			// filter conflicting addrs
 			nodeEgressMap := make(map[string]struct{})
 			nodeEgressMap := make(map[string]struct{})
-			for _, rangeI := range node.EgressGatewayRanges {
+			for _, rangeI := range node.EgressDetails.EgressGatewayRanges {
 				nodeEgressMap[rangeI] = struct{}{}
 				nodeEgressMap[rangeI] = struct{}{}
 			}
 			}
 			for i := len(egressIPs) - 1; i >= 0; i-- {
 			for i := len(egressIPs) - 1; i >= 0; i-- {

+ 17 - 1
logic/relay.go

@@ -1,14 +1,17 @@
 package logic
 package logic
 
 
 import (
 import (
+	"context"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net"
 	"net"
 
 
 	"github.com/google/uuid"
 	"github.com/google/uuid"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
 )
 )
 
 
 // GetRelays - gets all the nodes that are relays
 // GetRelays - gets all the nodes that are relays
@@ -109,11 +112,14 @@ func ValidateRelay(relay models.RelayRequest, update bool) error {
 	if !update && node.IsRelay {
 	if !update && node.IsRelay {
 		return errors.New("node is already acting as a relay")
 		return errors.New("node is already acting as a relay")
 	}
 	}
+	eli, _ := (&schema.Egress{Network: node.Network}).ListByNetwork(db.WithContext(context.TODO()))
+	acls, _ := ListAclsByNetwork(models.NetworkID(node.Network))
 	for _, relayedNodeID := range relay.RelayedNodes {
 	for _, relayedNodeID := range relay.RelayedNodes {
 		relayedNode, err := GetNodeByID(relayedNodeID)
 		relayedNode, err := GetNodeByID(relayedNodeID)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
+		GetNodeEgressInfo(&relayedNode, eli, acls)
 		if relayedNode.IsIngressGateway {
 		if relayedNode.IsIngressGateway {
 			return errors.New("cannot relay an ingress gateway (" + relayedNodeID + ")")
 			return errors.New("cannot relay an ingress gateway (" + relayedNodeID + ")")
 		}
 		}
@@ -185,6 +191,8 @@ func DeleteRelay(network, nodeid string) ([]models.Node, models.Node, error) {
 
 
 func RelayedAllowedIPs(peer, node *models.Node) []net.IPNet {
 func RelayedAllowedIPs(peer, node *models.Node) []net.IPNet {
 	var allowedIPs = []net.IPNet{}
 	var allowedIPs = []net.IPNet{}
+	eli, _ := (&schema.Egress{Network: node.Network}).ListByNetwork(db.WithContext(context.TODO()))
+	acls, _ := ListAclsByNetwork(models.NetworkID(node.Network))
 	for _, relayedNodeID := range peer.RelayedNodes {
 	for _, relayedNodeID := range peer.RelayedNodes {
 		if node.ID.String() == relayedNodeID {
 		if node.ID.String() == relayedNodeID {
 			continue
 			continue
@@ -193,8 +201,9 @@ func RelayedAllowedIPs(peer, node *models.Node) []net.IPNet {
 		if err != nil {
 		if err != nil {
 			continue
 			continue
 		}
 		}
+		GetNodeEgressInfo(&relayedNode, eli, acls)
 		allowed := getRelayedAddresses(relayedNodeID)
 		allowed := getRelayedAddresses(relayedNodeID)
-		if relayedNode.IsEgressGateway {
+		if relayedNode.EgressDetails.IsEgressGateway {
 			allowed = append(allowed, GetEgressIPs(&relayedNode)...)
 			allowed = append(allowed, GetEgressIPs(&relayedNode)...)
 		}
 		}
 		allowedIPs = append(allowedIPs, allowed...)
 		allowedIPs = append(allowedIPs, allowed...)
@@ -216,10 +225,17 @@ func GetAllowedIpsForRelayed(relayed, relay *models.Node) (allowedIPs []net.IPNe
 		logger.Log(0, "error getting network clients", err.Error())
 		logger.Log(0, "error getting network clients", err.Error())
 		return
 		return
 	}
 	}
+	acls, _ := ListAclsByNetwork(models.NetworkID(relay.Network))
+	eli, _ := (&schema.Egress{Network: relay.Network}).ListByNetwork(db.WithContext(context.TODO()))
+	defaultPolicy, _ := GetDefaultPolicy(models.NetworkID(relay.Network), models.DevicePolicy)
 	for _, peer := range peers {
 	for _, peer := range peers {
 		if peer.ID == relayed.ID || peer.ID == relay.ID {
 		if peer.ID == relayed.ID || peer.ID == relay.ID {
 			continue
 			continue
 		}
 		}
+		if !IsPeerAllowed(*relayed, peer, true) {
+			continue
+		}
+		AddEgressInfoToPeerByAccess(relayed, &peer, eli, acls, defaultPolicy.Enabled)
 		if nodeacls.AreNodesAllowed(nodeacls.NetworkID(relayed.Network), nodeacls.NodeID(relayed.ID.String()), nodeacls.NodeID(peer.ID.String())) {
 		if nodeacls.AreNodesAllowed(nodeacls.NetworkID(relayed.Network), nodeacls.NodeID(relayed.ID.String()), nodeacls.NodeID(peer.ID.String())) {
 			allowedIPs = append(allowedIPs, GetAllowedIPs(relayed, &peer, nil)...)
 			allowedIPs = append(allowedIPs, GetAllowedIPs(relayed, &peer, nil)...)
 		}
 		}

+ 15 - 0
logic/security.go

@@ -1,6 +1,7 @@
 package logic
 package logic
 
 
 import (
 import (
+	"errors"
 	"net/http"
 	"net/http"
 	"strings"
 	"strings"
 
 
@@ -32,6 +33,20 @@ func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
 			ReturnErrorResponse(w, r, FormatError(err, "unauthorized"))
 			ReturnErrorResponse(w, r, FormatError(err, "unauthorized"))
 			return
 			return
 		}
 		}
+		if username != MasterUser {
+			user, err := GetUser(username)
+			if err != nil {
+				ReturnErrorResponse(w, r, FormatError(err, "unauthorized"))
+				return
+			}
+
+			if user.AccountDisabled {
+				err = errors.New("user account disabled")
+				ReturnErrorResponse(w, r, FormatError(err, "unauthorized"))
+				return
+			}
+		}
+
 		// detect masteradmin
 		// detect masteradmin
 		if username == MasterUser {
 		if username == MasterUser {
 			r.Header.Set("ismaster", "yes")
 			r.Header.Set("ismaster", "yes")

+ 355 - 0
logic/settings.go

@@ -0,0 +1,355 @@
+package logic
+
+import (
+	"encoding/json"
+	"os"
+	"regexp"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/gravitl/netmaker/config"
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/servercfg"
+)
+
+var serverSettingsDBKey = "server_cfg"
+var SettingsMutex = &sync.RWMutex{}
+
+func GetServerSettings() (s models.ServerSettings) {
+	data, err := database.FetchRecord(database.SERVER_SETTINGS, serverSettingsDBKey)
+	if err != nil {
+		return
+	}
+	json.Unmarshal([]byte(data), &s)
+	return
+}
+
+func UpsertServerSettings(s models.ServerSettings) error {
+	// get curr settings
+	currSettings := GetServerSettings()
+	if s.ClientSecret == Mask() {
+		s.ClientSecret = currSettings.ClientSecret
+	}
+	data, err := json.Marshal(s)
+	if err != nil {
+		return err
+	}
+	err = database.Insert(serverSettingsDBKey, string(data), database.SERVER_SETTINGS)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func ValidateNewSettings(req models.ServerSettings) bool {
+	// TODO: add checks for different fields
+	return true
+}
+
+func GetServerSettingsFromEnv() (s models.ServerSettings) {
+
+	s = models.ServerSettings{
+		NetclientAutoUpdate:        servercfg.AutoUpdateEnabled(),
+		Verbosity:                  servercfg.GetVerbosity(),
+		AuthProvider:               os.Getenv("AUTH_PROVIDER"),
+		OIDCIssuer:                 os.Getenv("OIDC_ISSUER"),
+		ClientID:                   os.Getenv("CLIENT_ID"),
+		ClientSecret:               os.Getenv("CLIENT_SECRET"),
+		AzureTenant:                servercfg.GetAzureTenant(),
+		Telemetry:                  servercfg.Telemetry(),
+		BasicAuth:                  servercfg.IsBasicAuthEnabled(),
+		JwtValidityDuration:        servercfg.GetJwtValidityDurationFromEnv() / 60,
+		RacAutoDisable:             servercfg.GetRacAutoDisable(),
+		RacRestrictToSingleNetwork: servercfg.GetRacRestrictToSingleNetwork(),
+		EndpointDetection:          servercfg.IsEndpointDetectionEnabled(),
+		AllowedEmailDomains:        servercfg.GetAllowedEmailDomains(),
+		EmailSenderAddr:            servercfg.GetSenderEmail(),
+		EmailSenderUser:            servercfg.GetSenderUser(),
+		EmailSenderPassword:        servercfg.GetEmaiSenderPassword(),
+		SmtpHost:                   servercfg.GetSmtpHost(),
+		SmtpPort:                   servercfg.GetSmtpPort(),
+		MetricInterval:             servercfg.GetMetricInterval(),
+		MetricsPort:                servercfg.GetMetricsPort(),
+		ManageDNS:                  servercfg.GetManageDNS(),
+		DefaultDomain:              servercfg.GetDefaultDomain(),
+		Stun:                       servercfg.IsStunEnabled(),
+		StunServers:                servercfg.GetStunServers(),
+		TextSize:                   "16",
+		Theme:                      models.Dark,
+		ReducedMotion:              false,
+	}
+
+	return
+}
+
+// GetServerConfig - gets the server config into memory from file or env
+func GetServerConfig() config.ServerConfig {
+	var cfg config.ServerConfig
+	settings := GetServerSettings()
+	cfg.APIConnString = servercfg.GetAPIConnString()
+	cfg.CoreDNSAddr = servercfg.GetCoreDNSAddr()
+	cfg.APIHost = servercfg.GetAPIHost()
+	cfg.APIPort = servercfg.GetAPIPort()
+	cfg.MasterKey = "(hidden)"
+	cfg.DNSKey = "(hidden)"
+	cfg.AllowedOrigin = servercfg.GetAllowedOrigin()
+	cfg.RestBackend = "off"
+	cfg.NodeID = servercfg.GetNodeID()
+	cfg.BrokerType = servercfg.GetBrokerType()
+	cfg.EmqxRestEndpoint = servercfg.GetEmqxRestEndpoint()
+	if settings.NetclientAutoUpdate {
+		cfg.NetclientAutoUpdate = "enabled"
+	} else {
+		cfg.NetclientAutoUpdate = "disabled"
+	}
+	if servercfg.IsRestBackend() {
+		cfg.RestBackend = "on"
+	}
+	cfg.DNSMode = "off"
+	if servercfg.IsDNSMode() {
+		cfg.DNSMode = "on"
+	}
+	cfg.DisplayKeys = "off"
+	if servercfg.IsDisplayKeys() {
+		cfg.DisplayKeys = "on"
+	}
+	cfg.DisableRemoteIPCheck = "off"
+	if servercfg.DisableRemoteIPCheck() {
+		cfg.DisableRemoteIPCheck = "on"
+	}
+	cfg.Database = servercfg.GetDB()
+	cfg.Platform = servercfg.GetPlatform()
+	cfg.Version = servercfg.GetVersion()
+	cfg.PublicIp = servercfg.GetServerHostIP()
+
+	// == auth config ==
+	var authInfo = GetAuthProviderInfo(settings)
+	cfg.AuthProvider = authInfo[0]
+	cfg.ClientID = authInfo[1]
+	cfg.ClientSecret = authInfo[2]
+	cfg.FrontendURL = servercfg.GetFrontendURL()
+	cfg.AzureTenant = settings.AzureTenant
+	cfg.Telemetry = settings.Telemetry
+	cfg.Server = servercfg.GetServer()
+	cfg.Verbosity = settings.Verbosity
+	cfg.IsPro = "no"
+	if servercfg.IsPro {
+		cfg.IsPro = "yes"
+	}
+	cfg.JwtValidityDuration = time.Duration(settings.JwtValidityDuration) * time.Minute
+	cfg.RacAutoDisable = settings.RacAutoDisable
+	cfg.RacRestrictToSingleNetwork = settings.RacRestrictToSingleNetwork
+	cfg.MetricInterval = settings.MetricInterval
+	cfg.ManageDNS = settings.ManageDNS
+	cfg.Stun = settings.Stun
+	cfg.StunServers = settings.StunServers
+	cfg.DefaultDomain = settings.DefaultDomain
+	return cfg
+}
+
+// GetServerInfo - gets the server config into memory from file or env
+func GetServerInfo() models.ServerConfig {
+	var cfg models.ServerConfig
+	serverSettings := GetServerSettings()
+	cfg.Server = servercfg.GetServer()
+	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
+		cfg.MQUserName = "HOST_ID"
+		cfg.MQPassword = "HOST_PASS"
+	} else {
+		cfg.MQUserName = servercfg.GetMqUserName()
+		cfg.MQPassword = servercfg.GetMqPassword()
+	}
+	cfg.API = servercfg.GetAPIConnString()
+	cfg.CoreDNSAddr = servercfg.GetCoreDNSAddr()
+	cfg.APIPort = servercfg.GetAPIPort()
+	cfg.DNSMode = "off"
+	cfg.Broker = servercfg.GetPublicBrokerEndpoint()
+	cfg.BrokerType = servercfg.GetBrokerType()
+	if servercfg.IsDNSMode() {
+		cfg.DNSMode = "on"
+	}
+	cfg.Version = servercfg.GetVersion()
+	cfg.IsPro = servercfg.IsPro
+	cfg.MetricInterval = serverSettings.MetricInterval
+	cfg.MetricsPort = serverSettings.MetricsPort
+	cfg.ManageDNS = serverSettings.ManageDNS
+	cfg.Stun = serverSettings.Stun
+	cfg.StunServers = serverSettings.StunServers
+	cfg.DefaultDomain = serverSettings.DefaultDomain
+	cfg.EndpointDetection = serverSettings.EndpointDetection
+	return cfg
+}
+
+// GetDefaultDomain - get the default domain
+func GetDefaultDomain() string {
+	return GetServerSettings().DefaultDomain
+}
+
+func ValidateDomain(domain string) bool {
+	domainPattern := `[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}(\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,62})*(\.[a-zA-Z][a-zA-Z0-9]{0,10}){1}`
+
+	exp := regexp.MustCompile("^" + domainPattern + "$")
+
+	return exp.MatchString(domain)
+}
+
+// Telemetry - checks if telemetry data should be sent
+func Telemetry() string {
+	return GetServerSettings().Telemetry
+}
+
+// GetJwtValidityDuration - returns the JWT validity duration in minutes
+func GetJwtValidityDuration() time.Duration {
+	return GetServerConfig().JwtValidityDuration
+}
+
+// GetRacAutoDisable - returns whether the feature to autodisable RAC is enabled
+func GetRacAutoDisable() bool {
+	return GetServerSettings().RacAutoDisable
+}
+
+// GetRacRestrictToSingleNetwork - returns whether the feature to allow simultaneous network connections via RAC is enabled
+func GetRacRestrictToSingleNetwork() bool {
+	return GetServerSettings().RacRestrictToSingleNetwork
+}
+
+func GetSmtpHost() string {
+	return GetServerSettings().SmtpHost
+}
+
+func GetSmtpPort() int {
+	return GetServerSettings().SmtpPort
+}
+
+func GetSenderEmail() string {
+	return GetServerSettings().EmailSenderAddr
+}
+
+func GetSenderUser() string {
+	return GetServerSettings().EmailSenderUser
+}
+
+func GetEmaiSenderPassword() string {
+	return GetServerSettings().EmailSenderPassword
+}
+
+// AutoUpdateEnabled returns a boolean indicating whether netclient auto update is enabled or disabled
+// default is enabled
+func AutoUpdateEnabled() bool {
+	return GetServerSettings().NetclientAutoUpdate
+}
+
+// GetAuthProviderInfo = gets the oauth provider info
+func GetAuthProviderInfo(settings models.ServerSettings) (pi []string) {
+	var authProvider = ""
+
+	defer func() {
+		if authProvider == "oidc" {
+			if settings.OIDCIssuer != "" {
+				pi = append(pi, settings.OIDCIssuer)
+			} else {
+				pi = []string{"", "", ""}
+			}
+		}
+	}()
+
+	if settings.AuthProvider != "" && settings.ClientID != "" && settings.ClientSecret != "" {
+		authProvider = strings.ToLower(settings.AuthProvider)
+		if authProvider == "google" || authProvider == "azure-ad" || authProvider == "github" || authProvider == "oidc" {
+			return []string{authProvider, settings.ClientID, settings.ClientSecret}
+		} else {
+			authProvider = ""
+		}
+	}
+	return []string{"", "", ""}
+}
+
+// GetAzureTenant - retrieve the azure tenant ID from env variable or config file
+func GetAzureTenant() string {
+	return GetServerSettings().AzureTenant
+}
+
+// IsSyncEnabled returns whether auth provider sync is enabled.
+func IsSyncEnabled() bool {
+	return GetServerSettings().SyncEnabled
+}
+
+// GetIDPSyncInterval returns the interval at which the netmaker should sync
+// data from IDP.
+func GetIDPSyncInterval() time.Duration {
+	syncInterval, err := time.ParseDuration(GetServerSettings().IDPSyncInterval)
+	if err != nil {
+		return 24 * time.Hour
+	}
+
+	if syncInterval == 0 {
+		return 24 * time.Hour
+	}
+
+	return syncInterval
+}
+
+// GetMetricsPort - get metrics port
+func GetMetricsPort() int {
+	return GetServerSettings().MetricsPort
+}
+
+// GetMetricInterval - get the publish metric interval
+func GetMetricIntervalInMinutes() time.Duration {
+	//default 15 minutes
+	mi := "15"
+	if os.Getenv("PUBLISH_METRIC_INTERVAL") != "" {
+		mi = os.Getenv("PUBLISH_METRIC_INTERVAL")
+	}
+	interval, err := strconv.Atoi(mi)
+	if err != nil {
+		interval = 15
+	}
+
+	return time.Duration(interval) * time.Minute
+}
+
+// GetMetricInterval - get the publish metric interval
+func GetMetricInterval() string {
+	return GetServerSettings().MetricInterval
+}
+
+// GetManageDNS - if manage DNS enabled or not
+func GetManageDNS() bool {
+	return GetServerSettings().ManageDNS
+}
+
+// IsBasicAuthEnabled - checks if basic auth has been configured to be turned off
+func IsBasicAuthEnabled() bool {
+	return GetServerSettings().BasicAuth
+}
+
+// IsEndpointDetectionEnabled - returns true if endpoint detection enabled
+func IsEndpointDetectionEnabled() bool {
+	return GetServerSettings().EndpointDetection
+}
+
+// IsStunEnabled - returns true if STUN set to on
+func IsStunEnabled() bool {
+	return GetServerSettings().Stun
+}
+
+func GetStunServers() string {
+	return GetServerSettings().StunServers
+}
+
+// GetAllowedEmailDomains - gets the allowed email domains for oauth signup
+func GetAllowedEmailDomains() string {
+	return GetServerSettings().AllowedEmailDomains
+}
+
+func GetVerbosity() int32 {
+	return GetServerSettings().Verbosity
+}
+
+func Mask() string {
+	return ("..................")
+}

+ 8 - 2
logic/telemetry.go

@@ -7,6 +7,7 @@ import (
 
 
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
+
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/posthog/posthog-go"
 	"github.com/posthog/posthog-go"
@@ -19,6 +20,8 @@ var (
 	telServerRecord = models.Telemetry{}
 	telServerRecord = models.Telemetry{}
 )
 )
 
 
+var LogEvent = func(a *models.Event) {}
+
 // posthog_pub_key - Key for sending data to PostHog
 // posthog_pub_key - Key for sending data to PostHog
 const posthog_pub_key = "phc_1vEXhPOA1P7HP5jP2dVU9xDTUqXHAelmtravyZ1vvES"
 const posthog_pub_key = "phc_1vEXhPOA1P7HP5jP2dVU9xDTUqXHAelmtravyZ1vvES"
 
 
@@ -33,7 +36,7 @@ func SetFreeTierForTelemetry(freeTierFlag bool) {
 
 
 // sendTelemetry - gathers telemetry data and sends to posthog
 // sendTelemetry - gathers telemetry data and sends to posthog
 func sendTelemetry() error {
 func sendTelemetry() error {
-	if servercfg.Telemetry() == "off" {
+	if Telemetry() == "off" {
 		return nil
 		return nil
 	}
 	}
 
 
@@ -78,7 +81,8 @@ func sendTelemetry() error {
 			Set("pro_trial_end_date", d.ProTrialEndDate.In(time.UTC).Format("2006-01-02")).
 			Set("pro_trial_end_date", d.ProTrialEndDate.In(time.UTC).Format("2006-01-02")).
 			Set("admin_email", adminEmail).
 			Set("admin_email", adminEmail).
 			Set("email", adminEmail). // needed for posthog intgration with hubspot. "admin_email" can only be removed if not used in posthog
 			Set("email", adminEmail). // needed for posthog intgration with hubspot. "admin_email" can only be removed if not used in posthog
-			Set("is_saas_tenant", d.IsSaasTenant),
+			Set("is_saas_tenant", d.IsSaasTenant).
+			Set("domain", d.Domain),
 	})
 	})
 }
 }
 
 
@@ -102,6 +106,7 @@ func FetchTelemetryData() telemetryData {
 		data.IsProTrial = true
 		data.IsProTrial = true
 	}
 	}
 	data.IsSaasTenant = servercfg.DeployedByOperator()
 	data.IsSaasTenant = servercfg.DeployedByOperator()
+	data.Domain = servercfg.GetNmBaseDomain()
 	return data
 	return data
 }
 }
 
 
@@ -199,6 +204,7 @@ type telemetryData struct {
 	IsProTrial      bool
 	IsProTrial      bool
 	ProTrialEndDate time.Time
 	ProTrialEndDate time.Time
 	IsSaasTenant    bool
 	IsSaasTenant    bool
+	Domain          string
 }
 }
 
 
 // clientCount - What types of netclients we're tallying
 // clientCount - What types of netclients we're tallying

+ 4 - 20
logic/user_mgmt.go

@@ -50,6 +50,8 @@ var MigrateUserRoleAndGroups = func(u models.User) {
 
 
 }
 }
 
 
+var MigrateToUUIDs = func() {}
+
 var UpdateUserGwAccess = func(currentUser, changeUser models.User) {}
 var UpdateUserGwAccess = func(currentUser, changeUser models.User) {}
 
 
 var UpdateRole = func(r models.UserRolePermissionTemplate) error { return nil }
 var UpdateRole = func(r models.UserRolePermissionTemplate) error { return nil }
@@ -61,7 +63,8 @@ var CreateDefaultNetworkRolesAndGroups = func(netID models.NetworkID) {}
 var CreateDefaultUserPolicies = func(netID models.NetworkID) {}
 var CreateDefaultUserPolicies = func(netID models.NetworkID) {}
 var GetUserGroupsInNetwork = func(netID models.NetworkID) (networkGrps map[models.UserGroupID]models.UserGroup) { return }
 var GetUserGroupsInNetwork = func(netID models.NetworkID) (networkGrps map[models.UserGroupID]models.UserGroup) { return }
 var GetUserGroup = func(groupId models.UserGroupID) (userGrps models.UserGroup, err error) { return }
 var GetUserGroup = func(groupId models.UserGroupID) (userGrps models.UserGroup, err error) { return }
-var AddGlobalNetRolesToAdmins = func(u models.User) {}
+var AddGlobalNetRolesToAdmins = func(u *models.User) {}
+var EmailInit = func() {}
 
 
 // GetRole - fetches role template by id
 // GetRole - fetches role template by id
 func GetRole(roleID models.UserRoleID) (models.UserRolePermissionTemplate, error) {
 func GetRole(roleID models.UserRoleID) (models.UserRolePermissionTemplate, error) {
@@ -99,25 +102,6 @@ func ListPlatformRoles() ([]models.UserRolePermissionTemplate, error) {
 	return userRoles, nil
 	return userRoles, nil
 }
 }
 
 
-func GetUserGrpMap() map[models.UserGroupID]map[string]struct{} {
-	grpUsersMap := make(map[models.UserGroupID]map[string]struct{})
-	users, _ := GetUsersDB()
-	for _, user := range users {
-		for gID := range user.UserGroups {
-			if grpUsers, ok := grpUsersMap[gID]; ok {
-				grpUsers[user.UserName] = struct{}{}
-				grpUsersMap[gID] = grpUsers
-			} else {
-				grpUsersMap[gID] = make(map[string]struct{})
-				grpUsersMap[gID][user.UserName] = struct{}{}
-			}
-		}
-
-	}
-
-	return grpUsersMap
-}
-
 func userRolesInit() {
 func userRolesInit() {
 	d, _ := json.Marshal(SuperAdminPermissionTemplate)
 	d, _ := json.Marshal(SuperAdminPermissionTemplate)
 	database.Insert(SuperAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
 	database.Insert(SuperAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)

+ 28 - 9
logic/users.go

@@ -41,13 +41,15 @@ func GetReturnUser(username string) (models.ReturnUser, error) {
 // ToReturnUser - gets a user as a return user
 // ToReturnUser - gets a user as a return user
 func ToReturnUser(user models.User) models.ReturnUser {
 func ToReturnUser(user models.User) models.ReturnUser {
 	return models.ReturnUser{
 	return models.ReturnUser{
-		UserName:       user.UserName,
-		PlatformRoleID: user.PlatformRoleID,
-		AuthType:       user.AuthType,
-		UserGroups:     user.UserGroups,
-		NetworkRoles:   user.NetworkRoles,
-		RemoteGwIDs:    user.RemoteGwIDs,
-		LastLoginTime:  user.LastLoginTime,
+		UserName:        user.UserName,
+		DisplayName:     user.DisplayName,
+		AccountDisabled: user.AccountDisabled,
+		AuthType:        user.AuthType,
+		RemoteGwIDs:     user.RemoteGwIDs,
+		UserGroups:      user.UserGroups,
+		PlatformRoleID:  user.PlatformRoleID,
+		NetworkRoles:    user.NetworkRoles,
+		LastLoginTime:   user.LastLoginTime,
 	}
 	}
 }
 }
 
 
@@ -78,7 +80,7 @@ func GetSuperAdmin() (models.ReturnUser, error) {
 		return models.ReturnUser{}, err
 		return models.ReturnUser{}, err
 	}
 	}
 	for _, user := range users {
 	for _, user := range users {
-		if user.IsSuperAdmin {
+		if user.IsSuperAdmin || user.PlatformRoleID == models.SuperAdminRole {
 			return user, nil
 			return user, nil
 		}
 		}
 	}
 	}
@@ -113,7 +115,7 @@ func IsPendingUser(username string) bool {
 	return false
 	return false
 }
 }
 
 
-func ListPendingUsers() ([]models.ReturnUser, error) {
+func ListPendingReturnUsers() ([]models.ReturnUser, error) {
 	pendingUsers := []models.ReturnUser{}
 	pendingUsers := []models.ReturnUser{}
 	records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
 	records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
 	if err != nil && !database.IsEmptyRecord(err) {
 	if err != nil && !database.IsEmptyRecord(err) {
@@ -129,6 +131,22 @@ func ListPendingUsers() ([]models.ReturnUser, error) {
 	return pendingUsers, nil
 	return pendingUsers, nil
 }
 }
 
 
+func ListPendingUsers() ([]models.User, error) {
+	var pendingUsers []models.User
+	records, err := database.FetchRecords(database.PENDING_USERS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return pendingUsers, err
+	}
+	for _, record := range records {
+		var u models.User
+		err = json.Unmarshal([]byte(record), &u)
+		if err == nil {
+			pendingUsers = append(pendingUsers, u)
+		}
+	}
+	return pendingUsers, nil
+}
+
 func GetUserMap() (map[string]models.User, error) {
 func GetUserMap() (map[string]models.User, error) {
 	userMap := make(map[string]models.User)
 	userMap := make(map[string]models.User)
 	records, err := database.FetchRecords(database.USERS_TABLE_NAME)
 	records, err := database.FetchRecords(database.USERS_TABLE_NAME)
@@ -181,6 +199,7 @@ func ListUserInvites() ([]models.UserInvite, error) {
 func DeleteUserInvite(email string) error {
 func DeleteUserInvite(email string) error {
 	return database.DeleteRecord(database.USER_INVITES_TABLE_NAME, email)
 	return database.DeleteRecord(database.USER_INVITES_TABLE_NAME, email)
 }
 }
+
 func ValidateAndApproveUserInvite(email, code string) error {
 func ValidateAndApproveUserInvite(email, code string) error {
 	in, err := GetUserInvite(email)
 	in, err := GetUserInvite(email)
 	if err != nil {
 	if err != nil {

+ 21 - 0
logic/util.go

@@ -10,6 +10,7 @@ import (
 	"log/slog"
 	"log/slog"
 	"net"
 	"net"
 	"os"
 	"os"
+	"reflect"
 	"strings"
 	"strings"
 	"time"
 	"time"
 	"unicode"
 	"unicode"
@@ -201,3 +202,23 @@ func VersionLessThan(v1, v2 string) (bool, error) {
 	}
 	}
 	return sv1.LT(sv2), nil
 	return sv1.LT(sv2), nil
 }
 }
+
+// Compare any two maps with any key and value types
+func CompareMaps[K comparable, V any](a, b map[K]V) bool {
+	if len(a) != len(b) {
+		return false
+	}
+
+	for key, valA := range a {
+		valB, ok := b[key]
+		if !ok {
+			return false
+		}
+
+		if !reflect.DeepEqual(valA, valB) {
+			return false
+		}
+	}
+
+	return true
+}

+ 0 - 14
logic/wireguard.go

@@ -9,24 +9,10 @@ func IfaceDelta(currentNode *models.Node, newNode *models.Node) bool {
 	// single comparison statements
 	// single comparison statements
 	if newNode.Address.String() != currentNode.Address.String() ||
 	if newNode.Address.String() != currentNode.Address.String() ||
 		newNode.Address6.String() != currentNode.Address6.String() ||
 		newNode.Address6.String() != currentNode.Address6.String() ||
-		newNode.IsEgressGateway != currentNode.IsEgressGateway ||
-		newNode.IsIngressGateway != currentNode.IsIngressGateway ||
 		newNode.IsRelay != currentNode.IsRelay ||
 		newNode.IsRelay != currentNode.IsRelay ||
-		newNode.DNSOn != currentNode.DNSOn ||
 		newNode.Connected != currentNode.Connected {
 		newNode.Connected != currentNode.Connected {
 		return true
 		return true
 	}
 	}
-	// multi-comparison statements
-	if newNode.IsEgressGateway {
-		if len(currentNode.EgressGatewayRanges) != len(newNode.EgressGatewayRanges) {
-			return true
-		}
-		for _, address := range newNode.EgressGatewayRanges {
-			if !StringSliceContains(currentNode.EgressGatewayRanges, address) {
-				return true
-			}
-		}
-	}
 	if newNode.IsRelay {
 	if newNode.IsRelay {
 		if len(currentNode.RelayedNodes) != len(newNode.RelayedNodes) {
 		if len(currentNode.RelayedNodes) != len(newNode.RelayedNodes) {
 			return true
 			return true

+ 41 - 2
logic/zombie.go

@@ -7,6 +7,7 @@ import (
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
 const (
 const (
@@ -77,7 +78,7 @@ func checkForZombieHosts(h *models.Host) {
 func ManageZombies(ctx context.Context, peerUpdate chan *models.Node) {
 func ManageZombies(ctx context.Context, peerUpdate chan *models.Node) {
 	logger.Log(2, "Zombie management started")
 	logger.Log(2, "Zombie management started")
 	go InitializeZombies()
 	go InitializeZombies()
-	go checkPendingRemovalNodes()
+	go checkPendingRemovalNodes(peerUpdate)
 	// Zombie Nodes Cleanup Four Times a Day
 	// Zombie Nodes Cleanup Four Times a Day
 	ticker := time.NewTicker(time.Hour * ZOMBIE_TIMEOUT)
 	ticker := time.NewTicker(time.Hour * ZOMBIE_TIMEOUT)
 
 
@@ -135,15 +136,53 @@ func ManageZombies(ctx context.Context, peerUpdate chan *models.Node) {
 					}
 					}
 				}
 				}
 			}
 			}
+			if servercfg.IsAutoCleanUpEnabled() {
+				nodes, _ := GetAllNodes()
+				for _, node := range nodes {
+					if time.Since(node.LastCheckIn) > time.Minute*ZOMBIE_DELETE_TIME {
+						if err := DeleteNode(&node, true); err != nil {
+							continue
+						}
+						node.PendingDelete = true
+						node.Action = models.NODE_DELETE
+						peerUpdate <- &node
+						host, err := GetHost(node.HostID.String())
+						if err == nil && len(host.Nodes) == 0 {
+							RemoveHostByID(host.ID.String())
+						}
+
+					}
+				}
+			}
+
 		}
 		}
 	}
 	}
 }
 }
-func checkPendingRemovalNodes() {
+func checkPendingRemovalNodes(peerUpdate chan *models.Node) {
 	nodes, _ := GetAllNodes()
 	nodes, _ := GetAllNodes()
 	for _, node := range nodes {
 	for _, node := range nodes {
+		node := node
 		pendingDelete := node.PendingDelete || node.Action == models.NODE_DELETE
 		pendingDelete := node.PendingDelete || node.Action == models.NODE_DELETE
 		if pendingDelete {
 		if pendingDelete {
 			DeleteNode(&node, true)
 			DeleteNode(&node, true)
+			peerUpdate <- &node
+			continue
+		}
+		if servercfg.IsAutoCleanUpEnabled() {
+			if time.Since(node.LastCheckIn) > time.Minute*ZOMBIE_DELETE_TIME {
+				if err := DeleteNode(&node, true); err != nil {
+					continue
+				}
+				node.PendingDelete = true
+				node.Action = models.NODE_DELETE
+				peerUpdate <- &node
+				host, err := GetHost(node.HostID.String())
+				if err == nil && len(host.Nodes) == 0 {
+					RemoveHostByID(host.ID.String())
+				}
+
+			}
+
 		}
 		}
 	}
 	}
 }
 }

+ 53 - 4
main.go

@@ -3,6 +3,8 @@ package main
 
 
 import (
 import (
 	"context"
 	"context"
+	"crypto/rand"
+	"encoding/json"
 	"flag"
 	"flag"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
@@ -12,9 +14,11 @@ import (
 	"sync"
 	"sync"
 	"syscall"
 	"syscall"
 
 
+	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/config"
 	"github.com/gravitl/netmaker/config"
 	controller "github.com/gravitl/netmaker/controllers"
 	controller "github.com/gravitl/netmaker/controllers"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/functions"
 	"github.com/gravitl/netmaker/functions"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
@@ -22,16 +26,18 @@ import (
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/netclient/ncutils"
 	"github.com/gravitl/netmaker/netclient/ncutils"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/serverctl"
 	"github.com/gravitl/netmaker/serverctl"
 	_ "go.uber.org/automaxprocs"
 	_ "go.uber.org/automaxprocs"
+	"golang.org/x/crypto/nacl/box"
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
 )
 )
 
 
-var version = "v0.90.0"
+var version = "v0.99.0"
 
 
 //	@title			NetMaker
 //	@title			NetMaker
-//	@version		0.90.0
+//	@version		0.99.0
 //	@description	NetMaker API Docs
 //	@description	NetMaker API Docs
 //	@tag.name	    APIUsage
 //	@tag.name	    APIUsage
 //	@tag.description.markdown
 //	@tag.description.markdown
@@ -99,8 +105,13 @@ func initialize() { // Client Mode Prereq Check
 	if err = database.InitializeDatabase(); err != nil {
 	if err = database.InitializeDatabase(); err != nil {
 		logger.FatalLog("Error connecting to database: ", err.Error())
 		logger.FatalLog("Error connecting to database: ", err.Error())
 	}
 	}
+	// initialize sql schema db.
+	err = db.InitializeDB(schema.ListModels()...)
+	if err != nil {
+		logger.FatalLog("Error connecting to v1 database: ", err.Error())
+	}
 	logger.Log(0, "database successfully connected")
 	logger.Log(0, "database successfully connected")
-
+	initializeUUID()
 	//initialize cache
 	//initialize cache
 	_, _ = logic.GetNetworks()
 	_, _ = logic.GetNetworks()
 	_, _ = logic.GetAllNodes()
 	_, _ = logic.GetAllNodes()
@@ -178,7 +189,7 @@ func runMessageQueue(wg *sync.WaitGroup, ctx context.Context) {
 	defer mq.CloseClient()
 	defer mq.CloseClient()
 	go mq.Keepalive(ctx)
 	go mq.Keepalive(ctx)
 	go func() {
 	go func() {
-		peerUpdate := make(chan *models.Node)
+		peerUpdate := make(chan *models.Node, 100)
 		go logic.ManageZombies(ctx, peerUpdate)
 		go logic.ManageZombies(ctx, peerUpdate)
 		go logic.DeleteExpiredNodes(ctx, peerUpdate)
 		go logic.DeleteExpiredNodes(ctx, peerUpdate)
 		for nodeUpdate := range peerUpdate {
 		for nodeUpdate := range peerUpdate {
@@ -247,3 +258,41 @@ func setGarbageCollection() {
 		debug.SetGCPercent(ncutils.DEFAULT_GC_PERCENT)
 		debug.SetGCPercent(ncutils.DEFAULT_GC_PERCENT)
 	}
 	}
 }
 }
+
+// initializeUUID - create a UUID record for server if none exists
+func initializeUUID() error {
+	records, err := database.FetchRecords(database.SERVER_UUID_TABLE_NAME)
+	if err != nil {
+		if !database.IsEmptyRecord(err) {
+			return err
+		}
+	} else if len(records) > 0 {
+		return nil
+	}
+	// setup encryption keys
+	var trafficPubKey, trafficPrivKey, errT = box.GenerateKey(rand.Reader) // generate traffic keys
+	if errT != nil {
+		return errT
+	}
+	tPriv, err := ncutils.ConvertKeyToBytes(trafficPrivKey)
+	if err != nil {
+		return err
+	}
+
+	tPub, err := ncutils.ConvertKeyToBytes(trafficPubKey)
+	if err != nil {
+		return err
+	}
+
+	telemetry := models.Telemetry{
+		UUID:           uuid.NewString(),
+		TrafficKeyPriv: tPriv,
+		TrafficKeyPub:  tPub,
+	}
+	telJSON, err := json.Marshal(&telemetry)
+	if err != nil {
+		return err
+	}
+
+	return database.Insert(database.SERVER_UUID_RECORD_KEY, string(telJSON), database.SERVER_UUID_TABLE_NAME)
+}

+ 130 - 38
migrate/migrate.go

@@ -1,34 +1,41 @@
 package migrate
 package migrate
 
 
 import (
 import (
+	"context"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"log"
 	"log"
 	"time"
 	"time"
 
 
 	"golang.org/x/exp/slog"
 	"golang.org/x/exp/slog"
+	"gorm.io/datatypes"
 
 
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/logic/acls"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
 	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/servercfg"
 )
 )
 
 
 // Run - runs all migrations
 // Run - runs all migrations
 func Run() {
 func Run() {
+	settings()
 	updateEnrollmentKeys()
 	updateEnrollmentKeys()
 	assignSuperAdmin()
 	assignSuperAdmin()
 	createDefaultTagsAndPolicies()
 	createDefaultTagsAndPolicies()
 	removeOldUserGrps()
 	removeOldUserGrps()
+	migrateToUUIDs()
 	syncUsers()
 	syncUsers()
 	updateHosts()
 	updateHosts()
 	updateNodes()
 	updateNodes()
 	updateAcls()
 	updateAcls()
-	migrateToGws()
+	logic.MigrateToGws()
+	migrateToEgressV1()
 }
 }
 
 
 func assignSuperAdmin() {
 func assignSuperAdmin() {
@@ -151,6 +158,7 @@ func updateEnrollmentKeys() {
 			true,
 			true,
 			uuid.Nil,
 			uuid.Nil,
 			true,
 			true,
+			false,
 		)
 		)
 
 
 	}
 	}
@@ -188,6 +196,14 @@ func updateHosts() {
 				continue
 				continue
 			}
 			}
 		}
 		}
+		if host.DNS == "" || (host.DNS != "yes" && host.DNS != "no") {
+			if logic.GetServerSettings().ManageDNS {
+				host.DNS = "yes"
+			} else {
+				host.DNS = "no"
+			}
+			logic.UpsertHost(&host)
+		}
 	}
 	}
 }
 }
 
 
@@ -386,6 +402,10 @@ func MigrateEmqx() {
 
 
 }
 }
 
 
+func migrateToUUIDs() {
+	logic.MigrateToUUIDs()
+}
+
 func syncUsers() {
 func syncUsers() {
 	// create default network user roles for existing networks
 	// create default network user roles for existing networks
 	if servercfg.IsPro {
 	if servercfg.IsPro {
@@ -405,11 +425,12 @@ func syncUsers() {
 			}
 			}
 			if user.PlatformRoleID == models.SuperAdminRole && !user.IsSuperAdmin {
 			if user.PlatformRoleID == models.SuperAdminRole && !user.IsSuperAdmin {
 				user.IsSuperAdmin = true
 				user.IsSuperAdmin = true
-				logic.UpsertUser(user)
+
 			}
 			}
 			if user.PlatformRoleID.String() != "" {
 			if user.PlatformRoleID.String() != "" {
 				logic.MigrateUserRoleAndGroups(user)
 				logic.MigrateUserRoleAndGroups(user)
-				logic.AddGlobalNetRolesToAdmins(user)
+				logic.AddGlobalNetRolesToAdmins(&user)
+				logic.UpsertUser(user)
 				continue
 				continue
 			}
 			}
 			user.AuthType = models.BasicAuth
 			user.AuthType = models.BasicAuth
@@ -430,9 +451,9 @@ func syncUsers() {
 			} else {
 			} else {
 				user.PlatformRoleID = models.ServiceUser
 				user.PlatformRoleID = models.ServiceUser
 			}
 			}
-			logic.UpsertUser(user)
-			logic.AddGlobalNetRolesToAdmins(user)
+			logic.AddGlobalNetRolesToAdmins(&user)
 			logic.MigrateUserRoleAndGroups(user)
 			logic.MigrateUserRoleAndGroups(user)
+			logic.UpsertUser(user)
 		}
 		}
 	}
 	}
 
 
@@ -452,47 +473,118 @@ func createDefaultTagsAndPolicies() {
 	logic.MigrateAclPolicies()
 	logic.MigrateAclPolicies()
 }
 }
 
 
-func migrateToGws() {
-	nodes, err := logic.GetAllNodes()
+func migrateToEgressV1() {
+	nodes, _ := logic.GetAllNodes()
+	user, err := logic.GetSuperAdmin()
 	if err != nil {
 	if err != nil {
 		return
 		return
 	}
 	}
 	for _, node := range nodes {
 	for _, node := range nodes {
-		if node.IsIngressGateway || node.IsRelay {
-			node.IsGw = true
-			node.IsIngressGateway = true
-			node.IsRelay = true
-			if node.Tags == nil {
-				node.Tags = make(map[models.TagID]struct{})
+		if node.IsEgressGateway {
+			egressHost, err := logic.GetHost(node.HostID.String())
+			if err != nil {
+				continue
 			}
 			}
-			node.Tags[models.TagID(fmt.Sprintf("%s.%s", node.Network, models.GwTagName))] = struct{}{}
-			delete(node.Tags, models.TagID(fmt.Sprintf("%s.%s", node.Network, models.OldRemoteAccessTagName)))
+			for _, rangeI := range node.EgressGatewayRequest.Ranges {
+				e := schema.Egress{
+					ID:          uuid.New().String(),
+					Name:        fmt.Sprintf("%s egress", egressHost.Name),
+					Description: "",
+					Network:     node.Network,
+					Nodes: datatypes.JSONMap{
+						node.ID.String(): 256,
+					},
+					Tags:      make(datatypes.JSONMap),
+					Range:     rangeI,
+					Nat:       node.EgressGatewayRequest.NatEnabled == "yes",
+					Status:    true,
+					CreatedBy: user.UserName,
+					CreatedAt: time.Now().UTC(),
+				}
+				err = e.Create(db.WithContext(context.TODO()))
+				if err == nil {
+					acl := models.Acl{
+						ID:          uuid.New().String(),
+						Name:        "egress node policy",
+						MetaData:    "",
+						Default:     false,
+						ServiceType: models.Any,
+						NetworkID:   models.NetworkID(node.Network),
+						Proto:       models.ALL,
+						RuleType:    models.DevicePolicy,
+						Src: []models.AclPolicyTag{
+
+							{
+								ID:    models.NodeTagID,
+								Value: "*",
+							},
+						},
+						Dst: []models.AclPolicyTag{
+							{
+								ID:    models.EgressID,
+								Value: e.ID,
+							},
+						},
+
+						AllowedDirection: models.TrafficDirectionBi,
+						Enabled:          true,
+						CreatedBy:        "auto",
+						CreatedAt:        time.Now().UTC(),
+					}
+					logic.InsertAcl(acl)
+					acl = models.Acl{
+						ID:          uuid.New().String(),
+						Name:        "egress node policy",
+						MetaData:    "",
+						Default:     false,
+						ServiceType: models.Any,
+						NetworkID:   models.NetworkID(node.Network),
+						Proto:       models.ALL,
+						RuleType:    models.UserPolicy,
+						Src: []models.AclPolicyTag{
+
+							{
+								ID:    models.UserAclID,
+								Value: "*",
+							},
+						},
+						Dst: []models.AclPolicyTag{
+							{
+								ID:    models.EgressID,
+								Value: e.ID,
+							},
+						},
+
+						AllowedDirection: models.TrafficDirectionBi,
+						Enabled:          true,
+						CreatedBy:        "auto",
+						CreatedAt:        time.Now().UTC(),
+					}
+					logic.InsertAcl(acl)
+				}
+
+			}
+			node.IsEgressGateway = false
+			node.EgressGatewayRequest = models.EgressGatewayRequest{}
+			node.EgressGatewayNatEnabled = false
+			node.EgressGatewayRanges = []string{}
 			logic.UpsertNode(&node)
 			logic.UpsertNode(&node)
+
 		}
 		}
 	}
 	}
-	acls := logic.ListAcls()
-	for _, acl := range acls {
-		upsert := false
-		for i, srcI := range acl.Src {
-			if srcI.ID == models.NodeTagID && srcI.Value == fmt.Sprintf("%s.%s", acl.NetworkID.String(), models.OldRemoteAccessTagName) {
-				srcI.Value = fmt.Sprintf("%s.%s", acl.NetworkID.String(), models.GwTagName)
-				acl.Src[i] = srcI
-				upsert = true
-			}
-		}
-		for i, dstI := range acl.Dst {
-			if dstI.ID == models.NodeTagID && dstI.Value == fmt.Sprintf("%s.%s", acl.NetworkID.String(), models.OldRemoteAccessTagName) {
-				dstI.Value = fmt.Sprintf("%s.%s", acl.NetworkID.String(), models.GwTagName)
-				acl.Dst[i] = dstI
-				upsert = true
-			}
-		}
-		if upsert {
-			logic.UpsertAcl(acl)
-		}
+}
+
+func settings() {
+	_, err := database.FetchRecords(database.SERVER_SETTINGS)
+	if database.IsEmptyRecord(err) {
+		logic.UpsertServerSettings(logic.GetServerSettingsFromEnv())
+	}
+	settings := logic.GetServerSettings()
+	if settings.AuditLogsRetentionPeriodInDays == 0 {
+		settings.AuditLogsRetentionPeriodInDays = 30
 	}
 	}
-	nets, _ := logic.GetNetworks()
-	for _, netI := range nets {
-		logic.DeleteTag(models.TagID(fmt.Sprintf("%s.%s", netI.NetID, models.OldRemoteAccessTagName)), true)
+	if settings.DefaultDomain == "" {
+		settings.DefaultDomain = servercfg.GetDefaultDomain()
 	}
 	}
+	logic.UpsertServerSettings(settings)
 }
 }

+ 183 - 0
migrate/migrate_schema.go

@@ -0,0 +1,183 @@
+package migrate
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
+	"github.com/gravitl/netmaker/schema"
+	"github.com/gravitl/netmaker/servercfg"
+	"gorm.io/gorm"
+	"os"
+	"path/filepath"
+)
+
+// ToSQLSchema migrates the data from key-value
+// db to sql db.
+//
+// This function archives the old data and does not
+// delete it.
+//
+// Based on the db server, the archival is done in the
+// following way:
+//
+// 1. Sqlite: Moves the old data to a
+// netmaker_archive.db file.
+//
+// 2. Postgres: Moves the data to a netmaker_archive
+// schema within the same database.
+func ToSQLSchema() error {
+	// initialize sql schema db.
+	err := db.InitializeDB(schema.ListModels()...)
+	if err != nil {
+		return err
+	}
+
+	// migrate, if not done already.
+	err = migrate()
+	if err != nil {
+		return err
+	}
+
+	// archive key-value schema db, if not done already.
+	// ignore errors.
+	_ = archive()
+
+	return nil
+}
+
+func migrate() error {
+	// begin a new transaction.
+	dbctx := db.BeginTx(context.TODO())
+	commit := false
+	defer func() {
+		if commit {
+			db.FromContext(dbctx).Commit()
+		} else {
+			db.FromContext(dbctx).Rollback()
+		}
+	}()
+
+	// check if migrated already.
+	migrationJob := &schema.Job{
+		ID: "migration-v1.0.0",
+	}
+	err := migrationJob.Get(dbctx)
+	if err != nil {
+		if !errors.Is(err, gorm.ErrRecordNotFound) {
+			return err
+		}
+
+		// initialize key-value schema db.
+		err := database.InitializeDatabase()
+		if err != nil {
+			return err
+		}
+		defer database.CloseDB()
+
+		// migrate.
+		// TODO: add migration code.
+
+		// mark migration job completed.
+		err = migrationJob.Create(dbctx)
+		if err != nil {
+			return err
+		}
+
+		commit = true
+	}
+
+	return nil
+}
+
+func archive() error {
+	dbServer := servercfg.GetDB()
+	if dbServer != "sqlite" && dbServer != "postgres" {
+		return nil
+	}
+
+	// begin a new transaction.
+	dbctx := db.BeginTx(context.TODO())
+	commit := false
+	defer func() {
+		if commit {
+			db.FromContext(dbctx).Commit()
+		} else {
+			db.FromContext(dbctx).Rollback()
+		}
+	}()
+
+	// check if key-value schema db archived already.
+	archivalJob := &schema.Job{
+		ID: "archival-v1.0.0",
+	}
+	err := archivalJob.Get(dbctx)
+	if err != nil {
+		if !errors.Is(err, gorm.ErrRecordNotFound) {
+			return err
+		}
+
+		// archive.
+		switch dbServer {
+		case "sqlite":
+			err = sqliteArchiveOldData()
+		default:
+			err = pgArchiveOldData()
+		}
+		if err != nil {
+			return err
+		}
+
+		// mark archival job completed.
+		err = archivalJob.Create(dbctx)
+		if err != nil {
+			return err
+		}
+
+		commit = true
+	} else {
+		// remove the residual
+		if dbServer == "sqlite" {
+			_ = os.Remove(filepath.Join("data", "netmaker.db"))
+		}
+	}
+
+	return nil
+}
+
+func sqliteArchiveOldData() error {
+	oldDBFilePath := filepath.Join("data", "netmaker.db")
+	archiveDBFilePath := filepath.Join("data", "netmaker_archive.db")
+
+	// check if netmaker_archive.db exist.
+	_, err := os.Stat(archiveDBFilePath)
+	if err == nil {
+		return nil
+	} else if !os.IsNotExist(err) {
+		return err
+	}
+
+	// rename old db file to netmaker_archive.db.
+	return os.Rename(oldDBFilePath, archiveDBFilePath)
+}
+
+func pgArchiveOldData() error {
+	_, err := database.PGDB.Exec("CREATE SCHEMA IF NOT EXISTS netmaker_archive")
+	if err != nil {
+		return err
+	}
+
+	for _, table := range database.Tables {
+		_, err := database.PGDB.Exec(
+			fmt.Sprintf(
+				"ALTER TABLE public.%s SET SCHEMA netmaker_archive",
+				table,
+			),
+		)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}

+ 55 - 8
models/accessToken.go

@@ -1,13 +1,60 @@
 package models
 package models
 
 
-// AccessToken - token used to access netmaker
-type AccessToken struct {
-	APIConnString string `json:"apiconnstring"`
-	ClientConfig
+import (
+	"context"
+	"time"
+
+	"github.com/gravitl/netmaker/db"
+)
+
+// accessTokenTableName - access tokens table
+const accessTokenTableName = "user_access_tokens"
+
+// UserAccessToken - token used to access netmaker
+type UserAccessToken struct {
+	ID        string    `gorm:"id,primary_key" json:"id"`
+	Name      string    `gorm:"name" json:"name"`
+	UserName  string    `gorm:"user_name" json:"user_name"`
+	ExpiresAt time.Time `gorm:"expires_at" json:"expires_at"`
+	LastUsed  time.Time `gorm:"last_used" json:"last_used"`
+	CreatedBy string    `gorm:"created_by" json:"created_by"`
+	CreatedAt time.Time `gorm:"created_at" json:"created_at"`
+}
+
+func (a *UserAccessToken) Table() string {
+	return accessTokenTableName
+}
+
+func (a *UserAccessToken) Get() error {
+	return db.FromContext(context.TODO()).Table(a.Table()).First(&a).Where("id = ?", a.ID).Error
+}
+
+func (a *UserAccessToken) Update() error {
+	return db.FromContext(context.TODO()).Table(a.Table()).Where("id = ?", a.ID).Updates(&a).Error
 }
 }
 
 
-// ClientConfig - the config of the client
-type ClientConfig struct {
-	Network string `json:"network"`
-	Key     string `json:"key"`
+func (a *UserAccessToken) Create() error {
+	return db.FromContext(context.TODO()).Table(a.Table()).Create(&a).Error
+}
+
+func (a *UserAccessToken) List() (ats []UserAccessToken, err error) {
+	err = db.FromContext(context.TODO()).Table(a.Table()).Find(&ats).Error
+	return
+}
+
+func (a *UserAccessToken) ListByUser() (ats []UserAccessToken) {
+	db.FromContext(context.TODO()).Table(a.Table()).Where("user_name = ?", a.UserName).Find(&ats)
+	if ats == nil {
+		ats = []UserAccessToken{}
+	}
+	return
+}
+
+func (a *UserAccessToken) Delete() error {
+	return db.FromContext(context.TODO()).Table(a.Table()).Where("id = ?", a.ID).Delete(&a).Error
+}
+
+func (a *UserAccessToken) DeleteAllUserTokens() error {
+	return db.FromContext(context.TODO()).Table(a.Table()).Where("user_name = ? OR created_by = ?", a.UserName, a.UserName).Delete(&a).Error
+
 }
 }

+ 1 - 0
models/acl.go

@@ -60,6 +60,7 @@ const (
 	NodeTagID                AclGroupType = "tag"
 	NodeTagID                AclGroupType = "tag"
 	NodeID                   AclGroupType = "device"
 	NodeID                   AclGroupType = "device"
 	EgressRange              AclGroupType = "egress-range"
 	EgressRange              AclGroupType = "egress-range"
+	EgressID                 AclGroupType = "egress-id"
 	NetmakerIPAclID          AclGroupType = "ip"
 	NetmakerIPAclID          AclGroupType = "ip"
 	NetmakerSubNetRangeAClID AclGroupType = "ipset"
 	NetmakerSubNetRangeAClID AclGroupType = "ipset"
 )
 )

+ 3 - 0
models/api_host.go

@@ -31,6 +31,7 @@ type ApiHost struct {
 	NatType             string     `json:"nat_type"              yaml:"nat_type"`
 	NatType             string     `json:"nat_type"              yaml:"nat_type"`
 	PersistentKeepalive int        `json:"persistentkeepalive"   yaml:"persistentkeepalive"`
 	PersistentKeepalive int        `json:"persistentkeepalive"   yaml:"persistentkeepalive"`
 	AutoUpdate          bool       `json:"autoupdate"              yaml:"autoupdate"`
 	AutoUpdate          bool       `json:"autoupdate"              yaml:"autoupdate"`
+	DNS                 string     `json:"dns"               yaml:"dns"`
 }
 }
 
 
 // ApiIface - the interface struct for API usage
 // ApiIface - the interface struct for API usage
@@ -78,6 +79,7 @@ func (h *Host) ConvertNMHostToAPI() *ApiHost {
 	a.NatType = h.NatType
 	a.NatType = h.NatType
 	a.PersistentKeepalive = int(h.PersistentKeepalive.Seconds())
 	a.PersistentKeepalive = int(h.PersistentKeepalive.Seconds())
 	a.AutoUpdate = h.AutoUpdate
 	a.AutoUpdate = h.AutoUpdate
+	a.DNS = h.DNS
 	return &a
 	return &a
 }
 }
 
 
@@ -123,5 +125,6 @@ func (a *ApiHost) ConvertAPIHostToNMHost(currentHost *Host) *Host {
 	h.TurnEndpoint = currentHost.TurnEndpoint
 	h.TurnEndpoint = currentHost.TurnEndpoint
 	h.PersistentKeepalive = time.Duration(a.PersistentKeepalive) * time.Second
 	h.PersistentKeepalive = time.Duration(a.PersistentKeepalive) * time.Second
 	h.AutoUpdate = a.AutoUpdate
 	h.AutoUpdate = a.AutoUpdate
+	h.DNS = strings.ToLower(a.DNS)
 	return &h
 	return &h
 }
 }

+ 9 - 13
models/api_node.go

@@ -32,6 +32,7 @@ type ApiNode struct {
 	NetworkRange6                 string              `json:"networkrange6"`
 	NetworkRange6                 string              `json:"networkrange6"`
 	IsRelayed                     bool                `json:"isrelayed"`
 	IsRelayed                     bool                `json:"isrelayed"`
 	IsRelay                       bool                `json:"isrelay"`
 	IsRelay                       bool                `json:"isrelay"`
+	IsGw                          bool                `json:"is_gw"`
 	RelayedBy                     string              `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
 	RelayedBy                     string              `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
 	RelayedNodes                  []string            `json:"relaynodes" yaml:"relayedNodes"`
 	RelayedNodes                  []string            `json:"relaynodes" yaml:"relayedNodes"`
 	IsEgressGateway               bool                `json:"isegressgateway"`
 	IsEgressGateway               bool                `json:"isegressgateway"`
@@ -72,26 +73,20 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 	convertedNode.Connected = a.Connected
 	convertedNode.Connected = a.Connected
 	convertedNode.ID, _ = uuid.Parse(a.ID)
 	convertedNode.ID, _ = uuid.Parse(a.ID)
 	convertedNode.HostID, _ = uuid.Parse(a.HostID)
 	convertedNode.HostID, _ = uuid.Parse(a.HostID)
-	convertedNode.IsRelay = a.IsRelay
+	//convertedNode.IsRelay = a.IsRelay
 	convertedNode.IsRelayed = a.IsRelayed
 	convertedNode.IsRelayed = a.IsRelayed
 	convertedNode.RelayedBy = a.RelayedBy
 	convertedNode.RelayedBy = a.RelayedBy
 	convertedNode.RelayedNodes = a.RelayedNodes
 	convertedNode.RelayedNodes = a.RelayedNodes
 	convertedNode.PendingDelete = a.PendingDelete
 	convertedNode.PendingDelete = a.PendingDelete
 	convertedNode.FailedOverBy = currentNode.FailedOverBy
 	convertedNode.FailedOverBy = currentNode.FailedOverBy
 	convertedNode.FailOverPeers = currentNode.FailOverPeers
 	convertedNode.FailOverPeers = currentNode.FailOverPeers
-	convertedNode.IsEgressGateway = a.IsEgressGateway
-	convertedNode.IsIngressGateway = a.IsIngressGateway
-	// prevents user from changing ranges, must delete and recreate
-	convertedNode.EgressGatewayRanges = currentNode.EgressGatewayRanges
+	//convertedNode.IsIngressGateway = a.IsIngressGateway
 	convertedNode.IngressGatewayRange = currentNode.IngressGatewayRange
 	convertedNode.IngressGatewayRange = currentNode.IngressGatewayRange
 	convertedNode.IngressGatewayRange6 = currentNode.IngressGatewayRange6
 	convertedNode.IngressGatewayRange6 = currentNode.IngressGatewayRange6
-	convertedNode.DNSOn = a.DNSOn
 	convertedNode.IngressDNS = a.IngressDns
 	convertedNode.IngressDNS = a.IngressDns
 	convertedNode.IngressPersistentKeepalive = a.IngressPersistentKeepalive
 	convertedNode.IngressPersistentKeepalive = a.IngressPersistentKeepalive
 	convertedNode.IngressMTU = a.IngressMTU
 	convertedNode.IngressMTU = a.IngressMTU
 	convertedNode.IsInternetGateway = a.IsInternetGateway
 	convertedNode.IsInternetGateway = a.IsInternetGateway
-	convertedNode.EgressGatewayRequest = currentNode.EgressGatewayRequest
-	convertedNode.EgressGatewayNatEnabled = currentNode.EgressGatewayNatEnabled
 	convertedNode.InternetGwID = currentNode.InternetGwID
 	convertedNode.InternetGwID = currentNode.InternetGwID
 	convertedNode.InetNodeReq = currentNode.InetNodeReq
 	convertedNode.InetNodeReq = currentNode.InetNodeReq
 	convertedNode.RelayedNodes = a.RelayedNodes
 	convertedNode.RelayedNodes = a.RelayedNodes
@@ -137,6 +132,11 @@ func (a *ApiNode) ConvertToServerNode(currentNode *Node) *Node {
 		convertedNode.AdditionalRagIps = append(convertedNode.AdditionalRagIps, ragIp)
 		convertedNode.AdditionalRagIps = append(convertedNode.AdditionalRagIps, ragIp)
 	}
 	}
 	convertedNode.Tags = a.Tags
 	convertedNode.Tags = a.Tags
+	convertedNode.IsGw = a.IsGw
+	if convertedNode.IsGw {
+		convertedNode.IsRelay = true
+		convertedNode.IsIngressGateway = true
+	}
 	return &convertedNode
 	return &convertedNode
 }
 }
 
 
@@ -185,14 +185,10 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
 	}
 	}
 	apiNode.IsRelayed = nm.IsRelayed
 	apiNode.IsRelayed = nm.IsRelayed
 	apiNode.IsRelay = nm.IsRelay
 	apiNode.IsRelay = nm.IsRelay
+	apiNode.IsGw = nm.IsGw
 	apiNode.RelayedBy = nm.RelayedBy
 	apiNode.RelayedBy = nm.RelayedBy
 	apiNode.RelayedNodes = nm.RelayedNodes
 	apiNode.RelayedNodes = nm.RelayedNodes
-	apiNode.IsEgressGateway = nm.IsEgressGateway
 	apiNode.IsIngressGateway = nm.IsIngressGateway
 	apiNode.IsIngressGateway = nm.IsIngressGateway
-	apiNode.EgressGatewayRanges = nm.EgressGatewayRanges
-	apiNode.EgressGatewayRangesWithMetric = nm.EgressGatewayRequest.RangesWithMetric
-	apiNode.EgressGatewayNatEnabled = nm.EgressGatewayNatEnabled
-	apiNode.DNSOn = nm.DNSOn
 	apiNode.IngressDns = nm.IngressDNS
 	apiNode.IngressDns = nm.IngressDNS
 	apiNode.IngressPersistentKeepalive = nm.IngressPersistentKeepalive
 	apiNode.IngressPersistentKeepalive = nm.IngressPersistentKeepalive
 	apiNode.IngressMTU = nm.IngressMTU
 	apiNode.IngressMTU = nm.IngressMTU

+ 14 - 0
models/egress.go

@@ -0,0 +1,14 @@
+package models
+
+type EgressReq struct {
+	ID          string         `json:"id"`
+	Name        string         `json:"name"`
+	Network     string         `json:"network"`
+	Description string         `json:"description"`
+	Nodes       map[string]int `json:"nodes"`
+	Tags        []string       `json:"tags"`
+	Range       string         `json:"range"`
+	Nat         bool           `json:"nat"`
+	Status      bool           `json:"status"`
+	IsInetGw    bool           `json:"is_internet_gateway"`
+}

+ 2 - 0
models/enrollment_key.go

@@ -54,6 +54,7 @@ type EnrollmentKey struct {
 	Relay         uuid.UUID `json:"relay"`
 	Relay         uuid.UUID `json:"relay"`
 	Groups        []TagID   `json:"groups"`
 	Groups        []TagID   `json:"groups"`
 	Default       bool      `json:"default"`
 	Default       bool      `json:"default"`
+	AutoEgress    bool      `json:"auto_egress"`
 }
 }
 
 
 // APIEnrollmentKey - used to create enrollment keys via API
 // APIEnrollmentKey - used to create enrollment keys via API
@@ -66,6 +67,7 @@ type APIEnrollmentKey struct {
 	Type          KeyType  `json:"type"`
 	Type          KeyType  `json:"type"`
 	Relay         string   `json:"relay"`
 	Relay         string   `json:"relay"`
 	Groups        []TagID  `json:"groups"`
 	Groups        []TagID  `json:"groups"`
+	AutoEgress    bool     `json:"auto_egress"`
 }
 }
 
 
 // RegisterResponse - the response to a successful enrollment register
 // RegisterResponse - the response to a successful enrollment register

+ 78 - 0
models/events.go

@@ -0,0 +1,78 @@
+package models
+
+type Action string
+
+const (
+	Create            Action = "CREATE"
+	Update            Action = "UPDATE"
+	Delete            Action = "DELETE"
+	DeleteAll         Action = "DELETE_ALL"
+	Login             Action = "LOGIN"
+	LogOut            Action = "LOGOUT"
+	Connect           Action = "CONNECT"
+	Sync              Action = "SYNC"
+	RefreshKey        Action = "REFRESH_KEY"
+	RefreshAllKeys    Action = "REFRESH_ALL_KEYS"
+	SyncAll           Action = "SYNC_ALL"
+	UpgradeAll        Action = "UPGRADE_ALL"
+	Disconnect        Action = "DISCONNECT"
+	JoinHostToNet     Action = "JOIN_HOST_TO_NETWORK"
+	RemoveHostFromNet Action = "REMOVE_HOST_FROM_NETWORK"
+)
+
+type SubjectType string
+
+const (
+	UserSub            SubjectType = "USER"
+	UserAccessTokenSub SubjectType = "USER_ACCESS_TOKEN"
+	DeviceSub          SubjectType = "DEVICE"
+	NodeSub            SubjectType = "NODE"
+	GatewaySub         SubjectType = "GATEWAY"
+	SettingSub         SubjectType = "SETTING"
+	AclSub             SubjectType = "ACL"
+	TagSub             SubjectType = "TAG"
+	UserRoleSub        SubjectType = "USER_ROLE"
+	UserGroupSub       SubjectType = "USER_GROUP"
+	UserInviteSub      SubjectType = "USER_INVITE"
+	PendingUserSub     SubjectType = "PENDING_USER"
+	EgressSub          SubjectType = "EGRESS"
+	NetworkSub         SubjectType = "NETWORK"
+	DashboardSub       SubjectType = "DASHBOARD"
+	EnrollmentKeySub   SubjectType = "ENROLLMENT_KEY"
+	ClientAppSub       SubjectType = "CLIENT-APP"
+)
+
+func (sub SubjectType) String() string {
+	return string(sub)
+}
+
+type Origin string
+
+const (
+	Dashboard Origin = "DASHBOARD"
+	Api       Origin = "API"
+	NMCTL     Origin = "NMCTL"
+	ClientApp Origin = "CLIENT-APP"
+)
+
+type Subject struct {
+	ID   string      `json:"id"`
+	Name string      `json:"name"`
+	Type SubjectType `json:"subject_type"`
+	Info interface{} `json:"info"`
+}
+
+type Diff struct {
+	Old interface{}
+	New interface{}
+}
+
+type Event struct {
+	Action      Action
+	Source      Subject
+	Origin      Origin
+	Target      Subject
+	TriggeredBy string
+	NetworkID   NetworkID
+	Diff        Diff
+}

+ 1 - 0
models/host.go

@@ -69,6 +69,7 @@ type Host struct {
 	IsStaticPort        bool             `json:"isstaticport"            yaml:"isstaticport"`
 	IsStaticPort        bool             `json:"isstaticport"            yaml:"isstaticport"`
 	IsStatic            bool             `json:"isstatic"        yaml:"isstatic"`
 	IsStatic            bool             `json:"isstatic"        yaml:"isstatic"`
 	IsDefault           bool             `json:"isdefault"               yaml:"isdefault"`
 	IsDefault           bool             `json:"isdefault"               yaml:"isdefault"`
+	DNS                 string           `json:"dns_status"               yaml:"dns_status"`
 	NatType             string           `json:"nat_type,omitempty"      yaml:"nat_type,omitempty"`
 	NatType             string           `json:"nat_type,omitempty"      yaml:"nat_type,omitempty"`
 	TurnEndpoint        *netip.AddrPort  `json:"turn_endpoint,omitempty" yaml:"turn_endpoint,omitempty"`
 	TurnEndpoint        *netip.AddrPort  `json:"turn_endpoint,omitempty" yaml:"turn_endpoint,omitempty"`
 	PersistentKeepalive time.Duration    `json:"persistentkeepalive" swaggertype:"primitive,integer" format:"int64" yaml:"persistentkeepalive"`
 	PersistentKeepalive time.Duration    `json:"persistentkeepalive" swaggertype:"primitive,integer" format:"int64" yaml:"persistentkeepalive"`

+ 1 - 1
models/mqtt.go

@@ -107,7 +107,7 @@ type KeyUpdate struct {
 // FwUpdate - struct for firewall updates
 // FwUpdate - struct for firewall updates
 type FwUpdate struct {
 type FwUpdate struct {
 	AllowAll        bool                   `json:"allow_all"`
 	AllowAll        bool                   `json:"allow_all"`
-	AllowedNetworks []net.IPNet            `json:"networks"`
+	AllowedNetworks []AclRule              `json:"networks"`
 	IsEgressGw      bool                   `json:"is_egress_gw"`
 	IsEgressGw      bool                   `json:"is_egress_gw"`
 	IsIngressGw     bool                   `json:"is_ingress_gw"`
 	IsIngressGw     bool                   `json:"is_ingress_gw"`
 	EgressInfo      map[string]EgressInfo  `json:"egress_info"`
 	EgressInfo      map[string]EgressInfo  `json:"egress_info"`

+ 20 - 15
models/node.go

@@ -87,7 +87,6 @@ type CommonNode struct {
 	IsGw                bool      `json:"is_gw"             yaml:"is_gw"`
 	IsGw                bool      `json:"is_gw"             yaml:"is_gw"`
 	RelayedNodes        []string  `json:"relaynodes"          yaml:"relayedNodes"`
 	RelayedNodes        []string  `json:"relaynodes"          yaml:"relayedNodes"`
 	IngressDNS          string    `json:"ingressdns"          yaml:"ingressdns"`
 	IngressDNS          string    `json:"ingressdns"          yaml:"ingressdns"`
-	DNSOn               bool      `json:"dnson"               yaml:"dnson"`
 }
 }
 
 
 // Node - a model of a network node
 // Node - a model of a network node
@@ -109,7 +108,7 @@ type Node struct {
 	DefaultACL        string              `json:"defaultacl,omitempty"    bson:"defaultacl,omitempty"    yaml:"defaultacl,omitempty"    validate:"checkyesornoorunset"`
 	DefaultACL        string              `json:"defaultacl,omitempty"    bson:"defaultacl,omitempty"    yaml:"defaultacl,omitempty"    validate:"checkyesornoorunset"`
 	OwnerID           string              `json:"ownerid,omitempty"       bson:"ownerid,omitempty"       yaml:"ownerid,omitempty"`
 	OwnerID           string              `json:"ownerid,omitempty"       bson:"ownerid,omitempty"       yaml:"ownerid,omitempty"`
 	IsFailOver        bool                `json:"is_fail_over"                                           yaml:"is_fail_over"`
 	IsFailOver        bool                `json:"is_fail_over"                                           yaml:"is_fail_over"`
-	FailOverPeers     map[string]struct{} `json:"fail_over_peers"                                        yaml:"fail_over_peers"`
+	FailOverPeers     map[string]struct{} `json:"fail_over_peers"                                       yaml:"fail_over_peers"`
 	FailedOverBy      uuid.UUID           `json:"failed_over_by"                                         yaml:"failed_over_by"`
 	FailedOverBy      uuid.UUID           `json:"failed_over_by"                                         yaml:"failed_over_by"`
 	IsInternetGateway bool                `json:"isinternetgateway"                                      yaml:"isinternetgateway"`
 	IsInternetGateway bool                `json:"isinternetgateway"                                      yaml:"isinternetgateway"`
 	InetNodeReq       InetNodeReq         `json:"inet_node_req"                                          yaml:"inet_node_req"`
 	InetNodeReq       InetNodeReq         `json:"inet_node_req"                                          yaml:"inet_node_req"`
@@ -121,6 +120,16 @@ type Node struct {
 	StaticNode        ExtClient           `json:"static_node"`
 	StaticNode        ExtClient           `json:"static_node"`
 	Status            NodeStatus          `json:"node_status"`
 	Status            NodeStatus          `json:"node_status"`
 	Mutex             *sync.Mutex         `json:"-"`
 	Mutex             *sync.Mutex         `json:"-"`
+	EgressDetails     EgressDetails       `json:"-"`
+}
+type EgressDetails struct {
+	EgressGatewayNatEnabled bool
+	EgressGatewayRequest    EgressGatewayRequest
+	IsEgressGateway         bool
+	EgressGatewayRanges     []string
+	// IsInternetGateway       bool        `json:"isinternetgateway"                                      yaml:"isinternetgateway"`
+	// InetNodeReq             InetNodeReq `json:"inet_node_req"                                          yaml:"inet_node_req"`
+	// InternetGwID            string      `json:"internetgw_node_id"                                     yaml:"internetgw_node_id"`
 }
 }
 
 
 // LegacyNode - legacy struct for node model
 // LegacyNode - legacy struct for node model
@@ -377,17 +386,17 @@ func (node *LegacyNode) SetIsStaticDefault() {
 
 
 // Node.SetLastModified - set last modified initial time
 // Node.SetLastModified - set last modified initial time
 func (node *Node) SetLastModified() {
 func (node *Node) SetLastModified() {
-	node.LastModified = time.Now()
+	node.LastModified = time.Now().UTC()
 }
 }
 
 
 // Node.SetLastCheckIn - set checkin time of node
 // Node.SetLastCheckIn - set checkin time of node
 func (node *Node) SetLastCheckIn() {
 func (node *Node) SetLastCheckIn() {
-	node.LastCheckIn = time.Now()
+	node.LastCheckIn = time.Now().UTC()
 }
 }
 
 
 // Node.SetLastPeerUpdate - sets last peer update time
 // Node.SetLastPeerUpdate - sets last peer update time
 func (node *Node) SetLastPeerUpdate() {
 func (node *Node) SetLastPeerUpdate() {
-	node.LastPeerUpdate = time.Now()
+	node.LastPeerUpdate = time.Now().UTC()
 }
 }
 
 
 // Node.SetExpirationDateTime - sets node expiry time
 // Node.SetExpirationDateTime - sets node expiry time
@@ -442,15 +451,9 @@ func (newNode *Node) Fill(
 	if newNode.Network == "" {
 	if newNode.Network == "" {
 		newNode.Network = currentNode.Network
 		newNode.Network = currentNode.Network
 	}
 	}
-	if newNode.IsEgressGateway != currentNode.IsEgressGateway {
-		newNode.IsEgressGateway = currentNode.IsEgressGateway
-	}
 	if newNode.IsIngressGateway != currentNode.IsIngressGateway {
 	if newNode.IsIngressGateway != currentNode.IsIngressGateway {
 		newNode.IsIngressGateway = currentNode.IsIngressGateway
 		newNode.IsIngressGateway = currentNode.IsIngressGateway
 	}
 	}
-	if newNode.EgressGatewayRanges == nil {
-		newNode.EgressGatewayRanges = currentNode.EgressGatewayRanges
-	}
 	if newNode.IngressGatewayRange == "" {
 	if newNode.IngressGatewayRange == "" {
 		newNode.IngressGatewayRange = currentNode.IngressGatewayRange
 		newNode.IngressGatewayRange = currentNode.IngressGatewayRange
 	}
 	}
@@ -478,6 +481,12 @@ func (newNode *Node) Fill(
 	if newNode.IsFailOver != currentNode.IsFailOver {
 	if newNode.IsFailOver != currentNode.IsFailOver {
 		newNode.IsFailOver = currentNode.IsFailOver
 		newNode.IsFailOver = currentNode.IsFailOver
 	}
 	}
+	if newNode.Tags == nil {
+		if currentNode.Tags == nil {
+			currentNode.Tags = make(map[TagID]struct{})
+		}
+		newNode.Tags = currentNode.Tags
+	}
 }
 }
 
 
 // StringWithCharset - returns random string inside defined charset
 // StringWithCharset - returns random string inside defined charset
@@ -567,9 +576,7 @@ func (ln *LegacyNode) ConvertToNewNode() (*Host, *Node) {
 		}
 		}
 	}
 	}
 	node.Action = ln.Action
 	node.Action = ln.Action
-	node.IsEgressGateway = parseBool(ln.IsEgressGateway)
 	node.IsIngressGateway = parseBool(ln.IsIngressGateway)
 	node.IsIngressGateway = parseBool(ln.IsIngressGateway)
-	node.DNSOn = parseBool(ln.DNSOn)
 
 
 	return &host, &node
 	return &host, &node
 }
 }
@@ -601,7 +608,6 @@ func (n *Node) Legacy(h *Host, s *ServerConfig, net *Network) *LegacyNode {
 	//l.IsRelay = formatBool(n.IsRelay)
 	//l.IsRelay = formatBool(n.IsRelay)
 	//l.IsDocker = formatBool(n.IsDocker)
 	//l.IsDocker = formatBool(n.IsDocker)
 	//l.IsK8S = formatBool(n.IsK8S)
 	//l.IsK8S = formatBool(n.IsK8S)
-	l.IsEgressGateway = formatBool(n.IsEgressGateway)
 	l.IsIngressGateway = formatBool(n.IsIngressGateway)
 	l.IsIngressGateway = formatBool(n.IsIngressGateway)
 	//l.EgressGatewayRanges = n.EgressGatewayRanges
 	//l.EgressGatewayRanges = n.EgressGatewayRanges
 	//l.EgressGatewayNatEnabled = n.EgressGatewayNatEnabled
 	//l.EgressGatewayNatEnabled = n.EgressGatewayNatEnabled
@@ -611,7 +617,6 @@ func (n *Node) Legacy(h *Host, s *ServerConfig, net *Network) *LegacyNode {
 	//l.IngressGatewayRange6 = n.IngressGatewayRange6
 	//l.IngressGatewayRange6 = n.IngressGatewayRange6
 	l.IsStatic = formatBool(h.IsStatic)
 	l.IsStatic = formatBool(h.IsStatic)
 	l.UDPHolePunch = formatBool(true)
 	l.UDPHolePunch = formatBool(true)
-	l.DNSOn = formatBool(n.DNSOn)
 	l.Action = n.Action
 	l.Action = n.Action
 	l.IPForwarding = formatBool(h.IPForwarding)
 	l.IPForwarding = formatBool(h.IPForwarding)
 	l.OS = h.OS
 	l.OS = h.OS

+ 47 - 0
models/settings.go

@@ -0,0 +1,47 @@
+package models
+
+type Theme string
+
+const (
+	Dark   Theme = "dark"
+	Light  Theme = "light"
+	System Theme = "system"
+)
+
+type ServerSettings struct {
+	NetclientAutoUpdate            bool     `json:"netclientautoupdate"`
+	Verbosity                      int32    `json:"verbosity"`
+	AuthProvider                   string   `json:"authprovider"`
+	OIDCIssuer                     string   `json:"oidcissuer"`
+	ClientID                       string   `json:"client_id"`
+	ClientSecret                   string   `json:"client_secret"`
+	SyncEnabled                    bool     `json:"sync_enabled"`
+	GoogleAdminEmail               string   `json:"google_admin_email"`
+	GoogleSACredsJson              string   `json:"google_sa_creds_json"`
+	AzureTenant                    string   `json:"azure_tenant"`
+	UserFilters                    []string `json:"user_filters"`
+	GroupFilters                   []string `json:"group_filters"`
+	IDPSyncInterval                string   `json:"idp_sync_interval"`
+	Telemetry                      string   `json:"telemetry"`
+	BasicAuth                      bool     `json:"basic_auth"`
+	JwtValidityDuration            int      `json:"jwt_validity_duration"`
+	RacAutoDisable                 bool     `json:"rac_auto_disable"`
+	RacRestrictToSingleNetwork     bool     `json:"rac_restrict_to_single_network"`
+	EndpointDetection              bool     `json:"endpoint_detection"`
+	AllowedEmailDomains            string   `json:"allowed_email_domains"`
+	EmailSenderAddr                string   `json:"email_sender_addr"`
+	EmailSenderUser                string   `json:"email_sender_user"`
+	EmailSenderPassword            string   `json:"email_sender_password"`
+	SmtpHost                       string   `json:"smtp_host"`
+	SmtpPort                       int      `json:"smtp_port"`
+	MetricInterval                 string   `json:"metric_interval"`
+	MetricsPort                    int      `json:"metrics_port"`
+	ManageDNS                      bool     `json:"manage_dns"`
+	DefaultDomain                  string   `json:"default_domain"`
+	Stun                           bool     `json:"stun"`
+	StunServers                    string   `json:"stun_servers"`
+	Theme                          Theme    `json:"theme"`
+	TextSize                       string   `json:"text_size"`
+	ReducedMotion                  bool     `json:"reduced_motion"`
+	AuditLogsRetentionPeriodInDays int      `json:"audit_logs_retention_period"`
+}

+ 2 - 0
models/structs.go

@@ -156,6 +156,7 @@ type ExtPeersResponse struct {
 type EgressRangeMetric struct {
 type EgressRangeMetric struct {
 	Network     string `json:"network"`
 	Network     string `json:"network"`
 	RouteMetric uint32 `json:"route_metric"` // preffered range 1-999
 	RouteMetric uint32 `json:"route_metric"` // preffered range 1-999
+	Nat         bool   `json:"nat"`
 }
 }
 
 
 // EgressGatewayRequest - egress gateway request
 // EgressGatewayRequest - egress gateway request
@@ -263,6 +264,7 @@ type NodeJoinResponse struct {
 type ServerConfig struct {
 type ServerConfig struct {
 	CoreDNSAddr       string `yaml:"corednsaddr"`
 	CoreDNSAddr       string `yaml:"corednsaddr"`
 	API               string `yaml:"api"`
 	API               string `yaml:"api"`
+	APIHost           string `yaml:"apihost"`
 	APIPort           string `yaml:"apiport"`
 	APIPort           string `yaml:"apiport"`
 	DNSMode           string `yaml:"dnsmode"`
 	DNSMode           string `yaml:"dnsmode"`
 	Version           string `yaml:"version"`
 	Version           string `yaml:"version"`

+ 33 - 14
models/user_mgmt.go

@@ -13,6 +13,7 @@ type RsrcID string
 type UserRoleID string
 type UserRoleID string
 type UserGroupID string
 type UserGroupID string
 type AuthType string
 type AuthType string
+type TokenType string
 
 
 var (
 var (
 	BasicAuth AuthType = "basic_auth"
 	BasicAuth AuthType = "basic_auth"
@@ -35,6 +36,15 @@ func GetRAGRoleID(netID, hostID string) UserRoleID {
 	return UserRoleID(fmt.Sprintf("netID-%s-rag-%s", netID, hostID))
 	return UserRoleID(fmt.Sprintf("netID-%s-rag-%s", netID, hostID))
 }
 }
 
 
+func (t TokenType) String() string {
+	return string(t)
+}
+
+var (
+	UserIDTokenType TokenType = "user_id_token"
+	AccessTokenType TokenType = "access_token"
+)
+
 var RsrcTypeMap = map[RsrcType]struct{}{
 var RsrcTypeMap = map[RsrcType]struct{}{
 	HostRsrc:           {},
 	HostRsrc:           {},
 	RelayRsrc:          {},
 	RelayRsrc:          {},
@@ -55,6 +65,7 @@ const (
 	HostRsrc           RsrcType = "hosts"
 	HostRsrc           RsrcType = "hosts"
 	RelayRsrc          RsrcType = "relays"
 	RelayRsrc          RsrcType = "relays"
 	RemoteAccessGwRsrc RsrcType = "remote_access_gw"
 	RemoteAccessGwRsrc RsrcType = "remote_access_gw"
+	GatewayRsrc        RsrcType = "gateways"
 	ExtClientsRsrc     RsrcType = "extclients"
 	ExtClientsRsrc     RsrcType = "extclients"
 	InetGwRsrc         RsrcType = "inet_gw"
 	InetGwRsrc         RsrcType = "inet_gw"
 	EgressGwRsrc       RsrcType = "egress"
 	EgressGwRsrc       RsrcType = "egress"
@@ -134,17 +145,20 @@ type CreateGroupReq struct {
 }
 }
 
 
 type UserGroup struct {
 type UserGroup struct {
-	ID           UserGroupID                           `json:"id"`
-	Default      bool                                  `json:"default"`
-	Name         string                                `json:"name"`
-	NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
-	MetaData     string                                `json:"meta_data"`
+	ID                         UserGroupID                           `json:"id"`
+	ExternalIdentityProviderID string                                `json:"external_identity_provider_id"`
+	Default                    bool                                  `json:"default"`
+	Name                       string                                `json:"name"`
+	NetworkRoles               map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+	MetaData                   string                                `json:"meta_data"`
 }
 }
 
 
 // User struct - struct for Users
 // User struct - struct for Users
 type User struct {
 type User struct {
 	UserName                   string                                `json:"username" bson:"username" validate:"min=3,in_charset|email"`
 	UserName                   string                                `json:"username" bson:"username" validate:"min=3,in_charset|email"`
 	ExternalIdentityProviderID string                                `json:"external_identity_provider_id"`
 	ExternalIdentityProviderID string                                `json:"external_identity_provider_id"`
+	DisplayName                string                                `json:"display_name"`
+	AccountDisabled            bool                                  `json:"account_disabled"`
 	Password                   string                                `json:"password" bson:"password" validate:"required,min=5"`
 	Password                   string                                `json:"password" bson:"password" validate:"required,min=5"`
 	IsAdmin                    bool                                  `json:"isadmin" bson:"isadmin"` // deprecated
 	IsAdmin                    bool                                  `json:"isadmin" bson:"isadmin"` // deprecated
 	IsSuperAdmin               bool                                  `json:"issuperadmin"`           // deprecated
 	IsSuperAdmin               bool                                  `json:"issuperadmin"`           // deprecated
@@ -164,15 +178,18 @@ type ReturnUserWithRolesAndGroups struct {
 
 
 // ReturnUser - return user struct
 // ReturnUser - return user struct
 type ReturnUser struct {
 type ReturnUser struct {
-	UserName       string                                `json:"username"`
-	IsAdmin        bool                                  `json:"isadmin"`
-	IsSuperAdmin   bool                                  `json:"issuperadmin"`
-	AuthType       AuthType                              `json:"auth_type"`
-	RemoteGwIDs    map[string]struct{}                   `json:"remote_gw_ids"` // deprecated
-	UserGroups     map[UserGroupID]struct{}              `json:"user_group_ids"`
-	PlatformRoleID UserRoleID                            `json:"platform_role_id"`
-	NetworkRoles   map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
-	LastLoginTime  time.Time                             `json:"last_login_time"`
+	UserName                   string                                `json:"username"`
+	ExternalIdentityProviderID string                                `json:"external_identity_provider_id"`
+	DisplayName                string                                `json:"display_name"`
+	AccountDisabled            bool                                  `json:"account_disabled"`
+	IsAdmin                    bool                                  `json:"isadmin"`
+	IsSuperAdmin               bool                                  `json:"issuperadmin"`
+	AuthType                   AuthType                              `json:"auth_type"`
+	RemoteGwIDs                map[string]struct{}                   `json:"remote_gw_ids"` // deprecated
+	UserGroups                 map[UserGroupID]struct{}              `json:"user_group_ids"`
+	PlatformRoleID             UserRoleID                            `json:"platform_role_id"`
+	NetworkRoles               map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+	LastLoginTime              time.Time                             `json:"last_login_time"`
 }
 }
 
 
 // UserAuthParams - user auth params struct
 // UserAuthParams - user auth params struct
@@ -185,6 +202,8 @@ type UserAuthParams struct {
 type UserClaims struct {
 type UserClaims struct {
 	Role           UserRoleID
 	Role           UserRoleID
 	UserName       string
 	UserName       string
+	Api            string
+	TokenType      TokenType
 	RacAutoDisable bool
 	RacAutoDisable bool
 	jwt.RegisteredClaims
 	jwt.RegisteredClaims
 }
 }

+ 1 - 1
mq/publishers.go

@@ -21,7 +21,7 @@ func PublishPeerUpdate(replacePeers bool) error {
 		return nil
 		return nil
 	}
 	}
 
 
-	if servercfg.GetManageDNS() {
+	if logic.GetManageDNS() {
 		sendDNSSync()
 		sendDNSSync()
 	}
 	}
 
 

+ 48 - 8
pro/auth/auth.go

@@ -1,9 +1,11 @@
 package auth
 package auth
 
 
 import (
 import (
+	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"strconv"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -34,11 +36,38 @@ const (
 
 
 // OAuthUser - generic OAuth strategy user
 // OAuthUser - generic OAuth strategy user
 type OAuthUser struct {
 type OAuthUser struct {
-	Name              string `json:"name" bson:"name"`
-	Email             string `json:"email" bson:"email"`
-	Login             string `json:"login" bson:"login"`
-	UserPrincipalName string `json:"userPrincipalName" bson:"userPrincipalName"`
-	AccessToken       string `json:"accesstoken" bson:"accesstoken"`
+	ID                StringOrInt `json:"id" bson:"id"`
+	Name              string      `json:"name" bson:"name"`
+	Email             string      `json:"email" bson:"email"`
+	Login             string      `json:"login" bson:"login"`
+	UserPrincipalName string      `json:"userPrincipalName" bson:"userPrincipalName"`
+	AccessToken       string      `json:"accesstoken" bson:"accesstoken"`
+}
+
+// TODO: this is a very poor solution.
+// We should not return the same OAuthUser for different
+// IdPs. They should have the user that their APIs return.
+// But that's a very big change. So, making do with this
+// for now.
+
+type StringOrInt string
+
+func (s *StringOrInt) UnmarshalJSON(data []byte) error {
+	// Try to unmarshal as string directly
+	var strVal string
+	if err := json.Unmarshal(data, &strVal); err == nil {
+		*s = StringOrInt(strVal)
+		return nil
+	}
+
+	// Try to unmarshal as int and convert to string
+	var intVal int
+	if err := json.Unmarshal(data, &intVal); err == nil {
+		*s = StringOrInt(strconv.Itoa(intVal))
+		return nil
+	}
+
+	return fmt.Errorf("cannot unmarshal %s into StringOrInt", string(data))
 }
 }
 
 
 var (
 var (
@@ -47,7 +76,7 @@ var (
 )
 )
 
 
 func getCurrentAuthFunctions() map[string]interface{} {
 func getCurrentAuthFunctions() map[string]interface{} {
-	var authInfo = servercfg.GetAuthProviderInfo()
+	var authInfo = logic.GetAuthProviderInfo(logic.GetServerSettings())
 	var authProvider = authInfo[0]
 	var authProvider = authInfo[0]
 	switch authProvider {
 	switch authProvider {
 	case google_provider_name:
 	case google_provider_name:
@@ -63,6 +92,17 @@ func getCurrentAuthFunctions() map[string]interface{} {
 	}
 	}
 }
 }
 
 
+// ResetAuthProvider resets the auth provider configuration.
+func ResetAuthProvider() {
+	settings := logic.GetServerSettings()
+
+	if settings.AuthProvider == "" {
+		auth_provider = nil
+	}
+
+	InitializeAuthProvider()
+}
+
 // InitializeAuthProvider - initializes the auth provider if any is present
 // InitializeAuthProvider - initializes the auth provider if any is present
 func InitializeAuthProvider() string {
 func InitializeAuthProvider() string {
 	var functions = getCurrentAuthFunctions()
 	var functions = getCurrentAuthFunctions()
@@ -74,7 +114,7 @@ func InitializeAuthProvider() string {
 	if err != nil {
 	if err != nil {
 		logger.FatalLog("failed to set auth_secret", err.Error())
 		logger.FatalLog("failed to set auth_secret", err.Error())
 	}
 	}
-	var authInfo = servercfg.GetAuthProviderInfo()
+	var authInfo = logic.GetAuthProviderInfo(logic.GetServerSettings())
 	var serverConn = servercfg.GetAPIHost()
 	var serverConn = servercfg.GetAPIHost()
 	if strings.Contains(serverConn, "localhost") || strings.Contains(serverConn, "127.0.0.1") {
 	if strings.Contains(serverConn, "localhost") || strings.Contains(serverConn, "127.0.0.1") {
 		serverConn = "http://" + serverConn
 		serverConn = "http://" + serverConn
@@ -275,7 +315,7 @@ func isStateCached(state string) bool {
 
 
 // isEmailAllowed - checks if email is allowed to signup
 // isEmailAllowed - checks if email is allowed to signup
 func isEmailAllowed(email string) bool {
 func isEmailAllowed(email string) bool {
-	allowedDomains := servercfg.GetAllowedEmailDomains()
+	allowedDomains := logic.GetAllowedEmailDomains()
 	domains := strings.Split(allowedDomains, ",")
 	domains := strings.Split(allowedDomains, ",")
 	if len(domains) == 1 && domains[0] == "*" {
 	if len(domains) == 1 && domains[0] == "*" {
 		return true
 		return true

+ 27 - 5
pro/auth/azure-ad.go

@@ -35,7 +35,7 @@ func initAzureAD(redirectURL string, clientID string, clientSecret string) {
 		ClientID:     clientID,
 		ClientID:     clientID,
 		ClientSecret: clientSecret,
 		ClientSecret: clientSecret,
 		Scopes:       []string{"User.Read", "email", "profile", "openid"},
 		Scopes:       []string{"User.Read", "email", "profile", "openid"},
-		Endpoint:     microsoft.AzureADEndpoint(servercfg.GetAzureTenant()),
+		Endpoint:     microsoft.AzureADEndpoint(logic.GetAzureTenant()),
 	}
 	}
 }
 }
 
 
@@ -111,7 +111,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 					return
 					return
 				}
 				}
-				user.ExternalIdentityProviderID = content.UserPrincipalName
+				user.ExternalIdentityProviderID = string(content.ID)
 				if err = logic.CreateUser(&user); err != nil {
 				if err = logic.CreateUser(&user); err != nil {
 					handleSomethingWentWrong(w)
 					handleSomethingWentWrong(w)
 					return
 					return
@@ -124,7 +124,9 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 					return
 					return
 				}
 				}
 				err = logic.InsertPendingUser(&models.User{
 				err = logic.InsertPendingUser(&models.User{
-					UserName: content.Email,
+					UserName:                   content.Email,
+					ExternalIdentityProviderID: string(content.ID),
+					AuthType:                   models.OAuth,
 				})
 				})
 				if err != nil {
 				if err != nil {
 					handleSomethingWentWrong(w)
 					handleSomethingWentWrong(w)
@@ -152,6 +154,12 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotFound(w)
 		handleOauthUserNotFound(w)
 		return
 		return
 	}
 	}
+
+	if user.AccountDisabled {
+		handleUserAccountDisabled(w)
+		return
+	}
+
 	userRole, err := logic.GetRole(user.PlatformRoleID)
 	userRole, err := logic.GetRole(user.PlatformRoleID)
 	if err != nil {
 	if err != nil {
 		handleSomethingWentWrong(w)
 		handleSomethingWentWrong(w)
@@ -176,7 +184,22 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		logger.Log(1, "could not parse jwt for user", authRequest.UserName)
 		return
 		return
 	}
 	}
-
+	logic.LogEvent(&models.Event{
+		Action: models.Login,
+		Source: models.Subject{
+			ID:   user.UserName,
+			Name: user.UserName,
+			Type: models.UserSub,
+		},
+		TriggeredBy: user.UserName,
+		Target: models.Subject{
+			ID:   models.DashboardSub.String(),
+			Name: models.DashboardSub.String(),
+			Type: models.DashboardSub,
+			Info: user,
+		},
+		Origin: models.Dashboard,
+	})
 	logger.Log(1, "completed azure OAuth sigin in for", content.Email)
 	logger.Log(1, "completed azure OAuth sigin in for", content.Email)
 	http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect)
 	http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect)
 }
 }
@@ -220,7 +243,6 @@ func getAzureUserInfo(state string, code string) (*OAuthUser, error) {
 	}
 	}
 	if userInfo.Email == "" && userInfo.UserPrincipalName != "" {
 	if userInfo.Email == "" && userInfo.UserPrincipalName != "" {
 		userInfo.Email = userInfo.UserPrincipalName
 		userInfo.Email = userInfo.UserPrincipalName
-
 	}
 	}
 	if userInfo.Email == "" {
 	if userInfo.Email == "" {
 		err = errors.New("failed to fetch user email from SSO state")
 		err = errors.New("failed to fetch user email from SSO state")

Some files were not shown because too many files changed in this diff