Initial commit

This commit is contained in:
Joerg Lehmann 2021-03-09 20:32:57 +01:00
commit 0420b3efbd
56 changed files with 94426 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
wo-bisch-web
nohup.out
check_nodes/check_nodes

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# wo-bisch-web - LoraWAN GPS Tracker
Webapplikation, geschrieben in Golang.
## Administration
### Redis Commands
```
$ redis-cli
Show all keys:
127.0.0.1:6379> keys *
Create new user (devs prefixed with @ are read-only devs):
127.0.0.1:6379> HMSET user:joerg.lehmann@nbit.ch my_devs "0002CC01000003E4,@0002CC01000003E5"
Show all values of a key
127.0.0.1:6379> HGETALL user:joerg.lehmann@nbit.ch
1) "last_login"
2) "2020-04-11 13:41:57"
3) "confirm_id"
4) ""
5) "password"
6) "$2a$10$XdDSG2E9SpVuxLE59JWwsO9aJtbrwArSflBGwGVvjWDEQecXEUo06"
7) "my_devs"
8) "0002CC01000003E4,@0002CC01000003F5,0002CC01000003F1,0002CC01000003F7,0002CC01000003F3,0002CC01000003E1,0002CC01000003D4,0002CC01000003EC,0002CC01000003E2"
9) "new_password"
10) "$2a$10$XdDSG2E9SpVuxLE59JWwsO9aJtbrwArSflBGwGVvjWDEQecXEUo06"
Set password (htpasswd in httpd-tools rpm):
$ htpasswd -nbBC 5 USER PASSWORD
27.0.0.1:6379> HMSET user:joerg.lehmann@nbit.ch password '$2a$10$TmNA6PDKWBnMw/XcJ0DDi.zpWzZB0RYyrKc8Bh3x6LHAyCxbByhkC'
Change active_until date:
127.0.0.1:6379> HMSET dev:0002CC01000003E2 active_until "01.05.2021"
show range of last values:
127.0.0.1:6379> LRANGE lastvalues:0002CC01000003E4 0 -1
```
Autor: Joerg Lehmann, nbit Informatik GmbH

130
authentication.go Normal file
View File

@ -0,0 +1,130 @@
package main
import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/gorilla/securecookie"
"net/http"
)
// 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"
// .. check credentials ..
if checkLoginCredentials(name, pass) {
redirectTarget = "/tracker.html"
logit(fmt.Sprintf("loginHandler: successful login for User %s", name))
setSession(name, response)
updateLoginTime(name)
} 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 := "/wait_for_password_confirmation.html"
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))
if confirmUser(confirm_id) {
http.Redirect(response, request, "/password_changed.html", 302)
} else {
http.Redirect(response, request, "/", 302)
}
}

395
check_nodes/main.go Normal file
View File

@ -0,0 +1,395 @@
package main
import (
"bufio"
"bytes"
"fmt"
"github.com/gomodule/redigo/redis"
"github.com/jordan-wright/email"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
"time"
)
func sendEmailAccu(username string, alias string, deveui string, accu_percent string, threshold int, level string) {
fmt.Printf("SEND EMAIL ACCU (%s) - %s:%s\n", level, username, deveui)
mail_message := `Lieber Benutzer von wo-bisch.ch
Der Akku von "` + alias + `" (DevEUI: ` + deveui + `) ist noch zu ` + accu_percent + ` Prozent geladen.
Bitte rechtzeitig wieder laden! Bei 10%-er Ladung erscheint die letzte Warnung.
Mit freundlichen Grüssen
--
wo-bisch.ch`
e := email.NewEmail()
e.From = "wo-bisch.ch <info@wo-bisch.ch>"
e.To = []string{username}
e.Bcc = []string{"joerg.lehmann@nbit.ch"}
e.Subject = level + " - wo-bisch.ch: Akku Ladezustand (" + alias + ")"
e.Text = []byte(mail_message)
e.Send("127.0.0.1:25", nil)
}
func sendEmailAbo(username string, alias string, deveui string, days_left int, level string) {
fmt.Printf("SEND EMAIL ABO (%s) - %s:%s\n", level, username, deveui)
mail_message := `Lieber Benutzer von wo-bisch.ch
Das Abo von "` + alias + `" (DevEUI: ` + deveui + `) laeuft in ` + strconv.Itoa(days_left) + ` Tagen ab.
Bitte Abo verlaengern auf https://wo-bisch.ch
Mit freundlichen Grüssen
--
wo-bisch.ch`
e := email.NewEmail()
e.From = "wo-bisch.ch <info@wo-bisch.ch>"
e.To = []string{username}
e.Bcc = []string{"joerg.lehmann@nbit.ch"}
e.Subject = level + " - wo-bisch.ch: Abo laeuft ab (" + alias + ")"
e.Text = []byte(mail_message)
e.Send("127.0.0.1:25", nil)
}
var globalPool *redis.Pool
const userPrefix string = "user:"
const devPrefix string = "dev:"
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
}
type Dev struct {
Deveui string
Alias string
Alarmactive string
Smsnumber string
ActiveUntil string // Abo bezahlt bis TT.MM.YYYY
}
func initDB() {
// newPool returns a pointer to a redis.Pool
pool := newPool()
// get a connection from the globalPool (redis.Conn)
conn := pool.Get()
defer conn.Close()
globalPool = pool
// wir machen einen Connection Test
ping(conn)
}
func closeDB() {
globalPool.Close()
}
func getUsers() []string {
res := []string{}
conn := globalPool.Get()
defer conn.Close()
//logit("getUsers")
users, err := redis.Strings(conn.Do("KEYS", userPrefix+"*"))
if err == nil {
//logit("getUsers successful!")
res = users
} else {
log.Print(err)
}
return res
}
func getMyDevs(username string) []string {
res := []string{}
if username == "" {
return res
}
conn := globalPool.Get()
defer conn.Close()
//logit("getMyDevs: User: " + username)
mydevs, err := redis.String(conn.Do("HGET", userPrefix+username, "my_devs"))
if err == nil {
//logit("getMyDevs: mydevs: " + mydevs)
res = strings.Split(mydevs, ",")
} else {
log.Print(err)
}
return res
}
func getDevAlias(deveui string) string {
res := deveui
if deveui == "" {
return res
}
conn := globalPool.Get()
defer conn.Close()
//logit("getDevAlias: Deveui: " + deveui)
alias, err := redis.String(conn.Do("HGET", devPrefix+deveui, "alias"))
if err == nil {
//logit("getDevAlias: alias: " + alias)
res = alias
} else {
log.Print(err)
}
return res
}
func getActiveUntil(deveui string) string {
res := ""
if deveui == "" {
return res
}
conn := globalPool.Get()
defer conn.Close()
//logit("getActiveUntil: Deveui: " + deveui)
activeuntil, err := redis.String(conn.Do("HGET", devPrefix+deveui, "active_until"))
if err == nil {
//logit("getActiveUntil: activeuntil: " + activeuntil)
res = activeuntil
} else {
log.Print(err)
}
return res
}
func InsertAlert(prefix string, deveui string, email string, threshold int) {
conn := globalPool.Get()
defer conn.Close()
_, err := conn.Do("SET", prefix+deveui+":"+email, threshold)
if err != nil {
logit("InsertAlert: Error inserting: " + prefix + deveui + ":" + email)
}
}
func DeleteAlert(prefix string, deveui string, email string) {
conn := globalPool.Get()
defer conn.Close()
exists, _ := redis.Bool(conn.Do("EXISTS", prefix+deveui+":"+email))
if exists {
_, err := conn.Do("DEL", prefix+deveui+":"+email)
if err != nil {
logit("DeleteAlert: Error deleting: " + prefix + deveui + ":" + email)
}
}
}
func AlarmNotAlreadySent(prefix string, deveui string, email string, threshold int) bool {
conn := globalPool.Get()
defer conn.Close()
exists, _ := redis.Bool(conn.Do("EXISTS", prefix+deveui+":"+email))
if !exists {
return true
}
alarm_threshold, _ := redis.Int(conn.Do("GET", prefix+deveui+":"+email))
return threshold != alarm_threshold
}
type OneMetric struct {
Deveui string
Alias string
Timestamp string
BatteryPercent string
ActiveUntil string
DaysUntilDeactivated int // berechneter Wert
}
func CalcDaysUntil(mydate string) int {
var days int
layout := "02.01.2006"
t, err := time.Parse(layout, mydate)
if err != nil {
days = 0
}
days = int(t.Sub(time.Now()).Hours() / 24)
return days
}
func getLastMetrics(deveui string) OneMetric {
var res OneMetric
url := "http://localhost:8086/api/v2/query?org=wobischorg"
data := []byte(fmt.Sprintf(`from(bucket:"wobischbucket")
|> range(start:-5d)
|> filter(fn: (r) => r._measurement == "measurement" and r.deveui == "%s")
|> filter(fn: (r) => r._field == "vp")
|> last() |> yield(name: "last")`, deveui))
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
log.Fatal("Error reading request. ", err)
}
// Set headers
req.Header.Set("Authorization", "Token nKYCoz3TA-LItYXG988DjdiStMhrfKmFXQqzxrjzJJ7Ek_iUttzFSE9lfe3s6q99EMdcrjuGlDAjp4Y0VnNRXw==")
req.Header.Set("accept", "application/csv")
req.Header.Set("content-type", "application/vnd.flux")
// Set client timeout
client := &http.Client{Timeout: time.Second * 10}
// Send request
resp, err := client.Do(req)
if err != nil {
log.Fatal("Error reading response. ", err)
}
defer resp.Body.Close()
//fmt.Println("response Status:", resp.Status)
//fmt.Println("response Headers:", resp.Header)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal("Error reading body. ", err)
}
//fmt.Println("response Body:", string(body))
scanner := bufio.NewScanner(strings.NewReader(string(body)))
location, err := time.LoadLocation("Europe/Zurich")
for scanner.Scan() {
s := strings.Split(scanner.Text(), ",")
if (len(s) >= 7) && !(strings.HasPrefix(s[5], "_")) {
mytime, err := time.Parse(time.RFC3339, s[5])
if err != nil {
continue
}
res.Timestamp = mytime.In(location).Format("02.01.2006 15:04")
value := s[6]
field := s[7]
if field == "vp" {
res.BatteryPercent = value
}
}
}
res.Deveui = deveui
res.Alias = getDevAlias(deveui)
res.ActiveUntil = getActiveUntil(deveui)
res.DaysUntilDeactivated = CalcDaysUntil(res.ActiveUntil)
return res
}
func CheckThreshold(d string, vp int, u2 string, last_metric OneMetric, info_threshold int, warning_threshold int, alert_threshold int) bool {
var alias string
if vp <= info_threshold {
alias = getDevAlias(d)
}
if vp <= alert_threshold {
if AlarmNotAlreadySent("alarm_sent_accu:", d, u2, alert_threshold) {
sendEmailAccu(u2, alias, d, last_metric.BatteryPercent, alert_threshold, "ALARM")
InsertAlert("alarm_sent_accu:", d , u2, alert_threshold)
}
return false
}
if vp <= warning_threshold {
if AlarmNotAlreadySent("alarm_sent_accu:", d, u2, warning_threshold) {
sendEmailAccu(u2, alias, d, last_metric.BatteryPercent, warning_threshold, "WARNING")
InsertAlert("alarm_sent_accu:", d, u2, warning_threshold)
}
return false
}
if vp <= info_threshold {
if AlarmNotAlreadySent("alarm_sent_accu:", d, u2, info_threshold) {
sendEmailAccu(u2, alias, d, last_metric.BatteryPercent, alert_threshold, "INFO")
InsertAlert("alarm_sent_accu:", d, u2, info_threshold)
}
return false
}
return true
}
func logit(log_message string) {
log.Println(log_message)
}
func main() {
logit("Starting check_battery...")
initDB()
defer closeDB()
users := getUsers()
for _, u := range users {
//fmt.Println(u)
u2 := u[5:]
my_devs := getMyDevs(u2)
for _, d := range my_devs {
//fmt.Printf("%s:%s\n", u2, d)
if !strings.HasPrefix(d, "@") {
last_metric := getLastMetrics(d)
// Zuerst der Batteriealarm
if last_metric.BatteryPercent != "" {
fmt.Printf("%s:%s:%s Percent:%s:%d\n", u2, d, last_metric.BatteryPercent, last_metric.ActiveUntil, last_metric.DaysUntilDeactivated)
vp, _ := strconv.Atoi(last_metric.BatteryPercent)
if CheckThreshold(d, vp, u2, last_metric, 50, 20, 10) {
DeleteAlert("alarm_sent_accu:", d, u2)
}
}
// Jetzt der Alarm wegen der Abodauer
if last_metric.DaysUntilDeactivated < 30 {
fmt.Printf("SEND EMAIL %s:%s:%s Percent:%s:%d\n", u2, d, last_metric.BatteryPercent, last_metric.ActiveUntil, last_metric.DaysUntilDeactivated)
alias := getDevAlias(d)
sendEmailAbo("joerg.lehmann@nbit.ch", alias, d, last_metric.DaysUntilDeactivated, "INFO")
}
}
}
}
logit("Done with check_battery...")
}

