mirror of https://github.com/h44z/wg-portal.git
				
				
				
			wip: many small fixes and improvements...
This commit is contained in:
		
							parent
							
								
									e8e8d08d98
								
							
						
					
					
						commit
						c9a9c5b393
					
				|  | @ -170,7 +170,7 @@ | ||||||
|                                             <td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated using WireGuard Portal.</td> |                                             <td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated using WireGuard Portal.</td> | ||||||
|                                         </tr> |                                         </tr> | ||||||
|                                         <tr> |                                         <tr> | ||||||
|                                             <td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{.PortalURL}}" target="_blank" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit WireGuard Portal</span></a></td> |                                             <td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{.PortalUrl}}" target="_blank" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit WireGuard Portal</span></a></td> | ||||||
|                                         </tr> |                                         </tr> | ||||||
|                                     </table> |                                     </table> | ||||||
|                                 </td> |                                 </td> | ||||||
|  |  | ||||||
|  | @ -1,12 +1,10 @@ | ||||||
| <!DOCTYPE html> | <!DOCTYPE html> | ||||||
| <html lang="en"> | <html lang="en"> | ||||||
| <!-- http://paintstrap.com/preview_by_id/27826?design=large --> |  | ||||||
| <!-- http://www.colourlovers.com/palette/4657935 --> |  | ||||||
| <head> | <head> | ||||||
|     <meta charset="utf-8"> |     <meta charset="utf-8"> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> |     <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> | ||||||
|     <title>{{ .static.WebsiteTitle }} - Error</title> |     <title>{{ .Static.WebsiteTitle }} - Error</title> | ||||||
|     <meta name="description" content="{{ .static.WebsiteTitle }}"> |     <meta name="description" content="{{ .Static.WebsiteTitle }}"> | ||||||
|     <link rel="stylesheet" href="/css/bootstrap.min.css"> |     <link rel="stylesheet" href="/css/bootstrap.min.css"> | ||||||
|     <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"> |     <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"> | ||||||
|     <link rel="stylesheet" href="/fonts/fontawesome-all.min.css"> |     <link rel="stylesheet" href="/fonts/fontawesome-all.min.css"> | ||||||
|  | @ -17,16 +15,16 @@ | ||||||
| {{template "prt_nav.html" .}} | {{template "prt_nav.html" .}} | ||||||
| <div class="container"> | <div class="container"> | ||||||
|     <div class="text-center mt-5"> |     <div class="text-center mt-5"> | ||||||
|         <div class="error mx-auto" data-text="{{.data.Code}}"> |         <div class="error mx-auto" data-text="{{.Data.Code}}"> | ||||||
|             <p class="m-0">{{.data.Code}}</p> |             <p class="m-0">{{.Data.Code}}</p> | ||||||
|         </div> |         </div> | ||||||
|         <p class="text-dark mb-5 lead">{{.data.Message}}</p> |         <p class="text-dark mb-5 lead">{{.Data.Message}}</p> | ||||||
|         <p class="text-black-50 mb-0">{{.data.Details}}</p><a href="/">← Back to Dashboard</a> |         <p class="text-black-50 mb-0">{{.Data.Details}}</p><a href="/">← Back to Dashboard</a> | ||||||
|     </div> |     </div> | ||||||
| </div> | </div> | ||||||
| {{template "prt_footer.html"}} | {{template "prt_footer.html"}} | ||||||
| <script src="/js/jquery.min.js"></script> | <script src="/js/jquery.min.js"></script> | ||||||
| <script src="/js/bootstrap.min.js"></script> | <script src="/js/bootstrap.bundle.min.js"></script> | ||||||
| <script src="/js/jquery.easing.js"></script> | <script src="/js/jquery.easing.js"></script> | ||||||
| <script src="/js/custom.js"></script> | <script src="/js/custom.js"></script> | ||||||
| </body> | </body> | ||||||
|  |  | ||||||
|  | @ -18,7 +18,13 @@ | ||||||
|         <div class="page-header"> |         <div class="page-header"> | ||||||
|             <h1>WireGuard VPN Portal</h1> |             <h1>WireGuard VPN Portal</h1> | ||||||
|         </div> |         </div> | ||||||
|         <p class="lead">Please note that this page is only intended for internal use!</p> |         <p class="lead">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. </p> | ||||||
|  | 
 | ||||||
|  |         <h3>VPN Profiles and configuration</h3> | ||||||
|  |         <p>You can access your personal VPN configurations via your Userprofile: <a href="/user/profile" class="btn btn-primary" title="User-Profile">Open Userprofile</a></p> | ||||||
|  | 
 | ||||||
|  |         <h3>Client Software</h3> | ||||||
|  |         <p>Installation instructions for client software can be found on the official WireGuard website: <a href="https://www.wireguard.com/install/" title="WireGuard" target="_blank">https://www.wireguard.com/</a> </p> | ||||||
|     </div> |     </div> | ||||||
|     {{template "prt_footer.html"}} |     {{template "prt_footer.html"}} | ||||||
|     <script src="/js/jquery.min.js"></script> |     <script src="/js/jquery.min.js"></script> | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ | ||||||
|                         <a class="dropdown-item" href="/admin/"><i class="fas fa-file-export"></i> Administration</a> |                         <a class="dropdown-item" href="/admin/"><i class="fas fa-file-export"></i> Administration</a> | ||||||
|                         <div class="dropdown-divider"></div> |                         <div class="dropdown-divider"></div> | ||||||
|                     {{end}}{{end}} |                     {{end}}{{end}} | ||||||
|                     <a class="dropdown-item" href="/user/{{$.Session.UserName}}/profile"><i class="fas fa-user"></i> Profile</a> |                     <a class="dropdown-item" href="/user/profile"><i class="fas fa-user"></i> Profile</a> | ||||||
|                     <div class="dropdown-divider"></div> |                     <div class="dropdown-divider"></div> | ||||||
|                     <a class="dropdown-item" href="{{ $.Static.LogoutURL }}"><i class="fas fa-sign-out-alt"></i> Logout</a> |                     <a class="dropdown-item" href="{{ $.Static.LogoutURL }}"><i class="fas fa-sign-out-alt"></i> Logout</a> | ||||||
|                 </div> |                 </div> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,110 @@ | ||||||
|  | <!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 }} - Profile</title> | ||||||
|  |     <meta name="description" content="{{ .Static.WebsiteTitle }}"> | ||||||
|  |     <link rel="stylesheet" href="/css/bootstrap.min.css"> | ||||||
|  |     <!--link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"--> | ||||||
|  |     <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"> | ||||||
|  |         <h1>WireGuard VPN User-Portal</h1> | ||||||
|  | 
 | ||||||
|  |         <h2 class="mt-4">Your VPN Profiles</h2> | ||||||
|  |         <div class="mt-2 table-responsive"> | ||||||
|  |             <table class="table table-sm" id="userTable"> | ||||||
|  |                 <thead> | ||||||
|  |                 <tr> | ||||||
|  |                     <th scope="col" class="list-image-cell"></th><!-- Status and expand --> | ||||||
|  |                     <th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "id"}}"></i></a></th> | ||||||
|  |                     <th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "pubKey"}}"></i></a></th> | ||||||
|  |                     <th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "mail"}}"></i></a></th> | ||||||
|  |                     <th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "ip"}}"></i></a></th> | ||||||
|  |                     <th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "handshake"}}"></i></a></th> | ||||||
|  |                 </tr> | ||||||
|  |                 </thead> | ||||||
|  |                 <tbody> | ||||||
|  |                 {{range $i, $p :=.Peers}} | ||||||
|  |                     <tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}> | ||||||
|  |                         <th scope="row" class="list-image-cell"> | ||||||
|  |                             <a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a> | ||||||
|  |                             <!-- online check --> | ||||||
|  |                         </th> | ||||||
|  |                         <td>{{$p.Identifier}}</td> | ||||||
|  |                         <td>{{$p.PublicKey}}</td> | ||||||
|  |                         <td>{{$p.Email}}</td> | ||||||
|  |                         <td>{{$p.IPsStr}}</td> | ||||||
|  |                         <td><span data-toggle="tooltip" data-placement="left" title="" data-original-title="{{$p.LastHandshakeTime}}">{{$p.LastHandshake}}</span></td> | ||||||
|  |                     </tr> | ||||||
|  |                     <tr class="hiddenRow"> | ||||||
|  |                         <td colspan="6" class="hiddenCell" style="white-space:nowrap"> | ||||||
|  |                             <div class="collapse" id="{{$p.UID}}" data-parent="#userTable"> | ||||||
|  |                                 <div class="row collapsedRow"> | ||||||
|  |                                     <div class="col-md-6 leftBorder"> | ||||||
|  |                                         <ul class="nav nav-tabs"> | ||||||
|  |                                             <li class="nav-item"> | ||||||
|  |                                                 <a class="nav-link active" data-toggle="tab" href="#t1{{$p.UID}}">Personal</a> | ||||||
|  |                                             </li> | ||||||
|  |                                             <li class="nav-item"> | ||||||
|  |                                                 <a class="nav-link" data-toggle="tab" href="#t2{{$p.UID}}">Configuration</a> | ||||||
|  |                                             </li> | ||||||
|  |                                         </ul> | ||||||
|  |                                         <div class="tab-content" id="tabContent{{$p.UID}}"> | ||||||
|  |                                             <div id="t1{{$p.UID}}" class="tab-pane fade active show"> | ||||||
|  |                                                 <h4>User details</h4> | ||||||
|  |                                                 {{if not $p.LdapUser}} | ||||||
|  |                                                     <p>No LDAP user-information available...</p> | ||||||
|  |                                                 {{else}} | ||||||
|  |                                                     <ul> | ||||||
|  |                                                         <li>Firstname: {{$p.LdapUser.Firstname}}</li> | ||||||
|  |                                                         <li>Lastname: {{$p.LdapUser.Lastname}}</li> | ||||||
|  |                                                         <li>Phone: {{$p.UID}}</li> | ||||||
|  |                                                         <li>Mail: {{$p.LdapUser.Mail}}</li> | ||||||
|  |                                                         <li>Department: {{$p.UID}}</li> | ||||||
|  |                                                     </ul> | ||||||
|  |                                                 {{end}} | ||||||
|  |                                                 <h4>Traffic</h4> | ||||||
|  |                                                 {{if not $p.Peer}} | ||||||
|  |                                                     <p>No Traffic data available...</p> | ||||||
|  |                                                 {{else}} | ||||||
|  |                                                     <p>{{if $p.DeactivatedAt}}-{{else}}{{$p.Peer.ReceiveBytes}} / {{$p.Peer.TransmitBytes}}{{end}}</p> | ||||||
|  |                                                 {{end}} | ||||||
|  |                                             </div> | ||||||
|  |                                             <div id="t2{{$p.UID}}" class="tab-pane fade"> | ||||||
|  |                                                 <pre>{{$p.Config}}</pre> | ||||||
|  |                                             </div> | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class="col-md-3"> | ||||||
|  |                                         <img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class="col-md-3"> | ||||||
|  |                                         <div class="float-right mt-5"> | ||||||
|  |                                         <a href="/user/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a> | ||||||
|  |                                         <a href="/user/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a> | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                         </td> | ||||||
|  |                     </tr> | ||||||
|  |                 {{end}} | ||||||
|  |                 </tbody> | ||||||
|  |             </table> | ||||||
|  |             <p>Currently listed peers: <strong>{{len .Peers}}</strong></p> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |     {{template "prt_footer.html"}} | ||||||
|  |     <script src="/js/jquery.min.js"></script> | ||||||
|  |     <script src="/js/bootstrap.bundle.min.js"></script> | ||||||
|  |     <script src="/js/jquery.easing.js"></script> | ||||||
|  |     <script src="/js/custom.js"></script> | ||||||
|  | </body> | ||||||
|  | 
 | ||||||
|  | </html> | ||||||
|  | @ -38,6 +38,7 @@ type SessionData struct { | ||||||
| 	UserName      string | 	UserName      string | ||||||
| 	Firstname     string | 	Firstname     string | ||||||
| 	Lastname      string | 	Lastname      string | ||||||
|  | 	Email         string | ||||||
| 	SortedBy      string | 	SortedBy      string | ||||||
| 	SortDirection string | 	SortDirection string | ||||||
| 	Search        string | 	Search        string | ||||||
|  | @ -109,7 +110,7 @@ func (s *Server) Setup() error { | ||||||
| 	log.Infof("Real working directory: %s", rDir) | 	log.Infof("Real working directory: %s", rDir) | ||||||
| 	log.Infof("Current working directory: %s", dir) | 	log.Infof("Current working directory: %s", dir) | ||||||
| 	var err error | 	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 { | 	if err != nil { | ||||||
| 		return errors.New("unable to pare mail template") | 		return errors.New("unable to pare mail template") | ||||||
| 	} | 	} | ||||||
|  | @ -178,6 +179,7 @@ func (s *Server) getSessionData(c *gin.Context) SessionData { | ||||||
| 		sessionData = SessionData{ | 		sessionData = SessionData{ | ||||||
| 			SortedBy:      "mail", | 			SortedBy:      "mail", | ||||||
| 			SortDirection: "asc", | 			SortDirection: "asc", | ||||||
|  | 			Email:         "", | ||||||
| 			Firstname:     "", | 			Firstname:     "", | ||||||
| 			Lastname:      "", | 			Lastname:      "", | ||||||
| 			IsAdmin:       false, | 			IsAdmin:       false, | ||||||
|  |  | ||||||
|  | @ -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.JSON(code, gin.H{"error": message, "details": details})
 | ||||||
| 
 | 
 | ||||||
| 	c.HTML(code, "error.html", gin.H{ | 	c.HTML(code, "error.html", gin.H{ | ||||||
| 		"data": gin.H{ | 		"Data": gin.H{ | ||||||
| 			"Code":    strconv.Itoa(code), | 			"Code":    strconv.Itoa(code), | ||||||
| 			"Message": message, | 			"Message": message, | ||||||
| 			"Details": details, | 			"Details": details, | ||||||
| 		}, | 		}, | ||||||
| 		"route":   c.Request.URL.Path, | 		"Route":   c.Request.URL.Path, | ||||||
| 		"session": s.getSessionData(c), | 		"Session": s.getSessionData(c), | ||||||
| 		"static":  s.getStaticData(), | 		"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) { | func (s *Server) GetAdminEditInterface(c *gin.Context) { | ||||||
| 	device := s.users.GetDevice() | 	device := s.users.GetDevice() | ||||||
| 	users := s.users.GetAllUsers() | 	users := s.users.GetAllUsers() | ||||||
|  | @ -388,6 +434,12 @@ func (s *Server) GetAdminDeletePeer(c *gin.Context) { | ||||||
| 
 | 
 | ||||||
| func (s *Server) GetUserQRCode(c *gin.Context) { | func (s *Server) GetUserQRCode(c *gin.Context) { | ||||||
| 	user := s.users.GetUserByKey(c.Query("pkey")) | 	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() | 	png, err := user.GetQRCode() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		s.HandleError(c, http.StatusInternalServerError, "QRCode error", err.Error()) | 		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) { | func (s *Server) GetUserConfig(c *gin.Context) { | ||||||
| 	user := s.users.GetUserByKey(c.Query("pkey")) | 	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()) | 	cfg, err := user.GetClientConfigFile(s.users.GetDevice()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		s.HandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) | 		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) { | func (s *Server) GetUserConfigMail(c *gin.Context) { | ||||||
| 	user := s.users.GetUserByKey(c.Query("pkey")) | 	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()) | 	cfg, err := user.GetClientConfigFile(s.users.GetDevice()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		s.HandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) | 		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 { | 	if err := s.mailTpl.Execute(&tplBuff, struct { | ||||||
| 		Client        User | 		Client        User | ||||||
| 		QrcodePngName string | 		QrcodePngName string | ||||||
|  | 		PortalUrl     string | ||||||
| 	}{ | 	}{ | ||||||
| 		Client:        user, | 		Client:        user, | ||||||
| 		QrcodePngName: "wireguard-config.png", | 		QrcodePngName: "wireguard-config.png", | ||||||
|  | 		PortalUrl:     s.config.Core.ExternalUrl, | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
| 		s.HandleError(c, http.StatusInternalServerError, "Template error", err.Error()) | 		s.HandleError(c, http.StatusInternalServerError, "Template error", err.Error()) | ||||||
| 		return | 		return | ||||||
|  |  | ||||||
|  | @ -48,29 +48,51 @@ func (s *Server) PostLogin(c *gin.Context) { | ||||||
| 		return | 		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
 | 	// 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") | 		c.Redirect(http.StatusSeeOther, s.config.AuthRoutePrefix+"/login?err=authfail") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Check if username and password match
 | 	// 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") | 		c.Redirect(http.StatusSeeOther, s.config.AuthRoutePrefix+"/login?err=authfail") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	dn := s.ldapUsers.GetUserDN(username) | 	var sessionData SessionData | ||||||
| 	userData := s.ldapUsers.GetUserData(dn) | 	if adminAuthenticated { | ||||||
| 	sessionData := SessionData{ | 		sessionData = SessionData{ | ||||||
| 		LoggedIn:      true, | 			LoggedIn:      true, | ||||||
| 		IsAdmin:       s.ldapUsers.IsInGroup(username, s.config.AdminLdapGroup), | 			IsAdmin:       true, | ||||||
| 		UID:           userData.GetUID(), | 			Email:         "autodetected@example.com", | ||||||
| 		UserName:      username, | 			UID:           "adminuid", | ||||||
| 		Firstname:     userData.Firstname, | 			UserName:      username, | ||||||
| 		Lastname:      userData.Lastname, | 			Firstname:     "System", | ||||||
| 		SortedBy:      "mail", | 			Lastname:      "Administrator", | ||||||
| 		SortDirection: "asc", | 			SortedBy:      "mail", | ||||||
| 		Search:        "", | 			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 { | 	if err := s.updateSessionData(c, sessionData); err != nil { | ||||||
|  |  | ||||||
|  | @ -51,7 +51,15 @@ func (s *Server) CreateUserByEmail(email, identifierSuffix string, disabled bool | ||||||
| 	device := s.users.GetDevice() | 	device := s.users.GetDevice() | ||||||
| 	user := User{} | 	user := User{} | ||||||
| 	user.AllowedIPsStr = device.AllowedIPsStr | 	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() | 	psk, err := wgtypes.GenerateKey() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
|  | @ -78,7 +86,16 @@ func (s *Server) CreateUser(user User) error { | ||||||
| 
 | 
 | ||||||
| 	device := s.users.GetDevice() | 	device := s.users.GetDevice() | ||||||
| 	user.AllowedIPsStr = device.AllowedIPsStr | 	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
 | 	if user.PrivateKey == "" { // if private key is empty create a new one
 | ||||||
| 		psk, err := wgtypes.GenerateKey() | 		psk, err := wgtypes.GenerateKey() | ||||||
| 		if err != nil { | 		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))) | 	user.UID = fmt.Sprintf("u%x", md5.Sum([]byte(user.PublicKey))) | ||||||
| 
 | 
 | ||||||
| 	// Create wireguard interface
 | 	// Create WireGuard interface
 | ||||||
| 	if user.DeactivatedAt == nil { | 	if user.DeactivatedAt == nil { | ||||||
| 		if err := s.wg.AddPeer(user.GetPeerConfig()); err != nil { | 		if err := s.wg.AddPeer(user.GetPeerConfig()); err != nil { | ||||||
| 			return err | 			return err | ||||||
|  |  | ||||||
|  | @ -37,6 +37,9 @@ func SetupRoutes(s *Server) { | ||||||
| 	user := s.server.Group("/user") | 	user := s.server.Group("/user") | ||||||
| 	user.Use(s.RequireAuthentication("")) // empty scope = all logged in users
 | 	user.Use(s.RequireAuthentication("")) // empty scope = all logged in users
 | ||||||
| 	user.GET("/qrcode", s.GetUserQRCode) | 	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 { | func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc { | ||||||
|  | @ -50,7 +53,7 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc { | ||||||
| 			return | 			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) { | 			!s.ldapUsers.IsInGroup(session.UserName, scope) { | ||||||
| 			// Abort the request with the appropriate error code
 | 			// Abort the request with the appropriate error code
 | ||||||
| 			c.Abort() | 			c.Abort() | ||||||
|  |  | ||||||
|  | @ -477,9 +477,9 @@ func (u *UserManager) GetFilteredAndSortedUsers(sortKey, sortDirection, search s | ||||||
| 			sortValueRight = filteredUsers[j].IPsStr | 			sortValueRight = filteredUsers[j].IPsStr | ||||||
| 		case "handshake": | 		case "handshake": | ||||||
| 			if filteredUsers[i].Peer == nil { | 			if filteredUsers[i].Peer == nil { | ||||||
| 				return true |  | ||||||
| 			} else if filteredUsers[j].Peer == nil { |  | ||||||
| 				return false | 				return false | ||||||
|  | 			} else if filteredUsers[j].Peer == nil { | ||||||
|  | 				return true | ||||||
| 			} | 			} | ||||||
| 			sortValueLeft = filteredUsers[i].Peer.LastHandshakeTime.Format(time.RFC3339) | 			sortValueLeft = filteredUsers[i].Peer.LastHandshakeTime.Format(time.RFC3339) | ||||||
| 			sortValueRight = filteredUsers[j].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 | 	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 { | func (u *UserManager) GetDevice() Device { | ||||||
| 	devices := make([]Device, 0, 1) | 	devices := make([]Device, 0, 1) | ||||||
| 	u.db.Find(&devices) | 	u.db.Find(&devices) | ||||||
|  | @ -513,12 +558,14 @@ func (u *UserManager) GetUserByKey(publicKey string) User { | ||||||
| 	return user | 	return user | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (u *UserManager) GetUserByMail(mail string) User { | func (u *UserManager) GetUsersByMail(mail string) []User { | ||||||
| 	user := User{} | 	var users []User | ||||||
| 	u.db.Where("email = ?", mail).FirstOrInit(&user) | 	u.db.Where("email = ?", mail).Find(&users) | ||||||
| 	u.populateUserData(&user) | 	for i := range users { | ||||||
|  | 		u.populateUserData(&users[i]) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	return user | 	return users | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (u *UserManager) CreateUser(user User) error { | func (u *UserManager) CreateUser(user User) error { | ||||||
|  |  | ||||||
|  | @ -1,192 +1,6 @@ | ||||||
| package wireguard | package wireguard | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	emailTpl = ` |  | ||||||
| <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |  | ||||||
| <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"> |  | ||||||
| <head> |  | ||||||
|     <!--[if gte mso 9]> |  | ||||||
|     <xml> |  | ||||||
|         <o:OfficeDocumentSettings> |  | ||||||
|             <o:AllowPNG/> |  | ||||||
|             <o:PixelsPerInch>96</o:PixelsPerInch> |  | ||||||
|         </o:OfficeDocumentSettings> |  | ||||||
|     </xml> |  | ||||||
|     <![endif]--> |  | ||||||
|     <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> |  | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> |  | ||||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge" /> |  | ||||||
|     <meta name="format-detection" content="date=no" /> |  | ||||||
|     <meta name="format-detection" content="address=no" /> |  | ||||||
|     <meta name="format-detection" content="telephone=no" /> |  | ||||||
|     <meta name="x-apple-disable-message-reformatting" /> |  | ||||||
|     <!--[if !mso]><!--> |  | ||||||
|     <link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" /> |  | ||||||
|     <!--<![endif]--> |  | ||||||
|     <title>Email Template</title> |  | ||||||
|     <!--[if gte mso 9]> |  | ||||||
|     <style type="text/css" media="all"> |  | ||||||
|         sup { font-size: 100% !important; } |  | ||||||
|     </style> |  | ||||||
|     <![endif]--> |  | ||||||
|     <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> |  | ||||||
| 
 |  | ||||||
|     <style type="text/css" media="screen"> |  | ||||||
|         /* Linked Styles */ |  | ||||||
|         body { padding:0 !important; margin:0 !important; display:block !important; min-width:100% !important; width:100% !important; background:#001736; -webkit-text-size-adjust:none } |  | ||||||
|         a { color:#66c7ff; text-decoration:none } |  | ||||||
|         p { padding:0 !important; margin:0 !important } |  | ||||||
|         img { -ms-interpolation-mode: bicubic; /* Allow smoother rendering of resized image in Internet Explorer */ } |  | ||||||
|         .mcnPreviewText { display: none !important; } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         /* Mobile styles */ |  | ||||||
|         @media only screen and (max-device-width: 480px), only screen and (max-width: 480px) { |  | ||||||
|             .mobile-shell { width: 100% !important; min-width: 100% !important; } |  | ||||||
|             .bg { background-size: 100% auto !important; -webkit-background-size: 100% auto !important; } |  | ||||||
| 
 |  | ||||||
|             .text-header, |  | ||||||
|             .m-center { text-align: center !important; } |  | ||||||
| 
 |  | ||||||
|             .center { margin: 0 auto !important; } |  | ||||||
|             .container { padding: 20px 10px !important } |  | ||||||
| 
 |  | ||||||
|             .td { width: 100% !important; min-width: 100% !important; } |  | ||||||
| 
 |  | ||||||
|             .m-br-15 { height: 15px !important; } |  | ||||||
|             .p30-15 { padding: 30px 15px !important; } |  | ||||||
| 
 |  | ||||||
|             .m-td, |  | ||||||
|             .m-hide { display: none !important; width: 0 !important; height: 0 !important; font-size: 0 !important; line-height: 0 !important; min-height: 0 !important; } |  | ||||||
| 
 |  | ||||||
|             .m-block { display: block !important; } |  | ||||||
| 
 |  | ||||||
|             .fluid-img img { width: 100% !important; max-width: 100% !important; height: auto !important; } |  | ||||||
| 
 |  | ||||||
|             .column, |  | ||||||
|             .column-top, |  | ||||||
|             .column-empty, |  | ||||||
|             .column-empty2, |  | ||||||
|             .column-dir-top { float: left !important; width: 100% !important; display: block !important; } |  | ||||||
| 
 |  | ||||||
|             .column-empty { padding-bottom: 10px !important; } |  | ||||||
|             .column-empty2 { padding-bottom: 30px !important; } |  | ||||||
| 
 |  | ||||||
|             .content-spacing { width: 15px !important; } |  | ||||||
|         } |  | ||||||
|     </style> |  | ||||||
| </head> |  | ||||||
| <body class="body" style="padding:0 !important; margin:0 !important; display:block !important; min-width:100% !important; width:100% !important; background:#001736; -webkit-text-size-adjust:none;"> |  | ||||||
| <table width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="#001736"> |  | ||||||
|     <tr> |  | ||||||
|         <td align="center" valign="top"> |  | ||||||
|             <table width="650" border="0" cellspacing="0" cellpadding="0" class="mobile-shell"> |  | ||||||
|                 <tr> |  | ||||||
|                     <td class="td container" style="width:650px; min-width:650px; font-size:0pt; line-height:0pt; margin:0; font-weight:normal; padding:55px 0px;"> |  | ||||||
| 
 |  | ||||||
|                         <!-- Article / Image On The Left - Copy On The Right --> |  | ||||||
|                         <table width="100%" border="0" cellspacing="0" cellpadding="0"> |  | ||||||
|                             <tr> |  | ||||||
|                                 <td style="padding-bottom: 10px;"> |  | ||||||
|                                     <table width="100%" border="0" cellspacing="0" cellpadding="0"> |  | ||||||
|                                         <tr> |  | ||||||
|                                             <td class="tbrr p30-15" style="padding: 60px 30px; border-radius:26px 26px 0px 0px;" bgcolor="#12325c"> |  | ||||||
|                                                 <table width="100%" border="0" cellspacing="0" cellpadding="0"> |  | ||||||
|                                                     <tr> |  | ||||||
|                                                         <th class="column-top" width="280" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;"> |  | ||||||
|                                                             <table width="100%" border="0" cellspacing="0" cellpadding="0"> |  | ||||||
|                                                                 <tr> |  | ||||||
|                                                                     <td class="fluid-img" style="font-size:0pt; line-height:0pt; text-align:left;"><img src="cid:{{.QrcodePngName}}" width="280" height="210" border="0" alt="" /></td> |  | ||||||
|                                                                 </tr> |  | ||||||
|                                                             </table> |  | ||||||
|                                                         </th> |  | ||||||
|                                                         <th class="column-empty2" width="30" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;"></th> |  | ||||||
|                                                         <th class="column-top" width="280" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;"> |  | ||||||
|                                                             <table width="100%" border="0" cellspacing="0" cellpadding="0"> |  | ||||||
|                                                                 <tr> |  | ||||||
|                                                                     <td class="h4 pb20" style="color:#ffffff; font-family:'Muli', Arial,sans-serif; font-size:20px; line-height:28px; text-align:left; padding-bottom:20px;">Hello</td> |  | ||||||
|                                                                 </tr> |  | ||||||
|                                                                 <tr> |  | ||||||
|                                                                     <td class="text pb20" style="color:#ffffff; font-family:Arial,sans-serif; font-size:14px; line-height:26px; text-align:left; padding-bottom:20px;">You probably requested VPN configuration. Here is <strong>{{.Client.Name}}</strong> configuration created <strong>{{.Client.Created.Format "Monday, 02 January 06 15:04:05 MST"}}</strong>. Scan the Qrcode or open attached configuration file in VPN client.</td> |  | ||||||
|                                                                 </tr> |  | ||||||
|                                                             </table> |  | ||||||
|                                                         </th> |  | ||||||
|                                                     </tr> |  | ||||||
|                                                 </table> |  | ||||||
|                                             </td> |  | ||||||
|                                         </tr> |  | ||||||
|                                     </table> |  | ||||||
|                                 </td> |  | ||||||
|                             </tr> |  | ||||||
|                         </table> |  | ||||||
|                         <!-- END Article / Image On The Left - Copy On The Right --> |  | ||||||
| 
 |  | ||||||
|                         <!-- Two Columns / Articles --> |  | ||||||
|                         <table width="100%" border="0" cellspacing="0" cellpadding="0"> |  | ||||||
|                             <tr> |  | ||||||
|                                 <td style="padding-bottom: 10px;"> |  | ||||||
|                                     <table width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="#0e264b"> |  | ||||||
|                                         <tr> |  | ||||||
|                                             <td> |  | ||||||
|                                                 <table width="100%" border="0" cellspacing="0" cellpadding="0"> |  | ||||||
|                                                     <tr> |  | ||||||
|                                                         <td class="p30-15" style="padding: 50px 30px;"> |  | ||||||
|                                                             <table width="100%" border="0" cellspacing="0" cellpadding="0"> |  | ||||||
|                                                                 <tr> |  | ||||||
|                                                                     <td class="h3 pb20" style="color:#ffffff; font-family:'Muli', Arial,sans-serif; font-size:25px; line-height:32px; text-align:left; padding-bottom:20px;">About WireGuard</td> |  | ||||||
|                                                                 </tr> |  | ||||||
|                                                                 <tr> |  | ||||||
|                                                                     <td class="text pb20" style="color:#ffffff; font-family:Arial,sans-serif; font-size:14px; line-height:26px; text-align:left; padding-bottom:20px;">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.</td> |  | ||||||
|                                                                 </tr> |  | ||||||
|                                                                 <!-- Button --> |  | ||||||
|                                                                 <tr> |  | ||||||
|                                                                     <td align="left"> |  | ||||||
|                                                                         <table border="0" cellspacing="0" cellpadding="0"> |  | ||||||
|                                                                             <tr> |  | ||||||
|                                                                                 <td class="blue-button text-button" style="background:#66c7ff; color:#c1cddc; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td> |  | ||||||
|                                                                             </tr> |  | ||||||
|                                                                         </table> |  | ||||||
|                                                                     </td> |  | ||||||
|                                                                 </tr> |  | ||||||
|                                                                 <!-- END Button --> |  | ||||||
|                                                             </table> |  | ||||||
|                                                         </td> |  | ||||||
|                                                     </tr> |  | ||||||
|                                                 </table> |  | ||||||
|                                             </td> |  | ||||||
|                                         </tr> |  | ||||||
|                                     </table> |  | ||||||
|                                 </td> |  | ||||||
|                             </tr> |  | ||||||
|                         </table> |  | ||||||
|                         <!-- END Two Columns / Articles --> |  | ||||||
| 
 |  | ||||||
|                         <!-- Footer --> |  | ||||||
|                         <table width="100%" border="0" cellspacing="0" cellpadding="0"> |  | ||||||
|                             <tr> |  | ||||||
|                                 <td class="p30-15 bbrr" style="padding: 50px 30px; border-radius:0px 0px 26px 26px;" bgcolor="#0e264b"> |  | ||||||
|                                     <table width="100%" border="0" cellspacing="0" cellpadding="0"> |  | ||||||
|                                         <tr> |  | ||||||
|                                             <td class="text-footer1 pb10" style="color:#c1cddc; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">Wg Gen Web - Simple Web based configuration generator for WireGuard</td> |  | ||||||
|                                         </tr> |  | ||||||
|                                         <tr> |  | ||||||
|                                             <td class="text-footer2" style="color:#8297b3; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="https://github.com/vx3r/wg-gen-web" target="_blank" class="link" style="color:#66c7ff; text-decoration:none;"><span class="link" style="color:#66c7ff; text-decoration:none;">More info on Github</span></a></td> |  | ||||||
|                                         </tr> |  | ||||||
|                                     </table> |  | ||||||
|                                 </td> |  | ||||||
|                             </tr> |  | ||||||
|                         </table> |  | ||||||
|                         <!-- END Footer --> |  | ||||||
|                     </td> |  | ||||||
|                 </tr> |  | ||||||
|             </table> |  | ||||||
|         </td> |  | ||||||
|     </tr> |  | ||||||
| </table> |  | ||||||
| </body> |  | ||||||
| </html> |  | ||||||
| ` |  | ||||||
| 
 |  | ||||||
| 	ClientCfgTpl = `[Interface] | 	ClientCfgTpl = `[Interface] | ||||||
| #{{ .Client.Identifier }} | #{{ .Client.Identifier }} | ||||||
| Address = {{ .Client.IPsStr }} | Address = {{ .Client.IPsStr }} | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue