parent
d34a351fe9
commit
5010f029d1
18 changed files with 1904 additions and 0 deletions
@ -0,0 +1,22 @@ |
||||
# ---> Go |
||||
# If you prefer the allow list template instead of the deny list, see community template: |
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore |
||||
# |
||||
# Binaries for programs and plugins |
||||
*.exe |
||||
*.exe~ |
||||
*.dll |
||||
*.so |
||||
*.dylib |
||||
|
||||
# Test binary, built with `go test -c` |
||||
*.test |
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE |
||||
*.out |
||||
|
||||
# Dependency directories (remove the comment below to include it) |
||||
vendor/ |
||||
|
||||
# Go workspace file |
||||
go.work |
@ -0,0 +1,44 @@ |
||||
package domains |
||||
|
||||
var Names T_Domains |
||||
|
||||
type T_Domains struct { |
||||
Domains []T_Domain `json:"Domains"` |
||||
} |
||||
|
||||
type T_Domain struct { |
||||
DomainName string `json:"DomainName"` |
||||
RealServer string `json:"RealServer"` |
||||
RealPort string `json:"RealPort"` |
||||
Status string `json:"Satus"` |
||||
} |
||||
|
||||
const ( |
||||
Onboarding = "Onboarding" |
||||
NotPointedToUs = "NotPointedToUs" |
||||
Working = "Working" |
||||
Unoperational = "Unoperational" |
||||
) |
||||
|
||||
func (T_Domains) AddByDomain(domain T_Domain) { |
||||
Names.Domains = append(Names.Domains, domain) |
||||
} |
||||
|
||||
func (T_Domains) AddByParams(name string, realServer string, realPort string) { |
||||
var temp T_Domain |
||||
temp.DomainName = name |
||||
temp.RealServer = realServer |
||||
temp.RealPort = realPort |
||||
temp.Status = Onboarding |
||||
|
||||
Names.AddByDomain(temp) |
||||
} |
||||
|
||||
func (T_Domains) RemoveDomain(name string) { |
||||
for index, v := range Names.Domains { |
||||
if v.DomainName == name { |
||||
temp := append(Names.Domains[:index], Names.Domains[index+1]) |
||||
Names.Domains = temp |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
package healthReporter |
||||
|
||||
type HealthReporter interface { |
||||
Join(watcher Watcher) |
||||
SetState(state string) |
||||
Notify() |
||||
} |
||||
|
||||
type Watcher interface { |
||||
Update(state string) |
||||
} |
@ -0,0 +1,193 @@ |
||||
package management |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"log" |
||||
"os" |
||||
|
||||
"time" |
||||
|
||||
"github.com/asaskevich/govalidator" |
||||
domains "github.com/cr3a70r/shield/Domains" |
||||
) |
||||
|
||||
var Settings T_Management |
||||
|
||||
type T_Management struct { |
||||
Users []T_User `json:"Users"` |
||||
Domains domains.T_Domains `json:"Names"` |
||||
Debug bool `json:"Debug"` |
||||
} |
||||
|
||||
type T_User struct { |
||||
Email string `json:"Email"` |
||||
Password string `json:"Password"` |
||||
Cookie string `json:"Cookie"` |
||||
CreatedDate string `json:"CreatedDate"` |
||||
} |
||||
|
||||
func (T_Management) SaveConfig() { |
||||
|
||||
} |
||||
|
||||
func (T_Management) LoadConfig() { |
||||
|
||||
} |
||||
|
||||
func (T_Management) ReloadConfig() { |
||||
|
||||
} |
||||
|
||||
func (T_Management) Initialize() { |
||||
configExists, err := os.Open("config.json") |
||||
|
||||
if err != nil { |
||||
log.Println("management.initialize: new deployment: building config") |
||||
Settings.DefaultSuperAdmin() |
||||
|
||||
configNew, err := os.OpenFile("config.json", os.O_CREATE|os.O_WRONLY, 0644) |
||||
if err == nil { |
||||
configByte, err := json.MarshalIndent(Settings, "", " ") |
||||
if err != nil { |
||||
log.Fatal("management.initialize: could not Marshall Settings: ", err) |
||||
} |
||||
configNew.Write(configByte) |
||||
} else { |
||||
log.Fatal("management.initialize: could not create new config.json file: ", err) |
||||
} |
||||
|
||||
defer configNew.Close() |
||||
} else { |
||||
configByte, err := ioutil.ReadAll(configExists) |
||||
if err != nil { |
||||
log.Fatal("management.initialize: could not read config: ", err) |
||||
} |
||||
|
||||
json.Unmarshal(configByte, &Settings) |
||||
|
||||
defer configExists.Close() |
||||
} |
||||
} |
||||
|
||||
// Needs to be checked if a user already exist
|
||||
func (T_Management) AddUserByTUser(user T_User) error { |
||||
if !govalidator.IsEmail(user.Email) { |
||||
return errors.New("email is not an email") |
||||
} |
||||
if user.Email == "" { |
||||
return errors.New("email cannot be empty") |
||||
} |
||||
if user.Password == "" { |
||||
return errors.New("password cannot be empty") |
||||
} |
||||
currentTime := time.Now() |
||||
user.CreatedDate = currentTime.Format("2006-January-02") |
||||
|
||||
Settings.Users = append(Settings.Users, user) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (T_Management) AddUserByParams(email string, passwd string) error { |
||||
var temp T_User |
||||
temp.Email = email |
||||
temp.Password = passwd |
||||
err := Settings.AddUserByTUser(temp) |
||||
|
||||
return err |
||||
} |
||||
|
||||
func (T_Management) RemoveUserByEmail(email string) error { |
||||
found := false |
||||
for index, u := range Settings.Users { |
||||
if u.Email == email { |
||||
temp := append(Settings.Users[:index], Settings.Users[index+1]) |
||||
Settings.Users = temp |
||||
found = true |
||||
return nil |
||||
} |
||||
} |
||||
if !found { |
||||
return errors.New("did not find a user:" + email) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (T_Management) CheckPassword(email string, passwd string) (bool, error) { |
||||
found := false |
||||
for _, u := range Settings.Users { |
||||
if u.Email == email && u.Password == passwd { |
||||
return true, nil |
||||
} |
||||
} |
||||
if !found { |
||||
return false, errors.New("no combination with:" + email + " :" + passwd) |
||||
} |
||||
return false, nil |
||||
} |
||||
|
||||
func (T_Management) SaveCookie(email string, cookie string) error { |
||||
for index, _ := range Settings.Users { |
||||
if u.Email == email { |
||||
Settings.Users[index].Cookie = cookie |
||||
return nil |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (T_Management) OnboardDomain(name string, realServer string, realPort string) (bool, error) { |
||||
if !govalidator.IsDNSName(name) { |
||||
return false, errors.New("given name is not a DNS name") |
||||
} |
||||
if !govalidator.IsDNSName(realServer) && !govalidator.IsIP(realServer) { |
||||
return false, errors.New("given realServer is not a valid DNS name or IP Address") |
||||
} |
||||
if !govalidator.IsPort(realPort) { |
||||
return false, errors.New("given realPort is not a valid Port number") |
||||
} |
||||
|
||||
domains.Names.AddByParams(name, realServer, realPort) |
||||
|
||||
return true, nil |
||||
} |
||||
|
||||
func (T_Management) RemoveDomain(name string) { |
||||
domains.Names.RemoveDomain(name) |
||||
} |
||||
|
||||
func (T_Management) UserFindByEmail(email string) (T_User, error) { |
||||
found := false |
||||
var temp T_User |
||||
for _, u := range Settings.Users { |
||||
if u.Email == email { |
||||
return u, nil |
||||
} |
||||
} |
||||
if !found { |
||||
return temp, errors.New("no combination with:" + email) |
||||
} |
||||
return temp, nil |
||||
} |
||||
|
||||
func (T_Management) AllUsers() []T_User { |
||||
return Settings.Users |
||||
} |
||||
|
||||
func (T_Management) DefaultSuperAdmin() { |
||||
Settings.Debug = false |
||||
Settings.AddUserByParams("defadm@daydev.org", "siconmas") |
||||
} |
||||
|
||||
func (T_Management) ToString() string { |
||||
managementString, err := json.Marshal(Settings) |
||||
|
||||
if err != nil { |
||||
log.Println("management.ToString: " + err.Error()) |
||||
} |
||||
|
||||
return string(managementString) |
||||
} |
@ -0,0 +1,4 @@ |
||||
package management |
||||
|
||||
// Originator is T_Management
|
||||
|
@ -0,0 +1,151 @@ |
||||
package management |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"log" |
||||
"net/http" |
||||
"path/filepath" |
||||
"text/template" |
||||
|
||||
"github.com/asaskevich/govalidator" |
||||
"github.com/cr3a70r/shield/Utils" |
||||
"github.com/gorilla/mux" |
||||
"github.com/gorilla/securecookie" |
||||
) |
||||
|
||||
var hashKey = []byte("ckjstkldx-rlkjcmskl-rdlskjtmd") |
||||
var blockKey = []byte("opbckswle-sdnfekjtiw-dsmnwhekskd") |
||||
var secCookie = securecookie.New(hashKey, blockKey) |
||||
|
||||
func StartWebserver(addr string) { |
||||
router := mux.NewRouter() |
||||
|
||||
router.HandleFunc("/", renderIndex) |
||||
router.HandleFunc("/config", config) |
||||
router.HandleFunc("/dashboard", dashboard) |
||||
router.HandleFunc("/protection", protection) |
||||
router.HandleFunc("/config", config) |
||||
|
||||
log.Fatal(http.ListenAndServe(addr, router)) |
||||
} |
||||
|
||||
func health(writer http.ResponseWriter, req *http.Request) { |
||||
Utils.RespondJSON("Unknown state", 200, writer) |
||||
} |
||||
|
||||
func renderIndex(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method == "POST" { |
||||
|
||||
r.ParseForm() |
||||
if !govalidator.IsEmail(r.FormValue("Email")) { |
||||
log.Println("webserver.auth: email not found") |
||||
} |
||||
|
||||
success, err := Settings.CheckPassword(r.FormValue("Email"), r.FormValue("Password")) |
||||
if err != nil { |
||||
log.Println("webserver.auth: unseccessful auth ") |
||||
log.Println(err) |
||||
|
||||
} else if success { |
||||
log.Println("webserver.auth: login") |
||||
log.Println(r.FormValue("Email")) |
||||
|
||||
value := map[string]string{ |
||||
"email": r.FormValue("Email"), |
||||
"password": r.FormValue("Password"), |
||||
} |
||||
encoded, err := secCookie.Encode("Shield", value) |
||||
|
||||
Settings.SaveCookie(r.FormValue("Email"), encoded) |
||||
|
||||
if err != nil { |
||||
log.Println("webserver.auth: failed to encode cookie") |
||||
} |
||||
cookie := &http.Cookie{ |
||||
Name: "Shield", |
||||
Value: encoded, |
||||
Path: "/", |
||||
} |
||||
http.SetCookie(w, cookie) |
||||
r.Header.Set("x-shield", encoded) |
||||
|
||||
http.Redirect(w, r, "/dashboard", http.StatusSeeOther) |
||||
} |
||||
} |
||||
|
||||
parsedTemplate, _ := template.ParseFiles("static/index.html") |
||||
err := parsedTemplate.Execute(w, "") |
||||
if err != nil { |
||||
log.Println("Error executing template :", err) |
||||
return |
||||
} |
||||
|
||||
defer r.Body.Close() |
||||
} |
||||
|
||||
func dashboard(w http.ResponseWriter, r *http.Request) { |
||||
fpTemplate := filepath.Join("static", "template.html") |
||||
fpPage := filepath.Join("static", "dashboard.html") |
||||
|
||||
tmpl, err := template.ParseFiles(fpPage, fpTemplate) |
||||
|
||||
if err != nil { |
||||
log.Println("webserver.dashboard: " + err.Error()) |
||||
} |
||||
|
||||
err = tmpl.ExecuteTemplate(w, "template.html", nil) |
||||
if err != nil { |
||||
log.Println("webserver.dashboard: " + err.Error()) |
||||
} |
||||
} |
||||
|
||||
func protection(w http.ResponseWriter, r *http.Request) { |
||||
fpTemplate := filepath.Join("static", "template.html") |
||||
fpPage := filepath.Join("static", "protection.html") |
||||
|
||||
tmpl, err := template.ParseFiles(fpPage, fpTemplate) |
||||
if err != nil { |
||||
log.Println("webserver.protection: " + err.Error()) |
||||
} |
||||
|
||||
err = tmpl.ExecuteTemplate(w, "template.html", nil) |
||||
if err != nil { |
||||
log.Println("webserver.protection: " + err.Error()) |
||||
} |
||||
} |
||||
|
||||
type View struct { |
||||
Data string |
||||
} |
||||
|
||||
func config(w http.ResponseWriter, r *http.Request) { |
||||
fpTemplate := filepath.Join("static", "template.html") |
||||
fpPage := filepath.Join("static", "config.html") |
||||
|
||||
tmpl, err := template.ParseFiles(fpPage, fpTemplate) |
||||
if err != nil { |
||||
log.Println("webserver.config: " + err.Error()) |
||||
} |
||||
|
||||
bt, err := json.MarshalIndent(Settings, "", " ") |
||||
vd := View{string(bt)} |
||||
|
||||
//vd := ViewData{&Settings}
|
||||
|
||||
err = tmpl.ExecuteTemplate(w, "template.html", vd) |
||||
if err != nil { |
||||
log.Println("webserver.config: " + err.Error()) |
||||
} |
||||
} |
||||
|
||||
func requireAuth(h http.Handler) http.Handler { |
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||||
if cookie, err := r.Cookie("Shield"); err == nil { |
||||
value := make(map[string]string) |
||||
if err = secCookie.Decode("Shield", cookie.Value, &value); err == nil { |
||||
log.Println("webserver.requireAuth: unauthorized access denied " + r.RemoteAddr) |
||||
} |
||||
} |
||||
h.ServeHTTP(w, r) |
||||
}) |
||||
} |
@ -0,0 +1,12 @@ |
||||
package Utils |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net/http" |
||||
) |
||||
|
||||
func RespondJSON(data interface{}, code int, writer http.ResponseWriter) { |
||||
writer.Header().Add("Content-type", "application/json") |
||||
writer.WriteHeader(code) |
||||
json.NewEncoder(writer).Encode(data) |
||||
} |
@ -0,0 +1,14 @@ |
||||
{ |
||||
"Users": [ |
||||
{ |
||||
"Email": "defadm@daydev.org", |
||||
"Password": "siconmas", |
||||
"JWTHash": "", |
||||
"CreatedDate": "2022-July-13" |
||||
} |
||||
], |
||||
"Names": { |
||||
"Domains": null |
||||
}, |
||||
"Debug": false |
||||
} |
@ -0,0 +1,9 @@ |
||||
module github.com/cr3a70r/shield |
||||
|
||||
go 1.17 |
||||
|
||||
require ( |
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d |
||||
github.com/gorilla/mux v1.8.0 |
||||
github.com/gorilla/securecookie v1.1.1 |
||||
) |
@ -0,0 +1,6 @@ |
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= |
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= |
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= |
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= |
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= |
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"log" |
||||
"os" |
||||
|
||||
management "github.com/cr3a70r/shield/Management" |
||||
) |
||||
|
||||
func main() { |
||||
logFile, err := os.OpenFile("runlog.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} else { |
||||
log.SetOutput(logFile) |
||||
} |
||||
|
||||
defer logFile.Close() |
||||
|
||||
management.Settings.Initialize() |
||||
|
||||
|
||||
management.StartWebserver("127.0.0.1:8080") |
||||
} |
||||
|
@ -0,0 +1,13 @@ |
||||
{{define "title"}} |
||||
Config | Shield |
||||
{{end}} |
||||
|
||||
{{define "body"}} |
||||
|
||||
<div class="mb-3"> |
||||
<label for="exampleFormControlTextarea1" class="form-label">running-config</label> |
||||
<textarea class="form-control" id="exampleFormControlTextarea1" rows="30">{{ .Data }}</textarea> |
||||
</div> |
||||
|
||||
|
||||
{{end}} |
@ -0,0 +1,10 @@ |
||||
|
||||
{{define "title"}} |
||||
Dashboard | Shield |
||||
{{end}} |
||||
|
||||
{{define "body"}} |
||||
<h1>Hi</h1> |
||||
dskflsjflksdjfls |
||||
dflsdfsdlfks |
||||
{{end}} |
@ -0,0 +1,85 @@ |
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
||||
<meta name="description" content=""> |
||||
<meta name="author" content=""> |
||||
<link rel="icon" href="/docs/4.0/assets/img/favicons/favicon.ico"> |
||||
|
||||
<title>SignIn | Shield</title> |
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous"> |
||||
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.5/dist/umd/popper.min.js" integrity="sha384-Xe+8cL9oJa6tN/veChSP7q+mnSPaj5Bcu9mPX5F5xIGE0DVittaqT5lorf0EI7Vk" crossorigin="anonymous"></script> |
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.min.js" integrity="sha384-kjU+l4N0Yf4ZOJErLsIcvOU2qSb74wXpOhqTvwVx3OElZRweTnQ6d31fXEoRD1Jy" crossorigin="anonymous"></script> |
||||
</head> |
||||
<body class="text-center" > |
||||
<form method="POST" action="/" class="form-signin"> |
||||
<img class="mb-4" src="https://getbootstrap.com/docs/4.0/assets/brand/bootstrap-solid.svg" alt="" width="72" height="72"> |
||||
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1> |
||||
<label for="inputEmail" class="sr-only">Email address</label> |
||||
<input type="email" id="Email" name="Email" class="form-control" placeholder="Email address" required autofocus> |
||||
<label for="inputPassword" class="sr-only">Password</label> |
||||
<input type="password" id="Password" name="Password" class="form-control" placeholder="Password" required> |
||||
<div class="checkbox mb-3"> |
||||
<label> |
||||
<input type="checkbox" value="remember-me"> Remember me |
||||
</label> |
||||
</div> |
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> |
||||
<p class="mt-5 mb-3 text-muted">© 2017-2022</p> |
||||
</form> |
||||
</body> |
||||
</html> |
||||
|
||||
<style> |
||||
html, |
||||
body { |
||||
height: 100%; |
||||
} |
||||
|
||||
body { |
||||
display: -ms-flexbox; |
||||
display: -webkit-box; |
||||
display: flex; |
||||
-ms-flex-align: center; |
||||
-ms-flex-pack: center; |
||||
-webkit-box-align: center; |
||||
align-items: center; |
||||
-webkit-box-pack: center; |
||||
justify-content: center; |
||||
padding-top: 40px; |
||||
padding-bottom: 40px; |
||||
background-color: #f5f5f5; |
||||
} |
||||
|
||||
.form-signin { |
||||
width: 100%; |
||||
max-width: 330px; |
||||
padding: 15px; |
||||
margin: 0 auto; |
||||
} |
||||
.form-signin .checkbox { |
||||
font-weight: 400; |
||||
} |
||||
.form-signin .form-control { |
||||
position: relative; |
||||
box-sizing: border-box; |
||||
height: auto; |
||||
padding: 10px; |
||||
font-size: 16px; |
||||
} |
||||
.form-signin .form-control:focus { |
||||
z-index: 2; |
||||
} |
||||
.form-signin input[type="email"] { |
||||
margin-bottom: -1px; |
||||
border-bottom-right-radius: 0; |
||||
border-bottom-left-radius: 0; |
||||
} |
||||
.form-signin input[type="password"] { |
||||
margin-bottom: 10px; |
||||
border-top-left-radius: 0; |
||||
border-top-right-radius: 0; |
||||
} |
||||
</style> |
@ -0,0 +1,30 @@ |
||||
|
||||
{{define "title"}} |
||||
Protection | Shield |
||||
{{end}} |
||||
|
||||
{{define "body"}} |
||||
|
||||
<table class="table table-striped table-hover"> |
||||
<thead> |
||||
<tr> |
||||
<th scope="col">Domain</th> |
||||
<th scope="col">Real Server</th> |
||||
<th scope="col">Real Port</th> |
||||
<th scope="col">Status</th> |
||||
<th scope="col">Action</th> |
||||
<th scope="col"></th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
<tr scope="row"> |
||||
<td>Test</td> |
||||
<td>8.8.8.8</td> |
||||
<td>80</td> |
||||
<td>Onboarding</td> |
||||
<td><a class="btn btn-warning active" href="/protection/edit">Edit</a></td> |
||||
<td><a class="btn btn-danger active" href="/protection/delete">Delete</a></td> |
||||
</tr> |
||||
</tbody> |
||||
</table> |
||||
{{end}} |
@ -0,0 +1,46 @@ |
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
||||
<meta name="description" content=""> |
||||
<meta name="author" content=""> |
||||
<link rel="icon" href="/docs/4.0/assets/img/favicons/favicon.ico"> |
||||
|
||||
<title>{{ template "title"}}</title> |
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous"> |
||||
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.5/dist/umd/popper.min.js" integrity="sha384-Xe+8cL9oJa6tN/veChSP7q+mnSPaj5Bcu9mPX5F5xIGE0DVittaqT5lorf0EI7Vk" crossorigin="anonymous"></script> |
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.min.js" integrity="sha384-kjU+l4N0Yf4ZOJErLsIcvOU2qSb74wXpOhqTvwVx3OElZRweTnQ6d31fXEoRD1Jy" crossorigin="anonymous"></script> |
||||
</head> |
||||
<body class="text-center" > |
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light"> |
||||
<div class="container-fluid"> |
||||
<a class="navbar-brand" href="#">InetSet</a> |
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> |
||||
<span class="navbar-toggler-icon"></span> |
||||
</button> |
||||
<div class="collapse navbar-collapse" id="navbarNav"> |
||||
<ul class="navbar-nav"> |
||||
<li class="nav-item"> |
||||
<a class="nav-link active" aria-current="page" href="/dashboard">Home</a> |
||||
</li> |
||||
<li class="nav-item"> |
||||
<a class="nav-link" href="/protection">Protection</a> |
||||
</li> |
||||
<li class="nav-item"> |
||||
<a class="nav-link" href="#">Pricing</a> |
||||
</li> |
||||
<li class="nav-item"> |
||||
<a class="nav-link" href="/config" tabindex="-1" aria-disabled="true">Config</a> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
</nav> |
||||
|
||||
{{template "body" . }} |
||||
|
||||
<footer>inetset</footer> |
||||
</body> |
||||
</html> |
Loading…
Reference in new issue