Initial commit

This commit is contained in:
Joerg Lehmann 2019-03-30 18:41:00 +01:00
commit 10993b9209
28 changed files with 887 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
mini-beieli-web
nohup.out
database/

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# mini-beieli-web - Bienenstock Ueberwachung
Webapplikation, geschrieben in Golang.
Autor: Joerg Lehmann, nbit Informatik GmbH

129
authentication.go Normal file
View File

@ -0,0 +1,129 @@
package main
import (
"github.com/gorilla/securecookie"
"net/http"
"fmt"
"crypto/md5"
"encoding/hex"
)
// cookie handling
var cookieHandler = securecookie.New(
securecookie.GenerateRandomKey(64),
securecookie.GenerateRandomKey(32))
func getUserName(request *http.Request) (userName string) {
if cookie, err := request.Cookie("session"); err == nil {
cookieValue := make(map[string]string)
if err = cookieHandler.Decode("session", cookie.Value, &cookieValue); err == nil {
userName = cookieValue["name"]
}
}
return userName
}
func getUserNameHash(request *http.Request) (userName string) {
if cookie, err := request.Cookie("session"); err == nil {
cookieValue := make(map[string]string)
if err = cookieHandler.Decode("session", cookie.Value, &cookieValue); err == nil {
userName = cookieValue["name"]
}
}
hasher := md5.New()
hasher.Write([]byte(userName))
return hex.EncodeToString(hasher.Sum(nil))
}
func setSession(userName string, response http.ResponseWriter) {
value := map[string]string{
"name": userName,
}
if encoded, err := cookieHandler.Encode("session", value); err == nil {
cookie := &http.Cookie{
Name: "session",
Value: encoded,
Path: "/",
}
http.SetCookie(response, cookie)
}
}
func clearSession(response http.ResponseWriter) {
cookie := &http.Cookie{
Name: "session",
Value: "",
Path: "/",
MaxAge: -1,
}
http.SetCookie(response, cookie)
}
// login handler
func loginHandler(response http.ResponseWriter, request *http.Request) {
name := request.FormValue("email")
pass := request.FormValue("password")
redirectTarget := "/invalid_login.html"
//if name != "" && pass != "" {
if checkLoginCredentials(name,pass) {
// .. check credentials ..
logit(fmt.Sprintf("loginHandler: successful login for User %s",name))
setSession(name, response)
updateLoginTime(name)
redirectTarget = "/scales.html"
} else {
logit(fmt.Sprintf("loginHandler: invalid login for User %s",name))
}
http.Redirect(response, request, redirectTarget, 302)
}
// resetPassword handler
func resetPasswordHandler(response http.ResponseWriter, request *http.Request) {
name := request.FormValue("email")
pass := request.FormValue("password")
redirectTarget := "/"
logit(fmt.Sprintf("resetPasswordHandler: request for User %s",name))
if name != "" && pass != "" {
if checkUserAvailable(name) {
http.Redirect(response, request, "/user_does_not_exist.html", 302)
} else {
updateUser(name,pass)
http.Redirect(response, request, redirectTarget, 302)
}
}
http.Redirect(response, request, "/error_reset_password.html",302)
}
// setPassword handler
func setPasswordHandler(response http.ResponseWriter, request *http.Request) {
name := getUserName(request)
pass := request.FormValue("password")
if name != "" && pass != "" {
if checkUserAvailable(name) {
http.Redirect(response, request, "/user_does_not_exist.html", 302)
} else {
updateUser(name,pass)
}
}
}
// logout handler
func logoutHandler(response http.ResponseWriter, request *http.Request) {
clearSession(response)
http.Redirect(response, request, "/", 302)
}
// confirm handler
func confirmHandler(response http.ResponseWriter, request *http.Request) {
confirm_id := request.URL.Query().Get("id")
logit(fmt.Sprintf("Confirm ID: %s\n",confirm_id))
confirmUser(confirm_id)
http.Redirect(response, request, "/", 302)
}

9
log.go Normal file
View File

@ -0,0 +1,9 @@
package main
import (
"log"
)
func logit(log_message string) {
log.Println(log_message)
}

