Compare commits

...

57 Commits

Author SHA1 Message Date
Joerg Lehmann 7f639bca49 new address 2025-05-14 06:30:42 +02:00
Joerg Lehmann 483d10799d disable ordering possibility 2024-05-24 14:37:31 +02:00
Joerg Lehmann 8e3721da89 set mail content type to utf-8 2024-05-04 13:19:12 +02:00
Joerg Lehmann a297b127e1 update go version 2023-03-04 19:25:44 +01:00
Joerg Lehmann 8838103f2d make it the same as mini-beieli-web 2023-02-03 19:08:45 +01:00
Joerg Lehmann 0931ecb0f4 no receipt email 2023-02-03 18:49:24 +01:00
Joerg Lehmann 40134ceba9 go mod tidy 2023-02-03 17:26:28 +01:00
Joerg Lehmann 6364bedb29 new version with new stripe functionality 2023-02-03 17:25:51 +01:00
Joerg Lehmann 97d2bb2593 timezone in docker image 2022-12-28 17:14:09 +01:00
Joerg Lehmann 570de60338 add influx command to readme 2022-12-27 19:12:58 +01:00
Joerg Lehmann c6f98415e3 add some influx commands 2022-12-21 18:16:54 +01:00
Joerg Lehmann 0520b1eb02 use html mail for abocost because of table 2022-09-16 11:04:13 +02:00
Joerg Lehmann 1f1e34e213 add payment years to expiration date, not now 2022-09-15 18:17:15 +02:00
Joerg Lehmann 6fe448e3e2 make expiration text more precise 2022-08-01 11:06:17 +02:00
Joerg Lehmann e306a17ebd use same logig for date calculation as mini-beieli-web 2022-08-01 10:58:09 +02:00
Joerg Lehmann 5a2204876d only send email 30, 10 and 0 days before expiration 2022-07-22 19:32:21 +02:00
Joerg Lehmann 9aaf966728 send bcc to info@wo-bisch.ch 2022-07-18 19:35:45 +02:00
Joerg Lehmann 02cc375133 access control with header for checknodes 2022-07-14 20:20:34 +02:00
Joerg Lehmann 736fc6b058 integrate check_nodes in wo-bisch-web 2022-07-14 19:38:33 +02:00
Joerg Lehmann b4814970ce refactor email send functions 2022-07-13 19:22:43 +02:00
Joerg Lehmann 667051907b install tzdata because of dependency in go program 2022-07-05 19:11:04 +02:00
Joerg Lehmann cd79d3c852 make deployment container-ready 2022-07-05 18:15:04 +02:00
Joerg Lehmann 56fa4ab05f make deployment container-ready 2022-07-02 10:45:51 +02:00
Joerg Lehmann 695df00ebd price change 2022-06-04 19:22:48 +02:00
Joerg Lehmann 8232be9c01 allow spaces in greenzone 2022-06-03 18:13:36 +02:00
Joerg Lehmann 8e465698ad replace list on main page with ul 2022-04-16 19:50:51 +02:00
Joerg Lehmann e0b5b2498b fix timestamp - 2 2021-12-17 20:49:06 +01:00
Joerg Lehmann 8e4bc7a02b fix timestamp 2021-12-17 20:48:10 +01:00
Joerg Lehmann c678fbe5cd last vbat value not respecting invalid GPS positions 2021-12-17 20:45:05 +01:00
Joerg Lehmann fbcebb980a include vbat percent in download data 2021-12-17 19:37:26 +01:00
Joerg Lehmann 34fe1b107b do not filter unknown coordinates in main query 2021-12-17 19:16:19 +01:00
Joerg Lehmann e127b48642 fix getting last value 2021-08-23 14:04:23 +02:00
Joerg Lehmann 1c1cc71eff revert last change 2021-08-23 13:51:11 +02:00
Joerg Lehmann 260a9b1640 fix time problem 2021-08-23 13:47:32 +02:00
Joerg Lehmann a505fc5709 field order has changed with influxdb 2.0.8 2021-08-23 13:35:35 +02:00
Joerg Lehmann 61f61c474f AGB, Version 1.1 2021-07-05 20:03:57 +02:00
Joerg Lehmann 9784f44cb9 incl. Umhaengeband 2021-07-05 19:36:07 +02:00
Joerg Lehmann 4168f0f0ad change product images, no magnetic cable adapter 2021-07-05 19:29:03 +02:00
Joerg Lehmann eb257fea7d slight change on landing page 2021-06-29 19:49:17 +02:00
Joerg Lehmann 1a30cf7e84 modify landing page 2021-06-26 19:54:54 +02:00
Joerg Lehmann 5163a70261 typo 2021-06-09 20:02:44 +02:00
Joerg Lehmann e4a935d565 harmonize battery colors 2021-06-09 13:07:06 +02:00
Joerg Lehmann 40cf8cfabf harmonize padding 2021-06-09 10:07:40 +02:00
Joerg Lehmann 2830a648a5 typo 2021-06-08 15:27:19 +02:00
Joerg Lehmann 088af52bc9 cosmetic change 2021-06-08 15:26:01 +02:00
Joerg Lehmann 7db46d3f78 cosmetics... 2021-06-08 11:01:13 +02:00
Joerg Lehmann 8c3c75ae19 cosmetic change 2021-06-08 10:49:57 +02:00
Joerg Lehmann 6a451aae99 make more than one email receipient possible 2021-06-08 09:49:24 +02:00
Joerg Lehmann 19b52e4971 small text change 2021-06-08 09:04:19 +02:00
Joerg Lehmann c173fb653f place red marker above the orange ones 2021-06-07 20:48:06 +02:00
Joerg Lehmann 9db4ae08f3 not necessary to set height of daterangepicker 2021-06-07 20:41:35 +02:00
Joerg Lehmann 65c2fec4ac flags added 2021-06-07 19:43:49 +02:00
Joerg Lehmann 2cc02925a3 zuerst Laengengrad, dann Breitengrad 2021-06-07 13:13:34 +02:00
Joerg Lehmann 97a8e6d894 change text 2021-06-07 13:08:26 +02:00
Joerg Lehmann 9a717a5c5a activate Abo-Alert 2021-06-07 11:34:48 +02:00
Joerg Lehmann 6179b81f71 no-cache for templates, tune map position 2021-06-07 11:19:10 +02:00
Joerg Lehmann 992fb3d4f1 because of bootom margin on mobile browsers/ios 2021-06-05 20:35:57 +02:00
27 changed files with 457 additions and 591 deletions

16
Dockerfile Normal file
View File

@ -0,0 +1,16 @@
FROM golang:alpine AS builder
WORKDIR /build
ADD go.mod .
COPY . .
RUN go build
FROM alpine
RUN apk add --no-cache tzdata
ENV TZ=Europe/Zurich
WORKDIR /build
RUN apk add --no-cache tzdata
COPY --from=builder /build/wo-bisch-web /build/wo-bisch-web
COPY snippets snippets
COPY templates templates
COPY static static
CMD ["./wo-bisch-web"]
EXPOSE 4000

View File

@ -45,3 +45,23 @@ show range of last values:
Autor: Joerg Lehmann, nbit Informatik GmbH
### Influxdb Commands
```
Show tokens
# influx auth ls
Clone Admin Token in Webgui if not known anymore and use this to get all tokens:
Create Default Config:
# influx config create --config-name default --token XXXXXXXXXXX --host-url http://127.0.0.1:8086 -a
Update Token in Default Config:
# influx config update --config-name default --token XXXXXXXXXXX
Backup Influxdb:
# influx backup /root/joergs-backup -t XXXXXadmintokenXXXXX
Restore Influxdb:
# influx restore /var/lib/influxdb2/joergs-backup/ --full
```

133
check_nodes.go Normal file
View File

@ -0,0 +1,133 @@
package main
import (
"fmt"
"net/http"
"strconv"
"strings"
)
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 := "To: " + username + `
From: info@wo-bisch.ch
Subject: ` + level + ` - wo-bisch.ch: Akku Ladezustand (` + alias + `)
Content-Type: text/plain; charset="UTF-8"
Lieber Benutzer von wo-bisch.ch
Der Akku von "` + alias + `" (DevEUI: ` + deveui + `) ist noch zu ` + accu_percent + ` Prozent geladen.
Bitte rechtzeitig wieder laden! Bei Unterschreitung von 5% Ladung erscheint die letzte Warnung.
Mit freundlichen Grüssen
--
wo-bisch.ch`
sendEmail(username, "mail@wo-bisch.ch", mail_message)
}
func sendEmailAbo(username string, alias string, deveui string, days_left int, level string) {
var ablauftext string
if days_left == 0 {
fmt.Printf("SEND EMAIL ABO (%s) - %s:%s\n", level, username, deveui)
ablauftext = "Das Abo von \"" + alias + "\" (DevEUI: " + deveui + ") laeuft heute ab."
} else if days_left > 0 {
fmt.Printf("SEND EMAIL ABO (%s) - %s:%s\n", level, username, deveui)
ablauftext = "Das Abo von \"" + alias + "\" (DevEUI: " + deveui + ") laeuft in " + strconv.Itoa(days_left) + " Tagen ab."
}
mail_message := "To: " + username + `
From: info@wo-bisch.ch
Subject: ` + level + ` - wo-bisch.ch: Abo verlaengern (` + alias + `)
Content-Type: text/plain; charset="UTF-8"
Lieber Benutzer von wo-bisch.ch
` + ablauftext + `
Bitte Abo verlängern auf https://wo-bisch.ch
Mit freundlichen Grüssen
--
wo-bisch.ch`
sendEmail(username, "mail@wo-bisch.ch", mail_message)
}
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 sendReminder(days int) bool {
return (days == 0 || days == 5 || days == 10)
}
func checkNodes() {
logit("Starting check_battery...")
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, 20, 10, 5) {
DeleteAlert("alarm_sent_accu:", d, u2)
}
}
// Jetzt der Alarm wegen der Abodauer
if sendReminder(last_metric.DaysUntilDeactivated) {
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(u2, alias, d, last_metric.DaysUntilDeactivated, "INFO")
}
}
}
}
logit("Done with check_battery...")
}
func checkNodesHandler(w http.ResponseWriter, req *http.Request) {
headers := req.Header
val, ok := headers["X-Checknodes"]
if ok {
fmt.Printf("X-Checknodes header is present with value %s\n", val)
checkNodes()
} else {
fmt.Println("X-Checknodes header is not present")
}
w.WriteHeader(http.StatusOK)
}

View File

@ -1,419 +0,0 @@
package main
import (
"bufio"
"bytes"
"fmt"
"github.com/gomodule/redigo/redis"
"github.com/jordan-wright/email"
"io/ioutil"
"log"
"net/http"
"os"
"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 Unterschreitung von 5% 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 vbat2percent(vbat string) int {
i, err := strconv.Atoi(vbat)
res := 0
if err == nil {
res = int(float64(i-3400) / 6.0)
fmt.Printf("vbat2percent Result in Percent: %d\n", res)
if res < 0 {
res = 0
} else if res > 100 {
res = 100
}
}
return res
}
func getLastMetrics(deveui string) OneMetric {
var res OneMetric
res.BatteryPercent = "999"
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 == "vbat")
|> last(column: "_time")`, deveui))
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
log.Fatal("Error reading request. ", err)
}
// Set headers
var INFLUX_RO_TOKEN = os.Getenv("INFLUX_RO_TOKEN")
req.Header.Set("Authorization", "Token "+INFLUX_RO_TOKEN)
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(), ",")
//fmt.Printf("s: %q\n", s)
if (len(s) >= 7) && !(strings.HasPrefix(s[3], "_")) {
t, err := time.Parse(time.RFC3339, s[5])
if err != nil {
fmt.Printf("error converting time: %s\n", s[5])
continue
}
res.Timestamp = t.In(location).Format("02.01.2006 15:04")
res.BatteryPercent = strconv.Itoa(vbat2percent(s[6]))
fmt.Printf("vbat: %s\n", s[6])
}
}
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, 20, 10, 5) {
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...")
}

5
go.mod
View File

@ -1,11 +1,10 @@
module nbit.ch/wo-bisch-web/v2
go 1.14
go 1.17
require (
github.com/gomodule/redigo v1.8.4
github.com/gorilla/securecookie v1.1.1
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/stripe/stripe-go/v72 v72.41.0
github.com/stripe/stripe-go/v74 v74.7.0
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
)

17
go.sum
View File

@ -1,29 +1,32 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gomodule/redigo v1.8.4 h1:Z5JUg94HMTR1XpwBaSH4vq3+PNSIykBLxMdglbw10gg=
github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
github.com/gomodule/redigo/redis v0.0.0-do-not-use h1:J7XIp6Kau0WoyT4JtXHT3Ei0gA1KkSc6bc87j9v9WIo=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stripe/stripe-go v1.0.3 h1:RHgK2FUKawVNBPJ15pNM4IWkEVVCg5Ju3xK5QZNhTwY=
github.com/stripe/stripe-go v70.15.0+incompatible h1:hNML7M1zx8RgtepEMlxyu/FpVPrP7KZm1gPFQquJQvM=
github.com/stripe/stripe-go/v72 v72.41.0 h1:HkyJew+GkD/ClBT306+5vKLjBE4PRCJDiZ1enQyxeGQ=
github.com/stripe/stripe-go/v72 v72.41.0/go.mod h1:QwqJQtduHubZht9mek5sds9CtQcKFdsykV9ZepRWwo0=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stripe/stripe-go/v74 v74.7.0 h1:KHlyslQj9YOv62b1sycQ31LFj7KlqR+seHsSowAWrjc=
github.com/stripe/stripe-go/v74 v74.7.0/go.mod h1:5PoXNp30AJ3tGq57ZcFuaMylzNi8KpwlrYAFmO1fHZw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -2,6 +2,7 @@ package main
import (
"log"
"os"
)
// Contains tells whether a contains x.
@ -18,3 +19,11 @@ func Contains(a []string, x string) bool {
}
return false
}
func getenv(key, fallback string) string {
value := os.Getenv(key)
if len(value) == 0 {
return fallback
}
return value
}

95
mail.go
View File

@ -1,32 +1,45 @@
package main
import (
"bytes"
"log"
"net/smtp"
)
func sendEmail(username, confirm_id string) {
c, err := smtp.Dial("127.0.0.1:25")
func sendEmail(mail_to, mail_default_authuser, mail_message string) {
var auth smtp.Auth
if getenv("MAILSERVER_USER", "") != "" {
// Set up authentication information.
auth = smtp.PlainAuth(
"",
getenv("MAILSERVER_USER", ""),
getenv("MAILSERVER_PASSWORD", ""),
getenv("MAILSERVER_HOST", "127.0.0.1"),
)
}
// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
err := smtp.SendMail(
getenv("MAILSERVER_HOST", "127.0.0.1")+":"+getenv("MAILSERVER_PORT", "25"),
auth,
getenv("MAILSERVER_USER", mail_default_authuser),
[]string{mail_to, "info@wo-bisch.ch"},
[]byte(mail_message),
)
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()
}
func sendEmailConfirm(username, confirm_id string) {
mail_message := "To: " + username + `
From: register@wo-bisch.ch
Subject: Passwortaenderung auf https://wo-bisch.ch, bitte bestaetigen
Content-Type: text/plain; charset="UTF-8"
Lieber Benutzer von wo-bisch.ch
Sie haben soeben eine Passwortaenderung veranlasst. Bitte klicken Sie folgenden Link,
Sie haben soeben eine Passwortänderung veranlasst. Bitte klicken Sie folgenden Link,
um das neue Passwort zu aktivieren:
https://wo-bisch.ch/confirm?id=` + confirm_id + `
@ -37,30 +50,16 @@ Mit freundlichen Grüssen
--
wo-bisch.ch`
buf := bytes.NewBufferString(mail_message)
if _, err = buf.WriteTo(wc); err != nil {
log.Fatal(err)
}
sendEmail(username, "mail@wo-bisch.ch", mail_message)
}
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 + `
From: info@wo-bisch.ch
Subject: Zahlungsbestaetigung wo-bisch.ch
MIME-version: 1.0;
Content-Type: text/html; charset="UTF-8";
<pre>
Lieber Benutzer von wo-bisch.ch
Sie haben soeben erfolgreich folgende Abo-Verlaengerungen bezahlt:
@ -69,35 +68,20 @@ Sie haben soeben erfolgreich folgende Abo-Verlaengerungen bezahlt:
Mit freundlichen Grüssen
--
wo-bisch.ch`
wo-bisch.ch</pre>`
buf := bytes.NewBufferString(mail_message)
if _, err = buf.WriteTo(wc); err != nil {
log.Fatal(err)
}
sendEmail(username, "mail@wo-bisch.ch", mail_message)
}
func sendOrderEmail(username, body 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("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 + `
From: info@wo-bisch.ch
Subject: Bestelleingang auf https://wo-bisch.ch
Content-Type: text/plain; charset="UTF-8"
Lieber Administrator
Soeben ist eine Bestellung eingegangen, bitte pruefen:
Soeben ist eine Bestellung eingegangen, bitte prüfen:
` + body + `
@ -105,8 +89,5 @@ Mit freundlichen Grüssen
--
wo-bisch.ch`
buf := bytes.NewBufferString(mail_message)
if _, err = buf.WriteTo(wc); err != nil {
log.Fatal(err)
}
sendEmail(username, "mail@wo-bisch.ch", mail_message)
}

33
main.go
View File

@ -9,6 +9,24 @@ import (
"time"
)
var epoch = time.Unix(0, 0).Format(time.RFC1123)
var noCacheHeaders = map[string]string{
"Expires": epoch,
"Cache-Control": "no-cache, private, max-age=0",
"Pragma": "no-cache",
"X-Accel-Expires": "0",
}
var etagHeaders = []string{
"ETag",
"If-Modified-Since",
"If-Match",
"If-None-Match",
"If-Range",
"If-Unmodified-Since",
}
func serveTemplate(w http.ResponseWriter, r *http.Request) {
logit("Called URL: " + r.URL.Path)
// wennn kein File angegeben ist: index.html
@ -77,6 +95,18 @@ func serveTemplate(w http.ResponseWriter, r *http.Request) {
// because of caching, we need to set the Last-Modified header
w.Header().Set("Last-Modified", time.Now().Format(http.TimeFormat))
// Delete any ETag headers that may have been set
for _, v := range etagHeaders {
if r.Header.Get(v) != "" {
r.Header.Del(v)
}
}
// Set our NoCache headers
for k, v := range noCacheHeaders {
w.Header().Set(k, v)
}
if err := tmpl.ExecuteTemplate(w, "layout", &data); err != nil {
logit(err.Error())
http.Error(w, http.StatusText(500), 500)
@ -104,8 +134,9 @@ func main() {
http.HandleFunc("/save_tracker_greenzone", save_tracker_greenzoneHandler)
http.HandleFunc("/getstripepaymentintent", getstripepaymentintentHandler)
http.HandleFunc("/stripewebhook", stripeWebhookHandler)
http.HandleFunc("/checknodes", checkNodesHandler)
logit("Starting Web Application...")
http.ListenAndServe("127.0.0.1:4000", nil)
http.ListenAndServe(":4000", nil)
logit("Terminating Web Application...")
}

View File

@ -112,7 +112,7 @@ func metricsHandler(response http.ResponseWriter, request *http.Request) {
mystart = start[0]
}
url := "http://localhost:8086/api/v2/query?org=wobischorg"
url := getenv("INFLUX_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")
@ -163,16 +163,16 @@ func metricsHandler(response http.ResponseWriter, request *http.Request) {
fmt.Fprintf(response, " \"data\": [\n")
for scanner.Scan() {
s := strings.Split(scanner.Text(), ",")
fmt.Printf("Scanned Line: %v\n", s)
if (len(s) >= 12) && !(strings.HasPrefix(s[3], "_")) {
t, err := time.Parse(time.RFC3339, s[3])
fmt.Printf("Scanned Line: %v, %d elements\n", s, len(s))
if (len(s) >= 12) && !(strings.HasPrefix(s[5], "_")) {
t, err := time.Parse(time.RFC3339, s[5])
if err != nil {
fmt.Printf("error converting time: %s\n", s[3])
fmt.Printf("error converting time: %s\n", s[5])
continue
}
a := t.Unix()
lat = string2float64(s[9])
lon = string2float64(s[10])
lat = string2float64(s[len(s)-3])
lon = string2float64(s[len(s)-2])
if (lat == 0) || (lon == 0) {
fmt.Println("skip 0 value for lon/lat")
} else {
@ -195,7 +195,7 @@ func metricsHandler(response http.ResponseWriter, request *http.Request) {
fmt.Fprintf(response, " ")
}
fmt.Fprintf(response, "[%d, %s, %s, %s, %d]\n", a, s[9], s[10], s[11], vbat2percent(s[11]))
fmt.Fprintf(response, "[%d, %s, %s, %s, %d]\n", a, s[len(s)-3], s[len(s)-2], s[len(s)-1], vbat2percent(s[len(s)-1]))
}
@ -274,12 +274,12 @@ func downloadmetricsHandler(response http.ResponseWriter, request *http.Request)
mystart = start[0]
}
url := "http://localhost:8086/api/v2/query?org=wobischorg"
url := getenv("INFLUX_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.deveui == "%s")
|> filter(fn: (r) => r._field == "lon" or r._field == "lat")
|> filter(fn: (r) => r._field == "lon" or r._field == "lat" or r._field == "flags" or r._field == "vbat")
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")`, mystart, mystop, mydeveui))
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
@ -316,15 +316,34 @@ func downloadmetricsHandler(response http.ResponseWriter, request *http.Request)
for scanner.Scan() {
s := strings.Split(scanner.Text(), ",")
fmt.Printf("Scanned Line: %v\n", s)
if (len(s) >= 10) && !(strings.HasPrefix(s[3], "_")) {
t, err := time.Parse(time.RFC3339, s[3])
fmt.Printf("Scanned Line: %v, elements: %d\n", s, len(s))
if (len(s) >= 11) && !(strings.HasPrefix(s[5], "_")) {
s_flags := ""
lat := s[len(s)-3]
lon := s[len(s)-2]
if lat == "0" {
s_flags = "NOGPS"
} else {
s_flags = "GPS"
}
if len(s) == 12 {
flags, _ := strconv.Atoi(s[len(s)-4])
if flags&1 == 1 {
s_flags = s_flags + "+ALARM_BUTTON"
}
if flags&2 == 2 && lat != "0" {
s_flags = s_flags + "+OUTSIDE_GREENZONE"
}
}
t, err := time.Parse(time.RFC3339, s[5])
if err != nil {
fmt.Printf("error converting time: %s\n", s[3])
fmt.Printf("error converting time: %s\n", s[5])
continue
}
mytime := t.In(location).Format("02.01.2006 15:04")
fmt.Fprintf(response, "\"%s\",%s,%s\n", mytime, s[8], s[9])
vbat := s[len(s)-1]
percent := vbat2percent(vbat)
fmt.Fprintf(response, "\"%s\",%s,%s,%d,%s\n", mytime, lon, lat, percent, s_flags)
}
}
@ -360,7 +379,7 @@ func vbat2html(vbat string) string {
icon_color = "has-background-danger-dark has-text-white pl-2 pr-2"
} else if percent <= 40 {
fa_battery_string = "fa-battery-quarter"
icon_color = "has-background-danger-light has-text-danger pl-2 pr-2"
icon_color = "has-text-success pl-2 pr-2"
} else if percent <= 60 {
fa_battery_string = "fa-battery-half"
icon_color = "has-text-success pl-2 pr-2"
@ -383,7 +402,7 @@ func CalcDaysUntil(mydate string) int {
if err != nil {
days = 0
}
days = int(t.Sub(time.Now()).Hours() / 24)
days = int(t.Sub(time.Now().Truncate(24*time.Hour)).Hours() / 24)
return days
}
@ -391,12 +410,14 @@ func CalcDaysUntil(mydate string) int {
func getLastMetrics(deveui string) OneMetric {
var res OneMetric
url := "http://localhost:8086/api/v2/query?org=wobischorg"
url := getenv("INFLUX_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")
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
|> filter(fn: (r) => r.lat != 0)
|> filter(fn: (r) => r.lat != 0)
|> sort(columns: ["_time"], desc: false)
|> last(column: "_time")`, deveui))
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
@ -432,27 +453,68 @@ func getLastMetrics(deveui string) OneMetric {
location, err := time.LoadLocation("Europe/Zurich")
for scanner.Scan() {
s := strings.Split(scanner.Text(), ",")
fmt.Printf("BlaBla: %v\n", s)
if (len(s) >= 12) && !(strings.HasPrefix(s[3], "_")) {
t, err := time.Parse(time.RFC3339, s[3])
fmt.Printf("BlaBla: %v, elements: %d\n", s, len(s))
if (len(s) >= 12) && !(strings.HasPrefix(s[5], "_")) {
t, err := time.Parse(time.RFC3339, s[5])
if err != nil {
fmt.Printf("error converting time: %s\n", s[3])
fmt.Printf("error converting time: %s\n", s[5])
continue
}
res.Fw = s[8]
res.Fw = s[len(s)-4]
res.Timestamp = t.In(location).Format("02.01.2006 15:04")
if s[9] != "0" {
res.Lat = s[9]
res.Lat = s[len(s)-3]
res.PosTimestamp = t.In(location).Format("02.01.2006 15:04")
}
if s[10] != "0" {
res.Lon = s[10]
res.Lon = s[len(s)-2]
res.PosTimestamp = t.In(location).Format("02.01.2006 15:04")
}
res.BatteryPercent = strconv.Itoa(vbat2percent(s[11]))
res.BatteryPercentHTML = template.HTML(vbat2html(s[11]))
}
}
data2 := []byte(fmt.Sprintf(`from(bucket:"wobischbucket")
|> range(start:-365d)
|> filter(fn: (r) => r._measurement == "measurement" and r.deveui == "%s")
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
|> sort(columns: ["_time"], desc: false)
|> last(column: "_time")`, deveui))
req2, err := http.NewRequest("POST", url, bytes.NewBuffer(data2))
if err != nil {
log.Fatal("Error reading request. ", err)
}
// Set headers
req2.Header.Set("Authorization", "Token "+INFLUX_RO_TOKEN)
req2.Header.Set("accept", "application/csv")
req2.Header.Set("content-type", "application/vnd.flux")
// Send request
resp2, err := client.Do(req2)
if err != nil {
log.Fatal("Error reading response. ", err)
}
defer resp2.Body.Close()
fmt.Println("response Status:", resp2.Status)
fmt.Println("response Headers:", resp2.Header)
body2, err := ioutil.ReadAll(resp2.Body)
if err != nil {
log.Fatal("Error reading body. ", err)
}
fmt.Println("response Body 2:", string(body2))
scanner2 := bufio.NewScanner(strings.NewReader(string(body2)))
for scanner2.Scan() {
s := strings.Split(scanner2.Text(), ",")
fmt.Printf("BlaBla: %v, elements: %d\n", s, len(s))
if (len(s) >= 12) && !(strings.HasPrefix(s[5], "_")) {
res.BatteryPercent = strconv.Itoa(vbat2percent(s[len(s)-1]))
res.BatteryPercentHTML = template.HTML(vbat2html(s[len(s)-1]))
}
}

View File

@ -26,7 +26,7 @@ func newPool() *redis.Pool {
// Dial is an application supplied function for creating and
// configuring a connection.
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", ":6379")
c, err := redis.Dial("tcp", getenv("REDIS_CONNECTION_STRING", ":6379"))
if err != nil {
panic(err.Error())
}
@ -383,11 +383,7 @@ func prolongActivation(deveui string, years int) (string, error) {
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)
}
t_new = t.AddDate(years, 0, 0)
active_until_new := t_new.Format(layout)
// SET object
@ -452,7 +448,7 @@ func updateUser(username, password string) {
return
}
sendEmail(username, confirm_id)
sendEmailConfirm(username, confirm_id)
}
func checkLoginCredentials(username, password string) bool {
@ -521,3 +517,39 @@ func confirmUser(confirm_id string) bool {
return true
}
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
}

View File

@ -1,8 +1,8 @@
{{define "body_content"}}
<p class="title is-4">Kontakt</p>
<p>nbit Informatik GmbH<br />
Kirchweg 2<br />
3510 Konolfingen<br />
Untere Stockteile 16<br />
3806 B&ouml;nigen b. Interlaken<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>

View File

@ -3,46 +3,17 @@
{{define "body_content"}}
<p class="title is-4">Willkommen bei wo-bisch.ch</p>
<p>
Hier sind Sie am richtigen Ort, wenn Sie einen kleinen, sparsamen <a href="/product.html"><strong>GPS-Tracker</strong></a> suchen, der seinen Standort periodisch &uuml;bermittelt.
</p>
<p class="pl-4 pt-2">
<span class="icon"><i class="fa fa-arrow-right"></i></span>
einfacher Zugang auf Positionsdaten der letzten 365 Tage per Webbrowser
</p>
<p class="pl-4">
<span class="icon"><i class="fa fa-arrow-right"></i></span>
ohne SIM-Karte (verwendet LoraWAN Netzwerk der Swisscom)
</p>
<p class="pl-4">
<span class="icon"><i class="fa fa-arrow-right"></i></span>
Einsatz ganze Schweiz (Abdeckung: 95% der Schweizer Bev&ouml;lkerung)
</p>
<p class="pl-4 pb-2">
<span class="icon"><i class="fa fa-arrow-right"></i></span>
hohe Datensicherheit
Hier sind Sie am richtigen Ort, wenn Sie einen kleinen, sparsamen <a href="/product.html"><strong>GPS-Tracker</strong></a> suchen.
</p>
<div class="content">
<ul>
<li>einfacher Zugang auf Positionsdaten der letzten 365 Tage per Webbrowser</li>
<li>SMS und/oder E-Mail Alarm bei Druck des Knopfs oder Verlassen einer benutzerdefinierten "gr&uuml;nen Zone"</li>
<li>ohne Handy-Abo (verwendet LoraWAN Netzwerk der Swisscom)</li>
<li>Einsatz ganze Schweiz (Abdeckung: 95% der Schweizer Bev&ouml;lkerung)</li>
</ul>
</div>
<p class="pb-4">
Hier geht's zur <a href="/product.html">Produktbeschreibung</a>
</p>
{{ 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 }}
<div>
<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 class="pb-4">
<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>
</p>
</div>
{{end}}
{{end}}

View File

@ -5,7 +5,7 @@
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
crossorigin=""></script>
<script type="text/javascript" src="/static/js/Map.SelectArea.min.js"></script>
<script src="/static/js/Map.SelectArea.min.js"></script>
{{end}}
{{define "body_content"}}
{{ if ne .UserName "" }}

View File

@ -27,7 +27,7 @@
</figure>
</div>
<div class="column is-three-quarters">
<p class="is-size-2 has-text-weight-bold">CHF 150.00</p>
<p class="is-size-2 has-text-weight-bold">CHF 120.00</p>
<p class="is-size-7 neg-margin-1">pro Stück, inkl. MwSt.</p>
</div>
</div>
@ -36,6 +36,7 @@
<ul>
<li>GPS Tracker mit integriertem Akku</li>
<li>USB-Netzadapter mit Ladekabel USB-Typ-A</li>
<li>Umh&auml;ngeband</li>
<li>2 Dual Lock Klettpad 25 x 25mm zum optionalen einfachen Befestigen</li>
<li>12 Monate Abo, <span class="has-text-weight-bold">anschliessend CHF 5.00 pro Monat</span></li>
</ul>

View File

@ -23,7 +23,7 @@
</div>
</div>
<div class="content">
<p>Durch Dr&uuml;cken auf den roten Knopf f&uuml;r mehr als 3 Sekunden (bis die LED blinkt) wird der Alarmmodus ausgel&ouml;st. Damit wird anschliessend w&auml;hrend einer Stunde jede Minute der Standort &uuml;bermittelt. Die Alarmmeldungkann an eine SMS-Adresse und/oder E-Mail Adresse weitergeleitet werden. Die Alarm&uuml;bermittlung h&ouml;rt auf, sobald der GPS Tracker eine Best&auml;tigungsmeldung empfangen hat.</p>
<p>Durch Dr&uuml;cken auf den roten Knopf f&uuml;r mehr als 3 Sekunden (bis die LED blinkt) wird der Alarmmodus ausgel&ouml;st. Damit wird anschliessend w&auml;hrend einer Stunde jede Minute der Standort &uuml;bermittelt. Die Alarmmeldung kann an eine SMS-Adresse und/oder E-Mail Adresse weitergeleitet werden. Die Alarm&uuml;bermittlung h&ouml;rt auf, sobald der GPS Tracker eine Best&auml;tigungsmeldung empfangen hat.</p>
<p>Der GPS Tracker enth&auml;lt einen Akku, der per mitgeliefertem USB-Kabel aufgeladen werden kann. Der Akku-Ladezustand wird &uuml;berwacht und es werden Erinnerungsmails versendet, wenn der GPS Tracker wieder aufgeladen werden sollte (jeweils bei Unterschreiten von 20%, 10% und 5% Ladung).</p>
<p>Dieser GPS Tracker funktioniert ohne Mobilfunkabo/SIM-Karte. Daten werden &uuml;ber ein f&uuml;r solche Zwecke geschaffenes Funknetz (LoraWAN) &uuml;bermittelt: damit ist der geringe Strombedarf und auch die sehr geringe Strahlung (nur w&auml;hrend der &Uuml;bermittlung) begr&uuml;ndet. Das verwendete LoraWAN Netzwerk (Swisscom) deckt mehr als 95% der Schweizer Bev&ouml;kerung ab. Der Einsatz ist auf die Schweiz beschr&auml;nkt.</p>
<div class="content pb-4">
@ -57,6 +57,7 @@
<li>durch Klick auf "alle Positionen anzeigen" (Symbol oben rechts) werden alle Messpunkte dargestellt</li>
<li>Positionen können in ein CSV-Datei exportiert werden</li>
<li>der Kartenausschnitt wird automatisch so gezoomt, dass alle Positionen der gw&auml;hlten Periode darauf dargestellt werden k&ouml;nnen</li>
<li>durch gleichzeitiges Dr&uuml;cken der Ctrl-Taste kann mit der Maus eine "gr&uuml;ne Zone" selektiert werden</li>
</ul>
</p>
<p class="pt-4">Mit einem Doppelklick auf die Karte wird der automatische Zoom reaktiviert.</p>

View File

@ -58,9 +58,9 @@
</div>
<div class="field">
<label class="label">Alarm E-Mail (SMS)</label>
<label class="label">Alarm E-Mail(s) (mehrere durch Komma getrennt)</label>
<div class="control has-icons-right">
<input id="email" class="input" type="text" maxlength="50"> <span id="email_exclamation" class="icon is-small is-right">
<input id="email" class="input" type="text" maxlength="100"> <span id="email_exclamation" class="icon is-small is-right">
<i class="fas fa-exclamation-triangle"></i>
</span>
</div>
@ -152,7 +152,7 @@ Sie erhalten eine E-Mail, sobald die Zahlung erfolgreich abgeschlossen ist.
<p id="emailalarmactive_{{.Deveui}}" hidden>{{.Emailalarmactive}}</p>
<p id="email_{{.Deveui}}" hidden>{{.Email}}</p>
<p id="greenzone_{{.Deveui}}" hidden>{{.Greenzone}}</p>
<p id="lastmeasurement_{{.Deveui}}" class="has-text-centered">letzte &uuml;bermittelte Position: {{.Timestamp}}</p>
<p id="lastmeasurement_{{.Deveui}}" class="has-text-centered">letzte Position: {{.Timestamp}}</p>
<p id="batpercent_{{.Deveui}}" class="has-text-centered">{{.BatteryPercentHTML}}</p>
<div id="{{.Deveui}}" class="columns mt-3">
<p id="lat" hidden>{{.Lat}}</p>

View File

@ -3,7 +3,7 @@ html,body {
}
.section,.container {
height: 90%;
height: calc(100% - 24px);
}
hr {
@ -80,7 +80,7 @@ input:focus,
}
.mapbig {
height: 100%;
height: calc(100% - 100px);
}
.slidecontainer {

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -34,7 +34,7 @@ function vbat2icon(percent) {
icon_color = "has-background-danger-dark has-text-white pl-2 pr-2";
} else if (percent <= 40) {
fa_battery_string = "fa-battery-quarter";
icon_color = "has-background-danger-light has-text-danger pl-2 pr-2";
icon_color = "has-text-success pl-2 pr-2";
} else if (percent <= 60) {
fa_battery_string = "fa-battery-half";
icon_color = "has-text-success pl-2 pr-2";
@ -111,7 +111,7 @@ function refreshDatapoints(deveui, start, stop) {
for (let i of datapoints) {
unix_timestamp = i[0];
date = new Date(unix_timestamp * 1000);
markers.push(L.marker([i[1], i[2]], { icon: woBischIcon2, zIndexOffset: 1000 }).bindPopup(moment(date).format('DD.MM.YYYY HH:mm')));
markers.push(L.marker([i[1], i[2]], { icon: woBischIcon2, zIndexOffset: 500 }).bindPopup(moment(date).format('DD.MM.YYYY HH:mm')));
lat = i[1];
lon = i[2];
}
@ -128,14 +128,14 @@ function refreshDatapoints(deveui, start, stop) {
lcontrol = L.control.layers({}).addTo(map);
lcontrol.addOverlay(allmarkers, 'alle Positionen anzeigen');
bounds = new L.LatLngBounds([[mydata['max_lat'], mydata['max_lon']], [mydata['min_lat'], mydata['min_lon']]]);
map.fitBounds(bounds, { padding: [15, 15] });
map.fitBounds(bounds, { padding: [20, 20] });
marker.bindPopup(myhtml).openPopup();
} else {
$('#map').hide();
$('#datetimeslider').hide();
$('#chart').html(`<article class="message is-danger">
<div class="message-body">
Keine Messpunkte f&uuml;r diese Zeitperiode
Keine GPS-Messpunkte f&uuml;r diese Zeitperiode.<br /><br />Hinweis: heruntergeladene Daten enthalten alle &Uuml;bermittlungen, auch die ohne GPS-Messpunkt (L&auml;ngen- und Breitengrade haben den Wert 0).
</div>
</article>`);
$('#chart').show();
@ -247,7 +247,6 @@ $(document).ready(function () {
var tilelayer = new L.tileLayer(url);
map.attributionControl.setPrefix('Source: Swiss Federal Office of Topography')
map.addLayer(tilelayer);
//marker = L.marker([lat, lon], { icon: woBischIcon, zIndexOffset: 1000 }).addTo(map);
$('#reportrange span').html(moment(s_start).locale('de').format('D. MMM YYYY') + ' - ' + moment(s_stop).locale('de').format('D. MMM YYYY'));

View File

@ -8,9 +8,9 @@ function validate(what, text) {
} else if (what == 'smsnumber') {
var re = /^\+[0-9]{11,11}$/;
} else if (what == 'email') {
var re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
var re = /^(([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5}){1,25})+([,.](([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5}){1,25})+)*$/;
} else if (what == 'greenzone') {
var re = /^$|^[0-9]+\.[0-9]+,[0-9]+\.[0-9]+,[0-9]+\.[0-9]+,[0-9]+\.[0-9]+$/;
var re = /^ *$|^[0-9]+\.[0-9]+,[0-9]+\.[0-9]+,[0-9]+\.[0-9]+,[0-9]+\.[0-9]+$/;
}
return re.test(text);

View File

@ -3,8 +3,9 @@ package main
import (
"encoding/json"
"fmt"
"github.com/stripe/stripe-go/v72"
"github.com/stripe/stripe-go/v72/paymentintent"
"github.com/stripe/stripe-go/v74"
"github.com/stripe/stripe-go/v74/customer"
"github.com/stripe/stripe-go/v74/paymentintent"
"io/ioutil"
"log"
"net/http"
@ -21,6 +22,33 @@ func getStripePK() string {
return os.Getenv("STRIPE_PK")
}
func getCustomerid(name string) string {
// if customer does not already exist, create it...
var cid = ""
params := &stripe.CustomerSearchParams{}
params.Query = *stripe.String("name:'" + name + "'")
customers := customer.Search(params)
for customers.Next() {
fmt.Printf("%s\n", customers.Current().(*stripe.Customer).ID)
cid = customers.Current().(*stripe.Customer).ID
}
if cid == "" {
// create new customer
paramsc := &stripe.CustomerParams{
Name: stripe.String(name),
}
customer, err := customer.New(paramsc)
if err != nil {
log.Println("Error Creating Customer: " + name)
}
cid = customer.ID
}
return cid
}
func getstripepaymentintentHandler(response http.ResponseWriter, request *http.Request) {
name := getUserName(request)
if name != "" {
@ -51,10 +79,14 @@ func getstripepaymentintentHandler(response http.ResponseWriter, request *http.R
stripe.Key = getStripeKey()
// if customer does not already exist, create it...
customerid := getCustomerid(name)
// define payment
params := &stripe.PaymentIntentParams{
Amount: stripe.Int64(abo_amount),
Currency: stripe.String(string(stripe.CurrencyCHF)),
ReceiptEmail: stripe.String(name),
Customer: stripe.String(customerid),
Amount: stripe.Int64(abo_amount),
Currency: stripe.String(string(stripe.CurrencyCHF)),
}
params.AddMetadata("charge_data", charge_data[0])
params.AddMetadata("login_user", name)
@ -121,7 +153,7 @@ func stripeWebhookHandler(w http.ResponseWriter, req *http.Request) {
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)
HandlePayment(paymentIntent.Metadata["login_user"], paymentIntent.Metadata["charge_data"], paymentIntent.Amount)
// ... handle other event types
default:
fmt.Fprintf(os.Stderr, "Unexpected event type: %s\n", event.Type)

View File

@ -46,12 +46,6 @@
<span>Kontakt</span>
</div>
</a>
<a class="navbar-item" href="/order.html">
<div style="position:relative">
<span class="icon"><i class="fa fa-shopping-cart"></i></span>
<span>Bestellen</span>
</div>
</a>
{{ if ne .UserName "" }}
<a class="navbar-item" href="/tracker.html">
<div style="position:relative">

View File

@ -5,6 +5,7 @@ import (
"log"
"net/http"
"regexp"
"strings"
)
// tracker handler
@ -110,7 +111,7 @@ func save_tracker_settingsHandler(response http.ResponseWriter, request *http.Re
}
myemail := email[0]
match2, _ := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, myemail)
match2, _ := regexp.MatchString(`^(([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5}){1,25})+([,.](([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5}){1,25})+)*$`, myemail)
if !(match2) {
log.Println("Url Param 'email' is not valid")
fmt.Fprintf(response, "{ \"rc\": 15, \"msg\": \"email is not valid, must be in in format max.mustermann@example.com\" }")
@ -124,7 +125,7 @@ func save_tracker_settingsHandler(response http.ResponseWriter, request *http.Re
fmt.Fprintf(response, "{ \"rc\": 16, \"msg\": \"greenzone must be specified in URL\" }")
return
}
mygreenzone := greenzone[0]
mygreenzone := strings.ReplaceAll(greenzone[0], " ", "")
match3, _ := regexp.MatchString(`^$|^[0-9]+\.[0-9]+,[0-9]+\.[0-9]+,[0-9]+\.[0-9]+,[0-9]+\.[0-9]+$`, mygreenzone)
if !(match3) {