diff --git a/assets/tpl/email.html b/assets/tpl/email.html index 4231e48..853d195 100644 --- a/assets/tpl/email.html +++ b/assets/tpl/email.html @@ -170,7 +170,7 @@ This mail was generated using WireGuard Portal. - Visit WireGuard Portal + Visit WireGuard Portal diff --git a/assets/tpl/error.html b/assets/tpl/error.html index 033ced3..a9a76d0 100644 --- a/assets/tpl/error.html +++ b/assets/tpl/error.html @@ -1,12 +1,10 @@ - - - {{ .static.WebsiteTitle }} - Error - + {{ .Static.WebsiteTitle }} - Error + @@ -17,16 +15,16 @@ {{template "prt_nav.html" .}}
-
-

{{.data.Code}}

+
+

{{.Data.Code}}

-

{{.data.Message}}

-

{{.data.Details}}

← Back to Dashboard +

{{.Data.Message}}

+

{{.Data.Details}}

← Back to Dashboard
{{template "prt_footer.html"}} - + diff --git a/assets/tpl/index.html b/assets/tpl/index.html index da1734f..ca90f8c 100644 --- a/assets/tpl/index.html +++ b/assets/tpl/index.html @@ -18,7 +18,13 @@ -

Please note that this page is only intended for internal use!

+

WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN.

+ +

VPN Profiles and configuration

+

You can access your personal VPN configurations via your Userprofile: Open Userprofile

+ +

Client Software

+

Installation instructions for client software can be found on the official WireGuard website: https://www.wireguard.com/

{{template "prt_footer.html"}} diff --git a/assets/tpl/prt_nav.html b/assets/tpl/prt_nav.html index 1ff1697..dd83a5a 100644 --- a/assets/tpl/prt_nav.html +++ b/assets/tpl/prt_nav.html @@ -22,7 +22,7 @@ Administration {{end}}{{end}} - Profile + Profile Logout diff --git a/assets/tpl/user_index.html b/assets/tpl/user_index.html new file mode 100644 index 0000000..4fb3173 --- /dev/null +++ b/assets/tpl/user_index.html @@ -0,0 +1,110 @@ + + + + + + {{ .Static.WebsiteTitle }} - Profile + + + + + + + + + {{template "prt_nav.html" .}} +
+

WireGuard VPN User-Portal

+ +

Your VPN Profiles

+
+ + + + + + + + + + + + + {{range $i, $p :=.Peers}} + + + + + + + + + + + + {{end}} + +
Identifier Public Key E-Mail IP's Handshake
+ + + {{$p.Identifier}}{{$p.PublicKey}}{{$p.Email}}{{$p.IPsStr}}{{$p.LastHandshake}}
+
+
+
+ +
+
+

User details

+ {{if not $p.LdapUser}} +

No LDAP user-information available...

+ {{else}} +
    +
  • Firstname: {{$p.LdapUser.Firstname}}
  • +
  • Lastname: {{$p.LdapUser.Lastname}}
  • +
  • Phone: {{$p.UID}}
  • +
  • Mail: {{$p.LdapUser.Mail}}
  • +
  • Department: {{$p.UID}}
  • +
+ {{end}} +

Traffic

+ {{if not $p.Peer}} +

No Traffic data available...

+ {{else}} +

{{if $p.DeactivatedAt}}-{{else}}{{$p.Peer.ReceiveBytes}} / {{$p.Peer.TransmitBytes}}{{end}}

+ {{end}} +
+
+
{{$p.Config}}
+
+
+
+
+ +
+
+
+ Download + Email +
+
+
+
+
+

Currently listed peers: {{len .Peers}}

+
+
+ {{template "prt_footer.html"}} + + + + + + + \ No newline at end of file diff --git a/internal/server/core.go b/internal/server/core.go index 959b054..5dffe49 100644 --- a/internal/server/core.go +++ b/internal/server/core.go @@ -38,6 +38,7 @@ type SessionData struct { UserName string Firstname string Lastname string + Email string SortedBy string SortDirection string Search string @@ -109,7 +110,7 @@ func (s *Server) Setup() error { log.Infof("Real working directory: %s", rDir) log.Infof("Current working directory: %s", dir) var err error - s.mailTpl, err = template.New("email").ParseGlob(filepath.Join(dir, "/assets/tpl/email.html")) + s.mailTpl, err = template.New("email.html").ParseFiles(filepath.Join(dir, "/assets/tpl/email.html")) if err != nil { return errors.New("unable to pare mail template") } @@ -178,6 +179,7 @@ func (s *Server) getSessionData(c *gin.Context) SessionData { sessionData = SessionData{ SortedBy: "mail", SortDirection: "asc", + Email: "", Firstname: "", Lastname: "", IsAdmin: false, diff --git a/internal/server/handlers.go b/internal/server/handlers.go index b6af109..d62ff82 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -43,14 +43,14 @@ func (s *Server) HandleError(c *gin.Context, code int, message, details string) //c.JSON(code, gin.H{"error": message, "details": details}) c.HTML(code, "error.html", gin.H{ - "data": 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(), + "Route": c.Request.URL.Path, + "Session": s.getSessionData(c), + "Static": s.getStaticData(), }) } @@ -112,6 +112,52 @@ func (s *Server) GetAdminIndex(c *gin.Context) { }) } +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() @@ -388,6 +434,12 @@ func (s *Server) GetAdminDeletePeer(c *gin.Context) { 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()) @@ -399,6 +451,12 @@ func (s *Server) GetUserQRCode(c *gin.Context) { 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()) @@ -412,6 +470,12 @@ func (s *Server) GetUserConfig(c *gin.Context) { 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()) @@ -427,9 +491,11 @@ func (s *Server) GetUserConfigMail(c *gin.Context) { 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 diff --git a/internal/server/handlers_auth.go b/internal/server/handlers_auth.go index 55b4b44..65a951d 100644 --- a/internal/server/handlers_auth.go +++ b/internal/server/handlers_auth.go @@ -48,29 +48,51 @@ func (s *Server) PostLogin(c *gin.Context) { return } + adminAuthenticated := false + if s.config.Core.AdminUser != "" && username == s.config.Core.AdminUser && password == s.config.Core.AdminPassword { + adminAuthenticated = true + } + // Check if user is in cache, avoid unnecessary ldap requests - if !s.ldapUsers.UserExists(username) { + if !adminAuthenticated && !s.ldapUsers.UserExists(username) { c.Redirect(http.StatusSeeOther, s.config.AuthRoutePrefix+"/login?err=authfail") } // Check if username and password match - if !s.ldapAuth.CheckLogin(username, password) { + if !adminAuthenticated && !s.ldapAuth.CheckLogin(username, password) { c.Redirect(http.StatusSeeOther, s.config.AuthRoutePrefix+"/login?err=authfail") return } - dn := s.ldapUsers.GetUserDN(username) - userData := s.ldapUsers.GetUserData(dn) - sessionData := SessionData{ - LoggedIn: true, - IsAdmin: s.ldapUsers.IsInGroup(username, s.config.AdminLdapGroup), - UID: userData.GetUID(), - UserName: username, - Firstname: userData.Firstname, - Lastname: userData.Lastname, - SortedBy: "mail", - SortDirection: "asc", - Search: "", + var sessionData SessionData + if adminAuthenticated { + sessionData = SessionData{ + LoggedIn: true, + IsAdmin: true, + Email: "autodetected@example.com", + UID: "adminuid", + UserName: username, + Firstname: "System", + Lastname: "Administrator", + SortedBy: "mail", + SortDirection: "asc", + Search: "", + } + } else { + dn := s.ldapUsers.GetUserDN(username) + userData := s.ldapUsers.GetUserData(dn) + sessionData = SessionData{ + LoggedIn: true, + IsAdmin: s.ldapUsers.IsInGroup(username, s.config.AdminLdapGroup), + UID: userData.GetUID(), + UserName: username, + Email: userData.Mail, + Firstname: userData.Firstname, + Lastname: userData.Lastname, + SortedBy: "mail", + SortDirection: "asc", + Search: "", + } } if err := s.updateSessionData(c, sessionData); err != nil { diff --git a/internal/server/helper.go b/internal/server/helper.go index ac45e62..32d9b50 100644 --- a/internal/server/helper.go +++ b/internal/server/helper.go @@ -51,7 +51,15 @@ func (s *Server) CreateUserByEmail(email, identifierSuffix string, disabled bool device := s.users.GetDevice() user := User{} user.AllowedIPsStr = device.AllowedIPsStr - user.IPsStr = "" // TODO: add a valid ip here + user.IPs = make([]string, len(device.IPs)) + for i := range device.IPs { + freeIP, err := s.users.GetAvailableIp(device.IPs[i]) + if err != nil { + return err + } + user.IPs[i] = freeIP + } + user.IPsStr = common.ListToString(user.IPs) psk, err := wgtypes.GenerateKey() if err != nil { return err @@ -78,7 +86,16 @@ func (s *Server) CreateUser(user User) error { device := s.users.GetDevice() user.AllowedIPsStr = device.AllowedIPsStr - user.IPsStr = "" // TODO: add a valid ip here + if len(user.IPs) == 0 { + for i := range device.IPs { + freeIP, err := s.users.GetAvailableIp(device.IPs[i]) + if err != nil { + return err + } + user.IPs[i] = freeIP + } + user.IPsStr = common.ListToString(user.IPs) + } if user.PrivateKey == "" { // if private key is empty create a new one psk, err := wgtypes.GenerateKey() if err != nil { @@ -94,7 +111,7 @@ func (s *Server) CreateUser(user User) error { } user.UID = fmt.Sprintf("u%x", md5.Sum([]byte(user.PublicKey))) - // Create wireguard interface + // Create WireGuard interface if user.DeactivatedAt == nil { if err := s.wg.AddPeer(user.GetPeerConfig()); err != nil { return err diff --git a/internal/server/routes.go b/internal/server/routes.go index c817d6b..476d013 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -37,6 +37,9 @@ func SetupRoutes(s *Server) { user := s.server.Group("/user") user.Use(s.RequireAuthentication("")) // empty scope = all logged in users user.GET("/qrcode", s.GetUserQRCode) + user.GET("/profile", s.GetUserIndex) + user.GET("/download", s.GetUserConfig) + user.GET("/email", s.GetUserConfigMail) } func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc { @@ -50,7 +53,7 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc { return } - if scope != "" && !s.ldapUsers.IsInGroup(session.UserName, s.config.AdminLdapGroup) && // admins always have access + if scope != "" && !session.IsAdmin && // admins always have access !s.ldapUsers.IsInGroup(session.UserName, scope) { // Abort the request with the appropriate error code c.Abort() diff --git a/internal/server/usermanager.go b/internal/server/usermanager.go index 6f833d1..a11cbb9 100644 --- a/internal/server/usermanager.go +++ b/internal/server/usermanager.go @@ -477,9 +477,9 @@ func (u *UserManager) GetFilteredAndSortedUsers(sortKey, sortDirection, search s sortValueRight = filteredUsers[j].IPsStr case "handshake": if filteredUsers[i].Peer == nil { - return true - } else if filteredUsers[j].Peer == nil { return false + } else if filteredUsers[j].Peer == nil { + return true } sortValueLeft = filteredUsers[i].Peer.LastHandshakeTime.Format(time.RFC3339) sortValueRight = filteredUsers[j].Peer.LastHandshakeTime.Format(time.RFC3339) @@ -495,6 +495,51 @@ func (u *UserManager) GetFilteredAndSortedUsers(sortKey, sortDirection, search s return filteredUsers } +func (u *UserManager) GetSortedUsersForEmail(sortKey, sortDirection, email string) []User { + users := make([]User, 0) + u.db.Where("email = ?", email).Find(&users) + + for i := range users { + u.populateUserData(&users[i]) + } + + sort.Slice(users, func(i, j int) bool { + var sortValueLeft string + var sortValueRight string + + switch sortKey { + case "id": + sortValueLeft = users[i].Identifier + sortValueRight = users[j].Identifier + case "pubKey": + sortValueLeft = users[i].PublicKey + sortValueRight = users[j].PublicKey + case "mail": + sortValueLeft = users[i].Email + sortValueRight = users[j].Email + case "ip": + sortValueLeft = users[i].IPsStr + sortValueRight = users[j].IPsStr + case "handshake": + if users[i].Peer == nil { + return true + } else if users[j].Peer == nil { + return false + } + sortValueLeft = users[i].Peer.LastHandshakeTime.Format(time.RFC3339) + sortValueRight = users[j].Peer.LastHandshakeTime.Format(time.RFC3339) + } + + if sortDirection == "asc" { + return sortValueLeft < sortValueRight + } else { + return sortValueLeft > sortValueRight + } + }) + + return users +} + func (u *UserManager) GetDevice() Device { devices := make([]Device, 0, 1) u.db.Find(&devices) @@ -513,12 +558,14 @@ func (u *UserManager) GetUserByKey(publicKey string) User { return user } -func (u *UserManager) GetUserByMail(mail string) User { - user := User{} - u.db.Where("email = ?", mail).FirstOrInit(&user) - u.populateUserData(&user) +func (u *UserManager) GetUsersByMail(mail string) []User { + var users []User + u.db.Where("email = ?", mail).Find(&users) + for i := range users { + u.populateUserData(&users[i]) + } - return user + return users } func (u *UserManager) CreateUser(user User) error { diff --git a/internal/wireguard/template.go b/internal/wireguard/template.go index ef19515..f076678 100644 --- a/internal/wireguard/template.go +++ b/internal/wireguard/template.go @@ -1,192 +1,6 @@ package wireguard var ( - emailTpl = ` - - - - - - - - - - - - - - - Email Template - - - - - - - - - - -
- - - - -
- - - - - - -
- - - - -
- - - - - - -
- - - - -
-
- - - - - - - -
Hello
You probably requested VPN configuration. Here is {{.Client.Name}} configuration created {{.Client.Created.Format "Monday, 02 January 06 15:04:05 MST"}}. Scan the Qrcode or open attached configuration file in VPN client.
-
-
-
- - - - - - - -
- - - - -
- - - - -
- - - - - - - - - - - - -
About WireGuard
WireGuard is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN.
- - - - -
Download WireGuard VPN Client
-
-
-
-
- - - - - - - -
- - - - - - - -
Wg Gen Web - Simple Web based configuration generator for WireGuard
More info on Github
-
- -
-
- - -` - ClientCfgTpl = `[Interface] #{{ .Client.Identifier }} Address = {{ .Client.IPsStr }}