mirror of
https://github.com/DJSundog/wg-portal.git
synced 2024-11-23 15:13:52 -05:00
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>
|
||||||
|
110
assets/tpl/user_index.html
Normal file
110
assets/tpl/user_index.html
Normal file
@ -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
Block a user