rip out all auth stuff, add a containerfile
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.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() {
|
func run() {
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
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)
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,70 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ZWaveJSServer string
|
ZWaveJSServer string `json:"zwave-js-server"`
|
||||||
Database string
|
SqliteDatabase string `json:"sqlite-database"`
|
||||||
HTTPBind string
|
HTTPBind string `json:"http-bind"`
|
||||||
|
SessionSecrets []JSONBytes `json:"session-secrets"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var C = Config{
|
var C = Config{
|
||||||
ZWaveJSServer: "ws://home-assistant:3000",
|
ZWaveJSServer: "ws://home-assistant:3000",
|
||||||
Database: "lockserver.db",
|
SqliteDatabase: "lockserver.db",
|
||||||
HTTPBind: ":8080",
|
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
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"embed"
|
"embed"
|
||||||
|
|
||||||
|
@ -12,7 +13,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Get() (*Queries, *sql.DB, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -24,7 +25,7 @@ func Get() (*Queries, *sql.DB, error) {
|
||||||
var migrations embed.FS
|
var migrations embed.FS
|
||||||
|
|
||||||
func Migrate() error {
|
func Migrate() error {
|
||||||
logrus.WithField("dbfile", config.C.Database).Info("migrating database")
|
logrus.WithField("dbfile", config.C.SqliteDatabase).Info("migrating database")
|
||||||
|
|
||||||
_, conn, err := Get()
|
_, conn, err := Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -51,3 +52,36 @@ func NullString(s string) sql.NullString {
|
||||||
String: s,
|
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,
|
state TEXT NOT NULL,
|
||||||
code INTEGER REFERENCES lock_code_slots(id)
|
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 StatementEnd
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
-- +goose StatementBegin
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE user_code_slots;
|
||||||
|
DROP TABLE user_codes;
|
||||||
|
DROP TABLE users;
|
||||||
DROP TABLE lock_log;
|
DROP TABLE lock_log;
|
||||||
DROP TABLE lock_code_slots;
|
DROP TABLE lock_code_slots;
|
||||||
DROP TABLE locks;
|
DROP TABLE locks;
|
||||||
|
|
20
db/models.go
20
db/models.go
|
@ -30,3 +30,23 @@ type LockLog struct {
|
||||||
State string
|
State string
|
||||||
Code sql.NullInt64
|
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
|
go 1.21.8
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/failsafe-go/failsafe-go v0.6.2
|
||||||
github.com/getkin/kin-openapi v0.124.0
|
github.com/getkin/kin-openapi v0.124.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/gorilla/securecookie v1.1.2
|
||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.1
|
||||||
github.com/labstack/echo/v4 v4.11.4
|
github.com/labstack/echo/v4 v4.11.4
|
||||||
github.com/mattn/go-sqlite3 v1.14.22
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
|
@ -32,7 +34,7 @@ require (
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/crypto v0.18.0 // indirect
|
golang.org/x/crypto v0.18.0 // indirect
|
||||||
golang.org/x/net v0.20.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/sys v0.16.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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-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 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
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 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M=
|
||||||
github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
|
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=
|
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-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 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
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 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
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/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.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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
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 h1:1MvEhzI5pvP27e9Dzz861mxk9WzXZLSJwzOU67cKTbU=
|
||||||
github.com/tursodatabase/libsql-client-go v0.0.0-20240220085343-4ae0eb9d0898/go.mod h1:9bKuHS7eZh/0mJndbUOrCx8Ej3PlsRDszj4L7oVYMPQ=
|
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=
|
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/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 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
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-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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/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"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
echo "github.com/labstack/echo/v4"
|
echo "github.com/labstack/echo/v4"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
@ -25,9 +26,13 @@ func ListenAndServe(client *zwavejs.Client) {
|
||||||
server.HideBanner = true
|
server.HideBanner = true
|
||||||
server.HidePort = true
|
server.HidePort = true
|
||||||
server.HTTPErrorHandler = handleError
|
server.HTTPErrorHandler = handleError
|
||||||
|
server.Use(accessLogMiddleware)
|
||||||
|
|
||||||
|
browserEndpoints{}.Register(server)
|
||||||
|
|
||||||
openapi.RegisterHandlersWithBaseURL(server, lockserver{ZWaveJS: client}, "/api")
|
openapi.RegisterHandlersWithBaseURL(server, lockserver{ZWaveJS: client}, "/api")
|
||||||
|
|
||||||
|
logrus.WithField("address", config.C.HTTPBind).Info("starting http server")
|
||||||
err := server.Start(config.C.HTTPBind)
|
err := server.Start(config.C.HTTPBind)
|
||||||
if err != http.ErrServerClosed {
|
if err != http.ErrServerClosed {
|
||||||
logrus.WithError(err).Fatal("error starting http server")
|
logrus.WithError(err).Fatal("error starting http server")
|
||||||
|
@ -40,7 +45,11 @@ func handleError(err error, c echo.Context) {
|
||||||
return
|
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"})
|
_ = 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)
|
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"`
|
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.
|
// PutLockCodeSlotJSONRequestBody defines body for PutLockCodeSlot for application/json ContentType.
|
||||||
type PutLockCodeSlotJSONRequestBody = LockCodeSlot
|
type PutLockCodeSlotJSONRequestBody = LockCodeSlot
|
||||||
|
|
||||||
// ServerInterface represents all server handlers.
|
// ServerInterface represents all server handlers.
|
||||||
type ServerInterface interface {
|
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})
|
// (GET /locks/{lock}/slots/{slot})
|
||||||
GetLockCodeSlot(ctx echo.Context, lock int, slot int) error
|
GetLockCodeSlot(ctx echo.Context, lock int, slot int) error
|
||||||
|
|
||||||
|
@ -45,6 +66,24 @@ type ServerInterfaceWrapper struct {
|
||||||
Handler ServerInterface
|
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.
|
// GetLockCodeSlot converts echo context to params.
|
||||||
func (w *ServerInterfaceWrapper) GetLockCodeSlot(ctx echo.Context) error {
|
func (w *ServerInterfaceWrapper) GetLockCodeSlot(ctx echo.Context) error {
|
||||||
var err error
|
var err error
|
||||||
|
@ -121,11 +160,46 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||||
Handler: si,
|
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.GET(baseURL+"/locks/:lock/slots/:slot", wrapper.GetLockCodeSlot)
|
||||||
router.PUT(baseURL+"/locks/:lock/slots/:slot", wrapper.PutLockCodeSlot)
|
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 {
|
type GetLockCodeSlotRequestObject struct {
|
||||||
Lock int `json:"lock"`
|
Lock int `json:"lock"`
|
||||||
Slot int `json:"slot"`
|
Slot int `json:"slot"`
|
||||||
|
@ -157,6 +231,12 @@ type PutLockCodeSlotResponseObject interface {
|
||||||
// StrictServerInterface represents all server handlers.
|
// StrictServerInterface represents all server handlers.
|
||||||
type StrictServerInterface interface {
|
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})
|
// (GET /locks/{lock}/slots/{slot})
|
||||||
GetLockCodeSlot(ctx context.Context, request GetLockCodeSlotRequestObject) (GetLockCodeSlotResponseObject, error)
|
GetLockCodeSlot(ctx context.Context, request GetLockCodeSlotRequestObject) (GetLockCodeSlotResponseObject, error)
|
||||||
|
|
||||||
|
@ -176,6 +256,58 @@ type strictHandler struct {
|
||||||
middlewares []StrictMiddlewareFunc
|
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
|
// GetLockCodeSlot operation middleware
|
||||||
func (sh *strictHandler) GetLockCodeSlot(ctx echo.Context, lock int, slot int) error {
|
func (sh *strictHandler) GetLockCodeSlot(ctx echo.Context, lock int, slot int) error {
|
||||||
var request GetLockCodeSlotRequestObject
|
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
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
var swaggerSpec = []string{
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
"H4sIAAAAAAAC/+SSsY7bMAyGX8VgOzqW296k7S4FigM6FLipONygyEyiqyzqJDpFYOjdCyo+JAGydOnS",
|
"H4sIAAAAAAAC/+RVO2/bMBD+K8S1o2w6bSZtjgsUAToUCDoUQQZaOttMKZI9npwahv57cZQcx4mQB9pO",
|
||||||
"SbZI/qS+nzNYGiMFDJxBz5DtHkdTP7+T/bWmAZ88sfzHRBETO6xRSwPKyceIoCFzcmEHpQUMZuNxuIht",
|
"nSiRvMf3OGkPVWhi8Og5QbmHVG2wMfnxS6h+LEKNVy6wvEcKEYkt5tMq1Cgr7yJCCYnJ+jV0BaA3S4f1",
|
||||||
"iDyaAKW0kPBtcknCzyeJc8FL+15Am1e0DEUKXNiSaHlnMeTaMphRstbr1cPP1dP96q7roYUpedCwZ45Z",
|
"g7NlCA6Nh64rgPBna0mOr/sUx4Cb4hAQlrdYsST7lpAWQ6VX16/zhRpTRTayDR5KuNugV7xBJXEKf0VL",
|
||||||
"K2UTGnYHtDSOFHJHaacWiaw2R3XX9UqmZcdexOSxGdMBE7RwwJQdBdDQd5+6XvIoYjDRgYYv9aqFaHhf",
|
"mKB4Gp7YEL+YYIlVaDApU7Hd4lieNiGN9Nc9ASlb1q+CXHa2Qp8yLm8aubVYTC6+T67mk/PpDApoyUEJ",
|
||||||
"SSgvtWqWo6jsibOa5SgS3WGFJ+gMOwqPA2j4hnxFV9SSGZExZdDPMwyYbXKRT1M8fm1o24g+CBDQtTm0",
|
"G+aYSq0rQiP1q9A0wadpoLUeUiS93Onz6UxLL2zZSTJRNCFtkaCALVLqoc2mZ9OZ3AsRvYkWSviYtwqI",
|
||||||
"7ySWyJktpwnbxcsLI1xg3GESI263EEeafJroRp8l8hd9XiQ7RxLskvC570/LExhDBWNi9M5WNOo1yyzz",
|
"hjeZDG3qxnotqCbCQN5cYzaGyGKEpssaSviMPHfuIJxwTJhikJ7k8ofZrJfPM/ocbmJ0tsoJ9G2Sjg42",
|
||||||
"heDHhFvQ8EGdt1QtK6quCNZduX7SfSV28aaaE6cbfvyY/ic/3ibM/EDD8Z9ZwXtsAv5uDsZP2GwpNXJz",
|
"lCfL2OTA94QrKOGdPhpWD27V9zY50muIzK5n91RI45yqWiL0rIyv1arlllAJMtUjy0ExpBFw87q+r9Wb",
|
||||||
"c/xytTFh8r6UUv4EAAD///FFWy6nBAAA",
|
"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
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|
|
@ -51,6 +51,32 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/LockCodeSlot'
|
$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:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
LockCodeSlot:
|
LockCodeSlot:
|
||||||
|
@ -63,3 +89,16 @@ components:
|
||||||
type: string
|
type: string
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
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"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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/google/uuid"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"git.janky.solutions/finn/lockserver/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
|
@ -32,22 +36,32 @@ func New(server string) (*Client, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) DialAndListen(ctx context.Context) {
|
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 {
|
for {
|
||||||
logrus.WithField("server", c.Server).Info("connecting to zwave-js server")
|
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 {
|
if err != nil {
|
||||||
logrus.WithError(err).Error("error connecting to zwavejs server")
|
logrus.WithError(err).Fatal("error connecting to zwavejs server")
|
||||||
time.Sleep(time.Second * 10)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
c.conn = conn
|
c.conn = conn
|
||||||
logrus.Info("connected to zwave-js server")
|
logrus.Info("connected to zwave-js server")
|
||||||
|
|
||||||
if err := c.listen(ctx); err != nil {
|
if err := c.listen(ctx); err != nil {
|
||||||
|
if errors.Is(err, net.ErrClosed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
logrus.WithError(err).Error("error communicating with zwavejs server")
|
logrus.WithError(err).Error("error communicating with zwavejs server")
|
||||||
time.Sleep(time.Second * 10)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
_ = c.conn.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue