diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
index 47418d7..696e5b5 100644
--- a/.github/workflows/docker-build.yml
+++ b/.github/workflows/docker-build.yml
@@ -11,7 +11,7 @@ jobs:
   build-image:
     runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       # set environment
       - name: Set BUILD_TIME env
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..f6d3f08
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,26 @@
+name: Lint
+
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - master
+
+jobs:
+  lint:
+    name: Lint
+    runs-on: ubuntu-22.04
+    timeout-minutes: 10
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: actions/setup-go@v3
+        with:
+          go-version: "1.21"
+
+      - name: golangci-lint
+        uses: golangci/golangci-lint-action@v3
+        with:
+          version: v1.54
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a00dbca..585d196 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -24,7 +24,7 @@ jobs:
           - 7
     steps:
     # get the source code
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
     # set environment
     - name: Set APP_VERSION env
@@ -35,9 +35,9 @@ jobs:
       uses: managedkaos/print-env@v1.0
 
     # setup node
-    - uses: actions/setup-node@v2
+    - uses: actions/setup-node@v4
       with:
-        node-version: '14'
+        node-version: '20'
         registry-url: 'https://registry.npmjs.org'
 
     # prepare assets
@@ -53,7 +53,7 @@ jobs:
         github_token: ${{ secrets.GITHUB_TOKEN }}
         goos: ${{ matrix.goos }}
         goarch: ${{ matrix.goarch }}
