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" .}} -
-
-
-
-
- User Image -
-
- - - - - - -
-
-
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
-
-
- Go Back -
-
-
-
-
- {{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 @@ - PROLICHT + {{$.Static.CompanyName}} {{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 }