diff --git a/.gitignore b/.gitignore
index eef70a1..2a1c43e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@
.idea/
*.iws
out/
+dist/
ssh.key
.testCoverage.txt
wg_portal.db
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0cc3161
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,40 @@
+# Go parameters
+GOCMD=go
+MODULENAME=github.com/h44z/wg-portal
+GOFILES:=$(shell go list ./... | grep -v /vendor/)
+BUILDDIR=dist
+BINARIES=$(subst cmd/,,$(wildcard cmd/*))
+
+.PHONY: all test clean phony
+
+all: dep test build
+
+build: dep $(addprefix $(BUILDDIR)/,$(BINARIES))
+ cp -r assets $(BUILDDIR)
+
+dep:
+ $(GOCMD) mod download
+
+validate:
+ $(GOCMD) fmt $(GOFILES)
+ $(GOCMD) vet $(GOFILES)
+ $(GOCMD) test -race $(GOFILES)
+
+coverage:
+ $(GOCMD) fmt $(GOFILES)
+ $(GOCMD) test $(GOFILES) -v -coverprofile .testCoverage.txt
+ $(GOCMD) tool cover -func=.testCoverage.txt # use total:\s+\(statements\)\s+(\d+.\d+\%) as Gitlab CI regextotal:\s+\(statements\)\s+(\d+.\d+\%)
+
+coverage-html: coverage
+ $(GOCMD) tool cover -html=.testCoverage.txt
+
+test:
+ $(GOCMD) test $(MODULENAME)/... -v -count=1
+
+clean:
+ $(GOCMD) clean $(GOFILES)
+ rm -rf .testCoverage.txt
+ rm -rf $(BUILDDIR)
+
+$(BUILDDIR)/%: cmd/%/main.go dep phony
+ $(GOCMD) build -o $@ $<
\ No newline at end of file
diff --git a/assets/img/PROLICHT.png b/assets/img/PROLICHT.png
deleted file mode 100644
index f781789..0000000
Binary files a/assets/img/PROLICHT.png and /dev/null differ
diff --git a/assets/img/PROLICHT_FULL.png b/assets/img/PROLICHT_FULL.png
deleted file mode 100644
index 585ab0a..0000000
Binary files a/assets/img/PROLICHT_FULL.png and /dev/null differ
diff --git a/assets/img/avatar.png b/assets/img/avatar.png
deleted file mode 100644
index d7ac03a..0000000
Binary files a/assets/img/avatar.png and /dev/null differ
diff --git a/assets/img/close.png b/assets/img/close.png
deleted file mode 100644
index b10def2..0000000
Binary files a/assets/img/close.png and /dev/null differ
diff --git a/assets/img/header-logo.png b/assets/img/header-logo.png
new file mode 100644
index 0000000..7df31d7
Binary files /dev/null and b/assets/img/header-logo.png differ
diff --git a/assets/img/loading.gif b/assets/img/loading.gif
deleted file mode 100644
index 5087c2a..0000000
Binary files a/assets/img/loading.gif and /dev/null differ
diff --git a/assets/img/login.jpg b/assets/img/login.jpg
deleted file mode 100644
index 2c6f2f5..0000000
Binary files a/assets/img/login.jpg and /dev/null differ
diff --git a/assets/img/next.png b/assets/img/next.png
deleted file mode 100644
index 511a02c..0000000
Binary files a/assets/img/next.png and /dev/null differ
diff --git a/assets/img/prev.png b/assets/img/prev.png
deleted file mode 100644
index 487ba47..0000000
Binary files a/assets/img/prev.png and /dev/null differ
diff --git a/assets/img/prolicht_fallback.jpg b/assets/img/prolicht_fallback.jpg
deleted file mode 100644
index 657b261..0000000
Binary files a/assets/img/prolicht_fallback.jpg and /dev/null differ
diff --git a/assets/tpl/admin_create_clients.html b/assets/tpl/admin_create_clients.html
index a0eb6cf..53be56b 100644
--- a/assets/tpl/admin_create_clients.html
+++ b/assets/tpl/admin_create_clients.html
@@ -7,7 +7,6 @@
-
@@ -43,7 +42,7 @@
Cancel
- {{template "prt_footer.html"}}
+ {{template "prt_footer.html" .}}
diff --git a/assets/tpl/admin_edit_client.html b/assets/tpl/admin_edit_client.html
index c549c94..41203ce 100644
--- a/assets/tpl/admin_edit_client.html
+++ b/assets/tpl/admin_edit_client.html
@@ -6,7 +6,6 @@
{{ .Static.WebsiteTitle }} - Admin
-
@@ -83,7 +82,7 @@
Cancel
- {{template "prt_footer.html"}}
+ {{template "prt_footer.html" .}}
diff --git a/assets/tpl/admin_edit_interface.html b/assets/tpl/admin_edit_interface.html
index dd2238f..3a0380a 100644
--- a/assets/tpl/admin_edit_interface.html
+++ b/assets/tpl/admin_edit_interface.html
@@ -6,7 +6,6 @@
{{ .Static.WebsiteTitle }} - Admin
-
@@ -99,7 +98,7 @@
Cancel
- {{template "prt_footer.html"}}
+ {{template "prt_footer.html" .}}
diff --git a/assets/tpl/admin_index.html b/assets/tpl/admin_index.html
index 717218d..25be8a4 100644
--- a/assets/tpl/admin_index.html
+++ b/assets/tpl/admin_index.html
@@ -6,7 +6,6 @@
{{ .Static.WebsiteTitle }} - Admin
-
@@ -92,7 +91,9 @@
Current VPN Users
+ {{if not .Static.LdapDisabled}}
+ {{end}}
M
@@ -191,7 +192,7 @@
Currently listed peers: {{len .Peers}}
- {{template "prt_footer.html"}}
+ {{template "prt_footer.html" .}}
diff --git a/assets/tpl/error.html b/assets/tpl/error.html
index a9a76d0..611381b 100644
--- a/assets/tpl/error.html
+++ b/assets/tpl/error.html
@@ -6,7 +6,6 @@
{{ .Static.WebsiteTitle }} - Error
-
@@ -22,7 +21,7 @@
{{.Data.Details}}
← Back to Dashboard
-{{template "prt_footer.html"}}
+{{template "prt_footer.html" .}}
diff --git a/assets/tpl/index.html b/assets/tpl/index.html
index ca90f8c..c2e0a10 100644
--- a/assets/tpl/index.html
+++ b/assets/tpl/index.html
@@ -1,13 +1,12 @@
-
+
{{ .Static.WebsiteTitle }}
-
@@ -26,7 +25,7 @@
Client Software
Installation instructions for client software can be found on the official WireGuard website: https://www.wireguard.com/
- {{template "prt_footer.html"}}
+ {{template "prt_footer.html" .}}
diff --git a/assets/tpl/login.html b/assets/tpl/login.html
index f441f35..8277098 100644
--- a/assets/tpl/login.html
+++ b/assets/tpl/login.html
@@ -7,7 +7,6 @@
{{ .static.WebsiteTitle }} - Login
-
@@ -46,7 +45,7 @@
-
+
diff --git a/assets/tpl/profile.html b/assets/tpl/profile.html
deleted file mode 100644
index 739bc96..0000000
--- a/assets/tpl/profile.html
+++ /dev/null
@@ -1,120 +0,0 @@
-
-
-
-
-
-
-
- {{ .static.WebsiteTitle }} - Profile
-
-
-
-
-
-
-
-
- {{template "prt_nav.html" .}}
-
- {{template "prt_footer.html"}}
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/assets/tpl/prt_footer.html b/assets/tpl/prt_footer.html
index b7ebe9b..612e74c 100644
--- a/assets/tpl/prt_footer.html
+++ b/assets/tpl/prt_footer.html
@@ -1,5 +1,5 @@
\ No newline at end of file
diff --git a/assets/tpl/prt_nav.html b/assets/tpl/prt_nav.html
index dd83a5a..afb071a 100644
--- a/assets/tpl/prt_nav.html
+++ b/assets/tpl/prt_nav.html
@@ -3,7 +3,7 @@
-
+
{{else}}
- Login
+ Login
{{end}}
diff --git a/assets/tpl/user_index.html b/assets/tpl/user_index.html
index 4fb3173..a8d988b 100644
--- a/assets/tpl/user_index.html
+++ b/assets/tpl/user_index.html
@@ -6,7 +6,6 @@
{{ .Static.WebsiteTitle }} - Profile
-
@@ -100,7 +99,7 @@
Currently listed peers: {{len .Peers}}
- {{template "prt_footer.html"}}
+ {{template "prt_footer.html" .}}
diff --git a/internal/common/configuration.go b/internal/common/configuration.go
index c147f62..6d581e1 100644
--- a/internal/common/configuration.go
+++ b/internal/common/configuration.go
@@ -55,31 +55,39 @@ func loadConfigEnv(cfg interface{}) error {
type Config struct {
Core struct {
ListeningAddress string `yaml:"listeningAddress" envconfig:"LISTENING_ADDRESS"`
+ ExternalUrl string `yaml:"externalUrl" envconfig:"EXTERNAL_URL"`
Title string `yaml:"title" envconfig:"WEBSITE_TITLE"`
+ CompanyName string `yaml:"company" envconfig:"COMPANY_NAME"`
+ MailFrom string `yaml:"mailfrom" envconfig:"MAIL_FROM"`
+ AdminUser string `yaml:"adminUser" envconfig:"ADMIN_USER"` // optional, non LDAP admin user
+ AdminPassword string `yaml:"adminPass" envconfig:"ADMIN_PASS"`
} `yaml:"core"`
-
- LDAP ldap.Config `yaml:"ldap"`
- WG wireguard.Config `yaml:"wg"`
- AdminLdapGroup string `yaml:"adminLdapGroup" envconfig:"ADMIN_LDAP_GROUP"`
- LogoutRedirectPath string `yaml:"logoutRedirectPath" envconfig:"LOGOUT_REDIRECT_PATH"`
- AuthRoutePrefix string `yaml:"authRoutePrefix" envconfig:"AUTH_ROUTE_PREFIX"`
+ Email MailConfig `yaml:"email"`
+ LDAP ldap.Config `yaml:"ldap"`
+ WG wireguard.Config `yaml:"wg"`
+ AdminLdapGroup string `yaml:"adminLdapGroup" envconfig:"ADMIN_LDAP_GROUP"`
}
func NewConfig() *Config {
cfg := &Config{}
// Default config
- cfg.Core.ListeningAddress = ":8080"
+ cfg.Core.ListeningAddress = ":8123"
cfg.Core.Title = "WireGuard VPN"
+ cfg.Core.CompanyName = "WireGuard Portal"
+ cfg.Core.ExternalUrl = "http://localhost:8123"
+ cfg.Core.MailFrom = "WireGuard VPN "
+ cfg.Core.AdminUser = "" // non-ldap admin access is disabled by default
+ cfg.Core.AdminPassword = ""
cfg.LDAP.URL = "ldap://srv-ad01.company.local:389"
cfg.LDAP.BaseDN = "DC=COMPANY,DC=LOCAL"
cfg.LDAP.StartTLS = true
- cfg.LDAP.BindUser = "company\\ldap_wireguard"
+ cfg.LDAP.BindUser = "company\\\\ldap_wireguard"
cfg.LDAP.BindPass = "SuperSecret"
cfg.WG.DeviceName = "wg0"
cfg.AdminLdapGroup = "CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL"
- cfg.LogoutRedirectPath = "/"
- cfg.AuthRoutePrefix = "/auth"
+ cfg.Email.Host = "127.0.0.1"
+ cfg.Email.Port = 25
// Load config from file and environment
cfgFile, ok := os.LookupEnv("CONFIG_FILE")
diff --git a/internal/ldap/usercache.go b/internal/ldap/usercache.go
index 1afb7ad..6fe5593 100644
--- a/internal/ldap/usercache.go
+++ b/internal/ldap/usercache.go
@@ -18,9 +18,6 @@ var Fields = []string{"givenName", "sn", "mail", "department", "memberOf", "sAMA
"st", "postalCode", "co", "facsimileTelephoneNumber", "pager", "thumbnailPhoto", "otherMobile",
"extensionAttribute2", "distinguishedName", "userAccountControl"}
-var ModifiableFields = []string{"department", "telephoneNumber", "mobile", "displayName", "title", "company",
- "manager", "streetAddress", "employeeID", "l", "st", "postalCode", "co", "thumbnailPhoto"}
-
// --------------------------------------------------------------------------------------------------------------------
// Cache Data Store
// --------------------------------------------------------------------------------------------------------------------
@@ -28,7 +25,6 @@ var ModifiableFields = []string{"department", "telephoneNumber", "mobile", "disp
type UserCacheHolder interface {
Clear()
SetAllUsers(users []RawLdapData)
- SetUser(data RawLdapData)
GetUser(dn string) *RawLdapData
GetUsers() []*RawLdapData
}
@@ -95,14 +91,6 @@ func (h *SynchronizedUserCacheHolder) SetAllUsers(users []RawLdapData) {
}
}
-func (h *SynchronizedUserCacheHolder) SetUser(user RawLdapData) {
- h.mux.Lock()
- defer h.mux.Unlock()
-
- h.users[user.DN] = &UserCacheHolderEntry{RawLdapData: user}
- h.users[user.DN].CalcFieldsFromAttributes()
-}
-
func (h *SynchronizedUserCacheHolder) GetUser(dn string) *RawLdapData {
h.mux.RLock()
defer h.mux.RUnlock()
@@ -152,30 +140,6 @@ func (h *SynchronizedUserCacheHolder) GetSortedUsers(sortKey string, sortDirecti
}
-func (h *SynchronizedUserCacheHolder) GetFilteredUsers(sortKey string, sortDirection string, search, searchDepartment string) []*UserCacheHolderEntry {
- sortedUsers := h.GetSortedUsers(sortKey, sortDirection)
- if search == "" && searchDepartment == "" {
- return sortedUsers // skip filtering
- }
-
- filteredUsers := make([]*UserCacheHolderEntry, 0, len(sortedUsers))
- for _, user := range sortedUsers {
- if searchDepartment != "" && user.Attributes["department"] != searchDepartment {
- continue
- }
- if strings.Contains(user.Attributes["sn"], search) ||
- strings.Contains(user.Attributes["givenName"], search) ||
- strings.Contains(user.Mail, search) ||
- strings.Contains(user.Attributes["department"], search) ||
- strings.Contains(user.Attributes["telephoneNumber"], search) ||
- strings.Contains(user.Attributes["mobile"], search) {
- filteredUsers = append(filteredUsers, user)
- }
- }
-
- return filteredUsers
-}
-
func (h *SynchronizedUserCacheHolder) IsInGroup(username, gid string) bool {
userDN := h.GetUserDN(username)
if userDN == "" {
@@ -231,45 +195,6 @@ func (h *SynchronizedUserCacheHolder) GetUserDNByMail(mail string) string {
return userDN
}
-func (h *SynchronizedUserCacheHolder) GetTeamLeaders() []*UserCacheHolderEntry {
-
- sortedUsers := h.GetSortedUsers("sn", "asc")
- teamLeaders := make([]*UserCacheHolderEntry, 0, len(sortedUsers))
- for _, user := range sortedUsers {
- if user.Attributes["extensionAttribute2"] != "Teamleiter" {
- continue
- }
-
- teamLeaders = append(teamLeaders, user)
- }
-
- return teamLeaders
-}
-
-func (h *SynchronizedUserCacheHolder) GetDepartments() []string {
- h.mux.RLock()
- defer h.mux.RUnlock()
-
- departmentSet := make(map[string]struct{})
- for _, user := range h.users {
- if user.Attributes["department"] == "" {
- continue
- }
- departmentSet[user.Attributes["department"]] = struct{}{}
- }
-
- departments := make([]string, len(departmentSet))
- i := 0
- for department := range departmentSet {
- departments[i] = department
- i++
- }
-
- sort.Strings(departments)
-
- return departments
-}
-
// --------------------------------------------------------------------------------------------------------------------
// Cache Handler, LDAP interaction
// --------------------------------------------------------------------------------------------------------------------
@@ -398,58 +323,3 @@ func (u *UserCache) Update(filter bool) error {
return nil
}
-
-func (u *UserCache) ModifyUserData(dn string, newData RawLdapData, fields []string) error {
- if fields == nil {
- fields = ModifiableFields // default
- }
-
- existingUserData := u.userData.GetUser(dn)
- if existingUserData == nil {
- return fmt.Errorf("user with dn %s not found", dn)
- }
-
- modify := ldap.NewModifyRequest(dn, nil)
-
- for _, ldapAttribute := range fields {
- if existingUserData.Attributes[ldapAttribute] == newData.Attributes[ldapAttribute] {
- continue // do not update unchanged fields
- }
-
- if len(existingUserData.RawAttributes[ldapAttribute]) == 0 && newData.Attributes[ldapAttribute] != "" {
- modify.Add(ldapAttribute, []string{newData.Attributes[ldapAttribute]})
- newData.RawAttributes[ldapAttribute] = [][]byte{
- []byte(newData.Attributes[ldapAttribute]),
- }
- }
- if len(existingUserData.RawAttributes[ldapAttribute]) != 0 && newData.Attributes[ldapAttribute] != "" {
- modify.Replace(ldapAttribute, []string{newData.Attributes[ldapAttribute]})
- newData.RawAttributes[ldapAttribute][0] = []byte(newData.Attributes[ldapAttribute])
- }
- if len(existingUserData.RawAttributes[ldapAttribute]) != 0 && newData.Attributes[ldapAttribute] == "" {
- modify.Delete(ldapAttribute, []string{})
- newData.RawAttributes[ldapAttribute] = [][]byte{} // clear list
- }
- }
-
- if len(modify.Changes) == 0 {
- return nil
- }
-
- client, err := u.open()
- if err != nil {
- u.LastError = err
- return err
- }
- defer u.close(client)
-
- err = client.Modify(modify)
- if err != nil {
- return err
- }
-
- // Once written to ldap, update the local cache
- u.userData.SetUser(newData)
-
- return nil
-}
diff --git a/internal/server/core.go b/internal/server/core.go
index a8aba5f..d3d2151 100644
--- a/internal/server/core.go
+++ b/internal/server/core.go
@@ -56,8 +56,9 @@ type AlertData struct {
type StaticData struct {
WebsiteTitle string
WebsiteLogo string
- LoginURL string
- LogoutURL string
+ CompanyName string
+ Year int
+ LdapDisabled bool
}
type Server struct {
@@ -71,6 +72,7 @@ type Server struct {
wg *wireguard.Manager
// LDAP stuff
+ ldapDisabled bool
ldapAuth ldap.Authentication
ldapUsers *ldap.SynchronizedUserCacheHolder
ldapCacheUpdater *ldap.UserCache
@@ -88,7 +90,9 @@ func (s *Server) Setup() error {
s.ldapUsers.Init()
s.ldapCacheUpdater = ldap.NewUserCache(s.config.LDAP, s.ldapUsers)
if s.ldapCacheUpdater.LastError != nil {
- return s.ldapCacheUpdater.LastError
+ log.Warnf("LDAP error: %v", s.ldapCacheUpdater.LastError)
+ log.Warnf("LDAP features disabled!")
+ s.ldapDisabled = true
}
// Setup WireGuard stuff
@@ -141,15 +145,17 @@ func (s *Server) Setup() error {
func (s *Server) Run() {
// Start ldap group watcher
- go func(s *Server) {
- for {
- time.Sleep(CacheRefreshDuration)
- if err := s.ldapCacheUpdater.Update(true); err != nil {
- log.Warnf("Failed to update ldap group cache: %v", err)
+ if !s.ldapDisabled {
+ go func(s *Server) {
+ for {
+ time.Sleep(CacheRefreshDuration)
+ if err := s.ldapCacheUpdater.Update(true); err != nil {
+ log.Warnf("Failed to update ldap group cache: %v", err)
+ }
+ log.Debugf("Refreshed LDAP permissions!")
}
- log.Debugf("Refreshed LDAP permissions!")
- }
- }(s)
+ }(s)
+ }
// Run web service
err := s.server.Run(s.config.Core.ListeningAddress)
@@ -233,8 +239,10 @@ func (s *Server) destroySessionData(c *gin.Context) error {
func (s *Server) getStaticData() StaticData {
return StaticData{
WebsiteTitle: s.config.Core.Title,
- LoginURL: s.config.AuthRoutePrefix + "/login",
- LogoutURL: s.config.AuthRoutePrefix + "/logout",
+ WebsiteLogo: "/img/header-logo.png",
+ CompanyName: s.config.Core.CompanyName,
+ LdapDisabled: s.ldapDisabled,
+ Year: time.Now().Year(),
}
}
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
deleted file mode 100644
index d62ff82..0000000
--- a/internal/server/handlers.go
+++ /dev/null
@@ -1,588 +0,0 @@
-package server
-
-import (
- "bytes"
- "net/http"
- "net/url"
- "strconv"
- "strings"
- "time"
-
- log "github.com/sirupsen/logrus"
-
- "github.com/h44z/wg-portal/internal/ldap"
-
- "github.com/h44z/wg-portal/internal/common"
-
- "github.com/gin-gonic/gin"
-)
-
-type LdapCreateForm struct {
- Emails string `form:"email" binding:"required"`
- Identifier string `form:"identifier" binding:"required,lte=20"`
-}
-
-func (s *Server) GetIndex(c *gin.Context) {
- c.HTML(http.StatusOK, "index.html", struct {
- Route string
- Alerts AlertData
- Session SessionData
- Static StaticData
- Device Device
- }{
- Route: c.Request.URL.Path,
- Alerts: s.getAlertData(c),
- Session: s.getSessionData(c),
- Static: s.getStaticData(),
- Device: s.users.GetDevice(),
- })
-}
-
-func (s *Server) HandleError(c *gin.Context, code int, message, details string) {
- // TODO: if json
- //c.JSON(code, gin.H{"error": message, "details": details})
-
- c.HTML(code, "error.html", gin.H{
- "Data": gin.H{
- "Code": strconv.Itoa(code),
- "Message": message,
- "Details": details,
- },
- "Route": c.Request.URL.Path,
- "Session": s.getSessionData(c),
- "Static": s.getStaticData(),
- })
-}
-
-func (s *Server) GetAdminIndex(c *gin.Context) {
- currentSession := s.getSessionData(c)
-
- sort := c.Query("sort")
- if sort != "" {
- if currentSession.SortedBy != sort {
- currentSession.SortedBy = sort
- currentSession.SortDirection = "asc"
- } else {
- if currentSession.SortDirection == "asc" {
- currentSession.SortDirection = "desc"
- } else {
- currentSession.SortDirection = "asc"
- }
- }
-
- if err := s.updateSessionData(c, currentSession); err != nil {
- s.HandleError(c, http.StatusInternalServerError, "sort error", "failed to save session")
- return
- }
- c.Redirect(http.StatusSeeOther, "/admin")
- return
- }
-
- search, searching := c.GetQuery("search")
- if searching {
- currentSession.Search = search
-
- if err := s.updateSessionData(c, currentSession); err != nil {
- s.HandleError(c, http.StatusInternalServerError, "search error", "failed to save session")
- return
- }
- c.Redirect(http.StatusSeeOther, "/admin")
- return
- }
-
- device := s.users.GetDevice()
- users := s.users.GetFilteredAndSortedUsers(currentSession.SortedBy, currentSession.SortDirection, currentSession.Search)
-
- c.HTML(http.StatusOK, "admin_index.html", struct {
- Route string
- Alerts AlertData
- Session SessionData
- Static StaticData
- Peers []User
- TotalPeers int
- Device Device
- }{
- Route: c.Request.URL.Path,
- Alerts: s.getAlertData(c),
- Session: currentSession,
- Static: s.getStaticData(),
- Peers: users,
- TotalPeers: len(s.users.GetAllUsers()),
- Device: device,
- })
-}
-
-func (s *Server) GetUserIndex(c *gin.Context) {
- currentSession := s.getSessionData(c)
-
- sort := c.Query("sort")
- if sort != "" {
- if currentSession.SortedBy != sort {
- currentSession.SortedBy = sort
- currentSession.SortDirection = "asc"
- } else {
- if currentSession.SortDirection == "asc" {
- currentSession.SortDirection = "desc"
- } else {
- currentSession.SortDirection = "asc"
- }
- }
-
- if err := s.updateSessionData(c, currentSession); err != nil {
- s.HandleError(c, http.StatusInternalServerError, "sort error", "failed to save session")
- return
- }
- c.Redirect(http.StatusSeeOther, "/admin")
- return
- }
-
- device := s.users.GetDevice()
- users := s.users.GetSortedUsersForEmail(currentSession.SortedBy, currentSession.SortDirection, currentSession.Email)
-
- c.HTML(http.StatusOK, "user_index.html", struct {
- Route string
- Alerts AlertData
- Session SessionData
- Static StaticData
- Peers []User
- TotalPeers int
- Device Device
- }{
- Route: c.Request.URL.Path,
- Alerts: s.getAlertData(c),
- Session: currentSession,
- Static: s.getStaticData(),
- Peers: users,
- TotalPeers: len(users),
- Device: device,
- })
-}
-
-func (s *Server) GetAdminEditInterface(c *gin.Context) {
- device := s.users.GetDevice()
- users := s.users.GetAllUsers()
-
- currentSession, err := s.setFormInSession(c, device)
- if err != nil {
- s.HandleError(c, http.StatusInternalServerError, "Session error", err.Error())
- return
- }
-
- c.HTML(http.StatusOK, "admin_edit_interface.html", struct {
- Route string
- Alerts AlertData
- Session SessionData
- Static StaticData
- Peers []User
- Device Device
- }{
- Route: c.Request.URL.Path,
- Alerts: s.getAlertData(c),
- Session: currentSession,
- Static: s.getStaticData(),
- Peers: users,
- Device: currentSession.FormData.(Device),
- })
-}
-
-func (s *Server) PostAdminEditInterface(c *gin.Context) {
- currentSession := s.getSessionData(c)
- var formDevice Device
- if currentSession.FormData != nil {
- formDevice = currentSession.FormData.(Device)
- }
- if err := c.ShouldBind(&formDevice); err != nil {
- _ = s.updateFormInSession(c, formDevice)
- s.setAlert(c, "failed to bind form data: "+err.Error(), "danger")
- c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=bind")
- return
- }
- // Clean list input
- formDevice.IPs = common.ParseStringList(formDevice.IPsStr)
- formDevice.AllowedIPs = common.ParseStringList(formDevice.AllowedIPsStr)
- formDevice.DNS = common.ParseStringList(formDevice.DNSStr)
- formDevice.IPsStr = common.ListToString(formDevice.IPs)
- formDevice.AllowedIPsStr = common.ListToString(formDevice.AllowedIPs)
- formDevice.DNSStr = common.ListToString(formDevice.DNS)
-
- // Update WireGuard device
- err := s.wg.UpdateDevice(formDevice.DeviceName, formDevice.GetDeviceConfig())
- if err != nil {
- _ = s.updateFormInSession(c, formDevice)
- s.setAlert(c, "failed to update device in WireGuard: "+err.Error(), "danger")
- c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=wg")
- return
- }
-
- // Update in database
- err = s.users.UpdateDevice(formDevice)
- if err != nil {
- _ = s.updateFormInSession(c, formDevice)
- s.setAlert(c, "failed to update device in database: "+err.Error(), "danger")
- c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update")
- return
- }
-
- s.setAlert(c, "changes applied successfully", "success")
- c.Redirect(http.StatusSeeOther, "/admin/device/edit")
-}
-
-func (s *Server) GetAdminEditPeer(c *gin.Context) {
- device := s.users.GetDevice()
- user := s.users.GetUserByKey(c.Query("pkey"))
-
- currentSession, err := s.setFormInSession(c, user)
- if err != nil {
- s.HandleError(c, http.StatusInternalServerError, "Session error", err.Error())
- return
- }
-
- c.HTML(http.StatusOK, "admin_edit_client.html", struct {
- Route string
- Alerts AlertData
- Session SessionData
- Static StaticData
- Peer User
- Device Device
- }{
- Route: c.Request.URL.Path,
- Alerts: s.getAlertData(c),
- Session: currentSession,
- Static: s.getStaticData(),
- Peer: currentSession.FormData.(User),
- Device: device,
- })
-}
-
-func (s *Server) PostAdminEditPeer(c *gin.Context) {
- currentUser := s.users.GetUserByKey(c.Query("pkey"))
- urlEncodedKey := url.QueryEscape(c.Query("pkey"))
-
- currentSession := s.getSessionData(c)
- var formUser User
- if currentSession.FormData != nil {
- formUser = currentSession.FormData.(User)
- }
- if err := c.ShouldBind(&formUser); err != nil {
- _ = s.updateFormInSession(c, formUser)
- s.setAlert(c, "failed to bind form data: "+err.Error(), "danger")
- c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey+"&formerr=bind")
- return
- }
-
- // Clean list input
- formUser.IPs = common.ParseStringList(formUser.IPsStr)
- formUser.AllowedIPs = common.ParseStringList(formUser.AllowedIPsStr)
- formUser.IPsStr = common.ListToString(formUser.IPs)
- formUser.AllowedIPsStr = common.ListToString(formUser.AllowedIPs)
-
- disabled := c.PostForm("isdisabled") != ""
- now := time.Now()
- if disabled && currentUser.DeactivatedAt == nil {
- formUser.DeactivatedAt = &now
- } else if !disabled {
- formUser.DeactivatedAt = nil
- }
-
- // Update in database
- if err := s.UpdateUser(formUser, now); err != nil {
- _ = s.updateFormInSession(c, formUser)
- s.setAlert(c, "failed to update user: "+err.Error(), "danger")
- c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey+"&formerr=update")
- return
- }
-
- s.setAlert(c, "changes applied successfully", "success")
- c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey)
-}
-
-func (s *Server) GetAdminCreatePeer(c *gin.Context) {
- device := s.users.GetDevice()
-
- currentSession, err := s.setNewUserFormInSession(c)
- if err != nil {
- s.HandleError(c, http.StatusInternalServerError, "Session error", err.Error())
- return
- }
- c.HTML(http.StatusOK, "admin_edit_client.html", struct {
- Route string
- Alerts AlertData
- Session SessionData
- Static StaticData
- Peer User
- Device Device
- }{
- Route: c.Request.URL.Path,
- Alerts: s.getAlertData(c),
- Session: currentSession,
- Static: s.getStaticData(),
- Peer: currentSession.FormData.(User),
- Device: device,
- })
-}
-
-func (s *Server) PostAdminCreatePeer(c *gin.Context) {
- currentSession := s.getSessionData(c)
- var formUser User
- if currentSession.FormData != nil {
- formUser = currentSession.FormData.(User)
- }
- if err := c.ShouldBind(&formUser); err != nil {
- _ = s.updateFormInSession(c, formUser)
- s.setAlert(c, "failed to bind form data: "+err.Error(), "danger")
- c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=bind")
- return
- }
-
- // Clean list input
- formUser.IPs = common.ParseStringList(formUser.IPsStr)
- formUser.AllowedIPs = common.ParseStringList(formUser.AllowedIPsStr)
- formUser.IPsStr = common.ListToString(formUser.IPs)
- formUser.AllowedIPsStr = common.ListToString(formUser.AllowedIPs)
-
- disabled := c.PostForm("isdisabled") != ""
- now := time.Now()
- if disabled {
- formUser.DeactivatedAt = &now
- }
-
- if err := s.CreateUser(formUser); err != nil {
- _ = s.updateFormInSession(c, formUser)
- s.setAlert(c, "failed to add user: "+err.Error(), "danger")
- c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=create")
- return
- }
-
- s.setAlert(c, "client created successfully", "success")
- c.Redirect(http.StatusSeeOther, "/admin")
-}
-
-func (s *Server) GetAdminCreateLdapPeers(c *gin.Context) {
- currentSession, err := s.setFormInSession(c, LdapCreateForm{Identifier: "Default"})
- if err != nil {
- s.HandleError(c, http.StatusInternalServerError, "Session error", err.Error())
- return
- }
-
- c.HTML(http.StatusOK, "admin_create_clients.html", struct {
- Route string
- Alerts AlertData
- Session SessionData
- Static StaticData
- Users []*ldap.UserCacheHolderEntry
- FormData LdapCreateForm
- Device Device
- }{
- Route: c.Request.URL.Path,
- Alerts: s.getAlertData(c),
- Session: currentSession,
- Static: s.getStaticData(),
- Users: s.ldapUsers.GetSortedUsers("sn", "asc"),
- FormData: currentSession.FormData.(LdapCreateForm),
- Device: s.users.GetDevice(),
- })
-}
-
-func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) {
- currentSession := s.getSessionData(c)
- var formData LdapCreateForm
- if currentSession.FormData != nil {
- formData = currentSession.FormData.(LdapCreateForm)
- }
- if err := c.ShouldBind(&formData); err != nil {
- _ = s.updateFormInSession(c, formData)
- s.setAlert(c, "failed to bind form data: "+err.Error(), "danger")
- c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=bind")
- return
- }
-
- emails := common.ParseStringList(formData.Emails)
- for i := range emails {
- // TODO: also check email addr for validity?
- if !strings.ContainsRune(emails[i], '@') || s.ldapUsers.GetUserDNByMail(emails[i]) == "" {
- _ = s.updateFormInSession(c, formData)
- s.setAlert(c, "invalid email address: "+emails[i], "danger")
- c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=mail")
- return
- }
- }
-
- log.Infof("creating %d ldap peers", len(emails))
-
- for i := range emails {
- if err := s.CreateUserByEmail(emails[i], formData.Identifier, false); err != nil {
- _ = s.updateFormInSession(c, formData)
- s.setAlert(c, "failed to add user: "+err.Error(), "danger")
- c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=create")
- return
- }
- }
-
- s.setAlert(c, "client(s) created successfully", "success")
- c.Redirect(http.StatusSeeOther, "/admin/peer/createldap")
-}
-
-func (s *Server) GetAdminDeletePeer(c *gin.Context) {
- currentUser := s.users.GetUserByKey(c.Query("pkey"))
- if err := s.DeleteUser(currentUser); err != nil {
- s.HandleError(c, http.StatusInternalServerError, "Deletion error", err.Error())
- return
- }
- s.setAlert(c, "user deleted successfully", "success")
- c.Redirect(http.StatusSeeOther, "/admin")
-}
-
-func (s *Server) GetUserQRCode(c *gin.Context) {
- user := s.users.GetUserByKey(c.Query("pkey"))
- currentSession := s.getSessionData(c)
- if !currentSession.IsAdmin && user.Email != currentSession.Email {
- s.HandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
- return
- }
-
- png, err := user.GetQRCode()
- if err != nil {
- s.HandleError(c, http.StatusInternalServerError, "QRCode error", err.Error())
- return
- }
- c.Data(http.StatusOK, "image/png", png)
- return
-}
-
-func (s *Server) GetUserConfig(c *gin.Context) {
- user := s.users.GetUserByKey(c.Query("pkey"))
- currentSession := s.getSessionData(c)
- if !currentSession.IsAdmin && user.Email != currentSession.Email {
- s.HandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
- return
- }
-
- cfg, err := user.GetClientConfigFile(s.users.GetDevice())
- if err != nil {
- s.HandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
- return
- }
-
- c.Header("Content-Disposition", "attachment; filename="+user.GetConfigFileName())
- c.Data(http.StatusOK, "application/config", cfg)
- return
-}
-
-func (s *Server) GetUserConfigMail(c *gin.Context) {
- user := s.users.GetUserByKey(c.Query("pkey"))
- currentSession := s.getSessionData(c)
- if !currentSession.IsAdmin && user.Email != currentSession.Email {
- s.HandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
- return
- }
-
- cfg, err := user.GetClientConfigFile(s.users.GetDevice())
- if err != nil {
- s.HandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
- return
- }
- png, err := user.GetQRCode()
- if err != nil {
- s.HandleError(c, http.StatusInternalServerError, "QRCode error", err.Error())
- return
- }
- // Apply mail template
- var tplBuff bytes.Buffer
- if err := s.mailTpl.Execute(&tplBuff, struct {
- Client User
- QrcodePngName string
- PortalUrl string
- }{
- Client: user,
- QrcodePngName: "wireguard-config.png",
- PortalUrl: s.config.Core.ExternalUrl,
- }); err != nil {
- s.HandleError(c, http.StatusInternalServerError, "Template error", err.Error())
- return
- }
-
- // Send mail
- attachments := []common.MailAttachment{
- {
- Name: user.GetConfigFileName(),
- ContentType: "application/config",
- Data: bytes.NewReader(cfg),
- },
- {
- Name: "wireguard-config.png",
- ContentType: "image/png",
- Data: bytes.NewReader(png),
- },
- }
-
- if err := common.SendEmailWithAttachments(s.config.Email, s.config.Core.MailFrom, "", "WireGuard VPN Configuration",
- "Your mail client does not support HTML. Please find the configuration attached to this mail.", tplBuff.String(),
- []string{user.Email}, attachments); err != nil {
- s.HandleError(c, http.StatusInternalServerError, "Email error", err.Error())
- return
- }
-
- s.setAlert(c, "mail sent successfully", "success")
- c.Redirect(http.StatusSeeOther, "/admin")
-}
-
-func (s *Server) GetDeviceConfig(c *gin.Context) {
- device := s.users.GetDevice()
- users := s.users.GetActiveUsers()
- cfg, err := device.GetDeviceConfigFile(users)
- if err != nil {
- s.HandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
- return
- }
-
- filename := strings.ToLower(device.DeviceName) + ".conf"
-
- c.Header("Content-Disposition", "attachment; filename="+filename)
- c.Data(http.StatusOK, "application/config", cfg)
- return
-}
-
-func (s *Server) updateFormInSession(c *gin.Context, formData interface{}) error {
- currentSession := s.getSessionData(c)
- currentSession.FormData = formData
-
- if err := s.updateSessionData(c, currentSession); err != nil {
- return err
- }
-
- return nil
-}
-
-func (s *Server) setNewUserFormInSession(c *gin.Context) (SessionData, error) {
- currentSession := s.getSessionData(c)
- // If session does not contain a user form ignore update
- // If url contains a formerr parameter reset the form
- if currentSession.FormData == nil || c.Query("formerr") == "" {
- user, err := s.PrepareNewUser()
- if err != nil {
- return currentSession, err
- }
- currentSession.FormData = user
- }
-
- if err := s.updateSessionData(c, currentSession); err != nil {
- return currentSession, err
- }
-
- return currentSession, nil
-}
-
-func (s *Server) setFormInSession(c *gin.Context, formData interface{}) (SessionData, error) {
- currentSession := s.getSessionData(c)
- // If session does not contain a form ignore update
- // If url contains a formerr parameter reset the form
- if currentSession.FormData == nil || c.Query("formerr") == "" {
- currentSession.FormData = formData
- }
-
- if err := s.updateSessionData(c, currentSession); err != nil {
- return currentSession, err
- }
-
- return currentSession, nil
-}
diff --git a/internal/server/handlers_auth.go b/internal/server/handlers_auth.go
index 65a951d..8684641 100644
--- a/internal/server/handlers_auth.go
+++ b/internal/server/handlers_auth.go
@@ -44,7 +44,7 @@ func (s *Server) PostLogin(c *gin.Context) {
// Validate form input
if strings.Trim(username, " ") == "" || strings.Trim(password, " ") == "" {
- c.Redirect(http.StatusSeeOther, s.config.AuthRoutePrefix+"/login?err=missingdata")
+ c.Redirect(http.StatusSeeOther, "/auth/login?err=missingdata")
return
}
@@ -55,12 +55,12 @@ func (s *Server) PostLogin(c *gin.Context) {
// Check if user is in cache, avoid unnecessary ldap requests
if !adminAuthenticated && !s.ldapUsers.UserExists(username) {
- c.Redirect(http.StatusSeeOther, s.config.AuthRoutePrefix+"/login?err=authfail")
+ c.Redirect(http.StatusSeeOther, "/auth/login?err=authfail")
}
// Check if username and password match
if !adminAuthenticated && !s.ldapAuth.CheckLogin(username, password) {
- c.Redirect(http.StatusSeeOther, s.config.AuthRoutePrefix+"/login?err=authfail")
+ c.Redirect(http.StatusSeeOther, "/auth/login?err=authfail")
return
}
@@ -96,7 +96,7 @@ func (s *Server) PostLogin(c *gin.Context) {
}
if err := s.updateSessionData(c, sessionData); err != nil {
- s.HandleError(c, http.StatusInternalServerError, "login error", "failed to save session")
+ s.GetHandleError(c, http.StatusInternalServerError, "login error", "failed to save session")
return
}
c.Redirect(http.StatusSeeOther, "/")
@@ -106,13 +106,13 @@ func (s *Server) GetLogout(c *gin.Context) {
currentSession := s.getSessionData(c)
if !currentSession.LoggedIn { // Not logged in
- c.Redirect(http.StatusSeeOther, s.config.LogoutRedirectPath)
+ c.Redirect(http.StatusSeeOther, "/")
return
}
if err := s.destroySessionData(c); err != nil {
- s.HandleError(c, http.StatusInternalServerError, "logout error", "failed to destroy session")
+ s.GetHandleError(c, http.StatusInternalServerError, "logout error", "failed to destroy session")
return
}
- c.Redirect(http.StatusSeeOther, s.config.LogoutRedirectPath)
+ c.Redirect(http.StatusSeeOther, "/")
}
diff --git a/internal/server/handlers_common.go b/internal/server/handlers_common.go
new file mode 100644
index 0000000..85b72af
--- /dev/null
+++ b/internal/server/handlers_common.go
@@ -0,0 +1,188 @@
+package server
+
+import (
+ "net/http"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+)
+
+func (s *Server) GetHandleError(c *gin.Context, code int, message, details string) {
+ c.HTML(code, "error.html", gin.H{
+ "Data": gin.H{
+ "Code": strconv.Itoa(code),
+ "Message": message,
+ "Details": details,
+ },
+ "Route": c.Request.URL.Path,
+ "Session": s.getSessionData(c),
+ "Static": s.getStaticData(),
+ })
+}
+
+func (s *Server) GetIndex(c *gin.Context) {
+ c.HTML(http.StatusOK, "index.html", struct {
+ Route string
+ Alerts AlertData
+ Session SessionData
+ Static StaticData
+ Device Device
+ }{
+ Route: c.Request.URL.Path,
+ Alerts: s.getAlertData(c),
+ Session: s.getSessionData(c),
+ Static: s.getStaticData(),
+ Device: s.users.GetDevice(),
+ })
+}
+
+func (s *Server) GetAdminIndex(c *gin.Context) {
+ currentSession := s.getSessionData(c)
+
+ sort := c.Query("sort")
+ if sort != "" {
+ if currentSession.SortedBy != sort {
+ currentSession.SortedBy = sort
+ currentSession.SortDirection = "asc"
+ } else {
+ if currentSession.SortDirection == "asc" {
+ currentSession.SortDirection = "desc"
+ } else {
+ currentSession.SortDirection = "asc"
+ }
+ }
+
+ if err := s.updateSessionData(c, currentSession); err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "sort error", "failed to save session")
+ return
+ }
+ c.Redirect(http.StatusSeeOther, "/admin")
+ return
+ }
+
+ search, searching := c.GetQuery("search")
+ if searching {
+ currentSession.Search = search
+
+ if err := s.updateSessionData(c, currentSession); err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "search error", "failed to save session")
+ return
+ }
+ c.Redirect(http.StatusSeeOther, "/admin")
+ return
+ }
+
+ device := s.users.GetDevice()
+ users := s.users.GetFilteredAndSortedUsers(currentSession.SortedBy, currentSession.SortDirection, currentSession.Search)
+
+ c.HTML(http.StatusOK, "admin_index.html", struct {
+ Route string
+ Alerts AlertData
+ Session SessionData
+ Static StaticData
+ Peers []User
+ TotalPeers int
+ Device Device
+ LdapDisabled bool
+ }{
+ Route: c.Request.URL.Path,
+ Alerts: s.getAlertData(c),
+ Session: currentSession,
+ Static: s.getStaticData(),
+ Peers: users,
+ TotalPeers: len(s.users.GetAllUsers()),
+ Device: device,
+ LdapDisabled: s.ldapDisabled,
+ })
+}
+
+func (s *Server) GetUserIndex(c *gin.Context) {
+ currentSession := s.getSessionData(c)
+
+ sort := c.Query("sort")
+ if sort != "" {
+ if currentSession.SortedBy != sort {
+ currentSession.SortedBy = sort
+ currentSession.SortDirection = "asc"
+ } else {
+ if currentSession.SortDirection == "asc" {
+ currentSession.SortDirection = "desc"
+ } else {
+ currentSession.SortDirection = "asc"
+ }
+ }
+
+ if err := s.updateSessionData(c, currentSession); err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "sort error", "failed to save session")
+ return
+ }
+ c.Redirect(http.StatusSeeOther, "/admin")
+ return
+ }
+
+ device := s.users.GetDevice()
+ users := s.users.GetSortedUsersForEmail(currentSession.SortedBy, currentSession.SortDirection, currentSession.Email)
+
+ c.HTML(http.StatusOK, "user_index.html", struct {
+ Route string
+ Alerts AlertData
+ Session SessionData
+ Static StaticData
+ Peers []User
+ TotalPeers int
+ Device Device
+ }{
+ Route: c.Request.URL.Path,
+ Alerts: s.getAlertData(c),
+ Session: currentSession,
+ Static: s.getStaticData(),
+ Peers: users,
+ TotalPeers: len(users),
+ Device: device,
+ })
+}
+
+func (s *Server) updateFormInSession(c *gin.Context, formData interface{}) error {
+ currentSession := s.getSessionData(c)
+ currentSession.FormData = formData
+
+ if err := s.updateSessionData(c, currentSession); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (s *Server) setNewUserFormInSession(c *gin.Context) (SessionData, error) {
+ currentSession := s.getSessionData(c)
+ // If session does not contain a user form ignore update
+ // If url contains a formerr parameter reset the form
+ if currentSession.FormData == nil || c.Query("formerr") == "" {
+ user, err := s.PrepareNewUser()
+ if err != nil {
+ return currentSession, err
+ }
+ currentSession.FormData = user
+ }
+
+ if err := s.updateSessionData(c, currentSession); err != nil {
+ return currentSession, err
+ }
+
+ return currentSession, nil
+}
+
+func (s *Server) setFormInSession(c *gin.Context, formData interface{}) (SessionData, error) {
+ currentSession := s.getSessionData(c)
+ // If session does not contain a form ignore update
+ // If url contains a formerr parameter reset the form
+ if currentSession.FormData == nil || c.Query("formerr") == "" {
+ currentSession.FormData = formData
+ }
+
+ if err := s.updateSessionData(c, currentSession); err != nil {
+ return currentSession, err
+ }
+
+ return currentSession, nil
+}
diff --git a/internal/server/handlers_interface.go b/internal/server/handlers_interface.go
new file mode 100644
index 0000000..860e87a
--- /dev/null
+++ b/internal/server/handlers_interface.go
@@ -0,0 +1,94 @@
+package server
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+ "github.com/h44z/wg-portal/internal/common"
+)
+
+func (s *Server) GetAdminEditInterface(c *gin.Context) {
+ device := s.users.GetDevice()
+ users := s.users.GetAllUsers()
+
+ currentSession, err := s.setFormInSession(c, device)
+ if err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
+ return
+ }
+
+ c.HTML(http.StatusOK, "admin_edit_interface.html", struct {
+ Route string
+ Alerts AlertData
+ Session SessionData
+ Static StaticData
+ Peers []User
+ Device Device
+ }{
+ Route: c.Request.URL.Path,
+ Alerts: s.getAlertData(c),
+ Session: currentSession,
+ Static: s.getStaticData(),
+ Peers: users,
+ Device: currentSession.FormData.(Device),
+ })
+}
+
+func (s *Server) PostAdminEditInterface(c *gin.Context) {
+ currentSession := s.getSessionData(c)
+ var formDevice Device
+ if currentSession.FormData != nil {
+ formDevice = currentSession.FormData.(Device)
+ }
+ if err := c.ShouldBind(&formDevice); err != nil {
+ _ = s.updateFormInSession(c, formDevice)
+ s.setAlert(c, "failed to bind form data: "+err.Error(), "danger")
+ c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=bind")
+ return
+ }
+ // Clean list input
+ formDevice.IPs = common.ParseStringList(formDevice.IPsStr)
+ formDevice.AllowedIPs = common.ParseStringList(formDevice.AllowedIPsStr)
+ formDevice.DNS = common.ParseStringList(formDevice.DNSStr)
+ formDevice.IPsStr = common.ListToString(formDevice.IPs)
+ formDevice.AllowedIPsStr = common.ListToString(formDevice.AllowedIPs)
+ formDevice.DNSStr = common.ListToString(formDevice.DNS)
+
+ // Update WireGuard device
+ err := s.wg.UpdateDevice(formDevice.DeviceName, formDevice.GetDeviceConfig())
+ if err != nil {
+ _ = s.updateFormInSession(c, formDevice)
+ s.setAlert(c, "failed to update device in WireGuard: "+err.Error(), "danger")
+ c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=wg")
+ return
+ }
+
+ // Update in database
+ err = s.users.UpdateDevice(formDevice)
+ if err != nil {
+ _ = s.updateFormInSession(c, formDevice)
+ s.setAlert(c, "failed to update device in database: "+err.Error(), "danger")
+ c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update")
+ return
+ }
+
+ s.setAlert(c, "changes applied successfully", "success")
+ c.Redirect(http.StatusSeeOther, "/admin/device/edit")
+}
+
+func (s *Server) GetInterfaceConfig(c *gin.Context) {
+ device := s.users.GetDevice()
+ users := s.users.GetActiveUsers()
+ cfg, err := device.GetDeviceConfigFile(users)
+ if err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
+ return
+ }
+
+ filename := strings.ToLower(device.DeviceName) + ".conf"
+
+ c.Header("Content-Disposition", "attachment; filename="+filename)
+ c.Data(http.StatusOK, "application/config", cfg)
+ return
+}
diff --git a/internal/server/handlers_peer.go b/internal/server/handlers_peer.go
new file mode 100644
index 0000000..dc6e55b
--- /dev/null
+++ b/internal/server/handlers_peer.go
@@ -0,0 +1,318 @@
+package server
+
+import (
+ "bytes"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/h44z/wg-portal/internal/common"
+ "github.com/h44z/wg-portal/internal/ldap"
+ log "github.com/sirupsen/logrus"
+)
+
+type LdapCreateForm struct {
+ Emails string `form:"email" binding:"required"`
+ Identifier string `form:"identifier" binding:"required,lte=20"`
+}
+
+func (s *Server) GetAdminEditPeer(c *gin.Context) {
+ device := s.users.GetDevice()
+ user := s.users.GetUserByKey(c.Query("pkey"))
+
+ currentSession, err := s.setFormInSession(c, user)
+ if err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
+ return
+ }
+
+ c.HTML(http.StatusOK, "admin_edit_client.html", struct {
+ Route string
+ Alerts AlertData
+ Session SessionData
+ Static StaticData
+ Peer User
+ Device Device
+ }{
+ Route: c.Request.URL.Path,
+ Alerts: s.getAlertData(c),
+ Session: currentSession,
+ Static: s.getStaticData(),
+ Peer: currentSession.FormData.(User),
+ Device: device,
+ })
+}
+
+func (s *Server) PostAdminEditPeer(c *gin.Context) {
+ currentUser := s.users.GetUserByKey(c.Query("pkey"))
+ urlEncodedKey := url.QueryEscape(c.Query("pkey"))
+
+ currentSession := s.getSessionData(c)
+ var formUser User
+ if currentSession.FormData != nil {
+ formUser = currentSession.FormData.(User)
+ }
+ if err := c.ShouldBind(&formUser); err != nil {
+ _ = s.updateFormInSession(c, formUser)
+ s.setAlert(c, "failed to bind form data: "+err.Error(), "danger")
+ c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey+"&formerr=bind")
+ return
+ }
+
+ // Clean list input
+ formUser.IPs = common.ParseStringList(formUser.IPsStr)
+ formUser.AllowedIPs = common.ParseStringList(formUser.AllowedIPsStr)
+ formUser.IPsStr = common.ListToString(formUser.IPs)
+ formUser.AllowedIPsStr = common.ListToString(formUser.AllowedIPs)
+
+ disabled := c.PostForm("isdisabled") != ""
+ now := time.Now()
+ if disabled && currentUser.DeactivatedAt == nil {
+ formUser.DeactivatedAt = &now
+ } else if !disabled {
+ formUser.DeactivatedAt = nil
+ }
+
+ // Update in database
+ if err := s.UpdateUser(formUser, now); err != nil {
+ _ = s.updateFormInSession(c, formUser)
+ s.setAlert(c, "failed to update user: "+err.Error(), "danger")
+ c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey+"&formerr=update")
+ return
+ }
+
+ s.setAlert(c, "changes applied successfully", "success")
+ c.Redirect(http.StatusSeeOther, "/admin/peer/edit?pkey="+urlEncodedKey)
+}
+
+func (s *Server) GetAdminCreatePeer(c *gin.Context) {
+ device := s.users.GetDevice()
+
+ currentSession, err := s.setNewUserFormInSession(c)
+ if err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
+ return
+ }
+ c.HTML(http.StatusOK, "admin_edit_client.html", struct {
+ Route string
+ Alerts AlertData
+ Session SessionData
+ Static StaticData
+ Peer User
+ Device Device
+ }{
+ Route: c.Request.URL.Path,
+ Alerts: s.getAlertData(c),
+ Session: currentSession,
+ Static: s.getStaticData(),
+ Peer: currentSession.FormData.(User),
+ Device: device,
+ })
+}
+
+func (s *Server) PostAdminCreatePeer(c *gin.Context) {
+ currentSession := s.getSessionData(c)
+ var formUser User
+ if currentSession.FormData != nil {
+ formUser = currentSession.FormData.(User)
+ }
+ if err := c.ShouldBind(&formUser); err != nil {
+ _ = s.updateFormInSession(c, formUser)
+ s.setAlert(c, "failed to bind form data: "+err.Error(), "danger")
+ c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=bind")
+ return
+ }
+
+ // Clean list input
+ formUser.IPs = common.ParseStringList(formUser.IPsStr)
+ formUser.AllowedIPs = common.ParseStringList(formUser.AllowedIPsStr)
+ formUser.IPsStr = common.ListToString(formUser.IPs)
+ formUser.AllowedIPsStr = common.ListToString(formUser.AllowedIPs)
+
+ disabled := c.PostForm("isdisabled") != ""
+ now := time.Now()
+ if disabled {
+ formUser.DeactivatedAt = &now
+ }
+
+ if err := s.CreateUser(formUser); err != nil {
+ _ = s.updateFormInSession(c, formUser)
+ s.setAlert(c, "failed to add user: "+err.Error(), "danger")
+ c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=create")
+ return
+ }
+
+ s.setAlert(c, "client created successfully", "success")
+ c.Redirect(http.StatusSeeOther, "/admin")
+}
+
+func (s *Server) GetAdminCreateLdapPeers(c *gin.Context) {
+ currentSession, err := s.setFormInSession(c, LdapCreateForm{Identifier: "Default"})
+ if err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
+ return
+ }
+
+ c.HTML(http.StatusOK, "admin_create_clients.html", struct {
+ Route string
+ Alerts AlertData
+ Session SessionData
+ Static StaticData
+ Users []*ldap.UserCacheHolderEntry
+ FormData LdapCreateForm
+ Device Device
+ }{
+ Route: c.Request.URL.Path,
+ Alerts: s.getAlertData(c),
+ Session: currentSession,
+ Static: s.getStaticData(),
+ Users: s.ldapUsers.GetSortedUsers("sn", "asc"),
+ FormData: currentSession.FormData.(LdapCreateForm),
+ Device: s.users.GetDevice(),
+ })
+}
+
+func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) {
+ currentSession := s.getSessionData(c)
+ var formData LdapCreateForm
+ if currentSession.FormData != nil {
+ formData = currentSession.FormData.(LdapCreateForm)
+ }
+ if err := c.ShouldBind(&formData); err != nil {
+ _ = s.updateFormInSession(c, formData)
+ s.setAlert(c, "failed to bind form data: "+err.Error(), "danger")
+ c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=bind")
+ return
+ }
+
+ emails := common.ParseStringList(formData.Emails)
+ for i := range emails {
+ // TODO: also check email addr for validity?
+ if !strings.ContainsRune(emails[i], '@') || s.ldapUsers.GetUserDNByMail(emails[i]) == "" {
+ _ = s.updateFormInSession(c, formData)
+ s.setAlert(c, "invalid email address: "+emails[i], "danger")
+ c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=mail")
+ return
+ }
+ }
+
+ log.Infof("creating %d ldap peers", len(emails))
+
+ for i := range emails {
+ if err := s.CreateUserByEmail(emails[i], formData.Identifier, false); err != nil {
+ _ = s.updateFormInSession(c, formData)
+ s.setAlert(c, "failed to add user: "+err.Error(), "danger")
+ c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=create")
+ return
+ }
+ }
+
+ s.setAlert(c, "client(s) created successfully", "success")
+ c.Redirect(http.StatusSeeOther, "/admin/peer/createldap")
+}
+
+func (s *Server) GetAdminDeletePeer(c *gin.Context) {
+ currentUser := s.users.GetUserByKey(c.Query("pkey"))
+ if err := s.DeleteUser(currentUser); err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "Deletion error", err.Error())
+ return
+ }
+ s.setAlert(c, "user deleted successfully", "success")
+ c.Redirect(http.StatusSeeOther, "/admin")
+}
+
+func (s *Server) GetPeerQRCode(c *gin.Context) {
+ user := s.users.GetUserByKey(c.Query("pkey"))
+ currentSession := s.getSessionData(c)
+ if !currentSession.IsAdmin && user.Email != currentSession.Email {
+ s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
+ return
+ }
+
+ png, err := user.GetQRCode()
+ if err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "QRCode error", err.Error())
+ return
+ }
+ c.Data(http.StatusOK, "image/png", png)
+ return
+}
+
+func (s *Server) GetPeerConfig(c *gin.Context) {
+ user := s.users.GetUserByKey(c.Query("pkey"))
+ currentSession := s.getSessionData(c)
+ if !currentSession.IsAdmin && user.Email != currentSession.Email {
+ s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
+ return
+ }
+
+ cfg, err := user.GetClientConfigFile(s.users.GetDevice())
+ if err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
+ return
+ }
+
+ c.Header("Content-Disposition", "attachment; filename="+user.GetConfigFileName())
+ c.Data(http.StatusOK, "application/config", cfg)
+ return
+}
+
+func (s *Server) GetPeerConfigMail(c *gin.Context) {
+ user := s.users.GetUserByKey(c.Query("pkey"))
+ currentSession := s.getSessionData(c)
+ if !currentSession.IsAdmin && user.Email != currentSession.Email {
+ s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
+ return
+ }
+
+ cfg, err := user.GetClientConfigFile(s.users.GetDevice())
+ if err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
+ return
+ }
+ png, err := user.GetQRCode()
+ if err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "QRCode error", err.Error())
+ return
+ }
+ // Apply mail template
+ var tplBuff bytes.Buffer
+ if err := s.mailTpl.Execute(&tplBuff, struct {
+ Client User
+ QrcodePngName string
+ PortalUrl string
+ }{
+ Client: user,
+ QrcodePngName: "wireguard-config.png",
+ PortalUrl: s.config.Core.ExternalUrl,
+ }); err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "Template error", err.Error())
+ return
+ }
+
+ // Send mail
+ attachments := []common.MailAttachment{
+ {
+ Name: user.GetConfigFileName(),
+ ContentType: "application/config",
+ Data: bytes.NewReader(cfg),
+ },
+ {
+ Name: "wireguard-config.png",
+ ContentType: "image/png",
+ Data: bytes.NewReader(png),
+ },
+ }
+
+ if err := common.SendEmailWithAttachments(s.config.Email, s.config.Core.MailFrom, "", "WireGuard VPN Configuration",
+ "Your mail client does not support HTML. Please find the configuration attached to this mail.", tplBuff.String(),
+ []string{user.Email}, attachments); err != nil {
+ s.GetHandleError(c, http.StatusInternalServerError, "Email error", err.Error())
+ return
+ }
+
+ s.setAlert(c, "mail sent successfully", "success")
+ c.Redirect(http.StatusSeeOther, "/admin")
+}
diff --git a/internal/server/routes.go b/internal/server/routes.go
index 476d013..6da31d0 100644
--- a/internal/server/routes.go
+++ b/internal/server/routes.go
@@ -22,7 +22,7 @@ func SetupRoutes(s *Server) {
admin.GET("/", s.GetAdminIndex)
admin.GET("/device/edit", s.GetAdminEditInterface)
admin.POST("/device/edit", s.PostAdminEditInterface)
- admin.GET("/device/download", s.GetDeviceConfig)
+ admin.GET("/device/download", s.GetInterfaceConfig)
admin.GET("/peer/edit", s.GetAdminEditPeer)
admin.POST("/peer/edit", s.PostAdminEditPeer)
admin.GET("/peer/create", s.GetAdminCreatePeer)
@@ -30,16 +30,16 @@ func SetupRoutes(s *Server) {
admin.GET("/peer/createldap", s.GetAdminCreateLdapPeers)
admin.POST("/peer/createldap", s.PostAdminCreateLdapPeers)
admin.GET("/peer/delete", s.GetAdminDeletePeer)
- admin.GET("/peer/download", s.GetUserConfig)
- admin.GET("/peer/email", s.GetUserConfigMail)
+ admin.GET("/peer/download", s.GetPeerConfig)
+ admin.GET("/peer/email", s.GetPeerConfigMail)
// User routes
user := s.server.Group("/user")
user.Use(s.RequireAuthentication("")) // empty scope = all logged in users
- user.GET("/qrcode", s.GetUserQRCode)
+ user.GET("/qrcode", s.GetPeerQRCode)
user.GET("/profile", s.GetUserIndex)
- user.GET("/download", s.GetUserConfig)
- user.GET("/email", s.GetUserConfigMail)
+ user.GET("/download", s.GetPeerConfig)
+ user.GET("/email", s.GetPeerConfigMail)
}
func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
@@ -49,7 +49,7 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
if !session.LoggedIn {
// Abort the request with the appropriate error code
c.Abort()
- c.Redirect(http.StatusSeeOther, s.config.AuthRoutePrefix+"/login?err=loginreq")
+ c.Redirect(http.StatusSeeOther, "/auth/login?err=loginreq")
return
}
@@ -57,7 +57,7 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
!s.ldapUsers.IsInGroup(session.UserName, scope) {
// Abort the request with the appropriate error code
c.Abort()
- s.HandleError(c, http.StatusUnauthorized, "unauthorized", "not enough permissions")
+ s.GetHandleError(c, http.StatusUnauthorized, "unauthorized", "not enough permissions")
return
}