44
mail.go Normal file
View File

@ -0,0 +1,44 @@
package main
import (
"log"
"bytes"
"net/smtp"
)
func sendEmail(username,confirm_id string) {
c, err := smtp.Dial("127.0.0.1:25")
if err != nil {
log.Fatal(err)
}
defer c.Close()
// Set the sender and recipient.
c.Mail("register@mini-beieli.ch")
c.Rcpt(username)
// Send the email body.
wc, err := c.Data()
if err != nil {
log.Fatal(err)
}
defer wc.Close()
mail_message := "To: " + username + `
Subject: Passwortaenderung auf https://mini-beieli.ch, bitte bestaetigen
Lieber Benutzer von mini-beieli.ch
Sie haben soeben eine Passwortaenderung veranlasst. Bitte klicken Sie folgenden Link,
um die Registration abzuschliessen:
https://mini-beieli.ch/confirm?id=` + confirm_id + `
Bitte ignorieren Sie diese Meldung, falls die Aenderung nicht von Ihnen angefordert wurde!
Mit freundlichen Grüssen
--
mini-beieli.ch`
buf := bytes.NewBufferString(mail_message)
if _, err = buf.WriteTo(wc); err != nil {
log.Fatal(err)
}
}

91
main.go Normal file
View File

@ -0,0 +1,91 @@
package main
import (
"html/template"
"net/http"
"os"
"path"
"time"
)
type AccountData struct {
Full_name string
Phone string
Address string
Zip string
City string
}
type Scale struct {
Beielipi_id string
Beielipi_alias string
Id string
Alias string
My_sms_number string
Sms_alarm string
Last_alarm string
}
func serveTemplate(w http.ResponseWriter, r *http.Request) {
logit("Called URL: "+r.URL.Path)
// wennn kein File angegeben ist: index.html
if r.URL.Path == "/" {
r.URL.Path = "/index.html"
}
lp := path.Join("templates", "layout.html")
fp := path.Join("snippets", r.URL.Path)
// Return a 404 if the template doesn't exist
_, err := os.Stat(fp)
if err != nil {
if os.IsNotExist(err) {
logit("URL not found: " + fp)
fp = path.Join("snippets", "404.html")
}
}
tmpl, err := template.ParseFiles(lp, fp)
if err != nil {
// Log the detailed error
logit(err.Error())
// Return a generic "Internal Server Error" message
http.Error(w, http.StatusText(500), 500)
return
}
var userName = getUserName(r)
t := time.Now()
var datetimestring = t.Format("20060102150405")
data := struct {
UserName string
DateTimeString string
} {
userName,
datetimestring,
}
if err := tmpl.ExecuteTemplate(w, "layout", &data); err != nil {
logit(err.Error())
http.Error(w, http.StatusText(500), 500)
}
}
func main() {
initDB()
defer closeDB()
fs := http.FileServer(http.Dir("static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
http.Handle("/favicon.ico", fs)
http.HandleFunc("/", serveTemplate)
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/reset_password", resetPasswordHandler)
http.HandleFunc("/set_password", setPasswordHandler)
http.HandleFunc("/logout", logoutHandler)
http.HandleFunc("/confirm", confirmHandler)
logit("Starting Web Application...")
http.ListenAndServe("127.0.0.1:4000", nil)
logit("Terminating Web Application...")
}

235
persistence.go Normal file
View File

@ -0,0 +1,235 @@
package main
import (
"time"
"encoding/json"
"crypto/rand"
"golang.org/x/crypto/bcrypt"
"github.com/gomodule/redigo/redis"
)
var globalPool *redis.Pool
var globalConn redis.Conn
const userPrefix string = "user:"
const confirmPrefix string = "confirm:"
func newPool() *redis.Pool {
return &redis.Pool{
// Maximum number of idle connections in the pool.
MaxIdle: 80,
// max number of connections
MaxActive: 12000,
// Dial is an application supplied function for creating and
// configuring a connection.
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", ":6379")
if err != nil {
panic(err.Error())
}
return c, err
},
}
}
// ping tests connectivity for redis (PONG should be returned)
func ping(c redis.Conn) error {
// Send PING command to Redis
// PING command returns a Redis "Simple String"
// Use redis.String to convert the interface type to string
s, err := redis.String(c.Do("PING"))
if err != nil {
return err
}
logit("PING Response = "+s)
// Output: PONG
return nil
}
// User is a simple user struct for this example
type User struct {
Email string `json:"email"`
Password string `json:"password"`
NewPassword string `json:"new_password"`
ConfirmId string `json:"confirm_id"`
LastLogin string `json:"last_login"`
}
func setStruct(c redis.Conn, usr User) error {
// serialize User object to JSON
json, err := json.Marshal(usr)
if err != nil {
return err
}
// SET object
_, err = c.Do("SET", userPrefix+usr.Email, json)
if err != nil {
return err
}
return nil
}
func getStruct(c redis.Conn, username string) (User, error) {
usr := User{}
s, err := redis.String(c.Do("GET", userPrefix+username))
if err == redis.ErrNil {
logit("User does not exist:"+username)
} else if err != nil {
return usr, err
}
err = json.Unmarshal([]byte(s), &usr)
return usr, nil
}
func initDB() {
// newPool returns a pointer to a redis.Pool
pool := newPool()
// get a connection from the pool (redis.Conn)
conn := pool.Get()
globalPool = pool
globalConn = conn
// wir machen einen Connection Test
ping(globalConn)
// Wir legen einen initialen Admin User an, falls es diesen noch nicht gibt
if checkUserAvailable("joerg.lehmann@nbit.ch") {
insertUser("joerg.lehmann@nbit.ch","changeme123","Y");
}
}
func closeDB() {
globalConn.Close()
globalPool.Close()
}
func checkUserAvailable(username string) bool {
logit("checkUserAvailable: User: "+username)
_, err := redis.String(globalConn.Do("GET", userPrefix+username))
if (err == redis.ErrNil) {
logit("User does not exist and is therefore available:"+username)
return true
} else if err != nil {
logit("checkUserAvailable: Error to query Key Value Store")
return false
}
return false
}
func randString(n int) string {
const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
var bytes = make([]byte, n)
rand.Read(bytes)
for i, b := range bytes {
bytes[i] = alphanum[b % byte(len(alphanum))]
}
return string(bytes)
}
func insertUser(username,password,is_admin string) {
logit("insertUser: "+username)
pwd := []byte(password)
hashedPassword,err := bcrypt.GenerateFromPassword(pwd, bcrypt.DefaultCost)
if err != nil {
logit("insertUser: Error with bcrypt.GenerateFromPassword, User: "+username)
return
}
confirm_id := ""
_, err = globalConn.Do("HMSET", userPrefix+username, "password", string(hashedPassword), "new_password", string(hashedPassword), "confirm_id", confirm_id, "last_login", "")
if err != nil {
logit("insertUser: Error inserting User: "+username)
return
}
}
func updateUser(username,password string) {
logit("updateUser: "+username)
pwd := []byte(password)
hashedPassword,err := bcrypt.GenerateFromPassword(pwd, bcrypt.DefaultCost)
if err != nil {
logit("updateUser: Error with bcrypt.GenerateFromPassword, User: "+username)
return
}
confirm_id := randString(30)
_, err = globalConn.Do("HMSET", userPrefix+username, "new_password", string(hashedPassword), "confirm_id", confirm_id)
if err != nil {
logit("updateUser: Error updateing User: "+username)
return
}
_, err = globalConn.Do("SET", confirmPrefix+confirm_id,username)
if err != nil {
logit("updateUser: Error inserting confirm_id: "+confirm_id+": "+username)
return
}
sendEmail(username,confirm_id)
}
func checkLoginCredentials(username,password string) bool {
logit("checkLoginCredentials: called with username,password: "+username+","+password)
pwd, err := redis.String(globalConn.Do("HGET", userPrefix+username, "password"))
if err == nil {
cid, err := redis.String(globalConn.Do("HGET", userPrefix+username, "confirm_id"))
if err == nil {
logit("checkLoginCredentials: cid: "+cid)
if !(err != nil && cid != "") {
logit("checkLoginCredentials: pwd: "+pwd)
if err != nil {
return false
}
}
}
}
hashedPassword := []byte(pwd)
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(password))
return err == nil
}
func updateLoginTime(username string) {
_, err := globalConn.Do("HSET", userPrefix+username, "last_login", time.Now().UTC().Format("2006-01-02 15:04:05"))
if err != nil {
logit("updateUser: Error updateing User: "+username)
return
}
}
func confirmUser(confirm_id string) {
u, err := redis.String(globalConn.Do("GET", confirmPrefix+confirm_id))
if err != nil {
logit("confirmUser: Error with searching confirm_id: "+confirm_id)
return
}
new_password, err := redis.String(globalConn.Do("HGET", userPrefix+u, "new_password"))
if err != nil {
logit("confirmUser: Error with getting new_password: "+u)
return
}
_, err = globalConn.Do("HMSET", userPrefix+u, "confirm_id", "", "password", new_password)
if err != nil {
logit("confirmUser: Error updateing User: "+u)
return
}
_, err = globalConn.Do("DEL", confirmPrefix+confirm_id)
if err != nil {
logit("confirmUser: Error deleting confirm_id: "+confirm_id)
return
}
}

5
snippets/404.html Normal file
View File

@ -0,0 +1,5 @@
{{define "body_content"}}
<div class="notification is-danger">
<strong>Diese Seite existiert nicht</strong>
</div>
{{end}}

9
snippets/contact.html Normal file
View File

@ -0,0 +1,9 @@
{{define "body_content"}}
<p class="title is-4">Kontakt</p>
<p>nbit Informatik GmbH<br />
Kirchweg 2<br />
3510 Konolfingen<br />
<br />
+41 31 792 00 40<br />
<a href='&#109;ai&#108;&#116;o&#58;i&#37;6Ef&#111;&#64;%6Ebi&#116;&#46;%63h'>&#105;nfo&#64;nbit&#46;ch</a>
{{end}}

28
snippets/index.html Normal file
View File

@ -0,0 +1,28 @@
{{define "header_additions"}}
{{end}}
{{define "body_content"}}
<p class="title is-4">Die etwas andere Bienenstockwaage...</p>
<article class="message is-danger">
<div class="message-body">
Aktuell noch in Entwicklung, kommen Sie sp&auml;ter noch einmal vorbei...
</div>
</article>
{{ if ne .UserName "" }}
<p>
<strong>Ich will weitere bestellen!</strong>
<span class="icon"><i class="fa fa-arrow-right"></i></span>
Hier geht's zum <a href="/order.html">Bestellformular</a>
</p>
{{ else }}
<p>
<strong>Ich will auch eine!</strong>
<span class="icon"><i class="fa fa-arrow-right"></i></span>
Hier geht's zum <a href="/order.html">Bestellformular</a>
</p>
<p>&nbsp;</p>
<p>
<strong>Ich habe bereits eine (oder mehrere)</strong>
<span class="icon"><i class="fa fa-arrow-right"></i></span>
Hier geht's zum <a href="/login.html">Login</a>
{{end}}
{{end}}

View File

@ -0,0 +1,5 @@
{{define "body_content"}}
<div class="notification is-danger">
<strong>Ung&uuml;ltiges Login!</strong>
</div>
{{end}}

46
snippets/login.html Normal file
View File

@ -0,0 +1,46 @@
{{define "body_content"}}
{{ if ne .UserName "" }}
Sie sind bereits eingeloggt!
{{ else }}
<form class="form-signin" id="login-form" action="/login">
<div class="column is-auto">
<div class="columns is-centered">
<div class="column is-4">
<h1 class="title">Login</h1>
<div class="field">
<p class="control has-icons-left has-icons-right">
<input id="email" name="email" class="input is-success is-focused" type="text" placeholder="Email">
<span class="icon is-small is-left">
<i class="fa fa-envelope"></i>
</span>
<span class="icon is-small is-right">
<i class="fa fa-check"></i>
</span>
</p>
</div>
<div class="field">
<p class="control has-icons-left has-icons-right">
<input id="password" name="password" class="input" type="password" placeholder="Password">
<span class="icon is-small is-left">
<i class="fa fa-lock"></i>
</span>
<span class="icon is-small is-right">
<i class="fa fa-check"></i>
</span>
</p>
</div>
<div class="has-text-centered">
<a href="/reset_password.html">Passwort vergessen?</a><br/>
</div>
<br>
<div id="errorbox" class="notification is-danger is-size-7-mobile" style="display: none;">
</div>
<div class="has-text-centered">
<input id="login-button" name="login-button" type="submit" class="form-button button is-primary is-centered" value="Anmelden"></input>
</div>
</div>
</div>
</div>
</form>
{{end}}
{{end}}

8
snippets/order.html Normal file
View File

@ -0,0 +1,8 @@
{{define "body_content"}}
<p class="title is-4">Bestellformular</p>
<article class="message is-danger">
<div class="message-body">
Aktuell noch in Entwicklung, kommen Sie sp&auml;ter noch einmal vorbei...
</div>
</article>
{{end}}

View File

@ -0,0 +1,46 @@
{{define "body_content"}}
{{ if ne .UserName "" }}
Sie sind bereits eingeloggt!
{{ else }}
<p class="title is-4">Passwort zur&uuml;cksetzen</p>
<div class="notification is-info">
<p>Hier k&ouml;nnen Sie ein neues Passwort setzen. Sie erhalten anschliessend eine Meldung zur Best&auml;tigung zugesendet. Das neue Passwort wird erst g&uuml;ltig, wenn Sie die Best&auml;tigung durchgef&uuml;hrt haben.</p>
</div>
<form class="form-signin" id="reset-password-form" action="/reset_password">
<div class="column is-auto">
<div class="columns is-centered">
<div class="column is-4">
<div class="field">
<p class="control has-icons-left has-icons-right">
<input id="email" name="email" class="input is-success is-focused" type="text" placeholder="Email">
<span class="icon is-small is-left">
<i class="fa fa-envelope"></i>
</span>
<span class="icon is-small is-right">
<i class="fa fa-check"></i>
</span>
</p>
</div>
<div class="field">
<p class="control has-icons-left has-icons-right">
<input id="password" name="password" class="input" type="password" placeholder="Password">
<span class="icon is-small is-left">
<i class="fa fa-lock"></i>
</span>
<span class="icon is-small is-right">
<i class="fa fa-check"></i>
</span>
</p>
</div>
<br>
<div id="errorbox" class="notification is-danger is-size-7-mobile" style="display: none;">
</div>
<div class="has-text-centered">
<input id="login-button" name="login-button" type="submit" class="form-button button is-primary is-centered" value="Passwort zur&uuml;cksetzen"></input>
</div>
</div>
</div>
</div>
</form>
{{end}}
{{end}}

7
snippets/scales.html Normal file
View File

@ -0,0 +1,7 @@
{{define "body_content"}}
{{ if ne .UserName "" }}
<p>Datenauswertung</p>
{{ else }}
<h4>Bitte zuerst <a href="login.html">einloggen</a></h4>
{{end}}
{{end}}

View File

@ -0,0 +1,5 @@
{{define "body_content"}}
<div class="notification is-danger">
<strong>Benutzer existiert nicht!</strong>
</div>
{{end}}

1
static/css/bulma-0.7.4/bulma.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
.image.is-10by1 img, .image.is-20by3 img {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 100%;
}
.image.is-10by1 {
padding-top: 10%;
}
.image.is-20by3 {
padding-top: 15%;
}
hr {
margin: 0px 0 5px 0;
}
.signup-box {
margin: auto;
width: 300px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.3);
border-radius: 10px;
}

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

2
static/js/jquery-3.3.1/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,70 @@
$(document).ready(function() {
$("#email").focus();
// Check for click events on the navbar burger icon
$(".navbar-burger").click(function() {
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
$(".navbar-burger").toggleClass("is-active");
$(".navbar-menu").toggleClass("is-active");
});
// Login Button
$("#login-button").click(function(e){
//alert(true);
$("#login-form").submit();
});
// Login Form
$("#login-form-blabla").submit(function(e){
e.preventDefault();
var formData = {
next: $("#email").val(),
email: $("#email").val(),
password: $("#password").val(),
csrf_token: $("#csrf_token").val(),
next: $("#next").val()
};
//console.log(formData);
// send ajax
$.ajax({
url: '/login', // url where to submit the request
type : "POST", // type of action POST || GET
dataType : 'json', // data type
contentType: 'application/json',
data : JSON.stringify(formData), // post data || get data
success : function(result) {
// you can see the result from the console
// tab of the developer tools
console.log('SUCCESS');
console.log(result);
window.location.replace("/");
},
error: function(result) {
//console.log(xhr, resp, text);
console.log('ERROR');
console.log(result);
var errortext = '<ul style="list-style-type:disc">';
a = result.responseJSON.response.errors.email;
if (a != undefined) {
for (i=0; i < a.length; ++i) {
errortext = errortext + "<li>" + a[i] + "</li>";
}
}
a = result.responseJSON.response.errors.password;
if (a != undefined) {
for (i=0; i < a.length; ++i) {
errortext = errortext + "<li>" + a[i] + "</li>";
}
}
errortext = errortext + "</ul>";
$('#errorbox').html(errortext);
$('#errorbox').show();
}
})
});
});

View File

@ -0,0 +1,15 @@
[Unit]
Description=mini-beieli web service
After=syslog.target
After=network.target
[Service]
Type=simple
User=beieli
Group=beieli
WorkingDirectory=/home/beieli/mini-beieli-web
ExecStart=/home/beieli/mini-beieli-web/mini-beieli-web
Restart=always
[Install]
WantedBy=multi-user.target

88
templates/layout.html Normal file
View File

@ -0,0 +1,88 @@
{{define "header_additions"}}{{end}}
{{define "layout"}}<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>mini-beieli.ch - die besondere Bienenstockwaage</title>
<link rel="stylesheet" href="/static/css/bulma-0.7.4/bulma.min.css">
<link rel="stylesheet" href="static/css/mini-beieli-web.css">
<script defer src="/static/js/fontawesome-5.1.0/all.js"></script>
<script src="/static/js/jquery-3.3.1/jquery.min.js"></script>
{{template "header_additions" . }}
</head>
<body>
<section class="section">
<div class="container">
<figure class="image is-hidden-mobile is-20by3">
<img src="/static/images/bees-banner-1000x150.jpg" alt="mini-beieli Banner" >
</figure>
<figure class="image is-hidden-tablet is-3by1">
<img src="/static/images/bees-banner-450x150.jpg" alt="mini-beieli Banner" >
</figure>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<img src="/static/images/bee-logo-28.png" alt="mini-beieli Logo" width="28" height="28">
</a>
<a role="button" class="navbar-burger" data-target="navMenu" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu" id="navMenu">
<div class="navbar-start">
<a class="navbar-item" href="/">
<div style="position:relative">
<span class="icon"><i class="fa fa-home"></i></span>
<span>Home</span>
</div>
</a>
<a class="navbar-item" href="/contact.html">
<div style="position:relative">
<span class="icon"><i class="fa fa-address-card"></i></span>
<span>Kontakt</span>
</div>
</a>
{{ if ne .UserName "" }}
<a class="navbar-item" href="/scales.html">
<div style="position:relative">
<span class="icon"><i class="fa fa-balance-scale"></i></span>
<span>Meine Waagen</span>
</div>
</a>
{{ end }}
</div>
<div class="navbar-end">
{{ if ne .UserName "" }}
<a class="navbar-item" href="/logout">
<div style="position:relative">
<span class="icon"><i class="fa fa-sign-out-alt"></i></span>
<span>Logout {{ .UserName }}</span>
</div>
</a>
{{ else }}
<a class="navbar-item" href="/login.html">
<div style="position:relative">
<span class="icon"><i class="fa fa-sign-in-alt"></i></span>
<span>Login</span>
</div>
</a>
{{ end }}
</div>
</div>
</nav>
<hr />
{{template "body_content" . }}
</div>
</section>
<script src="/static/js/mini-beieli-web.js"></script>
</body>
</html>
{{end}}