-        goversion: "https://dl.google.com/go/go1.16.1.linux-amd64.tar.gz"
+        goversion: "https://dl.google.com/go/go1.21.5.linux-amd64.tar.gz"
         pre_command: export CGO_ENABLED=0
         binary_name: "wireguard-ui"
         build_flags: -v
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..42ec4b6
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,27 @@
+run:
+  timeout: 5m
+  skip-dirs:
+    - .github
+    - hack
+    - vendor
+linters:
+  disable-all: true
+  enable:
+    - gofmt
+    - revive
+    - goimports
+    - deadcode
+    - govet
+    - unused
+    - whitespace
+    - misspell
+  fast: false
+linters-settings:
+  gofmt:
+    simplify: false
+  revive:
+    rules:
+      - name: exported
+        disabled: true
+issues:
+  exclude-use-default: false
diff --git a/emailer/smtp.go b/emailer/smtp.go
index a721d6f..2586924 100644
--- a/emailer/smtp.go
+++ b/emailer/smtp.go
@@ -3,9 +3,10 @@ package emailer
 import (
 	"crypto/tls"
 	"fmt"
-	mail "github.com/xhit/go-simple-mail/v2"
 	"strings"
 	"time"
+
+	mail "github.com/xhit/go-simple-mail/v2"
 )
 
 type SmtpMail struct {
diff --git a/handler/middlewares.go b/handler/middlewares.go
index 231cbf5..b03ef46 100644
--- a/handler/middlewares.go
+++ b/handler/middlewares.go
@@ -1,8 +1,9 @@
 package handler
 
 import (
-	"github.com/labstack/echo/v4"
 	"net/http"
+
+	"github.com/labstack/echo/v4"
 )
 
 // ContentTypeJson checks that the requests have the Content-Type header set to "application/json".
diff --git a/handler/routes.go b/handler/routes.go
index 4dc95c6..513b0a6 100644
--- a/handler/routes.go
+++ b/handler/routes.go
@@ -131,7 +131,6 @@ func Login(db store.IStore) echo.HandlerFunc {
 // GetUsers handler return a JSON list of all users
 func GetUsers(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		usersList, err := db.GetUsers()
 		if err != nil {
 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
@@ -344,7 +343,6 @@ func RemoveUser(db store.IStore) echo.HandlerFunc {
 // WireGuardClients handler
 func WireGuardClients(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		clientDataList, err := db.GetClients(true)
 		if err != nil {
 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
@@ -362,7 +360,6 @@ func WireGuardClients(db store.IStore) echo.HandlerFunc {
 // GetClients handler return a JSON list of Wireguard client data
 func GetClients(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		clientDataList, err := db.GetClients(true)
 		if err != nil {
 			return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
@@ -381,7 +378,6 @@ func GetClients(db store.IStore) echo.HandlerFunc {
 // GetClient handler returns a JSON object of Wireguard client data
 func GetClient(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		clientID := c.Param("id")
 
 		if _, err := xid.FromString(clientID); err != nil {
@@ -406,7 +402,6 @@ func GetClient(db store.IStore) echo.HandlerFunc {
 // NewClient handler
 func NewClient(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		var client model.Client
 		c.Bind(&client)
 
@@ -475,7 +470,6 @@ func NewClient(db store.IStore) echo.HandlerFunc {
 					return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Duplicate Public Key"})
 				}
 			}
-
 		}
 
 		if client.PresharedKey == "" {
@@ -544,14 +538,14 @@ func EmailClient(db store.IStore, mailer emailer.Emailer, emailSubject, emailCon
 		globalSettings, _ := db.GetGlobalSettings()
 		config := util.BuildClientConfig(*clientData.Client, server, globalSettings)
 
-		cfgAtt := emailer.Attachment{"wg0.conf", []byte(config)}
+		cfgAtt := emailer.Attachment{Name: "wg0.conf", Data: []byte(config)}
 		var attachments []emailer.Attachment
 		if clientData.Client.PrivateKey != "" {
 			qrdata, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(clientData.QRCode, "data:image/png;base64,"))
 			if err != nil {
 				return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "decoding: " + err.Error()})
 			}
-			qrAtt := emailer.Attachment{"wg.png", qrdata}
+			qrAtt := emailer.Attachment{Name: "wg.png", Data: qrdata}
 			attachments = []emailer.Attachment{cfgAtt, qrAtt}
 		} else {
 			attachments = []emailer.Attachment{cfgAtt}
@@ -620,7 +614,6 @@ func SendTelegramClient(db store.IStore) echo.HandlerFunc {
 // UpdateClient handler to update client information
 func UpdateClient(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		var _client model.Client
 		c.Bind(&_client)
 
@@ -694,7 +687,6 @@ func UpdateClient(db store.IStore) echo.HandlerFunc {
 			if client.PrivateKey != "" {
 				client.PrivateKey = ""
 			}
-
 		}
 
 		// update Wireguard Client PresharedKey
@@ -733,7 +725,6 @@ func UpdateClient(db store.IStore) echo.HandlerFunc {
 // SetClientStatus handler to enable / disable a client
 func SetClientStatus(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		data := make(map[string]interface{})
 		err := json.NewDecoder(c.Request().Body).Decode(&data)
 
@@ -806,7 +797,6 @@ func DownloadClient(db store.IStore) echo.HandlerFunc {
 // RemoveClient handler
 func RemoveClient(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		client := new(model.Client)
 		c.Bind(client)
 
@@ -829,7 +819,6 @@ func RemoveClient(db store.IStore) echo.HandlerFunc {
 // WireGuardServer handler
 func WireGuardServer(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		server, err := db.GetServer()
 		if err != nil {
 			log.Error("Cannot get server config: ", err)
@@ -846,7 +835,6 @@ func WireGuardServer(db store.IStore) echo.HandlerFunc {
 // WireGuardServerInterfaces handler
 func WireGuardServerInterfaces(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		var serverInterface model.ServerInterface
 		c.Bind(&serverInterface)
 
@@ -872,7 +860,6 @@ func WireGuardServerInterfaces(db store.IStore) echo.HandlerFunc {
 // WireGuardServerKeyPair handler to generate private and public keys
 func WireGuardServerKeyPair(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		// gen Wireguard key pair
 		key, err := wgtypes.GeneratePrivateKey()
 		if err != nil {
@@ -897,7 +884,6 @@ func WireGuardServerKeyPair(db store.IStore) echo.HandlerFunc {
 // GlobalSettings handler
 func GlobalSettings(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		globalSettings, err := db.GetGlobalSettings()
 		if err != nil {
 			log.Error("Cannot get global settings: ", err)
@@ -930,7 +916,6 @@ func Status(db store.IStore) echo.HandlerFunc {
 		Peers []PeerVM
 	}
 	return func(c echo.Context) error {
-
 		wgClient, err := wgctrl.New()
 		if err != nil {
 			return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{
@@ -1011,7 +996,6 @@ func Status(db store.IStore) echo.HandlerFunc {
 // GlobalSettingSubmit handler to update the global settings
 func GlobalSettingSubmit(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		var globalSettings model.GlobalSetting
 		c.Bind(&globalSettings)
 
@@ -1037,7 +1021,6 @@ func GlobalSettingSubmit(db store.IStore) echo.HandlerFunc {
 // MachineIPAddresses handler to get local interface ip addresses
 func MachineIPAddresses() echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		// get private ip addresses
 		interfaceList, err := util.GetInterfaceIPs()
 		if err != nil {
@@ -1068,7 +1051,6 @@ func GetOrderedSubnetRanges() echo.HandlerFunc {
 // SuggestIPAllocation handler to get the list of ip address for client
 func SuggestIPAllocation(db store.IStore) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		server, err := db.GetServer()
 		if err != nil {
 			log.Error("Cannot fetch server config from database: ", err)
@@ -1135,7 +1117,6 @@ func SuggestIPAllocation(db store.IStore) echo.HandlerFunc {
 // ApplyServerConfig handler to write config file and restart Wireguard server
 func ApplyServerConfig(db store.IStore, tmplDir fs.FS) echo.HandlerFunc {
 	return func(c echo.Context) error {
-
 		server, err := db.GetServer()
 		if err != nil {
 			log.Error("Cannot get server config: ", err)
diff --git a/handler/routes_wake_on_lan.go b/handler/routes_wake_on_lan.go
index 43a6186..1747a1e 100644
--- a/handler/routes_wake_on_lan.go
+++ b/handler/routes_wake_on_lan.go
@@ -2,14 +2,15 @@ package handler
 
 import (
 	"fmt"
+	"net"
+	"net/http"
+	"time"
+
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/gommon/log"
 	"github.com/ngoduykhanh/wireguard-ui/model"
 	"github.com/ngoduykhanh/wireguard-ui/store"
 	"github.com/sabhiram/go-wol/wol"
-	"net"
-	"net/http"
-	"time"
 )
 
 type WakeOnLanHostSavePayload struct {
diff --git a/main.go b/main.go
index e11cf29..3d67e70 100644
--- a/main.go
+++ b/main.go
@@ -73,7 +73,6 @@ var embeddedTemplates embed.FS
 var embeddedAssets embed.FS
 
 func init() {
-
 	// command-line flags and env variables
 	flag.BoolVar(&flagDisableLogin, "disable-login", util.LookupEnvOrBool("DISABLE_LOGIN", flagDisableLogin), "Disable authentication on the app. This is potentially dangerous.")
 	flag.StringVar(&flagBindAddress, "bind-address", util.LookupEnvOrString("BIND_ADDRESS", flagBindAddress), "Address:Port to which the app will be bound.")
diff --git a/store/jsondb/jsondb.go b/store/jsondb/jsondb.go
index 8b5f84e..5d01066 100644
--- a/store/jsondb/jsondb.go
+++ b/store/jsondb/jsondb.go
@@ -33,7 +33,6 @@ func New(dbPath string) (*JsonDB, error) {
 		dbPath: dbPath,
 	}
 	return &ans, nil
-
 }
 
 func (o *JsonDB) Init() error {
@@ -77,7 +76,6 @@ func (o *JsonDB) Init() error {
 
 	// server's key pair
 	if _, err := os.Stat(serverKeyPairPath); os.IsNotExist(err) {
-
 		key, err := wgtypes.GeneratePrivateKey()
 		if err != nil {
 			return scribble.ErrMissingCollection
@@ -193,7 +191,6 @@ func (o *JsonDB) GetUsers() ([]model.User, error) {
 			return users, fmt.Errorf("cannot decode user json structure: %v", err)
 		}
 		users = append(users, user)
-
 	}
 	return users, err
 }
diff --git a/store/jsondb/jsondb_wake_on_lan.go b/store/jsondb/jsondb_wake_on_lan.go
index bf43fcf..d210d61 100644
--- a/store/jsondb/jsondb_wake_on_lan.go
+++ b/store/jsondb/jsondb_wake_on_lan.go
@@ -76,7 +76,6 @@ func (o *JsonDB) SaveWakeOnLanHost(host model.WakeOnLanHost) error {
 	}
 
 	return output
-
 }
 
 func (o *JsonDB) DeleteWakeOnHost(host model.WakeOnLanHost) error {
diff --git a/util/hash.go b/util/hash.go
index 2dc6b28..3733451 100644
--- a/util/hash.go
+++ b/util/hash.go
@@ -4,6 +4,7 @@ import (
 	"encoding/base64"
 	"errors"
 	"fmt"
+
 	"golang.org/x/crypto/bcrypt"
 )