20
helper.go Normal file
View File

@ -0,0 +1,20 @@
package main
import (
"log"
)
// Contains tells whether a contains x.
func Contains(a []string, x string) bool {
log.Println("Search for: " + x)
for _, n := range a {
log.Println("Piece of Array: " + n)
if x == n {
return true
}
if "@"+x == n {
return true
}
}
return false
}

9
log.go Normal file
View File

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

78
mail.go Normal file
View File

@ -0,0 +1,78 @@
package main
import (
"bytes"
"log"
"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@wo-bisch.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://wo-bisch.ch, bitte bestaetigen
Lieber Benutzer von wo-bisch.ch
Sie haben soeben eine Passwortaenderung veranlasst. Bitte klicken Sie folgenden Link,
um das neue Passwort zu aktivieren:
https://wo-bisch.ch/confirm?id=` + confirm_id + `
Bitte ignorieren Sie diese Meldung, falls die Aenderung nicht von Ihnen angefordert wurde!
Mit freundlichen Grüssen
--
wo-bisch.ch`
buf := bytes.NewBufferString(mail_message)
if _, err = buf.WriteTo(wc); err != nil {
log.Fatal(err)
}
}
func sendPaymentConfirmationEmail(username, charge_data string, amount int64) {
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("info@wo-bisch.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: Zahlungsbestaetigung wo-bisch.ch
Lieber Benutzer von wo-bisch.ch
Sie haben soeben erfolgreich folgende Abo-Verlaengerungen bezahlt:
` + charge_data + `
Mit freundlichen Grüssen
--
wo-bisch.ch`
buf := bytes.NewBufferString(mail_message)
if _, err = buf.WriteTo(wc); err != nil {
log.Fatal(err)
}
}

105
main.go Normal file
View File

@ -0,0 +1,105 @@
package main
import (
"html/template"
"net/http"
"os"
"path"
"strings"
"time"
)
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")
var trackers = getMyDevs(userName)
var last_metrics []OneMetric
query_values := r.URL.Query()
if r.URL.Path == "/tracker.html" {
// wir holen noch die letzten Metriken
for _, v := range trackers {
deveui := v
readonly := false
if strings.HasPrefix(deveui, "@") {
deveui = deveui[1:]
readonly = true
}
last_metric := getLastMetrics(deveui)
last_metric.Readonly = readonly
last_metrics = append(last_metrics, last_metric)
}
}
data := struct {
UserName string
DateTimeString string
Tracker []string
LastMetrics []OneMetric
QueryValues map[string][]string
}{
userName,
datetimestring,
trackers,
last_metrics,
query_values,
}
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)
http.HandleFunc("/metrics", metricsHandler)
http.HandleFunc("/lastmetrics", lastmetricsHandler)
http.HandleFunc("/save_tracker_settings", save_tracker_settingsHandler)
http.HandleFunc("/getstripepaymentintent", getstripepaymentintentHandler)
http.HandleFunc("/stripewebhook", stripeWebhookHandler)
logit("Starting Web Application...")
http.ListenAndServe("127.0.0.1:4000", nil)
logit("Terminating Web Application...")
}

354
metrics.go Normal file
View File

@ -0,0 +1,354 @@
package main
import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"time"
)
type OneMetric struct {
Deveui string
Alias string
Readonly bool
Timestamp string
Lat string
Lon string
Vbat string
Fw string
BatteryPercent string
ActiveUntil string
DaysUntilDeactivated int // berechneter Wert
}
// metrics handler
func validProperty(prop string) bool {
valid_properties := [...]string{"lat", "lon", "vbat", "fw"}
for _, p := range valid_properties {
if p == prop {
return true
}
}
return false
}
func metricsHandler(response http.ResponseWriter, request *http.Request) {
name := getUserName(request)
if name != "" {
property, ok := request.URL.Query()["property"]
if !ok || len(property[0]) < 1 {
log.Println("Url Param 'property' is missing")
fmt.Fprintf(response, "{ \"msg\": \"error: property must be specified in URL\" }")
return
}
if !validProperty(property[0]) {
log.Println("Url Param 'property' is invalid")
fmt.Fprintf(response, "{ \"msg\": \"error: invalid property\" }")
return
}
deveui, ok := request.URL.Query()["deveui"]
if !ok || len(deveui[0]) < 1 {
log.Println("Url Param 'deveui' is missing")
fmt.Fprintf(response, "{ \"msg\": \"error: deveui must be specified in URL\" }")
return
}
// Query()["deveui"] will return an array of items,
// we only want the single item.
mydeveui := deveui[0]
if !(Contains(getMyDevs(name), mydeveui)) {
log.Println("specified 'deveui' does not belong to this user")
fmt.Fprintf(response, "{ \"msg\": \"error: specified deveui does not belong to this user\" }")
return
}
if AboExpired(mydeveui) {
log.Println("specified 'deveui' has an expired abo")
fmt.Fprintf(response, "{ \"msg\": \"specified deveui has an expired abo\" }")
return
}
log.Println("Url Param 'deveui' is: " + string(mydeveui))
// Format of start and stop: YYYY-MM-DDTHH:MI:SSZ
stop, ok := request.URL.Query()["stop"]
var mystop string
if !ok || len(stop[0]) < 1 {
log.Println("Url Param 'stop' is missing, set it to now")
mystop = time.Now().Format("2006-01-02T15:04:05Z")
}
if ok {
mystop = stop[0]
}
layout := "2006-01-02T15:04:05Z"
stopDate, err := time.Parse(layout, mystop)
if err != nil {
fmt.Println(err)
}
start, ok := request.URL.Query()["start"]
var mystart string
if !ok || len(start[0]) < 1 {
log.Println("Url Param 'start' is missing, set it to stop minus one day")
t := stopDate.AddDate(0, 0, -1)
mystart = t.Format("2006-01-02T15:04:05Z")
}
if ok {
mystart = start[0]
}
url := "http://localhost:8086/api/v2/query?org=wobischorg"
data := []byte(fmt.Sprintf(`from(bucket:"wobischbucket") |> range(start: %s, stop: %s) |> filter(fn: (r) => r._measurement == "measurement") |> filter(fn: (r) => r._field == "%s") |> filter(fn: (r) => r.deveui == "%s")`, mystart, mystop, property[0], mydeveui))
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
log.Fatal("Error reading request. ", err)
}
// Set headers
req.Header.Set("Authorization", "Token TQvQxxLLAj1kTKWuEqcx7BA-KfE6WtJUeDlPa_Dnvms6Zqf6uh6lMbpXtzcsCjKO_x3PrpxxGDR5E6YnDB5PFg==")
req.Header.Set("accept", "application/csv")
req.Header.Set("content-type", "application/vnd.flux")
// Set client timeout
client := &http.Client{Timeout: time.Second * 10}
// Send request
resp, err := client.Do(req)
if err != nil {
log.Fatal("Error reading response. ", err)
}
defer resp.Body.Close()
fmt.Println("response Status:", resp.Status)
fmt.Println("response Headers:", resp.Header)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal("Error reading body. ", err)
}
fmt.Fprintf(response, "[\n")
scanner := bufio.NewScanner(strings.NewReader(string(body)))
first := true
for scanner.Scan() {
s := strings.Split(scanner.Text(), ",")
if (len(s) >= 7) && !(strings.HasPrefix(s[5], "_")) {
t, err := time.Parse(time.RFC3339, s[5])
if err != nil {
continue
}
a := t.Unix()
b := s[6]
if !(first) {
fmt.Fprintf(response, ",")
} else {
first = false
}
fmt.Fprintf(response, "[%d000,%s]\n", a, b)
}
}
fmt.Fprintf(response, "]\n")
} else {
fmt.Fprintf(response, "{ \"msg\": \"Only available for logged in users\" }")
}
}
func lastmetricsHandler(response http.ResponseWriter, request *http.Request) {
name := getUserName(request)
if name != "" {
deveui, ok := request.URL.Query()["deveui"]
if !ok || len(deveui[0]) < 1 {
log.Println("Url Param 'deveui' is missing")
fmt.Fprintf(response, "{ \"msg\": \"deveui must be specified in URL\" }")
return
}
// Query()["deveui"] will return an array of items,
// we only want the single item.
mydeveui := deveui[0]
if !(Contains(getMyDevs(name), mydeveui)) {
log.Println("specified 'deveui' does not belong to this user")
fmt.Fprintf(response, "{ \"msg\": \"specified deveui does not belong to this user\" }")
return
}
log.Println("Url Param 'deveui' is: " + string(mydeveui))
url := "http://localhost:8086/api/v2/query?org=wobischorg"
data := []byte(fmt.Sprintf(`from(bucket:"wobischbucket")
|> range(start:-365d)
|> filter(fn: (r) => r._measurement == "measurement" and r.deveui == "%s")
|> filter(fn: (r) => r._field == "lat" or r._field == "lon" or r._field == "vbat" or r._field == "fw")
|> last(column: "_time") |> yield(name: "last")`, mydeveui))
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
log.Fatal("Error reading request. ", err)
}
// Set headers
req.Header.Set("Authorization", "Token TQvQxxLLAj1kTKWuEqcx7BA-KfE6WtJUeDlPa_Dnvms6Zqf6uh6lMbpXtzcsCjKO_x3PrpxxGDR5E6YnDB5PFg==")
req.Header.Set("accept", "application/csv")
req.Header.Set("content-type", "application/vnd.flux")
// Set client timeout
client := &http.Client{Timeout: time.Second * 10}
// Send request
resp, err := client.Do(req)
if err != nil {
log.Fatal("Error reading response. ", err)
}
defer resp.Body.Close()
fmt.Println("response Status:", resp.Status)
fmt.Println("response Headers:", resp.Header)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal("Error reading body. ", err)
}
fmt.Println("response Body 1:", string(body))
scanner := bufio.NewScanner(strings.NewReader(string(body)))
ts := ""
lat := ""
lon := ""
vbat := ""
fw := ""
location, err := time.LoadLocation("Europe/Zurich")
for scanner.Scan() {
s := strings.Split(scanner.Text(), ",")
if (len(s) >= 7) && !(strings.HasPrefix(s[5], "_")) {
mytime, err := time.Parse(time.RFC3339, s[5])
if err != nil {
continue
}
ts = mytime.In(location).Format("02.01.2006 15:04")
value := s[6]
field := s[7]
if field == "lat" {
lat = value
} else if field == "lon" {
lon = value
} else if field == "vbat" {
vbat = value
} else if field == "fw" {
fw = value
}
}
}
fmt.Fprintf(response, `{
"ts": "%s",
"lat": "%s",
"lon": "%s",
"vbat": "%s",
"fw": "%s"
}`, ts, lat, lon, vbat, fw)
} else {
fmt.Fprintf(response, "{ \"msg\": \"Only available for logged in users\" }")
}
}
func CalcDaysUntil(mydate string) int {
var days int
layout := "02.01.2006"
t, err := time.Parse(layout, mydate)
if err != nil {
days = 0
}
days = int(t.Sub(time.Now()).Hours() / 24)
return days
}
func getLastMetrics(deveui string) OneMetric {
var res OneMetric
url := "http://localhost:8086/api/v2/query?org=wobischorg"
data := []byte(fmt.Sprintf(`from(bucket:"wobischbucket")
|> range(start:-365d)
|> filter(fn: (r) => r._measurement == "measurement" and r.deveui == "%s")
|> filter(fn: (r) => r._field == "lat" or r._field == "lon" or r._field == "vbat" or r._field == "fw")
|> last(column: "_time") |> yield(name: "last")`, deveui))
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
log.Fatal("Error reading request. ", err)
}
// Set headers
req.Header.Set("Authorization", "Token TQvQxxLLAj1kTKWuEqcx7BA-KfE6WtJUeDlPa_Dnvms6Zqf6uh6lMbpXtzcsCjKO_x3PrpxxGDR5E6YnDB5PFg==")
req.Header.Set("accept", "application/csv")
req.Header.Set("content-type", "application/vnd.flux")
// Set client timeout
client := &http.Client{Timeout: time.Second * 10}
// Send request
resp, err := client.Do(req)
if err != nil {
log.Fatal("Error reading response. ", err)
}
defer resp.Body.Close()
fmt.Println("response Status:", resp.Status)
fmt.Println("response Headers:", resp.Header)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal("Error reading body. ", err)
}
fmt.Println("response Body 2:", string(body))
scanner := bufio.NewScanner(strings.NewReader(string(body)))
location, err := time.LoadLocation("Europe/Zurich")
for scanner.Scan() {
s := strings.Split(scanner.Text(), ",")
fmt.Printf("BlaBla: %v\n", s)
if (len(s) >= 7) && !(strings.HasPrefix(s[5], "_")) {
mytime, err := time.Parse(time.RFC3339, s[5])
if err != nil {
continue
}
res.Timestamp = mytime.In(location).Format("02.01.2006 15:04")
value := s[6]
field := s[7]
if field == "lat" {
res.Lat = value
} else if field == "lon" {
res.Lon = value
} else if field == "vbat" {
res.Vbat = value
} else if field == "fw" {
res.Fw = value
}
}
}
res.Deveui = deveui
res.Alias = getDevAlias(deveui)
res.Readonly = false
res.ActiveUntil = getActiveUntil(deveui)
res.DaysUntilDeactivated = CalcDaysUntil(res.ActiveUntil)
return res
}

29
minify-fa-js.py Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/python3
#
# Minify Font Awesome js.all
#
# Remove all unused icons
#
# Pass a list of icon names (remove the "fa-" prefix), comma separated
#
# Usage example: minify-fa-js.py home,address-card
#
# Reads from STDIN, writes to STDOUT
#
import sys
import re
if len(sys.argv) < 2:
print("Usage (example): minify-fa-js.py home,address-card")
sys.exit(1)
fontnames = sys.argv[1].split(",")
for line in sys.stdin:
if re.search(r"\"[0-9a-z-]+\": \[", line):
for fontname in fontnames:
ss = '"'+fontname+'": ['
if ss in line:
print(line),
else:
print(line),

1
minify-fa.sh Executable file
View File

@ -0,0 +1 @@
cat /home/appuser/wo-bisch-web/static/js/fontawesome-5.11.2/all.js |/home/appuser/wo-bisch-web/minify-fa-js.py home,address-card,balance-scale,sign-out-alt,sign-in-alt,calendar,balance-scale,thermometer-half,tint,cloud,battery-three-quarters,envelope,check,lock,exclamation-triangle,cog,plus,shopping-cart,minus,arrow-right,caret-down,map-marked-alt >/home/appuser/wo-bisch-web/static/js/fontawesome-5.11.2/all-minified.js

362
persistence.go Normal file
View File

@ -0,0 +1,362 @@
package main
import (
"crypto/rand"
"fmt"
"github.com/gomodule/redigo/redis"
"golang.org/x/crypto/bcrypt"
"log"
"strconv"
"strings"
"time"
)
var globalPool *redis.Pool
const userPrefix string = "user:"
const devPrefix string = "dev:"
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
}
type Dev struct {
Deveui string
Alias string
ActiveUntil string // Abo bezahlt bis TT.MM.YYYY
}
func initDB() {
// newPool returns a pointer to a redis.Pool
pool := newPool()
// get a connection from the globalPool (redis.Conn)
conn := pool.Get()
defer conn.Close()
globalPool = pool
// wir machen einen Connection Test
ping(conn)
// 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() {
globalPool.Close()
}
func getUsers() []string {
res := []string{}
conn := globalPool.Get()
defer conn.Close()
logit("getUsers")
users, err := redis.Strings(conn.Do("KEYS", userPrefix+"*"))
if err == nil {
logit("getUsers successful!")
res = users
} else {
log.Print(err)
}
return res
}
func updateTrackerSettings(trackerSettings Dev) error {
conn := globalPool.Get()
defer conn.Close()
// SET object
_, err := conn.Do("HMSET", devPrefix+trackerSettings.Deveui, "alias", trackerSettings.Alias)
if err != nil {
return err
}
return nil
}
func checkUserAvailable(username string) bool {
logit("checkUserAvailable: User: " + username)
conn := globalPool.Get()
defer conn.Close()
res, err := redis.Int(conn.Do("EXISTS", userPrefix+username))
if err == nil {
logit("Result of EXISTS: " + strconv.Itoa(res))
return res == 0
} else {
logit("checkUserAvailable: Error to query Key Value Store")
return false
}
}
func getMyDevs(username string) []string {
res := []string{}
if username == "" {
return res
}
conn := globalPool.Get()
defer conn.Close()
logit("getMyDevs: User: " + username)
mydevs, err := redis.String(conn.Do("HGET", userPrefix+username, "my_devs"))
if err == nil {
logit("getMyDevs: mydevs: " + mydevs)
res = strings.Split(mydevs, ",")
} else {
log.Print(err)
}
return res
}
func getDevAlias(deveui string) string {
res := deveui
if deveui == "" {
return res
}
conn := globalPool.Get()
defer conn.Close()
logit("getDevAlias: Deveui: " + deveui)
alias, err := redis.String(conn.Do("HGET", devPrefix+deveui, "alias"))
if err == nil {
logit("getDevAlias: alias: " + alias)
res = alias
} else {
log.Print(err)
}
return res
}
func getActiveUntil(deveui string) string {
res := ""
if deveui == "" {
return res
}
conn := globalPool.Get()
defer conn.Close()
logit("getActiveUntil: Deveui: " + deveui)
activeuntil, err := redis.String(conn.Do("HGET", devPrefix+deveui, "active_until"))
if err == nil {
logit("getActiveUntil: activeuntil: " + activeuntil)
res = activeuntil
} else {
log.Print(err)
}
return res
}
func AboExpired(deveui string) bool {
active_until := getActiveUntil(deveui)
layout := "02.01.2006"
t, _ := time.Parse(layout, active_until)
return t.Before(time.Now())
}
func prolongActivation(deveui string, years int) (string, error) {
conn := globalPool.Get()
defer conn.Close()
active_until_old, err := redis.String(conn.Do("HGET", devPrefix+deveui, "active_until"))
if err == nil {
logit("prolongActivation: active_until: " + active_until_old)
} else {
log.Print(err)
}
layout := "02.01.2006"
t, err := time.Parse(layout, active_until_old)
if err != nil {
fmt.Println(err)
}
fmt.Println(t.Unix())
var t_new time.Time
if t.Before(time.Now()) {
t_new = time.Now().AddDate(years, 0, 0)
} else {
t_new = t.AddDate(years, 0, 0)
}
active_until_new := t_new.Format(layout)
// SET object
_, err1 := conn.Do("HMSET", devPrefix+deveui, "active_until", active_until_new)
if err1 != nil {
return "", err1
}
return active_until_new, nil
}
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) {
conn := globalPool.Get()
defer conn.Close()
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 = conn.Do("HMSET", userPrefix+username, "password", string(hashedPassword), "new_password", string(hashedPassword), "confirm_id", confirm_id, "last_login", "", "my_devs", "")
if err != nil {
logit("insertUser: Error inserting User: " + username)
return
}
}
func updateUser(username, password string) {
conn := globalPool.Get()
defer conn.Close()
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 = conn.Do("HMSET", userPrefix+username, "new_password", string(hashedPassword), "confirm_id", confirm_id)
if err != nil {
logit("updateUser: Error updateing User: " + username)
return
}
_, err = conn.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 {
conn := globalPool.Get()
defer conn.Close()
logit("checkLoginCredentials: called with username,password: " + username + "," + password)
pwd, err := redis.String(conn.Do("HGET", userPrefix+username, "password"))
if err == nil {
logit("checkLoginCredentials: pwd: " + pwd + " CMD: HGET " + userPrefix + username + userPrefix + username + " confirm_id")
cid, err := redis.String(conn.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
}
}
}
} else {
log.Print(err)
return false
}
hashedPassword := []byte(pwd)
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(password))
return err == nil
}
func updateLoginTime(username string) {
conn := globalPool.Get()
defer conn.Close()
_, err := conn.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) bool {
conn := globalPool.Get()
defer conn.Close()
u, err := redis.String(conn.Do("GET", confirmPrefix+confirm_id))
if err != nil {
logit("confirmUser: Error with searching confirm_id: " + confirm_id)
return false
}
new_password, err := redis.String(conn.Do("HGET", userPrefix+u, "new_password"))
if err != nil {
logit("confirmUser: Error with getting new_password: " + u)
return false
}
_, err = conn.Do("HMSET", userPrefix+u, "confirm_id", "", "password", new_password)
if err != nil {
logit("confirmUser: Error updateing User: " + u)
return false
}
_, err = conn.Do("DEL", confirmPrefix+confirm_id)
if err != nil {
logit("confirmUser: Error deleting confirm_id: " + confirm_id)
return false
}
return true
}

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}}

19
snippets/checkout.html Normal file
View File

@ -0,0 +1,19 @@
{{define "header_additions"}}
<script src="https://js.stripe.com/v3/"></script>
{{end}}
{{define "body_content"}}
<p class="title is-4">Checkout</p>
<p id="charge_data" hidden>0002CC01000003F7:2</p>
<div id="card-element">
<!-- Elements will create input elements here -->
</div>
<!-- We'll put the error messages in this element -->
<div id="card-errors" role="alert"></div>
<button id="submit">Pay</button>
<script src="static/js/checkout.js"></script>
{{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}}

63
snippets/graph.html Normal file
View File

@ -0,0 +1,63 @@
{{define "body_content"}}
{{ if ne .UserName "" }}
<script src="https://cdn.jsdelivr.net/jquery/latest/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/momentjs/latest/moment-with-locales.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
<link href="static/bulma-calendar/css/bulma-calendar.min.css" rel="stylesheet">
<script src="static/bulma-calendar/js/bulma-calendar.min.js"></script>
<div class="columns" id="myselectors">
<div class="column is-half py-1">
<div id="reportrange" style="background: #fff; cursor: pointer; padding: 5px 10px; border: 1px solid #ccc; width: 100%">
<i class="fa fa-calendar"></i>&nbsp;
<span></span> <i class="fa fa-caret-down"></i>
</div>
</div>
<div class="column is-half py-1">
<a id="btn_w" class="button">
<span class="icon">
<i class="fa fa-balance-scale"></i>
</span>
</a>
<a id="btn_t" class="button">
<span class="icon">
<i class="fa fa-thermometer-half"></i>
</span>
</a>
<a id="btn_h" class="button">
<span class="icon">
<i class="fa fa-tint"></i>
</span>
</a>
<a id="btn_p" class="button">
<span class="icon">
<i class="fa fa-cloud"></i>
</span>
</a>
<a id="btn_vp" class="button">
<span class="icon">
<i class="fa fa-battery-three-quarters"></i>
</span>
</a>
</div>
</div>
<script src="static/js/graph.js"></script>
<div>
<section id="chart">
<!-- Content ... -->
</section>
</div>
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script src="static/js/chart.js"></script>
{{ else }}
<h4>Bitte zuerst <a href="login.html">einloggen</a></h4>
{{end}}
{{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">GPS Tracking leichtgemacht...</p>
<div class="message is-danger">
<div class="message-body">
Aktuell noch in Entwicklung, kommen Sie sp&auml;ter noch einmal vorbei...
</div>
</div>
{{ if ne .UserName "" }}
<p>
<strong>Ich will weitere bestellen!</strong>
<span class="icon"><i class="fa fa-arrow-right"></i></span>
Hier geht's zur <a href="/order.html">Bestellung</a>
</p>
{{ else }}
<p>
<strong>Ich will auch einen!</strong>
<span class="icon"><i class="fa fa-arrow-right"></i></span>
Hier geht's zur <a href="/order.html">Bestellung</a>
</p>
<p>&nbsp;</p>
<p>
<strong>Ich habe bereits einen (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="E-Mail">
<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="Passwort">
<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>
<div class="message is-danger">
<div class="message-body">
Aktuell noch in Entwicklung, kommen Sie sp&auml;ter noch einmal vorbei...
</div>
</div>
{{end}}

View File

@ -0,0 +1,9 @@
{{define "body_content"}}
{{ if ne .UserName "" }}
Sie sind bereits eingeloggt!
{{ else }}
<div class="notification is-info">
Passwort wurde erfolgreich ge&auml;ndert!
</div>
{{ end }}
{{end}}

View File

@ -0,0 +1,9 @@
{{define "body_content"}}
{{ if ne .UserName "" }}
<p class="title is-4">Zahlung abgebrochen!</p>
<p>Zahlung wurde abgebrochen</p>
{{ else }}
<h4>Bitte zuerst <a href="login.html">einloggen</a></h4>
{{end}}
{{end}}

View File

@ -0,0 +1,9 @@
{{define "body_content"}}
{{ if ne .UserName "" }}
<p class="title is-4">Danke schoen!</p>
<p>Danke fuer die Zahlung!</p>
{{ else }}
<h4>Bitte zuerst <a href="login.html">einloggen</a></h4>
{{end}}
{{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="E-Mail">
<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}}

174
snippets/tracker.html Normal file
View File

@ -0,0 +1,174 @@
{{define "body_content"}}
{{ if ne .UserName "" }}
<div id="modal" class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Einstellungen</p>
</header>
<div class="modal-card-body">
<!-- Content ... -->
<div class="field"> <label id="label" class="label">Bezeichnung</label>
<div class="control has-icons-right">
<input id="alias" class="input" type="text" maxlength="25">
<span id="alias_exclamation" class="icon is-small is-right">
<i class="fas fa-exclamation-triangle"></i>
</span>
</div>
<p id="alias_errormsg" class="help is-danger"></p>
</div>
<div class="is-size-7">
Device ID: <span id="deveui"></span>
</div>
</div>
<footer class="modal-card-foot">
<button id="modal-save" class="button is-success">OK</button>
<button id="modal-close" class="button">Cancel</button>
</footer>
</div>
</div>
<div id="cart" class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Abo verl&auml;ngern</p>
</header>
<p id="charge_data" hidden>0</p>
<div class="modal-card-body">
<div id="abos_verlaengern">
<!-- Content ... -->
</div>
<div>
<div class="has-margin-top-20" id="card-element">
<!-- Elements will create input elements here -->
</div>
<!-- We'll put the error messages in this element -->
<div class="message is-danger has-margin-top-20" id="card-errors-article" role="alert">
<div class="message-body" id="card-errors">
</div>
</div>
</div>
</div>
<footer class="modal-card-foot">
<button id="cart-pay" class="button is-success">Bezahlen</button>
<button id="cart-close" class="button">Cancel</button>
</footer>
</div>
</div>
<div id="payment_notifier" class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Info: Bezahlung durchgef&uuml;hrt</p>
</header>
<div class="modal-card-body">
<div class="message is-info">
<div class="message-body">
Sie haben eine Abo Verl&auml;ngerung bezahlt, besten Dank!
Sie erhalten eine E-Mail, sobald die Zahlung erfolgreich abgeschlossen ist.
</div>
</div>
</div>
<footer class="modal-card-foot">
<button id="payment_notifier_close" class="button is-success">Schliessen</button>
</footer>
</div>
</div>
{{range .LastMetrics}}
<div class="column waage is-full notification is-warning">
<p class="is-size-2 is-size-5-mobile has-text-centered has-text-weight-bold" ><span class="alias" id="alias_{{.Deveui}}">{{.Alias}}</span>{{ if not .Readonly }}<a class="show-modal" class="block-link"><span style="float:right;" class="icon is-size-4 is-size-5-mobile"><i class="fa fa-cog"></i></span></a>{{ end }}</p>
<p id="lastmeasurement_{{.Deveui}}" class="has-text-centered">letzte &uuml;bermittelte Messung: {{.Timestamp}}</p>
<div id="{{.Deveui}}">
<div class="column is-full notification is-warning">
<nav class="level">
<div class="level-item has-text-centered">
<a class="block-link" href="/graph.html?deveui={{.Deveui}}&alias={{.Alias}}&property=t">
<div>
<p class="icon"><i class="fa fa-thermometer-half"></i></p>
<p id="temp_{{.Deveui}}" class="title">Lat {{.Lat}}</p>
</div>
</a>
</div>
<div class="level-item has-text-centered">
<a class="block-link" href="/graph.html?deveui={{.Deveui}}&alias={{.Alias}}&property=h">
<div>
<p class="icon"><i class="fa fa-tint"></i></p>
<p id="humidity_{{.Deveui}}" class="title">Lon {{.Lon}}</p>
</div>
</a>
</div>
{{ if (lt .DaysUntilDeactivated 0) }}
<div class="level-item has-text-centered">
<div>
<p class="icon"><i class="fa fa-balance-scale"></i></p>
<p id="weight_{{.Deveui}}" class="title is-size-2 has-text-weight-bold has-text-danger">Abo ist abgelaufen</p>
</div>
</div>
{{ else }}
{{ end }}
<div class="level-item has-text-centered">
<a class="block-link" href="/graph.html?deveui={{.Deveui}}&alias={{.Alias}}&property=p">
<div>
<p class="icon"><i class="fa fa-cloud"></i></p>
<p id="pressure_{{.Deveui}}" class="title">VBat {{.Vbat}} mV</p>
</div>
</a>
</div>
<div class="level-item has-text-centered">
<a class="block-link" href="/graph.html?deveui={{.Deveui}}&alias={{.Alias}}&property=vp">
<div>
<p class="icon"><i class="fa fa-battery-three-quarters"></i></p>
<p id="acculevel_{{.Deveui}}" class="title">Fw {{.Fw}}</p>
</div>
</a>
</div>
</nav>
</div>
<div class="has-text-centered">
<span class="is-size-6 has-text-centered">Abo aktiv bis</span>
<span class="paid_until is-size-6 has-text-centered">{{.ActiveUntil}}</span>
</div>
{{ if (lt .DaysUntilDeactivated 1095) }}
<div class="has-text-centered is-size-7">
<p class="abo_add_years_text" id="abo_add_years_text_{.Deveui}}">&nbsp;</p>
</div>
<div class="has-text-centered">
<p class="abo_add_years" id="abo_add_years_{{.Deveui}}" hidden>0</p>
<a class="block-link abo_plus is-unselectable" id="abo_plus_{{.Deveui}}">
<span class="icon is-medium">
<i class="fa fa-lg fa-plus"></i>
</span>
</a>
<a class="block-link abo_pay is-unselectable" id="abo_{{.Deveui}}">
<span class="icon is-medium">
<i class="fa fa-2x fa-shopping-cart"></i>
</span>
</a>
<a class="block-link abo_minus is-unselectable" id="abo_minus_{{.Deveui}}">
<span class="icon is-medium">
<i class="fa fa-lg fa-minus"></i>
</span>
</a>
</div>
{{ end }}
</div>
</div>
{{end}}
<script src="https://cdn.jsdelivr.net/momentjs/latest/moment-with-locales.min.js"></script>
<script src="static/js/scales.js"></script>
{{ 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}}

View File

@ -0,0 +1,10 @@
{{define "body_content"}}
{{ if ne .UserName "" }}
Sie sind bereits eingeloggt!
{{ else }}
<p class="title is-4">Passwort zur&uuml;cksetzen - warte auf Best&auml;tigung</p>
<div class="notification is-info">
<p>Bitte checken Sie Ihre Mailbox. Das neue Passwort wird erst g&uuml;ltig, wenn Sie die Best&auml;tigung durchgef&uuml;hrt haben.</p>
</div>
{{end}}
{{end}}

17
static/502.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>wo-bisch.ch - LoraWAN Tracker</title>
<link rel="stylesheet" href="/wo-bisch-web.css">
<link rel="stylesheet" href="/wo-bisch-web-custom.css">
</head>
<body>
<div class="section px-4 py-4">
<div class="notification is-danger">
<strong>Oops, da ist was schiefgegangen!</strong>
</div>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,88 @@
.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: 0 0 15px 0;
}
hr.top {
margin: 0 0 0 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;
}
.block-link {
text-decoration: none !important;
}
.apexcharts-title-text {
font-weight: 700 !important;
font-size: 24px !important;
font-family: "Rubik", sans-serif !important;
}
.apexcharts-legend {
font-family: "Rubik", sans-serif;
}
/**
* * The CSS shown here will not be introduced in the Quickstart guide, but
* * shows how you can use CSS to style your Element's container.
* */
input,
.StripeElement {
height: 40px;
padding: 10px 12px;
color: #32325d;
background-color: white;
border: 1px solid transparent;
border-radius: 4px;
box-shadow: 0 1px 3px 0 #e6ebf1;
-webkit-transition: box-shadow 150ms ease;
transition: box-shadow 150ms ease;
}
input:focus,
.StripeElement--focus {
box-shadow: 0 1px 3px 0 #cfd7df;
}
.StripeElement--invalid {
border-color: #fa755a;
}
.StripeElement--webkit-autofill {
background-color: #fefde5 !important;
}
.checkboxes input {
vertical-align: middle;
}
.checkboxes label span {
vertical-align: middle;
margin-left: 5px;
}

20353
static/css/wo-bisch-web.css Normal file

File diff suppressed because it is too large Load Diff

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: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

16
static/js/apexcharts.min.js vendored Normal file

File diff suppressed because one or more lines are too long

81
static/js/chekout.js Normal file
View File

@ -0,0 +1,81 @@
// Set your publishable key: remember to change this to your live publishable key in production
// See your keys here: https://dashboard.stripe.com/account/apikeys
var stripe = Stripe('pk_test_YkSGqH3Tk9WKK9HrlY63GhAg');
var elements = stripe.elements();
// Set up Stripe.js and Elements to use in checkout form
var style = {
base: {
color: "#32325d",
}
};
var card = elements.create("card", { style: style });
card.mount("#card-element");
card.addEventListener('change', ({error}) => {
const displayError = document.getElementById('card-errors');
if (error) {
displayError.textContent = error.message;
} else {
displayError.textContent = '';
}
});
var submitButton = document.getElementById('submit');
function GetClientSecret() {
var result = "";
$.ajax({
async: false,
url: "getstripepaymentintent",
type: "get", //send it through get method
dataType: "json",
data: {
charge_data: $("#charge_data").html()
},
success: function(response) {
console.log('pay success');
console.log(response.stripesessionid);
console.log('rc: '+response.rc);
if (response.rc == 0) {
result = response.stripeclientsecret;
}
},
error: function(xhr) {
console.log('getstripepaymentintent error');
//Do Something to handle error
}
});
return result;
}
submitButton.addEventListener('click', function(ev) {
var clientSecret = GetClientSecret();
stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: card,
billing_details: {
name: 'Jenny Rosen'
}
}
}).then(function(result) {
if (result.error) {
// Show error to your customer (e.g., insufficient funds)
console.log(result.error.message);
} else {
// The payment has been processed!
if (result.paymentIntent.status === 'succeeded') {
// Show a success message to your customer
// There's a risk of the customer closing the window before callback
// execution. Set up a webhook or plugin to listen for the
// payment_intent.succeeded event that handles any business critical
// post-payment actions.
alert("Payment succeeded!!!");
}
}
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

105
static/js/graph.js Normal file
View File

@ -0,0 +1,105 @@
$(function() {
var start = moment({hour: 0});
var end = moment();
var s_start = start.utc().format('YYYY-MM-DDTHH:mm:ss[Z]');
var s_end = end.utc().format('YYYY-MM-DDTHH:mm:ss[Z]');
console.log("XXX: "+start.format());
function change_property(new_property) {
if (new_property != property) {
property = new_property;
if (history.pushState) {
var newurl = window.location.href.replace(/property=[a-z]+/,'property='+new_property);
window.history.pushState({path:newurl},'',newurl);
}
}
}
function cb(start, end) {
$('#reportrange span').html(start.locale('de').format('D. MMM YYYY') + ' - ' + end.locale('de').format('D. MMM YYYY'));
console.log("A new date selection was made: " + start.format() + ' to ' + end.format());
s_start = start.utc().format('YYYY-MM-DDTHH:mm:ss[Z]');
s_end = end.utc().format('YYYY-MM-DDTHH:mm:ss[Z]');
console.log("Start: " + s_start + ' End: ' + s_end + ' Property: '+ property);
drawGraph(deveui, alias, property, s_start,s_end, false);
}
$('#btn_w').on("click", function() {
change_property('w');
drawGraph(deveui, alias, property, s_start, s_end, false);
});
$('#btn_t').on("click", function() {
change_property('t');
drawGraph(deveui, alias, property, s_start, s_end, false);
});
$('#btn_h').on("click", function() {
change_property('h');
drawGraph(deveui, alias, property, s_start, s_end, false);
});
$('#btn_p').on("click", function() {
change_property('p');
drawGraph(deveui, alias, property, s_start, s_end, false);
});
$('#btn_vp').on("click", function() {
change_property('vp');
drawGraph(deveui, alias, property, s_start, s_end, false);
});
$('#reportrange').daterangepicker({
startDate: start.local(),
endDate: end.local(),
regional: [ "de" ],
"locale": {
format: 'DD.MM.YYYY',
"separator": " - ",
"applyLabel": "Anwenden",
"cancelLabel": "Abbrechen",
"fromLabel": "Von",
"toLabel": "Bis",
customRangeLabel: 'Benutzerdefiniert',
"weekLabel": "W",
"daysOfWeek": [
"So",
"Mo",
"Di",
"Mi",
"Do",
"Fr",
"Sa"
],
"monthNames": [
"Jan",
"Feb",
"M&auml;r",
"Apr",
"Mai",
"Jun",
"Jul",
"Aug",
"Sep",
"Okt",
"Nov",
"Dez"
],
"firstDay": 1
},
ranges: {
'Heute': [moment(), moment()],
'Gestern': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
'Letzte 7 Tage': [moment().subtract(6, 'days'), moment()],
'Letzte 30 Tage': [moment().subtract(29, 'days'), moment()],
'Diesen Monat': [moment().startOf('month'), moment().endOf('month')],
'Letzten Monat': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
}
}, cb);
//cb(start, end);
});

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

File diff suppressed because one or more lines are too long

308
static/js/scales.js Normal file
View File

@ -0,0 +1,308 @@
function validate(what, text) {
if (what == 'alias') {
var re = /^[a-zA-Z0-9 ]{1,25}$/;
} else if (what == 'smsnumber') {
var re = /^\+[0-9]{11,11}$/;
}
return re.test(text);
}
// A $( document ).ready() block.
$( document ).ready(function() {
$(".show-modal").click(function() {
$("#alias_exclamation").hide();
$("#smsnumber_exclamation").hide();
var alias = $(this).prev().html();
var deveui = $(this).prev().attr('id').replace("alias_","");
$('#deveui').html(deveui);
var alarmactive = $('#alarmactive_'+deveui).html();
var smsnumber = $('#smsnumber_'+deveui).html();
$("#alias").val(alias);
console.log(alarmactive);
if (alarmactive == "1") {
$('#checkbox').prop('checked', true);
} else {
$('#checkbox').prop('checked', false);
}
$("#smsnumber").val(smsnumber);
$("#modal").addClass("is-active");
});
$("#cart-close").click(function() {
$("#cart").removeClass("is-active");
});
$("#payment_notifier_close").click(function() {
$("#payment_notifier").removeClass("is-active");
location.reload(true);
});
$(".abo_plus").click(function() {
console.log("abo_plus");
el = $(this).parent().find(".abo_add_years");
el_text = $(this).parent().parent().find(".abo_add_years_text");
counter = Number(el.html());
if (counter < 3) {
counter = counter + 1;
el.html(counter);
if (counter == 1) {
el_text.html("+" + counter + " Jahr");
} else {
el_text.html("+" + counter + " Jahre");
}
}
});
$(".abo_minus").click(function() {
console.log("abo_minus");
el = $(this).parent().find(".abo_add_years");
el_text = $(this).parent().parent().find(".abo_add_years_text");
counter = Number(el.html());
if (counter > 0) {
counter = counter - 1;
el.html(counter);
if (counter == 0) {
el_text.html("&nbsp;");
} else if (counter == 1) {
el_text.html("+" + counter + " Jahr");
} else {
el_text.html("+" + counter + " Jahre");
}
}
});
function add_years(dt,n)
{
return new Date(dt.setFullYear(dt.getFullYear() + n));
}
$(".abo_pay").click(function() {
console.log("pay...");
loadStripeLibrary();
counter = 0;
charge_data = '';
abo_table = '<table class="table is-bordered is-fullwidth" >';
abo_table += '<tr><th class="is-2">Alias</th><th>verl&auml;ngern bis</th><th class="has-text-right">Betrag</th></tr>';
$(".waage").each(function( index ) {
console.log( index + ": " + $( this ).find(".alias").html() );
this_count = Number($( this ).find(".abo_add_years").html());
if (this_count > 0) {
counter += this_count;
paid_until = $( this ).find(".paid_until").html();
if (moment(paid_until,'DD.MM.YYYY') < moment()) {
this_date = moment().format('DD.MM.YYYY');
} else {
this_date = paid_until;
}
abo_table += '<tr><td>' + $( this ).find('.alias').html() + '</td><td>' + moment(this_date,'DD.MM.YYYY').add('years', this_count).format('DD.MM.YYYY') + '</td><td class="has-text-right">' + (this_count * 24).toFixed(2) + '</td></tr>';
if (charge_data == '') {
charge_data = $( this ).find("div").first().attr('id') + ":" + this_count;
} else {
charge_data += "," + $( this ).find("div").first().attr('id') + ":" + this_count;
}
}
console.log( counter );
});
abo_table += '<tr><td>Total CHF</td><td></td><td class="has-text-right">' + (counter * 24).toFixed(2) + '</td></tr>';
console.log("Counter: "+counter);
abo_table += "</table>";
if (counter > 0) {
console.log(abo_table);
console.log("charge_data: "+charge_data);
$("#abos_verlaengern").html(abo_table);
$("#charge_data").html(charge_data);
$("#cart").addClass("is-active");
}
});
$("#modal-close").click(function() {
console.log("blabla");
$("#modal").removeClass("is-active");
});
$("#modal-save").click(function() {
var alarmactive = "0";
if ($('#checkbox').prop('checked')) {
alarmactive = "1";
}
// Validation Code
var is_valid = true;
if (!validate('alias',$('#alias').val())) {
$('#alias_errormsg').html('Ung&uuml;ltige Bezeichnung; erlaubte Zeichen A-Z, 0-9 und Leerschlag');
$("#alias").addClass("is-danger");
$("#alias_exclamation").show();
is_valid = false;
} else {
$('#alias_errormsg').html('');
$("#alias").removeClass("is-danger");
$("#alias_exclamation").hide();
}
if (!validate('smsnumber',$('#smsnumber').val())) {
$('#smsnumber_errormsg').html('Beispiel einer g&uuml;ltigen SMS Nummer: +41761234567');
$("#smsnumber").addClass("is-danger");
$("#smsnumber_exclamation").show();
is_valid = false;
} else {
$('#smsnumber_errormsg').html('');
$("#smsnumber").removeClass("is-danger");
$("#smsnumber_exclamation").hide();
}
if (!(is_valid)) {
return;
}
$.ajax({
url: "save_scale_settings",
type: "get", //send it through get method
dataType: "json",
data: {
deveui: $('#deveui').html(),
alias: $('#alias').val(),
smsnumber: $("#smsnumber").val(),
alarmactive: alarmactive
},
success: function(response) {
console.log('save success');
if (response.rc == 0) {
$('#alias_'+$('#deveui').html()).html($('#alias').val());
var alarmactive = "0";
if ($('#checkbox').prop('checked')) {
alarmactive = "1";
}
$('#alarmactive_'+$('#deveui').html()).html(alarmactive);
$('#smsnumber_'+$('#deveui').html()).html($('#smsnumber').val());
}
},
error: function(xhr) {
console.log('save error');
//Do Something to handle error
}
});
console.log("save");
$("#modal").removeClass("is-active");
});
});
function loadStripeLibrary() {
$.ajax({
url: "https://js.stripe.com/v3/",
dataType: "script",
async: false, // <-- This is the key
success: function () {
// all good...
console.log("loadStripeLibrary called...");
SetupStripe();
},
error: function () {
throw new Error("Could not load script " + script);
}
});
}
function SetupStripe() {
console.log("SetupStripe");
// Set your publishable key: remember to change this to your live publishable key in production
// See your keys here: https://dashboard.stripe.com/account/apikeys
stripe = Stripe('pk_test_YkSGqH3Tk9WKK9HrlY63GhAg');
elements = stripe.elements({ locale: "de" });
// Set up Stripe.js and Elements to use in checkout form
style = {
base: {
color: "#32325d",
}
};
card = elements.create("card", { style: style });
card.mount("#card-element");
$("#card-errors-article").hide();
card.addEventListener('change', ({error}) => {
if (error) {
$("#card-errors").text(error.message);
$("#card-errors-article").show();
} else {
$("#card-errors").text("");
$("#card-errors-article").hide();
}
});
}
function ConfirmPayment(clientSecret) {
stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: card,
billing_details: {
name: '{{ .UserName }}'
}
}
}).then(function(result) {
if (result.error) {
// Show error to your customer (e.g., insufficient funds)
console.log(result.error.message);
$('#card-errors').text(result.error.message);
$('#card-errors-article').show();
//Do Something to handle error
EndPaymentProgress();
} else {
// The payment has been processed!
if (result.paymentIntent.status === 'succeeded') {
// Show a success message to your customer
// There's a risk of the customer closing the window before callback
// execution. Set up a webhook or plugin to listen for the
// payment_intent.succeeded event that handles any business critical
// post-payment actions.
console.log("Payment succeeded!!!");
$('#card-errors').text("");
$('#card-errors-article').hide();
$("#cart").removeClass("is-active");
EndPaymentProgress();
$('#payment_notifier').addClass('is-active');
}
}
});
}
function StartPaymentProgress() {
$("#cart-pay").attr("disabled", true).addClass("is-loading");
$("#cart-close").attr("disabled", true);
}
function EndPaymentProgress() {
$("#cart-pay").attr("disabled", false).removeClass("is-loading");
$("#cart-close").attr("disabled", false);
}
function PayMe() {
$.ajax({
url: "getstripepaymentintent",
type: "get", //send it through get method
dataType: "json",
data: {
charge_data: $("#charge_data").html()
},
success: function(response) {
console.log('pay success');
console.log('rc: '+response.rc);
if (response.rc == 0) {
ConfirmPayment(response.stripeclientsecret);
}
},
error: function(xhr) {
console.log('getstripepaymentintent error');
//Do Something to handle error
EndPaymentProgress();
}
});
}
$('#cart-pay').on('click', function(ev) {
StartPaymentProgress();
PayMe();
});

70
static/js/wo-bisch-web.js Normal file
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,84 @@
.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: 0 0 15px 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;
}
.block-link {
text-decoration: none !important;
}
.apexcharts-title-text {
font-weight: 700 !important;
font-size: 24px !important;
font-family: "Rubik", sans-serif !important;
}
.apexcharts-legend {
font-family: "Rubik", sans-serif;
}
/**
* * The CSS shown here will not be introduced in the Quickstart guide, but
* * shows how you can use CSS to style your Element's container.
* */
input,
.StripeElement {
height: 40px;
padding: 10px 12px;
color: #32325d;
background-color: white;
border: 1px solid transparent;
border-radius: 4px;
box-shadow: 0 1px 3px 0 #e6ebf1;
-webkit-transition: box-shadow 150ms ease;
transition: box-shadow 150ms ease;
}
input:focus,
.StripeElement--focus {
box-shadow: 0 1px 3px 0 #cfd7df;
}
.StripeElement--invalid {
border-color: #fa755a;
}
.StripeElement--webkit-autofill {
background-color: #fefde5 !important;
}
.checkboxes input {
vertical-align: middle;
}
.checkboxes label span {
vertical-align: middle;
margin-left: 5px;
}

20353
static/wo-bisch-web.css Normal file

File diff suppressed because it is too large Load Diff

125
stripe.go Normal file
View File

@ -0,0 +1,125 @@
package main
import (
"encoding/json"
"fmt"
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/paymentintent"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"strings"
)
func getStripeKey() string {
return "sk_test_GJbXPD0IAFNvvGpNEpaeDfhl"
}
func getstripepaymentintentHandler(response http.ResponseWriter, request *http.Request) {
name := getUserName(request)
if name != "" {
charge_data, ok := request.URL.Query()["charge_data"]
if !ok || len(charge_data[0]) < 1 {
log.Println("Url Param 'charge_data' is missing")
fmt.Fprintf(response, "{ \"rc\": 1, \"msg\": \"charge_data must be specified in URL\" }")
return
}
trackers := strings.Split(charge_data[0], ",")
var abo_years = 0
var items []string
for _, tracker := range trackers {
items = strings.Split(tracker, ":")
if len(items) == 2 {
abo_count, err := strconv.Atoi(items[1])
if err == nil {
abo_years += abo_count
}
}
}
abo_amount := int64(abo_years * 2400)
stripe.Key = getStripeKey()
params := &stripe.PaymentIntentParams{
Amount: stripe.Int64(abo_amount),
Currency: stripe.String(string(stripe.CurrencyCHF)),
ReceiptEmail: stripe.String(name),
}
params.AddMetadata("charge_data", charge_data[0])
paymentintent, err := paymentintent.New(params)
if err != nil {
fmt.Fprintf(response, "{ \"rc\": 5, \"stripeclientsecret\": \"%s\" }\n", err)
} else {
fmt.Fprintf(response, "{ \"rc\": 0, \"stripeclientsecret\": \"%s\" }\n", paymentintent.ClientSecret)
}
} else {
fmt.Fprintf(response, "{ \"rc\": 6, \"msg\": \"Only available for logged in users\" }")
}
}
func HandlePayment(user string, charge_data string, amount int64) {
fmt.Printf("HandlePayment for %s (charge_data: %s, amount: %d)!\n", user, charge_data, amount)
charge_data_email_text := fmt.Sprintf("%-30s %20s %10s\n", "Alias", "verlängern bis", "Betrag")
charge_data_email_text = charge_data_email_text + strings.Repeat("-", 62) + "\n"
for _, token := range strings.Split(charge_data, ",") {
res := strings.Split(token, ":")
if (len(res)) == 2 {
deveui := res[0]
years, _ := strconv.Atoi(res[1])
fmt.Printf("prolongActivation %s: %d\n", deveui, years)
prolongActivation(deveui, years)
line := fmt.Sprintf("%-30s %20s %10.2f\n", getDevAlias(deveui), getActiveUntil(deveui), float64(24*years))
charge_data_email_text = charge_data_email_text + line
}
}
charge_data_email_text = charge_data_email_text + strings.Repeat("-", 62) + "\n"
charge_data_email_text = charge_data_email_text + fmt.Sprintf("%-30s %20s %10.2f\n", "Total CHF", "", float64(amount/100))
sendPaymentConfirmationEmail(user, charge_data_email_text, amount)
}
func stripeWebhookHandler(w http.ResponseWriter, req *http.Request) {
const MaxBodyBytes = int64(65536)
req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes)
payload, err := ioutil.ReadAll(req.Body)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err)
w.WriteHeader(http.StatusServiceUnavailable)
return
}
event := stripe.Event{}
if err := json.Unmarshal(payload, &event); err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse webhook body json: %v\n", err.Error())
w.WriteHeader(http.StatusBadRequest)
return
}
// Unmarshal the event data into an appropriate struct depending on its Type
switch event.Type {
case "payment_intent.succeeded":
var paymentIntent stripe.PaymentIntent
err := json.Unmarshal(event.Data.Raw, &paymentIntent)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
fmt.Printf("PaymentIntent was successful (charge_data: %s, amount: %d)!\n", paymentIntent.Metadata["charge_data"], paymentIntent.Amount)
HandlePayment(paymentIntent.ReceiptEmail, paymentIntent.Metadata["charge_data"], paymentIntent.Amount)
// ... handle other event types
default:
fmt.Fprintf(os.Stderr, "Unexpected event type: %s\n", event.Type)
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}

View File

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

84
templates/layout.html Normal file
View File

@ -0,0 +1,84 @@
{{define "header_additions"}}{{end}}
{{define "layout"}}<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no">
<title>wo-bisch.ch - der smarte GPS Tracker</title>
<link rel="stylesheet" href="static/css/wo-bisch-web.css">
<link rel="stylesheet" href="static/css/wo-bisch-web-custom.css">
<script src="/static/js/fontawesome-5.11.2/all-minified.js"></script>
<script src="/static/js/jquery-3.3.1/jquery.min.js"></script>
{{template "header_additions" . }}
</head>
<body>
<div class="section px-4 py-4">
<div class="container">
<hr class="top" />
<nav class="navbar" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<img src="/static/images/wo-bisch-logo-28x28.png" alt="wo-bisch 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="/tracker.html">
<div style="position:relative">
<span class="icon"><i class="fa fa-map-marked-alt"></i></span>
<span>Meine GPS Tracker</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>
</div>
<script src="/static/js/wo-bisch-web.js"></script>
</body>
</html>
{{end}}

76
tracker.go Normal file
View File

@ -0,0 +1,76 @@
package main
import (
"fmt"
"log"
"net/http"
"regexp"
)
// tracker handler
func save_tracker_settingsHandler(response http.ResponseWriter, request *http.Request) {
name := getUserName(request)
if name != "" {
deveui, ok := request.URL.Query()["deveui"]
if !ok || len(deveui[0]) < 1 {
log.Println("Url Param 'deveui' is missing")
fmt.Fprintf(response, "{ \"rc\": 1, \"msg\": \"deveui must be specified in URL\" }")
return
}
// Query()["deveui"] will return an array of items,
// we only want the single item.
mydeveui := deveui[0]
if len(mydeveui) != 16 {
log.Println("specified 'deveui' has invalid length")
fmt.Fprintf(response, "{ \"rc\": 8, \"msg\": \"specified deveui has invalid length\" }")
return
}
if !(Contains(getMyDevs(name), mydeveui)) {
log.Println("specified 'deveui' does not belong to this user")
fmt.Fprintf(response, "{ \"rc\": 2, \"msg\": \"specified deveui does not belong to this user\" }")
return
}
log.Println("Url Param 'deveui' is: " + string(mydeveui))
alias, ok2 := request.URL.Query()["alias"]
if !ok2 || len(alias[0]) < 1 {
log.Println("Url Param 'alias' is missing")
fmt.Fprintf(response, "{ \"rc\": 3, \"msg\": \"alias must be specified in URL\" }")
return
}
myalias := alias[0]
// validate alias
match, _ := regexp.MatchString("^[a-zA-Z0-9 ]{1,25}$", myalias)
if !(match) {
log.Println("Url Param 'alias' is not valid")
fmt.Fprintf(response, "{ \"rc\": 9, \"msg\": \"alias is not valid\" }")
return
}
var mydev Dev
mydev.Deveui = mydeveui
mydev.Alias = myalias
// now we try to save the settings
err := updateTrackerSettings(mydev)
if err != nil {
log.Println("Error to Update Device Settings")
fmt.Fprintf(response, "{ \"rc\": 6, \"msg\": \"error with saving device settings\" }")
return
} else {
fmt.Fprintf(response, "{ \"rc\": 0, \"msg\": \"SUCCESS\" }")
}
} else {
fmt.Fprintf(response, "{ \"rc\": 7, \"msg\": \"Only available for logged in users\" }")
}
}

3
wo-bisch-sass/README.md Normal file
View File

@ -0,0 +1,3 @@
To generate CSS:
`npm run css-build`

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
{
"name": "wo-bisch-sass",
"version": "1.0.1",
"description": "SASS for wo-bisch-web",
"main": "enter sass/wo-bisch-web.scss",
"scripts": {
"css-build": "node-sass --omit-source-map-url sass/wo-bisch-web.scss css/wo-bisch-web.css",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Joerg Lehmann",
"license": "ISC",
"devDependencies": {
"bulma": "^0.9.0",
"bulma-helpers": "^0.3.12",
"node-sass": "^4.14.1"
}
}

View File

@ -0,0 +1,12 @@
@charset "utf-8";
// Import a Google Font
@import url('https://fonts.googleapis.com/css?family=Rubik:400,700');
// Set nbit Style...
$family-sans-serif: "Rubik", sans-serif;
@import "../node_modules/bulma/bulma.sass";
@import "../node_modules/bulma-helpers/bulma-helpers.sass";
$navbar-height = 6.5rem