This commit is contained in:
Finn 2024-11-22 19:50:18 -08:00
parent b957988957
commit 58569dee2e
27 changed files with 623 additions and 220 deletions

126
httpserver/codes.go Normal file
View 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()
}

View file

@ -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,
})
}

View file

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