Quellcode durchsuchen

Merge pull request #3081 from gravitl/release-v0.25.0

v0.25.0
Abhishek K vor 1 Jahr
Ursprung
Commit
4a7fbc7456
95 geänderte Dateien mit 9889 neuen und 5650 gelöschten Zeilen
  1. 1 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 50 0
      .github/workflows/docs.yml
  3. 1 1
      Dockerfile
  4. 1 1
      Dockerfile-quick
  5. 1 1
      README.md
  6. 3 87
      auth/auth.go
  7. 19 19
      auth/host_session.go
  8. 29 4
      cli/cmd/user/create.go
  9. 6 5
      cli/cmd/user/flags.go
  10. 118 0
      cli/cmd/user/groups.go
  11. 7 3
      cli/cmd/user/list.go
  12. 121 0
      cli/cmd/user/roles.go
  13. 32 4
      cli/cmd/user/update.go
  14. 1 1
      cli/config/config.go
  15. 38 1
      cli/functions/user.go
  16. 1 1
      compose/docker-compose.netclient.yml
  17. 5 0
      config/config.go
  18. 19 6
      controllers/controller.go
  19. 79 88
      controllers/dns.go
  20. 0 475
      controllers/docs.go
  21. 49 58
      controllers/enrollmentkeys.go
  22. 194 142
      controllers/ext_client.go
  23. 7 11
      controllers/files.go
  24. 326 180
      controllers/hosts.go
  25. 6 11
      controllers/ipservice.go
  26. 8 12
      controllers/legacy.go
  27. 109 0
      controllers/middleware.go
  28. 138 102
      controllers/network.go
  29. 5 5
      controllers/network_test.go
  30. 202 155
      controllers/node.go
  31. 19 36
      controllers/server.go
  32. 275 256
      controllers/user.go
  33. 328 342
      controllers/user_test.go
  34. 6 0
      database/database.go
  35. 1 1
      docker/Dockerfile-go-builder
  36. 1 0
      docs/APIUsage.md
  37. 10 0
      docs/Authentication.md
  38. 1 0
      docs/Pricing.md
  39. 3 3
      functions/helpers_test.go
  40. 5 3
      go.mod
  41. 6 2
      go.sum
  42. 1 1
      k8s/client/netclient-daemonset.yaml
  43. 1 1
      k8s/client/netclient.yaml
  44. 1 1
      k8s/server/netmaker-ui.yaml
  45. 87 14
      logic/auth.go
  46. 58 15
      logic/enrollmentkey.go
  47. 1 1
      logic/enrollmentkey_test.go
  48. 19 0
      logic/extpeers.go
  49. 31 4
      logic/gateway.go
  50. 13 0
      logic/hosts.go
  51. 47 6
      logic/jwts.go
  52. 157 6
      logic/networks.go
  53. 20 0
      logic/nodes.go
  54. 1 1
      logic/peers.go
  55. 26 2
      logic/security.go
  56. 14 2
      logic/telemetry.go
  57. 96 0
      logic/user_mgmt.go
  58. 76 4
      logic/users.go
  59. 38 6
      main.go
  60. 109 3
      migrate/migrate.go
  61. 94 91
      models/node.go
  62. 5 33
      models/structs.go
  63. 199 0
      models/user_mgmt.go
  64. 7 2
      mq/emqx_on_prem.go
  65. 2 2
      mq/handlers.go
  66. 3 7
      mq/mq.go
  67. 40 18
      mq/publishers.go
  68. 23 0
      mq/util.go
  69. 12 0
      pro/auth/auth.go
  70. 48 16
      pro/auth/azure-ad.go
  71. 98 44
      pro/auth/error.go
  72. 45 15
      pro/auth/github.go
  73. 44 16
      pro/auth/google.go
  74. 1 2
      pro/auth/headless_callback.go
  75. 41 14
      pro/auth/oidc.go
  76. 2 1
      pro/auth/register_callback.go
  77. 108 60
      pro/controllers/failover.go
  78. 71 41
      pro/controllers/inet_gws.go
  79. 59 30
      pro/controllers/relay.go
  80. 744 153
      pro/controllers/users.go
  81. 54 0
      pro/email/email.go
  82. 27 0
      pro/email/invite.go
  83. 43 0
      pro/email/smtp.go
  84. 567 0
      pro/email/utils.go
  85. 15 1
      pro/initialize.go
  86. 75 2
      pro/logic/metrics.go
  87. 188 0
      pro/logic/security.go
  88. 1036 0
      pro/logic/user_mgmt.go
  89. 1 1
      pro/remote_access_client.go
  90. 6 7
      release.md
  91. 12 0
      scripts/netmaker.default.env
  92. 6 4
      scripts/nm-quick.sh
  93. 81 1
      servercfg/serverconf.go
  94. 3105 0
      swagger.yaml
  95. 0 3006
      swagger.yml

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

@@ -31,6 +31,7 @@ body:
       label: Version
       description: What version are you running?
       options:
+        - v0.25.0
         - v0.24.3
         - v0.24.2
         - v0.24.1

+ 50 - 0
.github/workflows/docs.yml

@@ -0,0 +1,50 @@
+name: Generate Documentation
+
+on: 
+  workflow_dispatch:
+    inputs:
+      branch:
+        description: 'Branch to run the workflow against'
+        required: true
+        default: 'master'
+
+jobs:
+  generate-docs:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          repository: gravitl/netmaker
+          ref: ${{ github.event.inputs.branch || 'master' }}
+
+      - name: Setup Go
+        uses: actions/setup-go@v5
+        with:
+          go-version-file: go.mod
+
+      - name: Install Swag
+        run: go install github.com/swaggo/swag/cmd/swag@latest
+
+      - name: Generating Docs
+        run: |
+          export PATH=$PATH:$(go env GOPATH)/bin
+          swag i --md docs/ --parseDependency  --parseInternal --outputTypes yaml  --parseDepth 1 --output .
+
+      - name: Get current timestamp
+        id: timestamp
+        run: echo "timestamp=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
+
+      - name: Create Pull Request
+        uses: peter-evans/create-pull-request@v6
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          commit-message: "Update documentation ${{ steps.timestamp.outputs.timestamp }}"
+          title: "Update Swagger documentation ${{ steps.timestamp.outputs.timestamp }}"
+          body: |
+            This PR updates the swagger.yml file with the latest documentation changes.
+            
+            Updated on: ${{ steps.timestamp.outputs.timestamp }}
+          branch: update-swagger-docs-${{ github.event.inputs.branch }}
+          base: ${{ github.event.inputs.branch }}
+          delete-branch: true

+ 1 - 1
Dockerfile

@@ -6,7 +6,7 @@ COPY . .
 
 RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -w " -tags ${tags} .
 # RUN go build -tags=ee . -o netmaker main.go
-FROM alpine:3.20.0
+FROM alpine:3.20.2
 
 # add a c lib
 # set the working directory

+ 1 - 1
Dockerfile-quick

@@ -1,5 +1,5 @@
 #first stage - builder
-FROM alpine:3.20.0
+FROM alpine:3.20.2
 ARG version 
 WORKDIR /app
 COPY ./netmaker /root/netmaker

+ 1 - 1
README.md

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

+ 3 - 87
auth/auth.go

@@ -1,15 +1,8 @@
 package auth
 
 import (
-	"encoding/base64"
-	"encoding/json"
-	"fmt"
-
-	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
-	"golang.org/x/crypto/bcrypt"
-	"golang.org/x/exp/slog"
 	"golang.org/x/oauth2"
 )
 
@@ -22,88 +15,11 @@ var (
 	auth_provider *oauth2.Config
 )
 
-// IsOauthUser - returns
-func IsOauthUser(user *models.User) error {
-	var currentValue, err = FetchPassValue("")
-	if err != nil {
-		return err
-	}
-	var bCryptErr = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(currentValue))
-	return bCryptErr
-}
-
-func FetchPassValue(newValue string) (string, error) {
-
-	type valueHolder struct {
-		Value string `json:"value" bson:"value"`
-	}
-	newValueHolder := valueHolder{}
-	var currentValue, err = logic.FetchAuthSecret()
-	if err != nil {
-		return "", err
-	}
-	var unmarshErr = json.Unmarshal([]byte(currentValue), &newValueHolder)
-	if unmarshErr != nil {
-		return "", unmarshErr
-	}
-
-	var b64CurrentValue, b64Err = base64.StdEncoding.DecodeString(newValueHolder.Value)
-	if b64Err != nil {
-		logger.Log(0, "could not decode pass")
-		return "", nil
-	}
-	return string(b64CurrentValue), nil
-}
-
-// == private ==
-
-func addUser(email string) error {
-	var hasSuperAdmin, err = logic.HasSuperAdmin()
-	if err != nil {
-		slog.Error("error checking for existence of admin user during OAuth login for", "email", email, "error", err)
-		return err
-	} // generate random password to adapt to current model
-	var newPass, fetchErr = FetchPassValue("")
-	if fetchErr != nil {
-		slog.Error("failed to get password", "error", fetchErr.Error())
-		return fetchErr
-	}
-	var newUser = models.User{
-		UserName: email,
-		Password: newPass,
-	}
-	if !hasSuperAdmin { // must be first attempt, create a superadmin
-		logger.Log(0, "creating superadmin")
-		if err = logic.CreateSuperAdmin(&newUser); err != nil {
-			slog.Error("error creating super admin from user", "email", email, "error", err)
-		} else {
-			slog.Info("superadmin created from user", "email", email)
-		}
-	} else { // otherwise add to db as admin..?
-		// TODO: add ability to add users with preemptive permissions
-		newUser.IsAdmin = false
-		if err = logic.CreateUser(&newUser); err != nil {
-			logger.Log(0, "error creating user,", email, "; user not added", "error", err.Error())
-		} else {
-			logger.Log(0, "user created from ", email)
-		}
-	}
-	return nil
-}
-
-func isUserIsAllowed(username, network string, shouldAddUser bool) (*models.User, error) {
+func isUserIsAllowed(username, network string) (*models.User, error) {
 
 	user, err := logic.GetUser(username)
-	if err != nil && shouldAddUser { // user must not exist, so try to make one
-		if err = addUser(username); err != nil {
-			logger.Log(0, "failed to add user", username, "during a node SSO network join on network", network)
-			// response := returnErrTemplate(user.UserName, "failed to add user", state, reqKeyIf)
-			// w.WriteHeader(http.StatusInternalServerError)
-			// w.Write(response)
-			return nil, fmt.Errorf("failed to add user to system")
-		}
-		logger.Log(0, "user", username, "was added during a node SSO network join on network", network)
-		user, _ = logic.GetUser(username)
+	if err != nil { // user must not exist, so try to make one
+		return &models.User{}, err
 	}
 
 	return user, nil

+ 19 - 19
auth/host_session.go

@@ -85,24 +85,24 @@ func SessionHandler(conn *websocket.Conn) {
 			return
 		}
 		req.Pass = req.Host.ID.String()
-		user, err := logic.GetUser(req.User)
-		if err != nil {
-			logger.Log(0, "failed to get user", req.User, "from database")
-			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
-			if err != nil {
-				logger.Log(0, "error during message writing:", err.Error())
-			}
-			return
-		}
-		if !user.IsAdmin && !user.IsSuperAdmin {
-			logger.Log(0, "user", req.User, "is neither an admin or superadmin. denying registeration")
-			conn.WriteMessage(messageType, []byte("cannot register with a non-admin or non-superadmin"))
-			err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
-			if err != nil {
-				logger.Log(0, "error during message writing:", err.Error())
-			}
-			return
-		}
+		// user, err := logic.GetUser(req.User)
+		// if err != nil {
+		// 	logger.Log(0, "failed to get user", req.User, "from database")
+		// 	err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
+		// 	if err != nil {
+		// 		logger.Log(0, "error during message writing:", err.Error())
+		// 	}
+		// 	return
+		// }
+		// if !user.IsAdmin && !user.IsSuperAdmin {
+		// 	logger.Log(0, "user", req.User, "is neither an admin or superadmin. denying registeration")
+		// 	conn.WriteMessage(messageType, []byte("cannot register with a non-admin or non-superadmin"))
+		// 	err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
+		// 	if err != nil {
+		// 		logger.Log(0, "error during message writing:", err.Error())
+		// 	}
+		// 	return
+		// }
 
 		if err = netcache.Set(stateStr, req); err != nil { // give the user's host access in the DB
 			logger.Log(0, "machine failed to complete join on network,", registerMessage.Network, "-", err.Error())
@@ -197,7 +197,7 @@ func SessionHandler(conn *websocket.Conn) {
 		for _, newNet := range currentNetworks {
 			if !logic.StringSliceContains(hostNets, newNet) {
 				if len(result.User) > 0 {
-					_, err := isUserIsAllowed(result.User, newNet, false)
+					_, err := isUserIsAllowed(result.User, newNet)
 					if err != nil {
 						logger.Log(0, "unauthorized user", result.User, "attempted to register to network", newNet)
 						handleHostRegErr(conn, err)

+ 29 - 4
cli/cmd/user/create.go

@@ -1,6 +1,8 @@
 package user
 
 import (
+	"strings"
+
 	"github.com/gravitl/netmaker/cli/functions"
 	"github.com/gravitl/netmaker/models"
 	"github.com/spf13/cobra"
@@ -12,18 +14,41 @@ var userCreateCmd = &cobra.Command{
 	Short: "Create a new user",
 	Long:  `Create a new user`,
 	Run: func(cmd *cobra.Command, args []string) {
-		user := &models.User{UserName: username, Password: password, IsAdmin: admin}
+		user := &models.User{UserName: username, Password: password, PlatformRoleID: models.UserRoleID(platformID)}
+		if len(networkRoles) > 0 {
+			netRolesMap := make(map[models.NetworkID]map[models.UserRoleID]struct{})
+			for netID, netRoles := range networkRoles {
+				roleMap := make(map[models.UserRoleID]struct{})
+				for _, roleID := range strings.Split(netRoles, " ") {
+					roleMap[models.UserRoleID(roleID)] = struct{}{}
+				}
+				netRolesMap[models.NetworkID(netID)] = roleMap
+			}
+			user.NetworkRoles = netRolesMap
+		}
+		if len(groups) > 0 {
+			grMap := make(map[models.UserGroupID]struct{})
+			for _, groupID := range groups {
+				grMap[models.UserGroupID(groupID)] = struct{}{}
+			}
+			user.UserGroups = grMap
+		}
+
 		functions.PrettyPrint(functions.CreateUser(user))
 	},
 }
 
 func init() {
+
 	userCreateCmd.Flags().StringVar(&username, "name", "", "Name of the user")
 	userCreateCmd.Flags().StringVar(&password, "password", "", "Password of the user")
+	userCreateCmd.Flags().StringVarP(&platformID, "platform-role", "r", models.ServiceUser.String(),
+		"Platform Role of the user; run `nmctl roles list` to see available user roles")
 	userCreateCmd.MarkFlagRequired("name")
 	userCreateCmd.MarkFlagRequired("password")
-	userCreateCmd.Flags().BoolVar(&admin, "admin", false, "Make the user an admin ?")
-	userCreateCmd.Flags().StringVar(&networks, "networks", "", "List of networks the user will access to (comma separated)")
-	userCreateCmd.Flags().StringVar(&groups, "groups", "", "List of user groups the user will be part of (comma separated)")
+	userCreateCmd.PersistentFlags().StringToStringVarP(&networkRoles, "network-roles", "n", nil,
+		"Mapping of networkID and list of roles user will be part of (comma separated)")
+	userCreateCmd.Flags().BoolVar(&admin, "admin", false, "Make the user an admin ? (deprecated v0.25.0 onwards)")
+	userCreateCmd.Flags().StringArrayVarP(&groups, "groups", "g", nil, "List of user groups the user will be part of (comma separated)")
 	rootCmd.AddCommand(userCreateCmd)
 }

+ 6 - 5
cli/cmd/user/flags.go

@@ -1,9 +1,10 @@
 package user
 
 var (
-	username string
-	password string
-	admin    bool
-	networks string
-	groups   string
+	username     string
+	password     string
+	platformID   string
+	admin        bool
+	networkRoles map[string]string
+	groups       []string
 )

+ 118 - 0
cli/cmd/user/groups.go

@@ -0,0 +1,118 @@
+package user
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/gravitl/netmaker/cli/cmd/commons"
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/guumaster/tablewriter"
+	"github.com/spf13/cobra"
+)
+
+var userGroupCmd = &cobra.Command{
+	Use:   "group",
+	Args:  cobra.NoArgs,
+	Short: "Manage User Groups",
+	Long:  `Manage User Groups`,
+}
+
+var userGroupListCmd = &cobra.Command{
+	Use:   "list",
+	Args:  cobra.NoArgs,
+	Short: "List all user groups",
+	Long:  `List all user groups`,
+	Run: func(cmd *cobra.Command, args []string) {
+		data := functions.ListUserGrps()
+		switch commons.OutputFormat {
+		case commons.JsonOutput:
+			functions.PrettyPrint(data)
+		default:
+			table := tablewriter.NewWriter(os.Stdout)
+			h := []string{"ID", "MetaData", "Network Roles"}
+			table.SetHeader(h)
+			for _, d := range data {
+
+				roleInfoStr := ""
+				for netID, netRoleMap := range d.NetworkRoles {
+					roleList := []string{}
+					for roleID := range netRoleMap {
+						roleList = append(roleList, roleID.String())
+					}
+					roleInfoStr += fmt.Sprintf("[%s]: %s", netID, strings.Join(roleList, ","))
+				}
+				e := []string{d.ID.String(), d.MetaData, roleInfoStr}
+				table.Append(e)
+			}
+			table.Render()
+		}
+	},
+}
+
+var userGroupCreateCmd = &cobra.Command{
+	Use:   "create",
+	Args:  cobra.NoArgs,
+	Short: "create user group",
+	Long:  `create user group`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println("CLI doesn't support creation of groups currently. Visit the dashboard to create one or refer to our api documentation https://docs.v2.netmaker.io/reference")
+	},
+}
+
+var userGroupDeleteCmd = &cobra.Command{
+	Use:   "delete [groupID]",
+	Args:  cobra.ExactArgs(1),
+	Short: "delete user group",
+	Long:  `delete user group`,
+	Run: func(cmd *cobra.Command, args []string) {
+		resp := functions.DeleteUserGrp(args[0])
+		if resp != nil {
+			fmt.Println(resp.Message)
+		}
+	},
+}
+
+var userGroupGetCmd = &cobra.Command{
+	Use:   "get [groupID]",
+	Args:  cobra.ExactArgs(1),
+	Short: "get user group",
+	Long:  `get user group`,
+	Run: func(cmd *cobra.Command, args []string) {
+		data := functions.GetUserGrp(args[0])
+		switch commons.OutputFormat {
+		case commons.JsonOutput:
+			functions.PrettyPrint(data)
+		default:
+			table := tablewriter.NewWriter(os.Stdout)
+			h := []string{"ID", "MetaData", "Network Roles"}
+			table.SetHeader(h)
+			roleInfoStr := ""
+			for netID, netRoleMap := range data.NetworkRoles {
+				roleList := []string{}
+				for roleID := range netRoleMap {
+					roleList = append(roleList, roleID.String())
+				}
+				roleInfoStr += fmt.Sprintf("[%s]: %s", netID, strings.Join(roleList, ","))
+			}
+			e := []string{data.ID.String(), data.MetaData, roleInfoStr}
+			table.Append(e)
+			table.Render()
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(userGroupCmd)
+	// list roles cmd
+	userGroupCmd.AddCommand(userGroupListCmd)
+
+	// create roles cmd
+	userGroupCmd.AddCommand(userGroupCreateCmd)
+
+	// delete role cmd
+	userGroupCmd.AddCommand(userGroupDeleteCmd)
+
+	// Get Role
+	userGroupCmd.AddCommand(userGroupGetCmd)
+}

+ 7 - 3
cli/cmd/user/list.go

@@ -2,7 +2,7 @@ package user
 
 import (
 	"os"
-	"strconv"
+	"strings"
 
 	"github.com/gravitl/netmaker/cli/cmd/commons"
 	"github.com/gravitl/netmaker/cli/functions"
@@ -22,9 +22,13 @@ var userListCmd = &cobra.Command{
 			functions.PrettyPrint(data)
 		default:
 			table := tablewriter.NewWriter(os.Stdout)
-			table.SetHeader([]string{"Name", "SuperAdmin", "Admin"})
+			table.SetHeader([]string{"Name", "Platform Role", "Groups"})
 			for _, d := range *data {
-				table.Append([]string{d.UserName, strconv.FormatBool(d.IsSuperAdmin), strconv.FormatBool(d.IsAdmin)})
+				g := []string{}
+				for gID := range d.UserGroups {
+					g = append(g, gID.String())
+				}
+				table.Append([]string{d.UserName, d.PlatformRoleID.String(), strings.Join(g, ",")})
 			}
 			table.Render()
 		}

+ 121 - 0
cli/cmd/user/roles.go

@@ -0,0 +1,121 @@
+package user
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+
+	"github.com/gravitl/netmaker/cli/cmd/commons"
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/guumaster/tablewriter"
+	"github.com/spf13/cobra"
+)
+
+var userRoleCmd = &cobra.Command{
+	Use:   "role",
+	Args:  cobra.NoArgs,
+	Short: "Manage User Roles",
+	Long:  `Manage User Roles`,
+}
+
+// List Roles
+var (
+	platformRoles bool
+)
+var userRoleListCmd = &cobra.Command{
+	Use:   "list",
+	Args:  cobra.NoArgs,
+	Short: "List all user roles",
+	Long:  `List all user roles`,
+	Run: func(cmd *cobra.Command, args []string) {
+		data := functions.ListUserRoles()
+		switch commons.OutputFormat {
+		case commons.JsonOutput:
+			functions.PrettyPrint(data)
+		default:
+			table := tablewriter.NewWriter(os.Stdout)
+			h := []string{"ID", "Default", "Dashboard Access", "Full Access"}
+
+			if !platformRoles {
+				h = append(h, "Network")
+			}
+			table.SetHeader(h)
+			for _, d := range data {
+				e := []string{d.ID.String(), strconv.FormatBool(d.Default), strconv.FormatBool(d.DenyDashboardAccess), strconv.FormatBool(d.FullAccess)}
+				if !platformRoles {
+					e = append(e, d.NetworkID.String())
+				}
+				table.Append(e)
+			}
+			table.Render()
+		}
+	},
+}
+
+var userRoleCreateCmd = &cobra.Command{
+	Use:   "create",
+	Args:  cobra.NoArgs,
+	Short: "create user role",
+	Long:  `create user role`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println("CLI doesn't support creation of roles currently. Visit the dashboard to create one or refer to our api documentation https://docs.v2.netmaker.io/reference")
+	},
+}
+
+var userRoleDeleteCmd = &cobra.Command{
+	Use:   "delete [roleID]",
+	Args:  cobra.ExactArgs(1),
+	Short: "delete user role",
+	Long:  `delete user role`,
+	Run: func(cmd *cobra.Command, args []string) {
+		resp := functions.DeleteUserRole(args[0])
+		if resp != nil {
+			fmt.Println(resp.Message)
+		}
+	},
+}
+
+var userRoleGetCmd = &cobra.Command{
+	Use:   "get [roleID]",
+	Args:  cobra.ExactArgs(1),
+	Short: "get user role",
+	Long:  `get user role`,
+	Run: func(cmd *cobra.Command, args []string) {
+		d := functions.GetUserRole(args[0])
+		switch commons.OutputFormat {
+		case commons.JsonOutput:
+			functions.PrettyPrint(d)
+		default:
+			table := tablewriter.NewWriter(os.Stdout)
+			h := []string{"ID", "Default Role", "Dashboard Access", "Full Access"}
+
+			if d.NetworkID != "" {
+				h = append(h, "Network")
+			}
+			table.SetHeader(h)
+			e := []string{d.ID.String(), strconv.FormatBool(d.Default), strconv.FormatBool(!d.DenyDashboardAccess), strconv.FormatBool(d.FullAccess)}
+			if !platformRoles {
+				e = append(e, d.NetworkID.String())
+			}
+			table.Append(e)
+			table.Render()
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(userRoleCmd)
+	// list roles cmd
+	userRoleListCmd.Flags().BoolVar(&platformRoles, "platform-roles", true,
+		"set to false to list network roles. By default it will only list platform roles")
+	userRoleCmd.AddCommand(userRoleListCmd)
+
+	// create roles cmd
+	userRoleCmd.AddCommand(userRoleCreateCmd)
+
+	// delete role cmd
+	userRoleCmd.AddCommand(userRoleDeleteCmd)
+
+	// Get Role
+	userRoleCmd.AddCommand(userRoleGetCmd)
+}

+ 32 - 4
cli/cmd/user/update.go

@@ -1,6 +1,8 @@
 package user
 
 import (
+	"strings"
+
 	"github.com/gravitl/netmaker/cli/functions"
 	"github.com/gravitl/netmaker/models"
 	"github.com/spf13/cobra"
@@ -12,14 +14,40 @@ var userUpdateCmd = &cobra.Command{
 	Short: "Update a user",
 	Long:  `Update a user`,
 	Run: func(cmd *cobra.Command, args []string) {
-		user := &models.User{UserName: args[0], IsAdmin: admin}
+		user := &models.User{UserName: args[0]}
+		if platformID != "" {
+			user.PlatformRoleID = models.UserRoleID(platformID)
+		}
+		if len(networkRoles) > 0 {
+			netRolesMap := make(map[models.NetworkID]map[models.UserRoleID]struct{})
+			for netID, netRoles := range networkRoles {
+				roleMap := make(map[models.UserRoleID]struct{})
+				for _, roleID := range strings.Split(netRoles, ",") {
+					roleMap[models.UserRoleID(roleID)] = struct{}{}
+				}
+				netRolesMap[models.NetworkID(netID)] = roleMap
+			}
+			user.NetworkRoles = netRolesMap
+		}
+		if len(groups) > 0 {
+			grMap := make(map[models.UserGroupID]struct{})
+			for _, groupID := range groups {
+				grMap[models.UserGroupID(groupID)] = struct{}{}
+			}
+			user.UserGroups = grMap
+		}
 		functions.PrettyPrint(functions.UpdateUser(user))
 	},
 }
 
 func init() {
-	userUpdateCmd.Flags().BoolVar(&admin, "admin", false, "Make the user an admin ?")
-	userUpdateCmd.Flags().StringVar(&networks, "networks", "", "List of networks the user will access to (comma separated)")
-	userUpdateCmd.Flags().StringVar(&groups, "groups", "", "List of user groups the user will be part of (comma separated)")
+
+	userUpdateCmd.Flags().StringVar(&password, "password", "", "Password of the user")
+	userUpdateCmd.Flags().StringVarP(&platformID, "platform-role", "r", "",
+		"Platform Role of the user; run `nmctl roles list` to see available user roles")
+	userUpdateCmd.PersistentFlags().StringToStringVarP(&networkRoles, "network-roles", "n", nil,
+		"Mapping of networkID and list of roles user will be part of (comma separated)")
+	userUpdateCmd.Flags().BoolVar(&admin, "admin", false, "Make the user an admin ? (deprecated v0.25.0 onwards)")
+	userUpdateCmd.Flags().StringArrayVarP(&groups, "groups", "g", nil, "List of user groups the user will be part of (comma separated)")
 	rootCmd.AddCommand(userUpdateCmd)
 }

+ 1 - 1
cli/config/config.go

@@ -86,7 +86,7 @@ func GetCurrentContext() (name string, ctx Context) {
 			return
 		}
 	}
-	log.Fatalf("No current context set, do so via `netmaker context use <name>`")
+	log.Fatalf("No current context set, do so via `nmctl context use <name>`")
 	return
 }
 

+ 38 - 1
cli/functions/user.go

@@ -1,6 +1,8 @@
 package functions
 
 import (
+	"encoding/json"
+	"fmt"
 	"net/http"
 
 	"github.com/gravitl/netmaker/models"
@@ -18,7 +20,7 @@ func CreateUser(payload *models.User) *models.User {
 
 // UpdateUser - update a user
 func UpdateUser(payload *models.User) *models.User {
-	return request[models.User](http.MethodPut, "/api/users/networks/"+payload.UserName, payload)
+	return request[models.User](http.MethodPut, "/api/users/"+payload.UserName, payload)
 }
 
 // DeleteUser - delete a user
@@ -35,3 +37,38 @@ func GetUser(username string) *models.User {
 func ListUsers() *[]models.ReturnUser {
 	return request[[]models.ReturnUser](http.MethodGet, "/api/users", nil)
 }
+
+func ListUserRoles() (roles []models.UserRolePermissionTemplate) {
+	resp := request[models.SuccessResponse](http.MethodGet, "/api/v1/users/roles", nil)
+	d, _ := json.Marshal(resp.Response)
+	json.Unmarshal(d, &roles)
+	return
+}
+
+func DeleteUserRole(roleID string) *models.SuccessResponse {
+	return request[models.SuccessResponse](http.MethodDelete, fmt.Sprintf("/api/v1/users/role?role_id=%s", roleID), nil)
+}
+func GetUserRole(roleID string) (role models.UserRolePermissionTemplate) {
+	resp := request[models.SuccessResponse](http.MethodGet, fmt.Sprintf("/api/v1/users/role?role_id=%s", roleID), nil)
+	d, _ := json.Marshal(resp.Response)
+	json.Unmarshal(d, &role)
+	return
+}
+
+func ListUserGrps() (groups []models.UserGroup) {
+	resp := request[models.SuccessResponse](http.MethodGet, "/api/v1/users/groups", nil)
+	d, _ := json.Marshal(resp.Response)
+	json.Unmarshal(d, &groups)
+	return
+}
+
+func DeleteUserGrp(grpID string) *models.SuccessResponse {
+	return request[models.SuccessResponse](http.MethodDelete, fmt.Sprintf("/api/v1/users/group?group_id=%s", grpID), nil)
+}
+
+func GetUserGrp(grpID string) (group models.UserGroup) {
+	resp := request[models.SuccessResponse](http.MethodGet, fmt.Sprintf("/api/v1/users/group?group_id=%s", grpID), nil)
+	d, _ := json.Marshal(resp.Response)
+	json.Unmarshal(d, &group)
+	return
+}

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

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

+ 5 - 0
config/config.go

@@ -94,6 +94,11 @@ type ServerConfig struct {
 	CacheEnabled               string        `yaml:"caching_enabled"`
 	EndpointDetection          bool          `json:"endpoint_detection"`
 	AllowedEmailDomains        string        `yaml:"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        `yaml:"metric_interval"`
 }
 

+ 19 - 6
controllers/controller.go

@@ -17,7 +17,9 @@ import (
 )
 
 // HttpMiddlewares - middleware functions for REST interactions
-var HttpMiddlewares []mux.MiddlewareFunc
+var HttpMiddlewares = []mux.MiddlewareFunc{
+	userMiddleWare,
+}
 
 // HttpHandlers - handler functions for REST interactions
 var HttpHandlers = []interface{}{
@@ -35,17 +37,25 @@ var HttpHandlers = []interface{}{
 	legacyHandlers,
 }
 
-// HandleRESTRequests - handles the rest requests
 func HandleRESTRequests(wg *sync.WaitGroup, ctx context.Context) {
 	defer wg.Done()
 
 	r := mux.NewRouter()
-
 	// Currently allowed dev origin is all. Should change in prod
 	// should consider analyzing the allowed methods further
-	headersOk := handlers.AllowedHeaders([]string{"Access-Control-Allow-Origin", "X-Requested-With", "Content-Type", "authorization", "From-Ui"})
+	headersOk := handlers.AllowedHeaders(
+		[]string{
+			"Access-Control-Allow-Origin",
+			"X-Requested-With",
+			"Content-Type",
+			"authorization",
+			"From-Ui",
+		},
+	)
 	originsOk := handlers.AllowedOrigins(strings.Split(servercfg.GetAllowedOrigin(), ","))
-	methodsOk := handlers.AllowedMethods([]string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete})
+	methodsOk := handlers.AllowedMethods(
+		[]string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete},
+	)
 
 	for _, middleware := range HttpMiddlewares {
 		r.Use(middleware)
@@ -57,7 +67,10 @@ func HandleRESTRequests(wg *sync.WaitGroup, ctx context.Context) {
 
 	port := servercfg.GetAPIPort()
 
-	srv := &http.Server{Addr: ":" + port, Handler: handlers.CORS(originsOk, headersOk, methodsOk)(r)}
+	srv := &http.Server{
+		Addr:    ":" + port,
+		Handler: handlers.CORS(originsOk, headersOk, methodsOk)(r),
+	}
 	go func() {
 		err := srv.ListenAndServe()
 		if err != nil {

+ 79 - 88
controllers/dns.go

@@ -16,25 +16,29 @@ import (
 
 func dnsHandlers(r *mux.Router) {
 
-	r.HandleFunc("/api/dns", logic.SecurityCheck(true, http.HandlerFunc(getAllDNS))).Methods(http.MethodGet)
-	r.HandleFunc("/api/dns/adm/{network}/nodes", logic.SecurityCheck(true, http.HandlerFunc(getNodeDNS))).Methods(http.MethodGet)
-	r.HandleFunc("/api/dns/adm/{network}/custom", logic.SecurityCheck(true, http.HandlerFunc(getCustomDNS))).Methods(http.MethodGet)
-	r.HandleFunc("/api/dns/adm/{network}", logic.SecurityCheck(true, http.HandlerFunc(getDNS))).Methods(http.MethodGet)
-	r.HandleFunc("/api/dns/{network}", logic.SecurityCheck(true, http.HandlerFunc(createDNS))).Methods(http.MethodPost)
-	r.HandleFunc("/api/dns/adm/pushdns", logic.SecurityCheck(true, http.HandlerFunc(pushDNS))).Methods(http.MethodPost)
-	r.HandleFunc("/api/dns/{network}/{domain}", logic.SecurityCheck(true, http.HandlerFunc(deleteDNS))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/dns", logic.SecurityCheck(true, http.HandlerFunc(getAllDNS))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/dns/adm/{network}/nodes", logic.SecurityCheck(true, http.HandlerFunc(getNodeDNS))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/dns/adm/{network}/custom", logic.SecurityCheck(true, http.HandlerFunc(getCustomDNS))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/dns/adm/{network}", logic.SecurityCheck(true, http.HandlerFunc(getDNS))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/dns/{network}", logic.SecurityCheck(true, http.HandlerFunc(createDNS))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/dns/adm/pushdns", logic.SecurityCheck(true, http.HandlerFunc(pushDNS))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/dns/{network}/{domain}", logic.SecurityCheck(true, http.HandlerFunc(deleteDNS))).
+		Methods(http.MethodDelete)
 }
 
-// swagger:route GET /api/dns/adm/{network}/nodes dns getNodeDNS
-//
-// Gets node DNS entries associated with a network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//			Responses:
-//			200: dnsResponse
+// @Summary     Gets node DNS entries associated with a network
+// @Router      /api/dns/{network} [get]
+// @Tags        DNS
+// @Accept      json
+// @Param       network path string true "Network identifier"
+// @Success     200 {array} models.DNSEntry
+// @Failure     500 {object} models.ErrorResponse
 func getNodeDNS(w http.ResponseWriter, r *http.Request) {
 
 	w.Header().Set("Content-Type", "application/json")
@@ -53,17 +57,12 @@ func getNodeDNS(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(dns)
 }
 
-// swagger:route GET /api/dns dns getAllDNS
-//
-// Gets all DNS entries.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//	  		200: dnsResponse
+// @Summary     Get all DNS entries
+// @Router      /api/dns [get]
+// @Tags        DNS
+// @Accept      json
+// @Success     200 {array} models.DNSEntry
+// @Failure     500 {object} models.ErrorResponse
 func getAllDNS(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	dns, err := logic.GetAllDNS()
@@ -77,17 +76,13 @@ func getAllDNS(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(dns)
 }
 
-// swagger:route GET /api/dns/adm/{network}/custom dns getCustomDNS
-//
-// Gets custom DNS entries associated with a network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//	  		200: dnsResponse
+// @Summary     Gets custom DNS entries associated with a network
+// @Router      /api/dns/adm/{network}/custom [get]
+// @Tags        DNS
+// @Accept      json
+// @Param       network path string true "Network identifier"
+// @Success     200 {array} models.DNSEntry
+// @Failure     500 {object} models.ErrorResponse
 func getCustomDNS(w http.ResponseWriter, r *http.Request) {
 
 	w.Header().Set("Content-Type", "application/json")
@@ -97,8 +92,15 @@ func getCustomDNS(w http.ResponseWriter, r *http.Request) {
 	network := params["network"]
 	dns, err := logic.GetCustomDNS(network)
 	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to get custom DNS entries for network [%s]: %v", network, err.Error()))
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			fmt.Sprintf(
+				"failed to get custom DNS entries for network [%s]: %v",
+				network,
+				err.Error(),
+			),
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
@@ -106,17 +108,13 @@ func getCustomDNS(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(dns)
 }
 
-// swagger:route GET /api/dns/adm/{network} dns getDNS
-//
-// Gets all DNS entries associated with the network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//	  		200: dnsResponse
+// @Summary     Get all DNS entries associated with the network
+// @Router      /api/dns/adm/{network} [get]
+// @Tags        DNS
+// @Accept      json
+// @Param       network path string true "Network identifier"
+// @Success     200 {array} models.DNSEntry
+// @Failure     500 {object} models.ErrorResponse
 func getDNS(w http.ResponseWriter, r *http.Request) {
 
 	w.Header().Set("Content-Type", "application/json")
@@ -135,17 +133,15 @@ func getDNS(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(dns)
 }
 
-// swagger:route POST /api/dns/{network} dns createDNS
-//
-// Create a DNS entry.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//	  		200: dnsResponse
+// @Summary     Create a new DNS entry
+// @Router      /api/dns/adm/{network} [post]
+// @Tags        DNS
+// @Accept      json
+// @Param       network path string true "Network identifier"
+// @Param       body body models.DNSEntry true "DNS entry details"
+// @Success     200 {object} models.DNSEntry
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func createDNS(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 
@@ -187,18 +183,14 @@ func createDNS(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(entry)
 }
 
-// swagger:route DELETE /api/dns/{network}/{domain} dns deleteDNS
-//
-// Delete a DNS entry.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: stringJSONResponse
-//				*: stringJSONResponse
+// @Summary     Delete a DNS entry
+// @Router      /api/dns/{network}/{domain} [delete]
+// @Tags        DNS
+// @Accept      json
+// @Param       network path string true "Network identifier"
+// @Param       domain path string true "Domain Name"
+// @Success     200 {array} models.DNSEntry
+// @Failure     500 {object} models.ErrorResponse
 func deleteDNS(w http.ResponseWriter, r *http.Request) {
 	// Set header
 	w.Header().Set("Content-Type", "application/json")
@@ -243,23 +235,22 @@ func GetDNSEntry(domain string, network string) (models.DNSEntry, error) {
 	return entry, err
 }
 
-// swagger:route POST /api/dns/adm/pushdns dns pushDNS
-//
-// Push DNS entries to nameserver.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//			200: dnsResponse
-//				*: dnsResponse
+// @Summary     Push DNS entries to nameserver
+// @Router      /api/dns/adm/pushdns [post]
+// @Tags        DNS
+// @Accept      json
+// @Success     200 {string} string "DNS Pushed to CoreDNS"
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func pushDNS(w http.ResponseWriter, r *http.Request) {
 	// Set header
 	w.Header().Set("Content-Type", "application/json")
 	if !servercfg.IsDNSMode() {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("DNS Mode is set to off"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("DNS Mode is set to off"), "badrequest"),
+		)
 		return
 	}
 	err := logic.SetDNS()

+ 0 - 475
controllers/docs.go

@@ -1,475 +0,0 @@
-// Package classification Netmaker
-//
-// # API Usage
-//
-// Most actions that can be performed via API can be performed via UI. We recommend managing your networks using the official netmaker-ui project. However, Netmaker can also be run without the UI, and all functions can be achieved via API calls. If your use case requires using Netmaker without the UI or you need to do some troubleshooting/advanced configuration, using the API directly may help.
-//
-// # Authentication
-//
-// API calls must be authenticated via a header of the format -H “Authorization: Bearer <YOUR_SECRET_KEY>” There are two methods to obtain YOUR_SECRET_KEY: 1. Using the masterkey. By default, this value is “secret key,” but you should change this on your instance and keep it secure. This value can be set via env var at startup or in a config file (config/environments/< env >.yaml). See the [Netmaker](https://docs.netmaker.org/index.html) documentation for more details. 2. Using a JWT received for a node. This can be retrieved by calling the /api/nodes/<network>/authenticate endpoint, as documented below.
-//
-//	Schemes: https
-//	BasePath: /
-//	Version: 0.24.3
-//	Host: api.demo.netmaker.io
-//
-//	Consumes:
-//	- application/json
-//
-//	Produces:
-//	- application/json
-//
-//	Security:
-//	- oauth
-//
-// swagger:meta
-package controller
-
-import (
-	"os"
-
-	"github.com/gravitl/netmaker/config"
-	"github.com/gravitl/netmaker/logic/acls"
-	"github.com/gravitl/netmaker/models"
-)
-
-var _ = useUnused() // "use" the function to prevent "unused function" errors
-
-// swagger:parameters getFile
-type filenameToGet struct {
-	// Filename
-	// in: path
-	// required: true
-	Filename string `json:"filename"`
-}
-
-// swagger:response hasAdmin
-type hasAdmin struct {
-	// in: body
-	Admin bool
-}
-
-// swagger:response apiHostSliceResponse
-type apiHostSliceResponse struct {
-	// in: body
-	Host []models.ApiHost
-}
-
-// swagger:response apiHostResponse
-type apiHostResponse struct {
-	// in: body
-	Host models.ApiHost
-}
-
-// swagger:parameters getNodeDNS getCustomDNS getDNS
-type dnsNetworkPathParam struct {
-	// Network
-	// in: path
-	Network string `json:"network"`
-}
-
-// swagger:parameters createDNS
-type dnsParams struct {
-	// Network
-	// in: path
-	Network string `json:"network"`
-	// DNS Entry
-	// in: body
-	Body []models.DNSEntry `json:"body"`
-}
-
-// Success
-// swagger:response dnsResponse
-type dnsResponse struct {
-	// in: body
-	Body []models.DNSEntry `json:"body"`
-}
-
-// swagger:parameters deleteDNS
-type dnsDeletePathParams struct {
-	// Network
-	// in: path
-	Network string `json:"network"`
-
-	// Domain
-	// in: path
-	Domain string `json:"domain"`
-}
-
-// swagger:response stringJSONResponse
-type stringJSONResponse struct {
-	// Response
-	// in: body
-	Response string `json:"response"`
-}
-
-//swagger:response EnrollmentKey
-type EnrollmentKey struct {
-	// in: body
-	EnrollmentKey models.EnrollmentKey
-}
-
-//swagger:response EnrollmentKeys
-type EnrollmentKeys struct {
-	// in: body
-	EnrollmentKeys []models.EnrollmentKey
-}
-
-// swagger:parameters getAllExtClients
-type getAllClientsRequest struct {
-	// Networks
-	// in:body
-	Networks []string `json:"networks"`
-}
-
-// swagger:response extClientSliceResponse
-type extClientSliceResponse struct {
-	// ExtClients
-	// in: body
-	ExtClients []models.ExtClient `json:"ext_clients"`
-}
-
-// swagger:response extClientResponse
-type extClientResponse struct {
-	// ExtClient
-	// in: body
-	ExtClient models.ExtClient `json:"ext_client"`
-}
-
-// swagger:response fileResponse
-type fileResponse struct {
-	// in: body
-	File os.File
-}
-
-// swagger:response successResponse
-type successResponse struct {
-	// Success Response
-	// in: body
-	SuccessResponse models.SuccessResponse `json:"success_response"`
-}
-
-// swagger:parameters getExtClientConf
-type extClientConfParams struct {
-	// Client ID
-	// in: path
-	ClientID string `json:"clientid"`
-	// Network
-	// in: path
-	Network string `json:"network"`
-	// Type
-	// in: path
-	Type string `json:"type"`
-}
-
-// swagger:parameters getExtClient getExtClientConf updateExtClient deleteExtClient
-type extClientPathParams struct {
-	// Client ID
-	// in: path
-	ClientID string `json:"clientid"`
-	// Network
-	// in: path
-	Network string `json:"network"`
-}
-
-// swagger:parameters updateExtClient
-type extClientBodyParam struct {
-	// ExtClient
-	// in: body
-	ExtClient models.ExtClient `json:"ext_client"`
-}
-
-// swagger:parameters getNetworkExtClients
-type extClientNetworkPathParam struct {
-	// Network
-	// in: path
-	Network string `json:"network"`
-}
-
-// swagger:parameters createExtClient
-type createExtClientPathParams struct {
-	// Network
-	// in: path
-	Network string `json:"network"`
-
-	// Node ID
-	// in: path
-	NodeID string `json:"nodeid"`
-
-	// Custom ExtClient
-	// in: body
-	CustomExtClient models.CustomExtClient `json:"custom_ext_client"`
-}
-
-// swagger:parameters getNode updateNode deleteNode createRelay deleteRelay createEgressGateway deleteEgressGateway createIngressGateway deleteIngressGateway ingressGatewayUsers
-type networkNodePathParams struct {
-	// in: path
-	Network string `json:"network"`
-	// in: path
-	NodeID string `json:"nodeid"`
-}
-
-// swagger:response byteArrayResponse
-type byteArrayResponse struct {
-	// in: body
-	ByteArray []byte `json:"byte_array"`
-}
-
-// swagger:parameters getNetwork deleteNetwork updateNetwork getNetworkACL updateNetworkACL
-type NetworkParam struct {
-	// name: network name
-	// in:  path
-	Networkname string `json:"networkname"`
-}
-
-// swagger:response getNetworksSliceResponse
-type getNetworksSliceResponse struct {
-	// Networks
-	// in: body
-	Networks []models.Network `json:"networks"`
-}
-
-// swagger:response hostPull
-type hostPull struct {
-	// hostPull
-	// in: body
-	HostPull models.HostPull
-}
-
-// swagger:parameters createNetwork updateNetwork
-type networkBodyParam struct {
-	// Network
-	// in: body
-	Network models.Network `json:"network"`
-}
-
-// swagger:parameters updateNetworkNodeLimit keyUpdate createAccessKey getAccessKeys getNetworkNodes
-type networkPathParam struct {
-	// Network
-	// in: path
-	Network string `json:"network"`
-}
-
-// swagger:response networkBodyResponse
-type networkBodyResponse struct {
-	// Network
-	// in: body
-	Network models.Network `json:"network"`
-}
-
-// swagger:parameters updateNetworkACL
-type aclContainerBodyParam struct {
-	// ACL Container
-	// in: body
-	ACLContainer acls.ACLContainer `json:"acl_container"`
-}
-
-// swagger:response aclContainerResponse
-type aclContainerResponse struct {
-	// ACL Container
-	// in: body
-	ACLContainer acls.ACLContainer `json:"acl_container"`
-}
-
-// swagger:response nodeSliceResponse
-type nodeSliceResponse struct {
-	// Nodes
-	// in: body
-	Nodes []models.ApiNode `json:"nodes"`
-}
-
-// swagger:response nodeResponse
-type nodeResponse struct {
-	// Node
-	// in: body
-	Node models.LegacyNode `json:"node"`
-}
-
-// swagger:parameters updateNode deleteNode
-type nodeBodyParam struct {
-	// Node
-	// in: body
-	Node models.LegacyNode `json:"node"`
-}
-
-//swagger:response okResponse
-type okRespone struct{}
-
-// swagger:response RegisterResponse
-type RegisterResponse struct {
-	// in: body
-	RegisterResponse models.RegisterResponse
-}
-
-// swagger:parameters createRelay
-type relayRequestBodyParam struct {
-	// Relay Request
-	// in: body
-	RelayRequest models.RelayRequest `json:"relay_request"`
-}
-
-// swagger:parameters createEgressGateway
-type egressGatewayBodyParam struct {
-	// Egress Gateway Request
-	// in: body
-	EgressGatewayRequest models.EgressGatewayRequest `json:"egress_gateway_request"`
-}
-
-// swagger:parameters attachUserToRemoteAccessGateway removeUserFromRemoteAccessGW getUserRemoteAccessGws
-type RemoteAccessGatewayUser struct {
-	// in: path
-	Username string `json:"username"`
-}
-
-// swagger:parameters authenticate
-type authParamBodyParam struct {
-	// network
-	// in: path
-	Network string `json:"network"`
-	// AuthParams
-	// in: body
-	AuthParams models.AuthParams `json:"auth_params"`
-}
-
-// swagger:response signal
-type signal struct {
-	// in: body
-	Signal models.Signal
-}
-
-// swagger:parameters synchost deleteHost updateHost signalPeer updateKeys
-type HostID struct {
-	// HostID
-	// in: path
-	HostID string `json:"hostid"`
-}
-
-// swagger:parameters addHostToNetwork deleteHostFromNetwork
-type HostFromNetworkParams struct {
-	// hostid to add or delete from network
-	// in: path
-	HostID string `json:"hostid"`
-	// network
-	// in: path
-	Network string `json:"network"`
-}
-
-// swagger:parameters createEnrollmentKey
-type createEnrollmentKeyParams struct {
-	// APIEnrollmentKey
-	// in: body
-	Body models.APIEnrollmentKey `json:"body"`
-}
-
-// swagger:parameters updateEnrollmentKey
-type updateEnrollmentKeyParams struct {
-	// KeyID
-	// in: path
-	KeyID string `json:"keyid"`
-
-	// APIEnrollmentKey
-	// in: body
-	Body models.APIEnrollmentKey `json:"body"`
-}
-
-// swagger:parameters deleteEnrollmentKey
-type deleteEnrollmentKeyParam struct {
-	// in: path
-	KeyID string `json:"keyid"`
-}
-
-// swagger:parameters handleHostRegister
-type RegisterParams struct {
-	// in: path
-	Token string `json:"token"`
-	// in: body
-	Host models.Host `json:"host"`
-}
-
-// swagger:response serverConfigResponse
-type serverConfigResponse struct {
-	// Server Config
-	// in: body
-	// example
-	//{
-	//"mqusername": "xxxxxxx"
-	//}
-	ServerConfig config.ServerConfig `json:"server_config"`
-}
-
-// swagger:parameters createAdmin updateUser updateUserNetworks createUser
-type userBodyParam struct {
-	// User
-	// in: body
-	User models.User `json:"user"`
-}
-
-// swagger:response userBodyResponse
-type userBodyResponse struct {
-	// User
-	// in: body
-	User models.User `json:"user"`
-}
-
-// swagger:parameters authenticateUser
-type userAuthBodyParam struct {
-	// User Auth Params
-	// in: body
-	UserAuthParams models.UserAuthParams `json:"user_auth_params"`
-}
-
-// swagger:parameters updateUser updateUserNetworks updateUserAdm createUser deleteUser getUser
-type usernamePathParam struct {
-	// Username
-	// in: path
-	Username string `json:"username"`
-}
-
-// prevent issues with integration tests for types just used by Swagger docs.
-func useUnused() bool {
-	_ = dnsParams{}
-	_ = dnsResponse{}
-	_ = dnsDeletePathParams{}
-	_ = stringJSONResponse{}
-	_ = getAllClientsRequest{}
-	_ = extClientSliceResponse{}
-	_ = extClientResponse{}
-	_ = successResponse{}
-	_ = extClientPathParams{}
-	_ = extClientBodyParam{}
-	_ = extClientNetworkPathParam{}
-	_ = createExtClientPathParams{}
-	_ = networkNodePathParams{}
-	_ = byteArrayResponse{}
-	_ = getNetworksSliceResponse{}
-	_ = networkBodyParam{}
-	_ = networkPathParam{}
-	_ = networkBodyResponse{}
-	_ = aclContainerBodyParam{}
-	_ = aclContainerResponse{}
-	_ = nodeSliceResponse{}
-	_ = nodeResponse{}
-	_ = nodeBodyParam{}
-	_ = relayRequestBodyParam{}
-	_ = egressGatewayBodyParam{}
-	_ = authParamBodyParam{}
-	_ = serverConfigResponse{}
-	_ = userBodyParam{}
-	_ = userBodyResponse{}
-	_ = userAuthBodyParam{}
-	_ = usernamePathParam{}
-	_ = hasAdmin{}
-	_ = apiHostSliceResponse{}
-	_ = apiHostResponse{}
-	_ = fileResponse{}
-	_ = extClientConfParams{}
-	_ = hostPull{}
-	_ = okRespone{}
-	_ = signal{}
-	_ = filenameToGet{}
-	_ = dnsNetworkPathParam{}
-	_ = createEnrollmentKeyParams{}
-	_ = updateEnrollmentKeyParams{}
-	_ = deleteEnrollmentKeyParam{}
-	return false
-}

+ 49 - 58
controllers/enrollmentkeys.go

@@ -32,17 +32,12 @@ func enrollmentKeyHandlers(r *mux.Router) {
 		Methods(http.MethodPut)
 }
 
-// swagger:route GET /api/v1/enrollment-keys enrollmentKeys getEnrollmentKeys
-//
-// Lists all EnrollmentKeys for admins.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: EnrollmentKeys
+// @Summary     Lists all EnrollmentKeys for admins
+// @Router      /api/v1/enrollment-keys [get]
+// @Tags        EnrollmentKeys
+// @Security    oauth
+// @Success     200 {array} models.EnrollmentKey
+// @Failure     500 {object} models.ErrorResponse
 func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
 	keys, err := logic.GetAllEnrollmentKeys()
 	if err != nil {
@@ -54,12 +49,12 @@ func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
 	ret := []*models.EnrollmentKey{}
 	for _, key := range keys {
 		key := key
-		if err = logic.Tokenize(key, servercfg.GetAPIHost()); err != nil {
+		if err = logic.Tokenize(&key, servercfg.GetAPIHost()); err != nil {
 			logger.Log(0, r.Header.Get("user"), "failed to get token values for keys:", err.Error())
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 			return
 		}
-		ret = append(ret, key)
+		ret = append(ret, &key)
 	}
 	// return JSON/API formatted keys
 	logger.Log(2, r.Header.Get("user"), "fetched enrollment keys")
@@ -67,17 +62,13 @@ func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(ret)
 }
 
-// swagger:route DELETE /api/v1/enrollment-keys/{keyid} enrollmentKeys deleteEnrollmentKey
-//
-// Deletes an EnrollmentKey from Netmaker server.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: okResponse
+// @Summary     Deletes an EnrollmentKey from Netmaker server
+// @Router      /api/v1/enrollment-keys/{keyid} [delete]
+// @Tags        EnrollmentKeys
+// @Security    oauth
+// @Param       keyid path string true "Enrollment Key ID"
+// @Success     200
+// @Failure     500 {object} models.ErrorResponse
 func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	params := mux.Vars(r)
 	keyID := params["keyID"]
@@ -91,17 +82,14 @@ func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 }
 
-// swagger:route POST /api/v1/enrollment-keys enrollmentKeys createEnrollmentKey
-//
-// Creates an EnrollmentKey for hosts to use on Netmaker server.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: EnrollmentKey
+// @Summary     Creates an EnrollmentKey for hosts to register with server and join networks
+// @Router      /api/v1/enrollment-keys [post]
+// @Tags        EnrollmentKeys
+// @Security    oauth
+// @Param       body body models.APIEnrollmentKey true "Enrollment Key parameters"
+// @Success     200 {object} models.EnrollmentKey
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	var enrollmentKeyBody models.APIEnrollmentKey
 
@@ -121,7 +109,14 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 		logger.Log(0, r.Header.Get("user"), "error validating request body: ",
 			err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("validation error: name length must be between 3 and 32: %w", err), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				fmt.Errorf("validation error: name length must be between 3 and 32: %w", err),
+				"badrequest",
+			),
+		)
 		return
 	}
 
@@ -180,17 +175,15 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(newEnrollmentKey)
 }
 
-// swagger:route PUT /api/v1/enrollment-keys/{keyid} enrollmentKeys updateEnrollmentKey
-//
-// Updates an EnrollmentKey for hosts to use on Netmaker server. Updates only the relay to use.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: EnrollmentKey
+// @Summary     Updates an EnrollmentKey. Updates are only limited to the relay to use
+// @Router      /api/v1/enrollment-keys/{keyid} [put]
+// @Tags        EnrollmentKeys
+// @Security    oauth
+// @Param       keyid path string true "Enrollment Key ID"
+// @Param       body body models.APIEnrollmentKey true "Enrollment Key parameters"
+// @Success     200 {object} models.EnrollmentKey
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	var enrollmentKeyBody models.APIEnrollmentKey
 	params := mux.Vars(r)
@@ -231,17 +224,15 @@ func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(newEnrollmentKey)
 }
 
-// swagger:route POST /api/v1/enrollment-keys/{token} enrollmentKeys handleHostRegister
-//
-// Handles a Netclient registration with server and add nodes accordingly.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: RegisterResponse
+// @Summary     Handles a Netclient registration with server and add nodes accordingly
+// @Router      /api/v1/host/register/{token} [post]
+// @Tags        EnrollmentKeys
+// @Security    oauth
+// @Param       token path string true "Enrollment Key Token"
+// @Param       body body models.Host true "Host registration parameters"
+// @Success     200 {object} models.RegisterResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func handleHostRegister(w http.ResponseWriter, r *http.Request) {
 	params := mux.Vars(r)
 	token := params["token"]

+ 194 - 142
controllers/ext_client.go

@@ -28,13 +28,20 @@ import (
 
 func extClientHandlers(r *mux.Router) {
 
-	r.HandleFunc("/api/extclients", logic.SecurityCheck(true, http.HandlerFunc(getAllExtClients))).Methods(http.MethodGet)
-	r.HandleFunc("/api/extclients/{network}", logic.SecurityCheck(true, http.HandlerFunc(getNetworkExtClients))).Methods(http.MethodGet)
-	r.HandleFunc("/api/extclients/{network}/{clientid}", logic.SecurityCheck(false, http.HandlerFunc(getExtClient))).Methods(http.MethodGet)
-	r.HandleFunc("/api/extclients/{network}/{clientid}/{type}", logic.SecurityCheck(false, http.HandlerFunc(getExtClientConf))).Methods(http.MethodGet)
-	r.HandleFunc("/api/extclients/{network}/{clientid}", logic.SecurityCheck(false, http.HandlerFunc(updateExtClient))).Methods(http.MethodPut)
-	r.HandleFunc("/api/extclients/{network}/{clientid}", logic.SecurityCheck(false, http.HandlerFunc(deleteExtClient))).Methods(http.MethodDelete)
-	r.HandleFunc("/api/extclients/{network}/{nodeid}", logic.SecurityCheck(false, checkFreeTierLimits(limitChoiceMachines, http.HandlerFunc(createExtClient)))).Methods(http.MethodPost)
+	r.HandleFunc("/api/extclients", logic.SecurityCheck(true, http.HandlerFunc(getAllExtClients))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/extclients/{network}", logic.SecurityCheck(true, http.HandlerFunc(getNetworkExtClients))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/extclients/{network}/{clientid}", logic.SecurityCheck(false, http.HandlerFunc(getExtClient))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/extclients/{network}/{clientid}/{type}", logic.SecurityCheck(false, http.HandlerFunc(getExtClientConf))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/extclients/{network}/{clientid}", logic.SecurityCheck(false, http.HandlerFunc(updateExtClient))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/extclients/{network}/{clientid}", logic.SecurityCheck(false, http.HandlerFunc(deleteExtClient))).
+		Methods(http.MethodDelete)
+	r.HandleFunc("/api/extclients/{network}/{nodeid}", logic.SecurityCheck(false, checkFreeTierLimits(limitChoiceMachines, http.HandlerFunc(createExtClient)))).
+		Methods(http.MethodPost)
 }
 
 func checkIngressExists(nodeID string) bool {
@@ -45,18 +52,12 @@ func checkIngressExists(nodeID string) bool {
 	return node.IsIngressGateway
 }
 
-// swagger:route GET /api/extclients/{network} ext_client getNetworkExtClients
-//
-// Get all extclients associated with network.
-// Gets all extclients associated with network, including pending extclients.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: extClientSliceResponse
+// @Summary     Get all remote access client associated with network
+// @Router      /api/extclients/{network} [get]
+// @Tags        Remote Access Client
+// @Security    oauth2
+// @Success     200 {object} models.ExtClient
+// @Failure     500 {object} models.ErrorResponse
 func getNetworkExtClients(w http.ResponseWriter, r *http.Request) {
 
 	w.Header().Set("Content-Type", "application/json")
@@ -77,18 +78,12 @@ func getNetworkExtClients(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(extclients)
 }
 
-// swagger:route GET /api/extclients ext_client getAllExtClients
-//
-// A separate function to get all extclients, not just extclients for a particular network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: extClientSliceResponse
-//
+// @Summary     Fetches All Remote Access Clients across all networks
+// @Router      /api/extclients [get]
+// @Tags        Remote Access Client
+// @Security    oauth2
+// @Success     200 {object} models.ExtClient
+// @Failure     500 {object} models.ErrorResponse
 // Not quite sure if this is necessary. Probably necessary based on front end but may
 // want to review after iteration 1 if it's being used or not
 func getAllExtClients(w http.ResponseWriter, r *http.Request) {
@@ -107,17 +102,13 @@ func getAllExtClients(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(clients)
 }
 
-// swagger:route GET /api/extclients/{network}/{clientid} ext_client getExtClient
-//
-// Get an individual extclient.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: extClientResponse
+// @Summary     Get an individual remote access client
+// @Router      /api/extclients/{network}/{clientid} [get]
+// @Tags        Remote Access Client
+// @Security    oauth2
+// @Success     200 {object} models.ExtClient
+// @Failure     500 {object} models.ErrorResponse
+// @Failure     403 {object} models.ErrorResponse
 func getExtClient(w http.ResponseWriter, r *http.Request) {
 	// set header.
 	w.Header().Set("Content-Type", "application/json")
@@ -128,35 +119,27 @@ func getExtClient(w http.ResponseWriter, r *http.Request) {
 	network := params["network"]
 	client, err := logic.GetExtClient(clientid, network)
 	if err != nil {
-		logger.Log(0, r.Header.Get("user"), fmt.Sprintf("failed to get extclient for [%s] on network [%s]: %v",
-			clientid, network, err))
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			fmt.Sprintf("failed to get extclient for [%s] on network [%s]: %v",
+				clientid, network, err),
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	if !logic.IsUserAllowedAccessToExtClient(r.Header.Get("user"), client) {
-		// check if user has access to extclient
-		slog.Error("failed to get extclient", "network", network, "clientID",
-			clientid, "error", errors.New("access is denied"))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("access is denied"), "forbidden"))
-		return
-
-	}
 
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(client)
 }
 
-// swagger:route GET /api/extclients/{network}/{clientid}/{type} ext_client getExtClientConf
-//
-// Get an individual extclient.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: extClientResponse
+// @Summary     Get an individual remote access client
+// @Router      /api/extclients/{network}/{clientid}/{type} [get]
+// @Tags        Remote Access Client
+// @Security    oauth2
+// @Success     200 {object} models.ExtClient
+// @Failure     500 {object} models.ErrorResponse
+// @Failure     403 {object} models.ErrorResponse
 func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	// set header.
 	w.Header().Set("Content-Type", "application/json")
@@ -166,36 +149,53 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 	networkid := params["network"]
 	client, err := logic.GetExtClient(clientid, networkid)
 	if err != nil {
-		logger.Log(0, r.Header.Get("user"), fmt.Sprintf("failed to get extclient for [%s] on network [%s]: %v",
-			clientid, networkid, err))
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			fmt.Sprintf("failed to get extclient for [%s] on network [%s]: %v",
+				clientid, networkid, err),
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	if !logic.IsUserAllowedAccessToExtClient(r.Header.Get("user"), client) {
-		slog.Error("failed to get extclient", "network", networkid, "clientID",
-			clientid, "error", errors.New("access is denied"))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("access is denied"), "forbidden"))
-		return
-	}
 
 	gwnode, err := logic.GetNodeByID(client.IngressGatewayID)
 	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to get ingress gateway node [%s] info: %v", client.IngressGatewayID, err))
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			fmt.Sprintf(
+				"failed to get ingress gateway node [%s] info: %v",
+				client.IngressGatewayID,
+				err,
+			),
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
 	host, err := logic.GetHost(gwnode.HostID.String())
 	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to get host for ingress gateway node [%s] info: %v", client.IngressGatewayID, err))
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			fmt.Sprintf(
+				"failed to get host for ingress gateway node [%s] info: %v",
+				client.IngressGatewayID,
+				err,
+			),
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
 
 	network, err := logic.GetParentNetwork(client.Network)
 	if err != nil {
-		logger.Log(1, r.Header.Get("user"), "Could not retrieve Ingress Gateway Network", client.Network)
+		logger.Log(
+			1,
+			r.Header.Get("user"),
+			"Could not retrieve Ingress Gateway Network",
+			client.Network,
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
@@ -209,8 +209,19 @@ func getExtClientConf(w http.ResponseWriter, r *http.Request) {
 		allowedPreferredIps = append(allowedPreferredIps, host.EndpointIP.String())
 		allowedPreferredIps = append(allowedPreferredIps, host.EndpointIPv6.String())
 		if !slices.Contains(allowedPreferredIps, preferredIp) {
-			slog.Warn("preferred endpoint ip is not associated with the RAG. proceeding with preferred ip", "preferred ip", preferredIp)
-			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("preferred endpoint ip is not associated with the RAG"), "badrequest"))
+			slog.Warn(
+				"preferred endpoint ip is not associated with the RAG. proceeding with preferred ip",
+				"preferred ip",
+				preferredIp,
+			)
+			logic.ReturnErrorResponse(
+				w,
+				r,
+				logic.FormatError(
+					errors.New("preferred endpoint ip is not associated with the RAG"),
+					"badrequest",
+				),
+			)
 			return
 		}
 		if net.ParseIP(preferredIp).To4() == nil {
@@ -354,16 +365,14 @@ Endpoint = %s
 	json.NewEncoder(w).Encode(client)
 }
 
-// swagger:route POST /api/extclients/{network}/{nodeid} ext_client createExtClient
-//
-// Create an individual extclient.  Must have valid key and be unique.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//			Responses:
-//			200:  okResponse
+// @Summary     Create an individual remote access client
+// @Router      /api/extclients/{network}/{nodeid} [post]
+// @Tags        Remote Access Client
+// @Security    oauth2
+// @Success     200 {string} string "OK"
+// @Failure     500 {object} models.ErrorResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     403 {object} models.ErrorResponse
 func createExtClient(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 
@@ -414,12 +423,6 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 		userName = caller.UserName
-		if _, ok := caller.RemoteGwIDs[nodeid]; (!caller.IsAdmin && !caller.IsSuperAdmin) && !ok {
-			err = errors.New("permission denied")
-			slog.Error("failed to create extclient", "error", err)
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
-			return
-		}
 		// check if user has a config already for remote access client
 		extclients, err := logic.GetNetworkExtClients(node.Network)
 		if err != nil {
@@ -466,16 +469,40 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if err = logic.CreateExtClient(&extclient); err != nil {
-		slog.Error("failed to create extclient", "user", r.Header.Get("user"), "network", node.Network, "error", err)
+		slog.Error(
+			"failed to create extclient",
+			"user",
+			r.Header.Get("user"),
+			"network",
+			node.Network,
+			"error",
+			err,
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
 
-	slog.Info("created extclient", "user", r.Header.Get("user"), "network", node.Network, "clientid", extclient.ClientID)
+	slog.Info(
+		"created extclient",
+		"user",
+		r.Header.Get("user"),
+		"network",
+		node.Network,
+		"clientid",
+		extclient.ClientID,
+	)
 	w.WriteHeader(http.StatusOK)
 	go func() {
 		if err := logic.SetClientDefaultACLs(&extclient); err != nil {
-			slog.Error("failed to set default acls for extclient", "user", r.Header.Get("user"), "network", node.Network, "error", err)
+			slog.Error(
+				"failed to set default acls for extclient",
+				"user",
+				r.Header.Get("user"),
+				"network",
+				node.Network,
+				"error",
+				err,
+			)
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 			return
 		}
@@ -488,17 +515,14 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
 	}()
 }
 
-// swagger:route PUT /api/extclients/{network}/{clientid} ext_client updateExtClient
-//
-// Update an individual extclient.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: extClientResponse
+// @Summary     Update an individual remote access client
+// @Router      /api/extclients/{network}/{clientid} [put]
+// @Tags        Remote Access Client
+// @Security    oauth2
+// @Success     200 {object} models.ExtClient
+// @Failure     500 {object} models.ErrorResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     403 {object} models.ErrorResponse
 func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 
@@ -515,21 +539,20 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	clientid := params["clientid"]
-	network := params["network"]
 	oldExtClient, err := logic.GetExtClientByName(clientid)
 	if err != nil {
-		slog.Error("failed to retrieve extclient", "user", r.Header.Get("user"), "id", clientid, "error", err)
+		slog.Error(
+			"failed to retrieve extclient",
+			"user",
+			r.Header.Get("user"),
+			"id",
+			clientid,
+			"error",
+			err,
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	if !logic.IsUserAllowedAccessToExtClient(r.Header.Get("user"), oldExtClient) {
-		// check if user has access to extclient
-		slog.Error("failed to get extclient", "network", network, "clientID",
-			clientid, "error", errors.New("access is denied"))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("access is denied"), "forbidden"))
-		return
-
-	}
 	if oldExtClient.ClientID == update.ClientID {
 		if err := validateCustomExtClient(&update, false); err != nil {
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
@@ -567,12 +590,32 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 	}
 	newclient := logic.UpdateExtClient(&oldExtClient, &update)
 	if err := logic.DeleteExtClient(oldExtClient.Network, oldExtClient.ClientID); err != nil {
-		slog.Error("failed to delete ext client", "user", r.Header.Get("user"), "id", oldExtClient.ClientID, "network", oldExtClient.Network, "error", err)
+		slog.Error(
+			"failed to delete ext client",
+			"user",
+			r.Header.Get("user"),
+			"id",
+			oldExtClient.ClientID,
+			"network",
+			oldExtClient.Network,
+			"error",
+			err,
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
 	if err := logic.SaveExtClient(&newclient); err != nil {
-		slog.Error("failed to save ext client", "user", r.Header.Get("user"), "id", newclient.ClientID, "network", newclient.Network, "error", err)
+		slog.Error(
+			"failed to save ext client",
+			"user",
+			r.Header.Get("user"),
+			"id",
+			newclient.ClientID,
+			"network",
+			newclient.Network,
+			"error",
+			err,
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
@@ -588,13 +631,25 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 			ingressNode, err := logic.GetNodeByID(newclient.IngressGatewayID)
 			if err == nil {
 				if err = mq.PublishPeerUpdate(false); err != nil {
-					logger.Log(1, "error setting ext peers on", ingressNode.ID.String(), ":", err.Error())
+					logger.Log(
+						1,
+						"error setting ext peers on",
+						ingressNode.ID.String(),
+						":",
+						err.Error(),
+					)
 				}
 			}
 			if !update.Enabled {
 				ingressHost, err := logic.GetHost(ingressNode.HostID.String())
 				if err != nil {
-					slog.Error("Failed to get ingress host", "node", ingressNode.ID.String(), "error", err)
+					slog.Error(
+						"Failed to get ingress host",
+						"node",
+						ingressNode.ID.String(),
+						"error",
+						err,
+					)
 					return
 				}
 				nodes, err := logic.GetAllNodes()
@@ -602,7 +657,7 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 					slog.Error("Failed to get nodes", "error", err)
 					return
 				}
-				go mq.PublishSingleHostPeerUpdate(ingressHost, nodes, nil, []models.ExtClient{oldExtClient}, false)
+				go mq.PublishSingleHostPeerUpdate(ingressHost, nodes, nil, []models.ExtClient{oldExtClient}, false, nil)
 			}
 		}
 
@@ -610,17 +665,13 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
 
 }
 
-// swagger:route DELETE /api/extclients/{network}/{clientid} ext_client deleteExtClient
-//
-// Delete an individual extclient.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: successResponse
+// @Summary     Delete an individual remote access client
+// @Router      /api/extclients/{network}/{clientid} [delete]
+// @Tags        Remote Access Client
+// @Security    oauth2
+// @Success     200
+// @Failure     500 {object} models.ErrorResponse
+// @Failure     403 {object} models.ErrorResponse
 func deleteExtClient(w http.ResponseWriter, r *http.Request) {
 	// Set header
 	w.Header().Set("Content-Type", "application/json")
@@ -637,16 +688,17 @@ func deleteExtClient(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	if !logic.IsUserAllowedAccessToExtClient(r.Header.Get("user"), extclient) {
-		slog.Error("user not allowed to delete", "network", network, "clientID",
-			clientid, "error", errors.New("access is denied"))
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("access is denied"), "forbidden"))
-		return
-	}
 	ingressnode, err := logic.GetNodeByID(extclient.IngressGatewayID)
 	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to get ingress gateway node [%s] info: %v", extclient.IngressGatewayID, err))
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			fmt.Sprintf(
+				"failed to get ingress gateway node [%s] info: %v",
+				extclient.IngressGatewayID,
+				err,
+			),
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}

+ 7 - 11
controllers/files.go

@@ -6,16 +6,12 @@ import (
 	"github.com/gorilla/mux"
 )
 
+// @Summary     Retrieve a file from the file server
+// @Router      /meshclient/files/{filename}  [get]
+// @Tags        Meshclient
+// @Success     200 {body} file "file"
+// @Failure     404 {string} string "404 not found"
 func fileHandlers(r *mux.Router) {
-	// swagger:route GET /meshclient/files/{filename} meshclient getFile
-	//
-	// Retrieve a file from the file server.
-	//
-	//		Schemes: https
-	//
-	// 		Security:
-	//   		oauth
-	//		Responses:
-	//		200: fileResponse
-	r.PathPrefix("/meshclient/files").Handler(http.StripPrefix("/meshclient/files", http.FileServer(http.Dir("./meshclient/files"))))
+	r.PathPrefix("/meshclient/files").
+		Handler(http.StripPrefix("/meshclient/files", http.FileServer(http.Dir("./meshclient/files"))))
 }

+ 326 - 180
controllers/hosts.go

@@ -19,23 +19,43 @@ import (
 )
 
 func hostHandlers(r *mux.Router) {
-	r.HandleFunc("/api/hosts", logic.SecurityCheck(true, http.HandlerFunc(getHosts))).Methods(http.MethodGet)
-	r.HandleFunc("/api/hosts/keys", logic.SecurityCheck(true, http.HandlerFunc(updateAllKeys))).Methods(http.MethodPut)
-	r.HandleFunc("/api/hosts/{hostid}/keys", logic.SecurityCheck(true, http.HandlerFunc(updateKeys))).Methods(http.MethodPut)
-	r.HandleFunc("/api/hosts/{hostid}/sync", logic.SecurityCheck(true, http.HandlerFunc(syncHost))).Methods(http.MethodPost)
-	r.HandleFunc("/api/hosts/{hostid}", logic.SecurityCheck(true, http.HandlerFunc(updateHost))).Methods(http.MethodPut)
-	r.HandleFunc("/api/hosts/{hostid}", Authorize(true, false, "all", http.HandlerFunc(deleteHost))).Methods(http.MethodDelete)
-	r.HandleFunc("/api/hosts/{hostid}/upgrade", logic.SecurityCheck(true, http.HandlerFunc(upgradeHost))).Methods(http.MethodPut)
-	r.HandleFunc("/api/hosts/{hostid}/networks/{network}", logic.SecurityCheck(true, http.HandlerFunc(addHostToNetwork))).Methods(http.MethodPost)
-	r.HandleFunc("/api/hosts/{hostid}/networks/{network}", logic.SecurityCheck(true, http.HandlerFunc(deleteHostFromNetwork))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/hosts", logic.SecurityCheck(true, http.HandlerFunc(getHosts))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/hosts/keys", logic.SecurityCheck(true, http.HandlerFunc(updateAllKeys))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/hosts/{hostid}/keys", logic.SecurityCheck(true, http.HandlerFunc(updateKeys))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/hosts/{hostid}/sync", logic.SecurityCheck(true, http.HandlerFunc(syncHost))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/hosts/{hostid}", logic.SecurityCheck(true, http.HandlerFunc(updateHost))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/hosts/{hostid}", Authorize(true, false, "all", http.HandlerFunc(deleteHost))).
+		Methods(http.MethodDelete)
+	r.HandleFunc("/api/hosts/{hostid}/upgrade", logic.SecurityCheck(true, http.HandlerFunc(upgradeHost))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/hosts/{hostid}/networks/{network}", logic.SecurityCheck(true, http.HandlerFunc(addHostToNetwork))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/hosts/{hostid}/networks/{network}", logic.SecurityCheck(true, http.HandlerFunc(deleteHostFromNetwork))).
+		Methods(http.MethodDelete)
 	r.HandleFunc("/api/hosts/adm/authenticate", authenticateHost).Methods(http.MethodPost)
-	r.HandleFunc("/api/v1/host", Authorize(true, false, "host", http.HandlerFunc(pull))).Methods(http.MethodGet)
-	r.HandleFunc("/api/v1/host/{hostid}/signalpeer", Authorize(true, false, "host", http.HandlerFunc(signalPeer))).Methods(http.MethodPost)
-	r.HandleFunc("/api/v1/fallback/host/{hostid}", Authorize(true, false, "host", http.HandlerFunc(hostUpdateFallback))).Methods(http.MethodPut)
-	r.HandleFunc("/api/emqx/hosts", logic.SecurityCheck(true, http.HandlerFunc(delEmqxHosts))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/host", Authorize(true, false, "host", http.HandlerFunc(pull))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/host/{hostid}/signalpeer", Authorize(true, false, "host", http.HandlerFunc(signalPeer))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/fallback/host/{hostid}", Authorize(true, false, "host", http.HandlerFunc(hostUpdateFallback))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/emqx/hosts", logic.SecurityCheck(true, http.HandlerFunc(delEmqxHosts))).
+		Methods(http.MethodDelete)
 	r.HandleFunc("/api/v1/auth-register/host", socketHandler)
 }
 
+// @Summary     Upgrade a host
+// @Router      /api/hosts/{hostid}/upgrade [put]
+// @Tags        Hosts
+// @Security    oauth
+// @Param       hostid path string true "Host ID"
+// @Success     200 {string} string "passed message to upgrade host"
+// @Failure     500 {object} models.ErrorResponse
 // upgrade host is a handler to send upgrade message to a host
 func upgradeHost(w http.ResponseWriter, r *http.Request) {
 	host, err := logic.GetHost(mux.Vars(r)["hostid"])
@@ -52,24 +72,70 @@ func upgradeHost(w http.ResponseWriter, r *http.Request) {
 	logic.ReturnSuccessResponse(w, r, "passed message to upgrade host")
 }
 
-// swagger:route GET /api/hosts hosts getHosts
-//
-// Lists all hosts.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: apiHostSliceResponse
+// @Summary     List all hosts
+// @Router      /api/hosts [get]
+// @Tags        Hosts
+// @Security    oauth
+// @Success     200 {array} models.ApiHost
+// @Failure     500 {object} models.ErrorResponse
 func getHosts(w http.ResponseWriter, r *http.Request) {
-	currentHosts, err := logic.GetAllHosts()
-	if err != nil {
-		logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
+	w.Header().Set("Content-Type", "application/json")
+	currentHosts := []models.Host{}
+	var err error
+	if r.Header.Get("ismaster") == "yes" {
+		currentHosts, err = logic.GetAllHosts()
+		if err != nil {
+			logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error())
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+	} else {
+		username := r.Header.Get("user")
+		user, err := logic.GetUser(username)
+		if err != nil {
+			return
+		}
+		userPlatformRole, err := logic.GetRole(user.PlatformRoleID)
+		if err != nil {
+			return
+		}
+		respHostsMap := make(map[string]struct{})
+		if !userPlatformRole.FullAccess {
+			nodes, err := logic.GetAllNodes()
+			if err != nil {
+				logger.Log(0, "error fetching all nodes info: ", err.Error())
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+				return
+			}
+			filteredNodes := logic.GetFilteredNodesByUserAccess(*user, nodes)
+			if len(filteredNodes) > 0 {
+				currentHostsMap, err := logic.GetHostsMap()
+				if err != nil {
+					logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error())
+					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+					return
+				}
+				for _, node := range filteredNodes {
+					if _, ok := respHostsMap[node.HostID.String()]; ok {
+						continue
+					}
+					if host, ok := currentHostsMap[node.HostID.String()]; ok {
+						currentHosts = append(currentHosts, host)
+						respHostsMap[host.ID.String()] = struct{}{}
+					}
+				}
+
+			}
+		} else {
+			currentHosts, err = logic.GetAllHosts()
+			if err != nil {
+				logger.Log(0, r.Header.Get("user"), "failed to fetch hosts: ", err.Error())
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+				return
+			}
+		}
 	}
+
 	apiHosts := logic.GetAllHostsAPI(currentHosts[:])
 	logger.Log(2, r.Header.Get("user"), "fetched all hosts")
 	logic.SortApiHosts(apiHosts[:])
@@ -77,23 +143,22 @@ func getHosts(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(apiHosts)
 }
 
-// swagger:route GET /api/v1/host hosts pullHost
-//
-// Used by clients for "pull" command
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: hostPull
+// @Summary     Used by clients for "pull" command
+// @Router      /api/v1/host [get]
+// @Tags        Hosts
+// @Security    oauth
+// @Success     200 {object} models.HostPull
+// @Failure     500 {object} models.ErrorResponse
 func pull(w http.ResponseWriter, r *http.Request) {
 
 	hostID := r.Header.Get(hostIDHeader) // return JSON/API formatted keys
 	if len(hostID) == 0 {
 		logger.Log(0, "no host authorized to pull")
-		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("no host authorized to pull"), "internal"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(fmt.Errorf("no host authorized to pull"), "internal"),
+		)
 		return
 	}
 	host, err := logic.GetHost(hostID)
@@ -153,17 +218,14 @@ func pull(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(&response)
 }
 
-// swagger:route PUT /api/hosts/{hostid} hosts updateHost
-//
-// Updates a Netclient host on Netmaker server.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: apiHostResponse
+// @Summary     Updates a Netclient host on Netmaker server
+// @Router      /api/hosts/{hostid} [put]
+// @Tags        Hosts
+// @Security    oauth
+// @Param       hostid path string true "Host ID"
+// @Param       body body models.ApiHost true "New host data"
+// @Success     200 {object} models.ApiHost
+// @Failure     500 {object} models.ErrorResponse
 func updateHost(w http.ResponseWriter, r *http.Request) {
 	var newHostData models.ApiHost
 	err := json.NewDecoder(r.Body).Decode(&newHostData)
@@ -183,6 +245,19 @@ func updateHost(w http.ResponseWriter, r *http.Request) {
 
 	newHost := newHostData.ConvertAPIHostToNMHost(currHost)
 
+	if newHost.Name != currHost.Name {
+		// update any rag role ids
+		for _, nodeID := range newHost.Nodes {
+			node, err := logic.GetNodeByID(nodeID)
+			if err == nil && node.IsIngressGateway {
+				role, err := logic.GetRole(models.GetRAGRoleID(node.Network, currHost.ID.String()))
+				if err == nil {
+					role.UiName = models.GetRAGRoleName(node.Network, newHost.Name)
+					logic.UpdateRole(role)
+				}
+			}
+		}
+	}
 	logic.UpdateHost(newHost, currHost) // update the in memory struct values
 	if err = logic.UpsertHost(newHost); err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to update a host:", err.Error())
@@ -194,7 +269,13 @@ func updateHost(w http.ResponseWriter, r *http.Request) {
 		Action: models.UpdateHost,
 		Host:   *newHost,
 	}); err != nil {
-		logger.Log(0, r.Header.Get("user"), "failed to send host update: ", currHost.ID.String(), err.Error())
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			"failed to send host update: ",
+			currHost.ID.String(),
+			err.Error(),
+		)
 	}
 	go func() {
 		if err := mq.PublishPeerUpdate(false); err != nil {
@@ -213,17 +294,14 @@ func updateHost(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(apiHostData)
 }
 
-// swagger:route PUT /api/v1/fallback/host/{hostid} hosts hostUpdateFallback
-//
-// Updates a Netclient host on Netmaker server.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: apiHostResponse
+// @Summary     Updates a Netclient host on Netmaker server
+// @Router      /api/v1/fallback/host/{hostid} [put]
+// @Tags        Hosts
+// @Security    oauth
+// @Param       hostid path string true "Host ID"
+// @Param       body body models.HostUpdate true "Host update data"
+// @Success     200 {string} string "updated host data"
+// @Failure     500 {object} models.ErrorResponse
 func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	hostid := params["hostid"]
@@ -273,17 +351,14 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
 	logic.ReturnSuccessResponse(w, r, "updated host data")
 }
 
-// swagger:route DELETE /api/hosts/{hostid} hosts deleteHost
-//
-// Deletes a Netclient host from Netmaker server.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: apiHostResponse
+// @Summary     Deletes a Netclient host from Netmaker server
+// @Router      /api/hosts/{hostid} [delete]
+// @Tags        Hosts
+// @Security    oauth
+// @Param       hostid path string true "Host ID"
+// @Param       force query bool false "Force delete"
+// @Success     200 {object} models.ApiHost
+// @Failure     500 {object} models.ErrorResponse
 func deleteHost(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	hostid := params["hostid"]
@@ -312,14 +387,26 @@ func deleteHost(w http.ResponseWriter, r *http.Request) {
 	if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
 		// delete EMQX credentials for host
 		if err := mq.GetEmqxHandler().DeleteEmqxUser(currHost.ID.String()); err != nil {
-			slog.Error("failed to remove host credentials from EMQX", "id", currHost.ID, "error", err)
+			slog.Error(
+				"failed to remove host credentials from EMQX",
+				"id",
+				currHost.ID,
+				"error",
+				err,
+			)
 		}
 	}
 	if err = mq.HostUpdate(&models.HostUpdate{
 		Action: models.DeleteHost,
 		Host:   *currHost,
 	}); err != nil {
-		logger.Log(0, r.Header.Get("user"), "failed to send delete host update: ", currHost.ID.String(), err.Error())
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			"failed to send delete host update: ",
+			currHost.ID.String(),
+			err.Error(),
+		)
 	}
 	if err = logic.RemoveHost(currHost, forceDelete); err != nil {
 		logger.Log(0, r.Header.Get("user"), "failed to delete a host:", err.Error())
@@ -333,23 +420,25 @@ func deleteHost(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(apiHostData)
 }
 
-// swagger:route POST /api/hosts/{hostid}/networks/{network} hosts addHostToNetwork
-//
-// Given a network, a host is added to the network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//			Responses:
-//				200: okResponse
+// @Summary     To Add Host To Network
+// @Router      /api/hosts/{hostid}/networks/{network} [post]
+// @Tags        Hosts
+// @Security    oauth
+// @Param       hostid path string true "Host ID"
+// @Param       network path string true "Network name"
+// @Success     200 {string} string "OK"
+// @Failure     500 {object} models.ErrorResponse
 func addHostToNetwork(w http.ResponseWriter, r *http.Request) {
 
 	var params = mux.Vars(r)
 	hostid := params["hostid"]
 	network := params["network"]
 	if hostid == "" || network == "" {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("hostid or network cannot be empty"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("hostid or network cannot be empty"), "badrequest"),
+		)
 		return
 	}
 	// confirm host exists
@@ -362,7 +451,14 @@ func addHostToNetwork(w http.ResponseWriter, r *http.Request) {
 
 	newNode, err := logic.UpdateHostNetwork(currHost, network, true)
 	if err != nil {
-		logger.Log(0, r.Header.Get("user"), "failed to add host to network:", hostid, network, err.Error())
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			"failed to add host to network:",
+			hostid,
+			network,
+			err.Error(),
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
@@ -384,21 +480,23 @@ func addHostToNetwork(w http.ResponseWriter, r *http.Request) {
 			logic.SetDNS()
 		}
 	}()
-	logger.Log(2, r.Header.Get("user"), fmt.Sprintf("added host %s to network %s", currHost.Name, network))
+	logger.Log(
+		2,
+		r.Header.Get("user"),
+		fmt.Sprintf("added host %s to network %s", currHost.Name, network),
+	)
 	w.WriteHeader(http.StatusOK)
 }
 
-// swagger:route DELETE /api/hosts/{hostid}/networks/{network} hosts deleteHostFromNetwork
-//
-// Given a network, a host is removed from the network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: okResponse
+// @Summary     To Remove Host from Network
+// @Router      /api/hosts/{hostid}/networks/{network} [delete]
+// @Tags        Hosts
+// @Security    oauth
+// @Param       hostid path string true "Host ID"
+// @Param       network path string true "Network name"
+// @Param       force query bool false "Force delete"
+// @Success     200 {string} string "OK"
+// @Failure     500 {object} models.ErrorResponse
 func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 
 	var params = mux.Vars(r)
@@ -406,7 +504,11 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 	network := params["network"]
 	forceDelete := r.URL.Query().Get("force") == "true"
 	if hostid == "" || network == "" {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("hostid or network cannot be empty"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("hostid or network cannot be empty"), "badrequest"),
+		)
 		return
 	}
 	// confirm host exists
@@ -416,14 +518,29 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 			// check if there is any daemon nodes that needs to be deleted
 			node, err := logic.GetNodeByHostRef(hostid, network)
 			if err != nil {
-				slog.Error("couldn't get node for host", "hostid", hostid, "network", network, "error", err)
+				slog.Error(
+					"couldn't get node for host",
+					"hostid",
+					hostid,
+					"network",
+					network,
+					"error",
+					err,
+				)
 				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 				return
 			}
 			if err = logic.DeleteNodeByID(&node); err != nil {
 				slog.Error("failed to force delete daemon node",
 					"nodeid", node.ID.String(), "hostid", hostid, "network", network, "error", err)
-				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to force delete daemon node: "+err.Error()), "internal"))
+				logic.ReturnErrorResponse(
+					w,
+					r,
+					logic.FormatError(
+						fmt.Errorf("failed to force delete daemon node: "+err.Error()),
+						"internal",
+					),
+				)
 				return
 			}
 			logic.ReturnSuccessResponse(w, r, "force deleted daemon node successfully")
@@ -441,20 +558,42 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 			// force cleanup the node
 			node, err := logic.GetNodeByHostRef(hostid, network)
 			if err != nil {
-				slog.Error("couldn't get node for host", "hostid", hostid, "network", network, "error", err)
+				slog.Error(
+					"couldn't get node for host",
+					"hostid",
+					hostid,
+					"network",
+					network,
+					"error",
+					err,
+				)
 				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 				return
 			}
 			if err = logic.DeleteNodeByID(&node); err != nil {
 				slog.Error("failed to force delete daemon node",
 					"nodeid", node.ID.String(), "hostid", hostid, "network", network, "error", err)
-				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to force delete daemon node: "+err.Error()), "internal"))
+				logic.ReturnErrorResponse(
+					w,
+					r,
+					logic.FormatError(
+						fmt.Errorf("failed to force delete daemon node: "+err.Error()),
+						"internal",
+					),
+				)
 				return
 			}
 			logic.ReturnSuccessResponse(w, r, "force deleted daemon node successfully")
 			return
 		}
-		logger.Log(0, r.Header.Get("user"), "failed to remove host from network:", hostid, network, err.Error())
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			"failed to remove host from network:",
+			hostid,
+			network,
+			err.Error(),
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
@@ -464,7 +603,11 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 	}
 	logger.Log(1, "deleting node", node.ID.String(), "from host", currHost.Name)
 	if err := logic.DeleteNode(node, forceDelete); err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete node"), "internal"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(fmt.Errorf("failed to delete node"), "internal"),
+		)
 		return
 	}
 	go func() {
@@ -473,21 +616,23 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
 			logic.SetDNS()
 		}
 	}()
-	logger.Log(2, r.Header.Get("user"), fmt.Sprintf("removed host %s from network %s", currHost.Name, network))
+	logger.Log(
+		2,
+		r.Header.Get("user"),
+		fmt.Sprintf("removed host %s from network %s", currHost.Name, network),
+	)
 	w.WriteHeader(http.StatusOK)
 }
 
-// swagger:route POST /api/hosts/adm/authenticate authenticate authenticateHost
-//
-// Host based authentication for making further API calls.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: successResponse
+// @Summary     To Fetch Auth Token for a Host
+// @Router      /api/hosts/adm/authenticate [post]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.AuthParams true "Authentication parameters"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func authenticateHost(response http.ResponseWriter, request *http.Request) {
 	var authRequest models.AuthParams
 	var errorResponse = models.ErrorResponse{
@@ -523,7 +668,7 @@ func authenticateHost(response http.ResponseWriter, request *http.Request) {
 		errorResponse.Code = http.StatusBadRequest
 		errorResponse.Message = err.Error()
 		logger.Log(0, request.Header.Get("user"),
-			"error retrieving host: ", err.Error())
+			"error retrieving host: ", authRequest.ID, err.Error())
 		logic.ReturnErrorResponse(response, request, errorResponse)
 		return
 	}
@@ -579,17 +724,14 @@ func authenticateHost(response http.ResponseWriter, request *http.Request) {
 	response.Write(successJSONResponse)
 }
 
-// swagger:route POST /api/hosts/{hostid}/signalpeer hosts signalPeer
-//
-// send signal to peer.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: signal
+// @Summary     Send signal to peer
+// @Router      /api/v1/host/{hostid}/signalpeer [post]
+// @Tags        Hosts
+// @Security    oauth
+// @Param       hostid path string true "Host ID"
+// @Param       body body models.Signal true "Signal data"
+// @Success     200 {object} models.Signal
+// @Failure     400 {object} models.ErrorResponse
 func signalPeer(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	hostid := params["hostid"]
@@ -617,7 +759,11 @@ func signalPeer(w http.ResponseWriter, r *http.Request) {
 	signal.IsPro = servercfg.IsPro
 	peerHost, err := logic.GetHost(signal.ToHostID)
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to signal, peer not found"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("failed to signal, peer not found"), "badrequest"),
+		)
 		return
 	}
 	err = mq.HostUpdate(&models.HostUpdate{
@@ -626,7 +772,14 @@ func signalPeer(w http.ResponseWriter, r *http.Request) {
 		Signal: signal,
 	})
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to publish signal to peer: "+err.Error()), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("failed to publish signal to peer: "+err.Error()),
+				"badrequest",
+			),
+		)
 		return
 	}
 
@@ -634,17 +787,12 @@ func signalPeer(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(signal)
 }
 
-// swagger:route POST /api/hosts/keys hosts updateAllKeys
-//
-// Update keys for a network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: networkBodyResponse
+// @Summary     Update keys for all hosts
+// @Router      /api/hosts/keys [put]
+// @Tags        Hosts
+// @Security    oauth
+// @Success     200 {string} string "OK"
+// @Failure     400 {object} models.ErrorResponse
 func updateAllKeys(w http.ResponseWriter, r *http.Request) {
 	var errorResponse = models.ErrorResponse{}
 	w.Header().Set("Content-Type", "application/json")
@@ -664,7 +812,12 @@ func updateAllKeys(w http.ResponseWriter, r *http.Request) {
 			hostUpdate.Host = host
 			logger.Log(2, "updating host", host.ID.String(), " for a key update")
 			if err = mq.HostUpdate(&hostUpdate); err != nil {
-				logger.Log(0, "failed to send update to node during a network wide key update", host.ID.String(), err.Error())
+				logger.Log(
+					0,
+					"failed to send update to node during a network wide key update",
+					host.ID.String(),
+					err.Error(),
+				)
 			}
 		}
 	}()
@@ -672,17 +825,13 @@ func updateAllKeys(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 }
 
-// swagger:route POST /api/hosts/{hostid}keys hosts updateKeys
-//
-// Update keys for a network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: networkBodyResponse
+// @Summary     Update keys for a host
+// @Router      /api/hosts/{hostid}/keys [put]
+// @Tags        Hosts
+// @Security    oauth
+// @Param       hostid path string true "Host ID"
+// @Success     200 {string} string "OK"
+// @Failure     400 {object} models.ErrorResponse
 func updateKeys(w http.ResponseWriter, r *http.Request) {
 	var errorResponse = models.ErrorResponse{}
 	w.Header().Set("Content-Type", "application/json")
@@ -711,17 +860,13 @@ func updateKeys(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 }
 
-// swagger:route POST /api/hosts/{hostid}/sync hosts synchost
-//
-// Requests a host to pull.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: networkBodyResponse
+// @Summary     Requests a host to pull
+// @Router      /api/hosts/{hostid}/sync [post]
+// @Tags        Hosts
+// @Security    oauth
+// @Param       hostid path string true "Host ID"
+// @Success     200 {string} string "OK"
+// @Failure     400 {object} models.ErrorResponse
 func syncHost(w http.ResponseWriter, r *http.Request) {
 	hostId := mux.Vars(r)["hostid"]
 
@@ -751,17 +896,12 @@ func syncHost(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 }
 
-// swagger:route DELETE /api/emqx/hosts hosts delEmqxHosts
-//
-// Lists all hosts.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: apiHostResponse
+// @Summary     Deletes all EMQX hosts
+// @Router      /api/emqx/hosts [delete]
+// @Tags        Hosts
+// @Security    oauth
+// @Success     200 {string} string "deleted hosts data on emqx"
+// @Failure     500 {object} models.ErrorResponse
 func delEmqxHosts(w http.ResponseWriter, r *http.Request) {
 	currentHosts, err := logic.GetAllHosts()
 	if err != nil {
@@ -777,7 +917,13 @@ func delEmqxHosts(w http.ResponseWriter, r *http.Request) {
 	}
 	err = mq.GetEmqxHandler().DeleteEmqxUser(servercfg.GetMqUserName())
 	if err != nil {
-		slog.Error("failed to remove server credentials from EMQX", "user", servercfg.GetMqUserName(), "error", err)
+		slog.Error(
+			"failed to remove server credentials from EMQX",
+			"user",
+			servercfg.GetMqUserName(),
+			"error",
+			err,
+		)
 	}
 	logic.ReturnSuccessResponse(w, r, "deleted hosts data on emqx")
 }

+ 6 - 11
controllers/ipservice.go

@@ -15,17 +15,12 @@ func ipHandlers(r *mux.Router) {
 	r.HandleFunc("/api/getip", http.HandlerFunc(getPublicIP)).Methods(http.MethodGet)
 }
 
-// swagger:route GET /api/getip ipservice getPublicIP
-//
-// Get the current public IP address.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: byteArrayResponse
+// @Summary     Get the current public IP address.
+// @Router      /api/getip [get]
+// @Tags        IP Service
+// @Security    oauth2
+// @Success     200 {string} string "The public IP address."
+// @Failure     400 {string} string "Invalid IP address or no IP found."
 func getPublicIP(w http.ResponseWriter, r *http.Request) {
 	r.Header.Set("Connection", "close")
 	ip, err := parseIP(r)

+ 8 - 12
controllers/legacy.go

@@ -9,20 +9,16 @@ import (
 )
 
 func legacyHandlers(r *mux.Router) {
-	r.HandleFunc("/api/v1/legacy/nodes", logic.SecurityCheck(true, http.HandlerFunc(wipeLegacyNodes))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/legacy/nodes", logic.SecurityCheck(true, http.HandlerFunc(wipeLegacyNodes))).
+		Methods(http.MethodDelete)
 }
 
-// swagger:route DELETE /api/v1/legacy/nodes nodes wipeLegacyNodes
-//
-// Delete all legacy nodes from DB.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: successResponse
+// @Summary     Delete all legacy nodes from DB.
+// @Router      /api/v1/legacy/nodes [delete]
+// @Tags        Nodes
+// @Security    oauth2
+// @Success     200 {string} string "Wiped all legacy nodes."
+// @Failure     400 {object} models.ErrorResponse
 func wipeLegacyNodes(w http.ResponseWriter, r *http.Request) {
 	// Set header
 	w.Header().Set("Content-Type", "application/json")

+ 109 - 0
controllers/middleware.go

@@ -0,0 +1,109 @@
+package controller
+
+import (
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/gorilla/mux"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+)
+
+func userMiddleWare(handler http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		var params = mux.Vars(r)
+		route, err := mux.CurrentRoute(r).GetPathTemplate()
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+			return
+		}
+		if r.Method == http.MethodPost && route == "/api/extclients/{network}/{nodeid}" {
+			node, err := logic.GetNodeByID(params["nodeid"])
+			if err == nil {
+				params["network"] = node.Network
+			}
+		}
+		r.Header.Set("IS_GLOBAL_ACCESS", "no")
+		r.Header.Set("TARGET_RSRC", "")
+		r.Header.Set("RSRC_TYPE", "")
+		r.Header.Set("TARGET_RSRC_ID", "")
+		r.Header.Set("NET_ID", params["network"])
+		if strings.Contains(route, "hosts") || strings.Contains(route, "nodes") {
+			r.Header.Set("TARGET_RSRC", models.HostRsrc.String())
+		}
+		if strings.Contains(route, "dns") {
+			r.Header.Set("TARGET_RSRC", models.DnsRsrc.String())
+		}
+		if strings.Contains(route, "users") {
+			r.Header.Set("TARGET_RSRC", models.UserRsrc.String())
+		}
+		if strings.Contains(route, "ingress") {
+			r.Header.Set("TARGET_RSRC", models.RemoteAccessGwRsrc.String())
+		}
+		if strings.Contains(route, "createrelay") || strings.Contains(route, "deleterelay") {
+			r.Header.Set("TARGET_RSRC", models.RelayRsrc.String())
+		}
+
+		if strings.Contains(route, "gateway") {
+			r.Header.Set("TARGET_RSRC", models.EgressGwRsrc.String())
+		}
+		if strings.Contains(route, "networks") {
+			r.Header.Set("TARGET_RSRC", models.NetworkRsrc.String())
+		}
+		if strings.Contains(route, "acls") {
+			r.Header.Set("TARGET_RSRC", models.AclRsrc.String())
+		}
+		if strings.Contains(route, "extclients") {
+			r.Header.Set("TARGET_RSRC", models.ExtClientsRsrc.String())
+		}
+		if strings.Contains(route, "enrollment-keys") {
+			r.Header.Set("TARGET_RSRC", models.EnrollmentKeysRsrc.String())
+		}
+		if strings.Contains(route, "metrics") {
+			r.Header.Set("TARGET_RSRC", models.MetricRsrc.String())
+		}
+		if keyID, ok := params["keyID"]; ok {
+			r.Header.Set("TARGET_RSRC_ID", keyID)
+		}
+		if nodeID, ok := params["nodeid"]; ok && r.Header.Get("TARGET_RSRC") != models.ExtClientsRsrc.String() {
+			r.Header.Set("TARGET_RSRC_ID", nodeID)
+		}
+		if strings.Contains(route, "failover") {
+			r.Header.Set("TARGET_RSRC", models.FailOverRsrc.String())
+			nodeID := r.Header.Get("TARGET_RSRC_ID")
+			node, _ := logic.GetNodeByID(nodeID)
+			r.Header.Set("NET_ID", node.Network)
+
+		}
+		if hostID, ok := params["hostid"]; ok {
+			r.Header.Set("TARGET_RSRC_ID", hostID)
+		}
+		if clientID, ok := params["clientid"]; ok {
+			r.Header.Set("TARGET_RSRC_ID", clientID)
+		}
+		if netID, ok := params["networkname"]; ok {
+			if !strings.Contains(route, "acls") {
+				r.Header.Set("TARGET_RSRC_ID", netID)
+			}
+			r.Header.Set("NET_ID", params["networkname"])
+		}
+
+		if userID, ok := params["username"]; ok {
+			r.Header.Set("TARGET_RSRC_ID", userID)
+		} else {
+			username, _ := url.QueryUnescape(r.URL.Query().Get("username"))
+			if username != "" {
+				r.Header.Set("TARGET_RSRC_ID", username)
+			}
+		}
+		if r.Header.Get("NET_ID") == "" && (r.Header.Get("TARGET_RSRC_ID") == "" ||
+			r.Header.Get("TARGET_RSRC") == models.EnrollmentKeysRsrc.String() ||
+			r.Header.Get("TARGET_RSRC") == models.UserRsrc.String()) {
+			r.Header.Set("IS_GLOBAL_ACCESS", "yes")
+		}
+
+		r.Header.Set("RSRC_TYPE", r.Header.Get("TARGET_RSRC"))
+		handler.ServeHTTP(w, r)
+	})
+}

+ 138 - 102
controllers/network.go

@@ -22,28 +22,32 @@ import (
 )
 
 func networkHandlers(r *mux.Router) {
-	r.HandleFunc("/api/networks", logic.SecurityCheck(true, http.HandlerFunc(getNetworks))).Methods(http.MethodGet)
-	r.HandleFunc("/api/networks", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceNetworks, http.HandlerFunc(createNetwork)))).Methods(http.MethodPost)
-	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(getNetwork))).Methods(http.MethodGet)
-	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(deleteNetwork))).Methods(http.MethodDelete)
-	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(updateNetwork))).Methods(http.MethodPut)
+	r.HandleFunc("/api/networks", logic.SecurityCheck(true, http.HandlerFunc(getNetworks))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/networks", logic.SecurityCheck(true, checkFreeTierLimits(limitChoiceNetworks, http.HandlerFunc(createNetwork)))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(getNetwork))).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(deleteNetwork))).
+		Methods(http.MethodDelete)
+	r.HandleFunc("/api/networks/{networkname}", logic.SecurityCheck(true, http.HandlerFunc(updateNetwork))).
+		Methods(http.MethodPut)
 	// ACLs
-	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(updateNetworkACL))).Methods(http.MethodPut)
-	r.HandleFunc("/api/networks/{networkname}/acls/v2", logic.SecurityCheck(true, http.HandlerFunc(updateNetworkACLv2))).Methods(http.MethodPut)
-	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(getNetworkACL))).Methods(http.MethodGet)
+	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(updateNetworkACL))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/networks/{networkname}/acls/v2", logic.SecurityCheck(true, http.HandlerFunc(updateNetworkACLv2))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/networks/{networkname}/acls", logic.SecurityCheck(true, http.HandlerFunc(getNetworkACL))).
+		Methods(http.MethodGet)
 }
 
-// swagger:route GET /api/networks networks getNetworks
-//
-// Lists all networks.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: getNetworksSliceResponse
+// @Summary     Lists all networks
+// @Router      /api/networks [get]
+// @Tags        Networks
+// @Security    oauth
+// @Produce     json
+// @Success     200 {object} models.Network
+// @Failure     500 {object} models.ErrorResponse
 func getNetworks(w http.ResponseWriter, r *http.Request) {
 
 	var err error
@@ -54,6 +58,15 @@ func getNetworks(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	if r.Header.Get("ismaster") != "yes" {
+		username := r.Header.Get("user")
+		user, err := logic.GetUser(username)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+		allnetworks = logic.FilterNetworksByRole(allnetworks, *user)
+	}
 
 	logger.Log(2, r.Header.Get("user"), "fetched networks.")
 	logic.SortNetworks(allnetworks[:])
@@ -61,17 +74,14 @@ func getNetworks(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(allnetworks)
 }
 
-// swagger:route GET /api/networks/{networkname} networks getNetwork
-//
-// Get a network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: networkBodyResponse
+// @Summary     Get a network
+// @Router      /api/networks/{networkname} [get]
+// @Tags        Networks
+// @Security    oauth
+// @Param       networkname path string true "Network name"
+// @Produce     json
+// @Success     200 {object} models.Network
+// @Failure     500 {object} models.ErrorResponse
 func getNetwork(w http.ResponseWriter, r *http.Request) {
 	// set header.
 	w.Header().Set("Content-Type", "application/json")
@@ -90,17 +100,16 @@ func getNetwork(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(network)
 }
 
-// swagger:route PUT /api/networks/{networkname}/acls networks updateNetworkACL
-//
-// Update a network ACL (Access Control List).
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: aclContainerResponse
+// @Summary     Update a network ACL (Access Control List)
+// @Router      /api/networks/{networkname}/acls [put]
+// @Tags        Networks
+// @Security    oauth
+// @Param       networkname path string true "Network name"
+// @Param       body body acls.ACLContainer true "ACL container"
+// @Produce     json
+// @Success     200 {object} acls.ACLContainer
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func updateNetworkACL(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	var params = mux.Vars(r)
@@ -140,17 +149,16 @@ func updateNetworkACL(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(newNetACL)
 }
 
-// swagger:route PUT /api/networks/{networkname}/acls/v2 networks updateNetworkACL
-//
-// Update a network ACL (Access Control List).
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: aclContainerResponse
+// @Summary     Update a network ACL (Access Control List)
+// @Router      /api/networks/{networkname}/acls/v2 [put]
+// @Tags        Networks
+// @Security    oauth
+// @Param       networkname path string true "Network name"
+// @Param       body body acls.ACLContainer true "ACL container"
+// @Produce     json
+// @Success     200 {object} acls.ACLContainer
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func updateNetworkACLv2(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	var params = mux.Vars(r)
@@ -279,13 +287,25 @@ func updateNetworkACLv2(w http.ResponseWriter, r *http.Request) {
 			client := client
 			err := logic.DeleteExtClient(client.Network, client.ClientID)
 			if err != nil {
-				slog.Error("failed to delete client during update", "client", client.ClientID, "error", err.Error())
+				slog.Error(
+					"failed to delete client during update",
+					"client",
+					client.ClientID,
+					"error",
+					err.Error(),
+				)
 				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 				return
 			}
 			err = logic.SaveExtClient(&client)
 			if err != nil {
-				slog.Error("failed to save client during update", "client", client.ClientID, "error", err.Error())
+				slog.Error(
+					"failed to save client during update",
+					"client",
+					client.ClientID,
+					"error",
+					err.Error(),
+				)
 				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 				return
 			}
@@ -310,7 +330,11 @@ func updateNetworkACLv2(w http.ResponseWriter, r *http.Request) {
 		// update ingress gateways of associated clients
 		hosts, err := logic.GetAllHosts()
 		if err != nil {
-			slog.Error("failed to fetch hosts after network ACL update. skipping publish extclients ACL", "network", netname)
+			slog.Error(
+				"failed to fetch hosts after network ACL update. skipping publish extclients ACL",
+				"network",
+				netname,
+			)
 			return
 		}
 		hostsMap := make(map[uuid.UUID]models.Host)
@@ -319,7 +343,7 @@ func updateNetworkACLv2(w http.ResponseWriter, r *http.Request) {
 		}
 		for hostId, clients := range assocClientsToDisconnectPerHost {
 			if host, ok := hostsMap[hostId]; ok {
-				if err = mq.PublishSingleHostPeerUpdate(&host, allNodes, nil, clients, false); err != nil {
+				if err = mq.PublishSingleHostPeerUpdate(&host, allNodes, nil, clients, false, nil); err != nil {
 					slog.Error("failed to publish peer update to ingress after ACL update on network", "network", netname, "host", hostId)
 				}
 			}
@@ -330,17 +354,14 @@ func updateNetworkACLv2(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(networkACLChange)
 }
 
-// swagger:route GET /api/networks/{networkname}/acls networks getNetworkACL
-//
-// Get a network ACL (Access Control List).
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: aclContainerResponse
+// @Summary     Get a network ACL (Access Control List)
+// @Router      /api/networks/{networkname}/acls [get]
+// @Tags        Networks
+// @Security    oauth
+// @Param       networkname path string true "Network name"
+// @Produce     json
+// @Success     200 {object} acls.ACLContainer
+// @Failure     500 {object} models.ErrorResponse
 func getNetworkACL(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	var params = mux.Vars(r)
@@ -364,17 +385,15 @@ func getNetworkACL(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(networkACL)
 }
 
-// swagger:route DELETE /api/networks/{networkname} networks deleteNetwork
-//
-// Delete a network.  Will not delete if there are any nodes that belong to the network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: successResponse
+// @Summary     Delete a network
+// @Router      /api/networks/{networkname} [delete]
+// @Tags        Networks
+// @Security    oauth
+// @Param       networkname path string true "Network name"
+// @Produce     json
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     403 {object} models.ErrorResponse
 func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 	// Set header
 	w.Header().Set("Content-Type", "application/json")
@@ -392,23 +411,23 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, errtype))
 		return
 	}
+	go logic.DeleteNetworkRoles(network)
+	//delete network from allocated ip map
+	go logic.RemoveNetworkFromAllocatedIpMap(network)
 
 	logger.Log(1, r.Header.Get("user"), "deleted network", network)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode("success")
 }
 
-// swagger:route POST /api/networks networks createNetwork
-//
-// Create a network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: networkBodyResponse
+// @Summary     Create a network
+// @Router      /api/networks [post]
+// @Tags        Networks
+// @Security    oauth
+// @Param       body body models.Network true "Network details"
+// @Produce     json
+// @Success     200 {object} models.Network
+// @Failure     400 {object} models.ErrorResponse
 func createNetwork(w http.ResponseWriter, r *http.Request) {
 
 	w.Header().Set("Content-Type", "application/json")
@@ -467,13 +486,25 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	logic.CreateDefaultNetworkRolesAndGroups(models.NetworkID(network.NetID))
+
+	//add new network to allocated ip map
+	go logic.AddNetworkToAllocatedIpMap(network.NetID)
+
 	go func() {
 		defaultHosts := logic.GetDefaultHosts()
 		for i := range defaultHosts {
 			currHost := &defaultHosts[i]
 			newNode, err := logic.UpdateHostNetwork(currHost, network.NetID, true)
 			if err != nil {
-				logger.Log(0, r.Header.Get("user"), "failed to add host to network:", currHost.ID.String(), network.NetID, err.Error())
+				logger.Log(
+					0,
+					r.Header.Get("user"),
+					"failed to add host to network:",
+					currHost.ID.String(),
+					network.NetID,
+					err.Error(),
+				)
 				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 				return
 			}
@@ -483,7 +514,14 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 				Host:   *currHost,
 				Node:   *newNode,
 			}); err != nil {
-				logger.Log(0, r.Header.Get("user"), "failed to add host to network:", currHost.ID.String(), network.NetID, err.Error())
+				logger.Log(
+					0,
+					r.Header.Get("user"),
+					"failed to add host to network:",
+					currHost.ID.String(),
+					network.NetID,
+					err.Error(),
+				)
 			}
 			// make  host failover
 			logic.CreateFailOver(*newNode)
@@ -501,17 +539,15 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(network)
 }
 
-// swagger:route PUT /api/networks/{networkname} networks updateNetwork
-//
-// Update pro settings for a network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: networkBodyResponse
+// @Summary     Update network settings
+// @Router      /api/networks/{networkname} [put]
+// @Tags        Networks
+// @Security    oauth
+// @Param       networkname path string true "Network name"
+// @Param       body body models.Network true "Network details"
+// @Produce     json
+// @Success     200 {object} models.Network
+// @Failure     400 {object} models.ErrorResponse
 func updateNetwork(w http.ResponseWriter, r *http.Request) {
 
 	w.Header().Set("Content-Type", "application/json")

+ 5 - 5
controllers/network_test.go

@@ -26,9 +26,9 @@ func TestMain(m *testing.M) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	logic.CreateSuperAdmin(&models.User{
-		UserName: "admin",
-		Password: "password",
-		IsAdmin:  true,
+		UserName:       "admin",
+		Password:       "password",
+		PlatformRoleID: models.SuperAdminRole,
 	})
 	peerUpdate := make(chan *models.Node)
 	go logic.ManageZombies(context.Background(), peerUpdate)
@@ -46,8 +46,8 @@ func TestCreateNetwork(t *testing.T) {
 	deleteAllNetworks()
 
 	var network models.Network
-	network.NetID = "skynet"
-	network.AddressRange = "10.0.0.1/24"
+	network.NetID = "skynet1"
+	network.AddressRange = "10.10.0.1/24"
 	// if tests break - check here (removed displayname)
 	//network.DisplayName = "mynetwork"
 

+ 202 - 155
controllers/node.go

@@ -21,8 +21,8 @@ var hostIDHeader = "host-id"
 
 func nodeHandlers(r *mux.Router) {
 
-	r.HandleFunc("/api/nodes", Authorize(false, false, "user", http.HandlerFunc(getAllNodes))).Methods(http.MethodGet)
-	r.HandleFunc("/api/nodes/{network}", Authorize(false, true, "network", http.HandlerFunc(getNetworkNodes))).Methods(http.MethodGet)
+	r.HandleFunc("/api/nodes", logic.SecurityCheck(true, http.HandlerFunc(getAllNodes))).Methods(http.MethodGet)
+	r.HandleFunc("/api/nodes/{network}", logic.SecurityCheck(true, http.HandlerFunc(getNetworkNodes))).Methods(http.MethodGet)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(getNode))).Methods(http.MethodGet)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}", logic.SecurityCheck(true, http.HandlerFunc(updateNode))).Methods(http.MethodPut)
 	r.HandleFunc("/api/nodes/{network}/{nodeid}", Authorize(true, true, "node", http.HandlerFunc(deleteNode))).Methods(http.MethodDelete)
@@ -34,17 +34,6 @@ func nodeHandlers(r *mux.Router) {
 	r.HandleFunc("/api/v1/nodes/migrate", migrate).Methods(http.MethodPost)
 }
 
-// swagger:route POST /api/nodes/adm/{network}/authenticate authenticate authenticate
-//
-// Authenticate to make further API calls related to a network.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: successResponse
 func authenticate(response http.ResponseWriter, request *http.Request) {
 
 	var authRequest models.AuthParams
@@ -95,7 +84,7 @@ func authenticate(response http.ResponseWriter, request *http.Request) {
 		errorResponse.Code = http.StatusBadRequest
 		errorResponse.Message = err.Error()
 		logger.Log(0, request.Header.Get("user"),
-			"error retrieving host: ", err.Error())
+			"error retrieving host: ", result.HostID.String(), err.Error())
 		logic.ReturnErrorResponse(response, request, errorResponse)
 		return
 	}
@@ -149,7 +138,11 @@ func authenticate(response http.ResponseWriter, request *http.Request) {
 // even if it's technically ok
 // This is kind of a poor man's RBAC. There's probably a better/smarter way.
 // TODO: Consider better RBAC implementations
-func Authorize(hostAllowed, networkCheck bool, authNetwork string, next http.Handler) http.HandlerFunc {
+func Authorize(
+	hostAllowed, networkCheck bool,
+	authNetwork string,
+	next http.Handler,
+) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var errorResponse = models.ErrorResponse{
 			Code: http.StatusForbidden, Message: logic.Forbidden_Msg,
@@ -258,17 +251,12 @@ func Authorize(hostAllowed, networkCheck bool, authNetwork string, next http.Han
 	}
 }
 
-// swagger:route GET /api/nodes/{network} nodes getNetworkNodes
-//
-// Gets all nodes associated with network including pending nodes.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeSliceResponse
+// @Summary     Gets all nodes associated with network including pending nodes
+// @Router      /api/nodes/adm/{network} [get]
+// @Securitydefinitions.oauth2.application OAuth2Application
+// @Tags        Nodes
+// @Success     200 {array} models.Node
+// @Failure     500 {object} models.ErrorResponse
 func getNetworkNodes(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	var params = mux.Vars(r)
@@ -280,6 +268,64 @@ func getNetworkNodes(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	filteredNodes := []models.Node{}
+	if r.Header.Get("ismaster") != "yes" {
+		username := r.Header.Get("user")
+		user, err := logic.GetUser(username)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+		userPlatformRole, err := logic.GetRole(user.PlatformRoleID)
+		if err != nil {
+			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+			return
+		}
+
+		if !userPlatformRole.FullAccess {
+			nodesMap := make(map[string]struct{})
+			networkRoles := user.NetworkRoles[models.NetworkID(networkName)]
+			for networkRoleID := range networkRoles {
+				userPermTemplate, err := logic.GetRole(networkRoleID)
+				if err != nil {
+					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+					return
+				}
+				if userPermTemplate.FullAccess {
+					break
+				}
+				if rsrcPerms, ok := userPermTemplate.NetworkLevelAccess[models.RemoteAccessGwRsrc]; ok {
+					if _, ok := rsrcPerms[models.AllRemoteAccessGwRsrcID]; ok {
+						for _, node := range nodes {
+							if _, ok := nodesMap[node.ID.String()]; ok {
+								continue
+							}
+							if node.IsIngressGateway {
+								nodesMap[node.ID.String()] = struct{}{}
+								filteredNodes = append(filteredNodes, node)
+							}
+						}
+					} else {
+						for gwID, scope := range rsrcPerms {
+							if _, ok := nodesMap[gwID.String()]; ok {
+								continue
+							}
+							if scope.Read {
+								gwNode, err := logic.GetNodeByID(gwID.String())
+								if err == nil && gwNode.IsIngressGateway {
+									filteredNodes = append(filteredNodes, gwNode)
+								}
+							}
+						}
+					}
+				}
+
+			}
+		}
+	}
+	if len(filteredNodes) > 0 {
+		nodes = filteredNodes
+	}
 
 	// returns all the nodes in JSON/API format
 	apiNodes := logic.GetAllNodesAPI(nodes[:])
@@ -288,36 +334,35 @@ func getNetworkNodes(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(apiNodes)
 }
 
-// swagger:route GET /api/nodes nodes getAllNodes
-//
-// Get all nodes across all networks.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeSliceResponse
-//
+// @Summary     Get all nodes across all networks
+// @Router      /api/nodes [get]
+// @Tags        Nodes
+// @Securitydefinitions.oauth2.application OAuth2Application
+// @Success     200 {array} models.ApiNode
+// @Failure     500 {object} models.ErrorResponse
 // Not quite sure if this is necessary. Probably necessary based on front end but may want to review after iteration 1 if it's being used or not
 func getAllNodes(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
-	user, err := logic.GetUser(r.Header.Get("user"))
-	if err != nil && r.Header.Get("ismasterkey") != "yes" {
-		logger.Log(0, r.Header.Get("user"),
-			"error fetching user info: ", err.Error())
+	var nodes []models.Node
+	nodes, err := logic.GetAllNodes()
+	if err != nil {
+		logger.Log(0, "error fetching all nodes info: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	var nodes []models.Node
-	if user.IsAdmin || r.Header.Get("ismasterkey") == "yes" {
-		nodes, err = logic.GetAllNodes()
+	username := r.Header.Get("user")
+	if r.Header.Get("ismaster") == "no" {
+		user, err := logic.GetUser(username)
 		if err != nil {
-			logger.Log(0, "error fetching all nodes info: ", err.Error())
-			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 			return
 		}
+		userPlatformRole, err := logic.GetRole(user.PlatformRoleID)
+		if err != nil {
+			return
+		}
+		if !userPlatformRole.FullAccess {
+			nodes = logic.GetFilteredNodesByUserAccess(*user, nodes)
+		}
 	}
 	// return all the nodes in JSON/API format
 	apiNodes := logic.GetAllNodesAPI(nodes[:])
@@ -327,17 +372,12 @@ func getAllNodes(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(apiNodes)
 }
 
-// swagger:route GET /api/nodes/{network}/{nodeid} nodes getNode
-//
-// Get an individual node.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Get an individual node
+// @Router      /api/nodes/{network}/{nodeid} [get]
+// @Tags        Nodes
+// @Security    oauth2
+// @Success     200 {object} models.NodeGet
+// @Failure     500 {object} models.ErrorResponse
 func getNode(w http.ResponseWriter, r *http.Request) {
 	// set header.
 	w.Header().Set("Content-Type", "application/json")
@@ -359,15 +399,29 @@ func getNode(w http.ResponseWriter, r *http.Request) {
 	}
 	allNodes, err := logic.GetAllNodes()
 	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("error fetching wg peers config for host [ %s ]: %v", host.ID.String(), err))
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			fmt.Sprintf(
+				"error fetching wg peers config for host [ %s ]: %v",
+				host.ID.String(),
+				err,
+			),
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
 	hostPeerUpdate, err := logic.GetPeerUpdateForHost(node.Network, host, allNodes, nil, nil)
 	if err != nil && !database.IsEmptyRecord(err) {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("error fetching wg peers config for host [ %s ]: %v", host.ID.String(), err))
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			fmt.Sprintf(
+				"error fetching wg peers config for host [ %s ]: %v",
+				host.ID.String(),
+				err,
+			),
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
@@ -388,17 +442,12 @@ func getNode(w http.ResponseWriter, r *http.Request) {
 
 // == EGRESS ==
 
-// swagger:route POST /api/nodes/{network}/{nodeid}/creategateway nodes createEgressGateway
-//
-// Create an egress gateway.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Create an egress gateway
+// @Router      /api/nodes/{network}/{nodeid}/creategateway [post]
+// @Tags        Nodes
+// @Security    oauth2
+// @Success     200 {object} models.ApiNode
+// @Failure     500 {object} models.ErrorResponse
 func createEgressGateway(w http.ResponseWriter, r *http.Request) {
 	var gateway models.EgressGatewayRequest
 	var params = mux.Vars(r)
@@ -431,7 +480,14 @@ func createEgressGateway(w http.ResponseWriter, r *http.Request) {
 	}
 
 	apiNode := node.ConvertToAPINode()
-	logger.Log(1, r.Header.Get("user"), "created egress gateway on node", gateway.NodeID, "on network", gateway.NetID)
+	logger.Log(
+		1,
+		r.Header.Get("user"),
+		"created egress gateway on node",
+		gateway.NodeID,
+		"on network",
+		gateway.NetID,
+	)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 	go func() {
@@ -442,17 +498,12 @@ func createEgressGateway(w http.ResponseWriter, r *http.Request) {
 	}()
 }
 
-// swagger:route DELETE /api/nodes/{network}/{nodeid}/deletegateway nodes deleteEgressGateway
-//
-// Delete an egress gateway.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Delete an egress gateway
+// @Router      /api/nodes/{network}/{nodeid}/deletegateway [delete]
+// @Tags        Nodes
+// @Security    oauth2
+// @Success     200 {object} models.ApiNode
+// @Failure     500 {object} models.ErrorResponse
 func deleteEgressGateway(w http.ResponseWriter, r *http.Request) {
 
 	w.Header().Set("Content-Type", "application/json")
@@ -474,7 +525,14 @@ func deleteEgressGateway(w http.ResponseWriter, r *http.Request) {
 	}
 
 	apiNode := node.ConvertToAPINode()
-	logger.Log(1, r.Header.Get("user"), "deleted egress gateway on node", nodeid, "on network", netid)
+	logger.Log(
+		1,
+		r.Header.Get("user"),
+		"deleted egress gateway on node",
+		nodeid,
+		"on network",
+		netid,
+	)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 	go func() {
@@ -487,17 +545,12 @@ func deleteEgressGateway(w http.ResponseWriter, r *http.Request) {
 
 // == INGRESS ==
 
-// swagger:route POST /api/nodes/{network}/{nodeid}/createingress nodes createIngressGateway
-//
-// Create an ingress gateway.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Create an remote access gateway
+// @Router      /api/nodes/{network}/{nodeid}/createingress [post]
+// @Tags        Nodes
+// @Security    oauth2
+// @Success     200 {object} models.ApiNode
+// @Failure     500 {object} models.ErrorResponse
 func createIngressGateway(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	w.Header().Set("Content-Type", "application/json")
@@ -520,7 +573,14 @@ func createIngressGateway(w http.ResponseWriter, r *http.Request) {
 	}
 
 	apiNode := node.ConvertToAPINode()
-	logger.Log(1, r.Header.Get("user"), "created ingress gateway on node", nodeid, "on network", netid)
+	logger.Log(
+		1,
+		r.Header.Get("user"),
+		"created ingress gateway on node",
+		nodeid,
+		"on network",
+		netid,
+	)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 	go func() {
@@ -530,17 +590,12 @@ func createIngressGateway(w http.ResponseWriter, r *http.Request) {
 	}()
 }
 
-// swagger:route DELETE /api/nodes/{network}/{nodeid}/deleteingress nodes deleteIngressGateway
-//
-// Delete an ingress gateway.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Delete an remote access gateway
+// @Router      /api/nodes/{network}/{nodeid}/deleteingress [delete]
+// @Tags        Nodes
+// @Security    oauth2
+// @Success     200 {object} models.ApiNode
+// @Failure     500 {object} models.ErrorResponse
 func deleteIngressGateway(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	var params = mux.Vars(r)
@@ -560,25 +615,6 @@ func deleteIngressGateway(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if servercfg.IsPro {
-		go func() {
-			users, err := logic.GetUsersDB()
-			if err == nil {
-				for _, user := range users {
-					if _, ok := user.RemoteGwIDs[nodeid]; ok {
-						delete(user.RemoteGwIDs, nodeid)
-						err = logic.UpsertUser(user)
-						if err != nil {
-							slog.Error("failed to get user", "user", user.UserName, "error", err)
-						}
-					}
-				}
-			} else {
-				slog.Error("failed to get users", "error", err)
-			}
-		}()
-	}
-
 	apiNode := node.ConvertToAPINode()
 	logger.Log(1, r.Header.Get("user"), "deleted ingress gateway", nodeid)
 	w.WriteHeader(http.StatusOK)
@@ -592,11 +628,17 @@ func deleteIngressGateway(w http.ResponseWriter, r *http.Request) {
 				return
 			}
 			go func() {
-				if err := mq.PublishSingleHostPeerUpdate(host, allNodes, nil, removedClients[:], false); err != nil {
+				if err := mq.PublishSingleHostPeerUpdate(host, allNodes, nil, removedClients[:], false, nil); err != nil {
 					slog.Error("publishSingleHostUpdate", "host", host.Name, "error", err)
 				}
 				if err := mq.NodeUpdate(&node); err != nil {
-					slog.Error("error publishing node update to node", "node", node.ID, "error", err)
+					slog.Error(
+						"error publishing node update to node",
+						"node",
+						node.ID,
+						"error",
+						err,
+					)
 				}
 				if servercfg.IsDNSMode() {
 					logic.SetDNS()
@@ -606,17 +648,12 @@ func deleteIngressGateway(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-// swagger:route PUT /api/nodes/{network}/{nodeid} nodes updateNode
-//
-// Update an individual node.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Update an individual node
+// @Router      /api/nodes/{network}/{nodeid} [put]
+// @Tags        Nodes
+// @Security    oauth2
+// @Success     200 {object} models.ApiNode
+// @Failure     500 {object} models.ErrorResponse
 func updateNode(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 
@@ -642,7 +679,11 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 	}
 	newNode := newData.ConvertToServerNode(&currentNode)
 	if newNode == nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("error converting node"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(fmt.Errorf("error converting node"), "badrequest"),
+		)
 		return
 	}
 	if newNode.IsInternetGateway != currentNode.IsInternetGateway {
@@ -686,7 +727,14 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 	}
 
 	apiNode := newNode.ConvertToAPINode()
-	logger.Log(1, r.Header.Get("user"), "updated node", currentNode.ID.String(), "on network", currentNode.Network)
+	logger.Log(
+		1,
+		r.Header.Get("user"),
+		"updated node",
+		currentNode.ID.String(),
+		"on network",
+		currentNode.Network,
+	)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 	go func(aclUpdate, relayupdate bool, newNode *models.Node) {
@@ -704,17 +752,12 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
 	}(aclUpdate, relayUpdate, newNode)
 }
 
-// swagger:route DELETE /api/nodes/{network}/{nodeid} nodes deleteNode
-//
-// Delete an individual node.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Delete an individual node
+// @Router      /api/nodes/{network}/{nodeid} [delete]
+// @Tags        Nodes
+// @Security    oauth2
+// @Success     200 {string} string "Node deleted."
+// @Failure     500 {object} models.ErrorResponse
 func deleteNode(w http.ResponseWriter, r *http.Request) {
 	// Set header
 	w.Header().Set("Content-Type", "application/json")
@@ -735,7 +778,11 @@ func deleteNode(w http.ResponseWriter, r *http.Request) {
 	}
 	purge := forceDelete || fromNode
 	if err := logic.DeleteNode(&node, purge); err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete node"), "internal"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(fmt.Errorf("failed to delete node"), "internal"),
+		)
 		return
 	}
 

+ 19 - 36
controllers/server.go

@@ -38,10 +38,10 @@ func serverHandlers(r *mux.Router) {
 	).Methods(http.MethodPost)
 	r.HandleFunc("/api/server/getconfig", allowUsers(http.HandlerFunc(getConfig))).
 		Methods(http.MethodGet)
-	r.HandleFunc("/api/server/getserverinfo", Authorize(true, false, "node", http.HandlerFunc(getServerInfo))).
+	r.HandleFunc("/api/server/getserverinfo", logic.SecurityCheck(true, http.HandlerFunc(getServerInfo))).
 		Methods(http.MethodGet)
 	r.HandleFunc("/api/server/status", getStatus).Methods(http.MethodGet)
-	r.HandleFunc("/api/server/usage", Authorize(true, false, "user", http.HandlerFunc(getUsage))).
+	r.HandleFunc("/api/server/usage", logic.SecurityCheck(false, http.HandlerFunc(getUsage))).
 		Methods(http.MethodGet)
 }
 
@@ -102,18 +102,12 @@ func getUsage(w http.ResponseWriter, _ *http.Request) {
 	})
 }
 
-// swagger:route GET /api/server/status server getStatus
-//
-// Get the server configuration.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: serverConfigResponse
+// @Summary     Get the server status
+// @Router      /api/server/status [get]
+// @Tags        Server
+// @Security    oauth2
 func getStatus(w http.ResponseWriter, r *http.Request) {
+	// @Success     200 {object} status
 	type status struct {
 		DB               bool      `json:"db_connected"`
 		Broker           bool      `json:"broker_connected"`
@@ -131,7 +125,8 @@ func getStatus(w http.ResponseWriter, r *http.Request) {
 	var trialEndDate time.Time
 	var err error
 	isOnTrial := false
-	if servercfg.IsPro && (servercfg.GetLicenseKey() == "" || servercfg.GetNetmakerTenantID() == "") {
+	if servercfg.IsPro &&
+		(servercfg.GetLicenseKey() == "" || servercfg.GetNetmakerTenantID() == "") {
 		trialEndDate, err = logic.GetTrialEndDate()
 		if err != nil {
 			slog.Error("failed to get trial end date", "error", err)
@@ -177,17 +172,11 @@ func allowUsers(next http.Handler) http.HandlerFunc {
 	}
 }
 
-// swagger:route GET /api/server/getserverinfo server getServerInfo
-//
-// Get the server configuration.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: serverConfigResponse
+// @Summary     Get the server information
+// @Router      /api/server/getserverinfo [get]
+// @Tags        Server
+// @Security    oauth2
+// @Success     200 {object} models.ServerConfig
 func getServerInfo(w http.ResponseWriter, r *http.Request) {
 	// Set header
 	w.Header().Set("Content-Type", "application/json")
@@ -198,17 +187,11 @@ func getServerInfo(w http.ResponseWriter, r *http.Request) {
 	// w.WriteHeader(http.StatusOK)
 }
 
-// swagger:route GET /api/server/getconfig server getConfig
-//
-// Get the server configuration.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: serverConfigResponse
+// @Summary     Get the server configuration
+// @Router      /api/server/getconfig [get]
+// @Tags        Server
+// @Security    oauth2
+// @Success     200 {object} config.ServerConfig
 func getConfig(w http.ResponseWriter, r *http.Request) {
 	// Set header
 	w.Header().Set("Content-Type", "application/json")

+ 275 - 256
controllers/user.go

@@ -5,11 +5,12 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"net/url"
+	"reflect"
 
 	"github.com/gorilla/mux"
 	"github.com/gorilla/websocket"
 	"github.com/gravitl/netmaker/auth"
-	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
@@ -22,34 +23,33 @@ var (
 	upgrader = websocket.Upgrader{}
 )
 
+var ListRoles = listRoles
+
 func userHandlers(r *mux.Router) {
 	r.HandleFunc("/api/users/adm/hassuperadmin", hasSuperAdmin).Methods(http.MethodGet)
 	r.HandleFunc("/api/users/adm/createsuperadmin", createSuperAdmin).Methods(http.MethodPost)
-	r.HandleFunc("/api/users/adm/transfersuperadmin/{username}", logic.SecurityCheck(true, http.HandlerFunc(transferSuperAdmin))).Methods(http.MethodPost)
+	r.HandleFunc("/api/users/adm/transfersuperadmin/{username}", logic.SecurityCheck(true, http.HandlerFunc(transferSuperAdmin))).
+		Methods(http.MethodPost)
 	r.HandleFunc("/api/users/adm/authenticate", authenticateUser).Methods(http.MethodPost)
 	r.HandleFunc("/api/users/{username}", logic.SecurityCheck(true, http.HandlerFunc(updateUser))).Methods(http.MethodPut)
 	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(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUser)))).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_pending", logic.SecurityCheck(true, http.HandlerFunc(getPendingUsers))).Methods(http.MethodGet)
-	r.HandleFunc("/api/users_pending", logic.SecurityCheck(true, http.HandlerFunc(deleteAllPendingUsers))).Methods(http.MethodDelete)
-	r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(deletePendingUser))).Methods(http.MethodDelete)
-	r.HandleFunc("/api/users_pending/user/{username}", logic.SecurityCheck(true, http.HandlerFunc(approvePendingUser))).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/users/roles", logic.SecurityCheck(true, http.HandlerFunc(ListRoles))).Methods(http.MethodGet)
 
 }
 
-// swagger:route POST /api/users/adm/authenticate authenticate authenticateUser
-//
-// User authenticates using its password and retrieves a JWT for authorization.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: successResponse
+// @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
@@ -60,7 +60,11 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 	}
 
 	if !servercfg.IsBasicAuthEnabled() {
-		logic.ReturnErrorResponse(response, request, logic.FormatError(fmt.Errorf("basic auth is disabled"), "badrequest"))
+		logic.ReturnErrorResponse(
+			response,
+			request,
+			logic.FormatError(fmt.Errorf("basic auth is disabled"), "badrequest"),
+		)
 		return
 	}
 
@@ -82,10 +86,24 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 			logic.ReturnErrorResponse(response, request, logic.FormatError(err, "unauthorized"))
 			return
 		}
-		if !(user.IsAdmin || user.IsSuperAdmin) {
-			logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("only admins can access dashboard"), "unauthorized"))
+		role, err := logic.GetRole(user.PlatformRoleID)
+		if err != nil {
+			logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
 			return
 		}
+		if role.DenyDashboardAccess {
+			logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
+			return
+		}
+	}
+	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
 	jwt, err := logic.VerifyAuthRequest(authRequest)
@@ -99,7 +117,11 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 	if jwt == "" {
 		// very unlikely that err is !nil and no jwt returned, but handle it anyways.
 		logger.Log(0, username, "jwt token is empty")
-		logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("no token returned"), "internal"))
+		logic.ReturnErrorResponse(
+			response,
+			request,
+			logic.FormatError(errors.New("no token returned"), "internal"),
+		)
 		return
 	}
 
@@ -133,9 +155,19 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 			}
 			for _, client := range clients {
 				if client.OwnerID == username && !client.Enabled {
-					slog.Info(fmt.Sprintf("enabling ext client %s for user %s due to RAC autodisabling feature", client.ClientID, client.OwnerID))
+					slog.Info(
+						fmt.Sprintf(
+							"enabling ext client %s for user %s due to RAC autodisabling feature",
+							client.ClientID,
+							client.OwnerID,
+						),
+					)
 					if newClient, err := logic.ToggleExtClientConnectivity(&client, true); err != nil {
-						slog.Error("error enabling ext client in RAC autodisable hook", "error", err)
+						slog.Error(
+							"error enabling ext client in RAC autodisable hook",
+							"error",
+							err,
+						)
 						continue // dont return but try for other clients
 					} else {
 						// publish peer update to ingress gateway
@@ -151,17 +183,11 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
 	}()
 }
 
-// swagger:route GET /api/users/adm/hassuperadmin user hasSuperAdmin
-//
-// Checks whether the server has an admin.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: hasAdmin
+// @Summary     Check if the server has a super admin
+// @Router      /api/users/adm/hassuperadmin [get]
+// @Tags        Users
+// @Success     200 {object} bool
+// @Failure     500 {object} models.ErrorResponse
 func hasSuperAdmin(w http.ResponseWriter, r *http.Request) {
 
 	w.Header().Set("Content-Type", "application/json")
@@ -177,9 +203,32 @@ func hasSuperAdmin(w http.ResponseWriter, r *http.Request) {
 
 }
 
-// swagger:route GET /api/users/{username} user getUser
+// @Summary     Get an individual user
+// @Router      /api/users/{username} [get]
+// @Tags        Users
+// @Param       username path string true "Username of the user to fetch"
+// @Success     200 {object} models.User
+// @Failure     500 {object} models.ErrorResponse
+func getUser(w http.ResponseWriter, r *http.Request) {
+	// set header.
+	w.Header().Set("Content-Type", "application/json")
+
+	var params = mux.Vars(r)
+	usernameFetched := params["username"]
+	user, err := logic.GetReturnUser(usernameFetched)
+
+	if err != nil {
+		logger.Log(0, usernameFetched, "failed to fetch user: ", err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	logger.Log(2, r.Header.Get("user"), "fetched user", usernameFetched)
+	json.NewEncoder(w).Encode(user)
+}
+
+// swagger:route GET /api/v1/users user getUserV1
 //
-// Get an individual user.
+// Get an individual user with role info.
 //
 //			Schemes: https
 //
@@ -187,22 +236,32 @@ func hasSuperAdmin(w http.ResponseWriter, r *http.Request) {
 //	  		oauth
 //
 //			Responses:
-//				200: userBodyResponse
-func getUser(w http.ResponseWriter, r *http.Request) {
+//				200: ReturnUserWithRolesAndGroups
+func getUserV1(w http.ResponseWriter, r *http.Request) {
 	// set header.
 	w.Header().Set("Content-Type", "application/json")
-
-	var params = mux.Vars(r)
-	usernameFetched := params["username"]
+	usernameFetched, _ := url.QueryUnescape(r.URL.Query().Get("username"))
+	if usernameFetched == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), "badrequest"))
+		return
+	}
 	user, err := logic.GetReturnUser(usernameFetched)
-
 	if err != nil {
 		logger.Log(0, usernameFetched, "failed to fetch user: ", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
+	userRoleTemplate, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	resp := models.ReturnUserWithRolesAndGroups{
+		ReturnUser:   user,
+		PlatformRole: userRoleTemplate,
+	}
 	logger.Log(2, r.Header.Get("user"), "fetched user", usernameFetched)
-	json.NewEncoder(w).Encode(user)
+	logic.ReturnSuccessResponseWithJson(w, r, resp, "fetched user with role info")
 }
 
 // swagger:route GET /api/users user getUsers
@@ -233,17 +292,13 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(users)
 }
 
-// swagger:route POST /api/users/adm/createsuperadmin user createAdmin
-//
-// Make a user an admin.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: userBodyResponse
+// @Summary     Create a super admin
+// @Router      /api/users/adm/createsuperadmin [post]
+// @Tags        Users
+// @Param       body body models.User true "User details"
+// @Success     200 {object} models.User
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func createSuperAdmin(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 
@@ -257,7 +312,11 @@ func createSuperAdmin(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if !servercfg.IsBasicAuthEnabled() {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("basic auth is disabled"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(fmt.Errorf("basic auth is disabled"), "badrequest"),
+		)
 		return
 	}
 
@@ -271,24 +330,20 @@ func createSuperAdmin(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(logic.ToReturnUser(u))
 }
 
-// swagger:route POST /api/users/adm/transfersuperadmin user transferSuperAdmin
-//
-// Transfers superadmin role to an admin user.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: userBodyResponse
+// @Summary     Transfer super admin role to another admin user
+// @Router      /api/users/adm/transfersuperadmin/{username} [post]
+// @Tags        Users
+// @Param       username path string true "Username of the user to transfer super admin role"
+// @Success     200 {object} models.User
+// @Failure     403 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func transferSuperAdmin(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	caller, err := logic.GetUser(r.Header.Get("user"))
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 	}
-	if !caller.IsSuperAdmin {
+	if caller.PlatformRoleID != models.SuperAdminRole {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only superadmin can assign the superadmin role to another user"), "forbidden"))
 		return
 	}
@@ -300,25 +355,27 @@ func transferSuperAdmin(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-	if !u.IsAdmin {
+	if u.PlatformRoleID != models.AdminRole {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only admins can be promoted to superadmin role"), "forbidden"))
 		return
 	}
 	if !servercfg.IsBasicAuthEnabled() {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("basic auth is disabled"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(fmt.Errorf("basic auth is disabled"), "badrequest"),
+		)
 		return
 	}
 
-	u.IsSuperAdmin = true
-	u.IsAdmin = false
+	u.PlatformRoleID = models.SuperAdminRole
 	err = logic.UpsertUser(*u)
 	if err != nil {
 		slog.Error("error updating user to superadmin: ", "user", u.UserName, "error", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	caller.IsSuperAdmin = false
-	caller.IsAdmin = true
+	caller.PlatformRoleID = models.AdminRole
 	err = logic.UpsertUser(*caller)
 	if err != nil {
 		slog.Error("error demoting user to admin: ", "user", caller.UserName, "error", err.Error())
@@ -329,22 +386,20 @@ func transferSuperAdmin(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*u))
 }
 
-// swagger:route POST /api/users/{username} user createUser
-//
-// Create a user.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: userBodyResponse
+// @Summary     Create a user
+// @Router      /api/users/{username} [post]
+// @Tags        Users
+// @Param       username path string true "Username of the user to create"
+// @Param       body body models.User true "User details"
+// @Success     200 {object} models.User
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     403 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func createUser(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	caller, err := logic.GetUser(r.Header.Get("user"))
 	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
 	var user models.User
@@ -355,19 +410,36 @@ func createUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-	if !caller.IsSuperAdmin && user.IsAdmin {
-		err = errors.New("only superadmin can create admin users")
+	if !servercfg.IsPro {
+		user.PlatformRoleID = models.AdminRole
+	}
+
+	if user.PlatformRoleID == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("platform role is missing"), "badrequest"))
+		return
+	}
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		err = errors.New("error fetching role " + user.PlatformRoleID.String() + " " + err.Error())
 		slog.Error("error creating new user: ", "user", user.UserName, "error", err)
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
-	if user.IsSuperAdmin {
+	if userRole.ID == models.SuperAdminRole {
 		err = errors.New("additional superadmins cannot be created")
 		slog.Error("error creating new user: ", "user", user.UserName, "error", err)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
 		return
 	}
-	if !servercfg.IsPro && !user.IsAdmin {
+
+	if caller.PlatformRoleID != models.SuperAdminRole && user.PlatformRoleID == models.AdminRole {
+		err = errors.New("only superadmin can create admin users")
+		slog.Error("error creating new user: ", "user", user.UserName, "error", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+		return
+	}
+
+	if !servercfg.IsPro && user.PlatformRoleID != models.AdminRole {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("non-admins users can only be created on Pro version"), "forbidden"))
 		return
 	}
@@ -378,21 +450,21 @@ func createUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
+	logic.DeleteUserInvite(user.UserName)
+	logic.DeletePendingUser(user.UserName)
 	slog.Info("user was created", "username", user.UserName)
 	json.NewEncoder(w).Encode(logic.ToReturnUser(user))
 }
 
-// swagger:route PUT /api/users/{username} user updateUser
-//
-// Update a user.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: userBodyResponse
+// @Summary     Update a user
+// @Router      /api/users/{username} [put]
+// @Tags        Users
+// @Param       username path string true "Username of the user to update"
+// @Param       body body models.User true "User details"
+// @Success     200 {object} models.User
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     403 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func updateUser(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	var params = mux.Vars(r)
@@ -426,7 +498,14 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	if user.UserName != userchange.UserName {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user in param and request body not matching"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("user in param and request body not matching"),
+				"badrequest",
+			),
+		)
 		return
 	}
 	selfUpdate := false
@@ -435,46 +514,71 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if !ismaster && !selfUpdate {
-		if caller.IsAdmin && user.IsSuperAdmin {
+		if caller.PlatformRoleID == models.AdminRole && user.PlatformRoleID == models.SuperAdminRole {
 			slog.Error("non-superadmin user", "caller", caller.UserName, "attempted to update superadmin user", username)
 			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update superadmin user"), "forbidden"))
 			return
 		}
-		if !caller.IsAdmin && !caller.IsSuperAdmin {
+		if caller.PlatformRoleID != models.AdminRole && caller.PlatformRoleID != models.SuperAdminRole {
 			slog.Error("operation not allowed", "caller", caller.UserName, "attempted to update user", username)
 			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("cannot update superadmin user"), "forbidden"))
 			return
 		}
-		if caller.IsAdmin && user.IsAdmin {
+		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"))
 			return
 		}
-		if caller.IsAdmin && userchange.IsAdmin {
+		if caller.PlatformRoleID == models.AdminRole && userchange.PlatformRoleID == models.AdminRole {
 			err = errors.New("admin user cannot update role of an another user to admin")
-			slog.Error("failed to update user", "caller", caller.UserName, "attempted to update user", username, "error", err)
+			slog.Error(
+				"failed to update user",
+				"caller",
+				caller.UserName,
+				"attempted to update user",
+				username,
+				"error",
+				err,
+			)
 			logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
 			return
 		}
 
 	}
 	if !ismaster && selfUpdate {
-		if user.IsAdmin != userchange.IsAdmin || user.IsSuperAdmin != userchange.IsSuperAdmin {
+		if user.PlatformRoleID != userchange.PlatformRoleID {
 			slog.Error("user cannot change his own role", "caller", caller.UserName, "attempted to update user role", username)
 			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user not allowed to self assign role"), "forbidden"))
 			return
 
 		}
+		if servercfg.IsPro {
+			// user cannot update his own roles and groups
+			if len(user.NetworkRoles) != len(userchange.NetworkRoles) || !reflect.DeepEqual(user.NetworkRoles, userchange.NetworkRoles) {
+				err = errors.New("user cannot update self update their network roles")
+				slog.Error("failed to update user", "caller", caller.UserName, "attempted to update user", username, "error", err)
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+				return
+			}
+			// user cannot update his own roles and groups
+			if len(user.UserGroups) != len(userchange.UserGroups) || !reflect.DeepEqual(user.UserGroups, userchange.UserGroups) {
+				err = errors.New("user cannot update self update their groups")
+				slog.Error("failed to update user", "caller", caller.UserName, "attempted to update user", username, "error", err)
+				logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
+				return
+			}
+		}
+
 	}
 	if ismaster {
-		if !user.IsSuperAdmin && userchange.IsSuperAdmin {
+		if user.PlatformRoleID != models.SuperAdminRole && userchange.PlatformRoleID == models.SuperAdminRole {
 			slog.Error("operation not allowed", "caller", logic.MasterUser, "attempted to update user role to superadmin", username)
 			logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("attempted to update user role to superadmin"), "forbidden"))
 			return
 		}
 	}
 
-	if auth.IsOauthUser(user) == nil && userchange.Password != "" {
+	if logic.IsOauthUser(user) == nil && userchange.Password != "" {
 		err := fmt.Errorf("cannot update user's password for an oauth user %s", username)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "forbidden"))
 		return
@@ -491,17 +595,12 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
 }
 
-// swagger:route DELETE /api/users/{username} user deleteUser
-//
-// Delete a user.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: userBodyResponse
+// @Summary     Delete a user
+// @Router      /api/users/{username} [delete]
+// @Tags        Users
+// @Param       username path string true "Username of the user to delete"
+// @Success     200 {string} string
+// @Failure     500 {object} models.ErrorResponse
 func deleteUser(w http.ResponseWriter, r *http.Request) {
 	// Set header
 	w.Header().Set("Content-Type", "application/json")
@@ -512,6 +611,12 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 	}
+	callerUserRole, err := logic.GetRole(caller.PlatformRoleID)
+	if err != nil {
+		slog.Error("failed to get role ", "role", callerUserRole.ID, "error", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
 	username := params["username"]
 	user, err := logic.GetUser(username)
 	if err != nil {
@@ -520,17 +625,39 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
-	if user.IsSuperAdmin {
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		slog.Error("failed to get role ", "role", userRole.ID, "error", err)
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		return
+	}
+	if userRole.ID == models.SuperAdminRole {
 		slog.Error(
 			"failed to delete user: ", "user", username, "error", "superadmin cannot be deleted")
-		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("superadmin cannot be deleted"), "internal"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(fmt.Errorf("superadmin cannot be deleted"), "internal"),
+		)
 		return
 	}
-	if !caller.IsSuperAdmin {
-		if caller.IsAdmin && user.IsAdmin {
+	if callerUserRole.ID != models.SuperAdminRole {
+		if callerUserRole.ID == models.AdminRole && userRole.ID == models.AdminRole {
 			slog.Error(
-				"failed to delete user: ", "user", username, "error", "admin cannot delete another admin user, including oneself")
-			logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("admin cannot delete another admin user, including oneself"), "internal"))
+				"failed to delete user: ",
+				"user",
+				username,
+				"error",
+				"admin cannot delete another admin user, including oneself",
+			)
+			logic.ReturnErrorResponse(
+				w,
+				r,
+				logic.FormatError(
+					fmt.Errorf("admin cannot delete another admin user, including oneself"),
+					"internal",
+				),
+			)
 			return
 		}
 	}
@@ -555,10 +682,14 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
 		}
 		for _, extclient := range extclients {
 			if extclient.OwnerID == user.UserName {
-				err = logic.DeleteExtClient(extclient.Network, extclient.ClientID)
+				err = logic.DeleteExtClientAndCleanup(extclient)
 				if err != nil {
 					slog.Error("failed to delete extclient",
-						"id", extclient.ClientID, "owner", user.UserName, "error", err)
+						"id", extclient.ClientID, "owner", username, "error", err)
+				} else {
+					if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
+						slog.Error("error setting ext peers: " + err.Error())
+					}
 				}
 			}
 		}
@@ -586,135 +717,23 @@ func socketHandler(w http.ResponseWriter, r *http.Request) {
 	go auth.SessionHandler(conn)
 }
 
-// swagger:route GET /api/users_pending user getPendingUsers
-//
-// Get all pending users.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: userBodyResponse
-func getPendingUsers(w http.ResponseWriter, r *http.Request) {
-	// set header.
-	w.Header().Set("Content-Type", "application/json")
-
-	users, err := logic.ListPendingUsers()
-	if err != nil {
-		logger.Log(0, "failed to fetch users: ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-
-	logic.SortUsers(users[:])
-	logger.Log(2, r.Header.Get("user"), "fetched pending users")
-	json.NewEncoder(w).Encode(users)
-}
-
-// swagger:route POST /api/users_pending/user/{username} user approvePendingUser
-//
-// approve pending user.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: userBodyResponse
-func approvePendingUser(w http.ResponseWriter, r *http.Request) {
-	// set header.
-	w.Header().Set("Content-Type", "application/json")
-	var params = mux.Vars(r)
-	username := params["username"]
-	users, err := logic.ListPendingUsers()
-
-	if err != nil {
-		logger.Log(0, "failed to fetch users: ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
-		return
-	}
-	for _, user := range users {
-		if user.UserName == username {
-			var newPass, fetchErr = auth.FetchPassValue("")
-			if fetchErr != nil {
-				logic.ReturnErrorResponse(w, r, logic.FormatError(fetchErr, "internal"))
-				return
-			}
-			if err = logic.CreateUser(&models.User{
-				UserName: user.UserName,
-				Password: newPass,
-			}); err != nil {
-				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to create user: %s", err), "internal"))
-				return
-			}
-			err = logic.DeletePendingUser(username)
-			if err != nil {
-				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete pending user: %s", err), "internal"))
-				return
-			}
-			break
-		}
-	}
-	logic.ReturnSuccessResponse(w, r, "approved "+username)
-}
-
-// swagger:route DELETE /api/users_pending/user/{username} user deletePendingUser
-//
-// delete pending user.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: userBodyResponse
-func deletePendingUser(w http.ResponseWriter, r *http.Request) {
-	// set header.
-	w.Header().Set("Content-Type", "application/json")
-	var params = mux.Vars(r)
-	username := params["username"]
-	users, err := logic.ListPendingUsers()
-
+// @Summary     lists all user roles.
+// @Router      /api/v1/user/roles [get]
+// @Tags        Users
+// @Param       role_id param string true "roleid required to get the role details"
+// @Success     200 {object}  []models.UserRolePermissionTemplate
+// @Failure     500 {object} models.ErrorResponse
+func listRoles(w http.ResponseWriter, r *http.Request) {
+	var roles []models.UserRolePermissionTemplate
+	var err error
+	roles, err = logic.ListPlatformRoles()
 	if err != nil {
-		logger.Log(0, "failed to fetch users: ", err.Error())
-		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+		logic.ReturnErrorResponse(w, r, models.ErrorResponse{
+			Code:    http.StatusInternalServerError,
+			Message: err.Error(),
+		})
 		return
 	}
-	for _, user := range users {
-		if user.UserName == username {
-			err = logic.DeletePendingUser(username)
-			if err != nil {
-				logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to delete pending user: %s", err), "internal"))
-				return
-			}
-			break
-		}
-	}
-	logic.ReturnSuccessResponse(w, r, "deleted pending "+username)
-}
 
-// swagger:route DELETE /api/users_pending/{username}/pending user deleteAllPendingUsers
-//
-// delete all pending users.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: userBodyResponse
-func deleteAllPendingUsers(w http.ResponseWriter, r *http.Request) {
-	// set header.
-	w.Header().Set("Content-Type", "application/json")
-	err := database.DeleteAllRecords(database.PENDING_USERS_TABLE_NAME)
-	if err != nil {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to delete all pending users "+err.Error()), "internal"))
-		return
-	}
-	logic.ReturnSuccessResponse(w, r, "cleared all pending users")
+	logic.ReturnSuccessResponseWithJson(w, r, roles, "successfully fetched user roles permission templates")
 }

+ 328 - 342
controllers/user_test.go

@@ -1,375 +1,361 @@
 package controller
 
-import (
-	"bytes"
-	"io"
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/go-jose/go-jose/v3/json"
-	"github.com/gorilla/mux"
-
-	"github.com/stretchr/testify/assert"
-
-	"github.com/gravitl/netmaker/logic"
-	"github.com/gravitl/netmaker/models"
-)
+// TODO: Need Update Tests for New User Mgmt
+// func deleteAllUsers(t *testing.T) {
+// 	t.Helper()
+// 	users, _ := logic.GetUsers()
+// 	for _, user := range users {
+// 		if _, err := logic.DeleteUser(user.UserName); err != nil {
+// 			t.Fatal(err)
+// 		}
+// 	}
+// }
 
-func deleteAllUsers(t *testing.T) {
-	t.Helper()
-	users, _ := logic.GetUsers()
-	for _, user := range users {
-		if _, err := logic.DeleteUser(user.UserName); err != nil {
-			t.Fatal(err)
-		}
-	}
-}
+// func TestGetUserNoHashedPassword(t *testing.T) {
+// 	// prepare existing user base
+// 	user := models.User{UserName: "freddie", Password: "password"}
+// 	haveOnlyOneUser(t, user)
 
-func TestGetUserNoHashedPassword(t *testing.T) {
-	// prepare existing user base
-	user := models.User{UserName: "freddie", Password: "password"}
-	haveOnlyOneUser(t, user)
+// 	// prepare request
+// 	rec, req := prepareUserRequest(t, models.User{}, user.UserName)
 
-	// prepare request
-	rec, req := prepareUserRequest(t, models.User{}, user.UserName)
+// 	// test response
+// 	getUser(rec, req)
+// 	assertUserNameButNoPassword(t, rec.Body, user.UserName)
+// }
 
-	// test response
-	getUser(rec, req)
-	assertUserNameButNoPassword(t, rec.Body, user.UserName)
-}
+// func TestCreateAdminNoHashedPassword(t *testing.T) {
+// 	// prepare existing user base
+// 	deleteAllUsers(t)
 
-func TestCreateAdminNoHashedPassword(t *testing.T) {
-	// prepare existing user base
-	deleteAllUsers(t)
+// 	// prepare request
+// 	user := models.User{UserName: "jonathan", Password: "password"}
+// 	rec, req := prepareUserRequest(t, user, "")
 
-	// prepare request
-	user := models.User{UserName: "jonathan", Password: "password"}
-	rec, req := prepareUserRequest(t, user, "")
+// 	// test response
+// 	createSuperAdmin(rec, req)
+// 	assertUserNameButNoPassword(t, rec.Body, user.UserName)
+// }
 
-	// test response
-	createSuperAdmin(rec, req)
-	assertUserNameButNoPassword(t, rec.Body, user.UserName)
-}
+// func prepareUserRequest(t *testing.T, userForBody models.User, userNameForParam string) (*httptest.ResponseRecorder, *http.Request) {
+// 	bits, err := json.Marshal(userForBody)
+// 	assert.Nil(t, err)
+// 	body := bytes.NewReader(bits)
+// 	rec := httptest.NewRecorder()
+// 	req := httptest.NewRequest("ANY", "https://example.com", body) // only the body matters here
+// 	req = mux.SetURLVars(req, map[string]string{"username": userNameForParam})
+// 	req.Header.Set("user", userForBody.UserName)
+// 	return rec, req
+// }
 
-func prepareUserRequest(t *testing.T, userForBody models.User, userNameForParam string) (*httptest.ResponseRecorder, *http.Request) {
-	bits, err := json.Marshal(userForBody)
-	assert.Nil(t, err)
-	body := bytes.NewReader(bits)
-	rec := httptest.NewRecorder()
-	req := httptest.NewRequest("ANY", "https://example.com", body) // only the body matters here
-	req = mux.SetURLVars(req, map[string]string{"username": userNameForParam})
-	req.Header.Set("user", userForBody.UserName)
-	return rec, req
-}
+// func haveOnlyOneUser(t *testing.T, user models.User) {
+// 	deleteAllUsers(t)
+// 	var err error
+// 	if user.PlatformRoleID == models.SuperAdminRole {
+// 		err = logic.CreateSuperAdmin(&user)
+// 	} else {
+// 		err = logic.CreateUser(&user)
+// 	}
+// 	assert.Nil(t, err)
+// }
 
-func haveOnlyOneUser(t *testing.T, user models.User) {
-	deleteAllUsers(t)
-	var err error
-	if user.IsSuperAdmin {
-		err = logic.CreateSuperAdmin(&user)
-	} else {
-		err = logic.CreateUser(&user)
-	}
-	assert.Nil(t, err)
-}
+// func assertUserNameButNoPassword(t *testing.T, r io.Reader, userName string) {
+// 	var resp models.User
+// 	err := json.NewDecoder(r).Decode(&resp)
+// 	assert.Nil(t, err)
+// 	assert.Equal(t, userName, resp.UserName)
+// 	assert.Empty(t, resp.Password)
+// }
 
-func assertUserNameButNoPassword(t *testing.T, r io.Reader, userName string) {
-	var resp models.User
-	err := json.NewDecoder(r).Decode(&resp)
-	assert.Nil(t, err)
-	assert.Equal(t, userName, resp.UserName)
-	assert.Empty(t, resp.Password)
-}
+// func TestHasSuperAdmin(t *testing.T) {
+// 	// delete all current users
+// 	users, _ := logic.GetUsers()
+// 	for _, user := range users {
+// 		success, err := logic.DeleteUser(user.UserName)
+// 		assert.Nil(t, err)
+// 		assert.True(t, success)
+// 	}
+// 	t.Run("NoUser", func(t *testing.T) {
+// 		found, err := logic.HasSuperAdmin()
+// 		assert.Nil(t, err)
+// 		assert.False(t, found)
+// 	})
+// 	t.Run("No superadmin user", func(t *testing.T) {
+// 		var user = models.User{UserName: "nosuperadmin", Password: "password"}
+// 		err := logic.CreateUser(&user)
+// 		assert.Nil(t, err)
+// 		found, err := logic.HasSuperAdmin()
+// 		assert.Nil(t, err)
+// 		assert.False(t, found)
+// 	})
+// 	t.Run("superadmin user", func(t *testing.T) {
+// 		var user = models.User{UserName: "superadmin", Password: "password", PlatformRoleID: models.SuperAdminRole}
+// 		err := logic.CreateUser(&user)
+// 		assert.Nil(t, err)
+// 		found, err := logic.HasSuperAdmin()
+// 		assert.Nil(t, err)
+// 		assert.True(t, found)
+// 	})
+// 	t.Run("multiple superadmins", func(t *testing.T) {
+// 		var user = models.User{UserName: "superadmin1", Password: "password", PlatformRoleID: models.SuperAdminRole}
+// 		err := logic.CreateUser(&user)
+// 		assert.Nil(t, err)
+// 		found, err := logic.HasSuperAdmin()
+// 		assert.Nil(t, err)
+// 		assert.True(t, found)
+// 	})
+// }
 
-func TestHasSuperAdmin(t *testing.T) {
-	// delete all current users
-	users, _ := logic.GetUsers()
-	for _, user := range users {
-		success, err := logic.DeleteUser(user.UserName)
-		assert.Nil(t, err)
-		assert.True(t, success)
-	}
-	t.Run("NoUser", func(t *testing.T) {
-		found, err := logic.HasSuperAdmin()
-		assert.Nil(t, err)
-		assert.False(t, found)
-	})
-	t.Run("No superadmin user", func(t *testing.T) {
-		var user = models.User{UserName: "nosuperadmin", Password: "password"}
-		err := logic.CreateUser(&user)
-		assert.Nil(t, err)
-		found, err := logic.HasSuperAdmin()
-		assert.Nil(t, err)
-		assert.False(t, found)
-	})
-	t.Run("superadmin user", func(t *testing.T) {
-		var user = models.User{UserName: "superadmin", Password: "password", IsSuperAdmin: true}
-		err := logic.CreateUser(&user)
-		assert.Nil(t, err)
-		found, err := logic.HasSuperAdmin()
-		assert.Nil(t, err)
-		assert.True(t, found)
-	})
-	t.Run("multiple superadmins", func(t *testing.T) {
-		var user = models.User{UserName: "superadmin1", Password: "password", IsSuperAdmin: true}
-		err := logic.CreateUser(&user)
-		assert.Nil(t, err)
-		found, err := logic.HasSuperAdmin()
-		assert.Nil(t, err)
-		assert.True(t, found)
-	})
-}
+// func TestCreateUser(t *testing.T) {
+// 	deleteAllUsers(t)
+// 	user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
+// 	t.Run("NoUser", func(t *testing.T) {
+// 		err := logic.CreateUser(&user)
+// 		assert.Nil(t, err)
+// 	})
+// 	t.Run("UserExists", func(t *testing.T) {
+// 		err := logic.CreateUser(&user)
+// 		assert.NotNil(t, err)
+// 		assert.EqualError(t, err, "user exists")
+// 	})
+// }
 
-func TestCreateUser(t *testing.T) {
-	deleteAllUsers(t)
-	user := models.User{UserName: "admin", Password: "password", IsAdmin: true}
-	t.Run("NoUser", func(t *testing.T) {
-		err := logic.CreateUser(&user)
-		assert.Nil(t, err)
-	})
-	t.Run("UserExists", func(t *testing.T) {
-		err := logic.CreateUser(&user)
-		assert.NotNil(t, err)
-		assert.EqualError(t, err, "user exists")
-	})
-}
+// func TestCreateSuperAdmin(t *testing.T) {
+// 	deleteAllUsers(t)
+// 	logic.ClearSuperUserCache()
+// 	var user models.User
+// 	t.Run("NoSuperAdmin", func(t *testing.T) {
+// 		user.UserName = "admin"
+// 		user.Password = "password"
+// 		err := logic.CreateSuperAdmin(&user)
+// 		assert.Nil(t, err)
+// 	})
+// 	t.Run("SuperAdminExists", func(t *testing.T) {
+// 		user.UserName = "admin2"
+// 		user.Password = "password1"
+// 		err := logic.CreateSuperAdmin(&user)
+// 		assert.EqualError(t, err, "superadmin user already exists")
+// 	})
+// }
 
-func TestCreateSuperAdmin(t *testing.T) {
-	deleteAllUsers(t)
-	var user models.User
-	t.Run("NoSuperAdmin", func(t *testing.T) {
-		user.UserName = "admin"
-		user.Password = "password"
-		err := logic.CreateSuperAdmin(&user)
-		assert.Nil(t, err)
-	})
-	t.Run("SuperAdminExists", func(t *testing.T) {
-		user.UserName = "admin2"
-		user.Password = "password1"
-		err := logic.CreateSuperAdmin(&user)
-		assert.EqualError(t, err, "superadmin user already exists")
-	})
-}
+// func TestDeleteUser(t *testing.T) {
+// 	deleteAllUsers(t)
+// 	t.Run("NonExistent User", func(t *testing.T) {
+// 		deleted, err := logic.DeleteUser("admin")
+// 		assert.EqualError(t, err, "user does not exist")
+// 		assert.False(t, deleted)
+// 	})
+// 	t.Run("Existing User", func(t *testing.T) {
+// 		user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
+// 		if err := logic.CreateUser(&user); err != nil {
+// 			t.Fatal(err)
+// 		}
+// 		deleted, err := logic.DeleteUser("admin")
+// 		assert.Nil(t, err)
+// 		assert.True(t, deleted)
+// 	})
+// }
 
-func TestDeleteUser(t *testing.T) {
-	deleteAllUsers(t)
-	t.Run("NonExistent User", func(t *testing.T) {
-		deleted, err := logic.DeleteUser("admin")
-		assert.EqualError(t, err, "user does not exist")
-		assert.False(t, deleted)
-	})
-	t.Run("Existing User", func(t *testing.T) {
-		user := models.User{UserName: "admin", Password: "password", IsAdmin: true}
-		if err := logic.CreateUser(&user); err != nil {
-			t.Fatal(err)
-		}
-		deleted, err := logic.DeleteUser("admin")
-		assert.Nil(t, err)
-		assert.True(t, deleted)
-	})
-}
+// func TestValidateUser(t *testing.T) {
+// 	var user models.User
+// 	t.Run("Valid Create", func(t *testing.T) {
+// 		user.UserName = "admin"
+// 		user.Password = "validpass"
+// 		err := logic.ValidateUser(&user)
+// 		assert.Nil(t, err)
+// 	})
+// 	t.Run("Valid Update", func(t *testing.T) {
+// 		user.UserName = "admin"
+// 		user.Password = "password"
+// 		err := logic.ValidateUser(&user)
+// 		assert.Nil(t, err)
+// 	})
+// 	t.Run("Invalid UserName", func(t *testing.T) {
+// 		t.Skip()
+// 		user.UserName = "*invalid"
+// 		err := logic.ValidateUser(&user)
+// 		assert.Error(t, err)
+// 		// assert.Contains(t, err.Error(), "Field validation for 'UserName' failed")
+// 	})
+// 	t.Run("Short UserName", func(t *testing.T) {
+// 		t.Skip()
+// 		user.UserName = "1"
+// 		err := logic.ValidateUser(&user)
+// 		assert.NotNil(t, err)
+// 		// assert.Contains(t, err.Error(), "Field validation for 'UserName' failed")
+// 	})
+// 	t.Run("Empty UserName", func(t *testing.T) {
+// 		t.Skip()
+// 		user.UserName = ""
+// 		err := logic.ValidateUser(&user)
+// 		assert.EqualError(t, err, "some string")
+// 		// assert.Contains(t, err.Error(), "Field validation for 'UserName' failed")
+// 	})
+// 	t.Run("EmptyPassword", func(t *testing.T) {
+// 		user.Password = ""
+// 		err := logic.ValidateUser(&user)
+// 		assert.EqualError(t, err, "Key: 'User.Password' Error:Field validation for 'Password' failed on the 'required' tag")
+// 	})
+// 	t.Run("ShortPassword", func(t *testing.T) {
+// 		user.Password = "123"
+// 		err := logic.ValidateUser(&user)
+// 		assert.EqualError(t, err, "Key: 'User.Password' Error:Field validation for 'Password' failed on the 'min' tag")
+// 	})
+// }
 
-func TestValidateUser(t *testing.T) {
-	var user models.User
-	t.Run("Valid Create", func(t *testing.T) {
-		user.UserName = "admin"
-		user.Password = "validpass"
-		err := logic.ValidateUser(&user)
-		assert.Nil(t, err)
-	})
-	t.Run("Valid Update", func(t *testing.T) {
-		user.UserName = "admin"
-		user.Password = "password"
-		err := logic.ValidateUser(&user)
-		assert.Nil(t, err)
-	})
-	t.Run("Invalid UserName", func(t *testing.T) {
-		t.Skip()
-		user.UserName = "*invalid"
-		err := logic.ValidateUser(&user)
-		assert.Error(t, err)
-		// assert.Contains(t, err.Error(), "Field validation for 'UserName' failed")
-	})
-	t.Run("Short UserName", func(t *testing.T) {
-		t.Skip()
-		user.UserName = "1"
-		err := logic.ValidateUser(&user)
-		assert.NotNil(t, err)
-		// assert.Contains(t, err.Error(), "Field validation for 'UserName' failed")
-	})
-	t.Run("Empty UserName", func(t *testing.T) {
-		t.Skip()
-		user.UserName = ""
-		err := logic.ValidateUser(&user)
-		assert.EqualError(t, err, "some string")
-		// assert.Contains(t, err.Error(), "Field validation for 'UserName' failed")
-	})
-	t.Run("EmptyPassword", func(t *testing.T) {
-		user.Password = ""
-		err := logic.ValidateUser(&user)
-		assert.EqualError(t, err, "Key: 'User.Password' Error:Field validation for 'Password' failed on the 'required' tag")
-	})
-	t.Run("ShortPassword", func(t *testing.T) {
-		user.Password = "123"
-		err := logic.ValidateUser(&user)
-		assert.EqualError(t, err, "Key: 'User.Password' Error:Field validation for 'Password' failed on the 'min' tag")
-	})
-}
+// func TestGetUser(t *testing.T) {
+// 	deleteAllUsers(t)
 
-func TestGetUser(t *testing.T) {
-	deleteAllUsers(t)
+// 	user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
 
-	user := models.User{UserName: "admin", Password: "password", IsAdmin: true}
+// 	t.Run("NonExistantUser", func(t *testing.T) {
+// 		admin, err := logic.GetUser("admin")
+// 		assert.EqualError(t, err, "could not find any records")
+// 		assert.Equal(t, "", admin.UserName)
+// 	})
+// 	t.Run("UserExisits", func(t *testing.T) {
+// 		if err := logic.CreateUser(&user); err != nil {
+// 			t.Error(err)
+// 		}
+// 		admin, err := logic.GetUser("admin")
+// 		assert.Nil(t, err)
+// 		assert.Equal(t, user.UserName, admin.UserName)
+// 	})
+// }
 
-	t.Run("NonExistantUser", func(t *testing.T) {
-		admin, err := logic.GetUser("admin")
-		assert.EqualError(t, err, "could not find any records")
-		assert.Equal(t, "", admin.UserName)
-	})
-	t.Run("UserExisits", func(t *testing.T) {
-		if err := logic.CreateUser(&user); err != nil {
-			t.Error(err)
-		}
-		admin, err := logic.GetUser("admin")
-		assert.Nil(t, err)
-		assert.Equal(t, user.UserName, admin.UserName)
-	})
-}
+// func TestGetUsers(t *testing.T) {
+// 	deleteAllUsers(t)
 
-func TestGetUsers(t *testing.T) {
-	deleteAllUsers(t)
+// 	adminUser := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
+// 	user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
 
-	adminUser := models.User{UserName: "admin", Password: "password", IsAdmin: true}
-	user := models.User{UserName: "admin", Password: "password", IsAdmin: false}
+// 	t.Run("NonExistantUser", func(t *testing.T) {
+// 		admin, err := logic.GetUsers()
+// 		assert.EqualError(t, err, "could not find any records")
+// 		assert.Equal(t, []models.ReturnUser(nil), admin)
+// 	})
+// 	t.Run("UserExisits", func(t *testing.T) {
+// 		user.UserName = "anotheruser"
+// 		if err := logic.CreateUser(&adminUser); err != nil {
+// 			t.Error(err)
+// 		}
+// 		admins, err := logic.GetUsers()
+// 		assert.Nil(t, err)
+// 		assert.Equal(t, adminUser.UserName, admins[0].UserName)
+// 	})
+// 	t.Run("MulipleUsers", func(t *testing.T) {
+// 		if err := logic.CreateUser(&user); err != nil {
+// 			t.Error(err)
+// 		}
+// 		admins, err := logic.GetUsers()
+// 		assert.Nil(t, err)
+// 		for _, u := range admins {
+// 			if u.UserName == "admin" {
+// 				assert.Equal(t, true, u.IsAdmin)
+// 			} else {
+// 				assert.Equal(t, user.UserName, u.UserName)
+// 				assert.Equal(t, user.PlatformRoleID, u.PlatformRoleID)
+// 			}
+// 		}
+// 	})
 
-	t.Run("NonExistantUser", func(t *testing.T) {
-		admin, err := logic.GetUsers()
-		assert.EqualError(t, err, "could not find any records")
-		assert.Equal(t, []models.ReturnUser(nil), admin)
-	})
-	t.Run("UserExisits", func(t *testing.T) {
-		user.UserName = "anotheruser"
-		if err := logic.CreateUser(&adminUser); err != nil {
-			t.Error(err)
-		}
-		admins, err := logic.GetUsers()
-		assert.Nil(t, err)
-		assert.Equal(t, adminUser.UserName, admins[0].UserName)
-	})
-	t.Run("MulipleUsers", func(t *testing.T) {
-		if err := logic.CreateUser(&user); err != nil {
-			t.Error(err)
-		}
-		admins, err := logic.GetUsers()
-		assert.Nil(t, err)
-		for _, u := range admins {
-			if u.UserName == "admin" {
-				assert.Equal(t, true, u.IsAdmin)
-			} else {
-				assert.Equal(t, user.UserName, u.UserName)
-				assert.Equal(t, user.IsAdmin, u.IsAdmin)
-			}
-		}
-	})
+// }
 
-}
+// func TestUpdateUser(t *testing.T) {
+// 	deleteAllUsers(t)
+// 	user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
+// 	newuser := models.User{UserName: "hello", Password: "world", PlatformRoleID: models.AdminRole}
+// 	t.Run("NonExistantUser", func(t *testing.T) {
+// 		admin, err := logic.UpdateUser(&newuser, &user)
+// 		assert.EqualError(t, err, "could not find any records")
+// 		assert.Equal(t, "", admin.UserName)
+// 	})
 
-func TestUpdateUser(t *testing.T) {
-	deleteAllUsers(t)
-	user := models.User{UserName: "admin", Password: "password", IsAdmin: true}
-	newuser := models.User{UserName: "hello", Password: "world", IsAdmin: true}
-	t.Run("NonExistantUser", func(t *testing.T) {
-		admin, err := logic.UpdateUser(&newuser, &user)
-		assert.EqualError(t, err, "could not find any records")
-		assert.Equal(t, "", admin.UserName)
-	})
+// 	t.Run("UserExists", func(t *testing.T) {
+// 		if err := logic.CreateUser(&user); err != nil {
+// 			t.Error(err)
+// 		}
+// 		admin, err := logic.UpdateUser(&newuser, &user)
+// 		assert.Nil(t, err)
+// 		assert.Equal(t, newuser.UserName, admin.UserName)
+// 	})
+// }
 
-	t.Run("UserExists", func(t *testing.T) {
-		if err := logic.CreateUser(&user); err != nil {
-			t.Error(err)
-		}
-		admin, err := logic.UpdateUser(&newuser, &user)
-		assert.Nil(t, err)
-		assert.Equal(t, newuser.UserName, admin.UserName)
-	})
-}
+// // func TestValidateUserToken(t *testing.T) {
+// // 	t.Run("EmptyToken", func(t *testing.T) {
+// // 		err := ValidateUserToken("", "", false)
+// // 		assert.NotNil(t, err)
+// // 		assert.Equal(t, "Missing Auth Token.", err.Error())
+// // 	})
+// // 	t.Run("InvalidToken", func(t *testing.T) {
+// // 		err := ValidateUserToken("Bearer: badtoken", "", false)
+// // 		assert.NotNil(t, err)
+// // 		assert.Equal(t, "Error Verifying Auth Token", err.Error())
+// // 	})
+// // 	t.Run("InvalidUser", func(t *testing.T) {
+// // 		t.Skip()
+// // 		err := ValidateUserToken("Bearer: secretkey", "baduser", false)
+// // 		assert.NotNil(t, err)
+// // 		assert.Equal(t, "Error Verifying Auth Token", err.Error())
+// // 		//need authorization
+// // 	})
+// // 	t.Run("ValidToken", func(t *testing.T) {
+// // 		err := ValidateUserToken("Bearer: secretkey", "", true)
+// // 		assert.Nil(t, err)
+// // 	})
+// // }
 
-// func TestValidateUserToken(t *testing.T) {
-// 	t.Run("EmptyToken", func(t *testing.T) {
-// 		err := ValidateUserToken("", "", false)
-// 		assert.NotNil(t, err)
-// 		assert.Equal(t, "Missing Auth Token.", err.Error())
+// func TestVerifyAuthRequest(t *testing.T) {
+// 	deleteAllUsers(t)
+// 	user := models.User{UserName: "admin", Password: "password", PlatformRoleID: models.AdminRole}
+// 	var authRequest models.UserAuthParams
+// 	t.Run("EmptyUserName", func(t *testing.T) {
+// 		authRequest.UserName = ""
+// 		authRequest.Password = "Password"
+// 		jwt, err := logic.VerifyAuthRequest(authRequest)
+// 		assert.Equal(t, "", jwt)
+// 		assert.EqualError(t, err, "username can't be empty")
 // 	})
-// 	t.Run("InvalidToken", func(t *testing.T) {
-// 		err := ValidateUserToken("Bearer: badtoken", "", false)
-// 		assert.NotNil(t, err)
-// 		assert.Equal(t, "Error Verifying Auth Token", err.Error())
+// 	t.Run("EmptyPassword", func(t *testing.T) {
+// 		authRequest.UserName = "admin"
+// 		authRequest.Password = ""
+// 		jwt, err := logic.VerifyAuthRequest(authRequest)
+// 		assert.Equal(t, "", jwt)
+// 		assert.EqualError(t, err, "password can't be empty")
 // 	})
-// 	t.Run("InvalidUser", func(t *testing.T) {
-// 		t.Skip()
-// 		err := ValidateUserToken("Bearer: secretkey", "baduser", false)
-// 		assert.NotNil(t, err)
-// 		assert.Equal(t, "Error Verifying Auth Token", err.Error())
-// 		//need authorization
+// 	t.Run("NonExistantUser", func(t *testing.T) {
+// 		authRequest.UserName = "admin"
+// 		authRequest.Password = "password"
+// 		jwt, err := logic.VerifyAuthRequest(authRequest)
+// 		assert.Equal(t, "", jwt)
+// 		assert.EqualError(t, err, "incorrect credentials")
 // 	})
-// 	t.Run("ValidToken", func(t *testing.T) {
-// 		err := ValidateUserToken("Bearer: secretkey", "", true)
+// 	t.Run("Non-Admin", func(t *testing.T) {
+// 		user.PlatformRoleID = models.ServiceUser
+// 		user.Password = "somepass"
+// 		user.UserName = "nonadmin"
+// 		if err := logic.CreateUser(&user); err != nil {
+// 			t.Error(err)
+// 		}
+// 		authRequest := models.UserAuthParams{UserName: "nonadmin", Password: "somepass"}
+// 		jwt, err := logic.VerifyAuthRequest(authRequest)
+// 		assert.NotNil(t, jwt)
 // 		assert.Nil(t, err)
 // 	})
+// 	t.Run("WrongPassword", func(t *testing.T) {
+// 		user := models.User{UserName: "admin", Password: "password"}
+// 		if err := logic.CreateUser(&user); err != nil {
+// 			t.Error(err)
+// 		}
+// 		authRequest := models.UserAuthParams{UserName: "admin", Password: "badpass"}
+// 		jwt, err := logic.VerifyAuthRequest(authRequest)
+// 		assert.Equal(t, "", jwt)
+// 		assert.EqualError(t, err, "incorrect credentials")
+// 	})
+// 	t.Run("Success", func(t *testing.T) {
+// 		authRequest := models.UserAuthParams{UserName: "admin", Password: "password"}
+// 		jwt, err := logic.VerifyAuthRequest(authRequest)
+// 		assert.Nil(t, err)
+// 		assert.NotNil(t, jwt)
+// 	})
 // }
-
-func TestVerifyAuthRequest(t *testing.T) {
-	deleteAllUsers(t)
-	user := models.User{UserName: "admin", Password: "password", IsSuperAdmin: false, IsAdmin: true}
-	var authRequest models.UserAuthParams
-	t.Run("EmptyUserName", func(t *testing.T) {
-		authRequest.UserName = ""
-		authRequest.Password = "Password"
-		jwt, err := logic.VerifyAuthRequest(authRequest)
-		assert.Equal(t, "", jwt)
-		assert.EqualError(t, err, "username can't be empty")
-	})
-	t.Run("EmptyPassword", func(t *testing.T) {
-		authRequest.UserName = "admin"
-		authRequest.Password = ""
-		jwt, err := logic.VerifyAuthRequest(authRequest)
-		assert.Equal(t, "", jwt)
-		assert.EqualError(t, err, "password can't be empty")
-	})
-	t.Run("NonExistantUser", func(t *testing.T) {
-		authRequest.UserName = "admin"
-		authRequest.Password = "password"
-		jwt, err := logic.VerifyAuthRequest(authRequest)
-		assert.Equal(t, "", jwt)
-		assert.EqualError(t, err, "incorrect credentials")
-	})
-	t.Run("Non-Admin", func(t *testing.T) {
-		user.IsAdmin = false
-		user.Password = "somepass"
-		user.UserName = "nonadmin"
-		if err := logic.CreateUser(&user); err != nil {
-			t.Error(err)
-		}
-		authRequest := models.UserAuthParams{UserName: "nonadmin", Password: "somepass"}
-		jwt, err := logic.VerifyAuthRequest(authRequest)
-		assert.NotNil(t, jwt)
-		assert.Nil(t, err)
-	})
-	t.Run("WrongPassword", func(t *testing.T) {
-		user := models.User{UserName: "admin", Password: "password"}
-		if err := logic.CreateUser(&user); err != nil {
-			t.Error(err)
-		}
-		authRequest := models.UserAuthParams{UserName: "admin", Password: "badpass"}
-		jwt, err := logic.VerifyAuthRequest(authRequest)
-		assert.Equal(t, "", jwt)
-		assert.EqualError(t, err, "incorrect credentials")
-	})
-	t.Run("Success", func(t *testing.T) {
-		authRequest := models.UserAuthParams{UserName: "admin", Password: "password"}
-		jwt, err := logic.VerifyAuthRequest(authRequest)
-		assert.Nil(t, err)
-		assert.NotNil(t, jwt)
-	})
-}

+ 6 - 0
database/database.go

@@ -25,6 +25,8 @@ const (
 	DELETED_NODES_TABLE_NAME = "deletednodes"
 	// USERS_TABLE_NAME - users table
 	USERS_TABLE_NAME = "users"
+	// USER_PERMISSIONS_TABLE_NAME - user permissions table
+	USER_PERMISSIONS_TABLE_NAME = "user_permissions"
 	// CERTS_TABLE_NAME - certificates table
 	CERTS_TABLE_NAME = "certs"
 	// DNS_TABLE_NAME - dns table
@@ -63,6 +65,8 @@ const (
 	HOST_ACTIONS_TABLE_NAME = "hostactions"
 	// PENDING_USERS_TABLE_NAME - table name for pending users
 	PENDING_USERS_TABLE_NAME = "pending_users"
+	// USER_INVITES - table for user invites
+	USER_INVITES_TABLE_NAME = "user_invites"
 	// == ERROR CONSTS ==
 	// NO_RECORD - no singular result found
 	NO_RECORD = "no result found"
@@ -146,6 +150,8 @@ func createTables() {
 	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)
 }
 
 func CreateTable(tableName string) error {

+ 1 - 1
docker/Dockerfile-go-builder

@@ -1,4 +1,4 @@
-FROM golang:1.19.6-alpine3.17 
+FROM golang:1.20.13-alpine3.19
 ARG version 
 RUN apk add build-base
 WORKDIR /app

+ 1 - 0
docs/APIUsage.md

@@ -0,0 +1 @@
+Most actions that can be performed via API can be performed via UI. We recommend managing your networks using the official netmaker-ui project. However, Netmaker can also be run without the UI, and all functions can be achieved via API calls. If your use case requires using Netmaker without the UI or you need to do some troubleshooting/advanced configuration, using the API directly may help.

+ 10 - 0
docs/Authentication.md

@@ -0,0 +1,10 @@
+API calls are primarily authenticated using a user authentication token. This token should be included in the header as follows:
+
+-H "Authorization: Bearer <YOUR_AUTH_TOKEN>"
+
+To obtain YOUR_AUTH_TOKEN:
+Call the api/users/adm/authenticate endpoint (see documentation below for details).
+
+Note: While a MasterKey exists (configurable via env var or config file), it should be considered a backup option, used only when server access is lost. By default, this key is "secret key," but it's crucial to change this and keep it secure in your instance.
+
+For more information on configuration and security best practices, refer to the [Netmaker documentation](https://docs.netmaker.org/index.html).

+ 1 - 0
docs/Pricing.md

@@ -0,0 +1 @@
+Check out our [Pricing](https://www.netmaker.io/pricing). And Feel Free to [Contact Us](https://www.netmaker.io/contact) if you have any questions or need some clarifications.

+ 3 - 3
functions/helpers_test.go

@@ -26,9 +26,9 @@ func TestMain(m *testing.M) {
 	database.InitializeDatabase()
 	defer database.CloseDB()
 	logic.CreateSuperAdmin(&models.User{
-		UserName:     "superadmin",
-		Password:     "password",
-		IsSuperAdmin: true,
+		UserName:       "superadmin",
+		Password:       "password",
+		PlatformRoleID: models.SuperAdminRole,
 	})
 	peerUpdate := make(chan *models.Node)
 	go logic.ManageZombies(context.Background(), peerUpdate)

+ 5 - 3
go.mod

@@ -17,7 +17,7 @@ require (
 	github.com/stretchr/testify v1.9.0
 	github.com/txn2/txeh v1.5.5
 	golang.org/x/crypto v0.23.0
-	golang.org/x/net v0.22.0 // indirect
+	golang.org/x/net v0.23.0 // indirect
 	golang.org/x/oauth2 v0.21.0
 	golang.org/x/sys v0.21.0 // indirect
 	golang.org/x/text v0.16.0 // indirect
@@ -38,20 +38,23 @@ require (
 )
 
 require (
-	github.com/go-jose/go-jose/v3 v3.0.3
 	github.com/guumaster/tablewriter v0.0.10
 	github.com/matryer/is v1.4.1
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/spf13/cobra v1.8.1
+	gopkg.in/mail.v2 v2.3.1
 )
 
 require (
 	cloud.google.com/go/compute/metadata v0.3.0 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+	github.com/go-jose/go-jose/v3 v3.0.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/seancfoley/bintree v1.3.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
+	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 )
 
 require (
@@ -63,6 +66,5 @@ require (
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
 	golang.org/x/sync v0.7.0 // indirect
 )

+ 6 - 2
go.sum

@@ -105,8 +105,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 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.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
-golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
+golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
 golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -144,8 +144,12 @@ 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.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=
+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/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
+gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 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=

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

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

+ 1 - 1
k8s/client/netclient.yaml

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

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

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

+ 87 - 14
logic/auth.go

@@ -20,9 +20,21 @@ const (
 	auth_key = "netmaker_auth"
 )
 
+var (
+	superUser = models.User{}
+)
+
+func ClearSuperUserCache() {
+	superUser = models.User{}
+}
+
 // HasSuperAdmin - checks if server has an superadmin/owner
 func HasSuperAdmin() (bool, error) {
 
+	if superUser.IsSuperAdmin {
+		return true, nil
+	}
+
 	collection, err := database.FetchRecords(database.USERS_TABLE_NAME)
 	if err != nil {
 		if database.IsEmptyRecord(err) {
@@ -37,7 +49,7 @@ func HasSuperAdmin() (bool, error) {
 		if err != nil {
 			continue
 		}
-		if user.IsSuperAdmin {
+		if user.PlatformRoleID == models.SuperAdminRole || user.IsSuperAdmin {
 			return true, nil
 		}
 	}
@@ -93,18 +105,58 @@ func GetUsers() ([]models.ReturnUser, error) {
 	return users, err
 }
 
+// IsOauthUser - returns
+func IsOauthUser(user *models.User) error {
+	var currentValue, err = FetchPassValue("")
+	if err != nil {
+		return err
+	}
+	var bCryptErr = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(currentValue))
+	return bCryptErr
+}
+
+func FetchPassValue(newValue string) (string, error) {
+
+	type valueHolder struct {
+		Value string `json:"value" bson:"value"`
+	}
+	newValueHolder := valueHolder{}
+	var currentValue, err = FetchAuthSecret()
+	if err != nil {
+		return "", err
+	}
+	var unmarshErr = json.Unmarshal([]byte(currentValue), &newValueHolder)
+	if unmarshErr != nil {
+		return "", unmarshErr
+	}
+
+	var b64CurrentValue, b64Err = base64.StdEncoding.DecodeString(newValueHolder.Value)
+	if b64Err != nil {
+		logger.Log(0, "could not decode pass")
+		return "", nil
+	}
+	return string(b64CurrentValue), nil
+}
+
 // CreateUser - creates a user
 func CreateUser(user *models.User) error {
 	// check if user exists
 	if _, err := GetUser(user.UserName); err == nil {
 		return errors.New("user exists")
 	}
+	SetUserDefaults(user)
+	if err := IsGroupsValid(user.UserGroups); err != nil {
+		return errors.New("invalid groups: " + err.Error())
+	}
+	if err := IsNetworkRolesValid(user.NetworkRoles); err != nil {
+		return errors.New("invalid network roles: " + err.Error())
+	}
+
 	var err = ValidateUser(user)
 	if err != nil {
 		logger.Log(0, "failed to validate user", err.Error())
 		return err
 	}
-
 	// encrypt that password so we never see it again
 	hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 5)
 	if err != nil {
@@ -113,15 +165,16 @@ func CreateUser(user *models.User) error {
 	}
 	// set password to encrypted password
 	user.Password = string(hash)
-
-	tokenString, _ := CreateUserJWT(user.UserName, user.IsSuperAdmin, user.IsAdmin)
-	if tokenString == "" {
+	user.AuthType = models.BasicAuth
+	if IsOauthUser(user) == nil {
+		user.AuthType = models.OAuth
+	}
+	_, err = CreateUserJWT(user.UserName, user.PlatformRoleID)
+	if err != nil {
 		logger.Log(0, "failed to generate token", err.Error())
 		return err
 	}
 
-	SetUserDefaults(user)
-
 	// connect db
 	data, err := json.Marshal(user)
 	if err != nil {
@@ -146,8 +199,7 @@ func CreateSuperAdmin(u *models.User) error {
 	if hassuperadmin {
 		return errors.New("superadmin user already exists")
 	}
-	u.IsSuperAdmin = true
-	u.IsAdmin = false
+	u.PlatformRoleID = models.SuperAdminRole
 	return CreateUser(u)
 }
 
@@ -176,7 +228,7 @@ func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) {
 	}
 
 	// Create a new JWT for the node
-	tokenString, err := CreateUserJWT(authRequest.UserName, result.IsSuperAdmin, result.IsAdmin)
+	tokenString, err := CreateUserJWT(authRequest.UserName, result.PlatformRoleID)
 	if err != nil {
 		slog.Error("error creating jwt", "error", err)
 		return "", err
@@ -204,6 +256,9 @@ func UpsertUser(user models.User) error {
 		slog.Error("error inserting user", "user", user.UserName, "error", err.Error())
 		return err
 	}
+	if user.IsSuperAdmin {
+		superUser = user
+	}
 	return nil
 }
 
@@ -223,6 +278,9 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) {
 		user.UserName = userchange.UserName
 	}
 	if userchange.Password != "" {
+		if len(userchange.Password) < 5 {
+			return &models.User{}, errors.New("password requires min 5 characters")
+		}
 		// encrypt that password so we never see it again
 		hash, err := bcrypt.GenerateFromPassword([]byte(userchange.Password), 5)
 
@@ -234,13 +292,23 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) {
 
 		user.Password = userchange.Password
 	}
-	user.IsAdmin = userchange.IsAdmin
-
+	if err := IsGroupsValid(userchange.UserGroups); err != nil {
+		return userchange, errors.New("invalid groups: " + err.Error())
+	}
+	if err := IsNetworkRolesValid(userchange.NetworkRoles); err != nil {
+		return userchange, errors.New("invalid network roles: " + err.Error())
+	}
+	// Reset Gw Access for service users
+	go UpdateUserGwAccess(*user, *userchange)
+	if userchange.PlatformRoleID != "" {
+		user.PlatformRoleID = userchange.PlatformRoleID
+	}
+	user.UserGroups = userchange.UserGroups
+	user.NetworkRoles = userchange.NetworkRoles
 	err := ValidateUser(user)
 	if err != nil {
 		return &models.User{}, err
 	}
-
 	if err = database.DeleteRecord(database.USERS_TABLE_NAME, queryUser); err != nil {
 		return &models.User{}, err
 	}
@@ -258,12 +326,17 @@ func UpdateUser(userchange, user *models.User) (*models.User, error) {
 // ValidateUser - validates a user model
 func ValidateUser(user *models.User) error {
 
+	// check if role is valid
+	_, err := GetRole(user.PlatformRoleID)
+	if err != nil {
+		return errors.New("failed to fetch platform role " + user.PlatformRoleID.String())
+	}
 	v := validator.New()
 	_ = v.RegisterValidation("in_charset", func(fl validator.FieldLevel) bool {
 		isgood := user.NameInCharSet()
 		return isgood
 	})
-	err := v.Struct(user)
+	err = v.Struct(user)
 
 	if err != nil {
 		for _, e := range err.(validator.ValidationErrors) {

+ 58 - 15
logic/enrollmentkey.go

@@ -5,11 +5,13 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"sync"
 	"time"
 
 	"github.com/google/uuid"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/exp/slices"
 )
 
@@ -29,6 +31,10 @@ var EnrollmentErrors = struct {
 	FailedToTokenize:   fmt.Errorf("failed to tokenize"),
 	FailedToDeTokenize: fmt.Errorf("failed to detokenize"),
 }
+var (
+	enrollmentkeyCacheMutex = &sync.RWMutex{}
+	enrollmentkeyCacheMap   = make(map[string]models.EnrollmentKey)
+)
 
 // CreateEnrollmentKey - creates a new enrollment key in db
 func CreateEnrollmentKey(uses int, expiration time.Time, networks, tags []string, unlimited bool, relay uuid.UUID) (*models.EnrollmentKey, error) {
@@ -104,21 +110,21 @@ func UpdateEnrollmentKey(keyId string, relayId uuid.UUID) (*models.EnrollmentKey
 
 	key.Relay = relayId
 
-	if err = upsertEnrollmentKey(key); err != nil {
+	if err = upsertEnrollmentKey(&key); err != nil {
 		return nil, err
 	}
 
-	return key, nil
+	return &key, nil
 }
 
 // GetAllEnrollmentKeys - fetches all enrollment keys from DB
 // TODO drop double pointer
-func GetAllEnrollmentKeys() ([]*models.EnrollmentKey, error) {
+func GetAllEnrollmentKeys() ([]models.EnrollmentKey, error) {
 	currentKeys, err := getEnrollmentKeysMap()
 	if err != nil {
 		return nil, err
 	}
-	var currentKeysList = []*models.EnrollmentKey{}
+	var currentKeysList = []models.EnrollmentKey{}
 	for k := range currentKeys {
 		currentKeysList = append(currentKeysList, currentKeys[k])
 	}
@@ -127,15 +133,21 @@ func GetAllEnrollmentKeys() ([]*models.EnrollmentKey, error) {
 
 // GetEnrollmentKey - fetches a single enrollment key
 // returns nil and error if not found
-func GetEnrollmentKey(value string) (*models.EnrollmentKey, error) {
+func GetEnrollmentKey(value string) (key models.EnrollmentKey, err error) {
 	currentKeys, err := getEnrollmentKeysMap()
 	if err != nil {
-		return nil, err
+		return key, err
 	}
 	if key, ok := currentKeys[value]; ok {
 		return key, nil
 	}
-	return nil, EnrollmentErrors.NoKeyFound
+	return key, EnrollmentErrors.NoKeyFound
+}
+
+func deleteEnrollmentkeyFromCache(key string) {
+	enrollmentkeyCacheMutex.Lock()
+	delete(enrollmentkeyCacheMap, key)
+	enrollmentkeyCacheMutex.Unlock()
 }
 
 // DeleteEnrollmentKey - delete's a given enrollment key by value
@@ -144,7 +156,13 @@ func DeleteEnrollmentKey(value string) error {
 	if err != nil {
 		return err
 	}
-	return database.DeleteRecord(database.ENROLLMENT_KEYS_TABLE_NAME, value)
+	err = database.DeleteRecord(database.ENROLLMENT_KEYS_TABLE_NAME, value)
+	if err == nil {
+		if servercfg.CacheEnabled() {
+			deleteEnrollmentkeyFromCache(value)
+		}
+	}
+	return err
 }
 
 // TryToUseEnrollmentKey - checks first if key can be decremented
@@ -200,7 +218,7 @@ func DeTokenize(b64Token string) (*models.EnrollmentKey, error) {
 	if err != nil {
 		return nil, err
 	}
-	return k, nil
+	return &k, nil
 }
 
 // == private ==
@@ -215,11 +233,11 @@ func decrementEnrollmentKey(value string) (*models.EnrollmentKey, error) {
 		return nil, EnrollmentErrors.NoUsesRemaining
 	}
 	k.UsesRemaining = k.UsesRemaining - 1
-	if err = upsertEnrollmentKey(k); err != nil {
+	if err = upsertEnrollmentKey(&k); err != nil {
 		return nil, err
 	}
 
-	return k, nil
+	return &k, nil
 }
 
 func upsertEnrollmentKey(k *models.EnrollmentKey) error {
@@ -230,7 +248,13 @@ func upsertEnrollmentKey(k *models.EnrollmentKey) error {
 	if err != nil {
 		return err
 	}
-	return database.Insert(k.Value, string(data), database.ENROLLMENT_KEYS_TABLE_NAME)
+	err = database.Insert(k.Value, string(data), database.ENROLLMENT_KEYS_TABLE_NAME)
+	if err == nil {
+		if servercfg.CacheEnabled() {
+			storeEnrollmentkeyInCache(k.Value, *k)
+		}
+	}
+	return nil
 }
 
 func getUniqueEnrollmentID() (string, error) {
@@ -245,7 +269,23 @@ func getUniqueEnrollmentID() (string, error) {
 	return newID, nil
 }
 
-func getEnrollmentKeysMap() (map[string]*models.EnrollmentKey, error) {
+func getEnrollmentkeysFromCache() map[string]models.EnrollmentKey {
+	return enrollmentkeyCacheMap
+}
+
+func storeEnrollmentkeyInCache(key string, enrollmentkey models.EnrollmentKey) {
+	enrollmentkeyCacheMutex.Lock()
+	enrollmentkeyCacheMap[key] = enrollmentkey
+	enrollmentkeyCacheMutex.Unlock()
+}
+
+func getEnrollmentKeysMap() (map[string]models.EnrollmentKey, error) {
+	if servercfg.CacheEnabled() {
+		keys := getEnrollmentkeysFromCache()
+		if len(keys) != 0 {
+			return keys, nil
+		}
+	}
 	records, err := database.FetchRecords(database.ENROLLMENT_KEYS_TABLE_NAME)
 	if err != nil {
 		if !database.IsEmptyRecord(err) {
@@ -255,14 +295,17 @@ func getEnrollmentKeysMap() (map[string]*models.EnrollmentKey, error) {
 	if records == nil {
 		records = make(map[string]string)
 	}
-	currentKeys := make(map[string]*models.EnrollmentKey, 0)
+	currentKeys := make(map[string]models.EnrollmentKey, 0)
 	if len(records) > 0 {
 		for k := range records {
 			var currentKey models.EnrollmentKey
 			if err = json.Unmarshal([]byte(records[k]), &currentKey); err != nil {
 				continue
 			}
-			currentKeys[k] = &currentKey
+			currentKeys[k] = currentKey
+			if servercfg.CacheEnabled() {
+				storeEnrollmentkeyInCache(currentKey.Value, currentKey)
+			}
 		}
 	}
 	return currentKeys, nil

+ 1 - 1
logic/enrollmentkey_test.go

@@ -68,7 +68,7 @@ func TestDelete_EnrollmentKey(t *testing.T) {
 		err := DeleteEnrollmentKey(newKey.Value)
 		assert.Nil(t, err)
 		oldKey, err := GetEnrollmentKey(newKey.Value)
-		assert.Nil(t, oldKey)
+		assert.Equal(t, oldKey, models.EnrollmentKey{})
 		assert.NotNil(t, err)
 		assert.Equal(t, err, EnrollmentErrors.NoKeyFound)
 	})

+ 19 - 0
logic/extpeers.go

@@ -86,10 +86,21 @@ func DeleteExtClient(network string, clientid string) error {
 	if err != nil {
 		return err
 	}
+	extClient, err := GetExtClient(clientid, network)
+	if err != nil {
+		return err
+	}
 	err = database.DeleteRecord(database.EXT_CLIENT_TABLE_NAME, key)
 	if err != nil {
 		return err
 	}
+	//recycle ip address
+	if extClient.Address != "" {
+		RemoveIpFromAllocatedIpMap(network, extClient.Address)
+	}
+	if extClient.Address6 != "" {
+		RemoveIpFromAllocatedIpMap(network, extClient.Address6)
+	}
 	if servercfg.CacheEnabled() {
 		deleteExtClientFromCache(key)
 	}
@@ -287,6 +298,14 @@ func SaveExtClient(extclient *models.ExtClient) error {
 	if servercfg.CacheEnabled() {
 		storeExtClientInCache(key, *extclient)
 	}
+	if _, ok := allocatedIpMap[extclient.Network]; ok {
+		if extclient.Address != "" {
+			AddIpToAllocatedIpMap(extclient.Network, net.ParseIP(extclient.Address))
+		}
+		if extclient.Address6 != "" {
+			AddIpToAllocatedIpMap(extclient.Network, net.ParseIP(extclient.Address6))
+		}
+	}
 	return SetNetworkNodesLastModified(extclient.Network)
 }
 

+ 31 - 4
logic/gateway.go

@@ -178,6 +178,30 @@ func CreateIngressGateway(netid string, nodeid string, ingress models.IngressReq
 	if err != nil {
 		return models.Node{}, err
 	}
+	// create network role for this gateway
+	CreateRole(models.UserRolePermissionTemplate{
+		ID:        models.GetRAGRoleID(node.Network, host.ID.String()),
+		UiName:    models.GetRAGRoleName(node.Network, host.Name),
+		NetworkID: models.NetworkID(node.Network),
+		Default:   true,
+		NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
+			models.RemoteAccessGwRsrc: {
+				models.RsrcID(node.ID.String()): models.RsrcPermissionScope{
+					Read:      true,
+					VPNaccess: true,
+				},
+			},
+			models.ExtClientsRsrc: {
+				models.AllExtClientsRsrcID: models.RsrcPermissionScope{
+					Read:     true,
+					Create:   true,
+					Update:   true,
+					Delete:   true,
+					SelfOnly: true,
+				},
+			},
+		},
+	})
 	err = SetNetworkNodesLastModified(netid)
 	return node, err
 }
@@ -231,6 +255,11 @@ func DeleteIngressGateway(nodeid string) (models.Node, []models.ExtClient, error
 	if err != nil {
 		return models.Node{}, removedClients, err
 	}
+	host, err := GetHost(node.HostID.String())
+	if err != nil {
+		return models.Node{}, removedClients, err
+	}
+	go DeleteRole(models.GetRAGRoleID(node.Network, host.ID.String()), true)
 	err = SetNetworkNodesLastModified(node.Network)
 	return node, removedClients, err
 }
@@ -264,10 +293,8 @@ func IsUserAllowedAccessToExtClient(username string, client models.ExtClient) bo
 	if err != nil {
 		return false
 	}
-	if !user.IsAdmin && !user.IsSuperAdmin {
-		if user.UserName != client.OwnerID {
-			return false
-		}
+	if user.UserName != client.OwnerID {
+		return false
 	}
 	return true
 }

+ 13 - 0
logic/hosts.go

@@ -269,6 +269,19 @@ func UpdateHostFromClient(newHost, currHost *models.Host) (sendPeerUpdate bool)
 	currHost.IsStaticPort = newHost.IsStaticPort
 	currHost.IsStatic = newHost.IsStatic
 	currHost.MTU = newHost.MTU
+	if newHost.Name != currHost.Name {
+		// update any rag role ids
+		for _, nodeID := range newHost.Nodes {
+			node, err := GetNodeByID(nodeID)
+			if err == nil && node.IsIngressGateway {
+				role, err := GetRole(models.GetRAGRoleID(node.Network, currHost.ID.String()))
+				if err == nil {
+					role.UiName = models.GetRAGRoleName(node.Network, newHost.Name)
+					UpdateRole(role)
+				}
+			}
+		}
+	}
 	currHost.Name = newHost.Name
 	if len(newHost.NatType) > 0 && newHost.NatType != currHost.NatType {
 		currHost.NatType = newHost.NatType

+ 47 - 6
logic/jwts.go

@@ -31,7 +31,7 @@ func SetJWTSecret() {
 
 // CreateJWT func will used to create the JWT while signing in and signing out
 func CreateJWT(uuid string, macAddress string, network string) (response string, err error) {
-	expirationTime := time.Now().Add(5 * time.Minute)
+	expirationTime := time.Now().Add(15 * time.Minute)
 	claims := &models.Claims{
 		ID:         uuid,
 		Network:    network,
@@ -53,12 +53,11 @@ func CreateJWT(uuid string, macAddress string, network string) (response string,
 }
 
 // CreateUserJWT - creates a user jwt token
-func CreateUserJWT(username string, issuperadmin, isadmin bool) (response string, err error) {
+func CreateUserJWT(username string, role models.UserRoleID) (response string, err error) {
 	expirationTime := time.Now().Add(servercfg.GetServerConfig().JwtValidityDuration)
 	claims := &models.UserClaims{
-		UserName:     username,
-		IsSuperAdmin: issuperadmin,
-		IsAdmin:      isadmin,
+		UserName: username,
+		Role:     role,
 		RegisteredClaims: jwt.RegisteredClaims{
 			Issuer:    "Netmaker",
 			Subject:   fmt.Sprintf("user|%s", username),
@@ -87,6 +86,47 @@ func VerifyJWT(bearerToken string) (username string, issuperadmin, isadmin bool,
 	return VerifyUserToken(token)
 }
 
+func GetUserNameFromToken(authtoken string) (username string, err error) {
+	claims := &models.UserClaims{}
+	var tokenSplit = strings.Split(authtoken, " ")
+	var tokenString = ""
+
+	if len(tokenSplit) < 2 {
+		return "", Unauthorized_Err
+	} else {
+		tokenString = tokenSplit[1]
+	}
+	if tokenString == servercfg.GetMasterKey() && servercfg.GetMasterKey() != "" {
+		return MasterUser, nil
+	}
+
+	token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
+		return jwtSecretKey, nil
+	})
+	if err != nil {
+		return "", Unauthorized_Err
+	}
+
+	if token != nil && token.Valid {
+		var user *models.User
+		// check that user exists
+		user, err = GetUser(claims.UserName)
+		if err != nil {
+			return "", err
+		}
+		if user.UserName != "" {
+			return user.UserName, nil
+		}
+		if user.PlatformRoleID != claims.Role {
+			return "", Unauthorized_Err
+		}
+		err = errors.New("user does not exist")
+	} else {
+		err = Unauthorized_Err
+	}
+	return "", err
+}
+
 // VerifyUserToken func will used to Verify the JWT Token while using APIS
 func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin bool, err error) {
 	claims := &models.UserClaims{}
@@ -107,7 +147,8 @@ func VerifyUserToken(tokenString string) (username string, issuperadmin, isadmin
 			return "", false, false, err
 		}
 		if user.UserName != "" {
-			return user.UserName, user.IsSuperAdmin, user.IsAdmin, nil
+			return user.UserName, user.PlatformRoleID == models.SuperAdminRole,
+				user.PlatformRoleID == models.AdminRole, nil
 		}
 		err = errors.New("user does not exist")
 	}

+ 157 - 6
logic/networks.go

@@ -15,13 +15,123 @@ import (
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic/acls/nodeacls"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/servercfg"
 	"github.com/gravitl/netmaker/validation"
+	"golang.org/x/exp/slog"
 )
 
+var (
+	networkCacheMutex = &sync.RWMutex{}
+	networkCacheMap   = make(map[string]models.Network)
+	allocatedIpMap    = make(map[string]map[string]net.IP)
+)
+
+// SetAllocatedIpMap - set allocated ip map for networks
+func SetAllocatedIpMap() error {
+	logger.Log(0, "start setting up allocated ip map")
+	if allocatedIpMap == nil {
+		allocatedIpMap = map[string]map[string]net.IP{}
+	}
+
+	currentNetworks, err := GetNetworks()
+	if err != nil {
+		return err
+	}
+
+	for _, v := range currentNetworks {
+		pMap := map[string]net.IP{}
+		netName := v.NetID
+
+		nodes, err := GetNetworkNodes(netName)
+		if err != nil {
+			slog.Error("could not load node for network", netName, "error", err.Error())
+			continue
+		}
+
+		for _, n := range nodes {
+
+			if n.Address.IP != nil {
+				pMap[n.Address.IP.String()] = n.Address.IP
+			}
+			if n.Address6.IP != nil {
+				pMap[n.Address6.IP.String()] = n.Address6.IP
+			}
+		}
+
+		allocatedIpMap[netName] = pMap
+	}
+	logger.Log(0, "setting up allocated ip map done")
+	return nil
+}
+
+// ClearAllocatedIpMap - set allocatedIpMap to nil
+func ClearAllocatedIpMap() {
+	allocatedIpMap = nil
+}
+
+func AddIpToAllocatedIpMap(networkName string, ip net.IP) {
+	networkCacheMutex.Lock()
+	allocatedIpMap[networkName][ip.String()] = ip
+	networkCacheMutex.Unlock()
+}
+
+func RemoveIpFromAllocatedIpMap(networkName string, ip string) {
+	networkCacheMutex.Lock()
+	delete(allocatedIpMap[networkName], ip)
+	networkCacheMutex.Unlock()
+}
+
+// AddNetworkToAllocatedIpMap - add network to allocated ip map when network is added
+func AddNetworkToAllocatedIpMap(networkName string) {
+	networkCacheMutex.Lock()
+	allocatedIpMap[networkName] = map[string]net.IP{}
+	networkCacheMutex.Unlock()
+}
+
+// RemoveNetworkFromAllocatedIpMap - remove network from allocated ip map when network is deleted
+func RemoveNetworkFromAllocatedIpMap(networkName string) {
+	networkCacheMutex.Lock()
+	delete(allocatedIpMap, networkName)
+	networkCacheMutex.Unlock()
+}
+
+func getNetworksFromCache() (networks []models.Network) {
+	networkCacheMutex.RLock()
+	for _, network := range networkCacheMap {
+		networks = append(networks, network)
+	}
+	networkCacheMutex.RUnlock()
+	return
+}
+
+func deleteNetworkFromCache(key string) {
+	networkCacheMutex.Lock()
+	delete(networkCacheMap, key)
+	networkCacheMutex.Unlock()
+}
+
+func getNetworkFromCache(key string) (network models.Network, ok bool) {
+	networkCacheMutex.RLock()
+	network, ok = networkCacheMap[key]
+	networkCacheMutex.RUnlock()
+	return
+}
+
+func storeNetworkInCache(key string, network models.Network) {
+	networkCacheMutex.Lock()
+	networkCacheMap[key] = network
+	networkCacheMutex.Unlock()
+}
+
 // GetNetworks - returns all networks from database
 func GetNetworks() ([]models.Network, error) {
 	var networks []models.Network
-
+	if servercfg.CacheEnabled() {
+		networks := getNetworksFromCache()
+		if len(networks) != 0 {
+			return networks, nil
+		}
+	}
 	collection, err := database.FetchRecords(database.NETWORKS_TABLE_NAME)
 	if err != nil {
 		return networks, err
@@ -34,6 +144,9 @@ func GetNetworks() ([]models.Network, error) {
 		}
 		// add network our array
 		networks = append(networks, network)
+		if servercfg.CacheEnabled() {
+			storeNetworkInCache(network.NetID, network)
+		}
 	}
 
 	return networks, err
@@ -49,7 +162,14 @@ func DeleteNetwork(network string) error {
 	nodeCount, err := GetNetworkNonServerNodeCount(network)
 	if nodeCount == 0 || database.IsEmptyRecord(err) {
 		// delete server nodes first then db records
-		return database.DeleteRecord(database.NETWORKS_TABLE_NAME, network)
+		err = database.DeleteRecord(database.NETWORKS_TABLE_NAME, network)
+		if err != nil {
+			return err
+		}
+		if servercfg.CacheEnabled() {
+			deleteNetworkFromCache(network)
+		}
+		return nil
 	}
 	return errors.New("node check failed. All nodes must be deleted before deleting network")
 }
@@ -93,6 +213,9 @@ func CreateNetwork(network models.Network) (models.Network, error) {
 	if err = database.Insert(network.NetID, string(data), database.NETWORKS_TABLE_NAME); err != nil {
 		return models.Network{}, err
 	}
+	if servercfg.CacheEnabled() {
+		storeNetworkInCache(network.NetID, network)
+	}
 
 	return network, nil
 }
@@ -128,6 +251,11 @@ func intersect(n1, n2 *net.IPNet) bool {
 func GetParentNetwork(networkname string) (models.Network, error) {
 
 	var network models.Network
+	if servercfg.CacheEnabled() {
+		if network, ok := getNetworkFromCache(networkname); ok {
+			return network, nil
+		}
+	}
 	networkData, err := database.FetchRecord(database.NETWORKS_TABLE_NAME, networkname)
 	if err != nil {
 		return network, err
@@ -142,6 +270,11 @@ func GetParentNetwork(networkname string) (models.Network, error) {
 func GetNetworkSettings(networkname string) (models.Network, error) {
 
 	var network models.Network
+	if servercfg.CacheEnabled() {
+		if network, ok := getNetworkFromCache(networkname); ok {
+			return network, nil
+		}
+	}
 	networkData, err := database.FetchRecord(database.NETWORKS_TABLE_NAME, networkname)
 	if err != nil {
 		return network, err
@@ -177,9 +310,9 @@ func UniqueAddress(networkName string, reverse bool) (net.IP, error) {
 		newAddrs = net4.LastAddress()
 	}
 
+	ipAllocated := allocatedIpMap[networkName]
 	for {
-		if IsIPUnique(networkName, newAddrs.String(), database.NODES_TABLE_NAME, false) &&
-			IsIPUnique(networkName, newAddrs.String(), database.EXT_CLIENT_TABLE_NAME, false) {
+		if _, ok := ipAllocated[newAddrs.String()]; !ok {
 			return newAddrs, nil
 		}
 		if reverse {
@@ -266,9 +399,9 @@ func UniqueAddress6(networkName string, reverse bool) (net.IP, error) {
 		return add, err
 	}
 
+	ipAllocated := allocatedIpMap[networkName]
 	for {
-		if IsIPUnique(networkName, newAddrs.String(), database.NODES_TABLE_NAME, true) &&
-			IsIPUnique(networkName, newAddrs.String(), database.EXT_CLIENT_TABLE_NAME, true) {
+		if _, ok := ipAllocated[newAddrs.String()]; !ok {
 			return newAddrs, nil
 		}
 		if reverse {
@@ -320,6 +453,11 @@ func UpdateNetwork(currentNetwork *models.Network, newNetwork *models.Network) (
 		}
 		newNetwork.SetNetworkLastModified()
 		err = database.Insert(newNetwork.NetID, string(data), database.NETWORKS_TABLE_NAME)
+		if err == nil {
+			if servercfg.CacheEnabled() {
+				storeNetworkInCache(newNetwork.NetID, *newNetwork)
+			}
+		}
 		return hasrangeupdate4, hasrangeupdate6, hasholepunchupdate, err
 	}
 	// copy values
@@ -330,6 +468,11 @@ func UpdateNetwork(currentNetwork *models.Network, newNetwork *models.Network) (
 func GetNetwork(networkname string) (models.Network, error) {
 
 	var network models.Network
+	if servercfg.CacheEnabled() {
+		if network, ok := getNetworkFromCache(networkname); ok {
+			return network, nil
+		}
+	}
 	networkData, err := database.FetchRecord(database.NETWORKS_TABLE_NAME, networkname)
 	if err != nil {
 		return network, err
@@ -394,6 +537,9 @@ func SaveNetwork(network *models.Network) error {
 	if err := database.Insert(network.NetID, string(data), database.NETWORKS_TABLE_NAME); err != nil {
 		return err
 	}
+	if servercfg.CacheEnabled() {
+		storeNetworkInCache(network.NetID, *network)
+	}
 	return nil
 }
 
@@ -402,6 +548,11 @@ func NetworkExists(name string) (bool, error) {
 
 	var network string
 	var err error
+	if servercfg.CacheEnabled() {
+		if _, ok := getNetworkFromCache(name); ok {
+			return ok, nil
+		}
+	}
 	if network, err = database.FetchRecord(database.NETWORKS_TABLE_NAME, name); err != nil {
 		return false, err
 	}

+ 20 - 0
logic/nodes.go

@@ -116,6 +116,7 @@ func UpdateNodeCheckin(node *models.Node) error {
 	if err != nil {
 		return err
 	}
+
 	err = database.Insert(node.ID.String(), string(data), database.NODES_TABLE_NAME)
 	if err != nil {
 		return err
@@ -195,6 +196,10 @@ func DeleteNode(node *models.Node, purge bool) error {
 		if err := DeleteGatewayExtClients(node.ID.String(), node.Network); err != nil {
 			slog.Error("failed to delete ext clients", "nodeid", node.ID.String(), "error", err.Error())
 		}
+		host, err := GetHost(node.HostID.String())
+		if err == nil {
+			go DeleteRole(models.GetRAGRoleID(node.Network, host.ID.String()), true)
+		}
 	}
 	if node.IsRelayed {
 		// cleanup node from relayednodes on relay node
@@ -300,6 +305,13 @@ func DeleteNodeByID(node *models.Node) error {
 	if err = DeleteMetrics(node.ID.String()); err != nil {
 		logger.Log(1, "unable to remove metrics from DB for node", node.ID.String(), err.Error())
 	}
+	//recycle ip address
+	if node.Address.IP != nil {
+		RemoveIpFromAllocatedIpMap(node.Network, node.Address.IP.String())
+	}
+	if node.Address6.IP != nil {
+		RemoveIpFromAllocatedIpMap(node.Network, node.Address6.IP.String())
+	}
 	return nil
 }
 
@@ -585,6 +597,14 @@ func createNode(node *models.Node) error {
 	if servercfg.CacheEnabled() {
 		storeNodeInCache(*node)
 	}
+	if _, ok := allocatedIpMap[node.Network]; ok {
+		if node.Address.IP != nil {
+			AddIpToAllocatedIpMap(node.Network, node.Address.IP)
+		}
+		if node.Address6.IP != nil {
+			AddIpToAllocatedIpMap(node.Network, node.Address6.IP)
+		}
+	}
 	_, err = nodeacls.CreateNodeACL(nodeacls.NetworkID(node.Network), nodeacls.NodeID(node.ID.String()), defaultACLVal)
 	if err != nil {
 		logger.Log(1, "failed to create node ACL for node,", node.ID.String(), "err:", err.Error())

+ 1 - 1
logic/peers.go

@@ -436,7 +436,7 @@ func GetEgressIPs(peer *models.Node) []net.IPNet {
 
 	peerHost, err := GetHost(peer.HostID.String())
 	if err != nil {
-		logger.Log(0, "error retrieving host for peer", peer.ID.String(), err.Error())
+		logger.Log(0, "error retrieving host for peer", peer.ID.String(), "host id", peer.HostID.String(), err.Error())
 	}
 
 	// check for internet gateway

+ 26 - 2
logic/security.go

@@ -2,6 +2,7 @@ package logic
 
 import (
 	"net/http"
+	"net/url"
 	"strings"
 
 	"github.com/gorilla/mux"
@@ -17,20 +18,40 @@ const (
 	Unauthorized_Err = models.Error(Unauthorized_Msg)
 )
 
+var NetworkPermissionsCheck = func(username string, r *http.Request) error { return nil }
+var GlobalPermissionsCheck = func(username string, r *http.Request) error { return nil }
+
 // SecurityCheck - Check if user has appropriate permissions
 func SecurityCheck(reqAdmin bool, next http.Handler) http.HandlerFunc {
 
 	return func(w http.ResponseWriter, r *http.Request) {
 		r.Header.Set("ismaster", "no")
+		isGlobalAccesss := r.Header.Get("IS_GLOBAL_ACCESS") == "yes"
 		bearerToken := r.Header.Get("Authorization")
-		username, err := UserPermissions(reqAdmin, bearerToken)
+		username, err := GetUserNameFromToken(bearerToken)
 		if err != nil {
-			ReturnErrorResponse(w, r, FormatError(err, err.Error()))
+			ReturnErrorResponse(w, r, FormatError(err, "unauthorized"))
 			return
 		}
 		// detect masteradmin
 		if username == MasterUser {
 			r.Header.Set("ismaster", "yes")
+		} else {
+			if isGlobalAccesss {
+				err = GlobalPermissionsCheck(username, r)
+			} else {
+				err = NetworkPermissionsCheck(username, r)
+			}
+		}
+		w.Header().Set("TARGET_RSRC", r.Header.Get("TARGET_RSRC"))
+		w.Header().Set("TARGET_RSRC_ID", r.Header.Get("TARGET_RSRC_ID"))
+		w.Header().Set("RSRC_TYPE", r.Header.Get("RSRC_TYPE"))
+		w.Header().Set("IS_GLOBAL_ACCESS", r.Header.Get("IS_GLOBAL_ACCESS"))
+		w.Header().Set("Access-Control-Allow-Origin", "*")
+		if err != nil {
+			w.Header().Set("ACCESS_PERM", err.Error())
+			ReturnErrorResponse(w, r, FormatError(err, "forbidden"))
+			return
 		}
 		r.Header.Set("user", username)
 		next.ServeHTTP(w, r)
@@ -75,6 +96,9 @@ func ContinueIfUserMatch(next http.Handler) http.HandlerFunc {
 		}
 		var params = mux.Vars(r)
 		var requestedUser = params["username"]
+		if requestedUser == "" {
+			requestedUser, _ = url.QueryUnescape(r.URL.Query().Get("username"))
+		}
 		if requestedUser != r.Header.Get("user") {
 			ReturnErrorResponse(w, r, errorResponse)
 			return

+ 14 - 2
logic/telemetry.go

@@ -13,8 +13,11 @@ import (
 	"golang.org/x/exp/slog"
 )
 
-// flags to keep for telemetry
-var isFreeTier bool
+var (
+	// flags to keep for telemetry
+	isFreeTier      bool
+	telServerRecord = models.Telemetry{}
+)
 
 // posthog_pub_key - Key for sending data to PostHog
 const posthog_pub_key = "phc_1vEXhPOA1P7HP5jP2dVU9xDTUqXHAelmtravyZ1vvES"
@@ -125,6 +128,9 @@ func setTelemetryTimestamp(telRecord *models.Telemetry) error {
 		return err
 	}
 	err = database.Insert(database.SERVER_UUID_RECORD_KEY, string(jsonObj), database.SERVER_UUID_TABLE_NAME)
+	if err == nil {
+		telServerRecord = serverTelData
+	}
 	return err
 }
 
@@ -152,6 +158,9 @@ func getClientCount(nodes []models.Node) clientCount {
 
 // FetchTelemetryRecord - get the existing UUID and Timestamp from the DB
 func FetchTelemetryRecord() (models.Telemetry, error) {
+	if telServerRecord.TrafficKeyPub != nil {
+		return telServerRecord, nil
+	}
 	var rawData string
 	var telObj models.Telemetry
 	var err error
@@ -160,6 +169,9 @@ func FetchTelemetryRecord() (models.Telemetry, error) {
 		return telObj, err
 	}
 	err = json.Unmarshal([]byte(rawData), &telObj)
+	if err == nil {
+		telServerRecord = telObj
+	}
 	return telObj, err
 }
 

+ 96 - 0
logic/user_mgmt.go

@@ -0,0 +1,96 @@
+package logic
+
+import (
+	"encoding/json"
+
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/models"
+)
+
+// Pre-Define Permission Templates for default Roles
+var SuperAdminPermissionTemplate = models.UserRolePermissionTemplate{
+	ID:         models.SuperAdminRole,
+	Default:    true,
+	FullAccess: true,
+}
+
+var AdminPermissionTemplate = models.UserRolePermissionTemplate{
+	ID:         models.AdminRole,
+	Default:    true,
+	FullAccess: true,
+}
+
+var GetFilteredNodesByUserAccess = func(user models.User, nodes []models.Node) (filteredNodes []models.Node) {
+	return
+}
+
+var CreateRole = func(r models.UserRolePermissionTemplate) error {
+	return nil
+}
+
+var DeleteRole = func(r models.UserRoleID, force bool) error {
+	return nil
+}
+
+var FilterNetworksByRole = func(allnetworks []models.Network, user models.User) []models.Network {
+	return allnetworks
+}
+
+var IsGroupsValid = func(groups map[models.UserGroupID]struct{}) error {
+	return nil
+}
+var IsNetworkRolesValid = func(networkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) error {
+	return nil
+}
+
+var UpdateUserGwAccess = func(currentUser, changeUser models.User) {}
+
+var UpdateRole = func(r models.UserRolePermissionTemplate) error { return nil }
+
+var InitialiseRoles = userRolesInit
+var DeleteNetworkRoles = func(netID string) {}
+var CreateDefaultNetworkRolesAndGroups = func(netID models.NetworkID) {}
+
+// GetRole - fetches role template by id
+func GetRole(roleID models.UserRoleID) (models.UserRolePermissionTemplate, error) {
+	// check if role already exists
+	data, err := database.FetchRecord(database.USER_PERMISSIONS_TABLE_NAME, roleID.String())
+	if err != nil {
+		return models.UserRolePermissionTemplate{}, err
+	}
+	ur := models.UserRolePermissionTemplate{}
+	err = json.Unmarshal([]byte(data), &ur)
+	if err != nil {
+		return ur, err
+	}
+	return ur, nil
+}
+
+// ListPlatformRoles - lists user platform roles permission templates
+func ListPlatformRoles() ([]models.UserRolePermissionTemplate, error) {
+	data, err := database.FetchRecords(database.USER_PERMISSIONS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return []models.UserRolePermissionTemplate{}, err
+	}
+	userRoles := []models.UserRolePermissionTemplate{}
+	for _, dataI := range data {
+		userRole := models.UserRolePermissionTemplate{}
+		err := json.Unmarshal([]byte(dataI), &userRole)
+		if err != nil {
+			continue
+		}
+		if userRole.NetworkID != "" {
+			continue
+		}
+		userRoles = append(userRoles, userRole)
+	}
+	return userRoles, nil
+}
+
+func userRolesInit() {
+	d, _ := json.Marshal(SuperAdminPermissionTemplate)
+	database.Insert(SuperAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(AdminPermissionTemplate)
+	database.Insert(AdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+
+}

+ 76 - 4
logic/users.go

@@ -41,10 +41,13 @@ func GetReturnUser(username string) (models.ReturnUser, error) {
 // ToReturnUser - gets a user as a return user
 func ToReturnUser(user models.User) models.ReturnUser {
 	return models.ReturnUser{
-		UserName:     user.UserName,
-		IsSuperAdmin: user.IsSuperAdmin,
-		IsAdmin:      user.IsAdmin,
-		RemoteGwIDs:  user.RemoteGwIDs,
+		UserName:       user.UserName,
+		PlatformRoleID: user.PlatformRoleID,
+		AuthType:       user.AuthType,
+		UserGroups:     user.UserGroups,
+		NetworkRoles:   user.NetworkRoles,
+		RemoteGwIDs:    user.RemoteGwIDs,
+		LastLoginTime:  user.LastLoginTime,
 	}
 }
 
@@ -53,6 +56,12 @@ func SetUserDefaults(user *models.User) {
 	if user.RemoteGwIDs == nil {
 		user.RemoteGwIDs = make(map[string]struct{})
 	}
+	if len(user.NetworkRoles) == 0 {
+		user.NetworkRoles = make(map[models.NetworkID]map[models.UserRoleID]struct{})
+	}
+	if len(user.UserGroups) == 0 {
+		user.UserGroups = make(map[models.UserGroupID]struct{})
+	}
 }
 
 // SortUsers - Sorts slice of Users by username
@@ -119,3 +128,66 @@ func ListPendingUsers() ([]models.ReturnUser, error) {
 	}
 	return pendingUsers, nil
 }
+
+func GetUserMap() (map[string]models.User, error) {
+	userMap := make(map[string]models.User)
+	records, err := database.FetchRecords(database.USERS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return userMap, err
+	}
+	for _, record := range records {
+		u := models.User{}
+		err = json.Unmarshal([]byte(record), &u)
+		if err == nil {
+			userMap[u.UserName] = u
+		}
+	}
+	return userMap, nil
+}
+
+func InsertUserInvite(invite models.UserInvite) error {
+	data, err := json.Marshal(invite)
+	if err != nil {
+		return err
+	}
+	return database.Insert(invite.Email, string(data), database.USER_INVITES_TABLE_NAME)
+}
+
+func GetUserInvite(email string) (in models.UserInvite, err error) {
+	d, err := database.FetchRecord(database.USER_INVITES_TABLE_NAME, email)
+	if err != nil {
+		return
+	}
+	err = json.Unmarshal([]byte(d), &in)
+	return
+}
+
+func ListUserInvites() ([]models.UserInvite, error) {
+	invites := []models.UserInvite{}
+	records, err := database.FetchRecords(database.USER_INVITES_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return invites, err
+	}
+	for _, record := range records {
+		in := models.UserInvite{}
+		err = json.Unmarshal([]byte(record), &in)
+		if err == nil {
+			invites = append(invites, in)
+		}
+	}
+	return invites, nil
+}
+
+func DeleteUserInvite(email string) error {
+	return database.DeleteRecord(database.USER_INVITES_TABLE_NAME, email)
+}
+func ValidateAndApproveUserInvite(email, code string) error {
+	in, err := GetUserInvite(email)
+	if err != nil {
+		return err
+	}
+	if code != in.InviteCode {
+		return errors.New("invalid code")
+	}
+	return nil
+}

+ 38 - 6
main.go

@@ -27,7 +27,18 @@ import (
 	"golang.org/x/exp/slog"
 )
 
-var version = "v0.24.3"
+var version = "v0.25.0"
+
+//	@title			NetMaker
+//	@version		0.24.3
+//	@description	NetMaker API Docs
+//	@tag.name	    APIUsage
+//	@tag.description.markdown
+//	@tag.name	    Authentication
+//	@tag.description.markdown
+//	@tag.name	    Pricing
+//	@tag.description.markdown
+//  @host      api.demo.netmaker.io
 
 // Start DB Connection and start API Request Handler
 func main() {
@@ -37,6 +48,8 @@ func main() {
 	servercfg.SetVersion(version)
 	fmt.Println(models.RetrieveLogo()) // print the logo
 	initialize()                       // initial db and acls
+	logic.SetAllocatedIpMap()
+	defer logic.ClearAllocatedIpMap()
 	setGarbageCollection()
 	setVerbosity()
 	if servercfg.DeployedByOperator() && !servercfg.IsPro {
@@ -89,7 +102,7 @@ func initialize() { // Client Mode Prereq Check
 	migrate.Run()
 
 	logic.SetJWTSecret()
-
+	logic.InitialiseRoles()
 	err = serverctl.SetDefaults()
 	if err != nil {
 		logger.FatalLog("error setting defaults: ", err.Error())
@@ -135,7 +148,10 @@ func startControllers(wg *sync.WaitGroup, ctx context.Context) {
 	}
 
 	if !servercfg.IsRestBackend() && !servercfg.IsMessageQueueBackend() {
-		logger.Log(0, "No Server Mode selected, so nothing is being served! Set Rest mode (REST_BACKEND) or MessageQueue (MESSAGEQUEUE_BACKEND) to 'true'.")
+		logger.Log(
+			0,
+			"No Server Mode selected, so nothing is being served! Set Rest mode (REST_BACKEND) or MessageQueue (MESSAGEQUEUE_BACKEND) to 'true'.",
+		)
 	}
 
 	wg.Add(1)
@@ -167,10 +183,21 @@ func runMessageQueue(wg *sync.WaitGroup, ctx context.Context) {
 			node.Action = models.NODE_DELETE
 			node.PendingDelete = true
 			if err := mq.NodeUpdate(node); err != nil {
-				logger.Log(0, "failed to send peer update for deleted node: ", node.ID.String(), err.Error())
+				logger.Log(
+					0,
+					"failed to send peer update for deleted node: ",
+					node.ID.String(),
+					err.Error(),
+				)
 			}
 			if err := logic.DeleteNode(node, true); err != nil {
-				slog.Error("error deleting expired node", "nodeid", node.ID.String(), "error", err.Error())
+				slog.Error(
+					"error deleting expired node",
+					"nodeid",
+					node.ID.String(),
+					"error",
+					err.Error(),
+				)
 			}
 			go mq.PublishDeletedNodePeerUpdate(node)
 		}
@@ -189,7 +216,12 @@ func setVerbosity() {
 		}
 		return a
 	}
-	logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{AddSource: true, ReplaceAttr: replace, Level: logLevel}))
+	logger := slog.New(
+		slog.NewJSONHandler(
+			os.Stderr,
+			&slog.HandlerOptions{AddSource: true, ReplaceAttr: replace, Level: logLevel},
+		),
+	)
 	slog.SetDefault(logger)
 	switch verbose {
 	case 4:

+ 109 - 3
migrate/migrate.go

@@ -21,6 +21,7 @@ import (
 func Run() {
 	updateEnrollmentKeys()
 	assignSuperAdmin()
+	syncUsers()
 	updateHosts()
 	updateNodes()
 	updateAcls()
@@ -43,8 +44,7 @@ func assignSuperAdmin() {
 		if err != nil {
 			log.Fatal("error getting user", "user", owner, "error", err.Error())
 		}
-		user.IsSuperAdmin = true
-		user.IsAdmin = false
+		user.PlatformRoleID = models.SuperAdminRole
 		err = logic.UpsertUser(*user)
 		if err != nil {
 			log.Fatal(
@@ -64,8 +64,8 @@ func assignSuperAdmin() {
 				slog.Error("error getting user", "user", u.UserName, "error", err.Error())
 				continue
 			}
+			user.PlatformRoleID = models.SuperAdminRole
 			user.IsSuperAdmin = true
-			user.IsAdmin = false
 			err = logic.UpsertUser(*user)
 			if err != nil {
 				slog.Error(
@@ -311,3 +311,109 @@ func MigrateEmqx() {
 	}
 
 }
+
+func syncUsers() {
+	// create default network user roles for existing networks
+	if servercfg.IsPro {
+		networks, _ := logic.GetNetworks()
+		nodes, err := logic.GetAllNodes()
+		if err == nil {
+			for _, netI := range networks {
+				networkNodes := logic.GetNetworkNodesMemory(nodes, netI.NetID)
+				for _, networkNodeI := range networkNodes {
+					if networkNodeI.IsIngressGateway {
+						h, err := logic.GetHost(networkNodeI.HostID.String())
+						if err == nil {
+							logic.CreateRole(models.UserRolePermissionTemplate{
+								ID:        models.GetRAGRoleID(networkNodeI.Network, h.ID.String()),
+								UiName:    models.GetRAGRoleName(networkNodeI.Network, h.Name),
+								NetworkID: models.NetworkID(netI.NetID),
+								NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
+									models.RemoteAccessGwRsrc: {
+										models.RsrcID(networkNodeI.ID.String()): models.RsrcPermissionScope{
+											Read:      true,
+											VPNaccess: true,
+										},
+									},
+									models.ExtClientsRsrc: {
+										models.AllExtClientsRsrcID: models.RsrcPermissionScope{
+											Read:     true,
+											Create:   true,
+											Update:   true,
+											Delete:   true,
+											SelfOnly: true,
+										},
+									},
+								},
+							})
+						}
+
+					}
+				}
+			}
+		}
+	}
+
+	users, err := logic.GetUsersDB()
+	if err == nil {
+		for _, user := range users {
+			user := user
+			if user.PlatformRoleID == models.AdminRole && !user.IsAdmin {
+				user.IsAdmin = true
+				logic.UpsertUser(user)
+			}
+			if user.PlatformRoleID == models.SuperAdminRole && !user.IsSuperAdmin {
+				user.IsSuperAdmin = true
+				logic.UpsertUser(user)
+			}
+			if user.PlatformRoleID.String() != "" {
+				continue
+			}
+			user.AuthType = models.BasicAuth
+			if logic.IsOauthUser(&user) == nil {
+				user.AuthType = models.OAuth
+			}
+			if len(user.NetworkRoles) == 0 {
+				user.NetworkRoles = make(map[models.NetworkID]map[models.UserRoleID]struct{})
+			}
+			if len(user.UserGroups) == 0 {
+				user.UserGroups = make(map[models.UserGroupID]struct{})
+			}
+			if user.IsSuperAdmin {
+				user.PlatformRoleID = models.SuperAdminRole
+
+			} else if user.IsAdmin {
+				user.PlatformRoleID = models.AdminRole
+			} else {
+				user.PlatformRoleID = models.ServiceUser
+			}
+			logic.UpsertUser(user)
+			if len(user.RemoteGwIDs) > 0 {
+				// define user roles for network
+				// assign relevant network role to user
+				for remoteGwID := range user.RemoteGwIDs {
+					gwNode, err := logic.GetNodeByID(remoteGwID)
+					if err != nil {
+						continue
+					}
+					h, err := logic.GetHost(gwNode.HostID.String())
+					if err != nil {
+						continue
+					}
+					r, err := logic.GetRole(models.GetRAGRoleID(gwNode.Network, h.ID.String()))
+					if err != nil {
+						continue
+					}
+					if netRoles, ok := user.NetworkRoles[models.NetworkID(gwNode.Network)]; ok {
+						netRoles[r.ID] = struct{}{}
+					} else {
+						user.NetworkRoles[models.NetworkID(gwNode.Network)] = map[models.UserRoleID]struct{}{
+							r.ID: {},
+						}
+					}
+				}
+				logic.UpsertUser(user)
+			}
+		}
+	}
+}

+ 94 - 91
models/node.go

@@ -52,111 +52,111 @@ type Iface struct {
 
 // CommonNode - represents a commonn node data elements shared by netmaker and netclient
 type CommonNode struct {
-	ID                  uuid.UUID `json:"id" yaml:"id"`
-	HostID              uuid.UUID `json:"hostid" yaml:"hostid"`
-	Network             string    `json:"network" yaml:"network"`
-	NetworkRange        net.IPNet `json:"networkrange" yaml:"networkrange"`
-	NetworkRange6       net.IPNet `json:"networkrange6" yaml:"networkrange6"`
-	Server              string    `json:"server" yaml:"server"`
-	Connected           bool      `json:"connected" yaml:"connected"`
-	Address             net.IPNet `json:"address" yaml:"address"`
-	Address6            net.IPNet `json:"address6" yaml:"address6"`
-	Action              string    `json:"action" yaml:"action"`
-	LocalAddress        net.IPNet `json:"localaddress" yaml:"localaddress"`
-	IsEgressGateway     bool      `json:"isegressgateway" yaml:"isegressgateway"`
-	EgressGatewayRanges []string  `json:"egressgatewayranges" bson:"egressgatewayranges" yaml:"egressgatewayranges"`
-	IsIngressGateway    bool      `json:"isingressgateway" yaml:"isingressgateway"`
-	IsRelayed           bool      `json:"isrelayed" bson:"isrelayed" yaml:"isrelayed"`
-	RelayedBy           string    `json:"relayedby" bson:"relayedby" yaml:"relayedby"`
-	IsRelay             bool      `json:"isrelay" bson:"isrelay" yaml:"isrelay"`
-	RelayedNodes        []string  `json:"relaynodes" yaml:"relayedNodes"`
-	IngressDNS          string    `json:"ingressdns" yaml:"ingressdns"`
-	DNSOn               bool      `json:"dnson" yaml:"dnson"`
+	ID                  uuid.UUID `json:"id"                  yaml:"id"`
+	HostID              uuid.UUID `json:"hostid"              yaml:"hostid"`
+	Network             string    `json:"network"             yaml:"network"`
+	NetworkRange        net.IPNet `json:"networkrange"        yaml:"networkrange"        swaggertype:"primitive,integer"`
+	NetworkRange6       net.IPNet `json:"networkrange6"       yaml:"networkrange6"       swaggertype:"primitive,number"`
+	Server              string    `json:"server"              yaml:"server"`
+	Connected           bool      `json:"connected"           yaml:"connected"`
+	Address             net.IPNet `json:"address"             yaml:"address"`
+	Address6            net.IPNet `json:"address6"            yaml:"address6"`
+	Action              string    `json:"action"              yaml:"action"`
+	LocalAddress        net.IPNet `json:"localaddress"        yaml:"localaddress"`
+	IsEgressGateway     bool      `json:"isegressgateway"     yaml:"isegressgateway"`
+	EgressGatewayRanges []string  `json:"egressgatewayranges" yaml:"egressgatewayranges"                                 bson:"egressgatewayranges"`
+	IsIngressGateway    bool      `json:"isingressgateway"    yaml:"isingressgateway"`
+	IsRelayed           bool      `json:"isrelayed"           yaml:"isrelayed"                                           bson:"isrelayed"`
+	RelayedBy           string    `json:"relayedby"           yaml:"relayedby"                                           bson:"relayedby"`
+	IsRelay             bool      `json:"isrelay"             yaml:"isrelay"                                             bson:"isrelay"`
+	RelayedNodes        []string  `json:"relaynodes"          yaml:"relayedNodes"`
+	IngressDNS          string    `json:"ingressdns"          yaml:"ingressdns"`
+	DNSOn               bool      `json:"dnson"               yaml:"dnson"`
 }
 
 // Node - a model of a network node
 type Node struct {
 	CommonNode
-	PendingDelete           bool                 `json:"pendingdelete" bson:"pendingdelete" yaml:"pendingdelete"`
-	LastModified            time.Time            `json:"lastmodified" bson:"lastmodified" yaml:"lastmodified"`
-	LastCheckIn             time.Time            `json:"lastcheckin" bson:"lastcheckin" yaml:"lastcheckin"`
-	LastPeerUpdate          time.Time            `json:"lastpeerupdate" bson:"lastpeerupdate" yaml:"lastpeerupdate"`
-	ExpirationDateTime      time.Time            `json:"expdatetime" bson:"expdatetime" yaml:"expdatetime"`
+	PendingDelete           bool                 `json:"pendingdelete"           bson:"pendingdelete"           yaml:"pendingdelete"`
+	LastModified            time.Time            `json:"lastmodified"            bson:"lastmodified"            yaml:"lastmodified"`
+	LastCheckIn             time.Time            `json:"lastcheckin"             bson:"lastcheckin"             yaml:"lastcheckin"`
+	LastPeerUpdate          time.Time            `json:"lastpeerupdate"          bson:"lastpeerupdate"          yaml:"lastpeerupdate"`
+	ExpirationDateTime      time.Time            `json:"expdatetime"             bson:"expdatetime"             yaml:"expdatetime"`
 	EgressGatewayNatEnabled bool                 `json:"egressgatewaynatenabled" bson:"egressgatewaynatenabled" yaml:"egressgatewaynatenabled"`
-	EgressGatewayRequest    EgressGatewayRequest `json:"egressgatewayrequest" bson:"egressgatewayrequest" yaml:"egressgatewayrequest"`
-	IngressGatewayRange     string               `json:"ingressgatewayrange" bson:"ingressgatewayrange" yaml:"ingressgatewayrange"`
-	IngressGatewayRange6    string               `json:"ingressgatewayrange6" bson:"ingressgatewayrange6" yaml:"ingressgatewayrange6"`
+	EgressGatewayRequest    EgressGatewayRequest `json:"egressgatewayrequest"    bson:"egressgatewayrequest"    yaml:"egressgatewayrequest"`
+	IngressGatewayRange     string               `json:"ingressgatewayrange"     bson:"ingressgatewayrange"     yaml:"ingressgatewayrange"`
+	IngressGatewayRange6    string               `json:"ingressgatewayrange6"    bson:"ingressgatewayrange6"    yaml:"ingressgatewayrange6"`
 	Metadata                string               `json:"metadata"`
 	// == PRO ==
-	DefaultACL        string              `json:"defaultacl,omitempty" bson:"defaultacl,omitempty" yaml:"defaultacl,omitempty" validate:"checkyesornoorunset"`
-	OwnerID           string              `json:"ownerid,omitempty" bson:"ownerid,omitempty" yaml:"ownerid,omitempty"`
-	IsFailOver        bool                `json:"is_fail_over" yaml:"is_fail_over"`
-	FailOverPeers     map[string]struct{} `json:"fail_over_peers" yaml:"fail_over_peers"`
-	FailedOverBy      uuid.UUID           `json:"failed_over_by" yaml:"failed_over_by"`
-	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"`
-	AdditionalRagIps  []net.IP            `json:"additional_rag_ips" yaml:"additional_rag_ips"`
+	DefaultACL        string              `json:"defaultacl,omitempty"    bson:"defaultacl,omitempty"    yaml:"defaultacl,omitempty"    validate:"checkyesornoorunset"`
+	OwnerID           string              `json:"ownerid,omitempty"       bson:"ownerid,omitempty"       yaml:"ownerid,omitempty"`
+	IsFailOver        bool                `json:"is_fail_over"                                           yaml:"is_fail_over"`
+	FailOverPeers     map[string]struct{} `json:"fail_over_peers"                                        yaml:"fail_over_peers"`
+	FailedOverBy      uuid.UUID           `json:"failed_over_by"                                         yaml:"failed_over_by"`
+	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"`
+	AdditionalRagIps  []net.IP            `json:"additional_rag_ips"                                     yaml:"additional_rag_ips"                                     swaggertype:"array,number"`
 }
 
 // LegacyNode - legacy struct for node model
 type LegacyNode struct {
-	ID                      string               `json:"id,omitempty" bson:"id,omitempty" yaml:"id,omitempty" validate:"required,min=5,id_unique"`
-	Address                 string               `json:"address" bson:"address" yaml:"address" validate:"omitempty,ipv4"`
-	Address6                string               `json:"address6" bson:"address6" yaml:"address6" validate:"omitempty,ipv6"`
-	LocalAddress            string               `json:"localaddress" bson:"localaddress" yaml:"localaddress" validate:"omitempty"`
-	Interfaces              []Iface              `json:"interfaces" yaml:"interfaces"`
-	Name                    string               `json:"name" bson:"name" yaml:"name" validate:"omitempty,max=62,in_charset"`
-	NetworkSettings         Network              `json:"networksettings" bson:"networksettings" yaml:"networksettings" validate:"-"`
-	ListenPort              int32                `json:"listenport" bson:"listenport" yaml:"listenport" validate:"omitempty,numeric,min=1024,max=65535"`
-	LocalListenPort         int32                `json:"locallistenport" bson:"locallistenport" yaml:"locallistenport" validate:"numeric,min=0,max=65535"`
-	PublicKey               string               `json:"publickey" bson:"publickey" yaml:"publickey" validate:"required,base64"`
-	Endpoint                string               `json:"endpoint" bson:"endpoint" yaml:"endpoint" validate:"required,ip"`
-	AllowedIPs              []string             `json:"allowedips" bson:"allowedips" yaml:"allowedips"`
-	PersistentKeepalive     int32                `json:"persistentkeepalive" bson:"persistentkeepalive" yaml:"persistentkeepalive" validate:"omitempty,numeric,max=1000"`
-	IsHub                   string               `json:"ishub" bson:"ishub" yaml:"ishub" validate:"checkyesorno"`
-	AccessKey               string               `json:"accesskey" bson:"accesskey" yaml:"accesskey"`
-	Interface               string               `json:"interface" bson:"interface" yaml:"interface"`
-	LastModified            int64                `json:"lastmodified" bson:"lastmodified" yaml:"lastmodified"`
-	ExpirationDateTime      int64                `json:"expdatetime" bson:"expdatetime" yaml:"expdatetime"`
-	LastPeerUpdate          int64                `json:"lastpeerupdate" bson:"lastpeerupdate" yaml:"lastpeerupdate"`
-	LastCheckIn             int64                `json:"lastcheckin" bson:"lastcheckin" yaml:"lastcheckin"`
-	MacAddress              string               `json:"macaddress" bson:"macaddress" yaml:"macaddress"`
-	Password                string               `json:"password" bson:"password" yaml:"password" validate:"required,min=6"`
-	Network                 string               `json:"network" bson:"network" yaml:"network" validate:"network_exists"`
-	IsRelayed               string               `json:"isrelayed" bson:"isrelayed" yaml:"isrelayed"`
-	IsPending               string               `json:"ispending" bson:"ispending" yaml:"ispending"`
-	IsRelay                 string               `json:"isrelay" bson:"isrelay" yaml:"isrelay" validate:"checkyesorno"`
-	IsDocker                string               `json:"isdocker" bson:"isdocker" yaml:"isdocker" validate:"checkyesorno"`
-	IsK8S                   string               `json:"isk8s" bson:"isk8s" yaml:"isk8s" validate:"checkyesorno"`
-	IsEgressGateway         string               `json:"isegressgateway" bson:"isegressgateway" yaml:"isegressgateway" validate:"checkyesorno"`
-	IsIngressGateway        string               `json:"isingressgateway" bson:"isingressgateway" yaml:"isingressgateway" validate:"checkyesorno"`
-	EgressGatewayRanges     []string             `json:"egressgatewayranges" bson:"egressgatewayranges" yaml:"egressgatewayranges"`
+	ID                      string               `json:"id,omitempty"            bson:"id,omitempty"            yaml:"id,omitempty"            validate:"required,min=5,id_unique"`
+	Address                 string               `json:"address"                 bson:"address"                 yaml:"address"                 validate:"omitempty,ipv4"`
+	Address6                string               `json:"address6"                bson:"address6"                yaml:"address6"                validate:"omitempty,ipv6"`
+	LocalAddress            string               `json:"localaddress"            bson:"localaddress"            yaml:"localaddress"            validate:"omitempty"`
+	Interfaces              []Iface              `json:"interfaces"                                             yaml:"interfaces"`
+	Name                    string               `json:"name"                    bson:"name"                    yaml:"name"                    validate:"omitempty,max=62,in_charset"`
+	NetworkSettings         Network              `json:"networksettings"         bson:"networksettings"         yaml:"networksettings"         validate:"-"`
+	ListenPort              int32                `json:"listenport"              bson:"listenport"              yaml:"listenport"              validate:"omitempty,numeric,min=1024,max=65535"`
+	LocalListenPort         int32                `json:"locallistenport"         bson:"locallistenport"         yaml:"locallistenport"         validate:"numeric,min=0,max=65535"`
+	PublicKey               string               `json:"publickey"               bson:"publickey"               yaml:"publickey"               validate:"required,base64"`
+	Endpoint                string               `json:"endpoint"                bson:"endpoint"                yaml:"endpoint"                validate:"required,ip"`
+	AllowedIPs              []string             `json:"allowedips"              bson:"allowedips"              yaml:"allowedips"`
+	PersistentKeepalive     int32                `json:"persistentkeepalive"     bson:"persistentkeepalive"     yaml:"persistentkeepalive"     validate:"omitempty,numeric,max=1000"`
+	IsHub                   string               `json:"ishub"                   bson:"ishub"                   yaml:"ishub"                   validate:"checkyesorno"`
+	AccessKey               string               `json:"accesskey"               bson:"accesskey"               yaml:"accesskey"`
+	Interface               string               `json:"interface"               bson:"interface"               yaml:"interface"`
+	LastModified            int64                `json:"lastmodified"            bson:"lastmodified"            yaml:"lastmodified"`
+	ExpirationDateTime      int64                `json:"expdatetime"             bson:"expdatetime"             yaml:"expdatetime"`
+	LastPeerUpdate          int64                `json:"lastpeerupdate"          bson:"lastpeerupdate"          yaml:"lastpeerupdate"`
+	LastCheckIn             int64                `json:"lastcheckin"             bson:"lastcheckin"             yaml:"lastcheckin"`
+	MacAddress              string               `json:"macaddress"              bson:"macaddress"              yaml:"macaddress"`
+	Password                string               `json:"password"                bson:"password"                yaml:"password"                validate:"required,min=6"`
+	Network                 string               `json:"network"                 bson:"network"                 yaml:"network"                 validate:"network_exists"`
+	IsRelayed               string               `json:"isrelayed"               bson:"isrelayed"               yaml:"isrelayed"`
+	IsPending               string               `json:"ispending"               bson:"ispending"               yaml:"ispending"`
+	IsRelay                 string               `json:"isrelay"                 bson:"isrelay"                 yaml:"isrelay"                 validate:"checkyesorno"`
+	IsDocker                string               `json:"isdocker"                bson:"isdocker"                yaml:"isdocker"                validate:"checkyesorno"`
+	IsK8S                   string               `json:"isk8s"                   bson:"isk8s"                   yaml:"isk8s"                   validate:"checkyesorno"`
+	IsEgressGateway         string               `json:"isegressgateway"         bson:"isegressgateway"         yaml:"isegressgateway"         validate:"checkyesorno"`
+	IsIngressGateway        string               `json:"isingressgateway"        bson:"isingressgateway"        yaml:"isingressgateway"        validate:"checkyesorno"`
+	EgressGatewayRanges     []string             `json:"egressgatewayranges"     bson:"egressgatewayranges"     yaml:"egressgatewayranges"`
 	EgressGatewayNatEnabled string               `json:"egressgatewaynatenabled" bson:"egressgatewaynatenabled" yaml:"egressgatewaynatenabled"`
-	EgressGatewayRequest    EgressGatewayRequest `json:"egressgatewayrequest" bson:"egressgatewayrequest" yaml:"egressgatewayrequest"`
-	RelayAddrs              []string             `json:"relayaddrs" bson:"relayaddrs" yaml:"relayaddrs"`
-	FailoverNode            string               `json:"failovernode" bson:"failovernode" yaml:"failovernode"`
-	IngressGatewayRange     string               `json:"ingressgatewayrange" bson:"ingressgatewayrange" yaml:"ingressgatewayrange"`
-	IngressGatewayRange6    string               `json:"ingressgatewayrange6" bson:"ingressgatewayrange6" yaml:"ingressgatewayrange6"`
+	EgressGatewayRequest    EgressGatewayRequest `json:"egressgatewayrequest"    bson:"egressgatewayrequest"    yaml:"egressgatewayrequest"`
+	RelayAddrs              []string             `json:"relayaddrs"              bson:"relayaddrs"              yaml:"relayaddrs"`
+	FailoverNode            string               `json:"failovernode"            bson:"failovernode"            yaml:"failovernode"`
+	IngressGatewayRange     string               `json:"ingressgatewayrange"     bson:"ingressgatewayrange"     yaml:"ingressgatewayrange"`
+	IngressGatewayRange6    string               `json:"ingressgatewayrange6"    bson:"ingressgatewayrange6"    yaml:"ingressgatewayrange6"`
 	// IsStatic - refers to if the Endpoint is set manually or dynamically
-	IsStatic        string      `json:"isstatic" bson:"isstatic" yaml:"isstatic" validate:"checkyesorno"`
-	UDPHolePunch    string      `json:"udpholepunch" bson:"udpholepunch" yaml:"udpholepunch" validate:"checkyesorno"`
-	DNSOn           string      `json:"dnson" bson:"dnson" yaml:"dnson" validate:"checkyesorno"`
-	IsServer        string      `json:"isserver" bson:"isserver" yaml:"isserver" validate:"checkyesorno"`
-	Action          string      `json:"action" bson:"action" yaml:"action"`
-	IPForwarding    string      `json:"ipforwarding" bson:"ipforwarding" yaml:"ipforwarding" validate:"checkyesorno"`
-	OS              string      `json:"os" bson:"os" yaml:"os"`
-	MTU             int32       `json:"mtu" bson:"mtu" yaml:"mtu"`
-	Version         string      `json:"version" bson:"version" yaml:"version"`
-	Server          string      `json:"server" bson:"server" yaml:"server"`
-	TrafficKeys     TrafficKeys `json:"traffickeys" bson:"traffickeys" yaml:"traffickeys"`
-	FirewallInUse   string      `json:"firewallinuse" bson:"firewallinuse" yaml:"firewallinuse"`
-	InternetGateway string      `json:"internetgateway" bson:"internetgateway" yaml:"internetgateway"`
-	Connected       string      `json:"connected" bson:"connected" yaml:"connected" validate:"checkyesorno"`
+	IsStatic        string      `json:"isstatic"                bson:"isstatic"                yaml:"isstatic"                validate:"checkyesorno"`
+	UDPHolePunch    string      `json:"udpholepunch"            bson:"udpholepunch"            yaml:"udpholepunch"            validate:"checkyesorno"`
+	DNSOn           string      `json:"dnson"                   bson:"dnson"                   yaml:"dnson"                   validate:"checkyesorno"`
+	IsServer        string      `json:"isserver"                bson:"isserver"                yaml:"isserver"                validate:"checkyesorno"`
+	Action          string      `json:"action"                  bson:"action"                  yaml:"action"`
+	IPForwarding    string      `json:"ipforwarding"            bson:"ipforwarding"            yaml:"ipforwarding"            validate:"checkyesorno"`
+	OS              string      `json:"os"                      bson:"os"                      yaml:"os"`
+	MTU             int32       `json:"mtu"                     bson:"mtu"                     yaml:"mtu"`
+	Version         string      `json:"version"                 bson:"version"                 yaml:"version"`
+	Server          string      `json:"server"                  bson:"server"                  yaml:"server"`
+	TrafficKeys     TrafficKeys `json:"traffickeys"             bson:"traffickeys"             yaml:"traffickeys"`
+	FirewallInUse   string      `json:"firewallinuse"           bson:"firewallinuse"           yaml:"firewallinuse"`
+	InternetGateway string      `json:"internetgateway"         bson:"internetgateway"         yaml:"internetgateway"`
+	Connected       string      `json:"connected"               bson:"connected"               yaml:"connected"               validate:"checkyesorno"`
 	// == PRO ==
-	DefaultACL string `json:"defaultacl,omitempty" bson:"defaultacl,omitempty" yaml:"defaultacl,omitempty" validate:"checkyesornoorunset"`
-	OwnerID    string `json:"ownerid,omitempty" bson:"ownerid,omitempty" yaml:"ownerid,omitempty"`
-	Failover   string `json:"failover" bson:"failover" yaml:"failover" validate:"checkyesorno"`
+	DefaultACL string `json:"defaultacl,omitempty"    bson:"defaultacl,omitempty"    yaml:"defaultacl,omitempty"    validate:"checkyesornoorunset"`
+	OwnerID    string `json:"ownerid,omitempty"       bson:"ownerid,omitempty"       yaml:"ownerid,omitempty"`
+	Failover   string `json:"failover"                bson:"failover"                yaml:"failover"                validate:"checkyesorno"`
 }
 
 // NodesArray - used for node sorting
@@ -375,7 +375,10 @@ func (node *LegacyNode) SetDefaultFailover() {
 }
 
 // Node.Fill - fills other node data into calling node data if not set on calling node (skips DNSOn)
-func (newNode *Node) Fill(currentNode *Node, isPro bool) { // TODO add new field for nftables present
+func (newNode *Node) Fill(
+	currentNode *Node,
+	isPro bool,
+) { // TODO add new field for nftables present
 	newNode.ID = currentNode.ID
 	newNode.HostID = currentNode.HostID
 	// Revisit the logic for boolean values

+ 5 - 33
models/structs.go

@@ -23,39 +23,6 @@ type AuthParams struct {
 	Password   string `json:"password"`
 }
 
-// User struct - struct for Users
-type User struct {
-	UserName      string              `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"`
-	Password      string              `json:"password" bson:"password" validate:"required,min=5"`
-	IsAdmin       bool                `json:"isadmin" bson:"isadmin"`
-	IsSuperAdmin  bool                `json:"issuperadmin"`
-	RemoteGwIDs   map[string]struct{} `json:"remote_gw_ids"`
-	LastLoginTime time.Time           `json:"last_login_time"`
-}
-
-// ReturnUser - return user struct
-type ReturnUser struct {
-	UserName      string              `json:"username"`
-	IsAdmin       bool                `json:"isadmin"`
-	IsSuperAdmin  bool                `json:"issuperadmin"`
-	RemoteGwIDs   map[string]struct{} `json:"remote_gw_ids"`
-	LastLoginTime time.Time           `json:"last_login_time"`
-}
-
-// UserAuthParams - user auth params struct
-type UserAuthParams struct {
-	UserName string `json:"username"`
-	Password string `json:"password"`
-}
-
-// UserClaims - user claims struct
-type UserClaims struct {
-	IsAdmin      bool
-	IsSuperAdmin bool
-	UserName     string
-	jwt.RegisteredClaims
-}
-
 // IngressGwUsers - struct to hold users on a ingress gw
 type IngressGwUsers struct {
 	NodeID  string       `json:"node_id"`
@@ -381,3 +348,8 @@ const (
 type GetClientConfReqDto struct {
 	PreferredIp string `json:"preferred_ip"`
 }
+
+type RsrcURLInfo struct {
+	Method string
+	Path   string
+}

+ 199 - 0
models/user_mgmt.go

@@ -0,0 +1,199 @@
+package models
+
+import (
+	"fmt"
+	"time"
+
+	jwt "github.com/golang-jwt/jwt/v4"
+)
+
+type NetworkID string
+type RsrcType string
+type RsrcID string
+type UserRoleID string
+type UserGroupID string
+type AuthType string
+
+var (
+	BasicAuth AuthType = "basic_auth"
+	OAuth     AuthType = "oauth"
+)
+
+func (r RsrcType) String() string {
+	return string(r)
+}
+
+func (rid RsrcID) String() string {
+	return string(rid)
+}
+
+func GetRAGRoleName(netID, hostName string) string {
+	return fmt.Sprintf("netID-%s-rag-%s", netID, hostName)
+}
+
+func GetRAGRoleID(netID, hostID string) UserRoleID {
+	return UserRoleID(fmt.Sprintf("netID-%s-rag-%s", netID, hostID))
+}
+
+var RsrcTypeMap = map[RsrcType]struct{}{
+	HostRsrc:           {},
+	RelayRsrc:          {},
+	RemoteAccessGwRsrc: {},
+	ExtClientsRsrc:     {},
+	InetGwRsrc:         {},
+	EgressGwRsrc:       {},
+	NetworkRsrc:        {},
+	EnrollmentKeysRsrc: {},
+	UserRsrc:           {},
+	AclRsrc:            {},
+	DnsRsrc:            {},
+	FailOverRsrc:       {},
+}
+
+const AllNetworks NetworkID = "all_networks"
+const (
+	HostRsrc           RsrcType = "hosts"
+	RelayRsrc          RsrcType = "relays"
+	RemoteAccessGwRsrc RsrcType = "remote_access_gw"
+	ExtClientsRsrc     RsrcType = "extclients"
+	InetGwRsrc         RsrcType = "inet_gw"
+	EgressGwRsrc       RsrcType = "egress"
+	NetworkRsrc        RsrcType = "networks"
+	EnrollmentKeysRsrc RsrcType = "enrollment_key"
+	UserRsrc           RsrcType = "users"
+	AclRsrc            RsrcType = "acl"
+	DnsRsrc            RsrcType = "dns"
+	FailOverRsrc       RsrcType = "fail_over"
+	MetricRsrc         RsrcType = "metrics"
+)
+
+const (
+	AllHostRsrcID           RsrcID = "all_host"
+	AllRelayRsrcID          RsrcID = "all_relay"
+	AllRemoteAccessGwRsrcID RsrcID = "all_remote_access_gw"
+	AllExtClientsRsrcID     RsrcID = "all_extclients"
+	AllInetGwRsrcID         RsrcID = "all_inet_gw"
+	AllEgressGwRsrcID       RsrcID = "all_egress"
+	AllNetworkRsrcID        RsrcID = "all_network"
+	AllEnrollmentKeysRsrcID RsrcID = "all_enrollment_key"
+	AllUserRsrcID           RsrcID = "all_user"
+	AllDnsRsrcID            RsrcID = "all_dns"
+	AllFailOverRsrcID       RsrcID = "all_fail_over"
+	AllAclsRsrcID           RsrcID = "all_acls"
+)
+
+// Pre-Defined User Roles
+
+const (
+	SuperAdminRole UserRoleID = "super-admin"
+	AdminRole      UserRoleID = "admin"
+	ServiceUser    UserRoleID = "service-user"
+	PlatformUser   UserRoleID = "platform-user"
+	NetworkAdmin   UserRoleID = "network-admin"
+	NetworkUser    UserRoleID = "network-user"
+)
+
+func (r UserRoleID) String() string {
+	return string(r)
+}
+
+func (g UserGroupID) String() string {
+	return string(g)
+}
+
+func (n NetworkID) String() string {
+	return string(n)
+}
+
+type RsrcPermissionScope struct {
+	Create    bool `json:"create"`
+	Read      bool `json:"read"`
+	Update    bool `json:"update"`
+	Delete    bool `json:"delete"`
+	VPNaccess bool `json:"vpn_access"`
+	SelfOnly  bool `json:"self_only"`
+}
+
+type UserRolePermissionTemplate struct {
+	ID                  UserRoleID                                  `json:"id"`
+	UiName              string                                      `json:"ui_name"`
+	Default             bool                                        `json:"default"`
+	DenyDashboardAccess bool                                        `json:"deny_dashboard_access"`
+	FullAccess          bool                                        `json:"full_access"`
+	NetworkID           NetworkID                                   `json:"network_id"`
+	NetworkLevelAccess  map[RsrcType]map[RsrcID]RsrcPermissionScope `json:"network_level_access"`
+	GlobalLevelAccess   map[RsrcType]map[RsrcID]RsrcPermissionScope `json:"global_level_access"`
+}
+
+type CreateGroupReq struct {
+	Group   UserGroup `json:"user_group"`
+	Members []string  `json:"members"`
+}
+
+type UserGroup struct {
+	ID           UserGroupID                           `json:"id"`
+	NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+	MetaData     string                                `json:"meta_data"`
+}
+
+// User struct - struct for Users
+type User struct {
+	UserName       string                                `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"`
+	Password       string                                `json:"password" bson:"password" validate:"required,min=5"`
+	IsAdmin        bool                                  `json:"isadmin" bson:"isadmin"` // deprecated
+	IsSuperAdmin   bool                                  `json:"issuperadmin"`           // deprecated
+	RemoteGwIDs    map[string]struct{}                   `json:"remote_gw_ids"`          // deprecated
+	AuthType       AuthType                              `json:"auth_type"`
+	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"`
+}
+
+type ReturnUserWithRolesAndGroups struct {
+	ReturnUser
+	PlatformRole UserRolePermissionTemplate `json:"platform_role"`
+}
+
+// ReturnUser - return user 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"`
+}
+
+// UserAuthParams - user auth params struct
+type UserAuthParams struct {
+	UserName string `json:"username"`
+	Password string `json:"password"`
+}
+
+// UserClaims - user claims struct
+type UserClaims struct {
+	Role     UserRoleID
+	UserName string
+	jwt.RegisteredClaims
+}
+
+type InviteUsersReq struct {
+	UserEmails     []string                              `json:"user_emails"`
+	PlatformRoleID string                                `json:"platform_role_id"`
+	UserGroups     map[UserGroupID]struct{}              `json:"user_group_ids"`
+	NetworkRoles   map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+}
+
+// UserInvite - model for user invite
+type UserInvite struct {
+	Email          string                                `json:"email"`
+	PlatformRoleID string                                `json:"platform_role_id"`
+	UserGroups     map[UserGroupID]struct{}              `json:"user_group_ids"`
+	NetworkRoles   map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"`
+	InviteCode     string                                `json:"invite_code"`
+	InviteURL      string                                `json:"invite_url"`
+}

+ 7 - 2
mq/emqx_on_prem.go

@@ -63,6 +63,7 @@ func getEmqxAuthToken() (string, error) {
 	if err != nil {
 		return "", err
 	}
+	defer resp.Body.Close()
 	msg, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return "", err
@@ -206,7 +207,9 @@ func (e *EmqxOnPrem) CreateEmqxDefaultAuthenticator() error {
 		if err != nil {
 			return err
 		}
-		return fmt.Errorf("error creating default EMQX authenticator %v", string(msg))
+		if !strings.ContainsAny(string(msg), "ALREADY_EXISTS") {
+			return fmt.Errorf("error creating default EMQX authenticator %v", string(msg))
+		}
 	}
 	return nil
 }
@@ -240,7 +243,9 @@ func (e *EmqxOnPrem) CreateEmqxDefaultAuthorizer() error {
 		if err != nil {
 			return err
 		}
-		return fmt.Errorf("error creating default EMQX ACL authorization mechanism %v", string(msg))
+		if !strings.ContainsAny(string(msg), "duplicated_authz_source_type") {
+			return fmt.Errorf("error creating default EMQX ACL authorization mechanism %v", string(msg))
+		}
 	}
 	return nil
 }

+ 2 - 2
mq/handlers.go

@@ -65,7 +65,7 @@ func UpdateNode(client mqtt.Client, msg mqtt.Message) {
 			}
 			allNodes, err := logic.GetAllNodes()
 			if err == nil {
-				PublishSingleHostPeerUpdate(host, allNodes, nil, nil, false)
+				PublishSingleHostPeerUpdate(host, allNodes, nil, nil, false, nil)
 			}
 		} else {
 			err = PublishPeerUpdate(false)
@@ -117,7 +117,7 @@ func UpdateHost(client mqtt.Client, msg mqtt.Message) {
 				if err != nil {
 					return
 				}
-				if err = PublishSingleHostPeerUpdate(currentHost, nodes, nil, nil, false); err != nil {
+				if err = PublishSingleHostPeerUpdate(currentHost, nodes, nil, nil, false, nil); err != nil {
 					slog.Error("failed peers publish after join acknowledged", "name", hostUpdate.Host.Name, "id", currentHost.ID, "error", err)
 					return
 				}

+ 3 - 7
mq/mq.go

@@ -34,9 +34,9 @@ func setMqOptions(user, password string, opts *mqtt.ClientOptions) {
 	opts.SetAutoReconnect(true)
 	opts.SetConnectRetry(true)
 	opts.SetCleanSession(true)
-	opts.SetConnectRetryInterval(time.Second * 4)
-	opts.SetKeepAlive(time.Minute)
-	opts.SetCleanSession(true)
+	opts.SetConnectRetryInterval(time.Second * 1)
+	opts.SetKeepAlive(time.Second * 10)
+	opts.SetOrderMatters(false)
 	opts.SetWriteTimeout(time.Minute)
 }
 
@@ -75,19 +75,15 @@ func SetupMQTT(fatal bool) {
 	opts.SetOnConnectHandler(func(client mqtt.Client) {
 		serverName := servercfg.GetServer()
 		if token := client.Subscribe(fmt.Sprintf("update/%s/#", serverName), 0, mqtt.MessageHandler(UpdateNode)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
-			client.Disconnect(240)
 			logger.Log(0, "node update subscription failed")
 		}
 		if token := client.Subscribe(fmt.Sprintf("host/serverupdate/%s/#", serverName), 0, mqtt.MessageHandler(UpdateHost)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
-			client.Disconnect(240)
 			logger.Log(0, "host update subscription failed")
 		}
 		if token := client.Subscribe(fmt.Sprintf("signal/%s/#", serverName), 0, mqtt.MessageHandler(ClientPeerUpdate)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
-			client.Disconnect(240)
 			logger.Log(0, "node client subscription failed")
 		}
 		if token := client.Subscribe(fmt.Sprintf("metrics/%s/#", serverName), 0, mqtt.MessageHandler(UpdateMetrics)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil {
-			client.Disconnect(240)
 			logger.Log(0, "node metrics subscription failed")
 		}
 

+ 40 - 18
mq/publishers.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"sync"
 	"time"
 
 	"github.com/gravitl/netmaker/logger"
@@ -13,6 +14,9 @@ import (
 	"golang.org/x/exp/slog"
 )
 
+var batchSize = servercfg.GetPeerUpdateBatchSize()
+var batchUpdate = servercfg.GetBatchPeerUpdate()
+
 // PublishPeerUpdate --- determines and publishes a peer update to all the hosts
 func PublishPeerUpdate(replacePeers bool) error {
 	if !servercfg.IsMessageQueueBackend() {
@@ -28,15 +32,37 @@ func PublishPeerUpdate(replacePeers bool) error {
 	if err != nil {
 		return err
 	}
-	for _, host := range hosts {
-		host := host
-		go func(host models.Host) {
-			if err = PublishSingleHostPeerUpdate(&host, allNodes, nil, nil, replacePeers); err != nil {
-				logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
-			}
-		}(host)
+
+	//if batch peer update disabled
+	if !batchUpdate {
+		for _, host := range hosts {
+			host := host
+			go func(host models.Host) {
+				if err = PublishSingleHostPeerUpdate(&host, allNodes, nil, nil, replacePeers, nil); err != nil {
+					logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
+				}
+			}(host)
+		}
+		return nil
 	}
-	return err
+
+	//if batch peer update enabled
+	batchHost := BatchItems(hosts, batchSize)
+	var wg sync.WaitGroup
+	for _, v := range batchHost {
+		hostLen := len(v)
+		wg.Add(hostLen)
+		for i := 0; i < hostLen; i++ {
+			host := hosts[i]
+			go func(host models.Host) {
+				if err = PublishSingleHostPeerUpdate(&host, allNodes, nil, nil, replacePeers, &wg); err != nil {
+					logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
+				}
+			}(host)
+		}
+		wg.Wait()
+	}
+	return nil
 }
 
 // PublishDeletedNodePeerUpdate --- determines and publishes a peer update
@@ -57,7 +83,7 @@ func PublishDeletedNodePeerUpdate(delNode *models.Node) error {
 	}
 	for _, host := range hosts {
 		host := host
-		if err = PublishSingleHostPeerUpdate(&host, allNodes, delNode, nil, false); err != nil {
+		if err = PublishSingleHostPeerUpdate(&host, allNodes, delNode, nil, false, nil); err != nil {
 			logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
 		}
 	}
@@ -83,7 +109,7 @@ func PublishDeletedClientPeerUpdate(delClient *models.ExtClient) error {
 	for _, host := range hosts {
 		host := host
 		if host.OS != models.OS_Types.IoT {
-			if err = PublishSingleHostPeerUpdate(&host, nodes, nil, []models.ExtClient{*delClient}, false); err != nil {
+			if err = PublishSingleHostPeerUpdate(&host, nodes, nil, []models.ExtClient{*delClient}, false, nil); err != nil {
 				logger.Log(1, "failed to publish peer update to host", host.ID.String(), ": ", err.Error())
 			}
 		}
@@ -92,8 +118,10 @@ func PublishDeletedClientPeerUpdate(delClient *models.ExtClient) error {
 }
 
 // PublishSingleHostPeerUpdate --- determines and publishes a peer update to one host
-func PublishSingleHostPeerUpdate(host *models.Host, allNodes []models.Node, deletedNode *models.Node, deletedClients []models.ExtClient, replacePeers bool) error {
-
+func PublishSingleHostPeerUpdate(host *models.Host, allNodes []models.Node, deletedNode *models.Node, deletedClients []models.ExtClient, replacePeers bool, wg *sync.WaitGroup) error {
+	if wg != nil {
+		defer wg.Done()
+	}
 	peerUpdate, err := logic.GetPeerUpdateForHost("", host, allNodes, deletedNode, deletedClients)
 	if err != nil {
 		return err
@@ -211,12 +239,6 @@ func PushMetricsToExporter(metrics models.Metrics) error {
 
 // sendPeers - retrieve networks, send peer ports to all peers
 func sendPeers() {
-
-	hosts, err := logic.GetAllHosts()
-	if err != nil && len(hosts) > 0 {
-		logger.Log(1, "error retrieving networks for keepalive", err.Error())
-	}
-
 	peer_force_send++
 	if peer_force_send == 5 {
 		servercfg.SetHost()

+ 23 - 0
mq/util.go

@@ -3,12 +3,14 @@ package mq
 import (
 	"errors"
 	"fmt"
+	"math"
 	"strings"
 	"time"
 
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/netclient/ncutils"
+	"golang.org/x/exp/slog"
 )
 
 func decryptMsgWithHost(host *models.Host, msg []byte) ([]byte, error) {
@@ -44,6 +46,26 @@ func DecryptMsg(node *models.Node, msg []byte) ([]byte, error) {
 	return decryptMsgWithHost(host, msg)
 }
 
+func BatchItems[T any](items []T, batchSize int) [][]T {
+	if batchSize <= 0 {
+		return nil
+	}
+	remainderBatchSize := len(items) % batchSize
+	nBatches := int(math.Ceil(float64(len(items)) / float64(batchSize)))
+	batches := make([][]T, nBatches)
+	for i := range batches {
+		if i == nBatches-1 && remainderBatchSize > 0 {
+			batches[i] = make([]T, remainderBatchSize)
+		} else {
+			batches[i] = make([]T, batchSize)
+		}
+		for j := range batches[i] {
+			batches[i][j] = items[i*batchSize+j]
+		}
+	}
+	return batches
+}
+
 func encryptMsg(host *models.Host, msg []byte) ([]byte, error) {
 	if host.OS == models.OS_Types.IoT {
 		return msg, nil
@@ -87,6 +109,7 @@ func publish(host *models.Host, dest string, msg []byte) error {
 		if token.Error() == nil {
 			err = errors.New("connection timeout")
 		} else {
+			slog.Error("publish to mq error", "error", token.Error().Error())
 			err = token.Error()
 		}
 		return err

+ 12 - 0
pro/auth/auth.go

@@ -7,6 +7,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/golang-jwt/jwt/v4"
 	"github.com/gorilla/websocket"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
@@ -236,6 +237,17 @@ func getStateAndCode(r *http.Request) (string, string) {
 	return state, code
 }
 
+func getUserEmailFromClaims(token string) string {
+	accessToken, _ := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
+		return []byte(""), nil
+	})
+	if accessToken == nil {
+		return ""
+	}
+	claims, _ := accessToken.Claims.(jwt.MapClaims)
+	return claims["email"].(string)
+}
+
 func (user *OAuthUser) getUserName() string {
 	var userName string
 	if user.Email != "" {

+ 48 - 16
pro/auth/azure-ad.go

@@ -8,11 +8,11 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
+	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/microsoft"
@@ -33,7 +33,7 @@ func initAzureAD(redirectURL string, clientID string, clientSecret string) {
 		RedirectURL:  redirectURL,
 		ClientID:     clientID,
 		ClientSecret: clientSecret,
-		Scopes:       []string{"User.Read"},
+		Scopes:       []string{"User.Read", "email", "profile", "openid"},
 		Endpoint:     microsoft.AzureADEndpoint(servercfg.GetAzureTenant()),
 	}
 }
@@ -67,27 +67,50 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
-	if !isEmailAllowed(content.UserPrincipalName) {
-		handleOauthUserNotAllowedToSignUp(w)
-		return
+	var inviteExists bool
+	// check if invite exists for User
+	in, err := logic.GetUserInvite(content.Email)
+	if err == nil {
+		inviteExists = true
 	}
 	// check if user approval is already pending
-	if logic.IsPendingUser(content.UserPrincipalName) {
+	if !inviteExists && logic.IsPendingUser(content.UserPrincipalName) {
 		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
+
 	_, err = logic.GetUser(content.UserPrincipalName)
 	if err != nil {
 		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
-			err = logic.InsertPendingUser(&models.User{
-				UserName: content.UserPrincipalName,
-			})
-			if err != nil {
-				handleSomethingWentWrong(w)
+			if inviteExists {
+				// create user
+				user, err := proLogic.PrepareOauthUserFromInvite(in)
+				if err != nil {
+					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+					return
+				}
+				user.UserName = content.UserPrincipalName // override username with azure id
+				if err = logic.CreateUser(&user); err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				logic.DeleteUserInvite(content.Email)
+				logic.DeletePendingUser(content.UserPrincipalName)
+			} else {
+				if !isEmailAllowed(content.UserPrincipalName) {
+					handleOauthUserNotAllowedToSignUp(w)
+					return
+				}
+				err = logic.InsertPendingUser(&models.User{
+					UserName: content.UserPrincipalName,
+				})
+				if err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				handleFirstTimeOauthUserSignUp(w)
 				return
 			}
-			handleFirstTimeOauthUserSignUp(w)
-			return
 		} else {
 			handleSomethingWentWrong(w)
 			return
@@ -98,11 +121,16 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotFound(w)
 		return
 	}
-	if !(user.IsSuperAdmin || user.IsAdmin) {
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		handleSomethingWentWrong(w)
+		return
+	}
+	if userRole.DenyDashboardAccess {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = auth.FetchPassValue("")
+	var newPass, fetchErr = logic.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}
@@ -138,8 +166,9 @@ func getAzureUserInfo(state string, code string) (*OAuthUser, error) {
 	}
 	var httpReq, reqErr = http.NewRequest("GET", "https://graph.microsoft.com/v1.0/me", nil)
 	if reqErr != nil {
-		return nil, fmt.Errorf("failed to create request to GitHub")
+		return nil, fmt.Errorf("failed to create request to microsoft")
 	}
+
 	httpReq.Header.Set("Authorization", "Bearer "+token.AccessToken)
 	response, err := http.DefaultClient.Do(httpReq)
 	if err != nil {
@@ -155,6 +184,9 @@ func getAzureUserInfo(state string, code string) (*OAuthUser, error) {
 		return nil, fmt.Errorf("failed parsing email from response data: %s", err.Error())
 	}
 	userInfo.AccessToken = string(data)
+	if userInfo.Email == "" {
+		userInfo.Email = getUserEmailFromClaims(token.AccessToken)
+	}
 	return userInfo, nil
 }
 

+ 98 - 44
pro/auth/error.go

@@ -1,60 +1,114 @@
 package auth
 
-import "net/http"
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/gravitl/netmaker/servercfg"
+)
+
+var htmlBaseTemplate = `<!DOCTYPE html>
+<html lang="en">
+
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+	<meta http-equiv="X-UA-Compatible" content="ie=edge">
+	<title>Netmaker :: SSO</title>
+	<script type="text/javascript">
+	function redirect()
+    {
+    	window.location.href="` + servercfg.GetFrontendURL() + `";
+    }
+	</script>
+	<style>
+		html,
+		body {
+			margin: 0px;
+			padding: 0px;
+		}
+
+		body {
+			height: 100vh;
+			overflow: hidden;
+			display: flex;
+			flex-flow: column nowrap;
+			justify-content: center;
+			align-items: center;
+		}
+
+		#logo {
+			width: 150px;
+		}
+
+		h3 {
+			margin-bottom: 3rem;
+			color: rgb(25, 135, 84);
+			font-size: xx-large;
+		}
+
+		h4 {
+			margin-bottom: 0px;
+		}
+
+		p {
+			margin-top: 0px;
+			margin-bottom: 0px;
+		}
+		.back-to-login-btn {
+			background: #5E5DF0;
+			border-radius: 999px;
+			box-shadow: #5E5DF0 0 10px 20px -10px;
+			box-sizing: border-box;
+			color: #FFFFFF;
+			cursor: pointer;
+			font-family: Inter,Helvetica,"Apple Color Emoji","Segoe UI Emoji",NotoColorEmoji,"Noto Color Emoji","Segoe UI Symbol","Android Emoji",EmojiSymbols,-apple-system,system-ui,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans",sans-serif;
+			font-size: 16px;
+			font-weight: 700;
+			line-height: 24px;
+			opacity: 1;
+			outline: 0 solid transparent;
+			padding: 8px 18px;
+			user-select: none;
+			-webkit-user-select: none;
+			touch-action: manipulation;
+			width: fit-content;
+			word-break: break-word;
+			border: 0;
+			margin: 20px;
+		  }
+	</style>
+</head>
 
-// == define error HTML here ==
-const oauthNotConfigured = `<!DOCTYPE html><html>
 <body>
-<h3>Your Netmaker server does not have OAuth configured.</h3>
-<p>Please visit the docs <a href="https://docs.netmaker.org/oauth.html" target="_blank" rel="noopener">here</a> to learn how to.</p>
+	<img
+		src="https://raw.githubusercontent.com/gravitl/netmaker-docs/master/images/netmaker-github/netmaker-teal.png"
+		alt="netmaker logo"
+		id="logo"
+	>
+	%s
+	<button class="back-to-login-btn" onClick="redirect()" role="button">Back To Login</button>
+	
 </body>
 </html>`
 
-const oauthStateInvalid = `<!DOCTYPE html><html>
-<body>
-<h3>Invalid OAuth Session. Please re-try again.</h3>
-</body>
-</html>`
+var oauthNotConfigured = fmt.Sprintf(htmlBaseTemplate, `<h2>Your Netmaker server does not have OAuth configured.</h2>
+<p>Please visit the docs <a href="https://docs.netmaker.org/oauth.html" target="_blank" rel="noopener">here</a> to learn how to.</p>`)
 
-const userNotAllowed = `<!DOCTYPE html><html>
-<body>
-<h3>Only administrators can access the Dashboard. Please contact your administrator to elevate your account.</h3>
-<p>Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/pro/rac.html" target="_blank" rel="noopener">RemoteAccessClient.</a></p>
-</body>
-</html>
-`
+var oauthStateInvalid = fmt.Sprintf(htmlBaseTemplate, `<h2>Invalid OAuth Session. Please re-try again.</h2>`)
 
-const userFirstTimeSignUp = `<!DOCTYPE html><html>
-<body>
-<h3>Thank you for signing up. Please contact your administrator for access.</h3>
-</body>
-</html>
-`
+var userNotAllowed = fmt.Sprintf(htmlBaseTemplate, `<h2>Your account does not have access to the dashboard. Please contact your administrator for more information about your account.</h2>
+<p>Non-Admins can access the netmaker networks using <a href="https://docs.netmaker.io/pro/rac.html" target="_blank" rel="noopener">RemoteAccessClient.</a></p>`)
 
-const userSignUpApprovalPending = `<!DOCTYPE html><html>
-<body>
-<h3>Your account is yet to be approved. Please contact your administrator for access.</h3>
-</body>
-</html>
-`
+var userFirstTimeSignUp = fmt.Sprintf(htmlBaseTemplate, `<h2>Thank you for signing up. Please contact your administrator for access.</h2>`)
 
-const userNotFound = `<!DOCTYPE html><html>
-<body>
-<h3>User Not Found.</h3>
-</body>
-</html>`
+var userSignUpApprovalPending = fmt.Sprintf(htmlBaseTemplate, `<h2>Your account is yet to be approved. Please contact your administrator for access.</h2>`)
 
-const somethingwentwrong = `<!DOCTYPE html><html>
-<body>
-<h3>Something went wrong. Contact Admin.</h3>
-</body>
-</html>`
+var userNotFound = fmt.Sprintf(htmlBaseTemplate, `<h2>User Not Found.</h2>`)
 
-const notallowedtosignup = `<!DOCTYPE html><html>
-<body>
-<h3>Your email is not allowed. Please contact your administrator.</h3>
-</body>
-</html>`
+var somethingwentwrong = fmt.Sprintf(htmlBaseTemplate, `<h2>Something went wrong. Contact Admin.</h2>`)
+
+var notallowedtosignup = fmt.Sprintf(htmlBaseTemplate, `<h2>Your email is not allowed. Please contact your administrator.</h2>`)
 
 func handleOauthUserNotFound(response http.ResponseWriter) {
 	response.Header().Set("Content-Type", "text/html; charset=utf-8")

+ 45 - 15
pro/auth/github.go

@@ -8,11 +8,11 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
+	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/github"
@@ -33,7 +33,7 @@ func initGithub(redirectURL string, clientID string, clientSecret string) {
 		RedirectURL:  redirectURL,
 		ClientID:     clientID,
 		ClientSecret: clientSecret,
-		Scopes:       []string{},
+		Scopes:       []string{"read:user", "user:email"},
 		Endpoint:     github.Endpoint,
 	}
 }
@@ -67,27 +67,49 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
-	if !isEmailAllowed(content.Login) {
-		handleOauthUserNotAllowedToSignUp(w)
-		return
+	var inviteExists bool
+	// check if invite exists for User
+	in, err := logic.GetUserInvite(content.Email)
+	if err == nil {
+		inviteExists = true
 	}
 	// check if user approval is already pending
-	if logic.IsPendingUser(content.Login) {
+	if !inviteExists && logic.IsPendingUser(content.Login) {
 		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
 	_, err = logic.GetUser(content.Login)
 	if err != nil {
 		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
-			err = logic.InsertPendingUser(&models.User{
-				UserName: content.Login,
-			})
-			if err != nil {
-				handleSomethingWentWrong(w)
+			if inviteExists {
+				// create user
+				user, err := proLogic.PrepareOauthUserFromInvite(in)
+				if err != nil {
+					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+					return
+				}
+				user.UserName = content.Login // overrides email with github id
+				if err = logic.CreateUser(&user); err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				logic.DeleteUserInvite(content.Email)
+				logic.DeletePendingUser(content.Login)
+			} else {
+				if !isEmailAllowed(content.Login) {
+					handleOauthUserNotAllowedToSignUp(w)
+					return
+				}
+				err = logic.InsertPendingUser(&models.User{
+					UserName: content.Login,
+				})
+				if err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				handleFirstTimeOauthUserSignUp(w)
 				return
 			}
-			handleFirstTimeOauthUserSignUp(w)
-			return
 		} else {
 			handleSomethingWentWrong(w)
 			return
@@ -98,11 +120,16 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotFound(w)
 		return
 	}
-	if !(user.IsSuperAdmin || user.IsAdmin) {
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		handleSomethingWentWrong(w)
+		return
+	}
+	if userRole.DenyDashboardAccess {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = auth.FetchPassValue("")
+	var newPass, fetchErr = logic.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}
@@ -159,6 +186,9 @@ func getGithubUserInfo(state string, code string) (*OAuthUser, error) {
 		return nil, fmt.Errorf("failed parsing email from response data: %s", err.Error())
 	}
 	userInfo.AccessToken = string(data)
+	if userInfo.Email == "" {
+		userInfo.Email = getUserEmailFromClaims(token.AccessToken)
+	}
 	return userInfo, nil
 }
 

+ 44 - 16
pro/auth/google.go

@@ -9,11 +9,11 @@ import (
 	"strings"
 	"time"
 
-	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
+	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/google"
@@ -45,7 +45,7 @@ func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
-
+	logger.Log(0, "Setting OAuth State ", oauth_state_string)
 	if err := logic.SetState(oauth_state_string); err != nil {
 		handleOauthNotConfigured(w)
 		return
@@ -58,7 +58,7 @@ func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
 func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 
 	var rState, rCode = getStateAndCode(r)
-
+	logger.Log(0, "Fetched OAuth State ", rState)
 	var content, err = getGoogleUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from google:", err.Error())
@@ -69,27 +69,50 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
-	if !isEmailAllowed(content.Email) {
-		handleOauthUserNotAllowedToSignUp(w)
-		return
+	var inviteExists bool
+	// check if invite exists for User
+	in, err := logic.GetUserInvite(content.Email)
+	if err == nil {
+		inviteExists = true
 	}
 	// check if user approval is already pending
-	if logic.IsPendingUser(content.Email) {
+	if !inviteExists && logic.IsPendingUser(content.Email) {
 		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
 	_, err = logic.GetUser(content.Email)
 	if err != nil {
 		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
-			err = logic.InsertPendingUser(&models.User{
-				UserName: content.Email,
-			})
-			if err != nil {
-				handleSomethingWentWrong(w)
+			if inviteExists {
+				// create user
+				user, err := proLogic.PrepareOauthUserFromInvite(in)
+				if err != nil {
+					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+					return
+				}
+
+				if err = logic.CreateUser(&user); err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				logic.DeleteUserInvite(user.UserName)
+				logic.DeletePendingUser(content.Email)
+			} else {
+				if !isEmailAllowed(content.Email) {
+					handleOauthUserNotAllowedToSignUp(w)
+					return
+				}
+				err = logic.InsertPendingUser(&models.User{
+					UserName: content.Email,
+				})
+				if err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				handleFirstTimeOauthUserSignUp(w)
 				return
 			}
-			handleFirstTimeOauthUserSignUp(w)
-			return
+
 		} else {
 			handleSomethingWentWrong(w)
 			return
@@ -101,11 +124,16 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotFound(w)
 		return
 	}
-	if !(user.IsSuperAdmin || user.IsAdmin) {
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		handleSomethingWentWrong(w)
+		return
+	}
+	if userRole.DenyDashboardAccess {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = auth.FetchPassValue("")
+	var newPass, fetchErr = logic.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 1 - 2
pro/auth/headless_callback.go

@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"net/http"
 
-	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
@@ -78,7 +77,7 @@ func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	}
-	newPass, fetchErr := auth.FetchPassValue("")
+	newPass, fetchErr := logic.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 41 - 14
pro/auth/oidc.go

@@ -8,11 +8,11 @@ import (
 	"time"
 
 	"github.com/coreos/go-oidc/v3/oidc"
-	"github.com/gravitl/netmaker/auth"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
+	proLogic "github.com/gravitl/netmaker/pro/logic"
 	"github.com/gravitl/netmaker/servercfg"
 	"golang.org/x/oauth2"
 )
@@ -80,27 +80,49 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthNotConfigured(w)
 		return
 	}
-	if !isEmailAllowed(content.Email) {
-		handleOauthUserNotAllowedToSignUp(w)
-		return
+
+	var inviteExists bool
+	// check if invite exists for User
+	in, err := logic.GetUserInvite(content.Login)
+	if err == nil {
+		inviteExists = true
 	}
 	// check if user approval is already pending
-	if logic.IsPendingUser(content.Email) {
+	if !inviteExists && logic.IsPendingUser(content.Email) {
 		handleOauthUserSignUpApprovalPending(w)
 		return
 	}
 	_, err = logic.GetUser(content.Email)
 	if err != nil {
 		if database.IsEmptyRecord(err) { // user must not exist, so try to make one
-			err = logic.InsertPendingUser(&models.User{
-				UserName: content.Email,
-			})
-			if err != nil {
-				handleSomethingWentWrong(w)
+			if inviteExists {
+				// create user
+				user, err := proLogic.PrepareOauthUserFromInvite(in)
+				if err != nil {
+					logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
+					return
+				}
+				if err = logic.CreateUser(&user); err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				logic.DeleteUserInvite(user.UserName)
+				logic.DeletePendingUser(content.Email)
+			} else {
+				if !isEmailAllowed(content.Email) {
+					handleOauthUserNotAllowedToSignUp(w)
+					return
+				}
+				err = logic.InsertPendingUser(&models.User{
+					UserName: content.Email,
+				})
+				if err != nil {
+					handleSomethingWentWrong(w)
+					return
+				}
+				handleFirstTimeOauthUserSignUp(w)
 				return
 			}
-			handleFirstTimeOauthUserSignUp(w)
-			return
 		} else {
 			handleSomethingWentWrong(w)
 			return
@@ -111,11 +133,16 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotFound(w)
 		return
 	}
-	if !(user.IsSuperAdmin || user.IsAdmin) {
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		handleSomethingWentWrong(w)
+		return
+	}
+	if userRole.DenyDashboardAccess {
 		handleOauthUserNotAllowed(w)
 		return
 	}
-	var newPass, fetchErr = auth.FetchPassValue("")
+	var newPass, fetchErr = logic.FetchPassValue("")
 	if fetchErr != nil {
 		return
 	}

+ 2 - 1
pro/auth/register_callback.go

@@ -10,6 +10,7 @@ import (
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/pro/netcache"
+	"github.com/gravitl/netmaker/models"
 )
 
 var (
@@ -73,7 +74,7 @@ func HandleHostSSOCallback(w http.ResponseWriter, r *http.Request) {
 		handleOauthUserNotFound(w)
 		return
 	}
-	if !user.IsAdmin && !user.IsSuperAdmin {
+	if user.PlatformRoleID != models.AdminRole && user.PlatformRoleID != models.SuperAdminRole {
 		response := returnErrTemplate(userClaims.getUserName(), "only admin users can register using SSO", state, reqKeyIf)
 		w.WriteHeader(http.StatusForbidden)
 		w.Write(response)

+ 108 - 60
pro/controllers/failover.go

@@ -19,55 +19,56 @@ import (
 
 // FailOverHandlers - handlers for FailOver
 func FailOverHandlers(r *mux.Router) {
-	r.HandleFunc("/api/v1/node/{nodeid}/failover", http.HandlerFunc(getfailOver)).Methods(http.MethodGet)
-	r.HandleFunc("/api/v1/node/{nodeid}/failover", logic.SecurityCheck(true, http.HandlerFunc(createfailOver))).Methods(http.MethodPost)
-	r.HandleFunc("/api/v1/node/{nodeid}/failover", logic.SecurityCheck(true, http.HandlerFunc(deletefailOver))).Methods(http.MethodDelete)
-	r.HandleFunc("/api/v1/node/{network}/failover/reset", logic.SecurityCheck(true, http.HandlerFunc(resetFailOver))).Methods(http.MethodPost)
-	r.HandleFunc("/api/v1/node/{nodeid}/failover_me", controller.Authorize(true, false, "host", http.HandlerFunc(failOverME))).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/node/{nodeid}/failover", http.HandlerFunc(getfailOver)).
+		Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/node/{nodeid}/failover", logic.SecurityCheck(true, http.HandlerFunc(createfailOver))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/node/{nodeid}/failover", logic.SecurityCheck(true, http.HandlerFunc(deletefailOver))).
+		Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/node/{network}/failover/reset", logic.SecurityCheck(true, http.HandlerFunc(resetFailOver))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/node/{nodeid}/failover_me", controller.Authorize(true, false, "host", http.HandlerFunc(failOverME))).
+		Methods(http.MethodPost)
 }
 
-// swagger:route GET /api/v1/node/failover node getfailOver
-//
-// get failover node.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Get failover node
+// @Router      /api/v1/node/{nodeid}/failover [get]
+// @Tags        PRO
+// @Param       nodeid path string true "Node ID"
+// @Success     200 {object} models.Node
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     404 {object} models.ErrorResponse
 func getfailOver(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	nodeid := params["nodeid"]
 	// confirm host exists
 	node, err := logic.GetNodeByID(nodeid)
 	if err != nil {
-		slog.Error("failed to get node:", "error", err.Error())
+		slog.Error("failed to get node:", "node", nodeid, "error", err.Error())
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
 		return
 	}
 
 	failOverNode, exists := proLogic.FailOverExists(node.Network)
 	if !exists {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failover node not found"), "notfound"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("failover node not found"), "notfound"),
+		)
 		return
 	}
 	w.Header().Set("Content-Type", "application/json")
 	logic.ReturnSuccessResponseWithJson(w, r, failOverNode, "get failover node successfully")
 }
 
-// swagger:route POST /api/v1/node/failover node createfailOver
-//
-// Create a relay.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Create failover node
+// @Router      /api/v1/node/{nodeid}/failover [post]
+// @Tags        PRO
+// @Param       nodeid path string true "Node ID"
+// @Success     200 {object} models.Node
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func createfailOver(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	nodeid := params["nodeid"]
@@ -88,6 +89,12 @@ func createfailOver(w http.ResponseWriter, r *http.Request) {
 	logic.ReturnSuccessResponseWithJson(w, r, node, "created failover successfully")
 }
 
+// @Summary     Reset failover for a network
+// @Router      /api/v1/node/{network}/failover/reset [post]
+// @Tags        PRO
+// @Param       network path string true "Network ID"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     500 {object} models.ErrorResponse
 func resetFailOver(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	net := params["network"]
@@ -108,17 +115,13 @@ func resetFailOver(w http.ResponseWriter, r *http.Request) {
 	logic.ReturnSuccessResponse(w, r, "failover has been reset successfully")
 }
 
-// swagger:route DELETE /api/v1/node/failover node deletefailOver
-//
-// Create a relay.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Delete failover node
+// @Router      /api/v1/node/{nodeid}/failover [delete]
+// @Tags        PRO
+// @Param       nodeid path string true "Node ID"
+// @Success     200 {object} models.Node
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func deletefailOver(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	nodeid := params["nodeid"]
@@ -145,17 +148,15 @@ func deletefailOver(w http.ResponseWriter, r *http.Request) {
 	logic.ReturnSuccessResponseWithJson(w, r, node, "deleted failover successfully")
 }
 
-// swagger:route POST /api/node/{nodeid}/failOverME node failOver_me
-//
-// Create a relay.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Failover me
+// @Router      /api/v1/node/{nodeid}/failover_me [post]
+// @Tags        PRO
+// @Param       nodeid path string true "Node ID"
+// @Accept      json
+// @Param       body body models.FailOverMeReq true "Failover request"
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func failOverME(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	nodeid := params["nodeid"]
@@ -174,7 +175,14 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 
 	failOverNode, exists := proLogic.FailOverExists(node.Network)
 	if !exists {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("req-from: %s, failover node doesn't exist in the network", host.Name), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				fmt.Errorf("req-from: %s, failover node doesn't exist in the network", host.Name),
+				"badrequest",
+			),
+		)
 		return
 	}
 	var failOverReq models.FailOverMeReq
@@ -188,27 +196,57 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 	peerNode, err := logic.GetNodeByID(failOverReq.NodeID)
 	if err != nil {
 		slog.Error("peer not found: ", "nodeid", failOverReq.NodeID, "error", err)
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("peer not found"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("peer not found"), "badrequest"),
+		)
 		return
 	}
 	if node.IsFailOver {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("node is acting as failover"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("node is acting as failover"), "badrequest"),
+		)
 		return
 	}
 	if node.IsRelayed && node.RelayedBy == peerNode.ID.String() {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("node is relayed by peer node"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("node is relayed by peer node"), "badrequest"),
+		)
 		return
 	}
 	if node.IsRelay && peerNode.RelayedBy == node.ID.String() {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("node acting as relay for the peer node"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("node acting as relay for the peer node"), "badrequest"),
+		)
 		return
 	}
 	if node.IsInternetGateway && peerNode.InternetGwID == node.ID.String() {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("node acting as internet gw for the peer node"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("node acting as internet gw for the peer node"),
+				"badrequest",
+			),
+		)
 		return
 	}
 	if node.InternetGwID != "" && node.InternetGwID == peerNode.ID.String() {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("node using a internet gw by the peer node"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("node using a internet gw by the peer node"),
+				"badrequest",
+			),
+		)
 		return
 	}
 
@@ -216,10 +254,20 @@ func failOverME(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 		slog.Error("failed to create failover", "id", node.ID.String(),
 			"network", node.Network, "error", err)
-		logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("failed to create failover: %v", err), "internal"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(fmt.Errorf("failed to create failover: %v", err), "internal"),
+		)
 		return
 	}
-	slog.Info("[auto-relay] created relay on node", "node", node.ID.String(), "network", node.Network)
+	slog.Info(
+		"[auto-relay] created relay on node",
+		"node",
+		node.ID.String(),
+		"network",
+		node.Network,
+	)
 	sendPeerUpdate = true
 
 	if sendPeerUpdate {

+ 71 - 41
pro/controllers/inet_gws.go

@@ -16,22 +16,24 @@ import (
 
 // InetHandlers - handlers for internet gw
 func InetHandlers(r *mux.Router) {
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/inet_gw", logic.SecurityCheck(true, http.HandlerFunc(createInternetGw))).Methods(http.MethodPost)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/inet_gw", logic.SecurityCheck(true, http.HandlerFunc(updateInternetGw))).Methods(http.MethodPut)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/inet_gw", logic.SecurityCheck(true, http.HandlerFunc(deleteInternetGw))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/inet_gw", logic.SecurityCheck(true, http.HandlerFunc(createInternetGw))).
+		Methods(http.MethodPost)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/inet_gw", logic.SecurityCheck(true, http.HandlerFunc(updateInternetGw))).
+		Methods(http.MethodPut)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/inet_gw", logic.SecurityCheck(true, http.HandlerFunc(deleteInternetGw))).
+		Methods(http.MethodDelete)
 }
 
-// swagger:route POST /api/nodes/{network}/{nodeid}/inet_gw nodes createInternetGw
-//
-// Create an inet node.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Create an internet gateway
+// @Router      /api/nodes/{network}/{nodeid}/inet_gw [post]
+// @Tags        PRO
+// @Accept      json
+// @Param       network path string true "Network ID"
+// @Param       nodeid path string true "Node ID"
+// @Param       body body models.InetNodeReq true "Internet gateway request"
+// @Success     200 {object} models.Node
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func createInternetGw(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	w.Header().Set("Content-Type", "application/json")
@@ -58,7 +60,14 @@ func createInternetGw(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	if host.OS != models.OS_Types.Linux {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only linux nodes can be made internet gws"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(
+				errors.New("only linux nodes can be made internet gws"),
+				"badrequest",
+			),
+		)
 		return
 	}
 	err = proLogic.ValidateInetGwReq(node, request, false)
@@ -81,23 +90,29 @@ func createInternetGw(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	apiNode := node.ConvertToAPINode()
-	logger.Log(1, r.Header.Get("user"), "created ingress gateway on node", nodeid, "on network", netid)
+	logger.Log(
+		1,
+		r.Header.Get("user"),
+		"created ingress gateway on node",
+		nodeid,
+		"on network",
+		netid,
+	)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 	go mq.PublishPeerUpdate(false)
 }
 
-// swagger:route PUT /api/nodes/{network}/{nodeid}/inet_gw nodes updateInternetGw
-//
-// update an inet node.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Update an internet gateway
+// @Router      /api/nodes/{network}/{nodeid}/inet_gw [put]
+// @Tags        PRO
+// @Accept      json
+// @Param       network path string true "Network ID"
+// @Param       nodeid path string true "Node ID"
+// @Param       body body models.InetNodeReq true "Internet gateway request"
+// @Success     200 {object} models.Node
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func updateInternetGw(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	w.Header().Set("Content-Type", "application/json")
@@ -115,7 +130,11 @@ func updateInternetGw(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	if !node.IsInternetGateway {
-		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("node is not a internet gw"), "badrequest"))
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("node is not a internet gw"), "badrequest"),
+		)
 		return
 	}
 	err = proLogic.ValidateInetGwReq(node, request, true)
@@ -131,23 +150,27 @@ func updateInternetGw(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	apiNode := node.ConvertToAPINode()
-	logger.Log(1, r.Header.Get("user"), "created ingress gateway on node", nodeid, "on network", netid)
+	logger.Log(
+		1,
+		r.Header.Get("user"),
+		"created ingress gateway on node",
+		nodeid,
+		"on network",
+		netid,
+	)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 	go mq.PublishPeerUpdate(false)
 }
 
-// swagger:route DELETE /api/nodes/{network}/{nodeid}/inet_gw nodes deleteInternetGw
-//
-// Delete an internet gw.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Delete an internet gateway
+// @Router      /api/nodes/{network}/{nodeid}/inet_gw [delete]
+// @Tags        PRO
+// @Param       network path string true "Network ID"
+// @Param       nodeid path string true "Node ID"
+// @Success     200 {object} models.Node
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func deleteInternetGw(w http.ResponseWriter, r *http.Request) {
 	var params = mux.Vars(r)
 	w.Header().Set("Content-Type", "application/json")
@@ -166,7 +189,14 @@ func deleteInternetGw(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	apiNode := node.ConvertToAPINode()
-	logger.Log(1, r.Header.Get("user"), "created ingress gateway on node", nodeid, "on network", netid)
+	logger.Log(
+		1,
+		r.Header.Get("user"),
+		"created ingress gateway on node",
+		nodeid,
+		"on network",
+		netid,
+	)
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 	go mq.PublishPeerUpdate(false)

+ 59 - 30
pro/controllers/relay.go

@@ -19,22 +19,22 @@ import (
 // RelayHandlers - handle Pro Relays
 func RelayHandlers(r *mux.Router) {
 
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/createrelay", controller.Authorize(false, true, "user", http.HandlerFunc(createRelay))).Methods(http.MethodPost)
-	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleterelay", controller.Authorize(false, true, "user", http.HandlerFunc(deleteRelay))).Methods(http.MethodDelete)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/createrelay", logic.SecurityCheck(true, http.HandlerFunc(createRelay))).Methods(http.MethodPost)
+	r.HandleFunc("/api/nodes/{network}/{nodeid}/deleterelay", logic.SecurityCheck(true, http.HandlerFunc(deleteRelay))).Methods(http.MethodDelete)
 	r.HandleFunc("/api/v1/host/{hostid}/failoverme", controller.Authorize(true, false, "host", http.HandlerFunc(failOverME))).Methods(http.MethodPost)
 }
 
-// swagger:route POST /api/nodes/{network}/{nodeid}/createrelay nodes createRelay
-//
-// Create a relay.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Create a relay
+// @Router      /api/nodes/{network}/{nodeid}/createrelay [post]
+// @Tags        PRO
+// @Accept      json
+// @Produce     json
+// @Param       network path string true "Network ID"
+// @Param       nodeid path string true "Node ID"
+// @Param       body body models.RelayRequest true "Relay request parameters"
+// @Success     200 {object} models.ApiNode
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func createRelay(w http.ResponseWriter, r *http.Request) {
 	var relayRequest models.RelayRequest
 	var params = mux.Vars(r)
@@ -49,8 +49,16 @@ func createRelay(w http.ResponseWriter, r *http.Request) {
 	relayRequest.NodeID = params["nodeid"]
 	_, relayNode, err := proLogic.CreateRelay(relayRequest)
 	if err != nil {
-		logger.Log(0, r.Header.Get("user"),
-			fmt.Sprintf("failed to create relay on node [%s] on network [%s]: %v", relayRequest.NodeID, relayRequest.NetID, err))
+		logger.Log(
+			0,
+			r.Header.Get("user"),
+			fmt.Sprintf(
+				"failed to create relay on node [%s] on network [%s]: %v",
+				relayRequest.NodeID,
+				relayRequest.NetID,
+				err,
+			),
+		)
 		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
 		return
 	}
@@ -64,23 +72,29 @@ func createRelay(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 	go mq.PublishPeerUpdate(false)
-	logger.Log(1, r.Header.Get("user"), "created relay on node", relayRequest.NodeID, "on network", relayRequest.NetID)
+	logger.Log(
+		1,
+		r.Header.Get("user"),
+		"created relay on node",
+		relayRequest.NodeID,
+		"on network",
+		relayRequest.NetID,
+	)
 	apiNode := relayNode.ConvertToAPINode()
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)
 }
 
-// swagger:route DELETE /api/nodes/{network}/{nodeid}/deleterelay nodes deleteRelay
-//
-// Remove a relay.
-//
-//			Schemes: https
-//
-//			Security:
-//	  		oauth
-//
-//			Responses:
-//				200: nodeResponse
+// @Summary     Remove a relay
+// @Router      /api/nodes/{network}/{nodeid}/deleterelay [delete]
+// @Tags        PRO
+// @Accept      json
+// @Produce     json
+// @Param       network path string true "Network ID"
+// @Param       nodeid path string true "Node ID"
+// @Success     200 {object} models.ApiNode
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
 func deleteRelay(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	var params = mux.Vars(r)
@@ -97,7 +111,15 @@ func deleteRelay(w http.ResponseWriter, r *http.Request) {
 		for _, relayedNode := range updateNodes {
 			err = mq.NodeUpdate(&relayedNode)
 			if err != nil {
-				logger.Log(1, "relayed node update ", relayedNode.ID.String(), "on network", relayedNode.Network, ": ", err.Error())
+				logger.Log(
+					1,
+					"relayed node update ",
+					relayedNode.ID.String(),
+					"on network",
+					relayedNode.Network,
+					": ",
+					err.Error(),
+				)
 
 			}
 			h, err := logic.GetHost(relayedNode.HostID.String())
@@ -108,7 +130,7 @@ func deleteRelay(w http.ResponseWriter, r *http.Request) {
 						return
 					}
 					node.IsRelay = true // for iot update to recognise that it has to delete relay peer
-					if err = mq.PublishSingleHostPeerUpdate(h, nodes, &node, nil, false); err != nil {
+					if err = mq.PublishSingleHostPeerUpdate(h, nodes, &node, nil, false, nil); err != nil {
 						logger.Log(1, "failed to publish peer update to host", h.ID.String(), ": ", err.Error())
 					}
 				}
@@ -116,7 +138,14 @@ func deleteRelay(w http.ResponseWriter, r *http.Request) {
 		}
 		mq.PublishPeerUpdate(false)
 	}()
-	logger.Log(1, r.Header.Get("user"), "deleted relay on node", node.ID.String(), "on network", node.Network)
+	logger.Log(
+		1,
+		r.Header.Get("user"),
+		"deleted relay on node",
+		node.ID.String(),
+		"on network",
+		node.Network,
+	)
 	apiNode := node.ConvertToAPINode()
 	w.WriteHeader(http.StatusOK)
 	json.NewEncoder(w).Encode(apiNode)

Datei-Diff unterdrückt, da er zu groß ist
+ 744 - 153
pro/controllers/users.go


+ 54 - 0
pro/email/email.go

@@ -0,0 +1,54 @@
+package email
+
+import (
+	"context"
+
+	"github.com/gravitl/netmaker/servercfg"
+)
+
+type EmailSenderType string
+
+var client EmailSender
+
+const (
+	Smtp   EmailSenderType = "smtp"
+	Resend EmailSenderType = "resend"
+)
+
+func init() {
+
+	smtpSender := &SmtpSender{
+		SmtpHost:    servercfg.GetSmtpHost(),
+		SmtpPort:    servercfg.GetSmtpPort(),
+		SenderEmail: servercfg.GetSenderEmail(),
+		SendUser:    servercfg.GetSenderUser(),
+		SenderPass:  servercfg.GetEmaiSenderPassword(),
+	}
+	if smtpSender.SendUser == "" {
+		smtpSender.SendUser = smtpSender.SenderEmail
+	}
+	client = smtpSender
+
+}
+
+// EmailSender - an interface for sending emails based on notifications and mail templates
+type EmailSender interface {
+	// SendEmail - sends an email based on a context, notification and mail template
+	SendEmail(ctx context.Context, notification Notification, email Mail) error
+}
+
+type Mail interface {
+	GetBody(info Notification) string
+	GetSubject(info Notification) string
+}
+
+// Notification - struct for notification details
+type Notification struct {
+	RecipientMail string
+	RecipientName string
+	ProductName   string
+}
+
+func GetClient() (e EmailSender) {
+	return client
+}

+ 27 - 0
pro/email/invite.go

@@ -0,0 +1,27 @@
+package email
+
+import (
+	"fmt"
+)
+
+// UserInvitedMail - mail for users that are invited to a tenant
+type UserInvitedMail struct {
+	BodyBuilder EmailBodyBuilder
+	InviteURL   string
+}
+
+// GetSubject - gets the subject of the email
+func (UserInvitedMail) GetSubject(info Notification) string {
+	return "Netmaker: Pending Invitation"
+}
+
+// GetBody - gets the body of the email
+func (invite UserInvitedMail) GetBody(info Notification) string {
+
+	return invite.BodyBuilder.
+		WithHeadline("Join Netmaker from this invite!").
+		WithParagraph("Hello from Netmaker,").
+		WithParagraph("You have been invited to join Netmaker.").
+		WithParagraph(fmt.Sprintf("Join Using This Invite Link <a href=\"%s\">Netmaker</a>", invite.InviteURL)).
+		Build()
+}

+ 43 - 0
pro/email/smtp.go

@@ -0,0 +1,43 @@
+package email
+
+import (
+	"context"
+	"crypto/tls"
+
+	gomail "gopkg.in/mail.v2"
+)
+
+type SmtpSender struct {
+	SmtpHost    string
+	SmtpPort    int
+	SenderEmail string
+	SendUser    string
+	SenderPass  string
+}
+
+func (s *SmtpSender) SendEmail(ctx context.Context, n Notification, e Mail) error {
+	m := gomail.NewMessage()
+
+	// Set E-Mail sender
+	m.SetHeader("From", s.SenderEmail)
+
+	// Set E-Mail receivers
+	m.SetHeader("To", n.RecipientMail)
+	// Set E-Mail subject
+	m.SetHeader("Subject", e.GetSubject(n))
+	// Set E-Mail body. You can set plain text or html with text/html
+	m.SetBody("text/html", e.GetBody(n))
+	// Settings for SMTP server
+	d := gomail.NewDialer(s.SmtpHost, s.SmtpPort, s.SendUser, s.SenderPass)
+
+	// This is only needed when SSL/TLS certificate is not valid on server.
+	// In production this should be set to false.
+	d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
+
+	// Now send E-Mail
+	if err := d.DialAndSend(m); err != nil {
+		return err
+	}
+
+	return nil
+}

+ 567 - 0
pro/email/utils.go

@@ -0,0 +1,567 @@
+package email
+
+import "strings"
+
+// mail related images hosted on github
+var (
+	nLogoTeal        = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/N_Teal.png"
+	netmakerLogoTeal = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/netmaker-logo-2.png"
+	netmakerMeshLogo = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/netmaker-mesh.png"
+	linkedinIcon     = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/linkedin2x.png"
+	discordIcon      = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/discord-logo-png-7617.png"
+	githubIcon       = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/Octocat.png"
+	mailIcon         = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/icons8-mail-24.png"
+	addressIcon      = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/icons8-address-16.png"
+	linkIcon         = "https://raw.githubusercontent.com/gravitl/netmaker/netmaker_logos/img/logos/icons8-hyperlink-64.png"
+)
+
+type EmailBodyBuilder interface {
+	WithHeadline(text string) EmailBodyBuilder
+	WithParagraph(text string) EmailBodyBuilder
+	WithSignature() EmailBodyBuilder
+	Build() string
+}
+
+type EmailBodyBuilderWithH1HeadlineAndImage struct {
+	headline     string
+	paragraphs   []string
+	hasSignature bool
+}
+
+func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithHeadline(text string) EmailBodyBuilder {
+	b.headline = text
+	return b
+}
+
+func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithParagraph(text string) EmailBodyBuilder {
+	b.paragraphs = append(b.paragraphs, text)
+	return b
+}
+
+func (b *EmailBodyBuilderWithH1HeadlineAndImage) WithSignature() EmailBodyBuilder {
+	b.hasSignature = true
+	return b
+}
+
+func (b *EmailBodyBuilderWithH1HeadlineAndImage) Build() string {
+	// map paragraphs to styled paragraphs
+	styledParagraphsSlice := make([]string, len(b.paragraphs))
+	for i, paragraph := range b.paragraphs {
+		styledParagraphsSlice[i] = styledParagraph(paragraph)
+	}
+	// join styled paragraphs
+	styledParagraphsString := strings.Join(styledParagraphsSlice, "")
+
+	signature := ""
+	if b.hasSignature {
+		signature = styledSignature()
+	}
+
+	return `
+		<!DOCTYPE html>
+		<html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" lang="en">
+		<head>
+		    <title></title>
+		    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+		    <meta name="viewport" content="width=device-width,initial-scale=1">
+		    <!--[if mso]>
+		    <xml>
+		        <o:OfficeDocumentSettings>
+		            <o:PixelsPerInch>96</o:PixelsPerInch>
+		            <o:AllowPNG/>
+		        </o:OfficeDocumentSettings>
+		    </xml>
+		    <![endif]-->
+		    <style>
+		        *{box-sizing:border-box}body{margin:0;padding:0}a[x-apple-data-detectors]{color:inherit!important;text-decoration:inherit!important}#MessageViewBody a{color:inherit;text-decoration:none}p{line-height:inherit}.desktop_hide,.desktop_hide table{mso-hide:all;display:none;max-height:0;overflow:hidden}@media (max-width:720px){.desktop_hide table.icons-inner{display:inline-block!important}.icons-inner{text-align:center}.icons-inner td{margin:0 auto}.image_block img.big,.row-content{width:100%!important}.mobile_hide{display:none}.stack .column{width:100%;display:block}.mobile_hide{min-height:0;max-height:0;max-width:0;overflow:hidden;font-size:0}.desktop_hide,.desktop_hide table{display:table!important;max-height:none!important}}
+		    </style>
+		</head>
+		<body style="background-color:transparent;margin:0;padding:0;-webkit-text-size-adjust:none;text-size-adjust:none">
+		<table class="nl-container" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;background-color:transparent">
+		    <tbody>
+		    <tr>
+		        <td>
+		            <table class="row row-1" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="50%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="image_block block-2" width="100%" border="0" cellpadding="0" cellspacing="0"
+		                                           role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad" style="padding-left:15px;padding-right:15px;width:100%;padding-top:5px">
+		                                                <div class="alignment" align="left" style="line-height:10px"><a href="https://www.netmaker.io/" target="_blank" style="outline:none" tabindex="-1"><img class="big" src="` + netmakerLogoTeal + `"
+		                                                                                                                                                                                                        style="display:block;height:auto;border:0;width:333px;max-width:100%" width="333" alt="Netmaker" title="Netmaker"></a></div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                    <table class="divider_block block-3" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad" style="padding-bottom:10px;padding-left:5px;padding-right:5px;padding-top:10px">
+		                                                <div class="alignment" align="center">
+		                                                    <table border="0" cellpadding="0" cellspacing="0"
+		                                                           role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                        <tr>
+		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                                <td class="column column-2" width="50%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="empty_block block-2" width="100%" border="0"
+		                                           cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad" style="padding-right:0;padding-bottom:5px;padding-left:0;padding-top:5px">
+		                                                <div></div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table class="row row-2" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content stack" align="center"
+		                               border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;padding-left:10px;padding-right:10px;vertical-align:top;padding-top:10px;padding-bottom:10px;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="divider_block block-1" width="100%" border="0"
+		                                           cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad">
+		                                                <div class="alignment" align="center">
+		                                                    <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                        <tr>
+		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table
+		                    class="row row-3" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="50%"
+		                                    style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="divider_block block-2" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad" style="padding-bottom:20px;padding-left:20px;padding-right:20px;padding-top:25px">
+		                                                <div class="alignment" align="center">
+		                                                    <table border="0" cellpadding="0"
+		                                                           cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                        <tr>
+		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                    <table class="heading_block block-3" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad"
+		                                                style="padding-bottom:15px;padding-left:10px;padding-right:10px;padding-top:10px;text-align:center;width:100%">
+		                                                <h1 style="margin:0;color:#2b2d2d;direction:ltr;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:28px;font-weight:400;letter-spacing:normal;line-height:120%;text-align:left;margin-top:0;margin-bottom:0"><strong>` + b.headline + `</strong></h1>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                                <td class="column column-2" width="50%"
+		                                    style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="image_block block-2" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad" style="width:100%;padding-right:0;padding-left:0;padding-top:5px;padding-bottom:5px">
+		                                                <div class="alignment" align="center" style="line-height:10px"><img
+		                                                        src="` + netmakerMeshLogo + `" style="display:block;height:auto;border:0;width:350px;max-width:100%" width="350" alt="Netmaker Mesh"></div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table class="row row-4" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
+		                   style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="divider_block block-1" width="100%" border="0" cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad">
+		                                                <div class="alignment" align="center">
+		                                                    <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                        <tr>
+		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table class="row row-5" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
+		                               style="mso-table-lspace:0;mso-table-rspace:0;background-color:#0098a5;color:#000;border-top:2px solid transparent;border-right:2px solid transparent;border-left:2px solid transparent;border-bottom:2px solid transparent;border-radius:0;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%"
+		                                    style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;border-bottom:0 solid #000;border-left:0 solid #000;border-right:0 solid #000;border-top:0 solid #000;vertical-align:top;padding-top:25px;padding-bottom:25px">
+		                                    <table class="text_block block-3" width="100%" border="0"
+		                                           cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word">
+		                                        <tr>
+		                                            <td class="pad" style="padding-bottom:10px;padding-left:50px;padding-right:50px;padding-top:10px">
+		                                                <div style="font-family:Verdana,sans-serif">
+		                                                    <div class="txtTinyMce-wrapper" style="font-size:12px;mso-line-height-alt:18px;color:#393d47;line-height:1.5;font-family:Verdana,Geneva,sans-serif">
+		
+		                                                        <p style="margin:0;font-size:12px;mso-line-height-alt:18px">&nbsp;</p>
+		                                                        ` + styledParagraphsString + `
+		                                                        <p style="margin:0;mso-line-height-alt:18px">&nbsp;</p>
+		                                                    </div>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table class="row row-6" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table
+		                                class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="divider_block block-1" width="100%" border="0"
+		                                           cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad">
+		                                                <div class="alignment" align="center">
+		                                                    <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                        <tr>
+		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table
+		                    class="row row-7" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;background-color:#f7fafe">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%"
+		                                    style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:25px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="divider_block block-1" width="100%" border="0" cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad">
+		                                                <div class="alignment" align="center">
+		                                                    <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%"
+		                                                           style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                        <tr>
+		                                                            <td class="divider_inner" style="font-size:1px;line-height:1px;border-top:0 solid #bbb"><span>&#8202;</span></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table class="row row-8" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;background-color:#090660">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table class="row-content stack"
+		                               align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="text_block block-1" width="100%" border="0" cellpadding="0" cellspacing="0"
+		                                           role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word">
+		                                        <tr>
+		                                            <td class="pad" style="padding-bottom:10px;padding-left:50px;padding-right:50px;padding-top:10px">
+		                                                <div style="font-family:sans-serif">
+		                                                    <div class="txtTinyMce-wrapper" style="font-size:12px;mso-line-height-alt:18px;color:#6f7077;line-height:1.5;font-family:Arial,Helvetica Neue,Helvetica,sans-serif">
+		                                                        <p style="margin:0;font-size:12px;mso-line-height-alt:33px">
+		                                                            <span style="color:#ffffff;font-size:22px;">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Get In Touch With Us</span>
+		                                                        </p>
+		                                                    </div>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                    <table class="social_block block-2" width="100%" border="0" cellpadding="10" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad">
+		                                                <div class="alignment" style="text-align:center">
+		                                                    <table class="social-table"
+		                                                           width="114.49624060150376px" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;display:inline-block">
+		                                                        <tr>
+		                                                            <td style="padding:0 2px 0 2px"><a href="https://www.linkedin.com/company/netmaker-inc/" target="_blank"><img src="` + linkedinIcon + `" width="32" height="32" alt="Linkedin" title="linkedin" style="display:block;height:auto;border:0"></a></td>
+		                                                            <td
+		                                                                    style="padding:0 2px 0 2px"><a href="https://discord.gg/zRb9Vfhk8A" target="_blank"><img src="` + discordIcon + `" width="32" height="32" alt="Discord" title="Discord" style="display:block;height:auto;border:0"></a></td>
+		                                                            <td style="padding:0 2px 0 2px"><a href="https://github.com/gravitl/netmaker" target="_blank"><img
+		                                                                    src="` + githubIcon + `" width="38.49624060150376" height="32" alt="Github" title="Github" style="display:block;height:auto;border:0"></a></td>
+		                                                        </tr>
+		                                                    </table>
+		                                                </div>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		            <table class="row row-9" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                <tbody>
+		                <tr>
+		                    <td>
+		                        <table
+		                                class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:700px" width="700">
+		                            <tbody>
+		                            <tr>
+		                                <td class="column column-1" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0">
+		                                    <table class="icons_block block-1" width="100%" border="0"
+		                                           cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                        <tr>
+		                                            <td class="pad" style="vertical-align:middle;padding-bottom:5px;padding-top:5px;text-align:center;color:#9d9d9d;font-family:inherit;font-size:15px">
+		                                                <table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace:0;mso-table-rspace:0">
+		                                                    <tr>
+		                                                        <td class="alignment" style="vertical-align:middle;text-align:center">
+		                                                            <!--[if vml]>
+		                                                            <table align="left" cellpadding="0" cellspacing="0" role="presentation" style="display:inline-block;padding-left:0px;padding-right:0px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;">
+		                                                            <![endif]--><!--[if !vml]><!-->
+		                                                            <table class="icons-inner" style="mso-table-lspace:0;mso-table-rspace:0;display:inline-block;margin-right:-4px;padding-left:0;padding-right:0" cellpadding="0" cellspacing="0" role="presentation">
+		                                                                <!--<![endif]-->
+		                                                            </table>
+		                                                        </td></tr>
+		                                                </table>
+		                                            </td>
+		                                        </tr>
+		                                    </table>
+		                                </td>
+		                            </tr>
+		                            </tbody>
+		                        </table>
+		                    </td>
+		                </tr>
+		                </tbody>
+		            </table>
+		        </td>
+		    </tr>
+		    </tbody>
+		</table>
+		<!-- End -->
+		</body>
+		` + signature + `
+		</html>`
+}
+
+func styledSignature() string {
+	return `
+	<footer style="display:block">
+	<table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+	<tbody>
+	   <tr>
+		  <td>
+			 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+				<tbody>
+				   <tr>
+					  <td style="vertical-align: top;">
+						 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+							<tbody>
+							   <tr>
+								  <td class="sc-TOsTZ kjYrri" style="text-align: center;"><img src="` + nLogoTeal + `" role="presentation" width="130" class="sc-cHGsZl bHiaRe" style="max-width: 130px; display: block;"></td>
+							   </tr>
+							   <tr>
+								  <td height="30"></td>
+							   </tr>
+							   <tr>
+								  <td style="text-align: center;">
+									 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial; display: inline-block;">
+										<tbody>
+										   <tr style="text-align: center;">
+											  <td><a href="https://www.linkedin.com/company/netmaker-inc/" color="#6a78d1" class="sc-hzDkRC kpsoyz" style="display: inline-block; padding: 0px; background-color: rgb(106, 120, 209);"><img src="` + linkedinIcon + `" alt="Linkedin" color="#6a78d1" height="24" class="sc-bRBYWo ccSRck" style="background-color: rgb(106, 120, 209); max-width: 135px; display: block;"></a></td>
+											  <td width="5">
+												 <div></div>
+											  </td>
+										 
+                                    <td><a href="https://discord.gg/zRb9Vfhk8A" class="sc-hzDkRC kpsoyz" style="display: inline-block; padding: 0px;"><img src="` + discordIcon + `" alt="Discord" height="24" class="sc-bRBYWo ccSRck" style="max-width: 135px; display: block;"></a></td>
+                                    <td width="5">
+                                    <div></div>
+                                    </td>
+                              
+                                    <td><a href="https://github.com/gravitl/netmaker" class="sc-hzDkRC kpsoyz" style="display: inline-block; padding: 0px;"><img src="` + githubIcon + `" alt="Github" height="24" class="sc-bRBYWo ccSRck" style="max-width: 135px; display: block;"></a></td>
+                                    <td width="5">
+                                    <div></div>
+                                    </td>
+                                 </tr>
+										</tbody>
+									 </table>
+								  </td>
+							   </tr>
+							</tbody>
+						 </table>
+					  </td>
+					  <td width="46">
+						 <div></div>
+					  </td>
+					  <td style="padding: 0px; vertical-align: middle;">
+						 <h3 color="#000000" class="sc-fBuWsC eeihxG" style="margin: 0px; font-size: 18px; color: rgb(0, 0, 0);"><span>Alex</span><span>&nbsp;</span><span>Feiszli</span></h3>
+						 <p color="#000000" font-size="medium" class="sc-fMiknA bxZCMx" style="margin: 0px; color: rgb(0, 0, 0); font-size: 14px; line-height: 22px;"><span>Co-Founder &amp; CEO</span></p>
+						 <p color="#000000" font-size="medium" class="sc-dVhcbM fghLuF" style="margin: 0px; font-weight: 500; color: rgb(0, 0, 0); font-size: 14px; line-height: 22px;"><span>Netmaker</span></p>
+						 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial; width: 100%;">
+							<tbody>
+							   <tr>
+								  <td height="30"></td>
+							   </tr>
+							   <tr>
+								  <td color="#545af2" direction="horizontal" height="1" class="sc-jhAzac hmXDXQ" style="width: 100%; border-bottom: 1px solid rgb(84, 90, 242); border-left: none; display: block;"></td>
+							   </tr>
+							   <tr>
+								  <td height="30"></td>
+							   </tr>
+							</tbody>
+						 </table>
+						 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+							<tbody>
+							   <tr height="25" style="vertical-align: middle;">
+								  <td width="30" style="vertical-align: middle;">
+									 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+										<tbody>
+										   <tr>
+											  <td style="vertical-align: bottom;"><span width="11" class="sc-jlyJG bbyJzT" style="display: block"><img src="` + mailIcon + `" width="13" class="sc-iRbamj blSEcj" style="display: block;"></span></td>
+										   </tr>
+										</tbody>
+									 </table>
+								  </td>
+								  <td style="padding: 0px;"><a href="mailto:[email protected]" color="#000000" class="sc-gipzik iyhjGb" style="text-decoration: none; color: rgb(0, 0, 0); font-size: 12px;"><span>[email protected]</span></a></td>
+							   </tr>
+							   <tr height="25" style="vertical-align: middle;">
+								  <td width="30" style="vertical-align: middle;">
+									 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+										<tbody>
+										   <tr>
+											  <td style="vertical-align: bottom;"><span width="11" class="sc-jlyJG bbyJzT" style="display: block;"><img src="` + linkIcon + `" color="#545af2" width="13" class="sc-iRbamj blSEcj" style="display: block;"></span></td>
+										   </tr>
+										</tbody>
+									 </table>
+								  </td>
+								  <td style="padding: 0px;"><a href="https://www.netmaker.io/" color="#000000" class="sc-gipzik iyhjGb" style="text-decoration: none; color: rgb(0, 0, 0); font-size: 12px;"><span>https://www.netmaker.io/</span></a></td>
+							   </tr>
+							   <tr height="25" style="vertical-align: middle;">
+								  <td width="30" style="vertical-align: middle;">
+									 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+										<tbody>
+										   <tr>
+											  <td style="vertical-align: bottom;"><span width="11" class="sc-jlyJG bbyJzT" style="display: block;"><img src="` + addressIcon + `"  width="13" class="sc-iRbamj blSEcj" style="display: block;"></span></td>
+										   </tr>
+										</tbody>
+									 </table>
+								  </td>
+								  <td style="padding: 0px;"><span color="#000000" class="sc-csuQGl CQhxV" style="font-size: 12px; color: rgb(0, 0, 0);"><span>1465 Sand Hill Rd.Suite 2014, Candler, NC 28715</span></span></td>
+							   </tr>
+							</tbody>
+						 </table>
+						 <table cellpadding="0" cellspacing="0" class="sc-gPEVay eQYmiW" style="vertical-align: -webkit-baseline-middle; font-size: medium; font-family: Arial;">
+							<tbody>
+							   <tr>
+								  <td height="30"></td>
+							   </tr>
+							</tbody>
+						 </table>
+					  </td>
+				   </tr>
+				</tbody>
+			 </table>
+		  </td>
+	   </tr>
+	</tbody>
+ </table>
+</footer>`
+}
+
+func styledParagraph(text string) string {
+	return `<p style="margin:0;mso-line-height-alt:22.5px">
+	<span style="color:#ffffff;font-size:15px;">` + text + `</span>
+	</p>`
+}
+
+func GetMailSignature() string {
+	return styledSignature()
+}

+ 15 - 1
pro/initialize.go

@@ -34,6 +34,7 @@ func InitPro() {
 		proControllers.FailOverHandlers,
 		proControllers.InetHandlers,
 	)
+	controller.ListRoles = proControllers.ListRoles
 	logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() {
 		// == License Handling ==
 		enableLicenseHook := false
@@ -88,7 +89,7 @@ func InitPro() {
 		} else {
 			slog.Error("no OAuth provider found or not configured, continuing without OAuth")
 		}
-
+		proLogic.LoadNodeMetricsToCache()
 	})
 	logic.ResetFailOver = proLogic.ResetFailOver
 	logic.ResetFailedOverPeer = proLogic.ResetFailedOverPeer
@@ -119,6 +120,19 @@ func InitPro() {
 	logic.GetAllowedIpForInetNodeClient = proLogic.GetAllowedIpForInetNodeClient
 	mq.UpdateMetrics = proLogic.MQUpdateMetrics
 	mq.UpdateMetricsFallBack = proLogic.MQUpdateMetricsFallBack
+	logic.GetFilteredNodesByUserAccess = proLogic.GetFilteredNodesByUserAccess
+	logic.CreateRole = proLogic.CreateRole
+	logic.UpdateRole = proLogic.UpdateRole
+	logic.DeleteRole = proLogic.DeleteRole
+	logic.NetworkPermissionsCheck = proLogic.NetworkPermissionsCheck
+	logic.GlobalPermissionsCheck = proLogic.GlobalPermissionsCheck
+	logic.DeleteNetworkRoles = proLogic.DeleteNetworkRoles
+	logic.CreateDefaultNetworkRolesAndGroups = proLogic.CreateDefaultNetworkRolesAndGroups
+	logic.FilterNetworksByRole = proLogic.FilterNetworksByRole
+	logic.IsGroupsValid = proLogic.IsGroupsValid
+	logic.IsNetworkRolesValid = proLogic.IsNetworkRolesValid
+	logic.InitialiseRoles = proLogic.UserRolesInit
+	logic.UpdateUserGwAccess = proLogic.UpdateUserGwAccess
 }
 
 func retrieveProLogo() string {

+ 75 - 2
pro/logic/metrics.go

@@ -3,6 +3,7 @@ package logic
 import (
 	"encoding/json"
 	"math"
+	"sync"
 	"time"
 
 	mqtt "github.com/eclipse/paho.mqtt.golang"
@@ -15,9 +16,64 @@ import (
 	"golang.org/x/exp/slog"
 )
 
+var (
+	metricsCacheMutex = &sync.RWMutex{}
+	metricsCacheMap   map[string]models.Metrics
+)
+
+func getMetricsFromCache(key string) (metrics models.Metrics, ok bool) {
+	metricsCacheMutex.RLock()
+	metrics, ok = metricsCacheMap[key]
+	metricsCacheMutex.RUnlock()
+	return
+}
+
+func storeMetricsInCache(key string, metrics models.Metrics) {
+	metricsCacheMutex.Lock()
+	metricsCacheMap[key] = metrics
+	metricsCacheMutex.Unlock()
+}
+
+func deleteNetworkFromCache(key string) {
+	metricsCacheMutex.Lock()
+	delete(metricsCacheMap, key)
+	metricsCacheMutex.Unlock()
+}
+
+func LoadNodeMetricsToCache() error {
+	slog.Info("loading metrics to cache")
+	if metricsCacheMap == nil {
+		metricsCacheMap = map[string]models.Metrics{}
+	}
+
+	collection, err := database.FetchRecords(database.METRICS_TABLE_NAME)
+	if err != nil {
+		return err
+	}
+
+	for key, value := range collection {
+		var metrics models.Metrics
+		if err := json.Unmarshal([]byte(value), &metrics); err != nil {
+			slog.Error("parse metric record error", "error", err.Error())
+			continue
+		}
+		if servercfg.CacheEnabled() {
+			storeMetricsInCache(key, metrics)
+		}
+	}
+
+	slog.Info("metrics loading done")
+	return nil
+}
+
 // GetMetrics - gets the metrics
 func GetMetrics(nodeid string) (*models.Metrics, error) {
 	var metrics models.Metrics
+	if servercfg.CacheEnabled() {
+		if metrics, ok := getMetricsFromCache(nodeid); ok {
+			return &metrics, nil
+		}
+	}
 	record, err := database.FetchRecord(database.METRICS_TABLE_NAME, nodeid)
 	if err != nil {
 		if database.IsEmptyRecord(err) {
@@ -29,6 +85,9 @@ func GetMetrics(nodeid string) (*models.Metrics, error) {
 	if err != nil {
 		return &metrics, err
 	}
+	if servercfg.CacheEnabled() {
+		storeMetricsInCache(nodeid, metrics)
+	}
 	return &metrics, nil
 }
 
@@ -38,12 +97,26 @@ func UpdateMetrics(nodeid string, metrics *models.Metrics) error {
 	if err != nil {
 		return err
 	}
-	return database.Insert(nodeid, string(data), database.METRICS_TABLE_NAME)
+	err = database.Insert(nodeid, string(data), database.METRICS_TABLE_NAME)
+	if err != nil {
+		return err
+	}
+	if servercfg.CacheEnabled() {
+		storeMetricsInCache(nodeid, *metrics)
+	}
+	return nil
 }
 
 // DeleteMetrics - deletes metrics of a given node
 func DeleteMetrics(nodeid string) error {
-	return database.DeleteRecord(database.METRICS_TABLE_NAME, nodeid)
+	err := database.DeleteRecord(database.METRICS_TABLE_NAME, nodeid)
+	if err != nil {
+		return err
+	}
+	if servercfg.CacheEnabled() {
+		deleteNetworkFromCache(nodeid)
+	}
+	return nil
 }
 
 // MQUpdateMetricsFallBack - called when mq fallback thread is triggered on client

+ 188 - 0
pro/logic/security.go

@@ -0,0 +1,188 @@
+package logic
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+)
+
+func NetworkPermissionsCheck(username string, r *http.Request) error {
+	// at this point global checks should be completed
+	user, err := logic.GetUser(username)
+	if err != nil {
+		return err
+	}
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		return errors.New("access denied")
+	}
+	if userRole.FullAccess {
+		return nil
+	}
+	// get info from header to determine the target rsrc
+	targetRsrc := r.Header.Get("TARGET_RSRC")
+	targetRsrcID := r.Header.Get("TARGET_RSRC_ID")
+	netID := r.Header.Get("NET_ID")
+	if targetRsrc == "" {
+		return errors.New("target rsrc is missing")
+	}
+	if netID == "" {
+		return errors.New("network id is missing")
+	}
+	if r.Method == "" {
+		r.Method = http.MethodGet
+	}
+	if targetRsrc == models.MetricRsrc.String() {
+		return nil
+	}
+
+	// check if user has scope for target resource
+	// TODO - differentitate between global scope and network scope apis
+	// check for global network role
+	if netRoles, ok := user.NetworkRoles[models.AllNetworks]; ok {
+		for netRoleID := range netRoles {
+			err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID)
+			if err == nil {
+				return nil
+			}
+		}
+	}
+	netRoles := user.NetworkRoles[models.NetworkID(netID)]
+	for netRoleID := range netRoles {
+		err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID)
+		if err == nil {
+			return nil
+		}
+	}
+	for groupID := range user.UserGroups {
+		userG, err := GetUserGroup(groupID)
+		if err == nil {
+			netRoles := userG.NetworkRoles[models.NetworkID(netID)]
+			for netRoleID := range netRoles {
+				err = checkNetworkAccessPermissions(netRoleID, username, r.Method, targetRsrc, targetRsrcID, netID)
+				if err == nil {
+					return nil
+				}
+			}
+		}
+	}
+
+	return errors.New("access denied")
+}
+
+func checkNetworkAccessPermissions(netRoleID models.UserRoleID, username, reqScope, targetRsrc, targetRsrcID, netID string) error {
+	networkPermissionScope, err := logic.GetRole(netRoleID)
+	if err != nil {
+		return err
+	}
+	if networkPermissionScope.FullAccess {
+		return nil
+	}
+	rsrcPermissionScope, ok := networkPermissionScope.NetworkLevelAccess[models.RsrcType(targetRsrc)]
+	if targetRsrc == models.HostRsrc.String() && !ok {
+		rsrcPermissionScope, ok = networkPermissionScope.NetworkLevelAccess[models.RemoteAccessGwRsrc]
+	}
+	if !ok {
+		return errors.New("access denied")
+	}
+	if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[models.RsrcID(fmt.Sprintf("all_%s", targetRsrc))]; ok {
+		// handle extclient apis here
+		if models.RsrcType(targetRsrc) == models.ExtClientsRsrc && allRsrcsTypePermissionScope.SelfOnly && targetRsrcID != "" {
+			extclient, err := logic.GetExtClient(targetRsrcID, netID)
+			if err != nil {
+				return err
+			}
+			if !logic.IsUserAllowedAccessToExtClient(username, extclient) {
+				return errors.New("access denied")
+			}
+		}
+		err = checkPermissionScopeWithReqMethod(allRsrcsTypePermissionScope, reqScope)
+		if err == nil {
+			return nil
+		}
+
+	}
+	if targetRsrc == models.HostRsrc.String() {
+		if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[models.RsrcID(fmt.Sprintf("all_%s", models.RemoteAccessGwRsrc))]; ok {
+			err = checkPermissionScopeWithReqMethod(allRsrcsTypePermissionScope, reqScope)
+			if err == nil {
+				return nil
+			}
+		}
+	}
+	if targetRsrcID == "" {
+		return errors.New("target rsrc id is empty")
+	}
+	if scope, ok := rsrcPermissionScope[models.RsrcID(targetRsrcID)]; ok {
+		err = checkPermissionScopeWithReqMethod(scope, reqScope)
+		if err == nil {
+			return nil
+		}
+	}
+	return errors.New("access denied")
+}
+
+func GlobalPermissionsCheck(username string, r *http.Request) error {
+	user, err := logic.GetUser(username)
+	if err != nil {
+		return err
+	}
+	userRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		return errors.New("access denied")
+	}
+	if userRole.FullAccess {
+		return nil
+	}
+	targetRsrc := r.Header.Get("TARGET_RSRC")
+	targetRsrcID := r.Header.Get("TARGET_RSRC_ID")
+	if targetRsrc == "" {
+		return errors.New("target rsrc is missing")
+	}
+	if r.Method == "" {
+		r.Method = http.MethodGet
+	}
+	if targetRsrc == models.MetricRsrc.String() {
+		return nil
+	}
+	if (targetRsrc == models.HostRsrc.String() || targetRsrc == models.NetworkRsrc.String()) && r.Method == http.MethodGet && targetRsrcID == "" {
+		return nil
+	}
+	if targetRsrc == models.UserRsrc.String() && username == targetRsrcID && (r.Method != http.MethodDelete) {
+		return nil
+	}
+	rsrcPermissionScope, ok := userRole.GlobalLevelAccess[models.RsrcType(targetRsrc)]
+	if !ok {
+		return fmt.Errorf("access denied to %s", targetRsrc)
+	}
+	if allRsrcsTypePermissionScope, ok := rsrcPermissionScope[models.RsrcID(fmt.Sprintf("all_%s", targetRsrc))]; ok {
+		return checkPermissionScopeWithReqMethod(allRsrcsTypePermissionScope, r.Method)
+
+	}
+	if targetRsrcID == "" {
+		return errors.New("target rsrc id is missing")
+	}
+	if scope, ok := rsrcPermissionScope[models.RsrcID(targetRsrcID)]; ok {
+		return checkPermissionScopeWithReqMethod(scope, r.Method)
+	}
+	return errors.New("access denied")
+}
+
+func checkPermissionScopeWithReqMethod(scope models.RsrcPermissionScope, reqmethod string) error {
+	if reqmethod == http.MethodGet && scope.Read {
+		return nil
+	}
+	if (reqmethod == http.MethodPatch || reqmethod == http.MethodPut) && scope.Update {
+		return nil
+	}
+	if reqmethod == http.MethodDelete && scope.Delete {
+		return nil
+	}
+	if reqmethod == http.MethodPost && scope.Create {
+		return nil
+	}
+	return errors.New("operation not permitted")
+}

+ 1036 - 0
pro/logic/user_mgmt.go

@@ -0,0 +1,1036 @@
+package logic
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/servercfg"
+	"golang.org/x/exp/slog"
+)
+
+var ServiceUserPermissionTemplate = models.UserRolePermissionTemplate{
+	ID:                  models.ServiceUser,
+	Default:             true,
+	FullAccess:          false,
+	DenyDashboardAccess: true,
+}
+
+var PlatformUserUserPermissionTemplate = models.UserRolePermissionTemplate{
+	ID:         models.PlatformUser,
+	Default:    true,
+	FullAccess: false,
+}
+
+var NetworkAdminAllPermissionTemplate = models.UserRolePermissionTemplate{
+	ID:         models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkAdmin)),
+	Default:    true,
+	FullAccess: true,
+	NetworkID:  models.AllNetworks,
+}
+
+var NetworkUserAllPermissionTemplate = models.UserRolePermissionTemplate{
+	ID:         models.UserRoleID(fmt.Sprintf("global-%s", models.NetworkUser)),
+	Default:    true,
+	FullAccess: false,
+	NetworkID:  models.AllNetworks,
+	NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
+		models.RemoteAccessGwRsrc: {
+			models.AllRemoteAccessGwRsrcID: models.RsrcPermissionScope{
+				Read:      true,
+				VPNaccess: true,
+			},
+		},
+		models.ExtClientsRsrc: {
+			models.AllExtClientsRsrcID: models.RsrcPermissionScope{
+				Read:     true,
+				Create:   true,
+				Update:   true,
+				Delete:   true,
+				SelfOnly: true,
+			},
+		},
+	},
+}
+
+func UserRolesInit() {
+	d, _ := json.Marshal(logic.SuperAdminPermissionTemplate)
+	database.Insert(logic.SuperAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(logic.AdminPermissionTemplate)
+	database.Insert(logic.AdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(ServiceUserPermissionTemplate)
+	database.Insert(ServiceUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(PlatformUserUserPermissionTemplate)
+	database.Insert(PlatformUserUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(NetworkAdminAllPermissionTemplate)
+	database.Insert(NetworkAdminAllPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(NetworkUserAllPermissionTemplate)
+	database.Insert(NetworkUserAllPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+
+}
+
+func CreateDefaultNetworkRolesAndGroups(netID models.NetworkID) {
+	var NetworkAdminPermissionTemplate = models.UserRolePermissionTemplate{
+		ID:                 models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkAdmin)),
+		Default:            true,
+		NetworkID:          netID,
+		FullAccess:         true,
+		NetworkLevelAccess: make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope),
+	}
+
+	var NetworkUserPermissionTemplate = models.UserRolePermissionTemplate{
+		ID:                  models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)),
+		Default:             true,
+		FullAccess:          false,
+		NetworkID:           netID,
+		DenyDashboardAccess: false,
+		NetworkLevelAccess: map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope{
+			models.RemoteAccessGwRsrc: {
+				models.AllRemoteAccessGwRsrcID: models.RsrcPermissionScope{
+					Read:      true,
+					VPNaccess: true,
+				},
+			},
+			models.ExtClientsRsrc: {
+				models.AllExtClientsRsrcID: models.RsrcPermissionScope{
+					Read:     true,
+					Create:   true,
+					Update:   true,
+					Delete:   true,
+					SelfOnly: true,
+				},
+			},
+		},
+	}
+	d, _ := json.Marshal(NetworkAdminPermissionTemplate)
+	database.Insert(NetworkAdminPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+	d, _ = json.Marshal(NetworkUserPermissionTemplate)
+	database.Insert(NetworkUserPermissionTemplate.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+
+	// create default network groups
+	var NetworkAdminGroup = models.UserGroup{
+		ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin)),
+		NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
+			netID: {
+				models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkAdmin)): {},
+			},
+		},
+		MetaData: "The network role was automatically created by Netmaker.",
+	}
+	var NetworkUserGroup = models.UserGroup{
+		ID: models.UserGroupID(fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)),
+		NetworkRoles: map[models.NetworkID]map[models.UserRoleID]struct{}{
+			netID: {
+				models.UserRoleID(fmt.Sprintf("%s-%s", netID, models.NetworkUser)): {},
+			},
+		},
+		MetaData: "The network role was automatically created by Netmaker.",
+	}
+	d, _ = json.Marshal(NetworkAdminGroup)
+	database.Insert(NetworkAdminGroup.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
+	d, _ = json.Marshal(NetworkUserGroup)
+	database.Insert(NetworkUserGroup.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
+}
+
+func DeleteNetworkRoles(netID string) {
+	users, err := logic.GetUsersDB()
+	if err != nil {
+		return
+	}
+	defaultUserGrp := fmt.Sprintf("%s-%s-grp", netID, models.NetworkUser)
+	defaultAdminGrp := fmt.Sprintf("%s-%s-grp", netID, models.NetworkAdmin)
+	for _, user := range users {
+		var upsert bool
+		if _, ok := user.NetworkRoles[models.NetworkID(netID)]; ok {
+			delete(user.NetworkRoles, models.NetworkID(netID))
+			upsert = true
+		}
+		if _, ok := user.UserGroups[models.UserGroupID(defaultUserGrp)]; ok {
+			delete(user.UserGroups, models.UserGroupID(defaultUserGrp))
+			upsert = true
+		}
+		if _, ok := user.UserGroups[models.UserGroupID(defaultAdminGrp)]; ok {
+			delete(user.UserGroups, models.UserGroupID(defaultAdminGrp))
+			upsert = true
+		}
+		if upsert {
+			logic.UpsertUser(user)
+		}
+	}
+	database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, defaultUserGrp)
+	database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, defaultAdminGrp)
+	userGs, _ := ListUserGroups()
+	for _, userGI := range userGs {
+		if _, ok := userGI.NetworkRoles[models.NetworkID(netID)]; ok {
+			delete(userGI.NetworkRoles, models.NetworkID(netID))
+			UpdateUserGroup(userGI)
+		}
+	}
+
+	roles, _ := ListNetworkRoles()
+	for _, role := range roles {
+		if role.NetworkID.String() == netID {
+			database.DeleteRecord(database.USER_PERMISSIONS_TABLE_NAME, role.ID.String())
+		}
+	}
+}
+
+// ListNetworkRoles - lists user network roles permission templates
+func ListNetworkRoles() ([]models.UserRolePermissionTemplate, error) {
+	data, err := database.FetchRecords(database.USER_PERMISSIONS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return []models.UserRolePermissionTemplate{}, err
+	}
+	userRoles := []models.UserRolePermissionTemplate{}
+	for _, dataI := range data {
+		userRole := models.UserRolePermissionTemplate{}
+		err := json.Unmarshal([]byte(dataI), &userRole)
+		if err != nil {
+			continue
+		}
+		if userRole.NetworkID == "" {
+			continue
+		}
+		userRoles = append(userRoles, userRole)
+	}
+	return userRoles, nil
+}
+
+func ValidateCreateRoleReq(userRole *models.UserRolePermissionTemplate) error {
+	// check if role exists with this id
+	_, err := logic.GetRole(userRole.ID)
+	if err == nil {
+		return fmt.Errorf("role with id `%s` exists already", userRole.ID.String())
+	}
+	if len(userRole.NetworkLevelAccess) > 0 {
+		for rsrcType := range userRole.NetworkLevelAccess {
+			if _, ok := models.RsrcTypeMap[rsrcType]; !ok {
+				return errors.New("invalid rsrc type " + rsrcType.String())
+			}
+			if rsrcType == models.RemoteAccessGwRsrc {
+				userRsrcPermissions := userRole.NetworkLevelAccess[models.RemoteAccessGwRsrc]
+				var vpnAccess bool
+				for _, scope := range userRsrcPermissions {
+					if scope.VPNaccess {
+						vpnAccess = true
+						break
+					}
+				}
+				if vpnAccess {
+					userRole.NetworkLevelAccess[models.ExtClientsRsrc] = map[models.RsrcID]models.RsrcPermissionScope{
+						models.AllExtClientsRsrcID: {
+							Read:     true,
+							Create:   true,
+							Update:   true,
+							Delete:   true,
+							SelfOnly: true,
+						},
+					}
+
+				}
+
+			}
+		}
+	}
+	if userRole.NetworkID == "" {
+		return errors.New("only network roles are allowed to be created")
+	}
+	return nil
+}
+
+func ValidateUpdateRoleReq(userRole *models.UserRolePermissionTemplate) error {
+	roleInDB, err := logic.GetRole(userRole.ID)
+	if err != nil {
+		return err
+	}
+	if roleInDB.NetworkID != userRole.NetworkID {
+		return errors.New("network id mismatch")
+	}
+	if roleInDB.Default {
+		return errors.New("cannot update default role")
+	}
+	if len(userRole.NetworkLevelAccess) > 0 {
+		for rsrcType := range userRole.NetworkLevelAccess {
+			if _, ok := models.RsrcTypeMap[rsrcType]; !ok {
+				return errors.New("invalid rsrc type " + rsrcType.String())
+			}
+			if rsrcType == models.RemoteAccessGwRsrc {
+				userRsrcPermissions := userRole.NetworkLevelAccess[models.RemoteAccessGwRsrc]
+				var vpnAccess bool
+				for _, scope := range userRsrcPermissions {
+					if scope.VPNaccess {
+						vpnAccess = true
+						break
+					}
+				}
+				if vpnAccess {
+					userRole.NetworkLevelAccess[models.ExtClientsRsrc] = map[models.RsrcID]models.RsrcPermissionScope{
+						models.AllExtClientsRsrcID: {
+							Read:     true,
+							Create:   true,
+							Update:   true,
+							Delete:   true,
+							SelfOnly: true,
+						},
+					}
+
+				}
+
+			}
+		}
+	}
+	return nil
+}
+
+// CreateRole - inserts new role into DB
+func CreateRole(r models.UserRolePermissionTemplate) error {
+	// check if role already exists
+	if r.ID.String() == "" {
+		return errors.New("role id cannot be empty")
+	}
+	_, err := database.FetchRecord(database.USER_PERMISSIONS_TABLE_NAME, r.ID.String())
+	if err == nil {
+		return errors.New("role already exists")
+	}
+	d, err := json.Marshal(r)
+	if err != nil {
+		return err
+	}
+	return database.Insert(r.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+}
+
+// UpdateRole - updates role template
+func UpdateRole(r models.UserRolePermissionTemplate) error {
+	if r.ID.String() == "" {
+		return errors.New("role id cannot be empty")
+	}
+	_, err := database.FetchRecord(database.USER_PERMISSIONS_TABLE_NAME, r.ID.String())
+	if err != nil {
+		return err
+	}
+	d, err := json.Marshal(r)
+	if err != nil {
+		return err
+	}
+	return database.Insert(r.ID.String(), string(d), database.USER_PERMISSIONS_TABLE_NAME)
+}
+
+// DeleteRole - deletes user role
+func DeleteRole(rid models.UserRoleID, force bool) error {
+	if rid.String() == "" {
+		return errors.New("role id cannot be empty")
+	}
+	users, err := logic.GetUsersDB()
+	if err != nil {
+		return err
+	}
+	role, err := logic.GetRole(rid)
+	if err != nil {
+		return err
+	}
+	if role.NetworkID == "" {
+		return errors.New("cannot delete platform role")
+	}
+	// allow deletion of default network roles if network doesn't exist
+	if role.NetworkID == models.AllNetworks {
+		return errors.New("cannot delete default network role")
+	}
+	// check if network exists
+	exists, _ := logic.NetworkExists(role.NetworkID.String())
+	if role.Default {
+		if exists && !force {
+			return errors.New("cannot delete default role")
+		}
+	}
+	for _, user := range users {
+		for userG := range user.UserGroups {
+			ug, err := GetUserGroup(userG)
+			if err == nil {
+				if role.NetworkID != "" {
+					for netID, networkRoles := range ug.NetworkRoles {
+						if _, ok := networkRoles[rid]; ok {
+							delete(networkRoles, rid)
+							ug.NetworkRoles[netID] = networkRoles
+							UpdateUserGroup(ug)
+						}
+
+					}
+				}
+
+			}
+		}
+
+		if user.PlatformRoleID == rid {
+			err = errors.New("active roles cannot be deleted.switch existing users to a new role before deleting")
+			return err
+		}
+		if role.NetworkID != "" {
+			for netID, networkRoles := range user.NetworkRoles {
+				if _, ok := networkRoles[rid]; ok {
+					delete(networkRoles, rid)
+					user.NetworkRoles[netID] = networkRoles
+					logic.UpsertUser(user)
+				}
+
+			}
+		}
+	}
+
+	return database.DeleteRecord(database.USER_PERMISSIONS_TABLE_NAME, rid.String())
+}
+
+func ValidateCreateGroupReq(g models.UserGroup) error {
+
+	// check if network roles are valid
+	for _, roleMap := range g.NetworkRoles {
+		for roleID := range roleMap {
+			role, err := logic.GetRole(roleID)
+			if err != nil {
+				return fmt.Errorf("invalid network role %s", roleID)
+			}
+			if role.NetworkID == "" {
+				return errors.New("platform role cannot be used as network role")
+			}
+		}
+	}
+	return nil
+}
+func ValidateUpdateGroupReq(g models.UserGroup) error {
+
+	for networkID := range g.NetworkRoles {
+		userRolesMap := g.NetworkRoles[networkID]
+		for roleID := range userRolesMap {
+			netRole, err := logic.GetRole(roleID)
+			if err != nil {
+				err = fmt.Errorf("invalid network role")
+				return err
+			}
+			if netRole.NetworkID == "" {
+				return errors.New("platform role cannot be used as network role")
+			}
+		}
+	}
+	return nil
+}
+
+// CreateUserGroup - creates new user group
+func CreateUserGroup(g models.UserGroup) error {
+	// check if role already exists
+	if g.ID == "" {
+		return errors.New("group id cannot be empty")
+	}
+	_, err := database.FetchRecord(database.USER_GROUPS_TABLE_NAME, g.ID.String())
+	if err == nil {
+		return errors.New("group already exists")
+	}
+	d, err := json.Marshal(g)
+	if err != nil {
+		return err
+	}
+	return database.Insert(g.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
+}
+
+// GetUserGroup - fetches user group
+func GetUserGroup(gid models.UserGroupID) (models.UserGroup, error) {
+	d, err := database.FetchRecord(database.USER_GROUPS_TABLE_NAME, gid.String())
+	if err != nil {
+		return models.UserGroup{}, err
+	}
+	var ug models.UserGroup
+	err = json.Unmarshal([]byte(d), &ug)
+	if err != nil {
+		return ug, err
+	}
+	return ug, nil
+}
+
+// ListUserGroups - lists user groups
+func ListUserGroups() ([]models.UserGroup, error) {
+	data, err := database.FetchRecords(database.USER_GROUPS_TABLE_NAME)
+	if err != nil && !database.IsEmptyRecord(err) {
+		return []models.UserGroup{}, err
+	}
+	userGroups := []models.UserGroup{}
+	for _, dataI := range data {
+		userGroup := models.UserGroup{}
+		err := json.Unmarshal([]byte(dataI), &userGroup)
+		if err != nil {
+			continue
+		}
+		userGroups = append(userGroups, userGroup)
+	}
+	return userGroups, nil
+}
+
+// UpdateUserGroup - updates new user group
+func UpdateUserGroup(g models.UserGroup) error {
+	// check if group exists
+	if g.ID == "" {
+		return errors.New("group id cannot be empty")
+	}
+	_, err := database.FetchRecord(database.USER_GROUPS_TABLE_NAME, g.ID.String())
+	if err != nil {
+		return err
+	}
+	d, err := json.Marshal(g)
+	if err != nil {
+		return err
+	}
+	return database.Insert(g.ID.String(), string(d), database.USER_GROUPS_TABLE_NAME)
+}
+
+// DeleteUserGroup - deletes user group
+func DeleteUserGroup(gid models.UserGroupID) error {
+	users, err := logic.GetUsersDB()
+	if err != nil {
+		return err
+	}
+	for _, user := range users {
+		delete(user.UserGroups, gid)
+		logic.UpsertUser(user)
+	}
+	return database.DeleteRecord(database.USER_GROUPS_TABLE_NAME, gid.String())
+}
+
+func HasNetworkRsrcScope(permissionTemplate models.UserRolePermissionTemplate, netid string, rsrcType models.RsrcType, rsrcID models.RsrcID, op string) bool {
+	if permissionTemplate.FullAccess {
+		return true
+	}
+
+	rsrcScope, ok := permissionTemplate.NetworkLevelAccess[rsrcType]
+	if !ok {
+		return false
+	}
+	_, ok = rsrcScope[rsrcID]
+	return ok
+}
+func GetUserRAGNodes(user models.User) (gws map[string]models.Node) {
+	gws = make(map[string]models.Node)
+	userGwAccessScope := GetUserNetworkRolesWithRemoteVPNAccess(user)
+	logger.Log(3, fmt.Sprintf("User Gw Access Scope: %+v", userGwAccessScope))
+	_, allNetAccess := userGwAccessScope["*"]
+	nodes, err := logic.GetAllNodes()
+	if err != nil {
+		return
+	}
+	for _, node := range nodes {
+		if node.IsIngressGateway && !node.PendingDelete {
+			if allNetAccess {
+				gws[node.ID.String()] = node
+			} else {
+				gwRsrcMap := userGwAccessScope[models.NetworkID(node.Network)]
+				scope, ok := gwRsrcMap[models.AllRemoteAccessGwRsrcID]
+				if !ok {
+					if scope, ok = gwRsrcMap[models.RsrcID(node.ID.String())]; !ok {
+						continue
+					}
+				}
+				if scope.VPNaccess {
+					gws[node.ID.String()] = node
+				}
+
+			}
+		}
+	}
+	return
+}
+
+// GetUserNetworkRoles - get user network roles
+func GetUserNetworkRolesWithRemoteVPNAccess(user models.User) (gwAccess map[models.NetworkID]map[models.RsrcID]models.RsrcPermissionScope) {
+	gwAccess = make(map[models.NetworkID]map[models.RsrcID]models.RsrcPermissionScope)
+	platformRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		return
+	}
+	if platformRole.FullAccess {
+		gwAccess[models.NetworkID("*")] = make(map[models.RsrcID]models.RsrcPermissionScope)
+		return
+	}
+	if _, ok := user.NetworkRoles[models.AllNetworks]; ok {
+		gwAccess[models.NetworkID("*")] = make(map[models.RsrcID]models.RsrcPermissionScope)
+	}
+	if len(user.UserGroups) > 0 {
+		for gID := range user.UserGroups {
+			userG, err := GetUserGroup(gID)
+			if err != nil {
+				continue
+			}
+			for netID, roleMap := range userG.NetworkRoles {
+				for roleID := range roleMap {
+					role, err := logic.GetRole(roleID)
+					if err == nil {
+						if role.FullAccess {
+							gwAccess[netID] = map[models.RsrcID]models.RsrcPermissionScope{
+								models.AllRemoteAccessGwRsrcID: {
+									Create:    true,
+									Read:      true,
+									Update:    true,
+									VPNaccess: true,
+									Delete:    true,
+								},
+								models.AllExtClientsRsrcID: {
+									Create: true,
+									Read:   true,
+									Update: true,
+									Delete: true,
+								},
+							}
+							break
+						}
+						if rsrcsMap, ok := role.NetworkLevelAccess[models.RemoteAccessGwRsrc]; ok {
+							if permissions, ok := rsrcsMap[models.AllRemoteAccessGwRsrcID]; ok && permissions.VPNaccess {
+								if len(gwAccess[netID]) == 0 {
+									gwAccess[netID] = make(map[models.RsrcID]models.RsrcPermissionScope)
+								}
+								gwAccess[netID][models.AllRemoteAccessGwRsrcID] = permissions
+								break
+							} else {
+								for gwID, scope := range rsrcsMap {
+									if scope.VPNaccess {
+										if len(gwAccess[netID]) == 0 {
+											gwAccess[netID] = make(map[models.RsrcID]models.RsrcPermissionScope)
+										}
+										gwAccess[netID][gwID] = scope
+									}
+								}
+							}
+
+						}
+
+					}
+				}
+			}
+		}
+	}
+	for netID, roleMap := range user.NetworkRoles {
+		for roleID := range roleMap {
+			role, err := logic.GetRole(roleID)
+			if err == nil {
+				if role.FullAccess {
+					gwAccess[netID] = map[models.RsrcID]models.RsrcPermissionScope{
+						models.AllRemoteAccessGwRsrcID: {
+							Create:    true,
+							Read:      true,
+							Update:    true,
+							VPNaccess: true,
+							Delete:    true,
+						},
+						models.AllExtClientsRsrcID: {
+							Create: true,
+							Read:   true,
+							Update: true,
+							Delete: true,
+						},
+					}
+					break
+				}
+				if rsrcsMap, ok := role.NetworkLevelAccess[models.RemoteAccessGwRsrc]; ok {
+					if permissions, ok := rsrcsMap[models.AllRemoteAccessGwRsrcID]; ok && permissions.VPNaccess {
+						if len(gwAccess[netID]) == 0 {
+							gwAccess[netID] = make(map[models.RsrcID]models.RsrcPermissionScope)
+						}
+						gwAccess[netID][models.AllRemoteAccessGwRsrcID] = permissions
+						break
+					} else {
+						for gwID, scope := range rsrcsMap {
+							if scope.VPNaccess {
+								if len(gwAccess[netID]) == 0 {
+									gwAccess[netID] = make(map[models.RsrcID]models.RsrcPermissionScope)
+								}
+								gwAccess[netID][gwID] = scope
+							}
+						}
+					}
+
+				}
+
+			}
+		}
+	}
+
+	return
+}
+
+func GetFilteredNodesByUserAccess(user models.User, nodes []models.Node) (filteredNodes []models.Node) {
+
+	nodesMap := make(map[string]struct{})
+	allNetworkRoles := make(map[models.UserRoleID]struct{})
+
+	if len(user.NetworkRoles) > 0 {
+		for _, netRoles := range user.NetworkRoles {
+			for netRoleI := range netRoles {
+				allNetworkRoles[netRoleI] = struct{}{}
+			}
+		}
+	}
+	if _, ok := user.NetworkRoles[models.AllNetworks]; ok {
+		return nodes
+	}
+	if len(user.UserGroups) > 0 {
+		for userGID := range user.UserGroups {
+			userG, err := GetUserGroup(userGID)
+			if err == nil {
+				if len(userG.NetworkRoles) > 0 {
+					if _, ok := userG.NetworkRoles[models.AllNetworks]; ok {
+						return nodes
+					}
+					for _, netRoles := range userG.NetworkRoles {
+						for netRoleI := range netRoles {
+							allNetworkRoles[netRoleI] = struct{}{}
+						}
+					}
+				}
+			}
+		}
+	}
+	for networkRoleID := range allNetworkRoles {
+		userPermTemplate, err := logic.GetRole(networkRoleID)
+		if err != nil {
+			continue
+		}
+		networkNodes := logic.GetNetworkNodesMemory(nodes, userPermTemplate.NetworkID.String())
+		if userPermTemplate.FullAccess {
+			for _, node := range networkNodes {
+				if _, ok := nodesMap[node.ID.String()]; ok {
+					continue
+				}
+				nodesMap[node.ID.String()] = struct{}{}
+				filteredNodes = append(filteredNodes, node)
+			}
+
+			continue
+		}
+		if rsrcPerms, ok := userPermTemplate.NetworkLevelAccess[models.RemoteAccessGwRsrc]; ok {
+			if _, ok := rsrcPerms[models.AllRemoteAccessGwRsrcID]; ok {
+				for _, node := range networkNodes {
+					if _, ok := nodesMap[node.ID.String()]; ok {
+						continue
+					}
+					if node.IsIngressGateway {
+						nodesMap[node.ID.String()] = struct{}{}
+						filteredNodes = append(filteredNodes, node)
+					}
+				}
+			} else {
+				for gwID, scope := range rsrcPerms {
+					if _, ok := nodesMap[gwID.String()]; ok {
+						continue
+					}
+					if scope.Read {
+						gwNode, err := logic.GetNodeByID(gwID.String())
+						if err == nil && gwNode.IsIngressGateway {
+							nodesMap[gwNode.ID.String()] = struct{}{}
+							filteredNodes = append(filteredNodes, gwNode)
+						}
+					}
+				}
+			}
+		}
+
+	}
+	return
+}
+
+func FilterNetworksByRole(allnetworks []models.Network, user models.User) []models.Network {
+	platformRole, err := logic.GetRole(user.PlatformRoleID)
+	if err != nil {
+		return []models.Network{}
+	}
+	if !platformRole.FullAccess {
+		allNetworkRoles := make(map[models.NetworkID]struct{})
+		if len(user.NetworkRoles) > 0 {
+			for netID := range user.NetworkRoles {
+				if netID == models.AllNetworks {
+					return allnetworks
+				}
+				allNetworkRoles[netID] = struct{}{}
+
+			}
+		}
+		if len(user.UserGroups) > 0 {
+			for userGID := range user.UserGroups {
+				userG, err := GetUserGroup(userGID)
+				if err == nil {
+					if len(userG.NetworkRoles) > 0 {
+						for netID := range userG.NetworkRoles {
+							if netID == models.AllNetworks {
+								return allnetworks
+							}
+							allNetworkRoles[netID] = struct{}{}
+
+						}
+					}
+				}
+			}
+		}
+		filteredNetworks := []models.Network{}
+		for _, networkI := range allnetworks {
+			if _, ok := allNetworkRoles[models.NetworkID(networkI.NetID)]; ok {
+				filteredNetworks = append(filteredNetworks, networkI)
+			}
+		}
+		allnetworks = filteredNetworks
+	}
+	return allnetworks
+}
+
+func IsGroupsValid(groups map[models.UserGroupID]struct{}) error {
+
+	for groupID := range groups {
+		_, err := GetUserGroup(groupID)
+		if err != nil {
+			return fmt.Errorf("user group `%s` not found", groupID)
+		}
+	}
+	return nil
+}
+
+func IsNetworkRolesValid(networkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) error {
+	for netID, netRoles := range networkRoles {
+
+		if netID != models.AllNetworks {
+			_, err := logic.GetNetwork(netID.String())
+			if err != nil {
+				return fmt.Errorf("failed to fetch network %s ", netID)
+			}
+		}
+		for netRoleID := range netRoles {
+			role, err := logic.GetRole(netRoleID)
+			if err != nil {
+				return fmt.Errorf("failed to fetch role %s ", netRoleID)
+			}
+			if role.NetworkID == "" {
+				return fmt.Errorf("cannot use platform as network role %s", netRoleID)
+			}
+		}
+	}
+	return nil
+}
+
+// PrepareOauthUserFromInvite - init oauth user before create
+func PrepareOauthUserFromInvite(in models.UserInvite) (models.User, error) {
+	var newPass, fetchErr = logic.FetchPassValue("")
+	if fetchErr != nil {
+		return models.User{}, fetchErr
+	}
+	user := models.User{
+		UserName: in.Email,
+		Password: newPass,
+	}
+	user.UserGroups = in.UserGroups
+	user.NetworkRoles = in.NetworkRoles
+	user.PlatformRoleID = models.UserRoleID(in.PlatformRoleID)
+	if user.PlatformRoleID == "" {
+		user.PlatformRoleID = models.ServiceUser
+	}
+	return user, nil
+}
+
+func UpdatesUserGwAccessOnRoleUpdates(currNetworkAccess,
+	changeNetworkAccess map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope, netID string) {
+	networkChangeMap := make(map[models.RsrcID]models.RsrcPermissionScope)
+	for rsrcType, RsrcPermsMap := range currNetworkAccess {
+		if rsrcType != models.RemoteAccessGwRsrc {
+			continue
+		}
+		if _, ok := changeNetworkAccess[rsrcType]; !ok {
+			for rsrcID, scope := range RsrcPermsMap {
+				networkChangeMap[rsrcID] = scope
+			}
+		} else {
+			for rsrcID, scope := range RsrcPermsMap {
+				if _, ok := changeNetworkAccess[rsrcType][rsrcID]; !ok {
+					networkChangeMap[rsrcID] = scope
+				}
+			}
+		}
+	}
+
+	extclients, err := logic.GetAllExtClients()
+	if err != nil {
+		slog.Error("failed to fetch extclients", "error", err)
+		return
+	}
+	userMap, err := logic.GetUserMap()
+	if err != nil {
+		return
+	}
+	for _, extclient := range extclients {
+		if extclient.Network != netID {
+			continue
+		}
+		if _, ok := networkChangeMap[models.AllRemoteAccessGwRsrcID]; ok {
+			if user, ok := userMap[extclient.OwnerID]; ok {
+				if user.PlatformRoleID != models.ServiceUser {
+					continue
+				}
+				err = logic.DeleteExtClientAndCleanup(extclient)
+				if err != nil {
+					slog.Error("failed to delete extclient",
+						"id", extclient.ClientID, "owner", user.UserName, "error", err)
+				} else {
+					if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
+						slog.Error("error setting ext peers: " + err.Error())
+					}
+				}
+			}
+			continue
+		}
+		if _, ok := networkChangeMap[models.RsrcID(extclient.IngressGatewayID)]; ok {
+			if user, ok := userMap[extclient.OwnerID]; ok {
+				if user.PlatformRoleID != models.ServiceUser {
+					continue
+				}
+				err = logic.DeleteExtClientAndCleanup(extclient)
+				if err != nil {
+					slog.Error("failed to delete extclient",
+						"id", extclient.ClientID, "owner", user.UserName, "error", err)
+				} else {
+					if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
+						slog.Error("error setting ext peers: " + err.Error())
+					}
+				}
+			}
+
+		}
+
+	}
+	if servercfg.IsDNSMode() {
+		logic.SetDNS()
+	}
+}
+
+func UpdatesUserGwAccessOnGrpUpdates(currNetworkRoles, changeNetworkRoles map[models.NetworkID]map[models.UserRoleID]struct{}) {
+	networkChangeMap := make(map[models.NetworkID]map[models.UserRoleID]struct{})
+	for netID, networkUserRoles := range currNetworkRoles {
+		if _, ok := changeNetworkRoles[netID]; !ok {
+			for netRoleID := range networkUserRoles {
+				if _, ok := networkChangeMap[netID]; !ok {
+					networkChangeMap[netID] = make(map[models.UserRoleID]struct{})
+				}
+				networkChangeMap[netID][netRoleID] = struct{}{}
+			}
+		} else {
+			for netRoleID := range networkUserRoles {
+				if _, ok := changeNetworkRoles[netID][netRoleID]; !ok {
+					if _, ok := networkChangeMap[netID]; !ok {
+						networkChangeMap[netID] = make(map[models.UserRoleID]struct{})
+					}
+					networkChangeMap[netID][netRoleID] = struct{}{}
+				}
+			}
+		}
+	}
+	extclients, err := logic.GetAllExtClients()
+	if err != nil {
+		slog.Error("failed to fetch extclients", "error", err)
+		return
+	}
+	userMap, err := logic.GetUserMap()
+	if err != nil {
+		return
+	}
+	for _, extclient := range extclients {
+
+		if _, ok := networkChangeMap[models.NetworkID(extclient.Network)]; ok {
+			if user, ok := userMap[extclient.OwnerID]; ok {
+				if user.PlatformRoleID != models.ServiceUser {
+					continue
+				}
+				err = logic.DeleteExtClientAndCleanup(extclient)
+				if err != nil {
+					slog.Error("failed to delete extclient",
+						"id", extclient.ClientID, "owner", user.UserName, "error", err)
+				} else {
+					if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
+						slog.Error("error setting ext peers: " + err.Error())
+					}
+				}
+			}
+
+		}
+
+	}
+	if servercfg.IsDNSMode() {
+		logic.SetDNS()
+	}
+
+}
+
+func UpdateUserGwAccess(currentUser, changeUser models.User) {
+	if changeUser.PlatformRoleID != models.ServiceUser {
+		return
+	}
+
+	networkChangeMap := make(map[models.NetworkID]map[models.UserRoleID]struct{})
+	for netID, networkUserRoles := range currentUser.NetworkRoles {
+		if _, ok := changeUser.NetworkRoles[netID]; !ok {
+			for netRoleID := range networkUserRoles {
+				if _, ok := networkChangeMap[netID]; !ok {
+					networkChangeMap[netID] = make(map[models.UserRoleID]struct{})
+				}
+				networkChangeMap[netID][netRoleID] = struct{}{}
+			}
+		} else {
+			for netRoleID := range networkUserRoles {
+				if _, ok := changeUser.NetworkRoles[netID][netRoleID]; !ok {
+					if _, ok := networkChangeMap[netID]; !ok {
+						networkChangeMap[netID] = make(map[models.UserRoleID]struct{})
+					}
+					networkChangeMap[netID][netRoleID] = struct{}{}
+				}
+			}
+		}
+	}
+	for gID := range currentUser.UserGroups {
+		if _, ok := changeUser.UserGroups[gID]; ok {
+			continue
+		}
+		userG, err := GetUserGroup(gID)
+		if err == nil {
+			for netID, networkUserRoles := range userG.NetworkRoles {
+				for netRoleID := range networkUserRoles {
+					if _, ok := networkChangeMap[netID]; !ok {
+						networkChangeMap[netID] = make(map[models.UserRoleID]struct{})
+					}
+					networkChangeMap[netID][netRoleID] = struct{}{}
+				}
+			}
+		}
+	}
+	if len(networkChangeMap) == 0 {
+		return
+	}
+	// TODO - cleanup gw access when role and groups are updated
+	//removedGwAccess
+	extclients, err := logic.GetAllExtClients()
+	if err != nil {
+		slog.Error("failed to fetch extclients", "error", err)
+		return
+	}
+	for _, extclient := range extclients {
+		if extclient.OwnerID == currentUser.UserName {
+			if _, ok := networkChangeMap[models.NetworkID(extclient.Network)]; ok {
+				err = logic.DeleteExtClientAndCleanup(extclient)
+				if err != nil {
+					slog.Error("failed to delete extclient",
+						"id", extclient.ClientID, "owner", changeUser.UserName, "error", err)
+				} else {
+					if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
+						slog.Error("error setting ext peers: " + err.Error())
+					}
+				}
+			}
+
+		}
+	}
+	if servercfg.IsDNSMode() {
+		logic.SetDNS()
+	}
+
+}

+ 1 - 1
pro/remote_access_client.go

@@ -78,7 +78,7 @@ func disableExtClient(client *models.ExtClient) error {
 			if err != nil {
 				return err
 			}
-			go mq.PublishSingleHostPeerUpdate(ingressHost, nodes, nil, []models.ExtClient{*client}, false)
+			go mq.PublishSingleHostPeerUpdate(ingressHost, nodes, nil, []models.ExtClient{*client}, false, nil)
 		} else {
 			return err
 		}

+ 6 - 7
release.md

@@ -1,15 +1,14 @@
-# Netmaker v0.24.3
+# Netmaker v0.25.0
 
 ## Whats New ✨
-- Validation Checks For Egress Routes
-- Network Change Detection System
-- Removed Creation Of ACLs For EMQX
+- Advanced User Management with Network Roles and Groups
+- User Invitation via Email and Magic Links
 
 ## What's Fixed/Improved 🛠
-- Removed RAG Metadata Length Restriction
+
 - Scalability Improvements
 - Optimised Traffic Flow Over MQ
-- Improved Validation Checks For Internet GWS
+- Improved Peer Updates with Batching
 
 ## Known Issues 🐞
 
@@ -18,5 +17,5 @@
 - IPv6 DNS Entries Are Not Working.
 - Stale Peer On The Interface, When Forced Removed From Multiple Networks At Once.
 - Can Still Ping Domain Name Even When DNS Toggle Is Switched Off.
-- WireGuard DNS issue on most flavors of Ubuntu 24.04 and some other newer Linux distributions. The issue is affecting the Remote Access Client (RAC) and the plain WireGuard external clients. Workaround can be found here https://help.netmaker.io/en/articles/9612016-extclient-rac-dns-issue-on-ubuntu-24-04.
+- WireGuard DNS issue on most flavours of Ubuntu 24.04 and some other newer Linux distributions. The issue is affecting the Remote Access Client (RAC) and the plain WireGuard external clients. Workaround can be found here https://help.netmaker.io/en/articles/9612016-extclient-rac-dns-issue-on-ubuntu-24-04.
 

+ 12 - 0
scripts/netmaker.default.env

@@ -75,3 +75,15 @@ RAC_AUTO_DISABLE=false
 CACHING_ENABLED=true
 # if turned on netclient checks if peers are reachable over private/LAN address, and choose that as peer endpoint
 ENDPOINT_DETECTION=true
+# config for sending emails
+# mail server host
+SMTP_HOST=smtp.gmail.com
+# mail server port
+SMTP_PORT=587
+# sender email
+EMAIL_SENDER_ADDR=
+# sender smtp user, if unset sender email will be used
+EMAIL_SENDER_USER=
+# sender smtp password
+EMAIL_SENDER_PASSWORD=
+

+ 6 - 4
scripts/nm-quick.sh

@@ -129,7 +129,7 @@ setup_netclient() {
 
 	echo "waiting for netclient to become available"
 	local found=false
-	local file=/etc/netclient/nodes.yml
+	local file=/etc/netclient/nodes.json
 	for ((a = 1; a <= 90; a++)); do
 		if [ -f "$file" ]; then
 			found=true
@@ -147,13 +147,13 @@ setup_netclient() {
 # configure_netclient - configures server's netclient as a default host and an ingress gateway
 configure_netclient() {
 	sleep 2
-	NODE_ID=$(sudo cat /etc/netclient/nodes.yml | yq -r .netmaker.commonnode.id)
+	NODE_ID=$(sudo cat /etc/netclient/nodes.json | jq -r .netmaker.id)
 	if [ "$NODE_ID" = "" ] || [ "$NODE_ID" = "null" ]; then
 		echo "Error obtaining NODE_ID for the new network"
 		exit 1
 	fi
 	echo "register complete. New node ID: $NODE_ID"
-	HOST_ID=$(sudo cat /etc/netclient/netclient.yml | yq -r .host.id)
+	HOST_ID=$(sudo cat /etc/netclient/netclient.json | jq -r .id)
 	if [ "$HOST_ID" = "" ] || [ "$HOST_ID" = "null" ]; then
 		echo "Error obtaining HOST_ID for the new network"
 		exit 1
@@ -231,6 +231,7 @@ save_config() { (
 	fi
 	if [ -n "$NETMAKER_BASE_DOMAIN" ]; then
 		save_config_item NM_DOMAIN "$NETMAKER_BASE_DOMAIN"
+		save_config_item FRONTEND_URL "https://dashboard.$NETMAKER_BASE_DOMAIN"
 	fi
 	save_config_item UI_IMAGE_TAG "$IMAGE_TAG"
 	# version-specific entries
@@ -252,7 +253,8 @@ save_config() { (
 		"INSTALL_TYPE" "NODE_ID" "DNS_MODE" "NETCLIENT_AUTO_UPDATE" "API_PORT"
 		"CORS_ALLOWED_ORIGIN" "DISPLAY_KEYS" "DATABASE" "SERVER_BROKER_ENDPOINT" "VERBOSITY"
 		"DEBUG_MODE"  "REST_BACKEND" "DISABLE_REMOTE_IP_CHECK" "TELEMETRY" "ALLOWED_EMAIL_DOMAINS" "AUTH_PROVIDER" "CLIENT_ID" "CLIENT_SECRET"
-		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE" "CACHING_ENABLED" "ENDPOINT_DETECTION")
+		"FRONTEND_URL" "AZURE_TENANT" "OIDC_ISSUER" "EXPORTER_API_PORT" "JWT_VALIDITY_DURATION" "RAC_AUTO_DISABLE" "CACHING_ENABLED" "ENDPOINT_DETECTION"
+		"SMTP_HOST" "SMTP_PORT" "EMAIL_SENDER_ADDR" "EMAIL_SENDER_USER" "EMAIL_SENDER_PASSWORD")
 	for name in "${toCopy[@]}"; do
 		save_config_item $name "${!name}"
 	done

+ 81 - 1
servercfg/serverconf.go

@@ -242,6 +242,59 @@ func GetPublicBrokerEndpoint() string {
 	}
 }
 
+func GetSmtpHost() string {
+	v := ""
+	if fromEnv := os.Getenv("SMTP_HOST"); fromEnv != "" {
+		v = fromEnv
+	} else if fromCfg := config.Config.Server.SmtpHost; fromCfg != "" {
+		v = fromCfg
+	}
+	return v
+}
+
+func GetSmtpPort() int {
+	v := 587
+	if fromEnv := os.Getenv("SMTP_PORT"); fromEnv != "" {
+		port, err := strconv.Atoi(fromEnv)
+		if err == nil {
+			v = port
+		}
+	} else if fromCfg := config.Config.Server.SmtpPort; fromCfg != 0 {
+		v = fromCfg
+	}
+	return v
+}
+
+func GetSenderEmail() string {
+	v := ""
+	if fromEnv := os.Getenv("EMAIL_SENDER_ADDR"); fromEnv != "" {
+		v = fromEnv
+	} else if fromCfg := config.Config.Server.EmailSenderAddr; fromCfg != "" {
+		v = fromCfg
+	}
+	return v
+}
+
+func GetSenderUser() string {
+	v := ""
+	if fromEnv := os.Getenv("EMAIL_SENDER_USER"); fromEnv != "" {
+		v = fromEnv
+	} else if fromCfg := config.Config.Server.EmailSenderUser; fromCfg != "" {
+		v = fromCfg
+	}
+	return v
+}
+
+func GetEmaiSenderPassword() string {
+	v := ""
+	if fromEnv := os.Getenv("EMAIL_SENDER_PASSWORD"); fromEnv != "" {
+		v = fromEnv
+	} else if fromCfg := config.Config.Server.EmailSenderPassword; fromCfg != "" {
+		v = fromCfg
+	}
+	return v
+}
+
 // GetOwnerEmail - gets the owner email (saas)
 func GetOwnerEmail() string {
 	return os.Getenv("SAAS_OWNER_EMAIL")
@@ -472,7 +525,7 @@ func GetPublicIP() (string, error) {
 			break
 		}
 	}
-	if err == nil && endpoint == "" {
+	if endpoint == "" {
 		err = errors.New("public address not found")
 	}
 	return endpoint, err
@@ -597,6 +650,28 @@ func GetMetricInterval() string {
 	return mi
 }
 
+// GetBatchPeerUpdate - if batch peer update
+func GetBatchPeerUpdate() bool {
+	enabled := true
+	if os.Getenv("PEER_UPDATE_BATCH") != "" {
+		enabled = os.Getenv("PEER_UPDATE_BATCH") == "true"
+	}
+	return enabled
+}
+
+// GetPeerUpdateBatchSize - get the batch size for peer update
+func GetPeerUpdateBatchSize() int {
+	//default 50
+	batchSize := 50
+	if os.Getenv("PEER_UPDATE_BATCH_SIZE") != "" {
+		b, e := strconv.Atoi(os.Getenv("PEER_UPDATE_BATCH_SIZE"))
+		if e == nil && b > 0 && b < 1000 {
+			batchSize = b
+		}
+	}
+	return batchSize
+}
+
 // GetEmqxRestEndpoint - returns the REST API Endpoint of EMQX
 func GetEmqxRestEndpoint() string {
 	return os.Getenv("EMQX_REST_ENDPOINT")
@@ -734,3 +809,8 @@ func GetAllowedEmailDomains() string {
 	}
 	return allowedDomains
 }
+
+// GetNmBaseDomain - fetches nm base domain
+func GetNmBaseDomain() string {
+	return os.Getenv("NM_DOMAIN")
+}

+ 3105 - 0
swagger.yaml

@@ -0,0 +1,3105 @@
+definitions:
+  acls.ACL:
+    additionalProperties:
+      type: integer
+    type: object
+  acls.ACLContainer:
+    additionalProperties:
+      $ref: '#/definitions/acls.ACL'
+    type: object
+  config.ServerConfig:
+    properties:
+      IsEE:
+        type: string
+      allowedEmailDomains:
+        type: string
+      allowedOrigin:
+        type: string
+      apiconnString:
+        type: string
+      apihost:
+        type: string
+      apiport:
+        type: string
+      authProvider:
+        type: string
+      azureTenant:
+        type: string
+      basicAuth:
+        type: string
+      broker:
+        type: string
+      brokerType:
+        type: string
+      cacheEnabled:
+        type: string
+      clientID:
+        type: string
+      clientSecret:
+        type: string
+      coreDNSAddr:
+        type: string
+      database:
+        type: string
+      deployedByOperator:
+        type: boolean
+      disableRemoteIPCheck:
+        type: string
+      displayKeys:
+        type: string
+      dnskey:
+        type: string
+      dnsmode:
+        type: string
+      egressesLimit:
+        type: integer
+      emqxRestEndpoint:
+        type: string
+      endpoint_detection:
+        type: boolean
+      environment:
+        type: string
+      frontendURL:
+        type: string
+      hostNetwork:
+        type: string
+      ingressesLimit:
+        type: integer
+      jwtValidityDuration:
+        $ref: '#/definitions/time.Duration'
+      licenseValue:
+        type: string
+      machinesLimit:
+        type: integer
+      masterKey:
+        type: string
+      messageQueueBackend:
+        type: string
+      metricInterval:
+        type: string
+      metricsExporter:
+        type: string
+      mqpassword:
+        type: string
+      mquserName:
+        type: string
+      netclientAutoUpdate:
+        type: string
+      netclientEndpointDetection:
+        type: string
+      netmakerTenantID:
+        type: string
+      networksLimit:
+        type: integer
+      nodeID:
+        type: string
+      oidcissuer:
+        type: string
+      platform:
+        type: string
+      publicIPService:
+        type: string
+      racAutoDisable:
+        type: boolean
+      restBackend:
+        type: string
+      server:
+        type: string
+      serverBrokerEndpoint:
+        type: string
+      sqlconn:
+        type: string
+      stunList:
+        type: string
+      stunPort:
+        type: integer
+      telemetry:
+        type: string
+      turnApiServer:
+        type: string
+      turnPassword:
+        type: string
+      turnPort:
+        type: integer
+      turnServer:
+        type: string
+      turnUserName:
+        type: string
+      useTurn:
+        type: boolean
+      usersLimit:
+        type: integer
+      verbosity:
+        type: integer
+      version:
+        type: string
+    type: object
+  models.APIEnrollmentKey:
+    properties:
+      expiration:
+        type: integer
+      networks:
+        items:
+          type: string
+        type: array
+      relay:
+        type: string
+      tags:
+        items:
+          type: string
+        type: array
+      type:
+        $ref: '#/definitions/models.KeyType'
+      unlimited:
+        type: boolean
+      uses_remaining:
+        type: integer
+    required:
+    - tags
+    type: object
+  models.ApiHost:
+    properties:
+      autoupdate:
+        type: boolean
+      debug:
+        type: boolean
+      defaultinterface:
+        type: string
+      endpointip:
+        type: string
+      endpointipv6:
+        type: string
+      firewallinuse:
+        type: string
+      id:
+        type: string
+      interfaces:
+        items:
+          $ref: '#/definitions/models.ApiIface'
+        type: array
+      isdefault:
+        type: boolean
+      isstatic:
+        type: boolean
+      isstaticport:
+        type: boolean
+      listenport:
+        type: integer
+      macaddress:
+        type: string
+      mtu:
+        type: integer
+      name:
+        type: string
+      nat_type:
+        type: string
+      nodes:
+        items:
+          type: string
+        type: array
+      os:
+        type: string
+      persistentkeepalive:
+        type: integer
+      publickey:
+        type: string
+      verbosity:
+        type: integer
+      version:
+        type: string
+      wg_public_listen_port:
+        type: integer
+    type: object
+  models.ApiIface:
+    properties:
+      addressString:
+        type: string
+      name:
+        type: string
+    type: object
+  models.ApiNode:
+    properties:
+      additional_rag_ips:
+        items:
+          type: string
+        type: array
+      address:
+        type: string
+      address6:
+        type: string
+      allowedips:
+        items:
+          type: string
+        type: array
+      connected:
+        type: boolean
+      defaultacl:
+        description: == PRO ==
+        type: string
+      dnson:
+        type: boolean
+      egressgatewaynatenabled:
+        type: boolean
+      egressgatewayranges:
+        items:
+          type: string
+        type: array
+      expdatetime:
+        type: integer
+      fail_over_peers:
+        additionalProperties:
+          type: object
+        type: object
+      failed_over_by:
+        type: string
+      hostid:
+        minLength: 5
+        type: string
+      id:
+        minLength: 5
+        type: string
+      inet_node_req:
+        $ref: '#/definitions/models.InetNodeReq'
+      ingressdns:
+        type: string
+      internetgw_node_id:
+        type: string
+      is_fail_over:
+        type: boolean
+      isegressgateway:
+        type: boolean
+      isingressgateway:
+        type: boolean
+      isinternetgateway:
+        type: boolean
+      isrelay:
+        type: boolean
+      isrelayed:
+        type: boolean
+      lastcheckin:
+        type: integer
+      lastmodified:
+        type: integer
+      lastpeerupdate:
+        type: integer
+      localaddress:
+        type: string
+      metadata:
+        type: string
+      network:
+        type: string
+      networkrange:
+        type: string
+      networkrange6:
+        type: string
+      pendingdelete:
+        type: boolean
+      relayedby:
+        type: string
+      relaynodes:
+        items:
+          type: string
+        type: array
+      server:
+        type: string
+    required:
+    - hostid
+    - id
+    type: object
+  models.AuthParams:
+    properties:
+      id:
+        type: string
+      macaddress:
+        type: string
+      password:
+        type: string
+    type: object
+  models.DNSEntry:
+    properties:
+      address:
+        type: string
+      address6:
+        type: string
+      name:
+        maxLength: 192
+        minLength: 1
+        type: string
+      network:
+        type: string
+    required:
+    - name
+    type: object
+  models.EgressGatewayRequest:
+    properties:
+      natenabled:
+        type: string
+      netid:
+        type: string
+      nodeid:
+        type: string
+      ranges:
+        items:
+          type: string
+        type: array
+    type: object
+  models.EgressInfo:
+    properties:
+      egress_gateway_cfg:
+        $ref: '#/definitions/models.EgressGatewayRequest'
+      egress_gw_addr:
+        $ref: '#/definitions/net.IPNet'
+      egress_gw_addr6:
+        $ref: '#/definitions/net.IPNet'
+      egress_id:
+        type: string
+      network:
+        $ref: '#/definitions/net.IPNet'
+      network6:
+        $ref: '#/definitions/net.IPNet'
+    type: object
+  models.EgressNetworkRoutes:
+    properties:
+      egress_gw_addr:
+        $ref: '#/definitions/net.IPNet'
+      egress_gw_addr6:
+        $ref: '#/definitions/net.IPNet'
+      egress_ranges:
+        items:
+          type: string
+        type: array
+      node_addr:
+        $ref: '#/definitions/net.IPNet'
+      node_addr6:
+        $ref: '#/definitions/net.IPNet'
+    type: object
+  models.EnrollmentKey:
+    properties:
+      expiration:
+        type: string
+      networks:
+        items:
+          type: string
+        type: array
+      relay:
+        type: string
+      tags:
+        items:
+          type: string
+        type: array
+      token:
+        description: B64 value of EnrollmentToken
+        type: string
+      type:
+        $ref: '#/definitions/models.KeyType'
+      unlimited:
+        type: boolean
+      uses_remaining:
+        type: integer
+      value:
+        type: string
+    type: object
+  models.ErrorResponse:
+    properties:
+      code:
+        type: integer
+      message:
+        type: string
+    type: object
+  models.ExtClient:
+    properties:
+      address:
+        type: string
+      address6:
+        type: string
+      allowed_ips:
+        items:
+          type: string
+        type: array
+      clientid:
+        type: string
+      deniednodeacls:
+        additionalProperties:
+          type: object
+        type: object
+      dns:
+        type: string
+      enabled:
+        type: boolean
+      extraallowedips:
+        items:
+          type: string
+        type: array
+      ingressgatewayendpoint:
+        type: string
+      ingressgatewayid:
+        type: string
+      lastmodified:
+        type: integer
+      network:
+        type: string
+      ownerid:
+        type: string
+      postdown:
+        type: string
+      postup:
+        type: string
+      privatekey:
+        type: string
+      publickey:
+        type: string
+      remote_access_client_id:
+        description: unique ID (MAC address) of RAC machine
+        type: string
+    type: object
+  models.FailOverMeReq:
+    properties:
+      node_id:
+        type: string
+    type: object
+  models.FwUpdate:
+    properties:
+      egress_info:
+        additionalProperties:
+          $ref: '#/definitions/models.EgressInfo'
+        type: object
+      is_egress_gw:
+        type: boolean
+    type: object
+  models.Host:
+    properties:
+      autoupdate:
+        type: boolean
+      daemoninstalled:
+        type: boolean
+      debug:
+        type: boolean
+      defaultinterface:
+        type: string
+      endpointip:
+        items:
+          type: integer
+        type: array
+      endpointipv6:
+        items:
+          type: integer
+        type: array
+      firewallinuse:
+        type: string
+      hostpass:
+        type: string
+      id:
+        type: string
+      interface:
+        type: string
+      interfaces:
+        items:
+          $ref: '#/definitions/models.Iface'
+        type: array
+      ipforwarding:
+        type: boolean
+      isdefault:
+        type: boolean
+      isdocker:
+        type: boolean
+      isk8s:
+        type: boolean
+      isstatic:
+        type: boolean
+      isstaticport:
+        type: boolean
+      listenport:
+        type: integer
+      macaddress:
+        items:
+          type: integer
+        type: array
+      mtu:
+        type: integer
+      name:
+        type: string
+      nat_type:
+        type: string
+      nodes:
+        items:
+          type: string
+        type: array
+      os:
+        type: string
+      persistentkeepalive:
+        $ref: '#/definitions/time.Duration'
+      publickey:
+        items:
+          type: integer
+        type: array
+      traffickeypublic:
+        items:
+          type: integer
+        type: array
+      turn_endpoint:
+        $ref: '#/definitions/netip.AddrPort'
+      verbosity:
+        type: integer
+      version:
+        type: string
+      wg_public_listen_port:
+        type: integer
+    type: object
+  models.HostInfoMap:
+    additionalProperties:
+      $ref: '#/definitions/models.HostNetworkInfo'
+    type: object
+  models.HostMqAction:
+    enum:
+    - UPGRADE
+    - SIGNAL_HOST
+    - UPDATE_HOST
+    - DELETE_HOST
+    - JOIN_HOST_TO_NETWORK
+    - ACK
+    - REQ_ACK
+    - CHECK_IN
+    - UPDATE_KEYS
+    - REQ_PULL
+    - SIGNAL_PULL
+    - UPDATE_METRICS
+    type: string
+    x-enum-varnames:
+    - Upgrade
+    - SignalHost
+    - UpdateHost
+    - DeleteHost
+    - JoinHostToNetwork
+    - Acknowledgement
+    - RequestAck
+    - CheckIn
+    - UpdateKeys
+    - RequestPull
+    - SignalPull
+    - UpdateMetrics
+  models.HostNetworkInfo:
+    properties:
+      interfaces:
+        items:
+          $ref: '#/definitions/models.Iface'
+        type: array
+      is_static:
+        type: boolean
+      is_static_port:
+        type: boolean
+      listen_port:
+        type: integer
+    type: object
+  models.HostPull:
+    properties:
+      change_default_gw:
+        type: boolean
+      default_gw_ip:
+        items:
+          type: integer
+        type: array
+      egress_network_routes:
+        items:
+          $ref: '#/definitions/models.EgressNetworkRoutes'
+        type: array
+      endpoint_detection:
+        type: boolean
+      fw_update:
+        $ref: '#/definitions/models.FwUpdate'
+      host:
+        $ref: '#/definitions/models.Host'
+      host_network_info:
+        $ref: '#/definitions/models.HostInfoMap'
+      is_inet_gw:
+        type: boolean
+      nodes:
+        items:
+          $ref: '#/definitions/models.Node'
+        type: array
+      peer_ids:
+        $ref: '#/definitions/models.PeerMap'
+      peers:
+        items:
+          $ref: '#/definitions/wgtypes.PeerConfig'
+        type: array
+      server_config:
+        $ref: '#/definitions/models.ServerConfig'
+    type: object
+  models.HostUpdate:
+    properties:
+      action:
+        $ref: '#/definitions/models.HostMqAction'
+      host:
+        $ref: '#/definitions/models.Host'
+      newMetrics:
+        $ref: '#/definitions/models.Metrics'
+      node:
+        $ref: '#/definitions/models.Node'
+      signal:
+        $ref: '#/definitions/models.Signal'
+    type: object
+  models.IDandAddr:
+    properties:
+      address:
+        type: string
+      host_id:
+        type: string
+      id:
+        type: string
+      is_extclient:
+        type: boolean
+      isserver:
+        type: string
+      listen_port:
+        type: integer
+      name:
+        type: string
+      network:
+        type: string
+    type: object
+  models.Iface:
+    properties:
+      address:
+        $ref: '#/definitions/net.IPNet'
+      addressString:
+        type: string
+      name:
+        type: string
+    type: object
+  models.InetNodeReq:
+    properties:
+      inet_node_client_ids:
+        items:
+          type: string
+        type: array
+    type: object
+  models.IngressGwUsers:
+    properties:
+      network:
+        type: string
+      node_id:
+        type: string
+      users:
+        items:
+          $ref: '#/definitions/models.ReturnUser'
+        type: array
+    type: object
+  models.KeyType:
+    enum:
+    - 0
+    - 1
+    - 2
+    - 3
+    type: integer
+    x-enum-varnames:
+    - Undefined
+    - TimeExpiration
+    - Uses
+    - Unlimited
+  models.Metric:
+    properties:
+      actualuptime:
+        $ref: '#/definitions/time.Duration'
+      connected:
+        type: boolean
+      latency:
+        type: integer
+      node_name:
+        type: string
+      percentup:
+        type: number
+      totalreceived:
+        type: integer
+      totalsent:
+        type: integer
+      totaltime:
+        type: integer
+      uptime:
+        type: integer
+    type: object
+  models.Metrics:
+    properties:
+      connectivity:
+        additionalProperties:
+          $ref: '#/definitions/models.Metric'
+        type: object
+      network:
+        type: string
+      node_id:
+        type: string
+      node_name:
+        type: string
+    type: object
+  models.Network:
+    properties:
+      addressrange:
+        type: string
+      addressrange6:
+        type: string
+      allowmanualsignup:
+        type: string
+      defaultacl:
+        type: string
+      defaultinterface:
+        maxLength: 35
+        minLength: 1
+        type: string
+      defaultkeepalive:
+        maximum: 1000
+        type: integer
+      defaultlistenport:
+        maximum: 65535
+        minimum: 1024
+        type: integer
+      defaultmtu:
+        type: integer
+      defaultpostdown:
+        type: string
+      defaultudpholepunch:
+        type: string
+      isipv4:
+        type: string
+      isipv6:
+        type: string
+      netid:
+        maxLength: 32
+        minLength: 1
+        type: string
+      networklastmodified:
+        type: integer
+      nodelimit:
+        type: integer
+      nodeslastmodified:
+        type: integer
+    required:
+    - netid
+    type: object
+  models.Node:
+    properties:
+      action:
+        type: string
+      additional_rag_ips:
+        items:
+          type: number
+        type: array
+      address:
+        $ref: '#/definitions/net.IPNet'
+      address6:
+        $ref: '#/definitions/net.IPNet'
+      connected:
+        type: boolean
+      defaultacl:
+        description: == PRO ==
+        type: string
+      dnson:
+        type: boolean
+      egressgatewaynatenabled:
+        type: boolean
+      egressgatewayranges:
+        items:
+          type: string
+        type: array
+      egressgatewayrequest:
+        $ref: '#/definitions/models.EgressGatewayRequest'
+      expdatetime:
+        type: string
+      fail_over_peers:
+        additionalProperties:
+          type: object
+        type: object
+      failed_over_by:
+        type: string
+      hostid:
+        type: string
+      id:
+        type: string
+      inet_node_req:
+        $ref: '#/definitions/models.InetNodeReq'
+      ingressdns:
+        type: string
+      ingressgatewayrange:
+        type: string
+      ingressgatewayrange6:
+        type: string
+      internetgw_node_id:
+        type: string
+      is_fail_over:
+        type: boolean
+      isegressgateway:
+        type: boolean
+      isingressgateway:
+        type: boolean
+      isinternetgateway:
+        type: boolean
+      isrelay:
+        type: boolean
+      isrelayed:
+        type: boolean
+      lastcheckin:
+        type: string
+      lastmodified:
+        type: string
+      lastpeerupdate:
+        type: string
+      localaddress:
+        $ref: '#/definitions/net.IPNet'
+      metadata:
+        type: string
+      network:
+        type: string
+      networkrange:
+        type: integer
+      networkrange6:
+        type: number
+      ownerid:
+        type: string
+      pendingdelete:
+        type: boolean
+      relayedby:
+        type: string
+      relaynodes:
+        items:
+          type: string
+        type: array
+      server:
+        type: string
+    type: object
+  models.NodeGet:
+    properties:
+      host:
+        $ref: '#/definitions/models.Host'
+      host_peers:
+        items:
+          $ref: '#/definitions/wgtypes.PeerConfig'
+        type: array
+      node:
+        $ref: '#/definitions/models.Node'
+      peerids:
+        $ref: '#/definitions/models.PeerMap'
+      peers:
+        items:
+          $ref: '#/definitions/wgtypes.PeerConfig'
+        type: array
+      serverconfig:
+        $ref: '#/definitions/models.ServerConfig'
+    type: object
+  models.PeerMap:
+    additionalProperties:
+      $ref: '#/definitions/models.IDandAddr'
+    type: object
+  models.RegisterResponse:
+    properties:
+      requested_host:
+        $ref: '#/definitions/models.Host'
+      server_config:
+        $ref: '#/definitions/models.ServerConfig'
+    type: object
+  models.RelayRequest:
+    properties:
+      netid:
+        type: string
+      nodeid:
+        type: string
+      relayaddrs:
+        items:
+          type: string
+        type: array
+    type: object
+  models.ReturnUser:
+    properties:
+      isadmin:
+        type: boolean
+      issuperadmin:
+        type: boolean
+      last_login_time:
+        type: string
+      remote_gw_ids:
+        additionalProperties:
+          type: object
+        type: object
+      username:
+        type: string
+    type: object
+  models.ServerConfig:
+    properties:
+      Is_EE:
+        type: boolean
+      api:
+        type: string
+      apiport:
+        type: string
+      broker:
+        type: string
+      brokerType:
+        type: string
+      coreDNSAddr:
+        type: string
+      dnsmode:
+        type: string
+      metricInterval:
+        type: string
+      mqpassword:
+        type: string
+      mqport:
+        type: string
+      mquserName:
+        type: string
+      server:
+        type: string
+      trafficKey:
+        items:
+          type: integer
+        type: array
+      version:
+        type: string
+    type: object
+  models.Signal:
+    properties:
+      action:
+        $ref: '#/definitions/models.SignalAction'
+      from_host_id:
+        type: string
+      from_host_pubkey:
+        type: string
+      from_node_id:
+        type: string
+      is_pro:
+        type: boolean
+      reply:
+        type: boolean
+      server:
+        type: string
+      timestamp:
+        type: integer
+      to_host_id:
+        type: string
+      to_host_pubkey:
+        type: string
+      to_node_id:
+        type: string
+    type: object
+  models.SignalAction:
+    enum:
+    - CONNECTION_NEGOTIATION
+    - RELAY_ME
+    type: string
+    x-enum-varnames:
+    - ConnNegotiation
+    - RelayME
+  models.SuccessResponse:
+    properties:
+      code:
+        type: integer
+      message:
+        type: string
+      response: {}
+    type: object
+  models.User:
+    properties:
+      isadmin:
+        type: boolean
+      issuperadmin:
+        type: boolean
+      last_login_time:
+        type: string
+      password:
+        minLength: 5
+        type: string
+      remote_gw_ids:
+        additionalProperties:
+          type: object
+        type: object
+      username:
+        maxLength: 40
+        minLength: 3
+        type: string
+    required:
+    - password
+    type: object
+  models.UserAuthParams:
+    properties:
+      password:
+        type: string
+      username:
+        type: string
+    type: object
+  models.UserRemoteGws:
+    properties:
+      allowed_endpoints:
+        items:
+          type: string
+        type: array
+      connected:
+        type: boolean
+      gw_client:
+        $ref: '#/definitions/models.ExtClient'
+      gw_listen_port:
+        type: integer
+      gw_name:
+        type: string
+      gw_peer_public_key:
+        type: string
+      is_internet_gateway:
+        type: boolean
+      metadata:
+        type: string
+      network:
+        type: string
+      network_addresses:
+        items:
+          type: string
+        type: array
+      remote_access_gw_id:
+        type: string
+    type: object
+  net.IPNet:
+    properties:
+      ip:
+        description: network number
+        items:
+          type: integer
+        type: array
+      mask:
+        description: network mask
+        items:
+          type: integer
+        type: array
+    type: object
+  net.UDPAddr:
+    properties:
+      ip:
+        items:
+          type: integer
+        type: array
+      port:
+        type: integer
+      zone:
+        description: IPv6 scoped addressing zone
+        type: string
+    type: object
+  netip.AddrPort:
+    type: object
+  time.Duration:
+    enum:
+    - -9223372036854775808
+    - 9223372036854775807
+    - 1
+    - 1000
+    - 1000000
+    - 1000000000
+    - 60000000000
+    - 3600000000000
+    - -9223372036854775808
+    - 9223372036854775807
+    - 1
+    - 1000
+    - 1000000
+    - 1000000000
+    - 60000000000
+    - 3600000000000
+    type: integer
+    x-enum-varnames:
+    - minDuration
+    - maxDuration
+    - Nanosecond
+    - Microsecond
+    - Millisecond
+    - Second
+    - Minute
+    - Hour
+    - minDuration
+    - maxDuration
+    - Nanosecond
+    - Microsecond
+    - Millisecond
+    - Second
+    - Minute
+    - Hour
+  wgtypes.PeerConfig:
+    properties:
+      allowedIPs:
+        description: |-
+          AllowedIPs specifies a list of allowed IP addresses in CIDR notation
+          for this peer.
+        items:
+          $ref: '#/definitions/net.IPNet'
+        type: array
+      endpoint:
+        allOf:
+        - $ref: '#/definitions/net.UDPAddr'
+        description: Endpoint specifies the endpoint of this peer entry, if not nil.
+      persistentKeepaliveInterval:
+        allOf:
+        - $ref: '#/definitions/time.Duration'
+        description: |-
+          PersistentKeepaliveInterval specifies the persistent keepalive interval
+          for this peer, if not nil.
+
+          A non-nil value of 0 will clear the persistent keepalive interval.
+      presharedKey:
+        description: |-
+          PresharedKey specifies a peer's preshared key configuration, if not nil.
+
+          A non-nil, zero-value Key will clear the preshared key.
+        items:
+          type: integer
+        type: array
+      publicKey:
+        description: |-
+          PublicKey specifies the public key of this peer.  PublicKey is a
+          mandatory field for all PeerConfigs.
+        items:
+          type: integer
+        type: array
+      remove:
+        description: |-
+          Remove specifies if the peer with this public key should be removed
+          from a device's peer list.
+        type: boolean
+      replaceAllowedIPs:
+        description: |-
+          ReplaceAllowedIPs specifies if the allowed IPs specified in this peer
+          configuration should replace any existing ones, instead of appending them
+          to the allowed IPs list.
+        type: boolean
+      updateOnly:
+        description: |-
+          UpdateOnly specifies that an operation will only occur on this peer
+          if the peer already exists as part of the interface.
+        type: boolean
+    type: object
+host: api.demo.netmaker.io
+info:
+  contact: {}
+  description: NetMaker API Docs
+  title: NetMaker
+  version: 0.24.3
+paths:
+  /api/dns:
+    get:
+      consumes:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.DNSEntry'
+            type: array
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Get all DNS entries
+      tags:
+      - DNS
+  /api/dns/{network}:
+    get:
+      consumes:
+      - application/json
+      parameters:
+      - description: Network identifier
+        in: path
+        name: network
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.DNSEntry'
+            type: array
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Gets node DNS entries associated with a network
+      tags:
+      - DNS
+  /api/dns/{network}/{domain}:
+    delete:
+      consumes:
+      - application/json
+      parameters:
+      - description: Network identifier
+        in: path
+        name: network
+        required: true
+        type: string
+      - description: Domain Name
+        in: path
+        name: domain
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.DNSEntry'
+            type: array
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Delete a DNS entry
+      tags:
+      - DNS
+  /api/dns/adm/{network}:
+    get:
+      consumes:
+      - application/json
+      parameters:
+      - description: Network identifier
+        in: path
+        name: network
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.DNSEntry'
+            type: array
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Get all DNS entries associated with the network
+      tags:
+      - DNS
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Network identifier
+        in: path
+        name: network
+        required: true
+        type: string
+      - description: DNS entry details
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.DNSEntry'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.DNSEntry'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Create a new DNS entry
+      tags:
+      - DNS
+  /api/dns/adm/{network}/custom:
+    get:
+      consumes:
+      - application/json
+      parameters:
+      - description: Network identifier
+        in: path
+        name: network
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.DNSEntry'
+            type: array
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Gets custom DNS entries associated with a network
+      tags:
+      - DNS
+  /api/dns/adm/pushdns:
+    post:
+      consumes:
+      - application/json
+      responses:
+        "200":
+          description: DNS Pushed to CoreDNS
+          schema:
+            type: string
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Push DNS entries to nameserver
+      tags:
+      - DNS
+  /api/emqx/hosts:
+    delete:
+      responses:
+        "200":
+          description: deleted hosts data on emqx
+          schema:
+            type: string
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Deletes all EMQX hosts
+      tags:
+      - Hosts
+  /api/extclients:
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ExtClient'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Fetches All Remote Access Clients across all networks
+      tags:
+      - Remote Access Client
+  /api/extclients/{network}:
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ExtClient'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Get all remote access client associated with network
+      tags:
+      - Remote Access Client
+  /api/extclients/{network}/{clientid}:
+    delete:
+      responses:
+        "200":
+          description: OK
+        "403":
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Delete an individual remote access client
+      tags:
+      - Remote Access Client
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ExtClient'
+        "403":
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Get an individual remote access client
+      tags:
+      - Remote Access Client
+    put:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ExtClient'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "403":
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Update an individual remote access client
+      tags:
+      - Remote Access Client
+  /api/extclients/{network}/{clientid}/{type}:
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ExtClient'
+        "403":
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Get an individual remote access client
+      tags:
+      - Remote Access Client
+  /api/extclients/{network}/{nodeid}:
+    post:
+      responses:
+        "200":
+          description: OK
+          schema:
+            type: string
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "403":
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Create an individual remote access client
+      tags:
+      - Remote Access Client
+  /api/getip:
+    get:
+      responses:
+        "200":
+          description: The public IP address.
+          schema:
+            type: string
+        "400":
+          description: Invalid IP address or no IP found.
+          schema:
+            type: string
+      security:
+      - oauth2: []
+      summary: Get the current public IP address.
+      tags:
+      - IP Service
+  /api/hosts:
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ApiHost'
+            type: array
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: List all hosts
+      tags:
+      - Hosts
+  /api/hosts/{hostid}:
+    delete:
+      parameters:
+      - description: Host ID
+        in: path
+        name: hostid
+        required: true
+        type: string
+      - description: Force delete
+        in: query
+        name: force
+        type: boolean
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ApiHost'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Deletes a Netclient host from Netmaker server
+      tags:
+      - Hosts
+    put:
+      parameters:
+      - description: Host ID
+        in: path
+        name: hostid
+        required: true
+        type: string
+      - description: New host data
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.ApiHost'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ApiHost'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Updates a Netclient host on Netmaker server
+      tags:
+      - Hosts
+  /api/hosts/{hostid}/keys:
+    put:
+      parameters:
+      - description: Host ID
+        in: path
+        name: hostid
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            type: string
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Update keys for a host
+      tags:
+      - Hosts
+  /api/hosts/{hostid}/networks/{network}:
+    delete:
+      parameters:
+      - description: Host ID
+        in: path
+        name: hostid
+        required: true
+        type: string
+      - description: Network name
+        in: path
+        name: network
+        required: true
+        type: string
+      - description: Force delete
+        in: query
+        name: force
+        type: boolean
+      responses:
+        "200":
+          description: OK
+          schema:
+            type: string
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: To Remove Host from Network
+      tags:
+      - Hosts
+    post:
+      parameters:
+      - description: Host ID
+        in: path
+        name: hostid
+        required: true
+        type: string
+      - description: Network name
+        in: path
+        name: network
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            type: string
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: To Add Host To Network
+      tags:
+      - Hosts
+  /api/hosts/{hostid}/sync:
+    post:
+      parameters:
+      - description: Host ID
+        in: path
+        name: hostid
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            type: string
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Requests a host to pull
+      tags:
+      - Hosts
+  /api/hosts/{hostid}/upgrade:
+    put:
+      parameters:
+      - description: Host ID
+        in: path
+        name: hostid
+        required: true
+        type: string
+      responses:
+        "200":
+          description: passed message to upgrade host
+          schema:
+            type: string
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Upgrade a host
+      tags:
+      - Hosts
+  /api/hosts/adm/authenticate:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Authentication parameters
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.AuthParams'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.SuccessResponse'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "401":
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: To Fetch Auth Token for a Host
+      tags:
+      - Auth
+  /api/hosts/keys:
+    put:
+      responses:
+        "200":
+          description: OK
+          schema:
+            type: string
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Update keys for all hosts
+      tags:
+      - Hosts
+  /api/networks:
+    get:
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Network'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Lists all networks
+      tags:
+      - Networks
+    post:
+      parameters:
+      - description: Network details
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.Network'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Network'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Create a network
+      tags:
+      - Networks
+  /api/networks/{networkname}:
+    delete:
+      parameters:
+      - description: Network name
+        in: path
+        name: networkname
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.SuccessResponse'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "403":
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Delete a network
+      tags:
+      - Networks
+    get:
+      parameters:
+      - description: Network name
+        in: path
+        name: networkname
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Network'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Get a network
+      tags:
+      - Networks
+    put:
+      parameters:
+      - description: Network name
+        in: path
+        name: networkname
+        required: true
+        type: string
+      - description: Network details
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.Network'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Network'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Update network settings
+      tags:
+      - Networks
+  /api/networks/{networkname}/acls:
+    get:
+      parameters:
+      - description: Network name
+        in: path
+        name: networkname
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/acls.ACLContainer'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Get a network ACL (Access Control List)
+      tags:
+      - Networks
+    put:
+      parameters:
+      - description: Network name
+        in: path
+        name: networkname
+        required: true
+        type: string
+      - description: ACL container
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/acls.ACLContainer'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/acls.ACLContainer'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Update a network ACL (Access Control List)
+      tags:
+      - Networks
+  /api/networks/{networkname}/acls/v2:
+    put:
+      parameters:
+      - description: Network name
+        in: path
+        name: networkname
+        required: true
+        type: string
+      - description: ACL container
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/acls.ACLContainer'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/acls.ACLContainer'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Update a network ACL (Access Control List)
+      tags:
+      - Networks
+  /api/nodes:
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.ApiNode'
+            type: array
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Get all nodes across all networks
+      tags:
+      - Nodes
+  /api/nodes/{network}/{nodeid}:
+    delete:
+      responses:
+        "200":
+          description: Node deleted.
+          schema:
+            type: string
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Delete an individual node
+      tags:
+      - Nodes
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.NodeGet'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Get an individual node
+      tags:
+      - Nodes
+    put:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ApiNode'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Update an individual node
+      tags:
+      - Nodes
+  /api/nodes/{network}/{nodeid}/creategateway:
+    post:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ApiNode'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Create an egress gateway
+      tags:
+      - Nodes
+  /api/nodes/{network}/{nodeid}/createingress:
+    post:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ApiNode'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Create an remote access gateway
+      tags:
+      - Nodes
+  /api/nodes/{network}/{nodeid}/createrelay:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Network ID
+        in: path
+        name: network
+        required: true
+        type: string
+      - description: Node ID
+        in: path
+        name: nodeid
+        required: true
+        type: string
+      - description: Relay request parameters
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.RelayRequest'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ApiNode'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Create a relay
+      tags:
+      - PRO
+  /api/nodes/{network}/{nodeid}/deletegateway:
+    delete:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ApiNode'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Delete an egress gateway
+      tags:
+      - Nodes
+  /api/nodes/{network}/{nodeid}/deleteingress:
+    delete:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ApiNode'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Delete an remote access gateway
+      tags:
+      - Nodes
+  /api/nodes/{network}/{nodeid}/deleterelay:
+    delete:
+      consumes:
+      - application/json
+      parameters:
+      - description: Network ID
+        in: path
+        name: network
+        required: true
+        type: string
+      - description: Node ID
+        in: path
+        name: nodeid
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ApiNode'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Remove a relay
+      tags:
+      - PRO
+  /api/nodes/{network}/{nodeid}/inet_gw:
+    delete:
+      parameters:
+      - description: Network ID
+        in: path
+        name: network
+        required: true
+        type: string
+      - description: Node ID
+        in: path
+        name: nodeid
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Node'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Delete an internet gateway
+      tags:
+      - PRO
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Network ID
+        in: path
+        name: network
+        required: true
+        type: string
+      - description: Node ID
+        in: path
+        name: nodeid
+        required: true
+        type: string
+      - description: Internet gateway request
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.InetNodeReq'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Node'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Create an internet gateway
+      tags:
+      - PRO
+    put:
+      consumes:
+      - application/json
+      parameters:
+      - description: Network ID
+        in: path
+        name: network
+        required: true
+        type: string
+      - description: Node ID
+        in: path
+        name: nodeid
+        required: true
+        type: string
+      - description: Internet gateway request
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.InetNodeReq'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Node'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Update an internet gateway
+      tags:
+      - PRO
+  /api/nodes/{network}/{nodeid}/ingress/users:
+    get:
+      consumes:
+      - application/json
+      parameters:
+      - description: Ingress Gateway ID
+        in: path
+        name: ingress_id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.IngressGwUsers'
+            type: array
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: List users attached to an ingress gateway
+      tags:
+      - PRO
+  /api/nodes/adm/{network}:
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.Node'
+            type: array
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Gets all nodes associated with network including pending nodes
+      tags:
+      - Nodes
+  /api/server/getconfig:
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/config.ServerConfig'
+      security:
+      - oauth2: []
+      summary: Get the server configuration
+      tags:
+      - Server
+  /api/server/getserverinfo:
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ServerConfig'
+      security:
+      - oauth2: []
+      summary: Get the server information
+      tags:
+      - Server
+  /api/server/status:
+    get:
+      responses: {}
+      security:
+      - oauth2: []
+      summary: Get the server status
+      tags:
+      - Server
+  /api/users:
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.User'
+            type: array
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Get all users
+      tags:
+      - Users
+  /api/users/{username}:
+    delete:
+      parameters:
+      - description: Username of the user to delete
+        in: path
+        name: username
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            type: string
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Delete a user
+      tags:
+      - Users
+    get:
+      parameters:
+      - description: Username of the user to fetch
+        in: path
+        name: username
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.User'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Get an individual user
+      tags:
+      - Users
+    post:
+      parameters:
+      - description: Username of the user to create
+        in: path
+        name: username
+        required: true
+        type: string
+      - description: User details
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.User'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.User'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "403":
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Create a user
+      tags:
+      - Users
+    put:
+      parameters:
+      - description: Username of the user to update
+        in: path
+        name: username
+        required: true
+        type: string
+      - description: User details
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.User'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.User'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "403":
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Update a user
+      tags:
+      - Users
+  /api/users/{username}/remote_access_gw:
+    get:
+      consumes:
+      - application/json
+      parameters:
+      - description: Username
+        in: path
+        name: username
+        required: true
+        type: string
+      - description: Remote Access Client ID
+        in: query
+        name: remote_access_clientid
+        type: string
+      - description: Request from mobile
+        in: query
+        name: from_mobile
+        type: boolean
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.UserRemoteGws'
+            type: array
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Get user's remote access gateways
+      tags:
+      - PRO
+  /api/users/{username}/remote_access_gw/{remote_access_gateway_id}:
+    delete:
+      consumes:
+      - application/json
+      parameters:
+      - description: Username
+        in: path
+        name: username
+        required: true
+        type: string
+      - description: Remote Access Gateway ID
+        in: path
+        name: remote_access_gateway_id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ReturnUser'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Remove user from a remote access gateway
+      tags:
+      - PRO
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Username
+        in: path
+        name: username
+        required: true
+        type: string
+      - description: Remote Access Gateway ID
+        in: path
+        name: remote_access_gateway_id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.ReturnUser'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Attach user to a remote access gateway
+      tags:
+      - PRO
+  /api/users/adm/authenticate:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Authentication parameters
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.UserAuthParams'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.SuccessResponse'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "401":
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Authenticate a user to retrieve an authorization token
+      tags:
+      - Auth
+  /api/users/adm/createsuperadmin:
+    post:
+      parameters:
+      - description: User details
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.User'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.User'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Create a super admin
+      tags:
+      - Users
+  /api/users/adm/hassuperadmin:
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            type: boolean
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Check if the server has a super admin
+      tags:
+      - Users
+  /api/users/adm/transfersuperadmin/{username}:
+    post:
+      parameters:
+      - description: Username of the user to transfer super admin role
+        in: path
+        name: username
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.User'
+        "403":
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Transfer super admin role to another admin user
+      tags:
+      - Users
+  /api/users_pending:
+    delete:
+      responses:
+        "200":
+          description: OK
+          schema:
+            type: string
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Delete all pending users
+      tags:
+      - Users
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.User'
+            type: array
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Get all pending users
+      tags:
+      - Users
+  /api/users_pending/user/{username}:
+    delete:
+      parameters:
+      - description: Username of the pending user to delete
+        in: path
+        name: username
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            type: string
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Delete a pending user
+      tags:
+      - Users
+    post:
+      parameters:
+      - description: Username of the pending user to approve
+        in: path
+        name: username
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            type: string
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Approve a pending user
+      tags:
+      - Users
+  /api/v1/enrollment-keys:
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.EnrollmentKey'
+            type: array
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Lists all EnrollmentKeys for admins
+      tags:
+      - EnrollmentKeys
+    post:
+      parameters:
+      - description: Enrollment Key parameters
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.APIEnrollmentKey'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.EnrollmentKey'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Creates an EnrollmentKey for hosts to register with server and join
+        networks
+      tags:
+      - EnrollmentKeys
+  /api/v1/enrollment-keys/{keyid}:
+    delete:
+      parameters:
+      - description: Enrollment Key ID
+        in: path
+        name: keyid
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Deletes an EnrollmentKey from Netmaker server
+      tags:
+      - EnrollmentKeys
+    put:
+      parameters:
+      - description: Enrollment Key ID
+        in: path
+        name: keyid
+        required: true
+        type: string
+      - description: Enrollment Key parameters
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.APIEnrollmentKey'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.EnrollmentKey'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Updates an EnrollmentKey. Updates are only limited to the relay to
+        use
+      tags:
+      - EnrollmentKeys
+  /api/v1/fallback/host/{hostid}:
+    put:
+      parameters:
+      - description: Host ID
+        in: path
+        name: hostid
+        required: true
+        type: string
+      - description: Host update data
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.HostUpdate'
+      responses:
+        "200":
+          description: updated host data
+          schema:
+            type: string
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Updates a Netclient host on Netmaker server
+      tags:
+      - Hosts
+  /api/v1/host:
+    get:
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.HostPull'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Used by clients for "pull" command
+      tags:
+      - Hosts
+  /api/v1/host/{hostid}/signalpeer:
+    post:
+      parameters:
+      - description: Host ID
+        in: path
+        name: hostid
+        required: true
+        type: string
+      - description: Signal data
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.Signal'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Signal'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Send signal to peer
+      tags:
+      - Hosts
+  /api/v1/host/register/{token}:
+    post:
+      parameters:
+      - description: Enrollment Key Token
+        in: path
+        name: token
+        required: true
+        type: string
+      - description: Host registration parameters
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.Host'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.RegisterResponse'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth: []
+      summary: Handles a Netclient registration with server and add nodes accordingly
+      tags:
+      - EnrollmentKeys
+  /api/v1/legacy/nodes:
+    delete:
+      responses:
+        "200":
+          description: Wiped all legacy nodes.
+          schema:
+            type: string
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      security:
+      - oauth2: []
+      summary: Delete all legacy nodes from DB.
+      tags:
+      - Nodes
+  /api/v1/node/{network}/failover/reset:
+    post:
+      parameters:
+      - description: Network ID
+        in: path
+        name: network
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.SuccessResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Reset failover for a network
+      tags:
+      - PRO
+  /api/v1/node/{nodeid}/failover:
+    delete:
+      parameters:
+      - description: Node ID
+        in: path
+        name: nodeid
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Node'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Delete failover node
+      tags:
+      - PRO
+    get:
+      parameters:
+      - description: Node ID
+        in: path
+        name: nodeid
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Node'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "404":
+          description: Not Found
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Get failover node
+      tags:
+      - PRO
+    post:
+      parameters:
+      - description: Node ID
+        in: path
+        name: nodeid
+        required: true
+        type: string
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Node'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Create failover node
+      tags:
+      - PRO
+  /api/v1/node/{nodeid}/failover_me:
+    post:
+      consumes:
+      - application/json
+      parameters:
+      - description: Node ID
+        in: path
+        name: nodeid
+        required: true
+        type: string
+      - description: Failover request
+        in: body
+        name: body
+        required: true
+        schema:
+          $ref: '#/definitions/models.FailOverMeReq'
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.SuccessResponse'
+        "400":
+          description: Bad Request
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+        "500":
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/models.ErrorResponse'
+      summary: Failover me
+      tags:
+      - PRO
+  /meshclient/files/{filename}:
+    get:
+      responses:
+        "200":
+          description: file
+          schema:
+            type: body
+        "404":
+          description: 404 not found
+          schema:
+            type: string
+      summary: Retrieve a file from the file server
+      tags:
+      - Meshclient
+swagger: "2.0"
+tags:
+- description: |
+    Most actions that can be performed via API can be performed via UI. We recommend managing your networks using the official netmaker-ui project. However, Netmaker can also be run without the UI, and all functions can be achieved via API calls. If your use case requires using Netmaker without the UI or you need to do some troubleshooting/advanced configuration, using the API directly may help.
+  name: APIUsage
+- description: |
+    API calls are primarily authenticated using a user authentication token. This token should be included in the header as follows:
+
+    -H "Authorization: Bearer <YOUR_AUTH_TOKEN>"
+
+    To obtain YOUR_AUTH_TOKEN:
+    Call the api/users/adm/authenticate endpoint (see documentation below for details).
+
+    Note: While a MasterKey exists (configurable via env var or config file), it should be considered a backup option, used only when server access is lost. By default, this key is "secret key," but it's crucial to change this and keep it secure in your instance.
+
+    For more information on configuration and security best practices, refer to the [Netmaker documentation](https://docs.netmaker.org/index.html).
+  name: Authentication
+- description: |
+    Check out our [Pricing](https://www.netmaker.io/pricing). And Feel Free to [Contact Us](https://www.netmaker.io/contact) if you have any questions or need some clarifications.
+  name: Pricing

+ 0 - 3006
swagger.yml

@@ -1,3006 +0,0 @@
-basePath: /
-consumes:
-    - application/json
-definitions:
-    ACL:
-        additionalProperties:
-            format: uint8
-            type: integer
-        description: ACL - the ACL of other nodes in a NetworkACL for a single unique node
-        type: object
-        x-go-package: github.com/gravitl/netmaker/logic/acls
-    ACLContainer:
-        additionalProperties:
-            $ref: '#/definitions/ACL'
-        description: ACLContainer - the total list of all node's ACL in a given network
-        type: object
-        x-go-package: github.com/gravitl/netmaker/logic/acls
-    APIEnrollmentKey:
-        description: APIEnrollmentKey - used to create enrollment keys via API
-        properties:
-            expiration:
-                format: int64
-                type: integer
-                x-go-name: Expiration
-            networks:
-                items:
-                    type: string
-                type: array
-                x-go-name: Networks
-            relay:
-                type: string
-                x-go-name: Relay
-            tags:
-                items:
-                    type: string
-                type: array
-                x-go-name: Tags
-            type:
-                $ref: '#/definitions/KeyType'
-            unlimited:
-                type: boolean
-                x-go-name: Unlimited
-            uses_remaining:
-                format: int64
-                type: integer
-                x-go-name: UsesRemaining
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    ApiHost:
-        description: ApiHost - the host struct for API usage
-        properties:
-            autoupdate:
-                type: boolean
-                x-go-name: AutoUpdate
-            debug:
-                type: boolean
-                x-go-name: Debug
-            defaultinterface:
-                type: string
-                x-go-name: DefaultInterface
-            endpointip:
-                type: string
-                x-go-name: EndpointIP
-            endpointipv6:
-                type: string
-                x-go-name: EndpointIPv6
-            firewallinuse:
-                type: string
-                x-go-name: FirewallInUse
-            id:
-                type: string
-                x-go-name: ID
-            interfaces:
-                items:
-                    $ref: '#/definitions/ApiIface'
-                type: array
-                x-go-name: Interfaces
-            isdefault:
-                type: boolean
-                x-go-name: IsDefault
-            isstatic:
-                type: boolean
-                x-go-name: IsStatic
-            listenport:
-                format: int64
-                type: integer
-                x-go-name: ListenPort
-            macaddress:
-                type: string
-                x-go-name: MacAddress
-            mtu:
-                format: int64
-                type: integer
-                x-go-name: MTU
-            name:
-                type: string
-                x-go-name: Name
-            nat_type:
-                type: string
-                x-go-name: NatType
-            nodes:
-                items:
-                    type: string
-                type: array
-                x-go-name: Nodes
-            os:
-                type: string
-                x-go-name: OS
-            persistentkeepalive:
-                format: int64
-                type: integer
-                x-go-name: PersistentKeepalive
-            publickey:
-                type: string
-                x-go-name: PublicKey
-            verbosity:
-                format: int64
-                type: integer
-                x-go-name: Verbosity
-            version:
-                type: string
-                x-go-name: Version
-            wg_public_listen_port:
-                format: int64
-                type: integer
-                x-go-name: WgPublicListenPort
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    ApiIface:
-        description: |-
-            ApiIface - the interface struct for API usage
-            The original Iface struct contains a net.Address, which does not get marshalled correctly
-        properties:
-            addressString:
-                type: string
-                x-go-name: AddressString
-            name:
-                type: string
-                x-go-name: Name
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    ApiNode:
-        description: ApiNode is a stripped down Node DTO that exposes only required fields to external systems
-        properties:
-            address:
-                type: string
-                x-go-name: Address
-            address6:
-                type: string
-                x-go-name: Address6
-            allowedips:
-                items:
-                    type: string
-                type: array
-                x-go-name: AllowedIPs
-            connected:
-                type: boolean
-                x-go-name: Connected
-            defaultacl:
-                description: == PRO ==
-                type: string
-                x-go-name: DefaultACL
-            dnson:
-                type: boolean
-                x-go-name: DNSOn
-            egressgatewaynatenabled:
-                type: boolean
-                x-go-name: EgressGatewayNatEnabled
-            egressgatewayranges:
-                items:
-                    type: string
-                type: array
-                x-go-name: EgressGatewayRanges
-            expdatetime:
-                format: int64
-                type: integer
-                x-go-name: ExpirationDateTime
-            fail_over_peers:
-                additionalProperties:
-                    type: object
-                type: object
-                x-go-name: FailOverPeers
-            failed_over_by:
-                format: uuid
-                type: string
-                x-go-name: FailedOverBy
-            hostid:
-                type: string
-                x-go-name: HostID
-            id:
-                type: string
-                x-go-name: ID
-            inet_node_req:
-                $ref: '#/definitions/InetNodeReq'
-            ingressdns:
-                type: string
-                x-go-name: IngressDns
-            internetgw_node_id:
-                type: string
-                x-go-name: InternetGwID
-            is_fail_over:
-                type: boolean
-                x-go-name: IsFailOver
-            isegressgateway:
-                type: boolean
-                x-go-name: IsEgressGateway
-            isingressgateway:
-                type: boolean
-                x-go-name: IsIngressGateway
-            isinternetgateway:
-                type: boolean
-                x-go-name: IsInternetGateway
-            isrelay:
-                type: boolean
-                x-go-name: IsRelay
-            isrelayed:
-                type: boolean
-                x-go-name: IsRelayed
-            lastcheckin:
-                format: int64
-                type: integer
-                x-go-name: LastCheckIn
-            lastmodified:
-                format: int64
-                type: integer
-                x-go-name: LastModified
-            lastpeerupdate:
-                format: int64
-                type: integer
-                x-go-name: LastPeerUpdate
-            localaddress:
-                type: string
-                x-go-name: LocalAddress
-            metadata:
-                type: string
-                x-go-name: Metadata
-            network:
-                type: string
-                x-go-name: Network
-            networkrange:
-                type: string
-                x-go-name: NetworkRange
-            networkrange6:
-                type: string
-                x-go-name: NetworkRange6
-            pendingdelete:
-                type: boolean
-                x-go-name: PendingDelete
-            relayedby:
-                type: string
-                x-go-name: RelayedBy
-            relaynodes:
-                items:
-                    type: string
-                type: array
-                x-go-name: RelayedNodes
-            server:
-                type: string
-                x-go-name: Server
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    AuthParams:
-        description: AuthParams - struct for auth params
-        properties:
-            id:
-                type: string
-                x-go-name: ID
-            macaddress:
-                type: string
-                x-go-name: MacAddress
-            password:
-                type: string
-                x-go-name: Password
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    CustomExtClient:
-        description: CustomExtClient - struct for CustomExtClient params
-        properties:
-            clientid:
-                type: string
-                x-go-name: ClientID
-            deniednodeacls:
-                additionalProperties:
-                    type: object
-                type: object
-                x-go-name: DeniedACLs
-            dns:
-                type: string
-                x-go-name: DNS
-            enabled:
-                type: boolean
-                x-go-name: Enabled
-            extraallowedips:
-                items:
-                    type: string
-                type: array
-                x-go-name: ExtraAllowedIPs
-            postdown:
-                type: string
-                x-go-name: PostDown
-            postup:
-                type: string
-                x-go-name: PostUp
-            publickey:
-                type: string
-                x-go-name: PublicKey
-            remote_access_client_id:
-                type: string
-                x-go-name: RemoteAccessClientID
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    DNSEntry:
-        description: DNSEntry - a DNS entry represented as struct
-        properties:
-            address:
-                type: string
-                x-go-name: Address
-            address6:
-                type: string
-                x-go-name: Address6
-            name:
-                type: string
-                x-go-name: Name
-            network:
-                type: string
-                x-go-name: Network
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    Duration:
-        description: |-
-            A Duration represents the elapsed time between two instants
-            as an int64 nanosecond count. The representation limits the
-            largest representable duration to approximately 290 years.
-        format: int64
-        type: integer
-        x-go-package: time
-    EgressGatewayRequest:
-        description: EgressGatewayRequest - egress gateway request
-        properties:
-            natenabled:
-                type: string
-                x-go-name: NatEnabled
-            netid:
-                type: string
-                x-go-name: NetID
-            nodeid:
-                type: string
-                x-go-name: NodeID
-            ranges:
-                items:
-                    type: string
-                type: array
-                x-go-name: Ranges
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    EgressInfo:
-        description: EgressInfo - struct for egress info
-        properties:
-            egress_gateway_cfg:
-                $ref: '#/definitions/EgressGatewayRequest'
-            egress_gw_addr:
-                $ref: '#/definitions/IPNet'
-            egress_id:
-                type: string
-                x-go-name: EgressID
-            network:
-                $ref: '#/definitions/IPNet'
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    EgressNetworkRoutes:
-        description: EgressNetworkRoutes - struct for egress network routes for adding routes to peer's interface
-        properties:
-            egress_ranges:
-                items:
-                    type: string
-                type: array
-                x-go-name: EgressRanges
-            node_addr:
-                $ref: '#/definitions/IPNet'
-            node_addr6:
-                $ref: '#/definitions/IPNet'
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    EnrollmentKey:
-        description: EnrollmentKey - the key used to register hosts and join them to specific networks
-        properties:
-            expiration:
-                format: date-time
-                type: string
-                x-go-name: Expiration
-            networks:
-                items:
-                    type: string
-                type: array
-                x-go-name: Networks
-            relay:
-                format: uuid
-                type: string
-                x-go-name: Relay
-            tags:
-                items:
-                    type: string
-                type: array
-                x-go-name: Tags
-            token:
-                type: string
-                x-go-name: Token
-            type:
-                $ref: '#/definitions/KeyType'
-            unlimited:
-                type: boolean
-                x-go-name: Unlimited
-            uses_remaining:
-                format: int64
-                type: integer
-                x-go-name: UsesRemaining
-            value:
-                type: string
-                x-go-name: Value
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    ExtClient:
-        description: ExtClient - struct for external clients
-        properties:
-            address:
-                type: string
-                x-go-name: Address
-            address6:
-                type: string
-                x-go-name: Address6
-            allowed_ips:
-                items:
-                    type: string
-                type: array
-                x-go-name: AllowedIPs
-            clientid:
-                type: string
-                x-go-name: ClientID
-            deniednodeacls:
-                additionalProperties:
-                    type: object
-                type: object
-                x-go-name: DeniedACLs
-            dns:
-                type: string
-                x-go-name: DNS
-            enabled:
-                type: boolean
-                x-go-name: Enabled
-            extraallowedips:
-                items:
-                    type: string
-                type: array
-                x-go-name: ExtraAllowedIPs
-            ingressgatewayendpoint:
-                type: string
-                x-go-name: IngressGatewayEndpoint
-            ingressgatewayid:
-                type: string
-                x-go-name: IngressGatewayID
-            lastmodified:
-                format: int64
-                type: integer
-                x-go-name: LastModified
-            network:
-                type: string
-                x-go-name: Network
-            ownerid:
-                type: string
-                x-go-name: OwnerID
-            postdown:
-                type: string
-                x-go-name: PostDown
-            postup:
-                type: string
-                x-go-name: PostUp
-            privatekey:
-                type: string
-                x-go-name: PrivateKey
-            publickey:
-                type: string
-                x-go-name: PublicKey
-            remote_access_client_id:
-                type: string
-                x-go-name: RemoteAccessClientID
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    File:
-        title: File represents an open file descriptor.
-        type: object
-        x-go-package: os
-    FwUpdate:
-        description: FwUpdate - struct for firewall updates
-        properties:
-            egress_info:
-                additionalProperties:
-                    $ref: '#/definitions/EgressInfo'
-                type: object
-                x-go-name: EgressInfo
-            is_egress_gw:
-                type: boolean
-                x-go-name: IsEgressGw
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    HardwareAddr:
-        items:
-            format: uint8
-            type: integer
-        title: A HardwareAddr represents a physical hardware address.
-        type: array
-        x-go-package: net
-    Host:
-        description: Host - represents a host on the network
-        properties:
-            autoupdate:
-                type: boolean
-                x-go-name: AutoUpdate
-            daemoninstalled:
-                type: boolean
-                x-go-name: DaemonInstalled
-            debug:
-                type: boolean
-                x-go-name: Debug
-            defaultinterface:
-                type: string
-                x-go-name: DefaultInterface
-            endpointip:
-                type: string
-                x-go-name: EndpointIP
-            endpointipv6:
-                type: string
-                x-go-name: EndpointIPv6
-            firewallinuse:
-                type: string
-                x-go-name: FirewallInUse
-            hostpass:
-                type: string
-                x-go-name: HostPass
-            id:
-                format: uuid
-                type: string
-                x-go-name: ID
-            interface:
-                type: string
-                x-go-name: Interface
-            interfaces:
-                items:
-                    $ref: '#/definitions/Iface'
-                type: array
-                x-go-name: Interfaces
-            ipforwarding:
-                type: boolean
-                x-go-name: IPForwarding
-            isdefault:
-                type: boolean
-                x-go-name: IsDefault
-            isdocker:
-                type: boolean
-                x-go-name: IsDocker
-            isk8s:
-                type: boolean
-                x-go-name: IsK8S
-            isstatic:
-                type: boolean
-                x-go-name: IsStatic
-            listenport:
-                format: int64
-                type: integer
-                x-go-name: ListenPort
-            macaddress:
-                $ref: '#/definitions/HardwareAddr'
-            mtu:
-                format: int64
-                type: integer
-                x-go-name: MTU
-            name:
-                type: string
-                x-go-name: Name
-            nat_type:
-                type: string
-                x-go-name: NatType
-            nodes:
-                items:
-                    type: string
-                type: array
-                x-go-name: Nodes
-            os:
-                type: string
-                x-go-name: OS
-            persistentkeepalive:
-                $ref: '#/definitions/Duration'
-            publickey:
-                $ref: '#/definitions/Key'
-            traffickeypublic:
-                items:
-                    format: uint8
-                    type: integer
-                type: array
-                x-go-name: TrafficKeyPublic
-            turn_endpoint:
-                type: string
-                x-go-name: TurnEndpoint
-            verbosity:
-                format: int64
-                type: integer
-                x-go-name: Verbosity
-            version:
-                type: string
-                x-go-name: Version
-            wg_public_listen_port:
-                format: int64
-                type: integer
-                x-go-name: WgPublicListenPort
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    HostInfoMap:
-        additionalProperties:
-            $ref: '#/definitions/HostNetworkInfo'
-        description: HostInfoMap - map of host public keys to host networking info
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    HostNetworkInfo:
-        description: HostNetworkInfo - holds info related to host networking (used for client side peer calculations)
-        properties:
-            interfaces:
-                items:
-                    $ref: '#/definitions/Iface'
-                type: array
-                x-go-name: Interfaces
-            is_static:
-                type: boolean
-                x-go-name: IsStatic
-            listen_port:
-                format: int64
-                type: integer
-                x-go-name: ListenPort
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    HostPull:
-        description: HostPull - response of a host's pull
-        properties:
-            change_default_gw:
-                type: boolean
-                x-go-name: ChangeDefaultGw
-            default_gw_ip:
-                type: string
-                x-go-name: DefaultGwIp
-            egress_network_routes:
-                items:
-                    $ref: '#/definitions/EgressNetworkRoutes'
-                type: array
-                x-go-name: EgressRoutes
-            endpoint_detection:
-                type: boolean
-                x-go-name: EndpointDetection
-            fw_update:
-                $ref: '#/definitions/FwUpdate'
-            host:
-                $ref: '#/definitions/Host'
-            host_network_info:
-                $ref: '#/definitions/HostInfoMap'
-            is_inet_gw:
-                type: boolean
-                x-go-name: IsInternetGw
-            nodes:
-                items:
-                    $ref: '#/definitions/Node'
-                type: array
-                x-go-name: Nodes
-            peer_ids:
-                $ref: '#/definitions/PeerMap'
-            peers:
-                items:
-                    $ref: '#/definitions/PeerConfig'
-                type: array
-                x-go-name: Peers
-            server_config:
-                $ref: '#/definitions/ServerConfig'
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    IDandAddr:
-        description: IDandAddr - struct to hold ID and primary Address
-        properties:
-            address:
-                type: string
-                x-go-name: Address
-            host_id:
-                type: string
-                x-go-name: HostID
-            id:
-                type: string
-                x-go-name: ID
-            is_extclient:
-                type: boolean
-                x-go-name: IsExtClient
-            isserver:
-                type: string
-                x-go-name: IsServer
-            listen_port:
-                format: int64
-                type: integer
-                x-go-name: ListenPort
-            name:
-                type: string
-                x-go-name: Name
-            network:
-                type: string
-                x-go-name: Network
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    IPMask:
-        description: See type IPNet and func ParseCIDR for details.
-        items:
-            format: uint8
-            type: integer
-        title: |-
-            An IPMask is a bitmask that can be used to manipulate
-            IP addresses for IP addressing and routing.
-        type: array
-        x-go-package: net
-    IPNet:
-        properties:
-            IP:
-                type: string
-            Mask:
-                $ref: '#/definitions/IPMask'
-        title: An IPNet represents an IP network.
-        type: object
-        x-go-package: net
-    Iface:
-        description: Iface struct for local interfaces of a node
-        properties:
-            address:
-                $ref: '#/definitions/IPNet'
-            addressString:
-                type: string
-                x-go-name: AddressString
-            name:
-                type: string
-                x-go-name: Name
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    InetNodeReq:
-        description: InetNodeReq - exit node request struct
-        properties:
-            inet_node_client_ids:
-                items:
-                    type: string
-                type: array
-                x-go-name: InetNodeClientIDs
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    Key:
-        description: |-
-            A Key is a public, private, or pre-shared secret key.  The Key constructor
-            functions in this package can be used to create Keys suitable for each of
-            these applications.
-        items:
-            format: uint8
-            type: integer
-        type: array
-        x-go-package: golang.zx2c4.com/wireguard/wgctrl/wgtypes
-    KeyType:
-        description: KeyType - the type of enrollment key
-        format: int64
-        type: integer
-        x-go-package: github.com/gravitl/netmaker/models
-    LegacyNode:
-        description: LegacyNode - legacy struct for node model
-        properties:
-            accesskey:
-                type: string
-                x-go-name: AccessKey
-            action:
-                type: string
-                x-go-name: Action
-            address:
-                type: string
-                x-go-name: Address
-            address6:
-                type: string
-                x-go-name: Address6
-            allowedips:
-                items:
-                    type: string
-                type: array
-                x-go-name: AllowedIPs
-            connected:
-                type: string
-                x-go-name: Connected
-            defaultacl:
-                description: == PRO ==
-                type: string
-                x-go-name: DefaultACL
-            dnson:
-                type: string
-                x-go-name: DNSOn
-            egressgatewaynatenabled:
-                type: string
-                x-go-name: EgressGatewayNatEnabled
-            egressgatewayranges:
-                items:
-                    type: string
-                type: array
-                x-go-name: EgressGatewayRanges
-            egressgatewayrequest:
-                $ref: '#/definitions/EgressGatewayRequest'
-            endpoint:
-                type: string
-                x-go-name: Endpoint
-            expdatetime:
-                format: int64
-                type: integer
-                x-go-name: ExpirationDateTime
-            failover:
-                type: string
-                x-go-name: Failover
-            failovernode:
-                type: string
-                x-go-name: FailoverNode
-            firewallinuse:
-                type: string
-                x-go-name: FirewallInUse
-            id:
-                type: string
-                x-go-name: ID
-            ingressgatewayrange:
-                type: string
-                x-go-name: IngressGatewayRange
-            ingressgatewayrange6:
-                type: string
-                x-go-name: IngressGatewayRange6
-            interface:
-                type: string
-                x-go-name: Interface
-            interfaces:
-                items:
-                    $ref: '#/definitions/Iface'
-                type: array
-                x-go-name: Interfaces
-            internetgateway:
-                type: string
-                x-go-name: InternetGateway
-            ipforwarding:
-                type: string
-                x-go-name: IPForwarding
-            isdocker:
-                type: string
-                x-go-name: IsDocker
-            isegressgateway:
-                type: string
-                x-go-name: IsEgressGateway
-            ishub:
-                type: string
-                x-go-name: IsHub
-            isingressgateway:
-                type: string
-                x-go-name: IsIngressGateway
-            isk8s:
-                type: string
-                x-go-name: IsK8S
-            ispending:
-                type: string
-                x-go-name: IsPending
-            isrelay:
-                type: string
-                x-go-name: IsRelay
-            isrelayed:
-                type: string
-                x-go-name: IsRelayed
-            isserver:
-                type: string
-                x-go-name: IsServer
-            isstatic:
-                description: IsStatic - refers to if the Endpoint is set manually or dynamically
-                type: string
-                x-go-name: IsStatic
-            lastcheckin:
-                format: int64
-                type: integer
-                x-go-name: LastCheckIn
-            lastmodified:
-                format: int64
-                type: integer
-                x-go-name: LastModified
-            lastpeerupdate:
-                format: int64
-                type: integer
-                x-go-name: LastPeerUpdate
-            listenport:
-                format: int32
-                type: integer
-                x-go-name: ListenPort
-            localaddress:
-                type: string
-                x-go-name: LocalAddress
-            locallistenport:
-                format: int32
-                type: integer
-                x-go-name: LocalListenPort
-            macaddress:
-                type: string
-                x-go-name: MacAddress
-            mtu:
-                format: int32
-                type: integer
-                x-go-name: MTU
-            name:
-                type: string
-                x-go-name: Name
-            network:
-                type: string
-                x-go-name: Network
-            networksettings:
-                $ref: '#/definitions/Network'
-            os:
-                type: string
-                x-go-name: OS
-            ownerid:
-                type: string
-                x-go-name: OwnerID
-            password:
-                type: string
-                x-go-name: Password
-            persistentkeepalive:
-                format: int32
-                type: integer
-                x-go-name: PersistentKeepalive
-            publickey:
-                type: string
-                x-go-name: PublicKey
-            relayaddrs:
-                items:
-                    type: string
-                type: array
-                x-go-name: RelayAddrs
-            server:
-                type: string
-                x-go-name: Server
-            traffickeys:
-                $ref: '#/definitions/TrafficKeys'
-            udpholepunch:
-                type: string
-                x-go-name: UDPHolePunch
-            version:
-                type: string
-                x-go-name: Version
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    Network:
-        description: |-
-            Network Struct - contains info for a given unique network
-            At  some point, need to replace all instances of Name with something else like  Identifier
-        properties:
-            addressrange:
-                type: string
-                x-go-name: AddressRange
-            addressrange6:
-                type: string
-                x-go-name: AddressRange6
-            allowmanualsignup:
-                type: string
-                x-go-name: AllowManualSignUp
-            defaultacl:
-                type: string
-                x-go-name: DefaultACL
-            defaultinterface:
-                type: string
-                x-go-name: DefaultInterface
-            defaultkeepalive:
-                format: int32
-                type: integer
-                x-go-name: DefaultKeepalive
-            defaultlistenport:
-                format: int32
-                type: integer
-                x-go-name: DefaultListenPort
-            defaultmtu:
-                format: int32
-                type: integer
-                x-go-name: DefaultMTU
-            defaultpostdown:
-                type: string
-                x-go-name: DefaultPostDown
-            defaultudpholepunch:
-                type: string
-                x-go-name: DefaultUDPHolePunch
-            isipv4:
-                type: string
-                x-go-name: IsIPv4
-            isipv6:
-                type: string
-                x-go-name: IsIPv6
-            netid:
-                type: string
-                x-go-name: NetID
-            networklastmodified:
-                format: int64
-                type: integer
-                x-go-name: NetworkLastModified
-            nodelimit:
-                format: int32
-                type: integer
-                x-go-name: NodeLimit
-            nodeslastmodified:
-                format: int64
-                type: integer
-                x-go-name: NodesLastModified
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    Node:
-        description: Node - a model of a network node
-        properties:
-            action:
-                type: string
-                x-go-name: Action
-            address:
-                $ref: '#/definitions/IPNet'
-            address6:
-                $ref: '#/definitions/IPNet'
-            connected:
-                type: boolean
-                x-go-name: Connected
-            defaultacl:
-                description: == PRO ==
-                type: string
-                x-go-name: DefaultACL
-            dnson:
-                type: boolean
-                x-go-name: DNSOn
-            egressgatewaynatenabled:
-                type: boolean
-                x-go-name: EgressGatewayNatEnabled
-            egressgatewayranges:
-                items:
-                    type: string
-                type: array
-                x-go-name: EgressGatewayRanges
-            egressgatewayrequest:
-                $ref: '#/definitions/EgressGatewayRequest'
-            expdatetime:
-                format: date-time
-                type: string
-                x-go-name: ExpirationDateTime
-            fail_over_peers:
-                additionalProperties:
-                    type: object
-                type: object
-                x-go-name: FailOverPeers
-            failed_over_by:
-                format: uuid
-                type: string
-                x-go-name: FailedOverBy
-            hostid:
-                format: uuid
-                type: string
-                x-go-name: HostID
-            id:
-                format: uuid
-                type: string
-                x-go-name: ID
-            inet_node_req:
-                $ref: '#/definitions/InetNodeReq'
-            ingressdns:
-                type: string
-                x-go-name: IngressDNS
-            ingressgatewayrange:
-                type: string
-                x-go-name: IngressGatewayRange
-            ingressgatewayrange6:
-                type: string
-                x-go-name: IngressGatewayRange6
-            internetgw_node_id:
-                type: string
-                x-go-name: InternetGwID
-            is_fail_over:
-                type: boolean
-                x-go-name: IsFailOver
-            isegressgateway:
-                type: boolean
-                x-go-name: IsEgressGateway
-            isingressgateway:
-                type: boolean
-                x-go-name: IsIngressGateway
-            isinternetgateway:
-                type: boolean
-                x-go-name: IsInternetGateway
-            isrelay:
-                type: boolean
-                x-go-name: IsRelay
-            isrelayed:
-                type: boolean
-                x-go-name: IsRelayed
-            lastcheckin:
-                format: date-time
-                type: string
-                x-go-name: LastCheckIn
-            lastmodified:
-                format: date-time
-                type: string
-                x-go-name: LastModified
-            lastpeerupdate:
-                format: date-time
-                type: string
-                x-go-name: LastPeerUpdate
-            localaddress:
-                $ref: '#/definitions/IPNet'
-            metadata:
-                type: string
-                x-go-name: Metadata
-            network:
-                type: string
-                x-go-name: Network
-            networkrange:
-                $ref: '#/definitions/IPNet'
-            networkrange6:
-                $ref: '#/definitions/IPNet'
-            ownerid:
-                type: string
-                x-go-name: OwnerID
-            pendingdelete:
-                type: boolean
-                x-go-name: PendingDelete
-            relayedby:
-                type: string
-                x-go-name: RelayedBy
-            relaynodes:
-                items:
-                    type: string
-                type: array
-                x-go-name: RelayedNodes
-            server:
-                type: string
-                x-go-name: Server
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    PeerConfig:
-        description: |-
-            Because the zero value of some Go types may be significant to WireGuard for
-            PeerConfig fields, pointer types are used for some of these fields. Only
-            pointer fields which are not nil will be applied when configuring a peer.
-        properties:
-            AllowedIPs:
-                description: |-
-                    AllowedIPs specifies a list of allowed IP addresses in CIDR notation
-                    for this peer.
-                items:
-                    $ref: '#/definitions/IPNet'
-                type: array
-            Endpoint:
-                $ref: '#/definitions/UDPAddr'
-            PersistentKeepaliveInterval:
-                $ref: '#/definitions/Duration'
-            PresharedKey:
-                $ref: '#/definitions/Key'
-            PublicKey:
-                $ref: '#/definitions/Key'
-            Remove:
-                description: |-
-                    Remove specifies if the peer with this public key should be removed
-                    from a device's peer list.
-                type: boolean
-            ReplaceAllowedIPs:
-                description: |-
-                    ReplaceAllowedIPs specifies if the allowed IPs specified in this peer
-                    configuration should replace any existing ones, instead of appending them
-                    to the allowed IPs list.
-                type: boolean
-            UpdateOnly:
-                description: |-
-                    UpdateOnly specifies that an operation will only occur on this peer
-                    if the peer already exists as part of the interface.
-                type: boolean
-        title: A PeerConfig is a WireGuard device peer configuration.
-        type: object
-        x-go-package: golang.zx2c4.com/wireguard/wgctrl/wgtypes
-    PeerMap:
-        additionalProperties:
-            $ref: '#/definitions/IDandAddr'
-        description: PeerMap - peer map for ids and addresses in metrics
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    RegisterResponse:
-        description: RegisterResponse - the response to a successful enrollment register
-        properties:
-            requested_host:
-                $ref: '#/definitions/Host'
-            server_config:
-                $ref: '#/definitions/ServerConfig'
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    RelayRequest:
-        description: RelayRequest - relay request struct
-        properties:
-            netid:
-                type: string
-                x-go-name: NetID
-            nodeid:
-                type: string
-                x-go-name: NodeID
-            relayaddrs:
-                items:
-                    type: string
-                type: array
-                x-go-name: RelayedNodes
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    ServerConfig:
-        description: ServerConfig - server conf struct
-        properties:
-            APIConnString:
-                type: string
-            APIHost:
-                type: string
-            APIPort:
-                type: string
-            AllowedEmailDomains:
-                type: string
-            AllowedOrigin:
-                type: string
-            AuthProvider:
-                type: string
-            AzureTenant:
-                type: string
-            BasicAuth:
-                type: string
-            Broker:
-                type: string
-            BrokerType:
-                type: string
-            CacheEnabled:
-                type: string
-            ClientID:
-                type: string
-            ClientSecret:
-                type: string
-            CoreDNSAddr:
-                type: string
-            DNSKey:
-                type: string
-            DNSMode:
-                type: string
-            Database:
-                type: string
-            DeployedByOperator:
-                type: boolean
-            DisableRemoteIPCheck:
-                type: string
-            DisplayKeys:
-                type: string
-            EgressesLimit:
-                format: int64
-                type: integer
-            EmqxRestEndpoint:
-                type: string
-            Environment:
-                type: string
-            FrontendURL:
-                type: string
-            HostNetwork:
-                type: string
-            IngressesLimit:
-                format: int64
-                type: integer
-            IsEE:
-                type: string
-                x-go-name: IsPro
-            JwtValidityDuration:
-                $ref: '#/definitions/Duration'
-            LicenseValue:
-                type: string
-            MQPassword:
-                type: string
-            MQUserName:
-                type: string
-            MachinesLimit:
-                format: int64
-                type: integer
-            MasterKey:
-                type: string
-            MessageQueueBackend:
-                type: string
-            MetricsExporter:
-                type: string
-            NetclientAutoUpdate:
-                type: string
-            NetclientEndpointDetection:
-                type: string
-            NetmakerTenantID:
-                type: string
-            NetworksLimit:
-                format: int64
-                type: integer
-            NodeID:
-                type: string
-            OIDCIssuer:
-                type: string
-            Platform:
-                type: string
-            PublicIPService:
-                type: string
-            RacAutoDisable:
-                type: boolean
-            RestBackend:
-                type: string
-            SQLConn:
-                type: string
-            Server:
-                type: string
-            ServerBrokerEndpoint:
-                type: string
-            StunList:
-                type: string
-            StunPort:
-                format: int64
-                type: integer
-            Telemetry:
-                type: string
-            TurnApiServer:
-                type: string
-            TurnPassword:
-                type: string
-            TurnPort:
-                format: int64
-                type: integer
-            TurnServer:
-                type: string
-            TurnUserName:
-                type: string
-            UseTurn:
-                type: boolean
-            UsersLimit:
-                format: int64
-                type: integer
-            Verbosity:
-                format: int32
-                type: integer
-            Version:
-                type: string
-            endpoint_detection:
-                type: boolean
-                x-go-name: EndpointDetection
-        type: object
-        x-go-package: github.com/gravitl/netmaker/config
-    Signal:
-        description: Signal - struct for signalling peer
-        properties:
-            action:
-                $ref: '#/definitions/SignalAction'
-            from_host_id:
-                type: string
-                x-go-name: FromHostID
-            from_host_pubkey:
-                type: string
-                x-go-name: FromHostPubKey
-            from_node_id:
-                type: string
-                x-go-name: FromNodeID
-            is_pro:
-                type: boolean
-                x-go-name: IsPro
-            reply:
-                type: boolean
-                x-go-name: Reply
-            server:
-                type: string
-                x-go-name: Server
-            timestamp:
-                format: int64
-                type: integer
-                x-go-name: TimeStamp
-            to_host_id:
-                type: string
-                x-go-name: ToHostID
-            to_host_pubkey:
-                type: string
-                x-go-name: ToHostPubKey
-            to_node_id:
-                type: string
-                x-go-name: ToNodeID
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    SignalAction:
-        description: SignalAction - turn peer signal action
-        type: string
-        x-go-package: github.com/gravitl/netmaker/models
-    SuccessResponse:
-        properties:
-            Code:
-                format: int64
-                type: integer
-            Message:
-                type: string
-            Response: {}
-        title: SuccessResponse is struct for sending error message with code.
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    TrafficKeys:
-        description: TrafficKeys - struct to hold public keys
-        properties:
-            mine:
-                items:
-                    format: uint8
-                    type: integer
-                type: array
-                x-go-name: Mine
-            server:
-                items:
-                    format: uint8
-                    type: integer
-                type: array
-                x-go-name: Server
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    UDPAddr:
-        properties:
-            IP:
-                type: string
-            Port:
-                format: int64
-                type: integer
-            Zone:
-                type: string
-        title: UDPAddr represents the address of a UDP end point.
-        type: object
-        x-go-package: net
-    User:
-        description: User struct - struct for Users
-        properties:
-            isadmin:
-                type: boolean
-                x-go-name: IsAdmin
-            issuperadmin:
-                type: boolean
-                x-go-name: IsSuperAdmin
-            last_login_time:
-                format: date-time
-                type: string
-                x-go-name: LastLoginTime
-            password:
-                type: string
-                x-go-name: Password
-            remote_gw_ids:
-                additionalProperties:
-                    type: object
-                type: object
-                x-go-name: RemoteGwIDs
-            username:
-                type: string
-                x-go-name: UserName
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-    UserAuthParams:
-        description: UserAuthParams - user auth params struct
-        properties:
-            password:
-                type: string
-                x-go-name: Password
-            username:
-                type: string
-                x-go-name: UserName
-        type: object
-        x-go-package: github.com/gravitl/netmaker/models
-host: api.demo.netmaker.io
-info:
-    description: |-
-        # API Usage
-
-        Most actions that can be performed via API can be performed via UI. We recommend managing your networks using the official netmaker-ui project. However, Netmaker can also be run without the UI, and all functions can be achieved via API calls. If your use case requires using Netmaker without the UI or you need to do some troubleshooting/advanced configuration, using the API directly may help.
-
-        # Authentication
-
-        API calls must be authenticated via a header of the format -H “Authorization: Bearer <YOUR_SECRET_KEY>” There are two methods to obtain YOUR_SECRET_KEY: 1. Using the masterkey. By default, this value is “secret key,” but you should change this on your instance and keep it secure. This value can be set via env var at startup or in a config file (config/environments/< env >.yaml). See the [Netmaker](https://docs.netmaker.org/index.html) documentation for more details. 2. Using a JWT received for a node. This can be retrieved by calling the /api/nodes/<network>/authenticate endpoint, as documented below.
-    title: Netmaker
-    version: 0.24.3
-paths:
-    /api/dns:
-        get:
-            operationId: getAllDNS
-            responses:
-                "200":
-                    $ref: '#/responses/dnsResponse'
-            schemes:
-                - https
-            summary: Gets all DNS entries.
-            tags:
-                - dns
-    /api/dns/{network}:
-        post:
-            operationId: createDNS
-            parameters:
-                - description: Network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - description: DNS Entry
-                  in: body
-                  name: body
-                  schema:
-                    items:
-                        $ref: '#/definitions/DNSEntry'
-                    type: array
-                  x-go-name: Body
-            responses:
-                "200":
-                    $ref: '#/responses/dnsResponse'
-            schemes:
-                - https
-            summary: Create a DNS entry.
-            tags:
-                - dns
-    /api/dns/{network}/{domain}:
-        delete:
-            operationId: deleteDNS
-            parameters:
-                - description: Network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - description: Domain
-                  in: path
-                  name: domain
-                  required: true
-                  type: string
-                  x-go-name: Domain
-            responses:
-                "200":
-                    $ref: '#/responses/stringJSONResponse'
-            schemes:
-                - https
-            summary: Delete a DNS entry.
-            tags:
-                - dns
-    /api/dns/adm/{network}:
-        get:
-            operationId: getDNS
-            parameters:
-                - description: Network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-            responses:
-                "200":
-                    $ref: '#/responses/dnsResponse'
-            schemes:
-                - https
-            summary: Gets all DNS entries associated with the network.
-            tags:
-                - dns
-    /api/dns/adm/{network}/custom:
-        get:
-            operationId: getCustomDNS
-            parameters:
-                - description: Network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-            responses:
-                "200":
-                    $ref: '#/responses/dnsResponse'
-            schemes:
-                - https
-            summary: Gets custom DNS entries associated with a network.
-            tags:
-                - dns
-    /api/dns/adm/{network}/nodes:
-        get:
-            operationId: getNodeDNS
-            parameters:
-                - description: Network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-            responses:
-                "200":
-                    $ref: '#/responses/dnsResponse'
-            schemes:
-                - https
-            summary: Gets node DNS entries associated with a network.
-            tags:
-                - dns
-    /api/dns/adm/pushdns:
-        post:
-            operationId: pushDNS
-            responses:
-                "200":
-                    $ref: '#/responses/dnsResponse'
-            schemes:
-                - https
-            summary: Push DNS entries to nameserver.
-            tags:
-                - dns
-    /api/emqx/hosts:
-        delete:
-            operationId: delEmqxHosts
-            responses:
-                "200":
-                    $ref: '#/responses/apiHostResponse'
-            schemes:
-                - https
-            summary: Lists all hosts.
-            tags:
-                - hosts
-    /api/extclients:
-        get:
-            operationId: getAllExtClients
-            parameters:
-                - description: Networks
-                  in: body
-                  name: networks
-                  schema:
-                    items:
-                        type: string
-                    type: array
-                  x-go-name: Networks
-            responses:
-                "200":
-                    $ref: '#/responses/extClientSliceResponse'
-            schemes:
-                - https
-            summary: A separate function to get all extclients, not just extclients for a particular network.
-            tags:
-                - ext_client
-    /api/extclients/{network}:
-        get:
-            description: Gets all extclients associated with network, including pending extclients.
-            operationId: getNetworkExtClients
-            parameters:
-                - description: Network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-            responses:
-                "200":
-                    $ref: '#/responses/extClientSliceResponse'
-            schemes:
-                - https
-            summary: Get all extclients associated with network.
-            tags:
-                - ext_client
-    /api/extclients/{network}/{clientid}:
-        delete:
-            operationId: deleteExtClient
-            parameters:
-                - description: Client ID
-                  in: path
-                  name: clientid
-                  required: true
-                  type: string
-                  x-go-name: ClientID
-                - description: Network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-            responses:
-                "200":
-                    $ref: '#/responses/successResponse'
-            schemes:
-                - https
-            summary: Delete an individual extclient.
-            tags:
-                - ext_client
-        get:
-            operationId: getExtClient
-            parameters:
-                - description: Client ID
-                  in: path
-                  name: clientid
-                  required: true
-                  type: string
-                  x-go-name: ClientID
-                - description: Network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-            responses:
-                "200":
-                    $ref: '#/responses/extClientResponse'
-            schemes:
-                - https
-            summary: Get an individual extclient.
-            tags:
-                - ext_client
-        put:
-            operationId: updateExtClient
-            parameters:
-                - description: Client ID
-                  in: path
-                  name: clientid
-                  required: true
-                  type: string
-                  x-go-name: ClientID
-                - description: Network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - description: ExtClient
-                  in: body
-                  name: ext_client
-                  schema:
-                    $ref: '#/definitions/ExtClient'
-                  x-go-name: ExtClient
-            responses:
-                "200":
-                    $ref: '#/responses/extClientResponse'
-            schemes:
-                - https
-            summary: Update an individual extclient.
-            tags:
-                - ext_client
-    /api/extclients/{network}/{clientid}/{type}:
-        get:
-            operationId: getExtClientConf
-            parameters:
-                - description: Type
-                  in: path
-                  name: type
-                  required: true
-                  type: string
-                  x-go-name: Type
-                - description: Client ID
-                  in: path
-                  name: clientid
-                  required: true
-                  type: string
-                  x-go-name: ClientID
-                - description: Network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-            responses:
-                "200":
-                    $ref: '#/responses/extClientResponse'
-            schemes:
-                - https
-            summary: Get an individual extclient.
-            tags:
-                - ext_client
-    /api/extclients/{network}/{nodeid}:
-        post:
-            operationId: createExtClient
-            parameters:
-                - description: Network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - description: Node ID
-                  in: path
-                  name: nodeid
-                  required: true
-                  type: string
-                  x-go-name: NodeID
-                - description: Custom ExtClient
-                  in: body
-                  name: custom_ext_client
-                  schema:
-                    $ref: '#/definitions/CustomExtClient'
-                  x-go-name: CustomExtClient
-            responses:
-                "200":
-                    $ref: '#/responses/okResponse'
-            schemes:
-                - https
-            summary: Create an individual extclient.  Must have valid key and be unique.
-            tags:
-                - ext_client
-    /api/getip:
-        get:
-            operationId: getPublicIP
-            responses:
-                "200":
-                    $ref: '#/responses/byteArrayResponse'
-            schemes:
-                - https
-            summary: Get the current public IP address.
-            tags:
-                - ipservice
-    /api/hosts:
-        get:
-            operationId: getHosts
-            responses:
-                "200":
-                    $ref: '#/responses/apiHostSliceResponse'
-            schemes:
-                - https
-            summary: Lists all hosts.
-            tags:
-                - hosts
-    /api/hosts/{hostid}:
-        delete:
-            operationId: deleteHost
-            parameters:
-                - description: HostID
-                  in: path
-                  name: hostid
-                  required: true
-                  type: string
-                  x-go-name: HostID
-            responses:
-                "200":
-                    $ref: '#/responses/apiHostResponse'
-            schemes:
-                - https
-            summary: Deletes a Netclient host from Netmaker server.
-            tags:
-                - hosts
-        put:
-            operationId: updateHost
-            parameters:
-                - description: HostID
-                  in: path
-                  name: hostid
-                  required: true
-                  type: string
-                  x-go-name: HostID
-            responses:
-                "200":
-                    $ref: '#/responses/apiHostResponse'
-            schemes:
-                - https
-            summary: Updates a Netclient host on Netmaker server.
-            tags:
-                - hosts
-    /api/hosts/{hostid}/networks/{network}:
-        delete:
-            operationId: deleteHostFromNetwork
-            parameters:
-                - description: hostid to add or delete from network
-                  in: path
-                  name: hostid
-                  required: true
-                  type: string
-                  x-go-name: HostID
-                - description: network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-            responses:
-                "200":
-                    $ref: '#/responses/okResponse'
-            schemes:
-                - https
-            summary: Given a network, a host is removed from the network.
-            tags:
-                - hosts
-        post:
-            operationId: addHostToNetwork
-            parameters:
-                - description: hostid to add or delete from network
-                  in: path
-                  name: hostid
-                  required: true
-                  type: string
-                  x-go-name: HostID
-                - description: network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-            responses:
-                "200":
-                    $ref: '#/responses/okResponse'
-            schemes:
-                - https
-            summary: Given a network, a host is added to the network.
-            tags:
-                - hosts
-    /api/hosts/{hostid}/signalpeer:
-        post:
-            operationId: signalPeer
-            parameters:
-                - description: HostID
-                  in: path
-                  name: hostid
-                  required: true
-                  type: string
-                  x-go-name: HostID
-            responses:
-                "200":
-                    $ref: '#/responses/signal'
-            schemes:
-                - https
-            summary: send signal to peer.
-            tags:
-                - hosts
-    /api/hosts/{hostid}/sync:
-        post:
-            operationId: synchost
-            parameters:
-                - description: HostID
-                  in: path
-                  name: hostid
-                  required: true
-                  type: string
-                  x-go-name: HostID
-            responses:
-                "200":
-                    $ref: '#/responses/networkBodyResponse'
-            schemes:
-                - https
-            summary: Requests a host to pull.
-            tags:
-                - hosts
-    /api/hosts/{hostid}keys:
-        post:
-            operationId: updateKeys
-            parameters:
-                - description: HostID
-                  in: path
-                  name: hostid
-                  required: true
-                  type: string
-                  x-go-name: HostID
-            responses:
-                "200":
-                    $ref: '#/responses/networkBodyResponse'
-            schemes:
-                - https
-            summary: Update keys for a network.
-            tags:
-                - hosts
-    /api/hosts/adm/authenticate:
-        post:
-            operationId: authenticateHost
-            responses:
-                "200":
-                    $ref: '#/responses/successResponse'
-            schemes:
-                - https
-            summary: Host based authentication for making further API calls.
-            tags:
-                - authenticate
-    /api/hosts/keys:
-        post:
-            operationId: updateAllKeys
-            responses:
-                "200":
-                    $ref: '#/responses/networkBodyResponse'
-            schemes:
-                - https
-            summary: Update keys for a network.
-            tags:
-                - hosts
-    /api/networks:
-        get:
-            operationId: getNetworks
-            responses:
-                "200":
-                    $ref: '#/responses/getNetworksSliceResponse'
-            schemes:
-                - https
-            summary: Lists all networks.
-            tags:
-                - networks
-        post:
-            operationId: createNetwork
-            parameters:
-                - description: Network
-                  in: body
-                  name: network
-                  schema:
-                    $ref: '#/definitions/Network'
-                  x-go-name: Network
-            responses:
-                "200":
-                    $ref: '#/responses/networkBodyResponse'
-            schemes:
-                - https
-            summary: Create a network.
-            tags:
-                - networks
-    /api/networks/{networkname}:
-        delete:
-            operationId: deleteNetwork
-            parameters:
-                - description: 'name: network name'
-                  in: path
-                  name: networkname
-                  required: true
-                  type: string
-                  x-go-name: Networkname
-            responses:
-                "200":
-                    $ref: '#/responses/successResponse'
-            schemes:
-                - https
-            summary: Delete a network.  Will not delete if there are any nodes that belong to the network.
-            tags:
-                - networks
-        get:
-            operationId: getNetwork
-            parameters:
-                - description: 'name: network name'
-                  in: path
-                  name: networkname
-                  required: true
-                  type: string
-                  x-go-name: Networkname
-            responses:
-                "200":
-                    $ref: '#/responses/networkBodyResponse'
-            schemes:
-                - https
-            summary: Get a network.
-            tags:
-                - networks
-        put:
-            operationId: updateNetwork
-            parameters:
-                - description: 'name: network name'
-                  in: path
-                  name: networkname
-                  required: true
-                  type: string
-                  x-go-name: Networkname
-                - description: Network
-                  in: body
-                  name: network
-                  schema:
-                    $ref: '#/definitions/Network'
-                  x-go-name: Network
-            responses:
-                "200":
-                    $ref: '#/responses/networkBodyResponse'
-            schemes:
-                - https
-            summary: Update pro settings for a network.
-            tags:
-                - networks
-    /api/networks/{networkname}/acls:
-        get:
-            operationId: getNetworkACL
-            parameters:
-                - description: 'name: network name'
-                  in: path
-                  name: networkname
-                  required: true
-                  type: string
-                  x-go-name: Networkname
-            responses:
-                "200":
-                    $ref: '#/responses/aclContainerResponse'
-            schemes:
-                - https
-            summary: Get a network ACL (Access Control List).
-            tags:
-                - networks
-        put:
-            operationId: updateNetworkACL
-            parameters:
-                - description: 'name: network name'
-                  in: path
-                  name: networkname
-                  required: true
-                  type: string
-                  x-go-name: Networkname
-                - description: ACL Container
-                  in: body
-                  name: acl_container
-                  schema:
-                    $ref: '#/definitions/ACLContainer'
-                  x-go-name: ACLContainer
-            responses:
-                "200":
-                    $ref: '#/responses/aclContainerResponse'
-            schemes:
-                - https
-            summary: Update a network ACL (Access Control List).
-            tags:
-                - networks
-    /api/networks/{networkname}/acls/v2:
-        put:
-            operationId: updateNetworkACL
-            parameters:
-                - description: 'name: network name'
-                  in: path
-                  name: networkname
-                  required: true
-                  type: string
-                  x-go-name: Networkname
-                - description: ACL Container
-                  in: body
-                  name: acl_container
-                  schema:
-                    $ref: '#/definitions/ACLContainer'
-                  x-go-name: ACLContainer
-            responses:
-                "200":
-                    $ref: '#/responses/aclContainerResponse'
-            schemes:
-                - https
-            summary: Update a network ACL (Access Control List).
-            tags:
-                - networks
-    /api/node/{nodeid}/failOverME:
-        post:
-            operationId: failOver_me
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Create a relay.
-            tags:
-                - node
-    /api/nodes:
-        get:
-            operationId: getAllNodes
-            responses:
-                "200":
-                    $ref: '#/responses/nodeSliceResponse'
-            schemes:
-                - https
-            summary: Get all nodes across all networks.
-            tags:
-                - nodes
-    /api/nodes/{network}:
-        get:
-            operationId: getNetworkNodes
-            parameters:
-                - description: Network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-            responses:
-                "200":
-                    $ref: '#/responses/nodeSliceResponse'
-            schemes:
-                - https
-            summary: Gets all nodes associated with network including pending nodes.
-            tags:
-                - nodes
-    /api/nodes/{network}/{nodeid}:
-        delete:
-            operationId: deleteNode
-            parameters:
-                - in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - in: path
-                  name: nodeid
-                  required: true
-                  type: string
-                  x-go-name: NodeID
-                - description: Node
-                  in: body
-                  name: node
-                  schema:
-                    $ref: '#/definitions/LegacyNode'
-                  x-go-name: Node
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Delete an individual node.
-            tags:
-                - nodes
-        get:
-            operationId: getNode
-            parameters:
-                - in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - in: path
-                  name: nodeid
-                  required: true
-                  type: string
-                  x-go-name: NodeID
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Get an individual node.
-            tags:
-                - nodes
-        put:
-            operationId: updateNode
-            parameters:
-                - in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - in: path
-                  name: nodeid
-                  required: true
-                  type: string
-                  x-go-name: NodeID
-                - description: Node
-                  in: body
-                  name: node
-                  schema:
-                    $ref: '#/definitions/LegacyNode'
-                  x-go-name: Node
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Update an individual node.
-            tags:
-                - nodes
-    /api/nodes/{network}/{nodeid}/creategateway:
-        post:
-            operationId: createEgressGateway
-            parameters:
-                - in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - in: path
-                  name: nodeid
-                  required: true
-                  type: string
-                  x-go-name: NodeID
-                - description: Egress Gateway Request
-                  in: body
-                  name: egress_gateway_request
-                  schema:
-                    $ref: '#/definitions/EgressGatewayRequest'
-                  x-go-name: EgressGatewayRequest
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Create an egress gateway.
-            tags:
-                - nodes
-    /api/nodes/{network}/{nodeid}/createingress:
-        post:
-            operationId: createIngressGateway
-            parameters:
-                - in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - in: path
-                  name: nodeid
-                  required: true
-                  type: string
-                  x-go-name: NodeID
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Create an ingress gateway.
-            tags:
-                - nodes
-    /api/nodes/{network}/{nodeid}/createrelay:
-        post:
-            operationId: createRelay
-            parameters:
-                - in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - in: path
-                  name: nodeid
-                  required: true
-                  type: string
-                  x-go-name: NodeID
-                - description: Relay Request
-                  in: body
-                  name: relay_request
-                  schema:
-                    $ref: '#/definitions/RelayRequest'
-                  x-go-name: RelayRequest
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Create a relay.
-            tags:
-                - nodes
-    /api/nodes/{network}/{nodeid}/deletegateway:
-        delete:
-            operationId: deleteEgressGateway
-            parameters:
-                - in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - in: path
-                  name: nodeid
-                  required: true
-                  type: string
-                  x-go-name: NodeID
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Delete an egress gateway.
-            tags:
-                - nodes
-    /api/nodes/{network}/{nodeid}/deleteingress:
-        delete:
-            operationId: deleteIngressGateway
-            parameters:
-                - in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - in: path
-                  name: nodeid
-                  required: true
-                  type: string
-                  x-go-name: NodeID
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Delete an ingress gateway.
-            tags:
-                - nodes
-    /api/nodes/{network}/{nodeid}/deleterelay:
-        delete:
-            operationId: deleteRelay
-            parameters:
-                - in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - in: path
-                  name: nodeid
-                  required: true
-                  type: string
-                  x-go-name: NodeID
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Remove a relay.
-            tags:
-                - nodes
-    /api/nodes/{network}/{nodeid}/inet_gw:
-        delete:
-            operationId: deleteInternetGw
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Delete an internet gw.
-            tags:
-                - nodes
-        post:
-            operationId: createInternetGw
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Create an inet node.
-            tags:
-                - nodes
-        put:
-            operationId: updateInternetGw
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: update an inet node.
-            tags:
-                - nodes
-    /api/nodes/{network}/{nodeid}/ingress/users:
-        get:
-            operationId: ingressGatewayUsers
-            parameters:
-                - in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - in: path
-                  name: nodeid
-                  required: true
-                  type: string
-                  x-go-name: NodeID
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Lists all the users attached to an ingress gateway.
-            tags:
-                - users
-    /api/nodes/adm/{network}/authenticate:
-        post:
-            operationId: authenticate
-            parameters:
-                - description: network
-                  in: path
-                  name: network
-                  required: true
-                  type: string
-                  x-go-name: Network
-                - description: AuthParams
-                  in: body
-                  name: auth_params
-                  schema:
-                    $ref: '#/definitions/AuthParams'
-                  x-go-name: AuthParams
-            responses:
-                "200":
-                    $ref: '#/responses/successResponse'
-            schemes:
-                - https
-            summary: Authenticate to make further API calls related to a network.
-            tags:
-                - authenticate
-    /api/oauth/login:
-        get:
-            operationId: HandleAuthLogin
-            responses:
-                "200":
-                    $ref: '#/responses/okResponse'
-            schemes:
-                - https
-            summary: Handles OAuth login.
-            tags:
-                - nodes
-    /api/server/getconfig:
-        get:
-            operationId: getConfig
-            responses:
-                "200":
-                    $ref: '#/responses/serverConfigResponse'
-            schemes:
-                - https
-            summary: Get the server configuration.
-            tags:
-                - server
-    /api/server/getserverinfo:
-        get:
-            operationId: getServerInfo
-            responses:
-                "200":
-                    $ref: '#/responses/serverConfigResponse'
-            schemes:
-                - https
-            summary: Get the server configuration.
-            tags:
-                - server
-    /api/server/status:
-        get:
-            operationId: getStatus
-            responses:
-                "200":
-                    $ref: '#/responses/serverConfigResponse'
-            schemes:
-                - https
-            summary: Get the server configuration.
-            tags:
-                - server
-    /api/users:
-        get:
-            operationId: getUsers
-            responses:
-                "200":
-                    $ref: '#/responses/userBodyResponse'
-            schemes:
-                - https
-            summary: Get all users.
-            tags:
-                - user
-    /api/users/{username}:
-        delete:
-            operationId: deleteUser
-            parameters:
-                - description: Username
-                  in: path
-                  name: username
-                  required: true
-                  type: string
-                  x-go-name: Username
-            responses:
-                "200":
-                    $ref: '#/responses/userBodyResponse'
-            schemes:
-                - https
-            summary: Delete a user.
-            tags:
-                - user
-        get:
-            operationId: getUser
-            parameters:
-                - description: Username
-                  in: path
-                  name: username
-                  required: true
-                  type: string
-                  x-go-name: Username
-            responses:
-                "200":
-                    $ref: '#/responses/userBodyResponse'
-            schemes:
-                - https
-            summary: Get an individual user.
-            tags:
-                - user
-        post:
-            operationId: createUser
-            parameters:
-                - description: User
-                  in: body
-                  name: user
-                  schema:
-                    $ref: '#/definitions/User'
-                  x-go-name: User
-                - description: Username
-                  in: path
-                  name: username
-                  required: true
-                  type: string
-                  x-go-name: Username
-            responses:
-                "200":
-                    $ref: '#/responses/userBodyResponse'
-            schemes:
-                - https
-            summary: Create a user.
-            tags:
-                - user
-        put:
-            operationId: updateUser
-            parameters:
-                - description: User
-                  in: body
-                  name: user
-                  schema:
-                    $ref: '#/definitions/User'
-                  x-go-name: User
-                - description: Username
-                  in: path
-                  name: username
-                  required: true
-                  type: string
-                  x-go-name: Username
-            responses:
-                "200":
-                    $ref: '#/responses/userBodyResponse'
-            schemes:
-                - https
-            summary: Update a user.
-            tags:
-                - user
-    /api/users/{username}/remote_access_gw:
-        delete:
-            operationId: removeUserFromRemoteAccessGW
-            parameters:
-                - in: path
-                  name: username
-                  required: true
-                  type: string
-                  x-go-name: Username
-            responses:
-                "200":
-                    $ref: '#/responses/userBodyResponse'
-            schemes:
-                - https
-            summary: Delete User from a remote access gateway.
-            tags:
-                - user
-        post:
-            operationId: attachUserToRemoteAccessGateway
-            parameters:
-                - in: path
-                  name: username
-                  required: true
-                  type: string
-                  x-go-name: Username
-            responses:
-                "200":
-                    $ref: '#/responses/userBodyResponse'
-            schemes:
-                - https
-            summary: Attach User to a remote access gateway.
-            tags:
-                - user
-    /api/users/adm/authenticate:
-        post:
-            operationId: authenticateUser
-            parameters:
-                - description: User Auth Params
-                  in: body
-                  name: user_auth_params
-                  schema:
-                    $ref: '#/definitions/UserAuthParams'
-                  x-go-name: UserAuthParams
-            responses:
-                "200":
-                    $ref: '#/responses/successResponse'
-            schemes:
-                - https
-            summary: User authenticates using its password and retrieves a JWT for authorization.
-            tags:
-                - authenticate
-    /api/users/adm/createsuperadmin:
-        post:
-            operationId: createAdmin
-            parameters:
-                - description: User
-                  in: body
-                  name: user
-                  schema:
-                    $ref: '#/definitions/User'
-                  x-go-name: User
-            responses:
-                "200":
-                    $ref: '#/responses/userBodyResponse'
-            schemes:
-                - https
-            summary: Make a user an admin.
-            tags:
-                - user
-    /api/users/adm/hassuperadmin:
-        get:
-            operationId: hasSuperAdmin
-            responses:
-                "200":
-                    $ref: '#/responses/hasAdmin'
-            schemes:
-                - https
-            summary: Checks whether the server has an admin.
-            tags:
-                - user
-    /api/users/adm/transfersuperadmin:
-        post:
-            operationId: transferSuperAdmin
-            responses:
-                "200":
-                    $ref: '#/responses/userBodyResponse'
-            schemes:
-                - https
-            summary: Transfers superadmin role to an admin user.
-            tags:
-                - user
-    /api/users_pending:
-        get:
-            operationId: getPendingUsers
-            responses:
-                "200":
-                    $ref: '#/responses/userBodyResponse'
-            schemes:
-                - https
-            summary: Get all pending users.
-            tags:
-                - user
-    /api/users_pending/{username}/pending:
-        delete:
-            operationId: deleteAllPendingUsers
-            responses:
-                "200":
-                    $ref: '#/responses/userBodyResponse'
-            schemes:
-                - https
-            summary: delete all pending users.
-            tags:
-                - user
-    /api/users_pending/user/{username}:
-        delete:
-            operationId: deletePendingUser
-            responses:
-                "200":
-                    $ref: '#/responses/userBodyResponse'
-            schemes:
-                - https
-            summary: delete pending user.
-            tags:
-                - user
-        post:
-            operationId: approvePendingUser
-            responses:
-                "200":
-                    $ref: '#/responses/userBodyResponse'
-            schemes:
-                - https
-            summary: approve pending user.
-            tags:
-                - user
-    /api/v1/enrollment-keys:
-        get:
-            operationId: getEnrollmentKeys
-            responses:
-                "200":
-                    $ref: '#/responses/EnrollmentKeys'
-            schemes:
-                - https
-            summary: Lists all EnrollmentKeys for admins.
-            tags:
-                - enrollmentKeys
-        post:
-            operationId: createEnrollmentKey
-            parameters:
-                - description: APIEnrollmentKey
-                  in: body
-                  name: body
-                  schema:
-                    $ref: '#/definitions/APIEnrollmentKey'
-                  x-go-name: Body
-            responses:
-                "200":
-                    $ref: '#/responses/EnrollmentKey'
-            schemes:
-                - https
-            summary: Creates an EnrollmentKey for hosts to use on Netmaker server.
-            tags:
-                - enrollmentKeys
-    /api/v1/enrollment-keys/{keyid}:
-        delete:
-            operationId: deleteEnrollmentKey
-            parameters:
-                - in: path
-                  name: keyid
-                  required: true
-                  type: string
-                  x-go-name: KeyID
-            responses:
-                "200":
-                    $ref: '#/responses/okResponse'
-            schemes:
-                - https
-            summary: Deletes an EnrollmentKey from Netmaker server.
-            tags:
-                - enrollmentKeys
-        put:
-            operationId: updateEnrollmentKey
-            parameters:
-                - description: KeyID
-                  in: path
-                  name: keyid
-                  required: true
-                  type: string
-                  x-go-name: KeyID
-                - description: APIEnrollmentKey
-                  in: body
-                  name: body
-                  schema:
-                    $ref: '#/definitions/APIEnrollmentKey'
-                  x-go-name: Body
-            responses:
-                "200":
-                    $ref: '#/responses/EnrollmentKey'
-            schemes:
-                - https
-            summary: Updates an EnrollmentKey for hosts to use on Netmaker server. Updates only the relay to use.
-            tags:
-                - enrollmentKeys
-    /api/v1/enrollment-keys/{token}:
-        post:
-            operationId: handleHostRegister
-            parameters:
-                - in: path
-                  name: token
-                  required: true
-                  type: string
-                  x-go-name: Token
-                - in: body
-                  name: host
-                  schema:
-                    $ref: '#/definitions/Host'
-                  x-go-name: Host
-            responses:
-                "200":
-                    $ref: '#/responses/RegisterResponse'
-            schemes:
-                - https
-            summary: Handles a Netclient registration with server and add nodes accordingly.
-            tags:
-                - enrollmentKeys
-    /api/v1/fallback/host/{hostid}:
-        put:
-            operationId: hostUpdateFallback
-            responses:
-                "200":
-                    $ref: '#/responses/apiHostResponse'
-            schemes:
-                - https
-            summary: Updates a Netclient host on Netmaker server.
-            tags:
-                - hosts
-    /api/v1/host:
-        get:
-            description: Used by clients for "pull" command
-            operationId: pullHost
-            responses:
-                "200":
-                    $ref: '#/responses/hostPull'
-            schemes:
-                - https
-            tags:
-                - hosts
-    /api/v1/legacy/nodes:
-        delete:
-            operationId: wipeLegacyNodes
-            responses:
-                "200":
-                    $ref: '#/responses/successResponse'
-            schemes:
-                - https
-            summary: Delete all legacy nodes from DB.
-            tags:
-                - nodes
-    /api/v1/node/failover:
-        delete:
-            operationId: deletefailOver
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Create a relay.
-            tags:
-                - node
-        post:
-            operationId: createfailOver
-            responses:
-                "200":
-                    $ref: '#/responses/nodeResponse'
-            schemes:
-                - https
-            summary: Create a relay.
-            tags:
-                - node
-    /api/v1/nodes/migrate:
-        put:
-            operationId: migrateData
-            responses:
-                "200":
-                    $ref: '#/responses/hostPull'
-            schemes:
-                - https
-            summary: Used to migrate a legacy node.
-            tags:
-                - nodes
-    /meshclient/files/{filename}:
-        get:
-            operationId: getFile
-            parameters:
-                - description: Filename
-                  in: path
-                  name: filename
-                  required: true
-                  type: string
-                  x-go-name: Filename
-            responses:
-                "200":
-                    $ref: '#/responses/fileResponse'
-            schemes:
-                - https
-            summary: Retrieve a file from the file server.
-            tags:
-                - meshclient
-produces:
-    - application/json
-responses:
-    EnrollmentKey:
-        description: ""
-        schema:
-            $ref: '#/definitions/EnrollmentKey'
-    EnrollmentKeys:
-        description: ""
-        schema:
-            items:
-                $ref: '#/definitions/EnrollmentKey'
-            type: array
-    RegisterResponse:
-        description: ""
-        schema:
-            $ref: '#/definitions/RegisterResponse'
-    aclContainerResponse:
-        description: ""
-        schema:
-            $ref: '#/definitions/ACLContainer'
-    apiHostResponse:
-        description: ""
-        schema:
-            $ref: '#/definitions/ApiHost'
-    apiHostSliceResponse:
-        description: ""
-        schema:
-            items:
-                $ref: '#/definitions/ApiHost'
-            type: array
-    byteArrayResponse:
-        description: ""
-        schema:
-            items:
-                format: uint8
-                type: integer
-            type: array
-    dnsResponse:
-        description: Success
-        schema:
-            items:
-                $ref: '#/definitions/DNSEntry'
-            type: array
-    extClientResponse:
-        description: ""
-        schema:
-            $ref: '#/definitions/ExtClient'
-    extClientSliceResponse:
-        description: ""
-        schema:
-            items:
-                $ref: '#/definitions/ExtClient'
-            type: array
-    fileResponse:
-        description: ""
-        schema:
-            $ref: '#/definitions/File'
-    getNetworksSliceResponse:
-        description: ""
-        schema:
-            items:
-                $ref: '#/definitions/Network'
-            type: array
-    hasAdmin:
-        description: ""
-    hostPull:
-        description: ""
-        schema:
-            $ref: '#/definitions/HostPull'
-    networkBodyResponse:
-        description: ""
-        schema:
-            $ref: '#/definitions/Network'
-    nodeResponse:
-        description: ""
-        schema:
-            $ref: '#/definitions/LegacyNode'
-    nodeSliceResponse:
-        description: ""
-        schema:
-            items:
-                $ref: '#/definitions/ApiNode'
-            type: array
-    okResponse:
-        description: ""
-    serverConfigResponse:
-        description: ""
-        schema:
-            $ref: '#/definitions/ServerConfig'
-    signal:
-        description: ""
-        schema:
-            $ref: '#/definitions/Signal'
-    stringJSONResponse:
-        description: ""
-    successResponse:
-        description: ""
-        schema:
-            $ref: '#/definitions/SuccessResponse'
-    userBodyResponse:
-        description: ""
-        schema:
-            $ref: '#/definitions/User'
-schemes:
-    - https
-swagger: "2.0"

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.