rip out all auth stuff, add a containerfile
All checks were successful
/ build-container (push) Successful in 1m3s
All checks were successful
/ build-container (push) Successful in 1m3s
This commit is contained in:
parent
7fdd02bd33
commit
350fc3b339
27 changed files with 885 additions and 28 deletions
19
.forgejo/workflows/docker-build.yaml
Normal file
19
.forgejo/workflows/docker-build.yaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: library/docker:dind
|
||||
steps:
|
||||
- run: apk add --no-cache nodejs git
|
||||
- name: login to container registry
|
||||
run: echo "${{ secrets.PACKAGE_PUBLISH_TOKEN }}" | docker login --username finn --password-stdin git.janky.solutions
|
||||
- name: build container
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
file: Containerfile
|
||||
tags: git.janky.solutions/finn/lockserver:latest
|
||||
push: true
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
lockserver.db
|
||||
lockserver.json
|
||||
|
|
24
Containerfile
Normal file
24
Containerfile
Normal file
|
@ -0,0 +1,24 @@
|
|||
# lockserver: manage z-wave locks
|
||||
# Copyright (C) 2024 Finn Herzfeld
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
FROM library/golang:1.21 AS build
|
||||
ADD . /go/lockserver
|
||||
WORKDIR /go/lockserver
|
||||
RUN CGO_ENABLED=0 go build .
|
||||
|
||||
FROM scratch
|
||||
COPY --from=build /go/lockserver/lockserver /lockserver
|
||||
ENTRYPOINT ["/lockserver"]
|
|
@ -19,6 +19,11 @@ func main() {
|
|||
|
||||
func run() {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
|
||||
if err := config.Load(); err != nil {
|
||||
logrus.WithError(err).Fatal("error loading config")
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
|
|
|
@ -1,13 +1,70 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ZWaveJSServer string
|
||||
Database string
|
||||
HTTPBind string
|
||||
ZWaveJSServer string `json:"zwave-js-server"`
|
||||
SqliteDatabase string `json:"sqlite-database"`
|
||||
HTTPBind string `json:"http-bind"`
|
||||
SessionSecrets []JSONBytes `json:"session-secrets"`
|
||||
}
|
||||
|
||||
var C = Config{
|
||||
ZWaveJSServer: "ws://home-assistant:3000",
|
||||
Database: "lockserver.db",
|
||||
HTTPBind: ":8080",
|
||||
ZWaveJSServer: "ws://home-assistant:3000",
|
||||
SqliteDatabase: "lockserver.db",
|
||||
HTTPBind: ":8080",
|
||||
SessionSecrets: []JSONBytes{},
|
||||
}
|
||||
|
||||
func Load() error {
|
||||
for _, path := range []string{"lockserver.json", "/etc/lockserver.json"} {
|
||||
err := load(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
logrus.WithField("file", path).Info("loaded config")
|
||||
}
|
||||
|
||||
if len(C.SessionSecrets) == 0 {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"rand_64": base64.URLEncoding.EncodeToString(securecookie.GenerateRandomKey(64)),
|
||||
"rand_32": base64.URLEncoding.EncodeToString(securecookie.GenerateRandomKey(32)),
|
||||
}).Info("some potential session secrets for you (hint: use both)")
|
||||
return errors.New("no session secrets defined, some possible values have been logged")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func load(path string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := json.NewDecoder(f).Decode(&C); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Config) GetSessionSecrets() [][]byte {
|
||||
var resp [][]byte
|
||||
for _, s := range c.SessionSecrets {
|
||||
resp = append(resp, s.AsByteArrayArray())
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
|
37
config/types.go
Normal file
37
config/types.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type JSONBytes []byte
|
||||
|
||||
func (j JSONBytes) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(base64.URLEncoding.EncodeToString(j))
|
||||
}
|
||||
|
||||
func (j JSONBytes) String() string {
|
||||
return base64.URLEncoding.EncodeToString(j)
|
||||
}
|
||||
|
||||
func (j *JSONBytes) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
err := json.Unmarshal(data, &str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsed, err := base64.URLEncoding.DecodeString(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*j = JSONBytes(parsed)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j JSONBytes) AsByteArrayArray() []byte {
|
||||
return []byte(j)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
|
||||
|
@ -12,7 +13,7 @@ import (
|
|||
)
|
||||
|
||||
func Get() (*Queries, *sql.DB, error) {
|
||||
db, err := sql.Open("sqlite3", config.C.Database)
|
||||
db, err := sql.Open("sqlite3", config.C.SqliteDatabase)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -24,7 +25,7 @@ func Get() (*Queries, *sql.DB, error) {
|
|||
var migrations embed.FS
|
||||
|
||||
func Migrate() error {
|
||||
logrus.WithField("dbfile", config.C.Database).Info("migrating database")
|
||||
logrus.WithField("dbfile", config.C.SqliteDatabase).Info("migrating database")
|
||||
|
||||
_, conn, err := Get()
|
||||
if err != nil {
|
||||
|
@ -51,3 +52,36 @@ func NullString(s string) sql.NullString {
|
|||
String: s,
|
||||
}
|
||||
}
|
||||
|
||||
type loggingDBTX struct {
|
||||
Next DBTX
|
||||
}
|
||||
|
||||
func (l loggingDBTX) ExecContext(ctx context.Context, query string, params ...interface{}) (sql.Result, error) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"query": query,
|
||||
"params": params,
|
||||
}).Debug("ExecContext")
|
||||
return l.Next.ExecContext(ctx, query, params...)
|
||||
}
|
||||
|
||||
func (l loggingDBTX) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"query": query,
|
||||
}).Debug("PrepareContext")
|
||||
return l.Next.PrepareContext(ctx, query)
|
||||
}
|
||||
func (l loggingDBTX) QueryContext(ctx context.Context, query string, params ...interface{}) (*sql.Rows, error) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"query": query,
|
||||
"params": params,
|
||||
}).Debug("QueryContext")
|
||||
return l.Next.QueryContext(ctx, query, params...)
|
||||
}
|
||||
func (l loggingDBTX) QueryRowContext(ctx context.Context, query string, params ...interface{}) *sql.Row {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"query": query,
|
||||
"params": params,
|
||||
}).Debug("QueryRowContext")
|
||||
return l.Next.QueryRowContext(ctx, query, params...)
|
||||
}
|
||||
|
|
|
@ -25,10 +25,35 @@ CREATE TABLE lock_log (
|
|||
state TEXT NOT NULL,
|
||||
code INTEGER REFERENCES lock_code_slots(id)
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE user_codes (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user INTEGER REFERENCES users(id),
|
||||
name TEXT,
|
||||
code TEXT UNIQUE NOT NULL,
|
||||
start DATETIME,
|
||||
end DATETIME
|
||||
);
|
||||
|
||||
CREATE TABLE user_code_slots (
|
||||
user_code REFERENCES user_codes(id),
|
||||
lock REFERENCES locks(id),
|
||||
slot REFERENCES lock_code_slots(id),
|
||||
|
||||
UNIQUE (user_code, lock)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE user_code_slots;
|
||||
DROP TABLE user_codes;
|
||||
DROP TABLE users;
|
||||
DROP TABLE lock_log;
|
||||
DROP TABLE lock_code_slots;
|
||||
DROP TABLE locks;
|
||||
|
|
20
db/models.go
20
db/models.go
|
@ -30,3 +30,23 @@ type LockLog struct {
|
|||
State string
|
||||
Code sql.NullInt64
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
Name string
|
||||
}
|
||||
|
||||
type UserCode struct {
|
||||
ID int64
|
||||
User sql.NullInt64
|
||||
Name sql.NullString
|
||||
Code string
|
||||
Start sql.NullTime
|
||||
End sql.NullTime
|
||||
}
|
||||
|
||||
type UserCodeSlot struct {
|
||||
UserCode interface{}
|
||||
Lock interface{}
|
||||
Slot interface{}
|
||||
}
|
||||
|
|
14
db/queries/user_codes.sql
Normal file
14
db/queries/user_codes.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
-- name: CreateUserCode :one
|
||||
INSERT INTO user_codes (user, code, start, end) VALUES (?, ?, ?, ?) RETURNING id;
|
||||
|
||||
-- name: DeleteUserCode :exec
|
||||
DELETE FROM user_codes WHERE id = ?;
|
||||
|
||||
-- name: AssignUserCodeSlot :exec
|
||||
INSERT INTO user_code_slots (user_code, lock, slot) VALUES (?, ?, ?);
|
||||
|
||||
-- name: UnassignUserCodeSlot :exec
|
||||
DELETE FROM user_code_slots WHERE user_code = ? AND lock = ?;
|
||||
|
||||
-- name: GetAllUserCodes :many
|
||||
SELECT user_codes.*, users.name FROM user_codes, users WHERE user_codes.user = users.id;
|
8
db/queries/users.sql
Normal file
8
db/queries/users.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
-- name: CreateUser :exec
|
||||
INSERT INTO users (name) VALUES (?);
|
||||
|
||||
-- name: GetUserByName :one
|
||||
SELECT * FROM users WHERE name = ?;
|
||||
|
||||
-- name: GetUserByID :one
|
||||
SELECT * FROM users WHERE id = ?;
|
27
db/user_codes.go
Normal file
27
db/user_codes.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/openapi"
|
||||
)
|
||||
|
||||
func (u GetAllUserCodesRow) OpenAPI() openapi.UserCode {
|
||||
resp := openapi.UserCode{Code: &u.Code}
|
||||
|
||||
if u.Name.Valid {
|
||||
resp.User = &u.Name.String
|
||||
}
|
||||
|
||||
if u.Start.Valid {
|
||||
start := u.Start.Time.Format(time.RFC3339)
|
||||
resp.Starts = &start
|
||||
}
|
||||
|
||||
if u.End.Valid {
|
||||
end := u.End.Time.Format(time.RFC3339)
|
||||
resp.Ends = &end
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
117
db/user_codes.sql.go
Normal file
117
db/user_codes.sql.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.20.0
|
||||
// source: user_codes.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const assignUserCodeSlot = `-- name: AssignUserCodeSlot :exec
|
||||
INSERT INTO user_code_slots (user_code, lock, slot) VALUES (?, ?, ?)
|
||||
`
|
||||
|
||||
type AssignUserCodeSlotParams struct {
|
||||
UserCode interface{}
|
||||
Lock interface{}
|
||||
Slot interface{}
|
||||
}
|
||||
|
||||
func (q *Queries) AssignUserCodeSlot(ctx context.Context, arg AssignUserCodeSlotParams) error {
|
||||
_, err := q.db.ExecContext(ctx, assignUserCodeSlot, arg.UserCode, arg.Lock, arg.Slot)
|
||||
return err
|
||||
}
|
||||
|
||||
const createUserCode = `-- name: CreateUserCode :one
|
||||
INSERT INTO user_codes (user, code, start, end) VALUES (?, ?, ?, ?) RETURNING id
|
||||
`
|
||||
|
||||
type CreateUserCodeParams struct {
|
||||
User sql.NullInt64
|
||||
Code string
|
||||
Start sql.NullTime
|
||||
End sql.NullTime
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUserCode(ctx context.Context, arg CreateUserCodeParams) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, createUserCode,
|
||||
arg.User,
|
||||
arg.Code,
|
||||
arg.Start,
|
||||
arg.End,
|
||||
)
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const deleteUserCode = `-- name: DeleteUserCode :exec
|
||||
DELETE FROM user_codes WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteUserCode(ctx context.Context, id int64) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteUserCode, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getAllUserCodes = `-- name: GetAllUserCodes :many
|
||||
SELECT user_codes.id, user_codes.user, user_codes.name, user_codes.code, user_codes.start, user_codes."end", users.name FROM user_codes, users WHERE user_codes.user = users.id
|
||||
`
|
||||
|
||||
type GetAllUserCodesRow struct {
|
||||
ID int64
|
||||
User sql.NullInt64
|
||||
Name sql.NullString
|
||||
Code string
|
||||
Start sql.NullTime
|
||||
End sql.NullTime
|
||||
Name_2 string
|
||||
}
|
||||
|
||||
func (q *Queries) GetAllUserCodes(ctx context.Context) ([]GetAllUserCodesRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAllUserCodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetAllUserCodesRow
|
||||
for rows.Next() {
|
||||
var i GetAllUserCodesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.User,
|
||||
&i.Name,
|
||||
&i.Code,
|
||||
&i.Start,
|
||||
&i.End,
|
||||
&i.Name_2,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const unassignUserCodeSlot = `-- name: UnassignUserCodeSlot :exec
|
||||
DELETE FROM user_code_slots WHERE user_code = ? AND lock = ?
|
||||
`
|
||||
|
||||
type UnassignUserCodeSlotParams struct {
|
||||
UserCode interface{}
|
||||
Lock interface{}
|
||||
}
|
||||
|
||||
func (q *Queries) UnassignUserCodeSlot(ctx context.Context, arg UnassignUserCodeSlotParams) error {
|
||||
_, err := q.db.ExecContext(ctx, unassignUserCodeSlot, arg.UserCode, arg.Lock)
|
||||
return err
|
||||
}
|
41
db/users.sql.go
Normal file
41
db/users.sql.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.20.0
|
||||
// source: users.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const createUser = `-- name: CreateUser :exec
|
||||
INSERT INTO users (name) VALUES (?)
|
||||
`
|
||||
|
||||
func (q *Queries) CreateUser(ctx context.Context, name string) error {
|
||||
_, err := q.db.ExecContext(ctx, createUser, name)
|
||||
return err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, name FROM users WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserByID, id)
|
||||
var i User
|
||||
err := row.Scan(&i.ID, &i.Name)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByName = `-- name: GetUserByName :one
|
||||
SELECT id, name FROM users WHERE name = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByName(ctx context.Context, name string) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserByName, name)
|
||||
var i User
|
||||
err := row.Scan(&i.ID, &i.Name)
|
||||
return i, err
|
||||
}
|
30
frontend/frontend.go
Normal file
30
frontend/frontend.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package frontend
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed static
|
||||
static embed.FS
|
||||
Static fs.FS
|
||||
|
||||
//go:embed *.html
|
||||
templatesFS embed.FS
|
||||
Templates *template.Template
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
Templates, err = template.ParseFS(templatesFS, "*")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Static, err = fs.Sub(static, "static")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
29
frontend/index.html
Normal file
29
frontend/index.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Lock Server</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
<link rel="stylesheet" href="/static/main.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<header>
|
||||
<h1>Lockserver</h1>
|
||||
<div id="user"></div>
|
||||
</header>
|
||||
<div id="main">
|
||||
<h3>locks</h3>
|
||||
<ul>
|
||||
{{ range .Locks }}<li><a href="/lock/{{ .ID }}">{{ .Name }} ({{ .ZwaveDeviceID }})</a></li>{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
<footer>
|
||||
<code>lockserver v0.x.x aaaaaa</code>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
12
frontend/static/main.css
Normal file
12
frontend/static/main.css
Normal file
|
@ -0,0 +1,12 @@
|
|||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
min-height: calc(100% - 2em);
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
row-gap: 1em;
|
||||
padding: 1em;
|
||||
}
|
4
go.mod
4
go.mod
|
@ -3,8 +3,10 @@ module git.janky.solutions/finn/lockserver
|
|||
go 1.21.8
|
||||
|
||||
require (
|
||||
github.com/failsafe-go/failsafe-go v0.6.2
|
||||
github.com/getkin/kin-openapi v0.124.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/securecookie v1.1.2
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/labstack/echo/v4 v4.11.4
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
|
@ -32,7 +34,7 @@ require (
|
|||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
|
14
go.sum
14
go.sum
|
@ -39,6 +39,8 @@ github.com/elastic/go-sysinfo v1.11.2 h1:mcm4OSYVMyws6+n2HIVMGkln5HOpo5Ie1ZmbbNn
|
|||
github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ=
|
||||
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
||||
github.com/failsafe-go/failsafe-go v0.6.2 h1:zRyfYykM080+h40uUuf9HYLRn7vpnR+wjcg68fhwD28=
|
||||
github.com/failsafe-go/failsafe-go v0.6.2/go.mod h1:UCRnPYTVzBt7QGPFAAmFZUtB49dCLVFt38YYzGHXBCA=
|
||||
github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M=
|
||||
github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
|
||||
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||
|
@ -60,10 +62,14 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW
|
|||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
|
@ -158,8 +164,8 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKk
|
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tursodatabase/libsql-client-go v0.0.0-20240220085343-4ae0eb9d0898 h1:1MvEhzI5pvP27e9Dzz861mxk9WzXZLSJwzOU67cKTbU=
|
||||
github.com/tursodatabase/libsql-client-go v0.0.0-20240220085343-4ae0eb9d0898/go.mod h1:9bKuHS7eZh/0mJndbUOrCx8Ej3PlsRDszj4L7oVYMPQ=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
|
@ -194,8 +200,8 @@ golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
|||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
44
httpserver/browser-endpoints.go
Normal file
44
httpserver/browser-endpoints.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
echo "github.com/labstack/echo/v4"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/db"
|
||||
"git.janky.solutions/finn/lockserver/frontend"
|
||||
)
|
||||
|
||||
type browserEndpoints struct{}
|
||||
|
||||
type baseTemplateData struct {
|
||||
Username string
|
||||
UserDisplayName string
|
||||
}
|
||||
|
||||
func (b browserEndpoints) Register(e *echo.Echo) {
|
||||
e.GET("/", b.Index)
|
||||
e.StaticFS("/static", frontend.Static)
|
||||
}
|
||||
|
||||
type indexTemplateData struct {
|
||||
baseTemplateData
|
||||
|
||||
Locks []db.Lock
|
||||
}
|
||||
|
||||
func (browserEndpoints) Index(c echo.Context) error {
|
||||
queries, dbc, err := db.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbc.Close()
|
||||
|
||||
locks, err := queries.GetLocks(c.Request().Context())
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
|
||||
return frontend.Templates.ExecuteTemplate(c.Response(), "index.html", indexTemplateData{Locks: locks})
|
||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
echo "github.com/labstack/echo/v4"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
@ -25,9 +26,13 @@ func ListenAndServe(client *zwavejs.Client) {
|
|||
server.HideBanner = true
|
||||
server.HidePort = true
|
||||
server.HTTPErrorHandler = handleError
|
||||
server.Use(accessLogMiddleware)
|
||||
|
||||
browserEndpoints{}.Register(server)
|
||||
|
||||
openapi.RegisterHandlersWithBaseURL(server, lockserver{ZWaveJS: client}, "/api")
|
||||
|
||||
logrus.WithField("address", config.C.HTTPBind).Info("starting http server")
|
||||
err := server.Start(config.C.HTTPBind)
|
||||
if err != http.ErrServerClosed {
|
||||
logrus.WithError(err).Fatal("error starting http server")
|
||||
|
@ -40,7 +45,11 @@ func handleError(err error, c echo.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
logrus.WithError(err).Error("error handling request")
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"path": c.Request().URL.Path,
|
||||
"method": c.Request().Method,
|
||||
"error": err,
|
||||
}).Error("error handling request")
|
||||
_ = c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"})
|
||||
}
|
||||
|
||||
|
@ -51,3 +60,24 @@ func Shutdown(ctx context.Context) error {
|
|||
|
||||
return server.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func accessLogMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
start := time.Now()
|
||||
err := next(c)
|
||||
|
||||
log := logrus.WithFields(logrus.Fields{
|
||||
"method": c.Request().Method,
|
||||
"path": c.Request().URL.Path,
|
||||
"duration": time.Since(start),
|
||||
"status": c.Response().Status,
|
||||
"source": c.Request().RemoteAddr,
|
||||
})
|
||||
if err != nil {
|
||||
log = log.WithError(err)
|
||||
}
|
||||
log.Info("request handled")
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
51
httpserver/user-codes-admin.go
Normal file
51
httpserver/user-codes-admin.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/db"
|
||||
"git.janky.solutions/finn/lockserver/openapi"
|
||||
echo "github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func (l lockserver) AddUserCode(c echo.Context) error {
|
||||
queries, dbc, err := db.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbc.Close()
|
||||
ctx := c.Request().Context()
|
||||
|
||||
if _, err = queries.CreateUserCode(ctx, db.CreateUserCodeParams{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func (l lockserver) GetAllUserCodes(c echo.Context) error {
|
||||
queries, dbc, err := db.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbc.Close()
|
||||
ctx := c.Request().Context()
|
||||
|
||||
resp := []openapi.UserCode{}
|
||||
|
||||
codes, err := queries.GetAllUserCodes(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, code := range codes {
|
||||
resp = append(resp, code.OpenAPI())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
6
lockserver.defaults.json
Normal file
6
lockserver.defaults.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"zwave-js-server": "ws://home-assistant:3000",
|
||||
"sqlite-database": "lockserver.db",
|
||||
"http-bind": ":8080",
|
||||
"session-secrets": []
|
||||
}
|
31
make-defaults-json.go
Normal file
31
make-defaults-json.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := writeConfig("lockserver.defaults.json"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeConfig(path string) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
encoder := json.NewEncoder(f)
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
if err := encoder.Encode(config.C); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -27,12 +27,33 @@ type LockCodeSlot struct {
|
|||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// UserCode defines model for UserCode.
|
||||
type UserCode struct {
|
||||
Code *string `json:"code,omitempty"`
|
||||
|
||||
// Ends when the code expires
|
||||
Ends *string `json:"ends,omitempty"`
|
||||
|
||||
// Starts when the code becomes active
|
||||
Starts *string `json:"starts,omitempty"`
|
||||
User *string `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// AddUserCodeJSONRequestBody defines body for AddUserCode for application/json ContentType.
|
||||
type AddUserCodeJSONRequestBody = UserCode
|
||||
|
||||
// PutLockCodeSlotJSONRequestBody defines body for PutLockCodeSlot for application/json ContentType.
|
||||
type PutLockCodeSlotJSONRequestBody = LockCodeSlot
|
||||
|
||||
// ServerInterface represents all server handlers.
|
||||
type ServerInterface interface {
|
||||
|
||||
// (GET /admin/user-codes)
|
||||
GetAllUserCodes(ctx echo.Context) error
|
||||
|
||||
// (POST /admin/user-codes)
|
||||
AddUserCode(ctx echo.Context) error
|
||||
|
||||
// (GET /locks/{lock}/slots/{slot})
|
||||
GetLockCodeSlot(ctx echo.Context, lock int, slot int) error
|
||||
|
||||
|
@ -45,6 +66,24 @@ type ServerInterfaceWrapper struct {
|
|||
Handler ServerInterface
|
||||
}
|
||||
|
||||
// GetAllUserCodes converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetAllUserCodes(ctx echo.Context) error {
|
||||
var err error
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.GetAllUserCodes(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddUserCode converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) AddUserCode(ctx echo.Context) error {
|
||||
var err error
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.AddUserCode(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetLockCodeSlot converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetLockCodeSlot(ctx echo.Context) error {
|
||||
var err error
|
||||
|
@ -121,11 +160,46 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
|||
Handler: si,
|
||||
}
|
||||
|
||||
router.GET(baseURL+"/admin/user-codes", wrapper.GetAllUserCodes)
|
||||
router.POST(baseURL+"/admin/user-codes", wrapper.AddUserCode)
|
||||
router.GET(baseURL+"/locks/:lock/slots/:slot", wrapper.GetLockCodeSlot)
|
||||
router.PUT(baseURL+"/locks/:lock/slots/:slot", wrapper.PutLockCodeSlot)
|
||||
|
||||
}
|
||||
|
||||
type GetAllUserCodesRequestObject struct {
|
||||
}
|
||||
|
||||
type GetAllUserCodesResponseObject interface {
|
||||
VisitGetAllUserCodesResponse(w http.ResponseWriter) error
|
||||
}
|
||||
|
||||
type GetAllUserCodes200JSONResponse []UserCode
|
||||
|
||||
func (response GetAllUserCodes200JSONResponse) VisitGetAllUserCodesResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(200)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type AddUserCodeRequestObject struct {
|
||||
Body *AddUserCodeJSONRequestBody
|
||||
}
|
||||
|
||||
type AddUserCodeResponseObject interface {
|
||||
VisitAddUserCodeResponse(w http.ResponseWriter) error
|
||||
}
|
||||
|
||||
type AddUserCode201JSONResponse UserCode
|
||||
|
||||
func (response AddUserCode201JSONResponse) VisitAddUserCodeResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(201)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type GetLockCodeSlotRequestObject struct {
|
||||
Lock int `json:"lock"`
|
||||
Slot int `json:"slot"`
|
||||
|
@ -157,6 +231,12 @@ type PutLockCodeSlotResponseObject interface {
|
|||
// StrictServerInterface represents all server handlers.
|
||||
type StrictServerInterface interface {
|
||||
|
||||
// (GET /admin/user-codes)
|
||||
GetAllUserCodes(ctx context.Context, request GetAllUserCodesRequestObject) (GetAllUserCodesResponseObject, error)
|
||||
|
||||
// (POST /admin/user-codes)
|
||||
AddUserCode(ctx context.Context, request AddUserCodeRequestObject) (AddUserCodeResponseObject, error)
|
||||
|
||||
// (GET /locks/{lock}/slots/{slot})
|
||||
GetLockCodeSlot(ctx context.Context, request GetLockCodeSlotRequestObject) (GetLockCodeSlotResponseObject, error)
|
||||
|
||||
|
@ -176,6 +256,58 @@ type strictHandler struct {
|
|||
middlewares []StrictMiddlewareFunc
|
||||
}
|
||||
|
||||
// GetAllUserCodes operation middleware
|
||||
func (sh *strictHandler) GetAllUserCodes(ctx echo.Context) error {
|
||||
var request GetAllUserCodesRequestObject
|
||||
|
||||
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
|
||||
return sh.ssi.GetAllUserCodes(ctx.Request().Context(), request.(GetAllUserCodesRequestObject))
|
||||
}
|
||||
for _, middleware := range sh.middlewares {
|
||||
handler = middleware(handler, "GetAllUserCodes")
|
||||
}
|
||||
|
||||
response, err := handler(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if validResponse, ok := response.(GetAllUserCodesResponseObject); ok {
|
||||
return validResponse.VisitGetAllUserCodesResponse(ctx.Response())
|
||||
} else if response != nil {
|
||||
return fmt.Errorf("unexpected response type: %T", response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddUserCode operation middleware
|
||||
func (sh *strictHandler) AddUserCode(ctx echo.Context) error {
|
||||
var request AddUserCodeRequestObject
|
||||
|
||||
var body AddUserCodeJSONRequestBody
|
||||
if err := ctx.Bind(&body); err != nil {
|
||||
return err
|
||||
}
|
||||
request.Body = &body
|
||||
|
||||
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
|
||||
return sh.ssi.AddUserCode(ctx.Request().Context(), request.(AddUserCodeRequestObject))
|
||||
}
|
||||
for _, middleware := range sh.middlewares {
|
||||
handler = middleware(handler, "AddUserCode")
|
||||
}
|
||||
|
||||
response, err := handler(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if validResponse, ok := response.(AddUserCodeResponseObject); ok {
|
||||
return validResponse.VisitAddUserCodeResponse(ctx.Response())
|
||||
} else if response != nil {
|
||||
return fmt.Errorf("unexpected response type: %T", response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLockCodeSlot operation middleware
|
||||
func (sh *strictHandler) GetLockCodeSlot(ctx echo.Context, lock int, slot int) error {
|
||||
var request GetLockCodeSlotRequestObject
|
||||
|
@ -237,14 +369,16 @@ func (sh *strictHandler) PutLockCodeSlot(ctx echo.Context, lock int, slot int) e
|
|||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"H4sIAAAAAAAC/+SSsY7bMAyGX8VgOzqW296k7S4FigM6FLipONygyEyiqyzqJDpFYOjdCyo+JAGydOnS",
|
||||
"SbZI/qS+nzNYGiMFDJxBz5DtHkdTP7+T/bWmAZ88sfzHRBETO6xRSwPKyceIoCFzcmEHpQUMZuNxuIht",
|
||||
"iDyaAKW0kPBtcknCzyeJc8FL+15Am1e0DEUKXNiSaHlnMeTaMphRstbr1cPP1dP96q7roYUpedCwZ45Z",
|
||||
"K2UTGnYHtDSOFHJHaacWiaw2R3XX9UqmZcdexOSxGdMBE7RwwJQdBdDQd5+6XvIoYjDRgYYv9aqFaHhf",
|
||||
"SSgvtWqWo6jsibOa5SgS3WGFJ+gMOwqPA2j4hnxFV9SSGZExZdDPMwyYbXKRT1M8fm1o24g+CBDQtTm0",
|
||||
"7ySWyJktpwnbxcsLI1xg3GESI263EEeafJroRp8l8hd9XiQ7RxLskvC570/LExhDBWNi9M5WNOo1yyzz",
|
||||
"heDHhFvQ8EGdt1QtK6quCNZduX7SfSV28aaaE6cbfvyY/ic/3ibM/EDD8Z9ZwXtsAv5uDsZP2GwpNXJz",
|
||||
"c/xytTFh8r6UUv4EAAD///FFWy6nBAAA",
|
||||
"H4sIAAAAAAAC/+RVO2/bMBD+K8S1o2w6bSZtjgsUAToUCDoUQQZaOttMKZI9npwahv57cZQcx4mQB9pO",
|
||||
"nSiRvMf3OGkPVWhi8Og5QbmHVG2wMfnxS6h+LEKNVy6wvEcKEYkt5tMq1Cgr7yJCCYnJ+jV0BaA3S4f1",
|
||||
"g7NlCA6Nh64rgPBna0mOr/sUx4Cb4hAQlrdYsST7lpAWQ6VX16/zhRpTRTayDR5KuNugV7xBJXEKf0VL",
|
||||
"mKB4Gp7YEL+YYIlVaDApU7Hd4lieNiGN9Nc9ASlb1q+CXHa2Qp8yLm8aubVYTC6+T67mk/PpDApoyUEJ",
|
||||
"G+aYSq0rQiP1q9A0wadpoLUeUiS93Onz6UxLL2zZSTJRNCFtkaCALVLqoc2mZ9OZ3AsRvYkWSviYtwqI",
|
||||
"hjeZDG3qxnotqCbCQN5cYzaGyGKEpssaSviMPHfuIJxwTJhikJ7k8ofZrJfPM/ocbmJ0tsoJ9G2Sjg42",
|
||||
"lCfL2OTA94QrKOGdPhpWD27V9zY50muIzK5n91RI45yqWiL0rIyv1arlllAJMtUjy0ExpBFw87q+r9Wb",
|
||||
"GRNfhHr3Jkyvg9L183LC3dk/qfOYonsu1J1JKrsM66Eh7cREei9Lp5MLnPRelu45R5x8S8RWZBpkpATl",
|
||||
"9eNJu/ykwkpJfpDJgDK7EIrDSAwnxy8JU4vFA9iDA6xnXCPJzI2XyAhT39FIneHkDXVu/tDrz+l1wuCI",
|
||||
"ZvPM2ANMvYvbET2+tv+THn9/RF+SQv4SHu/U1rgW1SpQ/m+Mtn864b51TqbsdwAAAP//L1q9IJUHAAA=",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
|
|
|
@ -51,6 +51,32 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LockCodeSlot'
|
||||
/admin/user-codes:
|
||||
get:
|
||||
operationId: get_all_user_codes
|
||||
responses:
|
||||
'200':
|
||||
description: all current and future user codes
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/UserCode'
|
||||
post:
|
||||
operationId: add_user_code
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserCode'
|
||||
responses:
|
||||
'201':
|
||||
description: user code was created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserCode'
|
||||
components:
|
||||
schemas:
|
||||
LockCodeSlot:
|
||||
|
@ -63,3 +89,16 @@ components:
|
|||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
UserCode:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
user:
|
||||
type: string
|
||||
starts:
|
||||
description: when the code becomes active
|
||||
type: string
|
||||
ends:
|
||||
description: when the code expires
|
||||
type: string
|
||||
|
|
|
@ -6,13 +6,17 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/db"
|
||||
"github.com/failsafe-go/failsafe-go"
|
||||
"github.com/failsafe-go/failsafe-go/retrypolicy"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"git.janky.solutions/finn/lockserver/db"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
|
@ -32,22 +36,32 @@ func New(server string) (*Client, error) {
|
|||
}
|
||||
|
||||
func (c *Client) DialAndListen(ctx context.Context) {
|
||||
// Retry on ErrConnecting up to 3 times with a 1 second delay between attempts
|
||||
connectRetryPolicy := retrypolicy.Builder[*websocket.Conn]().
|
||||
WithBackoff(time.Second, time.Minute).
|
||||
Build()
|
||||
|
||||
for {
|
||||
logrus.WithField("server", c.Server).Info("connecting to zwave-js server")
|
||||
conn, _, err := websocket.DefaultDialer.DialContext(ctx, c.Server, nil)
|
||||
conn, err := failsafe.Get(func() (*websocket.Conn, error) {
|
||||
conn, _, err := websocket.DefaultDialer.DialContext(ctx, c.Server, nil)
|
||||
return conn, err
|
||||
}, connectRetryPolicy)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("error connecting to zwavejs server")
|
||||
time.Sleep(time.Second * 10)
|
||||
continue
|
||||
logrus.WithError(err).Fatal("error connecting to zwavejs server")
|
||||
}
|
||||
c.conn = conn
|
||||
logrus.Info("connected to zwave-js server")
|
||||
|
||||
if err := c.listen(ctx); err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithError(err).Error("error communicating with zwavejs server")
|
||||
time.Sleep(time.Second * 10)
|
||||
continue
|
||||
}
|
||||
_ = c.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue