mirror of https://github.com/h44z/wg-portal.git
				
				
				
			user can manage own peers on default device (#82)
Co-authored-by: GitHubActionRunner <knm@knm.io>
This commit is contained in:
		
							parent
							
								
									b34d2e1174
								
							
						
					
					
						commit
						2f194884d3
					
				|  | @ -0,0 +1,44 @@ | ||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head> | ||||||
|  |     <meta charset="utf-8"> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> | ||||||
|  |     <title>{{ .Static.WebsiteTitle }} - Admin</title> | ||||||
|  |     <meta name="description" content="{{ .Static.WebsiteTitle }}"> | ||||||
|  |     <link rel="stylesheet" href="/css/bootstrap.min.css"> | ||||||
|  |     <link rel="stylesheet" href="/fonts/fontawesome-all.min.css"> | ||||||
|  |     <link rel="stylesheet" href="/css/custom.css"> | ||||||
|  | </head> | ||||||
|  | 
 | ||||||
|  | <body id="page-top" class="d-flex flex-column min-vh-100"> | ||||||
|  |     {{template "prt_nav.html" .}} | ||||||
|  |     <div class="container mt-5"> | ||||||
|  |         {{template "prt_flashes.html" .}} | ||||||
|  | 
 | ||||||
|  |         <!-- server mode --> | ||||||
|  |         <h1>Create a new client</h1> | ||||||
|  | 
 | ||||||
|  |         <form method="post" enctype="multipart/form-data"> | ||||||
|  |             <input type="hidden" name="_csrf" value="{{.Csrf}}"> | ||||||
|  |             <input type="hidden" name="uid" value="{{.Peer.UID}}"> | ||||||
|  |             <div class="form-row"> | ||||||
|  |                 <div class="form-group required col-md-12"> | ||||||
|  |                     <label for="server_PublicKey">Public Key</label> | ||||||
|  |                     <input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  | 
 | ||||||
|  |             <button type="submit" class="btn btn-primary">Save</button> | ||||||
|  |             <a href="/user/profile" class="btn btn-secondary">Cancel</a> | ||||||
|  |         </form> | ||||||
|  |     </div> | ||||||
|  |     {{template "prt_footer.html" .}} | ||||||
|  |     <script src="/js/jquery.min.js"></script> | ||||||
|  |     <script src="/js/jquery.easing.js"></script> | ||||||
|  |     <script src="/js/popper.min.js"></script> | ||||||
|  |     <script src="/js/bootstrap.bundle.min.js"></script> | ||||||
|  |     <script src="/js/bootstrap-confirmation.min.js"></script> | ||||||
|  |     <script src="/js/custom.js"></script> | ||||||
|  | </body> | ||||||
|  | 
 | ||||||
|  | </html> | ||||||
|  | @ -0,0 +1,54 @@ | ||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head> | ||||||
|  |     <meta charset="utf-8"> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> | ||||||
|  |     <title>{{ .Static.WebsiteTitle }} - Admin</title> | ||||||
|  |     <meta name="description" content="{{ .Static.WebsiteTitle }}"> | ||||||
|  |     <link rel="stylesheet" href="/css/bootstrap.min.css"> | ||||||
|  |     <link rel="stylesheet" href="/fonts/fontawesome-all.min.css"> | ||||||
|  |     <link rel="stylesheet" href="/css/custom.css"> | ||||||
|  | </head> | ||||||
|  | 
 | ||||||
|  | <body id="page-top" class="d-flex flex-column min-vh-100"> | ||||||
|  |     {{template "prt_nav.html" .}} | ||||||
|  |     <div class="container mt-5"> | ||||||
|  |         {{template "prt_flashes.html" .}} | ||||||
|  | 
 | ||||||
|  |         <!-- server mode --> | ||||||
|  |         <h1>Edit client: <strong>{{.Peer.Identifier}}</strong></h1> | ||||||
|  | 
 | ||||||
|  |         <form method="post" enctype="multipart/form-data"> | ||||||
|  |             <input type="hidden" name="_csrf" value="{{.Csrf}}"> | ||||||
|  |             <input type="hidden" name="uid"  value="{{.Peer.UID}}"> | ||||||
|  |             <div class="form-row"> | ||||||
|  |                 <div class="form-group required col-md-12"> | ||||||
|  |                     <label for="server_PublicKey">Public Key</label> | ||||||
|  |                     <input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required disabled="disabled"> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  | 
 | ||||||
|  |             <div class="form-row"> | ||||||
|  |                 <div class="form-group col-md-12"> | ||||||
|  |                     <div class="custom-control custom-switch"> | ||||||
|  |                         <input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="server_Disabled" {{if .Peer.DeactivatedAt}}disabled="disabled"{{end}} {{if .Peer.DeactivatedAt}}checked{{end}}> | ||||||
|  |                         <label class="custom-control-label" for="server_Disabled"> | ||||||
|  |                             Disabled | ||||||
|  |                         </label> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |             <button type="submit" class="btn btn-primary">Save</button> | ||||||
|  |             <a href="/user/profile" class="btn btn-secondary">Cancel</a> | ||||||
|  |         </form> | ||||||
|  |     </div> | ||||||
|  |     {{template "prt_footer.html" .}} | ||||||
|  |     <script src="/js/jquery.min.js"></script> | ||||||
|  |     <script src="/js/jquery.easing.js"></script> | ||||||
|  |     <script src="/js/popper.min.js"></script> | ||||||
|  |     <script src="/js/bootstrap.bundle.min.js"></script> | ||||||
|  |     <script src="/js/bootstrap-confirmation.min.js"></script> | ||||||
|  |     <script src="/js/custom.js"></script> | ||||||
|  | </body> | ||||||
|  | 
 | ||||||
|  | </html> | ||||||
|  | @ -15,7 +15,16 @@ | ||||||
|     <div class="container mt-5"> |     <div class="container mt-5"> | ||||||
|         <h1>WireGuard VPN User-Portal</h1> |         <h1>WireGuard VPN User-Portal</h1> | ||||||
| 
 | 
 | ||||||
|         <h2 class="mt-4">Your VPN Profiles</h2> |         <div class="mt-4 row"> | ||||||
|  |             <div class="col-sm-8 col-12">         | ||||||
|  |                 <h2 class="mt-2">Your VPN Profiles</h2> | ||||||
|  |             </div> | ||||||
|  |             <div class="col-sm-4 col-12 text-right"> | ||||||
|  |                 {{if eq $.UserManagePeers true}} | ||||||
|  |                 <a href="/user/peer/create" title="Add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-user"></i></a> | ||||||
|  |                 {{end}} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|         <div class="mt-2 table-responsive"> |         <div class="mt-2 table-responsive"> | ||||||
|             <table class="table table-sm" id="userTable"> |             <table class="table table-sm" id="userTable"> | ||||||
|                 <thead> |                 <thead> | ||||||
|  | @ -26,6 +35,9 @@ | ||||||
|                     <th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "mail"}}"></i></a></th> |                     <th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "mail"}}"></i></a></th> | ||||||
|                     <th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "ip"}}"></i></a></th> |                     <th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "ip"}}"></i></a></th> | ||||||
|                     <th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "handshake"}}"></i></a></th> |                     <th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "handshake"}}"></i></a></th> | ||||||
|  |                     {{if eq $.UserManagePeers true}} | ||||||
|  |                     <th scope="col"></th> | ||||||
|  |                     {{end}} | ||||||
|                 </tr> |                 </tr> | ||||||
|                 </thead> |                 </thead> | ||||||
|                 <tbody> |                 <tbody> | ||||||
|  | @ -42,6 +54,11 @@ | ||||||
|                         <td>{{$p.Email}}</td> |                         <td>{{$p.Email}}</td> | ||||||
|                         <td>{{$p.IPsStr}}</td> |                         <td>{{$p.IPsStr}}</td> | ||||||
|                         <td><span data-toggle="tooltip" data-placement="left" title="" data-original-title="{{$p.LastHandshakeTime}}">{{$p.LastHandshake}}</span></td> |                         <td><span data-toggle="tooltip" data-placement="left" title="" data-original-title="{{$p.LastHandshakeTime}}">{{$p.LastHandshake}}</span></td> | ||||||
|  |                         {{if eq $.UserManagePeers true}} | ||||||
|  |                         <td> | ||||||
|  |                             <a href="/user/peer/edit?pkey={{$p.PublicKey}}" title="Edit peer"><i class="fas fa-cog"></i></a> | ||||||
|  |                         </td> | ||||||
|  |                         {{end}} | ||||||
|                     </tr> |                     </tr> | ||||||
|                     <tr class="hiddenRow"> |                     <tr class="hiddenRow"> | ||||||
|                         <td colspan="6" class="hiddenCell" style="white-space:nowrap"> |                         <td colspan="6" class="hiddenCell" style="white-space:nowrap"> | ||||||
|  |  | ||||||
|  | @ -116,6 +116,7 @@ func NewConfig() *Config { | ||||||
| 	cfg.WG.DefaultDeviceName = "wg0" | 	cfg.WG.DefaultDeviceName = "wg0" | ||||||
| 	cfg.WG.ConfigDirectoryPath = "/etc/wireguard" | 	cfg.WG.ConfigDirectoryPath = "/etc/wireguard" | ||||||
| 	cfg.WG.ManageIPAddresses = true | 	cfg.WG.ManageIPAddresses = true | ||||||
|  | 	cfg.WG.UserManagePeers = false | ||||||
| 	cfg.Email.Host = "127.0.0.1" | 	cfg.Email.Host = "127.0.0.1" | ||||||
| 	cfg.Email.Port = 25 | 	cfg.Email.Port = 25 | ||||||
| 	cfg.Email.Encryption = common.MailEncryptionNone | 	cfg.Email.Encryption = common.MailEncryptionNone | ||||||
|  |  | ||||||
|  | @ -135,15 +135,16 @@ func (s *Server) GetUserIndex(c *gin.Context) { | ||||||
| 	peers := s.peers.GetSortedPeersForEmail(currentSession.SortedBy["userpeers"], currentSession.SortDirection["userpeers"], currentSession.Email) | 	peers := s.peers.GetSortedPeersForEmail(currentSession.SortedBy["userpeers"], currentSession.SortDirection["userpeers"], currentSession.Email) | ||||||
| 
 | 
 | ||||||
| 	c.HTML(http.StatusOK, "user_index.html", gin.H{ | 	c.HTML(http.StatusOK, "user_index.html", gin.H{ | ||||||
| 		"Route":       c.Request.URL.Path, | 		"Route":       			c.Request.URL.Path, | ||||||
| 		"Alerts":      GetFlashes(c), | 		"Alerts":      			GetFlashes(c), | ||||||
| 		"Session":     currentSession, | 		"Session":     			currentSession, | ||||||
| 		"Static":      s.getStaticData(), | 		"Static":      			s.getStaticData(), | ||||||
| 		"Peers":       peers, | 		"Peers":       			peers, | ||||||
| 		"TotalPeers":  len(peers), | 		"TotalPeers":  			len(peers), | ||||||
| 		"Users":       []users.User{*s.users.GetUser(currentSession.Email)}, | 		"Users":       			[]users.User{*s.users.GetUser(currentSession.Email)}, | ||||||
| 		"Device":      s.peers.GetDevice(currentSession.DeviceName), | 		"Device":      			s.peers.GetDevice(currentSession.DeviceName), | ||||||
| 		"DeviceNames": s.GetDeviceNames(), | 		"DeviceNames": 			s.GetDeviceNames(), | ||||||
|  | 		"UserManagePeers": 	s.config.WG.UserManagePeers, | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -392,3 +392,117 @@ func (s *Server) sendPeerConfigMail(peer wireguard.Peer) error { | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (s *Server) GetUserCreatePeer(c *gin.Context) { | ||||||
|  | 	currentSession, err := s.setNewPeerFormInSession(c) | ||||||
|  | 	if err != nil { | ||||||
|  | 		s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	c.HTML(http.StatusOK, "user_create_client.html", gin.H{ | ||||||
|  | 		"Route":        c.Request.URL.Path, | ||||||
|  | 		"Alerts":       GetFlashes(c), | ||||||
|  | 		"Session":      currentSession, | ||||||
|  | 		"Static":       s.getStaticData(), | ||||||
|  | 		"Peer":         currentSession.FormData.(wireguard.Peer), | ||||||
|  | 		"EditableKeys": s.config.Core.EditableKeys, | ||||||
|  | 		"Device":       s.peers.GetDevice(currentSession.DeviceName), | ||||||
|  | 		"DeviceNames":  s.GetDeviceNames(), | ||||||
|  | 		"AdminEmail":   s.config.Core.AdminUser, | ||||||
|  | 		"Csrf":         csrf.GetToken(c), | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Server) PostUserCreatePeer(c *gin.Context) { | ||||||
|  | 	currentSession := GetSessionData(c) | ||||||
|  | 	var formPeer wireguard.Peer | ||||||
|  | 	if currentSession.FormData != nil { | ||||||
|  | 		formPeer = currentSession.FormData.(wireguard.Peer) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	formPeer.Email = currentSession.Email; | ||||||
|  | 	formPeer.Identifier = currentSession.Email; | ||||||
|  | 	formPeer.DeviceType = wireguard.DeviceTypeServer; | ||||||
|  |   formPeer.PrivateKey = ""; | ||||||
|  | 	 | ||||||
|  | 	if err := c.ShouldBind(&formPeer); err != nil { | ||||||
|  | 		_ = s.updateFormInSession(c, formPeer) | ||||||
|  | 		SetFlashMessage(c, "failed to bind form data: "+err.Error(), "danger") | ||||||
|  | 		c.Redirect(http.StatusSeeOther, "/user/peer/create?formerr=bind") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	disabled := c.PostForm("isdisabled") != "" | ||||||
|  | 	now := time.Now() | ||||||
|  | 	if disabled { | ||||||
|  | 		formPeer.DeactivatedAt = &now | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil { | ||||||
|  | 		_ = s.updateFormInSession(c, formPeer) | ||||||
|  | 		SetFlashMessage(c, "failed to add user: "+err.Error(), "danger") | ||||||
|  | 		c.Redirect(http.StatusSeeOther, "/user/peer/create?formerr=create") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	SetFlashMessage(c, "client created successfully", "success") | ||||||
|  | 	c.Redirect(http.StatusSeeOther, "/user/profile") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Server) GetUserEditPeer(c *gin.Context) { | ||||||
|  | 	peer := s.peers.GetPeerByKey(c.Query("pkey")) | ||||||
|  | 
 | ||||||
|  | 	 | ||||||
|  | 	currentSession, err := s.setFormInSession(c, peer) | ||||||
|  | 	if err != nil { | ||||||
|  | 		s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if peer.Email != currentSession.Email { | ||||||
|  | 		s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	c.HTML(http.StatusOK, "user_edit_client.html", gin.H{ | ||||||
|  | 		"Route":        c.Request.URL.Path, | ||||||
|  | 		"Alerts":       GetFlashes(c), | ||||||
|  | 		"Session":      currentSession, | ||||||
|  | 		"Static":       s.getStaticData(), | ||||||
|  | 		"Peer":         currentSession.FormData.(wireguard.Peer), | ||||||
|  | 		"EditableKeys": s.config.Core.EditableKeys, | ||||||
|  | 		"Device":       s.peers.GetDevice(currentSession.DeviceName), | ||||||
|  | 		"DeviceNames":  s.GetDeviceNames(), | ||||||
|  | 		"AdminEmail":   s.config.Core.AdminUser, | ||||||
|  | 		"Csrf":         csrf.GetToken(c), | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Server) PostUserEditPeer(c *gin.Context) { | ||||||
|  | 	currentPeer := s.peers.GetPeerByKey(c.Query("pkey")) | ||||||
|  | 	urlEncodedKey := url.QueryEscape(c.Query("pkey")) | ||||||
|  | 
 | ||||||
|  | 	currentSession := GetSessionData(c) | ||||||
|  | 
 | ||||||
|  | 	if currentPeer.Email != currentSession.Email { | ||||||
|  | 		s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	disabled := c.PostForm("isdisabled") != "" | ||||||
|  | 	now := time.Now() | ||||||
|  | 	if disabled && currentPeer.DeactivatedAt == nil { | ||||||
|  | 		currentPeer.DeactivatedAt = &now | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	// Update in database
 | ||||||
|  | 	if err := s.UpdatePeer(currentPeer, now); err != nil { | ||||||
|  | 		_ = s.updateFormInSession(c, currentPeer) | ||||||
|  | 		SetFlashMessage(c, "failed to update user: "+err.Error(), "danger") | ||||||
|  | 		c.Redirect(http.StatusSeeOther, "/user/peer/edit?pkey="+urlEncodedKey+"&formerr=update") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	SetFlashMessage(c, "changes applied successfully", "success") | ||||||
|  | 	c.Redirect(http.StatusSeeOther, "/user/peer/edit?pkey="+urlEncodedKey) | ||||||
|  | } | ||||||
|  | @ -76,6 +76,13 @@ func SetupRoutes(s *Server) { | ||||||
| 	user.GET("/download", s.GetPeerConfig) | 	user.GET("/download", s.GetPeerConfig) | ||||||
| 	user.GET("/email", s.GetPeerConfigMail) | 	user.GET("/email", s.GetPeerConfigMail) | ||||||
| 	user.GET("/status", s.GetPeerStatus) | 	user.GET("/status", s.GetPeerStatus) | ||||||
|  | 
 | ||||||
|  | 	if s.config.WG.UserManagePeers { | ||||||
|  | 		user.GET("/peer/create", s.GetUserCreatePeer) | ||||||
|  | 		user.POST("/peer/create", s.PostUserCreatePeer) | ||||||
|  | 		user.GET("/peer/edit", s.GetUserEditPeer) | ||||||
|  | 		user.POST("/peer/edit", s.PostUserEditPeer) | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func SetupApiRoutes(s *Server) { | func SetupApiRoutes(s *Server) { | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ type Config struct { | ||||||
| 	DefaultDeviceName   string   `yaml:"defaultDevice" envconfig:"WG_DEFAULT_DEVICE"` // this device is used for auto-created peers, use GetDefaultDeviceName() to access this field
 | 	DefaultDeviceName   string   `yaml:"defaultDevice" envconfig:"WG_DEFAULT_DEVICE"` // this device is used for auto-created peers, use GetDefaultDeviceName() to access this field
 | ||||||
| 	ConfigDirectoryPath string   `yaml:"configDirectory" envconfig:"WG_CONFIG_PATH"`  // optional, if set, updates will be written to this path, filename: <devicename>.conf
 | 	ConfigDirectoryPath string   `yaml:"configDirectory" envconfig:"WG_CONFIG_PATH"`  // optional, if set, updates will be written to this path, filename: <devicename>.conf
 | ||||||
| 	ManageIPAddresses   bool     `yaml:"manageIPAddresses" envconfig:"MANAGE_IPS"`    // handle ip-address setup of interface
 | 	ManageIPAddresses   bool     `yaml:"manageIPAddresses" envconfig:"MANAGE_IPS"`    // handle ip-address setup of interface
 | ||||||
|  | 	UserManagePeers     bool     `yaml:"userManagePeers" envconfig:"USER_MANAGE_PEERS"`  // user can manage own peers
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c Config) GetDefaultDeviceName() string { | func (c Config) GetDefaultDeviceName() string { | ||||||
|  |  | ||||||
|  | @ -5,7 +5,8 @@ | ||||||
| [Interface] | [Interface] | ||||||
| 
 | 
 | ||||||
| # Core settings | # Core settings | ||||||
| PrivateKey = {{ .Peer.PrivateKey }} | 
 | ||||||
|  | PrivateKey = {{or .Peer.PrivateKey "<please-insert-your-private-key>" }} | ||||||
| Address = {{ .Peer.IPsStr }} | Address = {{ .Peer.IPsStr }} | ||||||
| 
 | 
 | ||||||
| # Misc. settings (optional) | # Misc. settings (optional) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue