WIP
This commit is contained in:
parent
b957988957
commit
58569dee2e
27 changed files with 623 additions and 220 deletions
126
httpserver/codes.go
Normal file
126
httpserver/codes.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/config"
|
||||
"git.janky.solutions/finn/lockserver/db"
|
||||
"git.janky.solutions/finn/lockserver/zwavejs"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func addCode(c echo.Context) error {
|
||||
// name := c.FormValue("name")
|
||||
code := c.FormValue("code")
|
||||
|
||||
queries, dbc, err := db.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbc.Close()
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
// generate a code that isn't used on any lock or if the user
|
||||
// supplied a code, check if it's already used on any lock and
|
||||
// error out if it is
|
||||
noCodeSupplied := code == "" // if no code is supplied, we must generate one
|
||||
for {
|
||||
if noCodeSupplied {
|
||||
code = generateCode()
|
||||
logrus.WithField("code", code).Debug("generated code")
|
||||
}
|
||||
|
||||
row, err := queries.GetLockCodesByCode(ctx, code)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking if code was already in use: %v", err)
|
||||
}
|
||||
|
||||
if len(row) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if noCodeSupplied {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("requested code (%s) is already in use on some or all locks", code)
|
||||
}
|
||||
|
||||
locks, err := queries.GetLocks(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return errors.New("no locks currently registered, cannot add code")
|
||||
}
|
||||
return fmt.Errorf("error getting locks: %v", err)
|
||||
}
|
||||
|
||||
client := c.Get(contextKeyZWaveClient).(*zwavejs.Client)
|
||||
for _, lock := range locks {
|
||||
slot, err := queries.GetEmptySlot(ctx, lock.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("no empty code slots found on lock %s (ZWaveDeviceID=%d ID=%d)", lock.Name, lock.ZwaveDeviceID, lock.ID)
|
||||
}
|
||||
return fmt.Errorf("error looking for empty code slot on lock %s (ZWaveDeviceID=%d ID=%d): %v", lock.Name, lock.ZwaveDeviceID, lock.ID, err)
|
||||
}
|
||||
|
||||
// send the code to the lock
|
||||
// sample from https://github.com/FutureTense/keymaster/blob/f4f1046bddb7901cbd3ce7820886be1ff7895fe7/tests/test_services.py#L88
|
||||
//
|
||||
// {
|
||||
// "ccVersion": 1,
|
||||
// "commandClassName": "User Code",
|
||||
// "commandClass": 99,
|
||||
// "endpoint": 0,
|
||||
// "property": "userCode",
|
||||
// "propertyName": "userCode",
|
||||
// "propertyKey": 1,
|
||||
// "propertyKeyName": "1",
|
||||
// "metadata": {
|
||||
// "type": "string",
|
||||
// "readable": True,
|
||||
// "writeable": True,
|
||||
// "minLength": 4,
|
||||
// "maxLength": 10,
|
||||
// "label": "User Code (1)",
|
||||
// },
|
||||
// "value": "123456",
|
||||
// }
|
||||
err = client.SetNodeValue(ctx, int(lock.ZwaveDeviceID), zwavejs.NodeValue{
|
||||
CCVersion: 1,
|
||||
CommandClassName: zwavejs.CommandClassNameUserCode,
|
||||
CommandClass: zwavejs.CommandClassUserCode,
|
||||
Endpoint: 0,
|
||||
Property: zwavejs.AnyType{Type: zwavejs.AnyTypeString, String: string(zwavejs.PropertyUserCode)},
|
||||
PropertyName: zwavejs.AnyType{Type: zwavejs.AnyTypeString, String: string(zwavejs.PropertyUserCode)},
|
||||
PropertyKey: zwavejs.AnyType{Type: zwavejs.AnyTypeInt, Int: int(slot.ID)},
|
||||
}, zwavejs.AnyType{Type: zwavejs.AnyTypeString, String: code})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error pushing code to lock %s (ZWaveDeviceID=%d ID=%d): %v", lock.Name, lock.ZwaveDeviceID, lock.ID, err)
|
||||
}
|
||||
|
||||
// set the code on the lock
|
||||
// set the code in the db
|
||||
}
|
||||
// add row to user_codes
|
||||
// add row to user_code_slots
|
||||
// redirect to /user-codes/<code id>
|
||||
|
||||
return errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
var codeCharSet = "0123456789"
|
||||
|
||||
func generateCode() string {
|
||||
var builder strings.Builder
|
||||
for i := 0; i < config.C.GeneratedCodeLength; i++ {
|
||||
builder.WriteByte(codeCharSet[rand.Int31n(int32(len(codeCharSet)))])
|
||||
}
|
||||
return builder.String()
|
||||
}
|
|
@ -3,7 +3,6 @@ package httpserver
|
|||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
echo "github.com/labstack/echo/v4"
|
||||
|
||||
|
@ -11,45 +10,11 @@ import (
|
|||
"git.janky.solutions/finn/lockserver/frontend"
|
||||
)
|
||||
|
||||
type baseTemplateData struct {
|
||||
BaseURL string
|
||||
Username string
|
||||
UserDisplayName string
|
||||
}
|
||||
|
||||
type indexTemplateData struct {
|
||||
baseTemplateData
|
||||
|
||||
Locks []db.Lock
|
||||
}
|
||||
|
||||
// header=X-Forwarded-Proto value="[http]"
|
||||
// header=Accept value="[text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8]"
|
||||
// header=Referer value="[https://ha.herzfeld.casa/hassio/store]"
|
||||
// header=X-Forwarded-For value="[10.5.0.235, 10.5.1.245, 172.30.32.1]"
|
||||
// header=X-Forwarded-Host value="[ha.herzfeld.casa]"
|
||||
// header=Sec-Fetch-Mode value="[same-origin]"
|
||||
// header=X-Ingress-Path value="[/api/hassio_ingress/VBVnN1BaXJun6ydy3xCuRoy1HyGH__attJ-gN193OU0]"
|
||||
// header=Accept-Encoding value="[gzip, deflate, br]"
|
||||
// header=Content-Length value="[0]"
|
||||
// header=X-Remote-User-Id value="[a65ffe9ec5664336b4c08def63384aa0]"
|
||||
// header=Accept-Language value="[en-US,en;q=0.5]"
|
||||
// header=Upgrade-Insecure-Requests value="[1]"
|
||||
// header=Sec-Fetch-Dest value="[empty]"
|
||||
// header=X-Hass-Source value="[core.ingress]"
|
||||
// header=X-Remote-User-Name value="[finn]"
|
||||
// header=X-Remote-User-Display-Name value="[Finn]"
|
||||
// header=Cookie value="[ingress_session=3dcd2006c013774475c5fec0a4301689803eacd343a3554ba2d920440bced0d973808524139c998d74b614e7950fe5f08cead286cde56e3541a3980b7d506a85]"
|
||||
// header=User-Agent value="[Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0]"
|
||||
// header=Sec-Fetch-Site value="[same-origin]"
|
||||
// header=Connection value="[close]"
|
||||
|
||||
func getBaseTemplateData(headers http.Header) baseTemplateData {
|
||||
return baseTemplateData{
|
||||
BaseURL: headers.Get("X-Ingress-Path"),
|
||||
Username: headers.Get("X-Remote-User-Name"),
|
||||
UserDisplayName: headers.Get("X-Remote-User-Display-Name"),
|
||||
}
|
||||
ActiveCodes []db.IssuedCode
|
||||
Locks []db.Lock
|
||||
}
|
||||
|
||||
func indexHandler(c echo.Context) error {
|
||||
|
@ -59,13 +24,21 @@ func indexHandler(c echo.Context) error {
|
|||
}
|
||||
defer dbc.Close()
|
||||
|
||||
locks, err := queries.GetLocks(c.Request().Context())
|
||||
ctx := c.Request().Context()
|
||||
|
||||
activeCodes, err := queries.GetActiveCodes(ctx)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
|
||||
locks, err := queries.GetLocks(ctx)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
|
||||
return frontend.Templates.ExecuteTemplate(c.Response(), "index.html", indexTemplateData{
|
||||
baseTemplateData: getBaseTemplateData(c.Request().Header),
|
||||
ActiveCodes: activeCodes,
|
||||
Locks: locks,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,11 +15,9 @@ import (
|
|||
"git.janky.solutions/finn/lockserver/zwavejs"
|
||||
)
|
||||
|
||||
var server *echo.Echo
|
||||
const contextKeyZWaveClient = "zwave-client"
|
||||
|
||||
type lockserver struct {
|
||||
ZWaveJS *zwavejs.Client
|
||||
}
|
||||
var server *echo.Echo
|
||||
|
||||
func ListenAndServe(client *zwavejs.Client) {
|
||||
server = echo.New()
|
||||
|
@ -27,10 +25,13 @@ func ListenAndServe(client *zwavejs.Client) {
|
|||
server.HidePort = true
|
||||
server.HTTPErrorHandler = handleError
|
||||
server.Use(accessLogMiddleware)
|
||||
server.RouteNotFound("/*", tmpl("404.html"))
|
||||
|
||||
server.StaticFS("/static", frontend.Static)
|
||||
server.GET("/", indexHandler)
|
||||
server.GET("/locks/{id}", lockHandler)
|
||||
server.GET("/locks/:id", lockHandler)
|
||||
server.GET("/add-code", tmpl("add-code.html"))
|
||||
server.POST("/add-code", addCode)
|
||||
|
||||
logrus.WithField("address", config.C.HTTPBind).Info("starting http server")
|
||||
err := server.Start(config.C.HTTPBind)
|
||||
|
@ -39,10 +40,17 @@ func ListenAndServe(client *zwavejs.Client) {
|
|||
}
|
||||
}
|
||||
|
||||
type errorTemplateData struct {
|
||||
baseTemplateData
|
||||
Error error
|
||||
}
|
||||
|
||||
func handleError(err error, c echo.Context) {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
_ = c.JSON(http.StatusNotFound, map[string]string{"error": "not found"})
|
||||
return
|
||||
err = frontend.Templates.ExecuteTemplate(c.Response(), "404.html", getBaseTemplateData(c.Request().Header))
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
|
@ -50,7 +58,10 @@ func handleError(err error, c echo.Context) {
|
|||
"method": c.Request().Method,
|
||||
"error": err,
|
||||
}).Error("error handling request")
|
||||
_ = c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"})
|
||||
frontend.Templates.ExecuteTemplate(c.Response(), "500.html", errorTemplateData{
|
||||
baseTemplateData: getBaseTemplateData(c.Request().Header),
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
|
||||
func Shutdown(ctx context.Context) error {
|
||||
|
@ -81,3 +92,32 @@ func accessLogMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
|||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func addZWaveClientToContextKey(client *zwavejs.Client) func(echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
c.Set(contextKeyZWaveClient, client)
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type baseTemplateData struct {
|
||||
BaseURL string
|
||||
Username string
|
||||
UserDisplayName string
|
||||
}
|
||||
|
||||
func getBaseTemplateData(headers http.Header) baseTemplateData {
|
||||
return baseTemplateData{
|
||||
BaseURL: headers.Get("X-Ingress-Path"),
|
||||
Username: headers.Get("X-Remote-User-Name"),
|
||||
UserDisplayName: headers.Get("X-Remote-User-Display-Name"),
|
||||
}
|
||||
}
|
||||
|
||||
func tmpl(filename string) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return frontend.Templates.ExecuteTemplate(c.Response(), filename, getBaseTemplateData(c.Request().Header))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue